packaging – 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 Vive setup.cfg (et mort à pyproject.toml) ! http://sametmax.com/vive-setup-cfg-et-mort-a-pyproject-toml/ Thu, 06 Dec 2018 10:07:48 +0000 http://sametmax.com/?p=25060 Après un long débat sur hackernews, qui n’est que le reflet de toutes les conversations que j’ai déjà eues à ce sujet sur twitter, github, et divers mailling lists, il est grand temps de faire un article. Urgent même.

Est-ce que vous savez quel chemin de croix on a vécu avec le packaging Python durant ces 15 dernières années ?

D’abord on a distutils, setuptools, distribute, and distribute2 qui ont tous été à un moment les “standards” recommandés pour packager une lib. Ensuite on a eu l’époque des eggs, exe, et autres trucs que easy_install allait chercher n’importe où dans la nature en suivant aveuglément des liens sur PyPi. Sans compter les machins qu’il fallait compiler à tout bout de champ. Et puis rien n’était chiffré au download, pip n’était pas packagé avec Python, il crevait sur des erreurs stupides type encodage mal géré…

À ça se rajoute que virtualenv était un truc à part, avec plein de concurrents, et linkait les packages système par défaut. Sans oublier qu’on avait pas Python -m.

Bref, le packaging Python, ça a été vraiment la merde. Avec en plus une doc de merde.

Aujourd’hui, le standard wheel a énormément amélioré la donne. On a des tutos corrects (ex: notre tuto sur comment créer son package avec setup.py). ensurepip fait qu’on a une version récente du truc presque partout.

En gros, notre situation est stable, saine. Améliorable de bien des façons, certes, mais un bon socle sur lequel s’appuyer.

Arrive setup.cfg

En 2016, l’équipe de setuptools, la lib utilisée par à peu près tout le monde pour créer des packages en Python aujourd’hui, a créé le format setup.cfg, un fichier INI dont le but est de remplacer setup.py.

Le problème de setup.py c’est que c’est du code Python exécutable, et en plus dépendant de la lib setuptools. Cela empêche non seulement l’interfaçage avec des outils externes, mais aussi freine l’émergence de nouveaux outils.

En effet, il y a un désir de continuer à améliorer la situation du packaging en Python, comme on peut le voir avec des projets comme pipenv ou poetry (je recommande d’ailleurs fortement ce dernier, et je vous invite à voter sur cette issue pour enfoncer le clou).

setup.cfg est la suite logique de tout ça, un format en texte brut, facile à manipuler pour le reste du monde.

Et ça a été bien fait:

  • setup.cfg marche out of the box. Il n’y a rien à faire de particulier : si vous aviez un setup.py, vous pouvez juste le convertir en setup.cfg. Si vous avez un nouveau projet, vous pouvez juste écrire le setup.cfg. Il n’y a pas de piège. Ça marche depuis… 2016. Ouais.
  • setup.cfg a une documentation décente. Si, si. C’est très améliorable, mais mieux que tout ce qu’on a jamais eu pour les premières années de setup.py
  • setup.cfg couvre la plupart des cas de figure que setup.py faisait et se permet de rajouter des goodies très cools. version = attr: src.__version__ inclue la version depuis __init__.py, "options.data_files" remplace le MANIFEST et "license = file: LICENCE.txt" injecte le corps de la licence depuis un fichier texte. Tous les hacks à la noix de setup.py ? Fini.
  • C’est simple à utiliser. Voici quelques projets pour démontrer le bouzin: exemple 1, exemple 2, exemple 3, exemple 4. Ça se comprend même sans lire la doc !
  • C’est compatible avec tous les outils legacy. En fait, il suffit de créer un fichier setup.py avec une seule ligne dedans (import setuptools; setuptools.setup()) et pouf, tout le worflow d’avant marche: python setup.py develop, python setup.py sdist upload, etc. Du coup, tout ce qui comprenanait setup.py (c’est à dire le putain de monde entier en Python) marche avec setup.cfg.

