PROJET AUTOBLOG


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

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

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

Mais qui donc set ce signal handler ? 3

jeudi 25 février 2016 à 08:59

Le module signal permet de réagir aux signaux envoyés l’OS, par exemple celui qui vous annonce qu’il va vous défoncer la gueule (SIGTERM) ou que vous votre mémoire n’est plus ce qu’elle était (SIGSEGV).

Mais le problème, c’est que rien n’annonce qu’un signal est attrapé. Pire, quand on enregistre un handler pour un signal, ça écrase le précédent. Yep, c’est très con.

S’aurait été plus malin d’avoir un truc comme propose le DOM, genre addEventListener, qui accumule les handlers pour un event donné, mais non, on override.

Donc parfois, une lib tierce partie se permet un petit handling de signal sur le pouce, comme ça, sans demander la permission, provoquant chaos et moult jurons. Car des signaux, y en a pas mal:

>>> import signal
>>> [s for s in dir(signal) if s.isupper()]
['ITIMER_PROF', 'ITIMER_REAL', 'ITIMER_VIRTUAL', 'NSIG', 'SIGABRT', 'SIGALRM', 'SIGBUS', 'SIGCHLD', 'SIGCLD', 'SIGCONT', 'SIGFPE', 'SIGHUP', 'SIGILL', 'SIGINT', 'SIGIO', 'SIGIOT', 'SIGKILL', 'SIGPIPE', 'SIGPOLL', 'SIGPROF', 'SIGPWR', 'SIGQUIT', 'SIGRTMAX', 'SIGRTMIN', 'SIGSEGV', 'SIGSTOP', 'SIGSYS', 'SIGTERM', 'SIGTRAP', 'SIGTSTP', 'SIGTTIN', 'SIGTTOU', 'SIGURG', 'SIGUSR1', 'SIGUSR2', 'SIGVTALRM', 'SIGWINCH', 'SIGXCPU', 'SIGXFSZ', 'SIG_BLOCK', 'SIG_DFL', 'SIG_IGN', 'SIG_SETMASK', 'SIG_UNBLOCK']

En plus, certains sont Windows only, d’autres Unix only…

Et pour faciliter les choses, les handlers peuvent être des entiers (bon, des enums dont __eq__ accepte les entiers…):

>>> type(signal.getsignal(signal.SIGKILL))
<enum 'Handlers'>
 
>>> type(signal.getsignal(signal.SIGINT) )
<class 'builtin_function_or_method'>

Voici un petit snippet pour checker si un sacripant n’est pas en train de vous brouter le signal:

>>> import signal
>>> handlers = {}
>>> for s in dir(signal):
...     if s.isupper(): # on chope uniquement les constantes
...         try:
...             handler = getattr(signal, s) # quel handler pour ce signal ?
...             if callable(handler): # on ignore SIG_DFL, SIG_IGN, etc.
...                 handlers[s] = handler # ok, on a un truc gégé !
...         except ValueError:
...            pass
>>> print(handlers[s])

On s’en sert pas souvent, mais je me suis dis que je ferai passer le mot.

L’internationalisation, c’est long et dur (et ça racle le fond) 7   Recently updated !

vendredi 19 février 2016 à 14:30

L’internationalisation, abrégé i18n car il y a 18 lettres entre le i et le n, est le processus qui consiste à proposer différentes versions d’un contenu ou d’une interface afin de coller au plus proche des attentes culturelles de différents groupes d’humains.

Quand je discute avec un client et qu’il me dit :

Je veux tradure mon site en Anglais, c’est combien ?

Je tique toujours un peu.

C’est que l’i18n, ce n’est pas juste traduire des mots vers des autres. C’est beaucoup, beaucoup plus compliqué que ça. Et il faut avant tout identifier qui on vise, ce qu’on veut leur apporter, et ce qu’on est prêt à investir comme énergie pour leur offrir.

