2019, May 05 - Dimitri Merejkowsky License: CC By 4.0
Note : cet article reprend en grande partie le cours donné à l'École du Logiciel Libre[1] le 4 mai 2019.
Quelques rappels pour commencer.
Soit le code suivant :
import foo foo.bar()
Ce code fonctionne s'il y a un ficher `foo.py` quelque part qui contient la fonction `bar`
Ce fichier peut être présent soit dans le répertoire courant, soit dans la bibliothèque standard Python.
Vous connaissez peut-être le rôle de la variable d'environnement `PATH`. Celle-ci contient une liste de chemins, et est utilisée par votre shell pour trouver le chemin complet des commandes que vous lancez.
Par exemple:
PATH="/bin:/usr/bin:/usr/sbin" $ ifconfig # lance le binaire /usr/sbin/ifconfig $ ls # lance le binaire /bin/ls
Le chemin est "résolu" par le shell en parcourant la liste de tout les segments de `PATH`, et en regardant si le chemin complet existe. La résolution s'arrête dès le premier chemin trouvé.
Par exemple, si vous avez `PATH="/home/user/bin:/usr/bin"` et un fichier `ls` dans `/home/user/bin/ls`, c'est ce fichier-là (et non `/bin/ls`) qui sera utilisé quand vous taperez `ls`.
En Python, il existe une variable `path` prédéfinie dans le module `sys` qui fonctionne de manière similaire.
Si j'essaye de l'afficher sur mon Arch Linux, voici ce que j'obtiens :
>>> import sys >>> sys.path [ "", "/usr/lib/python3.7", "/usr/lib/python3.7/lib-dynload", "/home/dmerej/.local/lib/python3.7/", "/usr/lib/python3.7/site-packages", ]
Notez que le résultat dépend de ma distribution, et de la présence ou non du répertoire `~/.local/lib/python3.7/` sur ma machine - cela prouve que `sys.path` est construit dynamiquement par l'interpréteur Python.
Notez également que `sys.path` commence par une chaîne vide. En pratique, cela signifie que le répertoire courant a la priorité sur tout le reste.
Ainsi, si vous avez un fichier `random.py` dans votre répertoire courant, et que vous lancez un script `foo.py` dans ce même répertoire, vous vous retrouvez à utiliser le code dans `random.py`, et non celui de la bibliothèque standard ! Pour information, la liste de tous les modules de la bibliothèque standard est présente dans la documentation[2].
2: https://docs.python.org/fr/3/library/index.html
Un autre aspect notable de `sys.path` est qu'il ne contient que deux répertoires dans lesquels l'utilisateur courant peut potentiellement écrire : le chemin courant et le chemin dans `~/.local/lib`. Tous les autres (`/usr/lib/python3.7/`, etc.) sont des chemins "système" et ne peuvent être modifiés que par un compte administrateur (avec `root` ou `sudo`, donc).
La situation est semblable sur macOS et Windows .
Prenons un exemple :
# dans foo.py import tabulate scores = [ ["John", 345], ["Mary-Jane", 2], ["Bob", 543], ] table = tabulate.tabulate(scores) print(table)
$ python3 foo.py --------- --- John 345 Mary-Jane 2 Bob 543 --------- ---
Ici, le module `tabulate` n'est ni dans la bibliothèque standard, ni écrit par l'auteur du script `foo.py`. On dit que c'est une bibliothèque tierce.
On peut trouver le code source de tabulate[3] facilement. La question qui se pose alors est: comment faire en sorte que `sys.path` contienne le module `tabulate`?
3: https://bitbucket.org/astanin/python-tabulate/src/master/
Eh bien, plusieurs solutions s'offrent à vous.
Si vous utilisez une distribution Linux, peut-être pourrez-vous utiliser votre gestionnaire de paquets :
$ sudo apt install python3-tabulate
Comme vous lancez votre gestionnaire de paquets avec `sudo`, celui-ci sera capable d'écrire dans les chemins système de `sys.path`.
Une autre méthode consiste à partir des sources - par exemple, si le paquet de votre distribution n'est pas assez récent, ou si vous avez besoin de modifier le code de la bibliothèque en question.
Voici une marche à suivre possible :
1. Récupérer les sources de la version qui vous intéresse dans la section téléchargement de bitbucket[4].
2. Extraire l'archive, par exemple dans `src/tabulate`
3. Se rendre dans `src/tabulate` et lancer `python3 setup.py install --user`
4: https://bitbucket.org/astanin/python-tabulate/downloads/?tab=tags
La plupart des bibliothèques Python contiennent un `setup.py` à la racine de leurs sources. Il sert à plein de choses, la commande `install` n'étant qu'une parmi d'autres.
Le fichier `setup.py` contient en général simplement un `import` de `setuptools`, et un appel à la fonction `setup()`, avec de nombreux arguments :
# tabulate/setup.py from setuptools import setup setup( name='tabulate', version='0.8.1', description='Pretty-print tabular data', py_modules=["tabulate"], scripts=["bin/tabulate"], ... )
Par défaut, `setup.py` essaiera d'écrire dans un des chemins système de `sys.path` , d'où l'utilisation de l'option `--user`.
Voici à quoi ressemble la sortie de la commande :
$ cd src/tabulate $ python3 setup.py install --user running install ... Copying tabulate-0.8.4-py3.7.egg to /home/dmerej/.local/lib/python3.7/site-packages ... Installing tabulate script to /home/dmerej/.local/bin
Notez que module a été copié dans `~/.local/lib/python3.7/site-packages/` et le script dans `~/.local/bin`. Cela signifie que *tous* les scripts Python lancés par l'utilisateur courant auront accès au module `tabulate`.
Notez également qu'un script a été installé dans `~/.local/bin` - Une bibliothèque Python peut contenir aussi bien des modules que des scripts.
Un point important est que vous n'avez en général pas besoin de lancer le script directement. Vous pouvez utiliser `python3 -m tabulate`. Procéder de cette façon est intéressant puisque vous n'avez pas à vous soucier de rajouter le chemin d'installation des scripts dans la variable d'environnement PATH.
Prenons une autre bibliothèque : `cli-ui`.
Elle permet d'afficher du texte en couleur dans un terminal
import cli_ui cli_ui.info("Ceci est en", cli_ui.red, "rouge")
Elle permet également d'afficher des tableaux en couleur :
headers=["name", "score"] data = [ [(bold, "John"), (green, 10.0)], [(bold, "Jane"), (green, 5.0)], ] cli_ui.info_table(data, headers=headers)
Pour ce faire, elle repose sur la bibliothèque `tabulate` vue précédemment. On dit que `cli-ui` *dépend* de `tabulate`.
La déclaration de la dépendance de `cli-ui` vers `tabulate` s'effectue également dans le fichier `setup.py`:
setup( name="cli-ui", version="0.9.1", install_requires=[ "tabulate", ... ], ... )
On comprend dès lors qu'il doit nécessairement exister un *annuaire* permettant de relier les noms de dépendances à leur code source.
Cet annuaire, c'est le site pypi.org[5]. Vous y trouverez les pages correspondant à tabulate[6] et cli-ui[7].
6: https://pypi.org/project/tabulate/
7: https://pypi.org/project/python-cli-ui/
`pip` est un outil qui vient par défaut avec Python3. Vous pouvez également l'installer grâce au script get-pip.py[8], en lançant `python3 get-pip.py --user`.
8: https://bootstrap.pypa.io/get-pip.py
Il est conseillé de *toujours* lancer `pip` avec `python3 -m pip`. De cette façon, vous êtes certains d'utiliser le module `pip` correspondant à votre binaire `python3`, et vous ne dépendez pas de ce qu'il y a dans votre `PATH`.
`pip` est capable d'interroger le site `pypi.org` pour retrouver les dépendances, et également de lancer les différents scripts `setup.py`.
Comme de nombreux outils, il s'utilise à l'aide de *commandes*. Voici comment installer `cli-ui` à l'aide de la commande 'install' de `pip`:
$ python3 -m pip install cli-ui --user Collecting cli-ui ... Requirement already satisfied: unidecode in /usr/lib/python3.7/site-packages (from cli-ui) (1.0.23) Requirement already satisfied: colorama in /usr/lib/python3.7/site-packages (from cli-ui) (0.4.1) Requirement already satisfied: tabulate in /mnt/data/dmerej/src/python-tabulate (from cli-ui) (0.8.4) Installing collected packages: cli-ui Successfully installed cli-ui-0.9.1
On constate ici quelques limitations de `pip`:
En revanche, `pip` contient de nombreuses fonctionnalités intéressantes:
Voici un extrait de la commande `python3 -m pip freeze` au moment de la rédaction de cet article sur ma machine:
$ python3 -m pip freeze apipkg==1.5 cli-ui==0.9.1 gaupol==1.5 tabulate==0.8.4
On y retrouve les bibliothèques `cli-ui` et `tabulate`, bien sûr, mais aussi la bibliothèque `gaupol`, qui correspond au programme d'édition de sous-titres[9] que j'ai installé à l'aide du gestionnaire de paquets de ma distribution. Précisons que les modules de la bibliothèque standard et ceux utilisés directement par pip sont omis de la liste.
9: https://otsaloma.io/gaupol/
On constate également que chaque bibliothèque possède un *numéro de version*.
Les numéros de version remplissent plusieurs rôles, mais l'un des principaux est de spécifier des changements incompatibles.
Par exemple, pour `cli-ui`, la façon d'appeler la fonction `ask_choice` a changé entre les versions 0.7 et 0.8, comme le montre le changelog[10]:
10: https://dmerejkowsky.github.io/python-cli-ui/changelog.html
the list of choices used by ask_choice is now a named keyword argument:
# Old (<= 0.7) ask_choice("select a fruit", ["apple", "banana"]) # New (>= 0.8) ask_choice("select a fruit", choices=["apple", "banana"])
Ceci s'appelle un *changement d'API*.
Plusieurs possibilités:
setup( install_requires=[ "cli-ui < 0.8", ... ] )
Souvenez-vous que les fichiers systèmes sont contrôlés par votre gestionnaire de paquets.
Les mainteneurs de votre distribution font en sorte qu'ils fonctionnent bien les uns avec les autres. Par exemple, le paquet `python3-cli-ui` ne sera mis à jour que lorsque tous les paquets qui en dépendent seront prêts à utiliser la nouvelle API.
En revanche, si vous lancez `sudo pip` (où `pip` avec un compte root), vous allez écrire dans ces mêmes répertoire et vous risquez de "casser" certains programmes de votre système.
Mais il y a un autre problème encore pire.
Supposons deux projets A et B dans votre répertoire personnel. Ils dépendent tous les deux de `cli-ui`, mais l'un des deux utilise `cli-ui 0.7` et l'autre `cli-ui 0.9`. Que faire ?
La solution est d'utiliser un environnement virtuel (*virtualenv* en abrégé). C'est un répertoire *isolé* du reste du système.
Pour créer un virtualenv il faut utiliser la commande:
$ python -m venv /chemin/vers/virtualenv
où `/chemin/vers/virtualenv` est le dossier cible. Les dossiers parents seront créés si nécessaire par le module `venv`.
En pratique, on préfère utiliser un chemin qui n'existe pas encore, typiquement :
$ cd /chemin/vers/project $ python -m .venv
Ici on a utilisé le répertoire relatif `.venv`.
La commande `python3 -m venv` fonctionne en général partout, dès l'installation de Python3 (*out of the box*, en Anglais), *sauf* sur Debian et ses dérivées .
Si vous utilisez Debian, la commande pourrait ne pas fonctionner. En fonction des messages d'erreur que vous obtenez, il est possible de résoudre le problème en :
Ce répertoire contient de nombreux fichiers et dossiers, et notamment un binaire dans `foo-venv/bin/python3`.
Voyons comment il se comporte en le comparant au binaire `/usr/bin/python3` habituel :
$ /usr/bin/python3 -c 'import sys; print(sys.path)' ['', ... '/usr/lib/python3.7', '/usr/lib/python3.7.zip', '/usr/lib/python3.7/lib-dynload', '/home/dmerej/.local/lib/python3.7/site-packages', '/usr/lib/python3.7/site-packages' ] $ /home/dmerej/foo-venv/bin/python -c 'import sys; print(sys.path)' ['', '/usr/lib/python3.7', '/usr/lib/python3.7.zip', '/usr/lib/python3.7/lib-dynload', '/home/dmerej/foo-venv/lib/python3.7/site-packages, ]
À noter:
Ainsi, l'isolation du virtualenv est reflété dans la différence de la valeur de `sys.path`.
Il faut aussi préciser que le virtualenv n'est pas complètement isolé du reste du système. En particulier, il dépend encore du binaire Python utilisé pour le créer.
Par exemple, si vous utilisez `/usr/local/bin/python3.7 -m venv foo-37`, le virtualenv dans `foo-37` utilisera Python 3.7 et fonctionnera tant que le binaire `/usr/local/bin/python3.7` existe.
Cela signifie également qu'il est possible qu'en mettant à jour le paquet `python3` sur votre distribution, vous rendiez inutilisables les virtualenvs créés avec l'ancienne version du paquet.
D'après ce qui précède, le virtualenv ne devrait contenir aucun module en dehors de la bibliothèque standard et de `pip` lui-même.
On peut s'en assurer en lançant `python3 -m pip freeze` depuis le virtualenv et en vérifiant que rien ne s'affiche.
$ python3 -m pip freeze # de nombreuses bibliothèques en dehors du virtualenv apipkg==1.5 cli-ui==0.9.1 gaupol==1.5 tabulate==0.8.4 $ /home/dmerej/foo-venv/bin/python3 -m pip freeze # rien :)
On peut alors utiliser le module `pip` *du virtualenv* pour installer des bibliothèques dans celui-ci :
$ /home/dmerej/foo-venv/bin/python3 -m pip install cli-ui Collecting cli-ui Using cached https://pythonhosted.org/..cli_ui-0.9.1-py3-none-any.whl Collecting colorama (from cli-ui) Using cached https://pythonhosted.org/..colorama-0.4.1-py2.py3-none-any.whl Collecting unidecode (from cli-ui) Using cached https://pythonhosted.org/..Unidecode-1.0.23-py2.py3-none-any.whl Collecting tabulate (from cli-ui) Installing collected packages: colorama, unidecode, tabulate, cli-ui Successfully installed cli-ui-0.9.1 colorama-0.4.1 tabulate-0.8.3 unidecode-1.0.23
Cette fois, aucune bibliothèque n'est marquée comme déjà installée, et on récupère donc `cli-ui` et toutes ses dépendances.
On a enfin notre solution pour résoudre notre conflit de dépendances : on peut simplement créer un virtualenv par projet. Ceci nous permettra d'avoir effectivement deux versions différentes de `cli-ui`, isolées les unes des autres.
Devoir préciser le chemin du virtualenv en entier pour chaque commande peut devenir fastidieux ; heureusement, il est possible *d'activer* un virtualenv, en lançant une des commandes suivantes :
Une fois le virtualenv activé, taper `python`, `python3` ou `pip` utilisera les binaires correspondants dans le virtualenv automatiquement, et ce, tant que la session du shell sera ouverte.
Le script d'activation ne fait en réalité pas grand-chose à part modifier la variable `PATH` et rajouter le nom du virtualenv au début de l'invite de commandes :
# Avant user@host:~/src $ source foo-env/bin/activate # Après (foo-env) user@host:~/src $
Pour sortir du virtualenv, entrez la commande `deactivate`.
Le système de gestions des dépendances de Python peut paraître compliqué et bizarre, surtout venant d'autres langages.
Mon conseil est de toujours suivre ces deux règles :
Certes, cela peut paraître fastidieux, mais c'est une méthode qui vous évitera probablement de vous arracher les cheveux (croyez-en mon expérience).
Dans un futur article, nous approfondirons la question, en évoquant d'autres sujets comme `PYTHONPATH`, le fichier `requirements.txt` ou des outils comme `poetry` ou `pipenv`. À suivre.
----