Donc setup.cfg, malgré des lacunes, c’est sympa.

Setup.cfg, ou es-tu ?

“Attend une minute, si c’est si bien que ça, pourquoi j’en ai jamais entendu parler ? Moi on m’a dit que setup.cfg il servait à rien…”

Je ne vous le fais pas dire !

Je n’ai aucune idée de la raison pour laquelle cette information ne circule pas plus. La seule raison pour laquelle je l’ai trouvée c’est parce que je passe des heures à faire de la veille informationnelle en Python, et que je suis tombé dessus au détour d’un carrefour, dans une ruelle sombre du Web. Et que j’ai pris le temps et le risque de le tester sur un de mes projets pour vérifier que oui, ça marche comme prévu.

Même la doc Python vous fait croire que setup.cfg c’est un artéfact vaguement utile pour une pauvre option monoligne.

Et merde, j’ai déjà écrit 900 articles sur ce blog, je ne peux pas mettre à ma charge d’avertir tout le monde pour chaque truc à savoir sur Python.

C’est comme pour le # -*- coding: utf-8 -*-. Un jour un mec utilisant Emacs a écrit un tuto avec ça, et tout le monde a copié-collé les hiéroglyphes alors qu’en fait # coding: utf8 est parfaitement valide. Ou comme nuikta, qui permet de compiler de Python de manière fiable, et que personne ne connait. Combien de temps doit-on gâcher avec des trucs comme ça ?

pyproject.toml vient foutre le bordel

Maintenant, figurez-vous que nous avons un groupe de personnes chargées de faire évoluer la situation du packaging, la Python Packaging Authority, ou PyPA.

Une très bonne initiative, vu que le free-for-all du passé ne nous avait pas trop réussi. Et un succès puisque la situation actuelle en packaging Python est maintenant beaucoup plus propre.

Et on en a besoin. En effet, setup.cfg doit être amélioré car il a certains défauts. Le format n’est pas parfaitement standardisé. Seule la doc fait figure de description, et pour l’instant setup.cfg, c’est whatever configparser comprends, sachant que configparser parse les trucs au motoculteur, et selon la version de Python. C’est pas gravissime, et ça n’a pas vraiment été un problème jusque là, mais pour la pérennité de la chose, une consolidation est nécessaire. On doit avoir une spec solide, un parseur robuste et stable, etc.

Sachant la purge qu’a été l’historique du packaging en Python, on s’attend donc a ce que la PyPA fasse le choix de standardiser setup.cfg, qui marche depuis 2 ans, fait le job, est compatible avec l’existant et résout déjà le problème de permettre au reste du monde de manipuler les données du package en texte brut. Puis, comme un bon groupe de décision sage, elle va proposer des solutions incrémentales aux défauts de setup.cfg et son écosystème.

Un exemple possible serait de s’allier avec l’équipe de setuptools pour figer le format setup.cfg, clarifier les edge cases de ce INI en particulier, et si besoin, créer une alternative a configparser (ou figer configparser) afin d’obtenir une implémentation de référence irréprochable pour parser le format. Puis désigner ce format comme le nouveau standard, en version 1 implicite, et le documenter puis en faire la promotion afin que tous les nouveaux outils puissent l’utiliser. Ensuite, ayant identifié des limitations, on crée le successeur de ce format, qui aura le même nom, mais un header de version qui lui sera explicite afin de permettre aux parseurs de s’adapter. Et on fait en sorte que cette nouvelle version soit plus propre, plus belle, super green. Et on l’introduit aussi progressivement et en douceur qu’un sex toy anal.

Bref, on s’attend à ce que la PyPA nous amène vers une totale liberté de pensée cosmique vers un nouvel âge réminiscence.

Sauf que non.

Ces ânes ont décidé… de créer un nouveau format et ignorer tout l’existant.

Fuck. That. Je hais que ce dessin de XKCD puisse être d’actualité encore et encore, chaque mois que l’humanité passe:
Le packaging en Python, c'est une forme de solidarité masochiste avec la communauté JS

