PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Embarquer un fichier non Python proprement 8

vendredi 12 février 2016 à 13:38

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:

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:

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).