Poetry, czyli kiedy pip już nie wystarcza

Niedawno zadane zostało mi pytanie, dlaczego korzystam z Poetry i w czym jest ono lepsze od pip. W tym wpisie spróbuję odpowiedzieć na te pytania. Zanim przejdę dalej, warto nadmienić, czym w ogóle są oba narzędzia. Pip1 to dobrze znany wszystkim programistom Pythona menadżer pakietów, Poetry2 natomiast stanowi jego rozwinięcie, zapewniające dodatkowe funkcje i upraszczające sposób jego użytkowania.

pyproject.toml

Definiując zależności dla projektu, można podzielić je na dwie główne grupy, zależności potrzebne do działania programu i deweloperskie, takie jak np. test runnery, lintery itp. Nie ma sensu budować paczki z gotowym programem, zawierającej przykładowo framework pytest, ponieważ dla użytkownika jest on kompletnie zbędny. W przypadku pip, aby rozdzielić zależności można skorzystać z podejścia wykorzystującego dwa pliki requirements.txt, w których odpowiednio będą zdefiniowane zależności deweloperskie i produkcyjne. Oczywiście oba pliki, ktoś będzie musiał utrzymywać i ręcznie aktualizować, co nie jest zbyt wygodne. W przypadku Poetry sprawy mają się nieco inaczej, narzędzie to do przechowywania listy zależności wykorzystuje plik pyproject.toml wprowadzony w PEP-5183, ponadto przechowywane są tam również inne informacje dotyczące konfiguracji projektu oraz jego budowania. Istotne jest to, że Poetry aktualizuje wspomniany plik automatycznie po dodaniu do projektu nowych zależności, co więcej mamy możliwość oznaczenia paczek jako deweloperskich, dzięki czemu znajdą się one na osobnej liście. A wszystko dzięki pojedynczej komendzie poetry add [--dev] <nazwa paczki>. Przykładowy plik pyproject.toml znajduje się poniżej.

[tool.poetry]
name = "django_app"
version = "0.1.0"
description = ""
authors = ["artur"]

[tool.poetry.dependencies]
python = "^3.10"
Django = "^4.0.5"
django-crispy-forms = "^1.14.0"

[tool.poetry.dev-dependencies]
pytest = "^7.1.2"
pylint = "^2.14.4"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

rozwiązywanie zależności

Poetry automatycznie rozwiązuje poważny problem powtarzalnego budowania środowiska, zanim jednak przejdę do wyjaśnienia, w jaki sposób to robi, warto omówić sobie pewne kwestie związane z pip. Korzystając z tego menadżera paczek, należy pamiętać, aby wykonać komendę pip freeze > requirements.txt, po każdorazowym zainstalowaniu jakiejś paczki, jest to istotne, ponieważ dodając jakąś paczkę do naszego środowiska, dodajemy również jej zależności, które warto zapisać wraz z ich wersjami. Manualne uzupełnianie pliku requirements.txt wprowadza ryzyko, że na różnych systemach mogą zostać zainstalowane inne wersje zależności instalowanych paczek (domyślnie pip instaluje najnowsze wersje paczek), co grozi konfliktami wersji, czy innymi błędami. Wracająć do Poetry, jak już wspominałem, paczki wymagane przez nasz projekt zdefiniowane są w pliku pyproject.toml, jednakże znajdują się tam tylko główne wymagane moduły. Poetry wprowadza do projektu jeszcze jeden plik nazwany poetry.lock, którego celem jest przechowywanie informacji o wszystkich zależnościach projektu, wraz ich wersjami oraz co istotne sumami kontrolnymi, do których wrócę w dalszej części wpisu. Podsumowując, wywołując poetry install, mamy pewność, że zainstalowane zostaną paczki o konkretnej wersji.

Załóżmy teraz, że chcemy zaktualizować jakąś paczkę menadżerem pip, jawnie instalujemy jej nową wersję, która okazuje się być niekompatybilna z inną wcześniej zainstalowaną paczką. Co w tym momencie zrobi pip? Odpowiedź jest prosta, zainstaluje paczkę ! Na potrzeby poniższego przykładu zmieniłem wersję Django z 4.0.5 na wersję 1.8, mając zainstalowany Dajngo Rest Framework (3.13.1), przykład ten doskonale oddaje zachowanie podstawowego menadżera paczek pip.

pip install django==1.8
Collecting django==1.8
  Using cached Django-1.8-py2.py3-none-any.whl (6.2 MB)
Installing collected packages: django
  Attempting uninstall: django
    Found existing installation: Django 4.0.5
    Uninstalling Django-4.0.5:
      Successfully uninstalled Django-4.0.5
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
djangorestframework 3.13.1 requires django>=2.2, but you have django 1.8 which is incompatible.
Successfully installed django-1.8

W przypadku Poetry opisana sytuacja nie może zaistnieć. W przypadku jakiejkolwiek niekompatybilności instalacja paczki zostanie zatrzymana, a w terminalu pojawi się stosowny komunikat, jak na przykładzie poniżej. Co więcej, Poetry pozwala na przetestowanie operacji podczas wykonywania których rozwiązywane są zależności (np. update, czy add) poprzez podanie argumentu --dry-run. Dzięki temu argumentowi narzędzie nie wykona żadnych faktycznych operacji, a jedynie wyświetli jakie kroki muszą zostać podjęte do ich wykonania.

poetry add django==1.8