En générale, les populations anglophones sont assez proches culturellement de nous, donc c’est plus facile de faire la transition que vers des cultures arabes, russes ou chinoises. Et si c’est imparfait, ce n’est pas trop grave, car l’utilisateur s’adaptera assez bien.

Néanmoins, il ne faut pas croire que prendre le texte et le coller dans Google translate va suffire, loin de là.

Et si le travail vise de gens culturellement plus éloignés de vous, alors le travail peu vite de venir énorme.

Le texte

Bien entendu, on pense tout de suite à la traduction, et c’est déjà un gros boulot.

Mais déjà, il faut s’adapter au contexte culturel que l’on vise. En Japonais, les niveaux de respect ne seront pas les mêmes selon les clients. Vous traduisez vers de l’espagnol, mais visez-vous l’Amérique du Sud et ses ustedes partout, ou l’Espagne et son vosotros ? Vous voulez proposer de louer un cab aux Londoniens ou un taxi aux New-Yorkais ?

Au passage, prenez un bon traducteur, car en plus de devoir traduire le sens du texte (et pas juste les mots), il faut qu’il adapte la ponctuation. Ainsi, on colle “:”, “!” et “!” au mot précédent en Anglais, mais pas en français.

On a à peine commencé l’article et c’est déjà la merde, mais attendez !

Il y a le sens de lecture aussi !

De gauche à droite, de droite à gauche, de haut en bas. Et si vous croyez que ce n’est que le texte, lol… Car ça influence également toute la mise en page, avec notamment le placement des éléments importants : titres, boutons, etc. afin qu’ils soient mis en évidence.

Après, il y a l’espacement. Vous savez, on peut se permettre de se la jouer cool sur les espacements en français, parceque’on a plein de ponctuation. Donc un bloc de texte est clair, facile à lire en diagonale. En Thaïlandais par contre, il n’y a pas de virgule, de point, ni même d’espace entre les mots et les phrases. Donc si vous avez un truc à bien séparer du reste, il va falloir trouver une astuce visuelle.

Et puis il y a mon favoris, particulièrement parce que Python est très sévère (mais juste:)) avec : l’encodage.

Car dans le meilleur des mondes, on a de l’UTF8 partout. Mais on est pas dans le meilleur des mondes, et si vous visez certains terminaux, il faudra encoder dans un truc couleur locale. Par exemple, si vous envoyez des SMS sur des vieux 3310 en Afrique.

Les nombres

Ah, les nombres. Qu’est-ce qu’il pourrait être plus simple ? C’est universel pas vrai ?

Pas vrai ?

Et bien disons que comme d’habitude, la donnée est universelle, et la représentation ne l’est pas.

Déjà, il a les bases. On aime beaucoup la base 10, et quand c’est du technique parfois un peu de binaire, d’octal ou d’hexa.

Mais certaines populations ont des manières de compter étonnantes. Par exemple, si vous faites un système à Bamako (ne rigolez pas, c’est un exemple de ma carrière), sachez qu’en Bambara 1000 se dit 2 fois 500.

À cela se rajoutent les séparateurs et devises. Mille dollars et quarante cents pour un américain, c’est $1,000.40 ou $1000.40, alors qu’en français se sera 1000,40 $ ou 1 000,40 $.

Et chaque format à ses règles de groupages, ses séparateurs, ses positions de préfixes… Les numéros de téléphone, les numéros de sécurité sociale, les codes postaux…

Attendez, ne fuyez pas, on n’en est pas encore à la moitié !

Les unités

Même la NASA a niqué une mission parce qu’ils se sont embrouillés entre les unités impériales et le système métrique.

Alors vous allez me dire, tant qu’on n’utilise pas des unités pifométriques, il suffit de convertir. Oui, mais encore faut-il savoir de quoi on parle. Il faut donc toujours demander à l’utilisateur en quoi il saisit ses données, et être très précis.

Très précis parce que figurez-vous que:

Les dates

Je ne vous apprends rien en vous disant que gérer les fuseaux horaires c’est chiant. Mettez tout en UTC, et utiliser une bonne lib parce que le temps c’est relou : des zones géographiques alignées n’ont pas les mêmes heures (France et UK), des pays très grands ont plusieurs fuseaux (USA) ou un seul malgré leur gigantisme (Chine), et certains on les heures d’été/hiver, mais pas tous. Et il y a les années bissextiles, et la fameuse leap second. Et le fait que certains pays décident de changer de fuseau horaire comme ça, pouf, parce que ça les arrange.

Du coup il faut bien demander aux utilisateurs où ils sont pour récupérer la date dans le bon fuseau (l’ordinateur peut être réglé pour envoyer la mauvaise info, il peut sortir de l’avion, il peut ne pas vouloir utiliser la timezone actuelle, etc), convertir tout en UTC, et l’afficher dans le bon.

Après, hop, on a le formage des dates. Mais si, vous savez, ce truc qui a 40 000 règles qui changent d’un pays à l’autre, d’un standard à l’autre. 02/03/2015: dd/mm/yyyy ou mm/dd/yyyy ? Timestamp en seconde ou en millisecondes ?

Allez, vous croyez que vous vous en sortirez comme ça ?

N’oubliez pas, tout le monde n’a pas le même calendrier…

Nous utilisons un calendrier basé sur le chrétien, mais vous avez des calendriers bouddhistes, musulmans, des trucs basés sur les phases de la lune, et même il y a quelque temps en France le très rigolo calendrier républicain. Donc maintenant, il faut se soucier de pour où, de pour qui, mais aussi de pour quand on formate. Je plains les gens qui font des apps pour les musées.

Je plains aussi tous les gens qui font des plannings internationaux, qui doivent décider si lundi ou dimanche est le premier jour à afficher et traduire les notations naturelles comme “il y a 3 minutes(s)”.

La loi

Oh, si, oh si !

La loi change d’un pays à l’autre ! Nudité, le copyright, l’accessibilité…

Le DMCA aux USA, la censure en Chine, l’Hadopi en France. Nan pour le dernier je déconne hein, ils ont condamné 5 personnes depuis le début du programme

Et allez prendre en compte les lecteurs d’écran, mettre du ARIA comme il faut, tester le bouzin pour les personnes en déficience visuelle (problèmes de couleur, aveugles, malvoyants…) ou motrice.

Bon alors là je ne vais pas rentrer dans les détails, car c’est trop, trop compliqué pour ma petite tête.

La culture

Le symbolisme diffère d’un groupe culturel à l’autre.

Vous allez faire un site pour le mariage, en beau blanc !

Mais en Chine, il faut du rouge.

Et dans certaines cultures, c’est la couleur du deuil.

Ce qui rend crédible commercialement aussi est différent. Un beau site bien épuré, avec plein d’espace négatif, c’est classe non ? Mais en Asie, avoir une page saturée est la norme.

En fait toute l’infrastructure, et même le système entier change la manière de voir les choses.

Quand on écrit du contenu pour un pays, il faut se rappeler des choses comme le climat, la monnaie différente, conduite à droite ou à gauche, voltage du courant, jours chômés, situation politique…

Si on fait une application avec beaucoup de carto, on ne mettra pas les mêmes choses en avant. Dans certains pays, les pistes sont importantes, dans d’autres les autoroutes le sont. Le truc le plus marquant qui me vient à l’esprit, c’est le fait que les cartes japonaises définissent un adresse en labélisant les blocs entre les rues, tandis que nous labélisons les rues entre les blocs.

Et je ne parle même pas du markéting

Qui est bien entendu radicalement différent selon la population visée, et non juste en termes d’i18n, mais aussi selon l’appartenance ethnique, sociale, culturelle, démographique, les handicape…

Il serait vraiment intéressant d’avoir un espèce de wiki open source qui, pour chaque groupe visé, liste les choses auxquelles il faut penser.

Finalement tout ça est trop chiant, z’ont cas se démerder ces estrangers de merde !

Arg, putain, on est 70 millions seulement, c’est nous les estrangers.

