modulable.py
modulable.py
est une librairie légère qui facilite l'écriture et la maintenance d'un code modulaire.
Elle marche avec des classes: les plug-ins chargés sont "injectés" dans la classe
en question. On peut préciser la façon dont les implémentations sont injectées,
soit par empilage de fonctions, avec le décorateur modulable
, ou bien par
surcharge, avec le décorateur overridable
, ou enfin, avec le décorateur
alternative
, qui exécute toutes les fonctions tant que ça lève une exception.
Ces décorateurs conservent les informations des méthodes décorées, comme leur nom, leur module de définition, la docstring, et les annotations.
Exemple
Imaginons que vous voulez coder un shell modulaire, où l'utilisateur pourrait implémenter ses propres commandes, et son propre prompt, par exemple.
1 2 3 | from modulable import * class Shell(Modular, plugin_directory='plugins'): |
Ce bout de code déclare une classe modulaire Shell
, dont les plug-ins
sont dans le répertoire plugin
, relatif au répertoire courant.
La librairie chargera tous les plug-ins (qui doivent porter l'extensin .py) dans ce répertoire, dès lors que la classe est déclarée.
Il est préférable de déclarer une méthode init
, appelée dans le vrai
__init__
, pour permettre aux utilisateurs d'initialiser leurs attributs
spécifiques à leur plug-in:
1 2 3 4 5 6 | def __init__(self, *args, **kwds): self.init(*args, **kwds) @modulable def init(self, *args, **kwds): self.running = False |
Là, on décore notre méthode init
avec modulable
. Cela signifie que chaque
implémentation de init
sera exécutée à l'appel.
Ensuite, on veut une fonction qui est exécutée entre chaque commande, et, disons, une autre fonction qui retournerait le prompt de notre shell.
1 2 3 4 5 6 7 | @modulable def update(self): pass @overridable def prompt(self): return '> ' |
Cette fois, on utilise le décorateur overridable
pour charger la dernière
implémentation de prompt
chargée.
Enfin, on veut une fonction qui réagit à l'entrée utilisateur. Ce qu'on voudrait
ici, c'est que toutes les implémentations soient exécutées jusqu'à ce qu'une
ne lève pas d'exception. Pour ça, on a le décorateur alternative
qui prend
le(s) type(s) de cette(ces) exception(s).
1 2 3 4 | @alternative(ValueError) def react(self, i): if i: print('Unrecognized command:', repr(i)) |
On donne ici un cas par défaut, qui sera exécuté si aucune implémentation ne marche.
Enfin, on définit une fonction run
(non modulable), pour faire marcher le tout:
1 2 3 4 5 6 7 | def run(self): self.running = True while self.running: i = input(self.prompt()) self.react(i) self.update() |
Pour l'instant, note shell n'implémente pas de plug-in. Juste pour l'exemple, on va coder 3 plug-ins:
quit
: qui arrête le shell lorque l'utilisateur entrequit
greet
: qui salue quelqu'un dont on précise le nomcommand_count_prompt
: qui affiche le nombre de commande en prompt
L'implémentation de la commande quit
est assez simple:
1 2 3 4 5 | def react(self, i): if i == 'quit': self.running = False else: raise ValueError |
En levant une ValueError
, on délègue l'entrée i
à la prochaine implémentation
de la méthode react
.
Le plug-in greet
est quasiment identique, avec un peu plus de parsing:
1 2 3 4 5 6 7 8 9 10 | def react(self, i): lexemes = i.split() try: if lexemes[0] == 'greet': print('Hey', lexemes[1], '!') else: raise ValueError except IndexError: raise ValueError |
Enfin, on définit notre module command_count_prompt
:
1 2 3 4 5 6 7 8 | def init(self, *args, **kwds): self.command_count = 0 def update(self): self.command_count += 1 def prompt(self): return '[{}]: '.format(self.command_count) |
Les plug-ins doivent être contenus dans le répertoire défini à la déclaration
de la classe, dans notre cas plugins
, pour être chargés automatiquement.
On devrait donc avoir un arbre similaire:
1 2 3 4 5 6 | . ├── plugins │ ├── command_count_prompt.py │ ├── greet.py │ └── quit.py └── shell.py |
Pour utiliser notre shell, on instancie tout simplement un objet Shell
, et
on appelle sa méthode run
:
1 2 | sh = Shell() sh.run() |
Voilà ce que ça donne:
1 2 3 4 5 6 7 8 9 10 | [0]: [1]: [2]: greet Jonathan Hey Jonathan ! [3]: [4]: [5]: unknown command Unrecognized command: 'unknown command' [6]: [7]: quit |
Vous pouvez consulter le code complet dans le dossier examples
.
Utilisation avancée
Vous pouvez charger temporairement un plug-in avec le context manager
plugin
:
1 2 | with Shell.plugin('greet'): sh.run() |
On peut aussi vérifier les plug-ins chargés avec Shell.loaded_plugins
.
Enfin, il y a un argument optionnel virtual
lors de la définition de classe.
virtual
vaut par défaut False
, mais quand on le règle à True
, la classe
ne chargera pas automatiquement les plug-ins:
1 2 | class AbstractShell(Modular, plugins='plugins', virtual=True): ... |
Installation
- Via pip:
1 | $ pip install modulable |
Et, si vous êtes sur Linux et rencontrez une erreur de permission, essayez
de lancer la commande avec sudo
avec l'argument -H
:
1 | $ sudo -H pip install modulable |
- Via git:
1 2 3 | $ git clone http://github.com/felko/modulable.git
$ cd modulable
$ sudo -H python3.4 setup.py install
|
Ou, si vous êtes sous Windows:
1 2 3 | $ git clone http://github.com/felko/modulable.git $ cd modulable $ py -3.4 setup.py install |
Si vous n'avez pas git, vous pouvez télécharger le fichier zip ici
Liens
- GitHub: http://github.com/felko/modulable
- Bug Tracker: http://github.com/feko/modulable/issues
- PyPI: http://pypi.python.org/pypi/modulable
- Téléchargement: http://pypi.python.org/pypi/modulable#downloads
License
modulable
est distribué sous la license MIT.