Или почему вы должны использовать поэзию для управления зависимостями Python

Если вы когда-либо пытались написать приложение Python, вы, вероятно, в какой-то момент столкнулись с печально известным адом зависимостей Python.

К счастью для вас (и для всех нас) на момент написания статьи, есть несколько хороших решений для запутывания болей, блестяще изображенных на изображении выше. На самом деле, вы, скорее всего, уже знаете, что если вы хотите разрабатывать несколько версий Python, вы можете легко использовать pyenv, чтобы начать распутывать вашу искривленную среду. Возможно, вы также узнали, что если вы работаете одновременно над несколькими проектами с конфликтующими зависимостями, вы можете использовать виртуальные среды для изоляции конфликтующих библиотек. В этом документе мы представим еще один инструмент, Поэзия, и приведем аргумент о том, почему вам, вероятно, следует добавить его в свой собственный набор инструментов.

Проблема

Представьте, что одной одинокой ночью вы решили запустить простой фиктивный проект Python с точным названием foo со следующей структурой.

├── foo
│   ├── foo
│   │   ├── bar
│   │   │   └── data.py
│   │   └── constants.py
│   └── README.md

Поскольку это не первый ваш проект Python, и вы хотите избежать бесконечных ночей, исправляющих несовместимости между вашей системой и модулями проекта, вы старательно инициируете виртуальную среду из своей оболочки с помощью

$> python -m venv ~/Desktop/venv/foo-venv

и активируйте его во вновь созданном проекте с помощью

$> source ~/Desktop/venv/foo-venv/bin/activate

Получив изолированную среду, вы с триумфом переходите к установке вездесущей библиотеки данных Pandas. Чтобы добиться этого, вы используете де-факто менеджер пакетов Python pip и тщательно закрепляете версию библиотеки, чтобы обеспечить воспроизводимость.

$> pip install pandas==0.25.3

Поскольку вам немного лень выполнять исследовательский анализ данных, вы также устанавливаете изящный модуль pandas-profiling, который поможет вам в этой утомительной работе.

$> pip install pandas-profiling==2.5.0

После всего этого флирта вы, наконец, начинаете кодить (при условии, что добавление следующих строк в файл data.py может, собственно, так и называться)

import pandas as pd
from pandas_profiling import ProfileReport
df=pd.DataFrame([['a',1],['b',None]],columns=['category', 'value'])
df['category']=df['category'].astype('category')
if __name__=='__main__':ProfileReport(df).to_file('foo.html')

Учитывая, что ваши дни, когда вы злоупотребляли операторами print в целях отладки, давно прошли, вы устанавливаете красивую и удобную библиотеку pdbpp, чтобы проверить, что эти строки работают должным образом.

$> pip install pdbpp==0.10.2

и запустите свой код в режиме постфактум-отладки с помощью python -m pdb -cc data.py.

Довольные чистым запуском, вы теперь понимаете, что для того, чтобы отправить ваше роскошное приложение, не попадая в ловушку «работает на моей машине», вам нужен способ собрать все ваши зависимости. Быстрый поиск в Google покажет вам, что подкоманда pip freeze позволяет записать текущий пакет среды в файл requirements.txt с помощью следующего заклинания

$> pip freeze > requirements.txt

который позволяет любому использовать ваш проект, просто установив необходимые зависимости с помощью

$> pip install -r requirements.txt

Как только вы собираетесь представить миру свой шедевральный проект, вы осознаете, что улучшенный модуль отладки на самом деле используется только вами во время разработки. С идеей разделения замороженных требований на отдельные файлы производства и разработки, вы заглядываете в сгенерированный файл только для того, чтобы обнаружить, что каждая отдельная подзависимость зависимостей вашего приложения указана в нем и привязана к определенной версии. Предвидя кошмар обслуживания этого огромного списка, вы удаляете библиотеку pdbpp, чтобы снова обеспечить чистый файл требований с помощью

$> pip uninstall -y pdbpp && pip freeze > requirements.txt

