Script Python qui installe ses dépendances


Quand on a un petit script, on ne veut pas forcément dire aux gens avec lesquels on le partage qu’il faut pip installer toutes les dépendances, ou faire un setup.py complet.

On peut évidemment se tourner vers des solutions de compilation type nuitka, mais si on veut être KISS, on peut juste utiliser pip depuis le code Python:

import pip, sys
 
# On essaye d'importer les dépendances, et si elles sont pas dispos on les installe 
# pour l'utilisateur courant.
def require(*packages):
    for package in packages:
        try:
            if not isinstance(package, str):
                import_name, install_name = package
            else:
                import_name = install_name = package
            __import__(import_name)
        except ImportError:
 
            cmd = ['install', install_name]
            if not hasattr(sys, 'real_prefix'):
                cmd.append('--user')
            pip.main(cmd)
 
require('arrow', 'requests', 'minibelt', ('path', 'path.py'))
 
# Et votre script peut joyeusement utiliser ses dépendances
import arrow
import requests
 
from path import Path
from minibelt import window
 
print(Path('.').realpath())
print(arrow.get(), len(list(window(requests.get('http://sametmax.com')))))

Bien entendu, ça marche que pour les dépendances en pur Python, mais ça fait déjà un paquet de trucs.

N’oubliez pas qu’on reste dans une solution quick and dirty, ne commencez pas à me mettre ça partout.