Je ne comprends pas cette décision, et leurs justifications sont d’une grande faiblesse, pour ne pas dire insultante pour une communauté qui en a marre de payer le prix de l’égo des gens qui ont envie d’avoir leur nom sur la nouvelle barre de fer officielle.

C’est d’autant plus étrange que la PyPA n’est pas composée de cons. Non. On a Brett Cannon (core dev, qui fait un travail exceptionnel avec Python VSCode), Nathaniel Smith (qui nous a révolutionné l’async avec trio) et Kenneth Reitz (l’auteur de Python requests).

Comment ce pet de cerveau a-t-il pu émané de ces brillantes personnes, je ne le sais.

Mais je vous invite tous à non seulement utiliser setup.cfg en masse, mais aussi à activement contester cette décision sur tous les mediums à votre disposition.

Parce qu’évidemment je l’ai faits, et comme d’hab, ils font la sourde oreille. Et on va en payer le prix. Mais bon, depuis le temps, vous avez l’habitude. C’est pas comme si j’avais pas déjà annoncé que redux et dockers étaient overkill pour la plupart des projets, que le NoSQL allait avoir un retour de baton, que vue était génial, flask PAS pour les débutants, bitcoin intéressant, pipenv –three stupide, python parfait pour l’enseignement… des années avant. Pour les consultations de boule de cristal, c’est uniquement le mardi à 15h.

N’est-ce pas trop tard ?

Au contraire, c’est exactement le bon moment. Le format le plus utilisé actuellement n’est ni le setup.cfg, ni le pyproject.toml, mais toujours le bon vieux setup.py. En fait, n’en déplaise aux defenseurs de pyproject.toml qui veulent nous faire croire que le projet est bien plus populaire et avancé qu’il ne l’est vraiment, il vaut l’équivalent d’un draft de proposal en beta testing. Une simple recherche github retourne:

De toute façon la transition sera longue, et les outils commencent à peine à supporter le nouveau format. En fait, ils ne sont même pas d’accord sur comment l’utiliser. On n’a pas de standard pour le lock file (pipfile semble se dégager, mais n’est pas enterriné) et les outils utilisent pyproject.toml en créant une section custo dedans au lieu des champs standardisés (poetry), voire pas du tout comme pipenv. De plus, setup.cfg est déjà utilisé dans la nature (voir les exemples plus haut).

Par ailleurs, extraire les données de setup.cfg plutôt que de pyproject.toml n’est pas très compliqué, les outils de packaging n’ont qu’une toute petite partie de leur code dédié à la gestion du fichier, le reste c’est la logique de management des dépendances, le téléchargement, la command line, le virtualenv, etc. Ils peuvent par ailleurs tout à fait supporter les deux pendant la transition.

Vu que c’est nous qui allons nous coltiner les conséquences du choix pour les 10 prochaines années à venir, autant élever la voie. D’autant que, surprise, ça ne coûte quasiment rien à la majorité des projets de migrer ou commencer avec setup.cfg. Tout marche déjà, et on peut produire de jolies wheels. On ne peut pas dire autant de pyproject qui demande d’adopter des outils tout neufs et qui doivent encore faire leur preuve. Aussi, bonne chance pour faire marcher votre toolchain de CI avec, j’ai essayé, et croyez moi c’est relou.

Maintenant, j’aime que le packaging avance. J’aime même les nouveaux outils comme poetry. Mais la suite n’est pas inéluctable, et on peut concilier modernité avec sanité.

]]>
25060
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
Le don du mois : nuitka http://sametmax.com/le-don-du-mois-nuitka/ http://sametmax.com/le-don-du-mois-nuitka/#comments Mon, 01 Feb 2016 16:37:56 +0000 http://sametmax.com/?p=18034 dernièrement, le packaging et les performances sont deux points qui méritent d'être améliorés en Python. Nuikta est un projet qui vise à tacler ces deux problèmes d'un coup en compilant le code Python pour obtenir un exécutable autonome sous forme de code machine.]]> Comme je vous l’ai dit dernièrement, le packaging et les performances sont deux points qui méritent d’être améliorés en Python.

