web-dev-qa-db-fra.com

Construire une architecture de plugin minimale dans Python

J'ai une application, écrite en Python, qui est utilisée par un public assez technique (scientifiques).

Je cherche un bon moyen de rendre l’application extensible par les utilisateurs, c’est-à-dire une architecture de script/plugin.

Je cherche quelque chose extrêmement léger. La plupart des scripts, ou plugins, ne seront pas développés et distribués par un tiers, ni installés, mais deviendront quelque chose de fouetté par un utilisateur en quelques minutes pour automatiser une tâche répétitive, ajouter la prise en charge d'un format de fichier, etc. Ainsi, les plugins devraient avoir le code minimum absolu et ne nécessiter aucune 'installation' autre que la copie dans un dossier (donc quelque chose comme les points d’entrée setuptools, ou l’architecture du plugin Zope semble trop.)

Existe-t-il déjà des systèmes de ce type ou des projets mettant en œuvre un système similaire sur lequel je devrais rechercher des idées/une inspiration?

177
dF.

Le mien est, en gros, un répertoire appelé "plugins" que l'application principale peut interroger puis utiliser imp.load_module pour récupérer des fichiers, recherchez un point d'entrée connu, éventuellement avec des paramètres de configuration au niveau du module et à partir de là. J'utilise des outils de surveillance de fichiers pour un certain dynamisme dans lequel les plugins sont actifs, mais c'est agréable à avoir.

Bien entendu, toute exigence disant "Je n'ai pas besoin de [chose complexe, complexe] X; je veux juste quelque chose de léger" risque de ré-implémenter X une exigence découverte à la fois. Mais cela ne veut pas dire que vous ne pouvez pas vous amuser quand même :)

144
TJG

module_example.py:

def plugin_main(*args, **kwargs):
    print args, kwargs

loader.py:

def load_plugin(name):
    mod = __import__("module_%s" % name)
    return mod

def call_plugin(name, *args, **kwargs):
    plugin = load_plugin(name)
    plugin.plugin_main(*args, **kwargs)

call_plugin("example", 1234)

C'est certainement "minimal", il n'a absolument aucune erreur de vérification, probablement d'innombrables problèmes de sécurité, ce n'est pas très flexible - mais cela devrait vous montrer à quel point un système de plugin en Python peut être ..

Vous voudrez probablement aussi vous pencher sur le module imp , même si vous pouvez faire beaucoup avec juste __import__, os.listdir et quelques manipulations de chaîne.

51
dbr

Jetez un oeil à à cette vue d'ensemble sur les plugins/frameworks existants , c'est un bon point de départ. J'aime assez yapsy , mais cela dépend de votre cas d'utilisation.

30
PhilS

Bien que cette question soit vraiment intéressante, je pense qu’il est assez difficile de répondre, sans plus de détails. De quel type d'application s'agit-il? At-il une interface graphique? Est-ce un outil de ligne de commande? Un ensemble de scripts? Un programme avec un point d’entrée unique, etc ...

Étant donné le peu d’information dont je dispose, je vais vous répondre de manière très générique.

Quels moyens avez-vous pour ajouter des plugins?

  • Vous devrez probablement ajouter un fichier de configuration, qui listera les chemins/répertoires à charger.
  • Une autre solution consisterait à indiquer que "tous les fichiers de ce plugin/répertoire seront chargés", mais qu’il est peu pratique de demander à vos utilisateurs de se déplacer dans les fichiers.
  • Une dernière option intermédiaire consisterait à exiger que tous les plugins soient dans le même plugin/dossier, puis à les activer/les désactiver en utilisant des chemins relatifs dans un fichier de configuration.

Sur une pratique pure de code/conception, vous devrez déterminer clairement quel comportement/quelles actions spécifiques vous souhaitez que vos utilisateurs étendent. Identifiez le point d'entrée commun/un ensemble de fonctionnalités qui seront toujours remplacées, et déterminez les groupes au sein de ces actions. Une fois cela fait, il devrait être facile d’étendre votre application,

Exemple utilisant hooks, inspiré de MediaWiki (PHP, mais le langage est-il vraiment important?):