25 thoughts on “Script Python qui installe ses dépendances

  • Sébastien

    Bonjour,

    pourquoi ne pas essayer de faire intégrer cette fonction “require” dans le code de pip ?

    Il serait alors possible de faire simplement

    from pip import require

    require('arrow', 'requests', 'minibelt', 'path.py')

    import arrow

    import requests

    from path import Path

    from minibelt import window

    print(Path('.').realpath())

    print(arrow.get(), len(list(window(requests.get('http://sametmax.com')))))

    bien sûr ça ne fonctionnerait que pour ceux qui auraient la version de pip qui possède cette fonction…

    mais au bout d’un moment, on peut espérer que la plupart des utilisateurs l’auront.

    Merci pour cette astuce

  • Yves D

    Là ce matin, je suis en train d’installer un script avec qlqs dépendances ….. Bref c’était juste ce dont j’avais besoin: malin (surtout le import() )

    Today is a good day !!

    Merci

  • entwanne

    Bonjour,

    Pourquoi utiliser la fonction __import__ plutôt qu’import_module du module importlib ?

    Sinon, ça ne fonctionnera pas pour les modules dont le nom du paquet diffère (pour path.py, le pip install sera réexécuté à chaque fois, vu que le module path.py n’existe pas).

    Il me semble aussi que vous aviez déjà parlé de cette fonctionnalité dans un article, mais je ne retrouve pas.

  • Dylann

    pourquoi ne pas essayer de faire intégrer cette fonction “require” dans le code de pip ?

    Pip permet de gérer les installations assez proprement : on sait que si on installe le paquet “toto”, il va installer les dépendances de ce paquet. Il suffit d’aller voir le setup.py de ce paquet pour connaître ses dépendances (bon on peut suivre les miettes de pains de paquet en paquet assez longtemps pour avoir la liste complète… un pip dependencies --list mon_paquet serait bien pratique…)

    Avoir une telle fonctionnalité authoriserait les devs à mettre leurs dépendances n’importe où dans leur code : ça rend le “paquet” bien moins lisible pour celui qui souhaiterait l’utiliser.

    La solution proposée est pratique quand on fait un “QuickAndDirty” qui n’a pas vocation a être releasé, mais ce n’est pas “propre” — attention, je n’ai pas dit que c’était mal le “QuickAndDirty” ;-) –. IMHO, ça ne serait pas cohérent que pip mette en natif une telle fonction dans leur module. (python n’est pas java :-p ok, c’était facile et Trollesque, mais qu’est-ce que c’est bon ^_^, comme le QuickAndDirty :-D)

  • Sam Post author

    @entwanne : par flemme de rajouter un import. Bien vu pour path.py, j’ai update le code.

    @Sébastien : c’est actuellement suggéré sur la mailling list et en débat. Je t’invite donc à faire entendre ta voix pour qu’un vote soit ajouté au “pour”.

  • Tryph

    cmd = [‘install’, install] -> cmd = [‘install’, install_name]

  • Seb

    Et pour les pauvres utilisateurs windows qui auraient malgré tout (mini)conda installé, la même chose mais qui vérifierait que l’environnement défini dans le environment.yml est bien activé et sinon, qui a) le crée (si pas encore créer) ou le met à jour et b) fork le process dans ce nouvel environnemnt ?

    Genre:

    def check_or_create_update():

    # lit le fichier environment.yml pour récupérer le nom de l'env

    # vérifie si le script est appelé dans cet environment

    # si oui, on sort pour continuer l'exécution du script

    # si non, on vérifie si l'environment existe

    # si non, on le crée

    # on forke un nouveau process pour le script dans le nouvel environnement

    # et à la sortie, on arrête le process du script original

    ...

    check_or_create_update()

    ...

    Cela évite le “tu dois d’abord lancer un ‘conda env create’ et puis un ‘activate nom_de_lenvironnement’ et finalement ‘python mon_script.py’.

  • Sam Post author

    Je n’utilise que rarement conda. Mais si tu le code, partage le :)

  • Sam Post author

    Non tu vas pas installer une dependance pour installer des dépendances, ça retire tout l’interêt du truc.

  • Mat

    S’il vous plait, ne faites surtout pas ça !!!

    Ne commencez pas à faire des scripts qui installent tout et n’importe quoi dans le dos des utilisateurs.

    Si je lance un script je ne veux surtout pas qu’il installe des trucs par défaut sans me le dire.

    Ça va juste pourrir ma machine en installant des trucs pas comme il faut, au pif, je peux vouloir :

    – installer en global

    – installer dans un venv (et –user ne marche pas dans un venv)

    – installer les paquets de ma distrib

    – ne pas lancer le script finalement

    – …

    Sérieusement, l’écosystème python est relativement propre par rapport à d’autre (que je ne nommerait pas), ne pourrissez pas tout :)

  • Sam Post author

    Je crois que tu n’as as bien compris l’usage. Le but n’est pas d’en faire une méthode de shipping de code généralisée, le but est de pouvoir envoyer par mail un script quick and dirty a son collègue non dev sans avoir à lui expliquer comme utiliser pip.

  • Dylann

    @Sam : ouais mais si ton collègue est capable de faire un python script.py, n’est-il pas capable également de faire un pip install -r script.requirements la première fois ?

  • Dylann

    PS (désolé du double post). D’autant que si c’est un collègue “non dev” / “pas habitué” et qu’il rencontre un problème lors de l’installation du script à cause des droits, il y a de forte sens qu’il fasse un sudo python yolooo.py TT_TT

  • Sam Post author

    @Sam : ouais mais si ton collègue est capable de faire un python script.py, n’est-il pas capable également de faire un pip install -r script.requirements la première fois ?

    Déjà il va cliquer sur le script, pas le lancer depuis le terminal, ensuite meme si il le lançait depuis un terminal, ça voudrait dire que je lui envoie un requirements.txt en plus, et je lui explique pip. Attend il faut qu’il tape le -r. Et le –user. Au bon endroit. Et que je sois là pour lui dire de le faire ou au tel. Et que j’ai le temps.

  • Sam Post author

    Tout le contenu du site est sous creative comon donc oui.

  • Frodon

    La 1ère fois que le script est exécuté j’ai une exception :

    ImportError: No module named arrow

    Pas de problème quand je le lance la 2ème fois.

    Pour que ça marche du 1er coup il faut dupliquer la ligne

    __import__(import_name)

    après

    pip.main(cmd)

  • bajazet

    Il y a la PEP 508 qui redéfinit le requirements.txt en Python, les deps de dev, la gestion du système etc.

    https://www.python.org/dev/peps/pep-0508

    https://github.com/pypa/pipfile

    Example (depuis github)

    source('https://pypi.org/', verify_ssl=True)

    requires_python('2.7')

    requires_platform('Windows')

    package('requests' extras=['socks'])

    package('Django', '>1.10')

    package('pinax', git='git://github.com/pinax/pinax.git', ref='1.4', editable=True)

    dev_package('nose')

  • Sam Post author

    Interessant. Mais bon sans pourquoi ne pas foutre tout ça dans setup.cfg ?

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.