Construire une architecture de plugin minimale en Python

190

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

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

Je recherche quelque chose d' extrêmement léger . La plupart des scripts, ou plugins, ne seront pas développés et distribués par un tiers et installés, mais seront 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 doivent avoir le code standard minimum absolu et ne nécessitent 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 comme celui-ci, ou des projets mettant en œuvre un schéma similaire que je devrais rechercher pour des idées / inspiration?

dF.
la source

Réponses:

150

Le mien est, fondamentalement, un répertoire appelé "plugins" que l'application principale peut interroger puis utiliser imp.load_module pour récupérer des fichiers, rechercher un point d'entrée bien 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 une bonne chose à avoir.

Bien sûr, toute exigence qui se présente en disant "Je n'ai pas besoin de [chose énorme et compliquée] X; je veux juste quelque chose de léger" court le 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 de toute façon :)

TJG
la source
26
Merci beaucoup! J'ai écrit un petit tutoriel basé sur votre message: lkubuntu.wordpress.com/2012/10/02/writing-a-python-plugin-api
MiJyn
9
Le impmodule est obsolète au profit du importlibdémarrage de python 3.4
b0fh
1
Dans de nombreux cas d'utilisation, vous pouvez utiliser importlib.import_module en remplacement de imp.load_module.
Chris Arndt
58

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 vérification d'erreur, 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 plugins en Python peut être simple.

Vous voulez probablement regarder dans le diablotin module de trop, mais vous pouvez faire beaucoup avec juste __import__, os.listdiret une manipulation de chaînes.

dbr
la source
4
Je pense que vous voudrez peut-être changer def call_plugin(name, *args)pour def call_plugin(name, *args, **kwargs), puis plugin.plugin_main(*args)pourplugin.plugin_main(*args, **kwargs)
Ron Klein
12
En python 3, impest obsolète en faveur deimportlib
Adam Baxter
25

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

Compte tenu du peu d'informations dont je dispose, je répondrai 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 façon serait de dire "tous les fichiers de ce plugin / répertoire seront chargés", mais cela a l'inconvénient d'exiger de vos utilisateurs qu'ils se déplacent dans les fichiers.
  • Une dernière option intermédiaire serait d'exiger que tous les plugins soient dans le même plugin / dossier, puis de les activer / désactiver en utilisant des chemins relatifs dans un fichier de configuration.

Dans le cadre d'une pratique de code / conception pure, vous devrez déterminer clairement le comportement / les actions spécifiques que 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 des hooks , inspiré de MediaWiki (PHP, mais le langage compte-t-il vraiment?):

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 de ligne de commande hg , étendant 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 d'une initialisation et d'une finalisation communes pour votre extension. Vous pouvez soit utiliser une interface commune que toute votre extension devra implémenter (correspond mieux à la deuxième approche; mercurial utilise un reposetup (ui, repo) qui est appelé pour toutes les extensions), soit utiliser une approche de type crochet, avec un hooks.setup hook.

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

Nicolas Dumazet
la source
11

Le framework de plugins simple de Marty Allchin est la base que j'utilise pour mes propres besoins. Je recommande vraiment d'y jeter un œil, je pense que c'est vraiment un bon début si vous voulez quelque chose de simple et facilement piratable. Vous pouvez également le trouver sous forme d'extraits de code Django .

edomaur
la source
J'essaye de faire quelque chose comme ça avec pyduck comme base.
edomaur
C'est très spécifique à Django d'après ce que je peux dire.
Zoran Pavlovic
3
@ZoranPavlovic: pas du tout, certaines lignes de Python standard, vous n'avez pas besoin d'utiliser Django.
edomaur
11

Je suis un biologiste à la retraite qui a travaillé avec des micrograqphes numériques et s'est retrouvé à devoir écrire un package de traitement et d'analyse d'images (pas techniquement 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 en utilisant Tk. Les commandes qui apparaissaient 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. Lorsque Tcl a vu la sous-chaîne "extensionName", le contrôle a été passé au package C. Cela a à son tour exécuté la commande via un lexer / parser (fait dans lex / yacc), puis appelé les routines C si nécessaire.

Les commandes pour faire fonctionner le paquet pouvaient être exécutées une par une via une fenêtre dans l'interface graphique, mais les travaux par lots étaient effectués en éditant des fichiers texte qui étaient des scripts Tcl valides; vous choisiriez le modèle qui a fait le type d'opération au niveau du fichier que vous vouliez faire, puis éditeriez une copie pour contenir le répertoire et les noms de fichier réels ainsi que les commandes du 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é environ 500 lignes, lorsque les capacités organisationnelles douteuses de Tcl ont commencé à devenir un réel inconvénient. Le temps s'est écoulé ...

J'ai pris ma retraite, Python a été inventé, et il ressemblait au successeur parfait de Tcl. Maintenant, je n'ai jamais fait le portage, car je n'ai jamais affronté les défis de compiler des (assez gros) programmes C sur un PC, d'étendre Python avec un package C et de faire des interfaces graphiques en Python / Gt? / Tk? /? ?. Cependant, la vieille idée d'avoir des scripts de modèle modifiables semble toujours réalisable. En outre, il ne devrait pas être trop lourd d'entrer des commandes de package sous une forme native Python, par exemple:

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