import hooks

# In your core code, on key points, you allow user to run actions:
def compute(...):
    try:
        hooks.runHook(hooks.registered.beforeCompute)
    except hooks.hookException:
        print('Error while executing plugin')

    # [compute main code] ...

    try:
        hooks.runHook(hooks.registered.afterCompute)
    except hooks.hookException:
        print('Error while executing plugin')

# The idea is to insert possibilities for users to extend the behavior 
# where it matters.
# If you need to, pass context parameters to runHook. Remember that
# runHook can be defined as a runHook(*args, **kwargs) function, not
# requiring you to define a common interface for *all* hooks. Quite flexible :)

# --------------------

# And in the plugin code:
# [...] plugin magic
def doStuff():
    # ....
# and register the functionalities in hooks

# doStuff will be called at the end of each core.compute() call
hooks.registered.afterCompute.append(doStuff)

Un autre exemple, inspiré de Mercurial. Ici, les extensions ajoutent uniquement des commandes à l'exécutable en ligne de commande hg, ce qui étend le comportement.

def doStuff(ui, repo, *args, **kwargs):
    # when called, a extension function always receives:
    # * an ui object (user interface, prints, warnings, etc)
    # * a repository object (main object from which most operations are doable)
    # * command-line arguments that were not used by the core program

    doMoreMagicStuff()
    obj = maybeCreateSomeObjects()

# each extension defines a commands dictionary in the main extension file
commands = { 'newcommand': doStuff }

Pour les deux approches, vous aurez peut-être besoin de common initialize et finalize pour votre extension. Vous pouvez soit utiliser une interface commune que toute votre extension devra implémenter (correspond mieux à la seconde approche; Mercurial utilise un reposetup (ui, repo) appelé pour toute extension), ou utiliser une approche de type crochet, avec une hooks.setup hook.

Mais encore une fois, si vous voulez des réponses plus utiles, vous devrez préciser votre question;)

25
Nicolas Dumazet

le framework de plugin simple de Marty Allchin est la base que j'utilise pour mes propres besoins. Je recommande vraiment de jeter un coup d'oeil là-dessus, je pense que c'est vraiment un bon début si vous voulez quelque chose de simple et facilement piratable. Vous pouvez le trouver aussi sous forme d'extraits Django .

11
edomaur

Biologiste à la retraite, je travaillais avec des micrograqphes numériques et devais écrire un logiciel de traitement et d'analyse d'images (qui n'était techniquement pas une bibliothèque) pour fonctionner sur une machine SGi. J'ai écrit le code en C et utilisé Tcl pour le langage de script. L'interface graphique, telle qu'elle était, a été réalisée à l'aide de Tk. Les commandes apparaissant dans Tcl étaient de la forme "extensionName commandName arg0 arg1 ... param0 param1 ...", c'est-à-dire de simples mots et nombres séparés par des espaces. Quand Tcl a vu la sous-chaîne "extensionName", le contrôle a été passé au paquet C. À son tour, la commande est exécutée via un lexer/parser (effectué dans Lex/yacc), puis appelée routine C si nécessaire.

Les commandes permettant d’exploiter le package peuvent être exécutées une par une via une fenêtre de l’interface graphique, mais les traitements par lots sont réalisés en modifiant des fichiers texte contenant des scripts Tcl valides; vous choisiriez le modèle qui effectuait le type d'opération que vous souhaitiez effectuer au niveau du fichier, puis modifiez une copie pour qu'elle contienne les noms de répertoire et de fichier réels, ainsi que les commandes de package. Ça a marché comme sur des roulettes. Jusqu'à ce que ...

1) Le monde s'est tourné vers les PC et 2) les scripts ont dépassé les 500 lignes, lorsque les capacités d'organisation douteuses de Tcl ont commencé à devenir un véritable inconvénient. Le temps passait ...

Je me suis retiré, Python a été inventé, et cela ressemblait parfaitement au successeur de Tcl. Maintenant, je n'ai jamais fait le portage, car je n'ai jamais relevé le défi de la compilation (assez grosse) Programmes C sur un PC, étendant Python avec un paquet C, et réalisant des interfaces graphiques en Python/Gt?/Tk?/??. Cependant, la vieille idée de disposer de scripts de modèles modifiables semble toujours fonctionner En outre, il ne devrait pas être trop lourd de saisir des commandes de paquet dans un formulaire natif Python, par exemple:

packageName.command (arg0, arg1, ..., param0, param1, ...)

Quelques points, parenthèses et virgules supplémentaires, mais ceux-ci ne sont pas révélateurs.

Je me souviens avoir vu que quelqu'un avait fait des versions de Lex et de yacc dans Python (essayez: http://www.dabeaz.com/ply/ ), alors sont toujours nécessaires, ils sont autour.

Le but de cette discussion est qu’il m’a semblé que Python lui-même IS le frontal "léger" souhaité, utilisable par les scientifiques. Je suis curieux de Sachez pourquoi vous pensez que ce n’est pas le cas, et je le dis sérieusement.


ajouté plus tard: L’application gedit prévoit l’ajout de plugins et leur site contient l’explication la plus claire d’une procédure de plugin simple que j’ai trouvée en quelques minutes regardant autour. Essayer:

https://wiki.gnome.org/Apps/Gedit/PythonPluginHowToOld

J'aimerais quand même mieux comprendre votre question. Je ne sais pas si vous 1) souhaitez que les scientifiques puissent utiliser votre application (Python) tout simplement de différentes manières ou 2) autorisez les scientifiques à ajouter de nouvelles fonctionnalités à votre application. Le choix n ° 1 est la situation à laquelle nous sommes confrontés avec les images et qui nous a amenés à utiliser des scripts génériques que nous avons modifiés pour répondre aux besoins du moment. Est-ce le choix n ° 2 qui vous amène à l'idée de plugins, ou est-ce un aspect de votre application qui rend impraticable ses commandes?

11
behindthefall

Lorsque je cherchais Python Decorators), j'ai trouvé un extrait de code simple mais utile. Il se peut qu'il ne corresponde pas à vos besoins mais qu'il soit très inspirant.

Système d’enregistrement de plug-in Python Advanced Python #

class TextProcessor(object):
    PLUGINS = []

    def process(self, text, plugins=()):
        if plugins is ():
            for plugin in self.PLUGINS:
                text = plugin().process(text)
        else:
            for plugin in plugins:
                text = plugin().process(text)
        return text

    @classmethod
    def plugin(cls, plugin):
        cls.PLUGINS.append(plugin)
        return plugin


@TextProcessor.plugin
class CleanMarkdownBolds(object):
    def process(self, text):
        return text.replace('**', '')

Usage:

processor = TextProcessor()
processed = processor.process(text="**foo bar**", plugins=(CleanMarkdownBolds, ))
processed = processor.process(text="**foo bar**")
10
guneysus

J'ai apprécié la discussion de Nice sur les différentes architectures de plug-in donnée par le Dr Andre Roberge à Pycon 2009. Il donne un bon aperçu des différentes manières de mettre en œuvre des plug-ins, à partir de quelque chose de très simple.

Il est disponible sous forme de podcast (deuxième partie après une explication de la correction de singe) accompagné d'une série de six entrées de blog .

Je recommande de l'écouter rapidement avant de prendre une décision.

7
Jon Mills

En fait setuptools fonctionne avec un "répertoire de plugins", comme dans l'exemple suivant tiré de la documentation du projet: http://peak.telecommunity.com/DevCenter/PkgResources#locating-plugins

Exemple d'utilisation:

plugin_dirs = ['foo/plugins'] + sys.path
env = Environment(plugin_dirs)
distributions, errors = working_set.find_plugins(env)
map(working_set.add, distributions)  # add plugins+libs to sys.path
print("Couldn't load plugins due to: %s" % errors)

À long terme, setuptools est un choix beaucoup plus sûr, car il peut charger des plug-ins sans conflits ni exigences manquantes.

Un autre avantage est que les plugins eux-mêmes peuvent être étendus en utilisant le même mécanisme, sans que les applications d'origine ne s'en soucient.

4
ankostis

