setuptools – Sam & Max http://sametmax.com Du code, du cul Wed, 23 Dec 2020 13:35:02 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Embarquer un fichier non Python proprement http://sametmax.com/embarquer-un-fichier-non-python-proprement/ http://sametmax.com/embarquer-un-fichier-non-python-proprement/#comments Fri, 12 Feb 2016 12:38:51 +0000 http://sametmax.com/?p=18160 Ah, le packaging Python, c’est toujours pas fun.

Parmi les nombreux problèmes, la question suivante se pose souvent : comment je livre proprement un fichier de données fourni avec mon package Python ?

Le problème est double:

  • Comment s’assurer que setup.py l’inclut bien ?
  • Comment trouver et lire le fichier proprement ?

On pourrait croire qu’un truc aussi basique serait simple, mais non.

Ca ne l’est pas car un package Python peut être livré sous forme de code source, ou d’un binaire, ou d’une archive eggs/whl/pyz… Et en prime il peut être transformé par les packageurs des repos debian, centos, homebrew, etc.

Mais skippy a la solution à tous vos soucis !

Include les fichiers avec votre code

A la racine de votre projet, il faut un fichier MANIFEST.IN bien rempli et include_package_data sur True dans setup.py. On a un article là dessus, ça tombe bien.

N’utilisez pas package_data, ça ne marche pas à tous les coups.

Charger les fichiers dans votre code

Si vous êtes pressés, copiez ça dans un fichier utils.py:

import os
import io

from types import ModuleType


class RessourceError(EnvironmentError):

    def __init__(self, msg, errors):
        super().__init__(msg)
        self.errors = errors


def binary_resource_stream(resource, locations):
    """ Return a resource from this path or package """

    # convert
    errors = []

    # If location is a string or a module, we wrap it in a tuple
    if isinstance(locations, (str, ModuleType)):
        locations = (locations,)

    for location in locations:

        # Assume location is a module and try to load it using pkg_resource
        try:
            import pkg_resources
            module_name = getattr(location, '__name__', location)
            return pkg_resources.resource_stream(module_name, resource)
        except (ImportError, EnvironmentError) as e:
            errors.append(e)

            # Falling back to load it from path.
            try:
                # location is a module
                module_path = __import__(location).__file__
                parent_dir = os.path_dirname(module_path)
            except (AttributeError, ImportError):
                # location is a dir path
                parent_dir = os.path.realpath(location)

            # Now we got a resource full path. Just open it as a regular file.
            canonical_path = os.path.join(parent_dir, resource)
            try:
                return open(os.path.join(canonical_path), mode="rb")
            except EnvironmentError as e:
                errors.append(e)

    msg = ('Unable to find resource "%s" in "%s". '
           'Inspect RessourceError.errors for list of encountered erros.')
    raise RessourceError(msg % (resource, locations), errors)


def text_resource_stream(path, locations, encoding="utf8", errors=None,
                    newline=None, line_buffering=False):
    """ Return a resource from this path or package. Transparently decode the stream. """
    stream = binary_resource_stream(path, locations)
    return io.TextIOWrapper(stream, encoding, errors, newline, line_buffering)


Et faites:

from utils import binary_resource_stream, text_resource_stream 
data_stream = binary_resource_stream('./chemin/relatif', package) # pour du binaire 
data = data_stream.read() 
txt_stream = text_resource_stream('./chemin/relatif', package) # pour du texte text = txt_stream.read()

Par exemple:

image_data = binary_resource_stream('./static/img/logo.png', "super_package").read() 
text = text_resource_stream('./config/default.ini', "super_package", encoding="cp850").read()

Si vous n’êtes pas pressés, voilà toute l’histoire…

A la base, on fait généralement un truc du genre:

PROJECT_DIR = os.path.dirname(os.path.realpath(__file__)) # ou pathlib/path.py 
data = open(os.path.join(PROJECT_DIR, chemin_relatif)).read())

Mais ça ne marche pas dans le cas d’une installation binaire, zippée, trafiquée, en plein questionnement existentiel après avoir regardé un film des frères Cohen, etc.

La manière la plus sûre de le faire est:

import pkg_resources 
data = pkg_resources.resource_stream('nom_de_votre_module', chemin_relatif).read()

Ça va marcher tant que votre package est importable. On se fout d’où il est, de sa forme, Python se démerde.

Mais ça pose plusieurs autres problèmes:

  • pkg_resources fait parti de setuptools, qui n’est pas forcément installé sur votre système. En effet, malgré l’existence de ensure_pip depuis la 3.4, beaucoup de distributions n’installent ni pip, ni setuptools par défaut et en font des paquets séparés.
  • data sera forcément de type bytes. Il faut le décoder manuellement derrière.
  • si votre code doit fonctionner aussi en dehors du cadre d’un package importable (genre on unzip et ça juste marche), pkg_resources ne fonctionnera pas puisque par essence il utilise le nom du package pour trouver la ressource.
  • si vous voulez spécifier plusieurs endroits ou potentiellement trouver la ressource, ou passer l’objet module à la place du nom du module, ça ne marche pas.
  • tous ces cas, bien entendu, lèvent des exceptions différentes histoire de faciliter votre try/except.
  • Il existe pkgutil qui est bien installé partout, mais ça load tout en mémoire d’un coup. Si vous avez un XML de 10Mo à charger ça fait mal au cul.
  • la doc de pkg_resource est aussi claire que l’anus de la mère du premier commentateur de cet article. On va voir qui lit l’article en entier là…

Le snippet fourni corrige ces problèmes en gérant les cas tordus pour vous. Moi je l’utilise souvent en faisant:

with text_resource_stream('./chemin/relatif', ['package_name', 'chemin_vers_au_cas_ou_c_est_pas_un_package']) as f:
     print('boom !', f.read())

Je devrais peut-être rajouter ça dans batbelt et minibelt…

Travailler avec des fichiers non Python

Ça, c’est pour lire les ressources fournies. Mais si vous devez écrire des trucs, il vous faut un dossier de travail.

Si c’est pour un boulot jetable, faites-le dans un dossier temporaire. Le module tempfile est votre ami.

Si c’est pour un boulot persistant, trouvez le dossier approprié à votre (fichier de config, fichier de log, etc) et travaillez dedans. path.py a des méthodes dédiées à cela (un des nombreuses raisons qui rendent ce module meilleur que pathlib).

]]>
http://sametmax.com/embarquer-un-fichier-non-python-proprement/feed/ 8 18160
Wheel installs require setuptools >= 0.8 for dist-info support. http://sametmax.com/wheel-installs-require-setuptools-0-8-for-dist-info-support/ http://sametmax.com/wheel-installs-require-setuptools-0-8-for-dist-info-support/#comments Wed, 22 Jan 2014 19:24:51 +0000 http://sametmax.com/?p=8745 Je ne sais pas pourquoi, mais une fois j’ai eu cette erreur en essayant d’installer un truc avec pip.

La solution est de lancer :

sudo pip install setuptools --no-use-wheel --upgrade

Et on peut réutiliser pip sans problème.

Zarb.

]]>
http://sametmax.com/wheel-installs-require-setuptools-0-8-for-dist-info-support/feed/ 2 8745
Votre Python aime les pip http://sametmax.com/votre-python-aime-les-pip/ http://sametmax.com/votre-python-aime-les-pip/#comments Sat, 08 Sep 2012 13:42:27 +0000 http://sametmax.com/?p=2014 Pip install par-ci, pip install par là. "Pour installer cette lib, il vous suffit de faire pip install". Mais merde, c'est quoi pip ?]]> A partir des versions 2.7.9 et et 3.4, pip est fournit automatiquement avec Python. Si c’est votre cas, vous pouvez sauter la partie installation et aller directement à la partie usage de cet article.

Pip install par-ci, pip install par là. “Pour installer cette lib, il vous suffit de faire pip install”.

Mais merde, c’est quoi pip ?

Python et les libs externes

La beauté avec Python, c’est qu’on peut prendre une lib, la balancer dans le répertoire courant, et l’importer. Rien à faire, ça marche tout seul.

Mais.