Après, ça ne veut pas dire qu’il faut tout faire, et trouver l’équilibre entre les ressources disponibles et le résultat qu’on vise. Mais il est bon de savoir ce qu’on décide de ne pas faire.

Comment foirer sa communication avec son client en une leçon 10   Recently updated !

mardi 16 février 2016 à 16:21

S.Lott est un excellent informaticien, très connu dans le monde de Python. J’ai régulièrement eu affaire à lui sur stackoverflow, et il sait de quoi il parle.

Je lisais son (excellent) article qui expliquait la difficulté à faire perdre aux gens leur habitude de penser en tables.

Malheureusement, si l’auteur a parfaitement réussi à faire passer son message aux lecteurs, il a complètement échoué à communiquer avec son client.

Voyez-vous, S.Lott fait preuve ici d’un péché auquel de nombreuses personnes intelligentes s’adonnent : il a raison.

Mais le client s’en branle.

Le client se fout complètement de savoir pourquoi on ne peut pas dumper ses données MongoDB telles-quel dans un tableau Excel. Il se fout complètement d’avoir tort.

Le client a un problème, et il souhaite une solution.

Notre taf n’est pas d’avoir raison. Notre taf est de résoudre des problèmes. On ne nous paie pas pour donner des leçons de justesse.

Donc, quand un client arrive avec une demande impossible à satisfaire, la bonne réaction à avoir n’est pas de lui expliquer que ce n’est pas possible. C’est inutile, improductif, et ça n’aide personne.

La bonne réaction à avoir, c’est de redéfinir le problème :

Vous pouvez, bien entendu, dans le cadre de la démarche vers la mise en place de la bonne solution, lui expliquer pourquoi c’est la manière la plus appropriée. Pourquoi sa solution initiale ne va pas lui résoudre son problème.

Par exemple, dans le cadre de l’article, des gens demandent un dump de données hiérarchiques (document Mongo) vers un format tabulaire (tableau, Excel, csv…).

La mauvaise réponse est de dire que c’est impossible. Zéro intérêt pour les gens en face, et peut-être un léger sentiment d’humiliation. Ce n’est ni le lieu ni le moment pour l’enseignement, il y a d’autres opportunités bien plus appropriées pour ça.

Il y a pourtant une réponse très simple:

Je vois. Donnez-moi le schéma de vos données et à quoi doit ressembler la feuille Excel.

Il faut que vous sachiez qu’une base Mongo a un format de stockage très différent d’une feuille Excel, donc il est possible que ça demande des aménagements, où peut être choisir une solution différente. Il risque d’y avoir de la perte d’informations, ou pas mal de travail manuel pour dénormaliser les données. Il faut donc qu’on passe un peu de temps ensemble pour définir clairement le cadre de tout ça. J’aurais besoin de faire un tête-à-tête avec une personne qui sait à quoi va servir la feuille Excel en détail, savoir qui va la lire, dans quel but, etc. On peut prendre rendez-vous ?

Bien entendu, le client peut être votre patron. Ou votre collègue de bureau. Auquel cas le format de la conversation va changer, mais pas le sens général.

Le but est de comprendre ce que la personne veut faire.

Parfois (souvent même), en définissant clairement le problème, la personne voit que sa solution ne lui convient pas et vous demande par elle-même une alternative.

Une fois sur deux, vous n’aurez pas besoin de prendre rendez-vous, car la personne sent que quelque chose se passe et va se lancer dans la conversation : son problème simple à l’air plus compliqué que prévu. Elle va se rebeller : « c’est simple, tu me fais juste un dump de la base dans un bête fichier Excel, c’est tout ».

Les mots-clés sont « juste », « bête », « c’est tout ». On peut avoir aussi « simple », « évident », « c’est pas bien compliqué »…

Il suffit de rester calme, et avoir un comportement respectueux, mais d’expert. « J’ai déjà travaillé sur un problème similaire, je sais que je ne saurais pas résoudre ce problème correctement sans ces informations. Vous avez 2 minutes pour griffonner à quoi doit ressembler le fichier Excel sur un papier ? »

Vous la prenez à son propre jeu. Ça ne durera pas deux minutes. Car pour faire ça il faut avoir le schéma des données, les bonnes personnes en jeu, et une idée claire de l’usage final, ce qui rend de plus en plus clair, et sans argumenter, que le problème demande plus de travail que « juste », « bête » et « c’est tout ». C’est l’occasion de trouver la solution ensemble.

Bref, « laisse-moi te démontrer que c’est con ce que tu dis » n’est jamais une bonne phrase à dire, même enrobée dans des termes polis et une explication technique. Il faut rester factuel : voilà comment je comprends le problème, voilà ce qu’il me faut pour le résoudre.

En plus, le meeting de définition du problème, vous pouvez le facturer. Hey :)

Signes qu’il faut extraire une fonction. 10   Recently updated !

lundi 15 février 2016 à 12:41

Organiser son code est tout un art. Ça se peaufine au fur et à mesure d’une carrière. J’ai découvert au fil du temps des règles qui formalisent quelque chose que je faisais sans vraiment m’en rendre compte. Vous savez comme ces règles de français à la noix que vous utilisez quotidiennement sans savoir qu’il y a règle.

DRY au bout de 3

Je commence par la plus facile.

Le célèbre Don’t Repeat Yourself n’a pas besoin d’être poussée à l’extrême. Copier/coller du code une fois est tout à fait acceptable, et souvent bien plus productif que de refactoriser son code. On commence donc à faire une fonction si on a 3 copies de son code.

Dans ce code par exemple, j’utilise 3 fois mon trigger, il est mur pour la cueillette :

async def setup(self, cwd=None):
 
 
    if cwd is None:
        cwd = get_project_dir()
    self.project_dir = Path(cwd)
 
    # Tell all component to hook them self to the events they want to react
    # to
    futures = [ensure_awaitable(c.setup) for c in self.components.values()]
    await asyncio.gather(*futures)
 
    self.state = value
    await self.trigger('init')
 
    self.state = value
    await self.trigger('ready')
 
    self.state = value
    return self.trigger('running')

Pouf:

def change_state(self, value):
    """ Change the inner state of the app, and call all state observers. """
    self.state = value
    return self.trigger(value)
 
async def setup(self, cwd=None):
 
    if cwd is None:
        cwd = get_project_dir()
    self.project_dir = Path(cwd)
 
    # Tell all component to hook them self to the events they want to react
    # to
    futures = [ensure_awaitable(c.setup) for c in self.components.values()]
    await asyncio.gather(*futures)
 
    await self.change_setate('init')
    await self.change_setate('ready')
    return self.change_setate('running')

Transformez vos commentaires en fonctions

C’est un conseil qui a apparemment été rendu populaire pas le courant extrem programming. Leur idée c’est que les noms de fonctions sont une forme de documentation. Chaque fonction devant être testée et documentée, si vous avez un commentaire qui se balade tout seul au milieu du code, c’est le signe qu’il faut en faire une fonction.

Dans le code qu’on vient de voir, j’ai un gros commentaire en plein milieu expliquant un bout de code un peu compliqué. Hop, on coupe :

async def setup(self, cwd=None):
 
    if cwd is None:
        cwd = get_project_dir()
    self.project_dir = Path(cwd)
 
    # Tell all component to hook them self to the events they want to react
    # to
    futures = [ensure_awaitable(c.setup) for c in self.components.values()]
    await asyncio.gather(*futures)
 
    await self.change_setate('init')
    await self.change_setate('ready')
    return self.change_setate('running')

Pouf:

async def setup_components(self):
    """ Give all components a chance to hook to life cycle events """
    futures = [ensure_awaitable(c.setup) for c in self.components.values()]
    return await asyncio.gather(*futures)
 
 
async def setup(self, cwd=None):
 
    if cwd is None:
        cwd = get_project_dir()
    self.project_dir = Path(cwd)
 
    await self.setup_components()
 
    await self.change_setate('init')
    await self.change_setate('ready')
    return self.change_setate('running')

On voit ici que la présence du commentaire signalait que le code était assez complexe, et donc méritait d’être isolé. Cela permet de :

Une partie qui n’a rien à voir avec la choucroute mérite sa propre choucroute

Dans de grosses fonctions, on a souvent des bouts qui sont des étapes, mais qui ne sont pas liés sémantiquement aux autres étapes. Elles doivent juste arriver chronologiquement avant ou après celle-ci.

Toujours dans le même code, on a un bout qui initialise le project_dir de l’app. Ce snippet, c’est comme l’inspecteur Karamazov, fils unique, aucun lien. En tout cas aucun avec le setup de components ou le changement du state. Il est là car il doit arriver en premier. Aucune, aucune, aucune hésitation ! Amputation, SO-LU-TION !

async def setup(self, cwd=None):
 
    if cwd is None:
        cwd = get_project_dir()
    self.project_dir = Path(cwd)
 
    await self.setup_components()
 
    await self.change_setate('init')
    await self.change_setate('ready')
    return self.change_setate('running')

Pouf :

async def setup_environnement(self, cwd=None):
    """ Set project dir for the current app """
    if cwd is None:
        cwd = get_project_dir()
    self.project_dir = Path(cwd)
 
async def setup(self, cwd=None):
 
    self.setup_environnement(cmd)
 
    await self.setup_components()
 
    await self.change_setate('init')
    await self.change_setate('ready')
    return self.change_setate('running')

On gagne en testabilité.

Et maintenant notre méthode setup() est très, très facile à comprendre. Son rôle est d’organiser 5 étapes. Pas de faire les étapes elle-même, ça c’est délégué. Le cycle de vie de l’application saute maintenant aux yeux. S’il y a un bug, on cherchera uniquement dans la fonction qui est responsable.

Sans le savoir, et sans vraiment le chercher, nous avons mis en oeuvre le principe de séparation des responsabilités. Seulement au lieu d’avoir cette notion floue et abstraite de responsabilité, on a maintenant quelques règles qui peuvent être facilement appliquées.

Diantre, je faisais donc de la prose, depuis toujours dans le savoir…

Pas de paramètre on/off

Le code de l’article d’hier était un cas d’école

Ma première version ressemblait à :

def resource_stream(resource, locations, encoding="utf8", ..., binary=False):
    """ Return a resource from this path or package """
    # du code, du code... puis:
    if binary:
        return stream
    return io.TextIOWrapper(stream, encoding, errors, newline, line_buffering)

On voit clairement ici que binary est un paramètre on/off, un truc qui déclenche un mode auquel on va passer un booléen.

Ça va s’utiliser comme ça:

txtstream =  resource_stream(...)
bstream =  resource_stream(..., True)

Quand on a des constantes magiques, il faut sortir la hache :

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

La seconde fonction utilise la première. A l’usage le code est beaucoup plus clair :

txtstream =  text_resource_stream(...)
bstream =  binary_resource_stream(...)

Encore une fois, c’est plus facile à tester, plus facile à comprendre, et on diminue le nombre de branches dans le code.

Pour les gens particulièrement intéressés par la lisibilité du code, il existe l’indicateur de McCabe qui se base notamment sur le nombre de branches logiques. Le max recommandé est en général de 10, personnellement mes codes dépassent rarement 5. flake8 a l’option –max-complexity pour vérifier que votre code s’y tient.

Embarquer un fichier non Python proprement 4   Recently updated !

vendredi 12 février 2016 à 13:38

Ah, le packaging Python, c’est toujours pas fun.

Parmi les nombres 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, ont 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 sure 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).

Error happened! 0 - count(): Argument #1 ($value) must be of type Countable|array, null given In: /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php:428 http://ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/?52 #0 /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php(999): VroumVroum_Blog->update() #1 /var/www/ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/index.php(1): require_once('...') #2 {main}