А как вам такой вариант управления зависимостями в Python?

от автора

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

Проблема

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

В рамках моей работы чаще всего я использую Python в двух целях: это либо анализ данных и машинное обучение используя блокноты Jupyter, либо небольшие Python скрипты, которые каким-то образом подготавливают данные. Хочу отметить, что я не занимаюсь созданием пакетов и их публикацией в Pypi (поэтому не акцентирую внимание на этом процессе в этой статье).

Очень часто мне требуется запускать скрипты или блокноты, которые были созданы довольно давно. Поэтому у меня возникла необходимость каким-то образом фиксировать версии зависимостей и запускать скрипты и блокноты в виртуальном окружении. С другой стороны, иногда в новой версии какой-либо библиотеки может появиться функциональность, которая позволит улучшить результаты старого блокнота или скрипта. Например, в scikit-learn (библиотека для машинного обучения) могут добавить имплементацию нового алгоритма, который отлично подходит для моего случая.

Очень часто при разработке какого-то скрипта, я также вынужден устанавливать какие-то дополнительные зависимости, которые требуются только для разработки. Например, так как я использую VSCode для разработки, то он требует, чтобы в виртуальном окружении был установлен pylint. Другие люди, с которыми я сотрудничаю, могут использовать другие инструменты для разработки, которым эта зависимость совершенно не требуется.

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

  1. У меня должна быть возможность разделять центральные зависимости (необходимые для запуска скрипта) и зависимости необходимые только для разработки.
  2. Я хочу указывать центральные зависимости без привязки к конкретной версии библиотеки. Таким образом, я смогу легко обновлять зависимости. С другой стороны, у меня должна быть возможность зафиксировать версии библиотек, чтобы полностью повторить мое виртуальное окружение.
  3. Один и тот же подход должен одинаково хорошо работать и для скриптов и для блокнотов.

Канонический подход по управлению зависимостями, когда создается отдельное виртуальное окружение для проекта и когда версии всех зависимостей фиксируются (используя pip freeze > requirements.txt) в requirements.txt, не работает исходя из моих требований. Во-первых, он не позволяет разделить центральные и зависимости необходимые только для разработки. Во-вторых, в этом случае requirements.txt содержит все зависимости и их подзависимости, поэтому искать версию определенной библиотеки становится проблематичным.

Исходя из этих требований, оптимальным решением казалось использование pipenv (вот, например, статья о том, как использовать этот инструмент). Но когда я начал экспериментировать с ним, я выяснил, что у него есть несколько недостатков. Например, когда я использовал его для установки библиотек, он иногда полностью зависал или работал на редкость медленно. Поэтому после нескольких неудачных попыток, я решил дальше не продолжать с ним, хотя он идеально подходит исходя из моих требований. Poetry же не работает для управления зависимостями для блокнотов.

Решение

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

Для решение этой проблемы, я применяю следующий подход. Во-первых, я разделил центральные и зависимости для разработки. При установке я записываю имена зависимостей в два разных файла: requirements.txt содержит центральные зависимости, в то время как requirements-dev.txt хранит зависимости необходимые для разработки. Чтобы автоматизировать этот процесс, я написал bash функцию pip-install.

pip-install