Однако беглый взгляд на измененный файл требований показывает, что все получилось не совсем так, как ожидалось: pdbpp действительно был удален, но его зависимости, такие как fancycompleter, все еще установлены. Поскольку это кажется тупиковым, вы решили начать с нуля, вручную создав файл requirements.txt только с производственными зависимостями.

pandas==0.25.3
pandas-profiling==2.5.0

и эквивалентный файл разработки, requirements_dev.txt, содержащий исключительно

pdbpp==0.10.2

Впечатленный изобретательностью, которая, казалось бы, помогла вам избежать ужасного ада зависимостей Python, ведя учет изолированных пакетов верхнего уровня, вы решаете на этом закончить и на следующий день окончательно раскрутить свое приложение.

Когда вы просыпаетесь утром, новости повсюду: Pandas v1 наконец-то вышла (всего двенадцать лет!). Пара часов промедления с невероятно длинным журналом изменений приводит к выводу, что ваш сложный foo-проект наверняка получит заметные улучшения, обновившись до новой версии. Теперь, поскольку вы заблокировали Pandas до точной версии, вы не можете просто запустить

$> pip install -U -r requirements.txt

Вместо этого вы должны выполнить

$> pip install pandas==1.0.0

что приводит к особенно странной и запутанной ситуации: в вашем терминале выскакивает ошибка

ERROR: pandas-profiling 2.5.0 has requirement pandas==0.25.3, but you'll have pandas 1.0.0 which is incompatible.

но установка pandas 1.0.0 тем не менее происходит. Предполагая, что это предупреждение о том, что pip ошибается, вы соответствующим образом обновляете свой файл requirements.txt и с радостью продолжаете запускать свой модуль data.py в последний раз только для того, чтобы обнаружить, что он выдает загадочное TypeError. Чувствуя себя обманутым из-за очевидной неспособности pip разрешать зависимости, вы откатываете свои изменения и придерживаетесь устаревшей (теперь) версии Pandas.

На данный момент кажется, что у вас есть работающий проект, но i) вы не уверены, мог ли возврат версии Pandas нарушить желаемую воспроизводимость вашего приложения, ii) код определенно мог бы выглядеть лучше и iii) после хорошего ночного сна вы признаете, что общая функциональность вашего приложения не так сложна и богата, как вы думали накануне вечером. Чтобы решить первые две проблемы, вы сначала добавляете средство форматирования black к вашему requirements_dev.txt

black==19.10b0

а затем в каталоге вашего проекта вы воссоздаете свою виртуальную среду с помощью

$> rm -rf ~/Desktop/venv/foo-venv
$> python -m venv ~/Desktop/venv/foo-venv
$> source ~/Desktop/venv/foo-venv/bin/activate
$> pip install -r requirements_dev.txt
$> pip install -r requirements.txt

Теперь вы запускаете black в корне вашего проекта (с black .) и в основном удовлетворены работой по приукрашиванию, которую он выполнил, но придерживаетесь стиля формата Mutt Data (который по совпадению согласуется с вашей неприязнью превращать каждую одинарную кавычку в двойную), вы добавляете pyproject.toml говорит black пропустить такую ​​ужасную настройку нормализации строк по умолчанию

[tool.black]
skip-string-normalization = true

Теперь код выглядит великолепно, и новый запуск отладки после вскрытия показывает, что в новой (реплицируемой) среде все работает нормально. Единственное, что осталось сделать перед тем, как развернуть код на сервере или поделиться им со всем миром, — это избежать наличия констант, таких как имя отчета, жестко запрограммированных по всему коду. Таким образом, вы решаете добавить следующие строки в ваш пустой модуль constants.py

REPORT_FILE = 'foo.html'

и измените data.py, чтобы импортировать такую ​​константу из относительного родительского файла с помощью

from ..constants import REPORT_FILE

Однако новый запуск data.py теперь, к сожалению, показывает следующую ошибку

ImportError: attempted relative import with no known parent package

что, согласно всеведущему SO, имеет смысл, поскольку относительный импорт Python работает только внутри пакета, и поэтому, если вы хотите импортировать из родительского каталога, вы должны либо создать такой пакет, либо взломать файл sys.path. Как настоящий питонист-пурист, вы выбираете прежний путь и создаете setup.py со следующим содержимым.