Je suis arrivé ici à la recherche d'une architecture de plugin minimale et j'ai trouvé beaucoup de choses qui me paraissaient excessives. J'ai donc implémenté Super Simple Python Plugins) . Pour l'utiliser, vous créez un ou plusieurs répertoires et supprimez un __init__.py fichier dans chacun. L’importation de ces répertoires entraînera le chargement de tous les autres Python fichiers) en tant que sous-modules, et leur (s) nom (s) seront placés dans le fichier __all__ liste. Ensuite, c’est à vous de valider/initialiser/enregistrer ces modules. Il y a un exemple dans le fichier README).

4
samwyse

setuptools a un EntryPoint :

Les points d’entrée constituent un moyen simple pour les distributions de "faire de la publicité" Python (tels que des fonctions ou des classes) pouvant être utilisées par d’autres distributions. groupe, soit à partir d’une distribution spécifique, soit à partir de toutes les distributions actives sur sys.path, puis inspectez ou chargez les objets publiés à volonté.

Autant que je sache, ce package est toujours disponible si vous utilisez pip ou virtualenv.

3
guettli

Comme autre approche du système de plug-in, vous pouvez vérifier projet Extend Me .

Par exemple, définissons une classe simple et son extension

# Define base class for extensions (mount point)
class MyCoolClass(Extensible):
    my_attr_1 = 25
    def my_method1(self, arg1):
        print('Hello, %s' % arg1)

# Define extension, which implements some aditional logic
# or modifies existing logic of base class (MyCoolClass)
# Also any extension class maby be placed in any module You like,
# It just needs to be imported at start of app
class MyCoolClassExtension1(MyCoolClass):
    def my_method1(self, arg1):
        super(MyCoolClassExtension1, self).my_method1(arg1.upper())

    def my_method2(self, arg1):
        print("Good by, %s" % arg1)

Et essayez de l'utiliser:

>>> my_cool_obj = MyCoolClass()
>>> print(my_cool_obj.my_attr_1)
25
>>> my_cool_obj.my_method1('World')
Hello, WORLD
>>> my_cool_obj.my_method2('World')
Good by, World

Et montrez ce qui se cache derrière la scène:

>>> my_cool_obj.__class__.__bases__
[MyCoolClassExtension1, MyCoolClass]

extend_me La bibliothèque manipule le processus de création de classe via des métaclasses. Ainsi, dans l'exemple ci-dessus, lors de la création d'une nouvelle instance de MyCoolClass, nous avons obtenu une instance de nouvelle classe est la sous-classe de MyCoolClassExtension et de MyCoolClass ayant les deux fonctions, grâce à Python héritage multiple

Pour un meilleur contrôle sur la création de classe, il existe peu de métaclasses définies dans cette bibliothèque:

  • ExtensibleType - permet une extensibilité simple en sous-classant

  • ExtensibleByHashType - similaire à ExtensibleType, mais ayant la capacité de construire des versions spécialisées de classe, permettant l'extension globale de la classe de base et l'extension de versions spécialisées de la classe

Cette lib est utilisée dans OpenERP Proxy Project , et semble fonctionner assez bien!

Pour un exemple réel d'utilisation, consultez extension 'field_datetime' du proxy OpenERP :

from ..orm.record import Record
import datetime

class RecordDateTime(Record):
    """ Provides auto conversion of datetime fields from
        string got from server to comparable datetime objects
    """

    def _get_field(self, ftype, name):
        res = super(RecordDateTime, self)._get_field(ftype, name)
        if res and ftype == 'date':
            return datetime.datetime.strptime(res, '%Y-%m-%d').date()
        Elif res and ftype == 'datetime':
            return datetime.datetime.strptime(res, '%Y-%m-%d %H:%M:%S')
        return res

Record voici un objet extesible. RecordDateTime est une extension.

Pour activer l'extension, il suffit d'importer le module contenant la classe d'extension et (dans le cas ci-dessus) tous les objets Record créés après qu'il aura une classe d'extension dans les classes de base, afin de disposer de toutes ses fonctionnalités.

Le principal avantage de cette bibliothèque est que le code qui exploite des objets extensibles n'a pas besoin de connaître l'extension et que les extensions peuvent tout changer dans les objets extensibles.

3
FireMage

J'ai passé du temps à lire ce fil pendant que je cherchais un framework de plug-in dans Python de temps en temps. J'ai tilisé quelques-uns mais il y avait des défauts avec eux. Voici ce que je propose à votre examen en 2017, un système de gestion de plug-ins à interface libre et faiblement couplée: chargez-moi plus tard . Voici tutoriels sur la façon de l'utiliser.

2
chfw

Pour élargir la réponse de @ edomaur, puis-je suggérer de jeter un oeil à simple_plugins (plug sans vergogne), qui est un simple cadre de plug-in inspiré du travail de Marty Alchin .

Un court exemple d'utilisation basé sur le fichier README du projet:

# All plugin info
>>> BaseHttpResponse.plugins.keys()
['valid_ids', 'instances_sorted_by_id', 'id_to_class', 'instances',
 'classes', 'class_to_id', 'id_to_instance']

# Plugin info can be accessed using either dict...
>>> BaseHttpResponse.plugins['valid_ids']
set([304, 400, 404, 200, 301])

# ... or object notation
>>> BaseHttpResponse.plugins.valid_ids
set([304, 400, 404, 200, 301])

>>> BaseHttpResponse.plugins.classes
set([<class '__main__.NotFound'>, <class '__main__.OK'>,
     <class '__main__.NotModified'>, <class '__main__.BadRequest'>,
     <class '__main__.MovedPermanently'>])

>>> BaseHttpResponse.plugins.id_to_class[200]
<class '__main__.OK'>

>>> BaseHttpResponse.plugins.id_to_instance[200]
<OK: 200>

>>> BaseHttpResponse.plugins.instances_sorted_by_id
[<OK: 200>, <MovedPermanently: 301>, <NotModified: 304>, <BadRequest: 400>, <NotFound: 404>]

# Coerce the passed value into the right instance
>>> BaseHttpResponse.coerce(200)
<OK: 200>
2
Petar Marić

Vous pouvez utiliser pluginlib .

Les plugins sont faciles à créer et peuvent être chargés à partir d'autres packages, chemins de fichiers ou points d'entrée.

Créez une classe parent de plugin, définissant les méthodes requises:

import pluginlib

@pluginlib.Parent('parser')
class Parser(object):

    @pluginlib.abstractmethod
    def parse(self, string):
        pass

Créez un plugin en héritant d'une classe parent:

import json

class JSON(Parser):
    _alias_ = 'json'

    def parse(self, string):
        return json.loads(string)

Chargez les plugins:

loader = pluginlib.PluginLoader(modules=['sample_plugins'])
plugins = loader.plugins
parser = plugins.parser.json()
print(parser.parse('{"json": "test"}'))
1
aviso

Vous pouvez également consulter Groundwork .

L'idée est de construire des applications autour de composants réutilisables, appelés patterns et plugins. Les plugins sont des classes qui dérivent de GwBasePattern. Voici un exemple de base:

from groundwork import App
from groundwork.patterns import GwBasePattern

class MyPlugin(GwBasePattern):
    def __init__(self, app, **kwargs):
        self.name = "My Plugin"
        super().__init__(app, **kwargs)

    def activate(self): 
        pass

    def deactivate(self):
        pass

my_app = App(plugins=[MyPlugin])       # register plugin
my_app.plugins.activate(["My Plugin"]) # activate it

Il existe également des modèles plus avancés à gérer, par exemple. interfaces de ligne de commande, objets de signalisation ou partagés.

Groundwork trouve ses plugins en les liant par programme à une application comme indiqué ci-dessus ou automatiquement via setuptools. Python contenant des plugins doivent les déclarer en utilisant un point d’entrée spécial groundwork.plugin.

Voici les docs .

Disclaimer : Je suis l'un des auteurs de Groundwork.

1
ub_marco

J'ai passé beaucoup de temps à essayer de trouver un petit système de plug-in pour Python, qui conviendrait à mes besoins. Mais je me suis dit que s'il existait déjà un héritage naturel et flexible, pourquoi ne pas l'utiliser.