function pip-install() {     packages=()     dev_dependency=0     requirements_file=     while [ $# -gt 0 ]     do         case "$1" in             -h|--help)                 echo "Usage: pip-install [-d|--dev] [-r|--req <file>] <package1> <package2> ..." 1>&2                 echo ""                 echo "This function installs provided Python packages using pip"                 echo "and adds this dependency to the file listing requirements."                 echo "The name of the package is added to the file without"                 echo "concreate version only if it is absent there."                  echo ""                 echo "-h|--help        - prints this message and exits."                 echo "-d|--dev         - if the dependency is development."                 echo "-r|--req <file>  - in which file write the dependency."                 echo "    If the filename is not provided by default the function"                 echo "    writes this information to requirements.txt or to"                 echo "    requirements-dev.txt if -d parameter is provided."                 echo "<package1> <package2> ..."                 return 0                 ;;             -d|--dev)                 shift                 dev_dependency=1                 ;;             -r|--req)                 shift                 requirements_file="$1"                 echo "Requirements file specified: $requirements_file"                 shift                 ;;             *)                 packages+=( "$1" )                 echo "$1"                 shift                 ;;         esac     done      if ! [ -x "$(command -v pip)" ]; then         echo "Cannot find pip tool. Aborting!"         exit 1     fi      echo "Requirements file: $requirements_file"     echo "Development dependencies: $dev_dependency"     echo "Packages: ${packages[@]}"      if [ -z "$requirements_file" ]; then         if [ $dev_dependency -eq 0 ]; then             requirements_file="requirements.txt"         else             requirements_file="requirements-dev.txt"         fi     fi      for p in "${packages[@]}"     do         echo "Installing package: $p"         pip install $p         if [ $? -eq 0 ]; then             echo "Package installed successfully"             echo "$p" >> $requirements_file             if [ $(grep -Ec "^$p([~=!<>]|$)" "$requirements_file") -eq 0 ]; then                 echo "$p" >> $requirements_file             else                 echo "Package $p is already in $requirements_file"             fi         else             echo "Cannot install package: $p"         fi     done }

Чаще всего, я вызываю эту функцию следующим образом: pip-install scikit-learn или pip-install --dev pylint. В первом случае, эта функция устанавливает пакет scikit-learn используя pip и записывает имя пакета (которое вы написали) в файл requirements.txt. Во втором случае, имя пакета записывается в файл requirements-dev.txt. Стоит отметить, что так как эта функция используется при разработке, то я не указываю версию библиотеки, которую надо установить (в этом случае устанавливается последняя доступная версия). В последующем, в этом файле можно добавить ограничения на версию библиотеки вручную.

Когда мне требуется зафиксировать версии библиотек, я вызываю функцию pip-freeze. pip-freeze выбирает все зависимости из файла requirements.txt, фиксирует их версии и записывает результат в requirements.lock файл. После этого, мы можете легко восстановить свое виртуальное окружение на новой машине используя команду pip install -r requirements.lock. Команда pip-freeze --dev проделывает тот же фокус, только с зависимостями из requirements-dev.txt.

pip-freeze

function pip-freeze() {     dump_all=0     dev_dependency=0     requirements_file=     while [ $# -gt 0 ]     do         case "$1" in             -h|--help)                 echo "Usage: pip-freeze [-a|--all] [-d|--dev] [-r|--req <file>]" 1>&2                 echo ""                 echo "This function freezes only the top-level dependencies listed"                 echo "in the <file> and writes the results to the <file>.lock file."                 echo "Later, the data from this file can be used to install all"                 echo "top-level dependencies."                  echo ""                 echo "-h|--help        - prints this message and exits."                 echo "-d|--dev         - if the dependency is development."                 echo "-a|--all         - if we should freeze all dependencies"                 echo "  (not only top-level)."                 echo "-r|--req <file>  - what file to use to look for the list of"                 echo "    top-level dependencies. The results will be written to"                 echo "    the \"<file>.lock\" file."                  echo "    If the <file> is not provided by default the function"                 echo "    uses \"requirements.txt\" or \"requirements-dev.txt\""                 echo "    if -d parameter is provided and writes the results to the"                 echo "    \"requirements.txt.lock\" or \"requirements-dev.txt.lock\""                 echo "    correspondingly."                 return 0                 ;;             -d|--dev)                 shift                 echo "Development dependency"                 dev_dependency=1                 ;;             -a|--all)                 shift                 dump_all=1                  ;;             -r|--req)                 shift                 requirements_file="$1"                 echo "Requirements file specified: $requirements_file"                 shift                 ;;         esac     done      if ! [ -x "$(command -v pip)" ]; then         echo "Cannot find pip tool. Aborting!"         exit 1     fi      if [ -z "$requirements_file" ]; then         if [ $dev_dependency -eq 0 ]; then             requirements_file="requirements.txt"         else             requirements_file="requirements-dev.txt"         fi     fi      lock_file="$requirements_file.lock"     if [ $dump_all -eq 1 ]      then         pip freeze > "$lock_file"         if [ $? -eq 0 ]; then             echo "Locked all dependencies to: $lock_file"         else             echo "Error happened while locking all dependencies"         fi     else         cmd_output=$(pip freeze -r "$requirements_file")         if [ $? -eq 0 ]; then             > "$lock_file"             while IFS= read -r line; do                 if [ "$line" = "## The following requirements were added by pip freeze:" ]; then                     break                 fi                 echo "$line" >> "$lock_file"             done <<< "$cmd_output"         fi     fi }

Таким образом, в репозитории вместе с проектом я храню 4 файла: requirements.txt, requirements-dev.txt, requirements.lock и requirements-dev.lock.

Исходный код этих двух функций хранится у меня в файле (Всегда проверяйте исходники!!!). Вы можете скопировать его в директорию ~/.bash/, и чтобы сделать эти функции доступными у себя, добавить следующие строчки к себе в .bashrc:

if [ -f ~/.bash/pip_functions.sh ]; then     source ~/.bash/pip_functions.sh fi

Заключение

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

P.S.

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

ссылка на оригинал статьи https://habr.com/ru/post/488782/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *