En novembre dernier je donnais une conférence à la PyConFR sur le mécanisme des imports en Python1 et je commentais comme quoi les exemples étaient volontairement superflus car je n’avais pas d’utilisation utile à montrer.
Aujourd’hui j’en ai trouvé une au sys.meta_path
.
- Je prévois de le porter en article sur le site, mais c’est toujours en brouillon.↩
Une histoire d'entry points
Dans le cadre d’un projet, j’exploite le mécanisme d'entry points de Python.
Les entry points (« points d’entrée ») sont des enregistrements liés à un paquet Python, qui permettent de référencer un module / une fonction du paquet pour un usage particulier.
Un cas d’usage classique est celui des console scripts : définir une commande foo
sur le système qui invoque une fonction Python particulière dans notre paquet.
Ils se définissent comme suit :
Il suffit ensuite d’installer le paquet pour que la commande foo
devienne disponible.
% pip install .
...
Successfully built spam
Installing collected packages: spam
Successfully installed spam-1
% foo
Hello World!
- Dans le cas particulier des console scripts, on les définit habituellement plutôt dans une section
[project.scripts]
que[project.entry-points.console_scripts]
, mais cela revient au même.↩
Un système de plugins
Dans mon cas j’utilise les entry points différemment : j’ai un programme principal (un paquet installé) qui se base sur des entry points pour gérer des plugins.
Tous les paquets installés exposant un entry point de type spam.plugins
se retrouvent donc utilisables depuis le projet.
Par exemple un plugin peut être défini ainsi :
Une fois installé dans l’environnement courant (pip install ...
), il est possible de trouver l'entry point depuis n’importe où.
>>> from importlib.metadata import entry_points
>>> entry_points(group='spam.plugins')
(EntryPoint(name='foo', value='foo_plugin:run', group='spam.plugins'),)
>>> entry_point, = entry_points(group='spam.plugins')
>>> func = entry_point.load()
>>> func
<function run at 0x7a1e2c3a8220>
>>> func()
Plugin loaded
Comment tester tout ça ?
Mais comment tester que le mécanisme de chargement de plugins est fonctionnel ?
Il faudrait pour cela définir des paquets à la volée que l’on installerait ensuite à l’aide de pip
… ça semble fastidieux et pip
n’est pas vraiment prévu pour être invoqué depuis du code Python.
Non, il y a un autre moyen de procéder, et c’est là qu’intervient le sys.meta_path
.
Pour la faire rapidement, sys.meta_path
définit des emplacements dynamiques qui sont interrogés par Python lorsqu’on cherche à importer un module / paquet.1.
Mais ils ne se limitent pas à cela : ils définissent aussi comment trouver la distribution d’un paquet, c’est-à-dire toutes les métadonnées (nom, version, description, mais aussi entry points) qui lui sont associées.
Et donc dans mon cas je n’ai pas besoin de réellement installer de paquet depuis les tests, il me suffit qu’une entrée (un finder) du meta-path déclare que le paquet est bien installé pour qu’il le soit.
Le finder dispose pour cela d’une méthode find_distributions
qui reçoit en argument un contexte qui ne nous sera pas utile ici et renvoie une liste de distributions (tous les paquets installés connus par ce finder).
Une distribution définit typiquement un nom et une liste d'entry points ; et l'entry point comme on l’a vu est composé d’un nom, d’un groupe (console_scripts
/ spam.plugins
) et d’une valeur (l’action à charger).
Partant de là, je peux alors relativement facilement arriver à mes fins.
% pytest test_plugins.py
test_plugins.py .
- Mais je vous invite à consulter le support de conférence évoqué en introduction pour en savoir plus.↩
Et voilà !
Bon, je conviens que ça reste un cas d’usage à la marge, mais je suis content d’avoir trouvé à sys.meta_path
une utilité dans un cas qui ne soit pas tordu.