А как вам такой вариант управления зависимостями в 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 окружение, то я написал очень длинную статью (на английском) по этой теме у себя в блоге.

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 8

      +2

      А как же разделение вида requirements.prod.txt и requirements.dev.txt? Если зашивать версии зависимостей для вас не критично, то достаточно в этих файлах определить зависимости первого уровня.


      Poetry же не работает для управления зависимостями для блокнотов.

      Расскажите пожалуйста поподробнее, в чём проблема с Poetry?

        +5

        Так и не понял, чем не устроил Poetry. У вас в тексте всего 2 упоминания о нём:


        проштудировал документацию к poetry
        Poetry же не работает для управления зависимостями для блокнотов.

        Poetry на текущий момент самый современный и продвинутый инструмент (не считая dephell, но это скорее meta packaging tool) для управления рабочим окружением и зависимостями. В нём как минимум есть умный ресолвер зависимостей (чего нет ни в pip, ни в pipenv), оно позволяет управлять окружениями, dev и extra зависимостями, а также лочить их. Также оно использует современный формат конфигурационного файла pyproject.toml и lock-файл для фиксации версий зависимостей. Зависимости можно обновлять, используя команду update, и это не сломает вам окружение из-за несовместимости версий, потмоу что за этим следит ресолвер.


        А requirements-файлы (и любые инструменты на их основе) — это костыли из трудного прошлого. Не стоит оно того, чтобы тратить на это своё время и силы, всё равно рано или поздно отомрёт.

          0
          Когда увидел зависимости Питона я немного удивился (в основном на Node.js пишу)… а потом понял кто придумал Докер, это ведь идеально для зависимостей Питона)
            +4
            Спасибо BasicWolf и iroln! После ваших коментариев, я решил ещё раз более внимательно посмотреть на poetry. После того, как я прочитал прошлый раз его документацию, я думал, что у меня не получится использовать poetry для того, чтобы управлять зависимостями в Jupyter Notebook проектах. Более того, я думал, что poetry умеет хранить файлы виртуального окружения только в отдельной папке.

            Но сегодня, после ваших коментариев, я проштудировал ещё раз документацию, а также создал виртуальную машину, где проверил все мои сценарии. Оказалось, что действительно poetry можно использовать исходя из моих требований. Спасибо вам огромное за то, что указали ещё раз посмотреть в сторону этого инструмента!

            Собираюсь сейчас написать для себя заметку в блоге, как использовать этот инструмент исходя из моих требований. Как думаете, стоит ли об этом написать на хабре?
              0

              Всегда стоит, ведь чем точнее (и специфичнее) кейсы использования — тем понятнее как владеть инструментом.

                +1
                Вот, опубликовал у себя в блоге пока: ссылка
              +1
              создать директорию requirements, в ней файлы base.txt, dev.txt, test.txt, prod.txt. в base.txt прописать общие для всех зависимости, в остальных первой строкой -r base.txt, а дальше специфичные зависимости. для замороженных можно продублировать с указанием версий.
                0
                Можно посмотреть, например, на pip-tools

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

                Самое читаемое