Quelques points, parens et virgules supplémentaires, mais ce ne sont pas des bouchons.

Je me souviens avoir vu que quelqu'un a fait des versions de lex et yacc en Python (essayez: http://www.dabeaz.com/ply/ ), donc si elles sont encore nécessaires, elles sont là.

Le but de cette randonnée est qu'il m'a semblé que Python lui-même EST le frontal "léger" désiré et utilisable par les scientifiques. Je suis curieux de savoir pourquoi vous pensez que ce n'est pas le cas, et je le pense sérieusement.


ajouté plus tard: l'application gedit anticipe 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 de recherche. Essayer:

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

J'aimerais quand même mieux comprendre votre question. Je ne sais pas si vous 1) voulez que les scientifiques puissent utiliser votre application (Python) tout simplement de différentes manières ou 2) souhaitez permettre aux scientifiques d'ajouter de nouvelles fonctionnalités à votre application. Le choix n ° 1 est la situation à laquelle nous avons été confrontés avec les images et qui nous a conduit à utiliser des scripts génériques que nous avons modifiés en fonction des 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 impossible l'envoi de commandes?

derrière la chute
la source
2
Réparation de la pourriture des liens: le plugin Gedit est maintenant - wiki.gnome.org/Apps/Gedit/PythonPluginHowTo
ohhorob
1
C'est un beau billet, car il montre clairement et de manière concise à quel point nous, les biologistes modernes, sommes chanceux. Pour lui / elle, python est le langage de script modulaire utilisé pour donner une certaine abstraction aux développeurs de modules afin qu'ils n'aient pas besoin d'analyser le code C principal. De nos jours, cependant, peu de biologistes apprendront jamais C, au lieu de tout faire en Python. Comment faire abstraction de la complexité de nos principaux programmes python lors de l'écriture de modules? Dans 10 ans, les programmes seront peut-être écrits en Emoji et les modules ne seront que des fichiers audio contenant une série de grognements. Et peut-être que c'est OK.
JJ
10

Lorsque je recherche des décorateurs Python, j'ai trouvé un extrait de code simple mais utile. Cela peut ne pas correspondre à vos besoins mais très inspirant.

Système d'enregistrement de plug-in Scipy 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**")
guneysus
la source
1
Remarque: Dans cet exemple, WordProcessor.pluginne retourne rien ( None), donc importer la CleanMdashesExtensionclasse plus tard simplement importe None. Si les classes de plugin sont utiles en elles-mêmes, créez la .pluginméthode de classe return plugin.
jkmacc
@jkmacc Vous avez raison. J'ai modifié l'extrait 13 jours après votre commentaire. Je vous remercie.
guneysus
7

J'ai apprécié la belle discussion sur les différentes architectures de plugins donnée par le Dr André Roberge à Pycon 2009. Il donne un bon aperçu des différentes façons d'implémenter des plugins, en partant de quelque chose de vraiment simple.

Il est disponible sous forme de podcast (deuxième partie après une explication du patching 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.

Jon Mills
la source
4

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

Samwyse
la source
4

En fait, setuptools fonctionne avec un "répertoire plugins", comme 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 plugins 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 aient à s'en soucier.

Ankostis
la source
3

Comme une autre approche du système de plugins, vous pouvez vérifier le 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]

La bibliothèque extend_me 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, MyCoolClassnous avons une instance de nouvelle classe qui est une sous-classe des deux MyCoolClassExtensionet qui MyCoolClasspossède la fonctionnalité des deux, grâce à l' héritage multiple de Python

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

  • ExtensibleType - permet une extensibilité simple par sous-classement

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

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

Pour un exemple réel d'utilisation, regardez dans l'extension 'field_datetime' d'OpenERP Proxy :

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

Recordvoici un objet extensible. RecordDateTimeest l'extension.

Pour activer l'extension, importez simplement le module contenant la classe d'extension et (dans le cas ci-dessus) Record objets créés après avoir une classe d'extension dans les classes de base, ayant ainsi 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 les extensions pourraient tout changer dans les objets extensibles.

Mage de feu
la source
Je pense que vous voulez instancier à partir de la sous-classe, c'est-à-dire my_cool_obj = MyCoolClassExtension1()au lieu demy_cool_obj = MyCoolClass()
pylang
non, la classe Extensible a une __new__méthode surchargée , donc elle trouve automatiquement toutes les sous-classes, et construit une nouvelle classe, c'est-à-dire une sous-classe de toutes, et retourne une nouvelle instance de cette classe créée. Ainsi, l'application d'origine n'a pas besoin de connaître toutes les extensions. cette approche est utile lors de la construction de la bibliothèque, pour permettre à l'utilisateur final de modifier ou d'étendre son comportement facilement. dans l'exemple ci-dessus, MyCoolClass peut être défini dans la bibliothèque et utilisé par celle-ci, et MyCoolClassExtension peut être défini par l'utilisateur final.
FireMage
Un autre exemple a été ajouté pour répondre
FireMage
3