from setuptools import setup
with open('requirements.txt') as f:
    install_requires = f.read().splitlines()
with open('requirements_dev.txt') as f:
    extras_dev_requires = f.read().splitlines()
setup(
    name='foo',
    version='0.0.1',
    author='Mutt',
    author_email='[email protected]',
    install_requires=install_requires,
    extras_require={'dev': extras_dev_requires},
    packages=['foo'],
)

Теперь в совершенно новой виртуальной среде вы устанавливаете свой пакет в редактируемом режиме с помощью pip install -e .[dev], измените строку импорта в data.py, чтобы учесть структуру пакета.

from foo.constants import REPORT_FILE

и скрестить пальцы в надежде, что все наконец заработает…

Все действительно (хрупко) работает, но почему-то все прыжки с обруча, чтобы заставить его работать, вызывают у вас беспокойство. Краткий самоанализ выявляет несколько причин волны опасений:

  1. Поскольку вы планируете работать над несколькими проектами Python одновременно, изоляция является фундаментальной частью вашего рабочего процесса. Виртуальные среды действительно решают эту проблему, но процесс активации/деактивации громоздок и легко забывается.
  2. Наличие изолированных зависимостей между проектами не устраняет конфликты зависимостей внутри проекта. Надлежащее разрешение зависимостей — это главная необходимая функция любого менеджера пакетов, заслуживающая уважения, но pip реализовал эту функцию только к концу октября 2020 года. Ручная гарантия согласованности зависимостей в сложных проектах — тупиковая ситуация.
  3. Если вы хотите установить приложение/проект в виде пакета, вам придется добавить setup.py поверх ваших уже многочисленных файлов требований. Однако вы прочитали PEP 517-518 и хотите попробовать более простые и безопасные механизмы сборки, упомянутые в них.
  4. Вы думали о том, чтобы попробовать свое приложение на другом компьютере, но поняли, что на нем работает Python 3.7, а на вашем локальном компьютере — 3.8. Чтобы использовать pyenv с вашими изолированными виртуальными окружениями, вам нужен дополнительный плагин pyenv-virtualenv, который делает управление venv еще более обременительным.
  5. Вы немного поиграли с Pipenv, который обещал привнести в Python завидные функции более зрелых менеджеров пакетов других языков (таких как yarn/npm в Javascript или Cargo в Rust), но быстро разочаровались. Pipenv не только ошибочно утверждал, что является официальным рекомендуемым инструментом упаковки Python (хотя он действительно был разработан для написания приложений, а не пакетов), но также не выпускал выпуски более года и до сих пор бесконечно зависает при создании файла блокировки, который гарантирует повторяемые/детерминированные сборки.

В состоянии безнадежного отчаяния вы лихорадочно начинаете искать в Интернете, существует ли уже решение всех этих проблем. Среди множества неполных/неполных кандидатов вы, наконец, сталкиваетесь с одним, который невероятно ломает их всех: это называется Поэзия.

Решение

Установка (с Pipx)

Poetry — это CLI-приложение, написанное на Python, поэтому вы можете просто установить его с помощью pip install --user poetry. Однако вы, вероятно, уже установили или установите другие приложения Python CLI (например, причудливый клиент PostgreSQL pgcli или youtube-dl для загрузки видео с YouTube). Если вы установите их с помощью диспетчера пакетов вашей системы (например, apt, yay или brew), они будут установлены на глобальном уровне, и их зависимости потенциально могут конфликтовать. Вместо этого вы можете создать отдельный venv для каждого, но для того, чтобы использовать их, вам придется сначала пройти через активацию среды…

Чтобы обойти этот надоедливый сценарий, вы можете использовать pipx, который точно установит пакеты в изолированной виртуальной среде и в то же время сделает их доступными в вашей оболочке (т. е. добавит исполняемый файл в ваши двоичные файлы $PATH). Помимо предоставления приложений CLI для глобального доступа, он также упрощает перечисление, обновление и удаление этих приложений. Чтобы установить Poetry с помощью pipx, сначала установите pipx с помощью

$> python -m pip install --user pipx
$> python -m pipx ensurepath

а затем непосредственно сделать

$> pipx install poetry

Если вы предпочитаете жить на грани (как и я), вы можете в качестве альтернативы установить версию pre-release с pipx install --pip-args='--pre' poetry.

Применение

Теперь вы готовы испытать чудеса, обещанные Поэзией. Для этого вы создаете новую папку/проект под названием foo-poetry с вашими .py файлами, указанными выше, а затем запускаете poetry init. Интерактивная подсказка начнет запрашивать у вас основную информацию о вашем пакете (имя, автор и т. д.), которая будет использоваться для создания файла pyproject.toml. По сути, это те же самые метаданные, которые вы ранее добавили в setup.py, с некоторыми минимальными вариациями.

This command will guide you through creating your pyproject.toml config.
Package name [foo-poetry]:  foo
Version [0.1.0]:  0.0.1
Description []:
Author [petobens <[email protected]>, n to skip]:  Mutt <[email protected]>
License []:
Compatible Python versions [^3.8]:  ~3.7
Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Generated file
[tool.poetry]
name = "foo"
version = "0.0.1"
description = ""
authors = ["Mutt <[email protected]>"]
[tool.poetry.dependencies]
python = "^3.7"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0a5"]
build-backend = "poetry.core.masonry.api"

Do you confirm generation? (yes/no) [yes] yes

Два важных параметра, которые следует выделить, — это система сборки и спецификация версии Python. Единственное, что вам нужно знать на данный момент о первом, это то, что он использует стандарты PEP 517–518 для определения альтернативного способа сборки проекта из исходного кода без setuptools (и, следовательно, устраняет необходимость в файле setup.py). ). Что касается второго параметра, чтобы понять синтаксис, определяющий ограничения версии Python, вам следует прочитать Документацию по версии Poetry, где вы узнаете, что требование знака вставки (^) означает, что разрешены только незначительные обновления и исправления (т. е. наше приложение будет работать с Python 3.7 и 3.8, но не с 4.0).

Пока у вас есть только файл TOML (который вы также можете использовать для централизации конфигурации black). Как вы указываете зависимости? Просто запустите

$> poetry add pandas==0.25.3

что приводит к

Creating virtualenv foo-KLaC03aC-py3.8 in /home/pedro/.cache/pypoetry/virtualenvs
Updating dependencies
Resolving dependencies... (0.6s)
Writing lock file

Package operations: 5 installs, 0 updates, 0 removals
  - Installing six (1.15.0)
  - Installing numpy (1.19.1)
  - Installing python-dateutil (2.8.1)
  - Installing pytz (2020.1)
  - Installing pandas (0.25.3)

Другими словами, начальная команда add будет i) создавать виртуальную среду, ii) устанавливать запрошенные пакеты и их подзависимости, iii) записывать точную версию каждой загруженной зависимости в файл poetry.lock (который вы должны зафиксировать в своей системе контроля версий, чтобы обеспечить возможность репликации) и iv) добавить строку с вновь добавленным пакетом в раздел tool.poetry.dependencies файла pyproject.toml. Последний пункт также сигнализирует о том, что если вы хотите установить новую зависимость, вы можете повторно использовать команду add или напрямую добавить такую ​​строку в свой файл pyproject.toml. Например, если теперь вы хотите добавить библиотеку pandas-profiling, вы можете изменить pyproject так, чтобы

pandas-profiling = "2.5.0"

Поскольку на этом этапе файл poetry.lock уже существует, если вы сейчас запустите poetry install, то Poetry разрешит и установит зависимости, используя версии, указанные в таком файле блокировки (для обеспечения согласованности версий). Однако из-за того, что вы вручную добавили новую зависимость в файл pyproject.toml, команда install завершится ошибкой. Следовательно, в этом случае вам нужно запустить poetry update, что по сути эквивалентно удалению файла блокировки и повторному запуску poetry install.