Le seul problème avec l'utilisation de l'héritage pour les plugins est que vous ne savez pas quelles sont les classes de plug-in les plus spécifiques (l'arborescence d'héritage).

Mais cela pourrait être résolu avec la métaclasse, qui garde la trace de l'héritage de la classe de base, et peut éventuellement construire la classe, qui hérite de la plupart des plugins spécifiques ('Root extended' sur la figure ci-dessous)

enter image description here

Je suis donc venu avec une solution en codant une telle métaclasse:

class PluginBaseMeta(type):
    def __new__(mcls, name, bases, namespace):
        cls = super(PluginBaseMeta, mcls).__new__(mcls, name, bases, namespace)
        if not hasattr(cls, '__pluginextensions__'):  # parent class
            cls.__pluginextensions__ = {cls}  # set reflects lowest plugins
            cls.__pluginroot__ = cls
            cls.__pluginiscachevalid__ = False
        else:  # subclass
            assert not set(namespace) & {'__pluginextensions__',
                                         '__pluginroot__'}     # only in parent
            exts = cls.__pluginextensions__
            exts.difference_update(set(bases))  # remove parents
            exts.add(cls)  # and add current
            cls.__pluginroot__.__pluginiscachevalid__ = False
        return cls

    @property
    def PluginExtended(cls):
        # After PluginExtended creation we'll have only 1 item in set
        # so this is used for caching, mainly not to create same PluginExtended
        if cls.__pluginroot__.__pluginiscachevalid__:
            return next(iter(cls.__pluginextensions__))  # only 1 item in set
        else:
            name = cls.__pluginroot__.__+ 'PluginExtended'
            extended = type(name, Tuple(cls.__pluginextensions__), {})
            cls.__pluginroot__.__pluginiscachevalid__ = True
return extended

Ainsi, lorsque vous avez une base Root, faite avec une métaclasse, et une arborescence de plugins qui en hérite, vous pouvez automatiquement obtenir une classe, qui hérite des plugins les plus spécifiques simplement en sous-classant:

class RootExtended(RootBase.PluginExtended):
    ... your code here ...

La base de code est assez petite (~ 30 lignes de code pur) et aussi flexible que l’héritage le permet.

Si cela vous intéresse, impliquez-vous @ https://github.com/thodnev/pluginlib

1
thodnev

Dans notre produit de santé actuel, nous avons une architecture de plug-in mise en œuvre avec une classe d'interface. Notre pile technologique est Django au dessus de Python pour l'API et Nuxtjs au dessus de nodejs pour le frontend.

Nous avons une application de gestionnaire de plug-in écrite pour notre produit qui est essentiellement un paquet pip et npm conforme à Django et à Nuxtjs.

Pour le développement de nouveaux plugins (pip et npm), nous avons créé le gestionnaire de plugins en tant que dépendance.

Dans le package Pip: à l’aide de setup.py, vous pouvez ajouter un point d’entrée du plug-in pour faire quelque chose avec le gestionnaire de plug-ins (registre, initiations, ... etc.) https://setuptools.readthedocs.io/en /latest/setuptools.html#automatic-script-creation

Dans le package npm: Semblable au pip, il existe des points d'ancrage dans les scripts npm permettant de gérer l'installation. https://docs.npmjs.com/misc/scripts

Notre cas d'utilisation:

l’équipe de développement du plugin est maintenant séparée de l’équipe de développement principale. La portée du développement de plug-in concerne l'intégration avec des applications tierces définies dans l'une des catégories du produit. Les interfaces de plug-in sont classées par exemple pour: - le gestionnaire de plug-in de fax, téléphone, e-mail, etc., peut être amélioré pour de nouvelles catégories.

Dans votre cas: Vous pouvez peut-être écrire un plugin et le réutiliser pour faire des choses.

Si les développeurs de plug-in doivent utiliser les objets principaux de réutilisation, cet objet peut être utilisé en effectuant un niveau d'abstraction dans le gestionnaire de plug-in afin que tous les plug-ins puissent hériter de ces méthodes.

Il suffit de partager comment nous avons implémenté notre produit dans l’espoir que cela donnera une petite idée.

0
shangan