modulable.py: Une librairie pour écrire des codes modulables

a marqué ce sujet comme résolu.

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 entre quit
  • greet: qui salue quelqu'un dont on précise le nom
  • command_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

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

License

modulable est distribué sous la license MIT.

+3 -0

Je suis un petit peu sceptique sur l'approche employée.

Le but du jeu est donc de définir une classe dont le comportement de certaines méthodes peut être surchargé via une simple fonction nommée correctement dans le plugin… Pourquoi pas, mais ça me semble assez fragile de le faire dans ce sens (le code du plugin est implicitement lié à la classe surchargée, que faire si deux classes ont des méthodes modulables du même nom ?) plutôt qu'en utilisant le mécanisme inverse (comme un dispatcher par exemple) qui demanderait au plugin de définir explicitement ce qu'il souhaite surcharger.

Pour moi c'est dans le plugin lui-même que l'on devrait décorer les surcharges, ne serait-ce que pour une question de lisibilité du plugin, mais surtout pour dissiper toute ambiguïté.

En dehors de ça ça a l'air sympa. En tout cas j'ai en tête quelques projets qui pourraient bénéficier d'un module de ce style.

+1 -0

Salut,

En effet j'ai pas pensé au cas où deux méthodes de deux classes différentes pourraient avoir le même nom, mais normalement un plug-in n'est chargé que dans une classe. Après on pourrait imaginer une vérification pour pas qu'on puisse le charger dans une autre classe que prévue.

L'avantage de restreindre un peu les libertés du codeur de plug-ins, c'est que le développeur peut d'avantage contrôler les fonctionnements de base de sa classe.

Après c'est vrai qu'avec le mécanisme inverse, où les décorateurs sont utilisés dans le plugin, ça serait à la fois plus lisible et ça compromettrait pas trop le système.

L'avantage de restreindre un peu les libertés du codeur de plug-ins, c'est que le développeur peut d'avantage contrôler les fonctionnements de base de sa classe.

En fait je ne vois pas trop ce que ça change pour le développeur du code de base : dans tous les cas il est le seul à choisir ce qui peut être pluginisé dans sa classe son code. (Pourquoi lui imposer une classe ? ;-) ).

Ma philosophie perso dans ce genre de truc c'est que les développeurs sont pires que des utilisateurs normaux : si tu leur imposes une façon de faire (ici le nommage des fonctions et/ou l'organisation des modules) tu peux être sûr qu'un beau jour il y en aura un qui va arriver avec le besoin tout à fait légitime de contourner ta restriction. :-D

Je jetterai un oeil à l'occasion sur ton code pour voir si ça ne peut pas se faire facilement. J'ai un use-case bien concret (un projet au boulot que l'on compte pluginifier et open-sourcer) que je pourrais te soumettre si tu veux.

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte