Comment aimez-vous cette option de gestion des dépendances en Python?

J'ai récemment décidé qu'il était temps de régler enfin le sujet de la gestion des dépendances dans mes projets Python et j'ai commencé à chercher une solution qui me conviendrait parfaitement. J'ai expérimenté avec pipenv, étudié la documentation de poésie, lu d'autres articles connexes. Malheureusement, je n'ai jamais trouvé la solution parfaite. Du coup, j'ai inventé un nouveau vélo, ma démarche, que je propose de discuter sous la coupe.


Problème


Mais avant de passer directement à la description de la démarche, je voudrais expliquer pourquoi un tel besoin est apparu et pourquoi les solutions existantes ne me convenaient pas.


Dans le cadre de mon travail, j'utilise le plus souvent Python à deux fins: c'est soit l'analyse des données et l'apprentissage automatique à l'aide des cahiers Jupyter, soit de petits scripts Python qui préparent les données en quelque sorte. Je tiens à noter que je ne suis pas impliqué dans la création de packages et leur publication dans Pypi (par conséquent, je ne me concentre pas sur ce processus dans cet article).


Très souvent, j'ai besoin d'exécuter des scripts ou des blocs-notes créés il y a longtemps. Par conséquent, j'avais besoin de corriger les versions de dépendance et d'exécuter des scripts et des blocs-notes dans un environnement virtuel. D'un autre côté, parfois dans une nouvelle version d'une bibliothèque, des fonctionnalités peuvent apparaître qui amélioreront les résultats d'un ancien bloc-notes ou script. Par exemple, dans scikit-learn (une bibliothèque pour l'apprentissage automatique), ils peuvent ajouter une implémentation d'un nouvel algorithme qui fonctionne très bien pour mon cas.


Très souvent, lors du développement d'un script, je dois également installer des dépendances supplémentaires qui ne sont requises que pour le développement. Par exemple, puisque j'utilise VSCode pour le développement, il nécessite que pylint soit installé dans l'environnement virtuel. Les autres personnes avec lesquelles je travaille peuvent utiliser d'autres outils de développement qui n'ont pas du tout besoin de cette dépendance.


Sur la base de ces hypothèses, j'ai développé les exigences suivantes pour la gestion des dépendances:


  1. Je devrais être capable de séparer les dépendances centrales (nécessaires pour exécuter le script) et les dépendances nécessaires uniquement pour le développement.
  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.


Le code source de ces deux fonctions est stocké dans mon fichier (vérifiez toujours la source !!!). Vous pouvez le copier dans un répertoire ~/.bash/et pour rendre ces fonctions disponibles à la maison, ajoutez-vous les lignes suivantes dans .bashrc:


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

Conclusion


Je viens de commencer à utiliser cette solution et je n'ai peut-être pas encore découvert de défauts. D'une manière générale, ce billet a été conçu pour discuter de cette approche et savoir s'il a droit à la vie. Si vous voyez des problèmes, je serai très heureux d'en entendre parler.


PS

, Python , ( ) .


All Articles