Updating dependencies
Resolving dependencies... (0.0s)

  SolverProblemError

  Because djangorestframework (3.13.1) depends on django (>=2.2)
   and no versions of djangorestframework match >3.13.1,<4.0.0, djangorestframework (>=3.13.1,<4.0.0) requires django (>=2.2).
  So, because django-app depends on both Django (1.8) and djangorestframework (^3.13.1), version solving failed.

  at ~/.local/lib/python3.10/site-packages/poetry/puzzle/solver.py:241 in _solve
      237│             packages = result.packages
      238│         except OverrideNeeded as e:
      239│             return self.solve_in_compatibility_mode(e.overrides, use_latest=use_latest)
      240│         except SolveFailure as e:
    → 241│             raise SolverProblemError(e)
      242│ 
      243│         results = dict(
      244│             depth_first_search(
      245│                 PackageNode(self._package, packages), aggregate_package_nodes

bezpieczeństwo

Instalując paczki z poziomu menadżera pip, ich sumy kontrolne nie są weryfikowane, co wprowadza potencjalne zagrożenie. Podczas pobierania zależności może wystąpić błąd sieciowy, przez co dane paczki zostaną uszkodzone i będzie ona wadliwa lub co gorsza, ktoś wprowadzi spreparowaną paczkę o tej samej wersji, ale wzbogaconą o złośliwy kod. Możliwe jest weryfikowanie hashy przy pomocy pip, jednakże nie ma prostej metody na zapisanie ich wartości w pliku requirements.txt. W przypadku Poetry każda zainstalowana paczka, wraz z jej zależnościami jest zapisywana w pliku poetry.lock razem z sumą kontrolną. Tak więc jeżeli przy ponownym instalowaniu paczek jakiś hash się zmieni, Poetry poinformuje nas o zaistnieniu niespójności.

izolacja

Zakładając nowy projekt, zapewne korzystasz z virtualenv4, aby odizolować środowisko projektu od jego hosta. Jeżeli nie izolujesz środowiska deweloperskiego, a paczki instalujesz za pomocą pip, mogą spotkać Cię nieprzyjemne konsekwencje, w postaci, chociażby konfliktów pomiędzy wersjami paczek, których potrzebuje nowy projekt, a tym co aktualnie dostępne jest w systemie. Poetry tworzy wirtualne środowisko Pythona za Ciebie. Nie musisz o niczym pamiętać, po prostu korzystasz z Poetry, które w tle zajmuje się wszystkim. Virtualenv stworzony przez Poetry można aktywować przy pomocy komendy poetry shell, czy uruchomić w nim moduł przy pomocy poetry run <moduł>. Warto nadmienić, że Poetry można zintegrować z IDE Pychram, czy IntelliJ poprzez plugin5, dzięki któremu, IDE będzie korzystać z virtualenv stworzonego przez Poetry.

Intellij Poetry plugin setup

Co więcej, Poetry pozwala na zarządzanie różnymi wersjami Pythona, realizując podobne funkcje do projektu pyenv6, dzięki czemu możliwe jest rozwijanie i testowanie kodu dla różnych wersji języka. Zmiany używanej wersji języka można dokonać przy pomocy komendy przedstawionej poniżej, należy jednak pamiętać, że podana wersja Pythona musi być dostępna na danym systemie.

poetry env use pypy3

dystrybucja paczek

Poetry pozwala na szybką i prostą budowę paczek oraz ich dystrybucję. Komenda build pozwala na zbudowanie paczek w formacie wheel7 oraz sdist8 na podstawie pliku pyproject.toml. Niepotrzebne są do tego żadne dodatkowe narzędzia, takie jak np. setuptools.

poetry build

Publikacja paczki jest równie prosta, po zbudowaniu paczki komendą build wystarczy wywołać komendę publish w celu wprowadzenia paczki do indeksu pypi9.

poetry publish

podsumowanie

Poetry nie jest po prostu menadżerem paczek, to uniwersalne narzędzie, z którego poziomu możemy zarządzać zależnościami, wersjami Pythona, budową oraz publikacją paczek. Bez wątpienia można dzięki niemu zaoszczędzić sporo czasu. Czy w takim razie warto korzystać z Poetry? Moim zdaniem, tak! Poetry Warto wprowadzić do swojego projektu, jeżeli nadal korzysta on z czystego pip i requirements.txt. Jeżeli nie podoba Ci się Poetry, możesz skorzystać z innych rozwiązań takich jak Pipenv10, czy PDM11, które podobnie jak Poetry, nie tylko skrupulatnie zarządzają zależnościami projektu, ale również je weryfikują. O PDM bez wątpienia napiszę w przyszłości, gdyż jest to bardzo ciekawa alternatywa dla opisanego menadżera paczek, zapewniająca izolację środowiska bez virtualenv.

  1. podstawowy menadżer pakietów dla Python: https://pypi.org/project/pip/ []
  2. nowoczesny menadżer pakietów dla języka Python: https://python-poetry.org/ []
  3. Python Enhancement Proposal 518, dokumentacja dostępna pod adresem: https://peps.python.org/pep-0518/ []
  4. narzędzie do izolacji środowiska Python, link do strony projektu: https://virtualenv.pypa.io/en/latest/ []
  5. https://plugins.jetbrains.com/plugin/14307-poetry []
  6. Menadżer wersji Pythona, link do projektu na Github: https://github.com/pyenv/pyenv []
  7. format paczki, więcej w PEP-427: https://peps.python.org/pep-0427/ []
  8. source distribution, format paczki, więcej w dokumentacji: https://docs.python.org/3/distutils/sourcedist.html []
  9. Python Package Index – rejestr paczek dla języka Python []
  10. menadżer paczek, strona projektu: https://pipenv.pypa.io/en/latest/ []
  11. menadżer paczek, strona projektu: https://pdm.fming.dev/latest/ []