You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

преди 4 години
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. +++
  2. title = "sys.path"
  3. weight = 2
  4. +++
  5. # sys.path
  6. En Python, il existe une variable `path` prédéfinie dans le module `sys` qui fonctionne de manière similaire
  7. à la variable d'environnement `PATH`.
  8. Si j'essaye de l'afficher sur ma machine, voici ce que j'obtiens :
  9. ```python
  10. import sys
  11. print(sys.path)
  12. ```
  13. ```
  14. [
  15. "",
  16. "/usr/lib/python3.8",
  17. "/usr/lib/python3.8/lib-dynload",
  18. "/home/dmerej/.local/lib/python3.8/",
  19. "/usr/lib/python3.8/site-packages",
  20. ]
  21. ```
  22. Le résultat dépend:
  23. * du système d'exploitation
  24. * de la façon dont Python a été installé
  25. * et de la présence ou non de certains réportoires.
  26. En fait, `sys.path` est construit dynamiquement par l'interpréteur Python au démarrage.
  27. 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.
  28. ## Priorité du répertoire courant
  29. Prenons un exemple. Si vous ouvrez un explorateur de fichiers dans le deuxième
  30. élément de la liste de `sys.path` (`/usr/lib/python3.8/` sur ma machine), vous trouverez
  31. un grand nombre de fichiers Python.
  32. Notamment, vous devriez trouver un fichier `random.py` dans ce répertoire.
  33. En fait, vous trouverez la plupart des modules de la bibliothèque standard dans
  34. ce répertoire.
  35. Maintenant, imaginons que vous avez un deuxième fichier `random.py` dans votre répertoire courant. Finalement, imaginez
  36. que vous lancez un fichier `foo.py` contentant `import random` dans ce même réportoire.
  37. Et bien, c'est le fichier `random.py` de votre répertoire qui sera utilisé, et non celui de la bibliothèque standard!
  38. ## Permissions des répertoires de sys.path
  39. 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.8/`, etc.) sont des chemins "système" et ne peuvent être modifiés que par un compte administrateur (avec `root` ou `sudo`, donc).
  40. La situation est semblable sur macOS et Windows [^2].
  41. ## Bibliothèques tierces
  42. Prenons un exemple :
  43. ```python
  44. # dans foo.py
  45. import tabulate
  46. scores = [
  47. ["John", 345],
  48. ["Mary-Jane", 2],
  49. ["Bob", 543],
  50. ]
  51. table = tabulate.tabulate(scores)
  52. print(table)
  53. ```
  54. ```
  55. $ python3 foo.py
  56. --------- ---
  57. John 345
  58. Mary-Jane 2
  59. Bob 543
  60. --------- ---
  61. ```
  62. 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.
  63. On peut trouver [le code source de tabulate](https://bitbucket.org/astanin/python-tabulate/src/master/) facilement. La question qui se pose alors est: comment faire en sorte que `sys.path` contienne le module `tabulate`?
  64. Eh bien, plusieurs solutions s'offrent à vous.
  65. # Le gestionnaire de paquets
  66. Si vous utilisez une distribution Linux, peut-être pourrez-vous utiliser votre gestionnaire de paquets :
  67. ```bash
  68. $ sudo apt install python3-tabulate
  69. ```
  70. Comme vous lancez votre gestionnaire de paquets avec `sudo`, celui-ci sera capable d'écrire dans les chemins système de `sys.path`.
  71. # À la main
  72. 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.
  73. Voici une marche à suivre possible :
  74. 1. Récupérer les sources de la version qui vous intéresse dans la [section téléchargement de bitbucket](https://bitbucket.org/astanin/python-tabulate/downloads/?tab=tags).
  75. 1. Extraire l'archive, par exemple dans `src/tabulate`
  76. 1. Se rendre dans `src/tabulate` et lancer `python3 setup.py install --user`
  77. # Anatomie du fichier setup.py
  78. La plupart des bibliothèques Python contiennent un `setup.py` à
  79. la racine de leurs sources. Il sert à plein de choses, la commande `install`
  80. n'étant qu'une parmi d'autres.
  81. Le fichier `setup.py` contient en général simplement un `import` de `setuptools`, et un appel à la fonction `setup()`, avec de nombreux arguments :
  82. ```python
  83. # tabulate/setup.py
  84. from setuptools import setup
  85. setup(
  86. name='tabulate',
  87. version='0.8.1',
  88. description='Pretty-print tabular data',
  89. py_modules=["tabulate"],
  90. scripts=["bin/tabulate"],
  91. ...
  92. )
  93. ```
  94. # Résultat de l'invocation de setup.py
  95. Par défaut, `setup.py` essaiera d'écrire dans un des chemins système de
  96. `sys.path` [^3], d'où l'utilisation de l'option `--user`.
  97. Voici à quoi ressemble la sortie de la commande :
  98. ```bash
  99. $ cd src/tabulate
  100. $ python3 setup.py install --user
  101. running install
  102. ...
  103. Copying tabulate-0.8.4-py3.7.egg to /home/dmerej/.local/lib/python3.7/site-packages
  104. ...
  105. Installing tabulate script to /home/dmerej/.local/bin
  106. ```
  107. 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`.
  108. Notez également qu'un script a été installé dans `~/.local/bin` - Une bibliothèque Python peut contenir aussi bien des modules que des scripts.
  109. 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.
  110. # Dépendances
  111. Prenons une autre bibliothèque : `cli-ui`.
  112. Elle permet d'afficher du texte en couleur dans un terminal
  113. ```python
  114. import cli_ui
  115. cli_ui.info("Ceci est en", cli_ui.red, "rouge")
  116. ```
  117. Elle permet également d'afficher des tableaux en couleur :
  118. ```python
  119. headers=["name", "score"]
  120. data = [
  121. [(bold, "John"), (green, 10.0)],
  122. [(bold, "Jane"), (green, 5.0)],
  123. ]
  124. cli_ui.info_table(data, headers=headers)
  125. ```
  126. Pour ce faire, elle repose sur la bibliothèque `tabulate` vue précédemment. On dit que `cli-ui` *dépend* de `tabulate`.
  127. # Déclaration des dépendances
  128. La déclaration de la dépendance de `cli-ui` vers `tabulate` s'effectue également dans le fichier `setup.py`:
  129. ```python
  130. setup(
  131. name="cli-ui",
  132. version="0.9.1",
  133. install_requires=[
  134. "tabulate",
  135. ...
  136. ],
  137. ...
  138. )
  139. ```
  140. # pypi.org
  141. On comprend dès lors qu'il doit nécessairement exister un *annuaire* permettant de relier les noms de dépendances à leur code source.
  142. Cet annuaire, c'est le site [pypi.org](https://pypi.org/). Vous y trouverez les pages correspondant à [tabulate](https://pypi.org/project/tabulate/) et [cli-ui](https://pypi.org/project/python-cli-ui/).
  143. # pip
  144. `pip` est un outil qui vient par défaut avec Python3[^4]. Vous pouvez également l'installer grâce au script [get-pip.py](https://bootstrap.pypa.io/get-pip.py), en lançant `python3 get-pip.py --user`.
  145. 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`.
  146. `pip` est capable d'interroger le site `pypi.org` pour retrouver les dépendances, et également de lancer les différents scripts `setup.py`.
  147. Comme de nombreux outils, il s'utilise à l'aide de *commandes*. Voici comment installer `cli-ui` à l'aide de la commande 'install' de `pip`:
  148. ```bash
  149. $ python3 -m pip install cli-ui --user
  150. Collecting cli-ui
  151. ...
  152. Requirement already satisfied: unidecode in /usr/lib/python3.7/site-packages (from cli-ui) (1.0.23)
  153. Requirement already satisfied: colorama in /usr/lib/python3.7/site-packages (from cli-ui) (0.4.1)
  154. Requirement already satisfied: tabulate in /mnt/data/dmerej/src/python-tabulate (from cli-ui) (0.8.4)
  155. Installing collected packages: cli-ui
  156. Successfully installed cli-ui-0.9.1
  157. ```
  158. On constate ici quelques limitations de `pip`:
  159. * Il faut penser à utiliser `--user` (de la même façon que lorsqu'on lance `setup.py` à la main)
  160. * Si le paquet est déjà installé dans le système, pip ne saura pas le mettre à jour - il faudra passer par le gestionnaire de paquet de
  161. la distribution
  162. En revanche, `pip` contient de nombreuses fonctionnalités intéressantes:
  163. * Il est capable de désinstaller des bibliothèques (à condition toutefois qu'elles ne soient pas dans un répertoire système)
  164. * Il est aussi capable d'afficher la liste complète des bibliothèques Python accessibles par l'utilisateur courant avec `freeze`.
  165. Voici un extrait de la commande `python3 -m pip freeze` au moment de la rédaction de cet article sur ma machine:
  166. ```
  167. $ python3 -m pip freeze
  168. apipkg==1.5
  169. cli-ui==0.9.1
  170. gaupol==1.5
  171. tabulate==0.8.4
  172. ```
  173. 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](https://otsaloma.io/gaupol/) 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.
  174. On constate également que chaque bibliothèque possède un *numéro de version*.
  175. # Numéros de version
  176. Les numéros de version remplissent plusieurs rôles, mais l'un des principaux est de spécifier des changements incompatibles.
  177. 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 cet extrait du [changelog](https://tankerhq.github.io/python-cli-ui/changelog.html#v0-8-0):
  178. > the list of choices used by ask_choice is now a named keyword argument:
  179. ```python
  180. # Old (<= 0.7)
  181. ask_choice("select a fruit", ["apple", "banana"])
  182. # New (>= 0.8)
  183. ask_choice("select a fruit", choices=["apple", "banana"])
  184. ```
  185. Ceci s'appelle un *changement d'API*.
  186. # Réagir aux changements d'API
  187. Plusieurs possibilités:
  188. * On peut bien sûr adapter le code pour utiliser la nouvelle API, mais cela n'est pas toujours possible ni souhaitable.
  189. * Une autre solution est de spécifier des *contraintes* sur le numéro de version dans la déclaration des dépendances. Par exemple :
  190. ```python
  191. setup(
  192. install_requires=[
  193. "cli-ui < 0.8",
  194. ...
  195. ]
  196. )
  197. ```
  198. # Aparté : pourquoi éviter sudo pip
  199. Souvenez-vous que les fichiers systèmes sont contrôlés par votre gestionnaire de paquets.
  200. Les mainteneurs de votre distribution font en sorte qu'ils fonctionnent bien les uns
  201. 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.
  202. 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.
  203. Mais il y a un autre problème encore pire.
  204. # Conflit de dépendances
  205. 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 ?
  206. # Environnements virtuels
  207. La solution est d'utiliser un environnement virtuel (*virtualenv* en abrégé). C'est un répertoire *isolé* du reste du système.
  208. Il se crée par exemple avec la commande `python3 -m venv foo-venv`. où `foo-venv` est un répertoire quelconque.
  209. ## Aparté : python3 -m venv sur Debian
  210. 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 [^5].
  211. 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 :
  212. * installant le paquet `python3-venv`,
  213. * ou en utilisant d'abord `pip` pour installer `virtualenv`, avec `python3 -m pip install virtualenv --user` puis en lançant `python3 -m virtualenv foo-venv`.
  214. ## Comportement de python dans le virtualenv
  215. Ce répertoire contient de nombreux fichiers et dossiers, et notamment un binaire dans `foo-venv/bin/python3`.
  216. Voyons comment il se comporte en le comparant au binaire `/usr/bin/python3` habituel :
  217. ```
  218. $ /usr/bin/python3 -c 'import sys; print(sys.path)'
  219. ['',
  220. ...
  221. '/usr/lib/python3.7',
  222. '/usr/lib/python3.7.zip',
  223. '/usr/lib/python3.7/lib-dynload',
  224. '/home/dmerej/.local/lib/python3.7/site-packages',
  225. '/usr/lib/python3.7/site-packages'
  226. ]
  227. $ /home/dmerej/foo-venv/bin/python -c 'import sys; print(sys.path)'
  228. ['',
  229. '/usr/lib/python3.7',
  230. '/usr/lib/python3.7.zip',
  231. '/usr/lib/python3.7/lib-dynload',
  232. '/home/dmerej/foo-venv/lib/python3.7/site-packages,
  233. ]
  234. ```
  235. À noter:
  236. * Le répertoire "global" dans `~/.local/lib` a disparu
  237. * Seuls quelques répertoires systèmes sont présents (ils correspondent plus ou moins à l'emplacement des modules de la bibliothèque standard)
  238. * Un répertoire *au sein* du virtualenv a été rajouté
  239. Ainsi, l'isolation du virtualenv est reflété dans la différence de la valeur de `sys.path`.
  240. 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.
  241. 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.
  242. 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.
  243. ## Comportement de pip dans le virtualenv
  244. 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.
  245. On peut s'en assurer en lançant `python3 -m pip freeze` depuis le virtualenv et en vérifiant que rien ne s'affiche.
  246. ```
  247. $ python3 -m pip freeze
  248. # de nombreuses bibliothèques en dehors du virtualenv
  249. apipkg==1.5
  250. cli-ui==0.9.1
  251. gaupol==1.5
  252. tabulate==0.8.4
  253. $ /home/dmerej/foo-venv/bin/python3 -m pip freeze
  254. # rien :)
  255. ```
  256. On peut alors utiliser le module `pip` *du virtualenv* pour installer des bibliothèques dans celui-ci :
  257. ```
  258. $ /home/dmerej/foo-venv/bin/python3 -m pip install cli-ui
  259. Collecting cli-ui
  260. Using cached https://pythonhosted.org/..cli_ui-0.9.1-py3-none-any.whl
  261. Collecting colorama (from cli-ui)
  262. Using cached https://pythonhosted.org/..colorama-0.4.1-py2.py3-none-any.whl
  263. Collecting unidecode (from cli-ui)
  264. Using cached https://pythonhosted.org/..Unidecode-1.0.23-py2.py3-none-any.whl
  265. Collecting tabulate (from cli-ui)
  266. Installing collected packages: colorama, unidecode, tabulate, cli-ui
  267. Successfully installed cli-ui-0.9.1 colorama-0.4.1 tabulate-0.8.3
  268. unidecode-1.0.23
  269. ```
  270. 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.
  271. On a enfin notre solution pour résoudre notre conflit de dépendances :
  272. on peut simplement créer un virtualenv par projet. Ceci nous permettra
  273. d'avoir effectivement deux versions différentes de `cli-ui`, isolées les
  274. unes des autres.
  275. # Activer un virtualenv
  276. 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 :
  277. * `source foo-venv/bin/activate` - si vous utilisez un shell POSIX
  278. * `source foo-venv/bin/activate.fish` - si vous utilisez Fish
  279. * `foo-venv\bin\activate.bat` - sous Windows
  280. Une fois le virtualenv activé, taper `python`, `python3` ou `pip` utilisera les binaires correspondants dans le virtualenv automatiquement,
  281. et ce, tant que la session du shell sera ouverte.
  282. 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 :
  283. ```
  284. # Avant
  285. user@host:~/src $ source foo-env/bin/activate
  286. # Après
  287. (foo-env) user@host:~/src $
  288. ```
  289. Pour sortir du virtualenv, entrez la commande `deactivate`.
  290. # Conclusion
  291. Le système de gestions des dépendances de Python peut paraître compliqué et bizarre, surtout venant d'autres langages.
  292. Mon conseil est de toujours suivre ces deux règles :
  293. * Un virtualenv par projet et par version de Python
  294. * Toujours utiliser `pip` *depuis* un virtualenv
  295. 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).
  296. 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.
  297. [^1]: C'est une condition suffisante, mais pas nécessaire - on y reviendra.
  298. [^2]: Presque. Il peut arriver que l'utilisateur courant ait les droits d'écriture dans *tous* les segments de `sys.path`, en fonction de l'installation de Python. Cela dit, c'est plutôt l'exception que la règle.
  299. [^3]: Cela peut vous paraître étrange à première vue. Il y a de nombreuses raisons historiques à ce comportement, et il n'est pas sûr qu'il puisse être changé un jour.
  300. [^4]: Presque. Parfois il faut installer un paquet supplémentaire, notamment sur les distributions basées sur Debian
  301. [^5]: Je n'ai pas réussi à trouver une explication satisfaisante à ce choix des mainteneurs Debian. Si vous avez des informations à ce sujet, je suis preneur. _Mise à jour: Il se trouve que cette décision s'inscrit au sein de la "debian policy", c'est à dire une liste de règles que doivent respecter tous les programmes maintenus par Debian._