Добавление зависимости development работает аналогичным образом с той лишь оговоркой, что вам нужно использовать флаг --dev при выполнении команды add

$> poetry add pdbpp==0.10.2 --dev
$> poetry add black==19.10b0 --dev

и полученные пакеты будут добавлены в раздел tool.poetry.dev-dependencies.

Теперь, когда зависимости установлены, вы можете запустить файл кода data.py, выполнив

$> poetry run python data.py

который выполнит команду в файле virtualenv проекта. Кроме того, вы можете создать оболочку в активном venv, просто запустив

$> poetry shell

Теперь представьте, что вы хотите обновить версию Pandas, как вы делали это раньше, проверяя неспособность pip обеспечить разрешение зависимостей. Для этого вы обновляете ограничение, например

$> poetry add pandas==1.0.0

который на этот раз правильно терпит неудачу со следующей ошибкой

Updating dependencies
Resolving dependencies... (0.0s)
[SolverProblemError]
Because pandas-profiling (2.5.0) depends on pandas (0.25.3)
 and foo depends on pandas (1.0.0), pandas-profiling is forbidden.
So, because foo depends on pandas-profiling (2.5.0), version solving failed.

К настоящему моменту вы заметили, что Poetry, кажется, удовлетворяет первые два запроса, которые вы перечислили в предыдущем разделе (а именно, простая изоляция проекта и правильное автоматическое разрешение зависимостей). Прежде чем возлагать надежды, вы проверяете, может ли он напрямую упаковать ваш код (особенно без setup.py). Примечательно, что это просто сводится к включению следующей строки в раздел tool.poetry файла pyproject.toml

packages = [{include = "foo"}]

с последующим выполнением нового poetry install, который по умолчанию установит проект в редактируемом режиме.

В восторге от простоты и легкости использования Poetry, вы начинаете задаваться вопросом, является ли Poetry идеальным инструментом, который вы искали. Может ли он поставить все галочки? Чтобы окончательно ответить на этот вопрос, вам нужно проверить, легко ли переключаться между разными версиями Python. Учитывая, что ваш локальный компьютер использует Python 3.8 по умолчанию, вы, следовательно, устанавливаете 3.7.7 с pyenv install 3.7.7 (установка предыдущего выпуска не сработала бы в будущем, поскольку вы установили 3.7 в качестве нижней границы в вашем приложении pyproject.toml). Чтобы сделать эту версию локально доступной, вы добавляете файл .python-version в корень вашего проекта, содержащий одну строку с 3.7.7, а затем указываете Poetry создать и использовать виртуальную среду с этой версией с помощью

$> poetry env use 3.7

Как только вы убедитесь, что он правильно активирован с помощью poetry env list, вы устанавливаете все зависимости с помощью poetry install и в конечном итоге запускаете свой код, который (что неудивительно) завершается без проблем.

Пораженный ее интуитивной простотой, вы заключаете, что Поэзия — это именно то, что вам нужно. На самом деле, вы еще этого не знаете, но вы получили гораздо больше, чем рассчитывали, поскольку вы только коснулись функций. Вы все еще должны обнаружить, что он устанавливает пакеты параллельно, выбрасывает красивые цветные исключения, когда все рушится, интегрируется с выбранной вами IDE/редактором (если это vim, вы можете попробовать бессовестный взгляд вашего покорного слуги на этот вопрос), имеет команда для прямой публикации пакета и, помимо других бесчисленных прелестей, планируется иметь систему плагинов для дальнейшего расширения.

Одно совершенно ясно: Poetry — это диспетчер пакетов Python завтрашнего дня. Вы также можете начать использовать его сегодня.

Если вы хотите прочитать больше материалов, связанных с машинным обучением, посетите блог нашей компании. Я надеюсь, что вы нашли этот пост полезным и, по крайней мере, слегка развлекательным. Если вам нужна помощь в определении наилучшего способа развития бизнеса с помощью данных, вы можете найти нашу компанию здесь.

Эта статья также была опубликована в блоге компании Mutt Data.