setuptools a un EntryPoint :

Les points d'entrée sont un moyen simple pour les distributions de «publier» des objets Python (tels que des fonctions ou des classes) à utiliser par d'autres distributions. Les applications et infrastructures extensibles peuvent rechercher des points d'entrée avec un nom ou un groupe particulier, soit à partir d'une distribution spécifique, soit à partir de toutes les distributions actives sur sys.path, puis inspecter ou charger les objets publiés à volonté.

AFAIK ce package est toujours disponible si vous utilisez pip ou virtualenv.

guettli
la source
2

Pour développer la réponse de @ edomaur, je suggère de jeter un coup d'œil à simple_plugins (plug sans vergogne), qui est un simple framework de plugin inspiré du travail de Marty Alchin .

Un court exemple d'utilisation basé sur le 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>
Petar Marić
la source
2

J'ai passé du temps à lire ce fil pendant que je cherchais un framework de plugin en Python de temps en temps. J'en ai utilisé mais il y avait des lacunes avec eux. Voici ce que je propose pour votre examen en 2017, un système de gestion de plugins sans interface et faiblement couplé: Chargez-moi plus tard . Voici des tutoriels sur son utilisation.

chfw
la source
2

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 toutes 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"}'))
aviso
la source
1
Merci pour l'exemple. J'ai eu du mal avec une question. Puisque vous avez évoqué la possibilité de charger les plugins à partir de différents packages, vous y avez peut-être déjà pensé. Je me demande où la classe parent devrait résider. Habituellement, il est recommandé de l'avoir dans le package de l'application (vraisemblablement un référentiel de code source distinct), mais comment en hériterions-nous dans la base de code du plugin? Importons-nous toute l'application pour cela? Ou est-il nécessaire d'avoir le code d'interface comme la classe Parser ou des abstractions similaires dans un 3ème paquet (qui serait un 3ème référentiel de code)?
JAponte
1
La classe parente doit résider dans la même base de code que l'application, mais probablement dans son propre module. Ainsi, pour un package appelé foo, vous pouvez avoir un module appelé foo.parentsoù vous définissez les classes parentes. Ensuite, vos plugins seraient importés foo.parents. Cela fonctionne bien pour la plupart des cas d'utilisation. Parce que 'foo' lui-même est également importé, pour éviter la possibilité d'importations circulaires, de nombreux projets laissent la racine du module vide et utilisent un __main__.pyfichier ou des points d'entrée pour lancer l'application.
aviso
1

J'ai passé beaucoup de temps à essayer de trouver un petit système de plugins pour Python, qui répondrait à mes besoins. Mais alors j'ai juste pensé, s'il y a déjà un héritage, qui est 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 plugins les plus spécifiques (les plus basses de l'arbre 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 pourrait éventuellement créer une classe, qui hérite de la plupart des plugins spécifiques ('Root extended' sur la figure ci-dessous)

entrez la description de l'image ici

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__.__name__ + 'PluginExtended'
            extended = type(name, tuple(cls.__pluginextensions__), {})
            cls.__pluginroot__.__pluginiscachevalid__ = True
return extended

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

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 vous êtes intéressé, impliquez-vous @ https://github.com/thodnev/pluginlib

thodnev
la source
1

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 pour gérer, par exemple, les interfaces de ligne de commande, la signalisation ou les objets partagés.

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

Voici les documents .

Avertissement : je suis l'un des auteurs de Groundwork.

ub_marco
la source
0

Dans notre produit de santé actuel, nous avons une architecture de plugin implémentée 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 plugins écrite pour notre produit qui est essentiellement un package pip et npm en conformité avec Django et Nuxtjs.

Pour le développement de nouveaux plugins (pip et npm), nous avons fait du gestionnaire de plugins une dépendance.

Dans le package Pip: Avec l'aide de setup.py, vous pouvez ajouter un point d'entrée du plugin pour faire quelque chose avec le gestionnaire de plugin (registre, initiations, ... etc.) Https://setuptools.readthedocs.io/en/latest/setuptools .html # création-de-script-automatique

Dans le package npm: Similaire à pip, il existe des hooks dans les scripts npm pour gérer l'installation. https://docs.npmjs.com/misc/scripts

Notre cas d'utilisation:

L'équipe de développement de plugins est désormais séparée de l'équipe de développement principale. La portée du développement de plugins est pour l'intégration avec des applications tierces qui sont définies dans l'une des catégories du produit. Les interfaces de plugin sont classées par exemple pour: - Le gestionnaire de plugin de fax, téléphone, email ... etc peut être amélioré à de nouvelles catégories.

Dans votre cas: Peut-être que vous pouvez avoir un plugin écrit et le réutiliser pour faire des choses.

Si les développeurs de plugins ont besoin d'utiliser des objets de base de réutilisation, cet objet peut être utilisé en faisant un niveau d'abstraction dans le gestionnaire de plugins afin que tous les plugins puissent hériter de ces méthodes.

Le simple fait de partager comment nous avons mis en œuvre notre produit espère que cela vous donnera une petite idée.

Shankar Ganesh Jayaraman
la source