Car oui, il y a toujours un mais (souvent après le mois de mars).

Quand il faut mettre à jour ses libs, c’est chiant. Quand il faut recréer un environnement complet en prod, c’est chiant. Quand il faut trouver la dernière version, avec le bon site Web, et le lien de téléchargement, c’est chiant. Quand on veut installer une lib automatiquement, vous l’avez deviné, c’est chiant.

Mais surtout, quand on a une lib qui a des parties en C à compiler comme les libs de crypto, d’accès à la base de données, de traitement XML, de parsing ou de sérialisation, de calculs scientifiques, etc. ça ne marche tout simplement pas.

Là, il y a deux écoles. Les mecs qui utilisent les packages *.exe ou *.deb precompilés et qui se retrouvent avec d’autres problèmes.

Et les mecs qui utilisent setuptools (et qui se retrouvent avec encore d’autres problèmes, mais c’est mieux parce que je le dis).

Setuptools, la solution de skippy à tous vos soucis

setuptools est une lib et un outil de distribution de code Python, et elle permet notament d’installer des libs externes automatiquement en les téléchargeant depuis le grand InterBoule.

Comme gem install sous ruby, npm pour NodeJs, Python possède sa commande d’installation appelée easy_install, qui va piocher dans le site Web pypi.python.org, un équivalent de Cpan de Perl ou des channels pear pour PHP.

Bien entendu, ça serait trop simple sinon, Python ne vient pas par défaut installé avec cet outil pourtant indispensable.

Sous Ubuntu c’est facile:

sudo apt-get install python-setuptools

Et il existe un paquet similaire sur toutes les distribs.

Sous windows, c’est un exe à installer. Choisissez la bonne version selon votre version de Python (python --version). Généralement, la 2.7.

Sous mac, il faut télécharger l’egg correspondant à sa version puis:

sh setuptools-votre_version.egg --prefix=~

Et vous avez alors accès à la commande magique, easy_install. Par exemple, pour installer Django:

easy_install --user django

(ou easy_install django en mode administrateur – ou avec sudo – pour l’installer pour tout le système et pas juste l’utilisateur courant)

Il faut bien entendu être connecté à internet pour que ça marche. Notez par ailleurs que la recherche est insensible à la casse, et que tout underscore sera automatiquement remplacé par un tiret.

Sous Windows, si la commande n’est pas trouvée, il vous faudra rajouter le répertoire qui contient la commande easy_install (le plus souvent C:\Python27\Scripts) à la main dans le PATH.

Vous avez easy_install ? Oubliez-le !

Vous n’utiliserez normalement easy_install que pour une seule chose: installer pip.

easy_install --user pip

pip est un easy_install en plus court à taper, plus puissant, plus souple, plus mieux.

Il s’utilise comme easy_install :

pip install --user bottle

(ou sudo pip install bottle, ou en root, mais je sauterai ce genre de détails pour la suite)

Grâce à pip, vous allez avoir accès à des milliers de libs Python en quelques frappes, et donc pouvoir beaucoup plus facilement expérimenter avec des nouveaux bidules.

Mais pip va plus loin qu’easy_install, et permet carrément de gérer toutes les dépendances de son projet.

Ce que fait pip que easy_install ne fait pas

Déjà, pip télécharge toutes les dépendances avant de lancer l’installation, ce qui évite une installation partielle.

Mais surtout, pip ajoute des opérations qu’on pourrait attendre de base, notamment, la désinstallation :-)

pip uninstall bottle

Évidement, on peut choisir d’installer une version particulière:

pip install bottle==0.9

Et mettre à jour une lib à la dernière version :

pip install bottle --upgrade

Ou downgrader vers une version précédente:

pip install bottle==0.9 --upgrade

Cependant, le plus intéressant avec pip, c’est sa capacité à recréer tout un environnement de développement en une commande:

$ pip freeze
Axiom==0.6.0
BeautifulSoup==3.2.0
Brlapi==0.5.6
BzrTools==2.5.0
CherryPy==3.2.2
Coherence==0.6.6.2
[...]

pip freeze liste toutes les librairies installées et leurs versions. On peut ensuite les réinstaller sur une autre machine facilement:

pip freeze > requirements.txt

Et sur l’autre machine:

pip install -r requirements.txt

On est garanti d’avoir les mêmes libs, avec les mêmes versions. Ca marche de la même manière sous tous les OS, et comme des milliers de libs ne sont pas disponibles en paquets *.deb, *.rpm ou *.exe, c’est extrêmement pratique.

Si vous vous attendez à être privé de connexion internet ou juste pour accélérer le procédé, pip permet de créer un bundle, c’est à dire un gros zip qui contient toutes les dépendances et qui peut être installé indépendamment :

pip bundle nom_du_projet.pybundle -r requirements.txt

Et sur l’autre machine:

pip install nom_du_projet.pybundle

Quelques astuces pour être productif avec pip

Si vous installez beaucoup de fois les mêmes paquets, par exemple dans de nombreux virtualenvs j’installe toujours ipython + ipdb + requests + clize + peewee, les télécharger à chaque fois est une perte de temps. On peut demander à pip de mettre en cache les paquets déjà installés, pour de futures installations:

export PIP_DOWNLOAD_CACHE='~/.pip/cache'; dans le .bashrc

Pour les windowsiens, je ne sais pas du tout où mettre ça…

Sachez cependant que le cache ne dispense pas d’être connecté à Internet.

Si une bibliothèque n’est pas encore sur pypi, pip est capable, pourvu que le code source vienne avec un fichier setup.py, de l’installer depuis un fichier local, une URL, ou même un simple repo git. Donc si vous trouvez une lib chouette sur github, on peut l’installer ainsi:

pip install git+git://github.com/sametmax/0bin.git

Et elle sera même référencée par le pip freeze !

Remarquez qu’il faut avoir Git installé, et que le protocole doit être changé pour git+git.

Parfois, pypi est indisponible: un problème sur le site, un problème de dns, un problème de votre FAI… Dans ce cas là on est un peu désemparé. Heureusement, pip permet aussi de faire un fallback sur des miroirs:

pip install --use-mirrors requests

Dans ce cas pip essayera de trouver le paquet sur une liste de sites miroirs de pypi qu’il possède en interne. Il va les essayer un par un jusqu’à en trouver un de disponible.

Dans le cas les plus extrêmes, certains vont jusqu’à créer leur propre miroir en local. Mais c’est un article à part entière.

Installer des bibliothèques avec des extensions en C

Les bibliothèques avec des extensions en C sont très courantes dans les domaines qui ont besoin de performances: drivers de base de données (mysql-python), calculs scientifiques (numpy), parsing (lxml), cryptographie (pycrypto), etc.

Pip ne fonctionnera que partiellement pour installer ces libs: il devra en effet lancer la compilation de l’extension. Bien que le processus soit automatique et bien fait, il arrive souvent qu’il manque des dépendances.

Pour ce genre de lib, soit vous utilisez un paquet tout fait (*.deb, *.exe, *.rpm, etc.), soit vous tentez d’installer les dépendances.

La première chose à faire, c’est installer les headers de Python. Sous Ubuntu, c’est:

sudo apt-get install python-dev

(et sous CentOs, c’est un yum install python-devel. Pour Mac, aucune idée.)

Ensuite, on lance l’installation une fois de la lib, et on voit ce qui plante, pour installer les dépendances en conséquence. Par exemple, lxml a besoin des headers de libxml2 et libxslt1. Donc on va installer libxml2-dev et libxslt1-dev. Pour python-mysql, il va falloir installer libmysqlclient-dev.

Quand on est une grosse feignasse, mais que l’on a quand même envie d’installer la lib avec pip (typiquement, dans le cas d’un virtualenv). on peut faire le compromis de l’installer sur le système avec un paquet, et ensuite de l’installer avec pip. Ca évite de faire la chasse aux dépendances, mais ça fait double installation.

Enfin, il y a des libs qui ne s’installeront jamais avec pip. Par exemple, un mastodonte genre PyQT. Oubliez, c’est tout.

]]>
http://sametmax.com/votre-python-aime-les-pip/feed/ 35 2014