Nuitka est un projet qui vise à tacler ces deux problèmes d’un coup en compilant le code Python pour obtenir un exécutable autonome sous forme de code machine.

Plus besoin de s’inquiéter d’avoir la bonne version de Python installée. Plus besoin de se soucier d’avoir Python, ou une lib quelconque installée. Et en prime, le compilateur implémente le plus d’optimisations qu’il peut.

Nuitka est un projet en cours et son équipe est petite. Les progrès sont sérieux, mais lents. Histoire d’encourager tout ça, je fais un don de 50€ à l’auteur.

Pour donner à votre tour, c’est par ici.

]]>
http://sametmax.com/le-don-du-mois-nuitka/feed/ 16 18034
Travailler sur une lib externe à votre projet proprement en Python http://sametmax.com/travailler-sur-une-lib-externe-a-votre-projet-proprement-en-python/ http://sametmax.com/travailler-sur-une-lib-externe-a-votre-projet-proprement-en-python/#comments Mon, 06 Oct 2014 12:53:23 +0000 http://sametmax.com/?p=12269 Quand on a une lib externe en dépendance à son projet, on veut être capable de l’importer MAIS pouvoir en modifier le code.

L’installer avec pip ou python setup.py install va copier le code dans le dossier site-packages et ce n’est pas ce que l’on veut car ça oblige à refaire l’installation à chaque modif.

Le mettre dans un dossier “libs” qu’on ajoute au PYTHON PATH ou pire, ajouter chaque dépendance au PYTHON PATH n’est pas une solution qu’on est fier d’exhiber au hackerspace du coin.

On s’en remet donc généralement à un symlink, sans savoir qu’il existe en fait un outil fait pour ça :

python setup.py develop

Cette commande va ajouter des entrées spéciales dans des fichiers placés dans site-packages qui vont vous permettre d’utiliser votre lib comme si elle était installée. Mais en vérité il va chercher le code dans votre dossier de dev, donc si vous modifiez le code, vous avez toujours la version de votre lib la plus fraîche.

Une fois qu’on a une lib stable, on peut retirer ce lien avec :

python setup.py develop --uninstall

Et installer la lib normalement.

Notez que tout ceci ne fonctionne que si le setup.py utilise setuptools et non distutils qui provoque l’erreur error: invalid command ‘develop’.

Comme setuptools inclut maintenant le meilleur de distutils, on peut remplacer :

from distutils.core import setup

Par :

from setuptools import setup

Sans soucis.

pip vient lui aussi avec un outil pour ça en la forme du flag -e qui fait exactement la même chose. Exemple :

pip install -e /chemin/local/vers/projet

Cela fonctionne comme pour python setup.py develop --uninstall mais on bénéficie de tous les goodies de pip en prime.

Néanmoins, pip pousse plus loin l’automatisation. Si vous faites pip install -e sur un repo distant, il va également cloner pour vous le code.

Ex :

pip install -e git+https://git@github.com/sametmax/minibelt.git#egg=minibelt

Et on retrouvera un clone du projet dans /chemin/vers/virtualenv/src/ qui sera importable, modifiable et pushable. Le résultat est automatisable, puisque pip freeze le prend en compte:

$ pip freeze
argparse==1.2.1
-e git+https://git@github.com/sametmax/minibelt.git@b898155b40d7de73cc404db7d274128f2b2fc330#egg=minibelt-master
wsgiref==0.1.2

L’URL est assez bâtarde à trouver par contre, car elle doit toujours finir par #egg=nom-du-projet et commencer par un double protocole, celui du VCS et celui du transport.

P.S: j’ai remis la coloration syntaxique et les iframes (donc la musique). J’aime bien ce thème là, mais le header chie sa mère. Est-ce que vous l’aimez suffisamment pour que je me casse le cul à le réparer ?

]]>
http://sametmax.com/travailler-sur-une-lib-externe-a-votre-projet-proprement-en-python/feed/ 13 12269