Erstellen einer minimalen Plugin-Architektur in Python

190

Ich habe eine in Python geschriebene Anwendung, die von einem ziemlich technischen Publikum (Wissenschaftlern) verwendet wird.

Ich suche nach einer guten Möglichkeit, die Anwendung für die Benutzer erweiterbar zu machen, dh nach einer Skript- / Plugin-Architektur.

Ich suche etwas extrem Leichtes . Die meisten Skripte oder Plugins werden nicht von einem Drittanbieter entwickelt und verteilt und installiert, sondern von einem Benutzer in wenigen Minuten erstellt, um eine sich wiederholende Aufgabe zu automatisieren und Unterstützung für ein Dateiformat hinzuzufügen. usw. Plugins sollten also den absoluten Mindest-Boilerplate-Code haben und keine andere 'Installation' erfordern als das Kopieren in einen Ordner (so etwas wie setuptools-Einstiegspunkte oder die Zope-Plugin-Architektur scheinen zu viel zu sein.)

Gibt es bereits solche Systeme oder Projekte, die ein ähnliches Schema implementieren, bei dem ich nach Ideen / Inspirationen suchen sollte?

dF.
quelle

Antworten:

150

Meins ist im Grunde ein Verzeichnis namens "Plugins", das die Haupt-App abfragen und dann mit imp.load_module Dateien abrufen , nach einem bekannten Einstiegspunkt suchen kann, möglicherweise mit Konfigurationsparametern auf Modulebene, und von dort aus fortfahren kann. Ich verwende Dateiüberwachungsmaterial für eine gewisse Dynamik, in der Plugins aktiv sind, aber das ist schön zu haben.

Natürlich besteht bei jeder Anforderung, die lautet: "Ich brauche kein [großes, kompliziertes Ding] X; ich möchte nur etwas Leichtes", das Risiko, dass X eine entdeckte Anforderung nach der anderen erneut implementiert wird. Aber das heißt nicht, dass du sowieso keinen Spaß daran haben kannst :)

TJG
quelle
26
Vielen Dank! Ich habe ein kleines Tutorial basierend auf Ihrem Beitrag geschrieben: lkubuntu.wordpress.com/2012/10/02/writing-a-python-plugin-api
MiJyn
9
Das impModul wird zugunsten importlibvon Python 3.4
b0fh
1
In vielen Anwendungsfällen können Sie importlib.import_module als Ersatz für verwenden 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)

Es ist sicherlich "minimal", es hat absolut keine Fehlerprüfung, wahrscheinlich unzählige Sicherheitsprobleme, es ist nicht sehr flexibel - aber es sollte Ihnen zeigen, wie einfach ein Plugin-System in Python sein kann.

Sie wollen wahrscheinlich in die aussehen imp Modul auch, obwohl Sie nur mit viel tun können __import__, os.listdirund einige String - Manipulation.

dbr
quelle
4
Ich denke , man könnte sich ändern wollen def call_plugin(name, *args)zu def call_plugin(name, *args, **kwargs), und dann plugin.plugin_main(*args)zuplugin.plugin_main(*args, **kwargs)
Ron Klein
12
In Python 3 impwird zugunsten vonimportlib
Adam Baxter
25

Obwohl diese Frage wirklich interessant ist, denke ich, dass sie ohne weitere Details ziemlich schwer zu beantworten ist. Was für eine Anwendung ist das? Hat es eine GUI? Ist es ein Kommandozeilen-Tool? Eine Reihe von Skripten? Ein Programm mit einem eindeutigen Einstiegspunkt usw.

Angesichts der wenigen Informationen, die ich habe, werde ich sehr allgemein antworten.

Was bedeutet, dass Sie Plugins hinzufügen müssen?

  • Sie müssen wahrscheinlich eine Konfigurationsdatei hinzufügen, in der die zu ladenden Pfade / Verzeichnisse aufgelistet sind.
  • Eine andere Möglichkeit wäre zu sagen, dass "alle Dateien in diesem Plugin / Verzeichnis geladen werden", aber es ist unpraktisch, von Ihren Benutzern zu verlangen, dass sie sich in Dateien bewegen.
  • Eine letzte Zwischenoption wäre, zu verlangen, dass sich alle Plugins im selben Plugin / Ordner befinden, und sie dann mithilfe relativer Pfade in einer Konfigurationsdatei zu aktivieren / deaktivieren.

Bei einer reinen Code- / Entwurfspraxis müssen Sie klar bestimmen, welches Verhalten / welche spezifischen Aktionen Ihre Benutzer erweitern sollen. Identifizieren Sie den gemeinsamen Einstiegspunkt / eine Reihe von Funktionen, die immer überschrieben werden, und bestimmen Sie Gruppen innerhalb dieser Aktionen. Sobald dies erledigt ist, sollte es einfach sein, Ihre Anwendung zu erweitern.

Beispiel mit Hooks , inspiriert von MediaWiki (PHP, aber spielt Sprache wirklich eine Rolle?):

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)

Ein weiteres Beispiel, inspiriert von Quecksilber. Hier fügen Erweiterungen der ausführbaren Datei der hg- Befehlszeile nur Befehle hinzu, wodurch das Verhalten erweitert wird.

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 }

Für beide Ansätze könnten Sie gemeinsam müssen initialize und Finalisierung für Ihre Erweiterung. Sie können entweder eine gemeinsame Schnittstelle verwenden, die alle Ihre Erweiterungen implementieren müssen (passt besser zum zweiten Ansatz; mercurial verwendet ein Reposetup (ui, repo), das für alle Erweiterungen aufgerufen wird), oder einen Hook-Ansatz mit a verwenden hooks.setup hook.

Aber wenn Sie nützlichere Antworten wünschen, müssen Sie Ihre Frage eingrenzen;)

Nicolas Dumazet
quelle
11

Das einfache Plugin-Framework von Marty Allchin ist die Basis, die ich für meine eigenen Bedürfnisse verwende. Ich empfehle wirklich, einen Blick darauf zu werfen. Ich denke, es ist wirklich ein guter Anfang, wenn Sie etwas einfaches und leicht hackbares wollen. Sie können es auch als Django-Snippets finden .

Edomaur
quelle
Ich versuche so etwas mit Pyduck als Basis zu machen.
Edomaur
Es ist sehr Django-spezifisch, soweit ich das beurteilen kann.
Zoran Pavlovic
3
@ZoranPavlovic: Überhaupt nicht, einige Zeilen von Standard-Python, Sie müssen Django nicht verwenden.
Edomaur
11

Ich bin ein pensionierter Biologe, der sich mit digitalen Mikrofotografien befasste und ein Bildverarbeitungs- und Analysepaket (technisch gesehen keine Bibliothek) schreiben musste, um auf einem SGi-Computer ausgeführt zu werden. Ich habe den Code in C geschrieben und Tcl für die Skriptsprache verwendet. Die GUI, so wie sie war, wurde mit Tk erstellt. Die in Tcl angezeigten Befehle hatten die Form "extensionName commandName arg0 arg1 ... param0 param1 ...", dh einfache durch Leerzeichen getrennte Wörter und Zahlen. Als Tcl den Teilstring "extensionName" sah, wurde die Steuerung an das C-Paket übergeben. Dadurch wurde der Befehl über einen Lexer / Parser (in lex / yacc ausgeführt) ausgeführt und anschließend nach Bedarf C-Routinen aufgerufen.

Die Befehle zum Bedienen des Pakets konnten einzeln über ein Fenster in der GUI ausgeführt werden. Stapeljobs wurden jedoch durch Bearbeiten von Textdateien ausgeführt, die gültige Tcl-Skripte waren. Sie würden die Vorlage auswählen, die die gewünschte Operation auf Dateiebene ausgeführt hat, und dann eine Kopie bearbeiten, die das tatsächliche Verzeichnis und die Dateinamen sowie die Paketbefehle enthält. Es funktionierte wie ein Zauber. Bis um ...

1) Die Welt wandte sich PCs zu und 2) die Skripte wurden länger als etwa 500 Zeilen, als die zweifelhaften organisatorischen Fähigkeiten von Tcl zu einer echten Unannehmlichkeit wurden. Zeit verging ...

Ich habe mich zurückgezogen, Python wurde erfunden und es sah aus wie der perfekte Nachfolger von Tcl. Jetzt habe ich den Port nie ausgeführt, weil ich mich nie den Herausforderungen gestellt habe, (ziemlich große) C-Programme auf einem PC zu kompilieren, Python mit einem C-Paket zu erweitern und GUIs in Python / Gt? / Tk? /? ?. Die alte Idee, bearbeitbare Vorlagenskripte zu haben, scheint jedoch immer noch praktikabel zu sein. Es sollte auch keine allzu große Belastung sein, Paketbefehle in einer nativen Python-Form einzugeben, z.

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

Ein paar zusätzliche Punkte, Parens und Kommas, aber das sind keine Showstopper.

Ich erinnere mich, dass jemand Versionen von Lex und Yacc in Python erstellt hat (versuchen Sie: http://www.dabeaz.com/ply/ ). Wenn diese also noch benötigt werden, sind sie da.

Der Punkt dieses Streifens ist, dass es mir so vorgekommen ist, als ob Python selbst das gewünschte "leichte" Frontend ist, das von Wissenschaftlern verwendet werden kann. Ich bin neugierig zu wissen, warum du denkst, dass es nicht so ist, und das meine ich ernst.


später hinzugefügt: Die Anwendung gedit geht davon aus, dass Plugins hinzugefügt werden, und auf ihrer Website finden Sie die klarste Erklärung für ein einfaches Plugin-Verfahren, das ich in wenigen Minuten gefunden habe. Versuchen:

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

Ich würde Ihre Frage immer noch gerne besser verstehen. Ich bin mir nicht sicher, ob Sie 1) möchten, dass Wissenschaftler Ihre (Python-) Anwendung ganz einfach auf verschiedene Arten verwenden können, oder 2) möchten, dass die Wissenschaftler Ihrer Anwendung neue Funktionen hinzufügen. Wahl Nr. 1 ist die Situation, mit der wir mit den Bildern konfrontiert waren, und die dazu führte, dass wir generische Skripte verwendeten, die wir an die Bedürfnisse des Augenblicks anpassten. Ist es Wahl 2, die Sie zur Idee von Plugins führt, oder ist es ein Aspekt Ihrer Anwendung, der die Ausgabe von Befehlen an Plugins unmöglich macht?

hinter dem Fall
quelle
2
Link Rot Reparatur: Gedit Plugin ist jetzt - wiki.gnome.org/Apps/Gedit/PythonPluginHowTo
ohhorob
1
Dies ist ein wunderschöner Beitrag, denn er zeigt deutlich und prägnant, wie viel Glück wir modernen Biologen haben. Für ihn / sie ist Python die modulare Skriptsprache, mit der Modulentwicklern eine gewisse Abstraktion verliehen wird, damit sie den Haupt-C-Code nicht analysieren müssen. Heutzutage werden jedoch nur wenige Biologen jemals C lernen, anstatt alles in Python zu tun. Wie abstrahieren wir die Komplexität unserer wichtigsten Python-Programme beim Schreiben von Modulen? In 10 Jahren werden vielleicht Programme in Emoji geschrieben und Module werden nur Audiodateien sein, die eine Reihe von Grunzen enthalten. Und vielleicht ist das in Ordnung.
JJ
10

Als ich nach Python Decorators suchte, fand ich ein einfaches, aber nützliches Code-Snippet. Es passt vielleicht nicht zu Ihren Bedürfnissen, ist aber sehr inspirierend.

Scipy Advanced Python # Plugin Registrierungssystem

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('**', '')

Verwendung:

processor = TextProcessor()
processed = processor.process(text="**foo bar**", plugins=(CleanMarkdownBolds, ))
processed = processor.process(text="**foo bar**")
Guneysos
quelle
1
Hinweis: In diesem Beispiel wird WordProcessor.pluginnichts zurückgegeben ( None), sodass beim CleanMdashesExtensionspäteren Importieren der Klasse nur importiert wird None. Wenn die Plugin-Klassen für sich genommen nützlich sind, erstellen Sie die .pluginKlassenmethode return plugin.
Jkmacc
@jkmacc Du hast recht. Ich habe das Snippet 13 Tage nach Ihrem Kommentar geändert. Danke dir.
Guneysos
7

Ich habe die nette Diskussion über verschiedene Plugin-Architekturen von Dr. Andre Roberge auf der Pycon 2009 genossen. Er gibt einen guten Überblick über verschiedene Arten der Implementierung von Plugins, beginnend mit etwas wirklich Einfachem.

Es ist als Podcast erhältlich (zweiter Teil nach einer Erklärung zum Affen-Patching), begleitet von einer Reihe von sechs Blogeinträgen .

Ich empfehle, es kurz anzuhören, bevor Sie eine Entscheidung treffen.

Jon Mills
quelle
4

Ich kam hier auf der Suche nach einer minimalen Plugin-Architektur an und fand viele Dinge, die mir alle übertrieben erschienen. Also habe ich Super Simple Python Plugins implementiert . Um es zu verwenden, erstellen Sie ein oder mehrere Verzeichnisse und legen __init__.pyin jedem eine spezielle Datei ab. Durch das Importieren dieser Verzeichnisse werden alle anderen Python-Dateien als Submodule geladen und ihre Namen in die __all__Liste aufgenommen. Dann liegt es an Ihnen, diese Module zu validieren / zu initialisieren / zu registrieren. Es gibt ein Beispiel in der README-Datei.

Samwyse
quelle
4

Tatsächlich arbeitet setuptools mit einem "Plugins-Verzeichnis", wie das folgende Beispiel aus der Projektdokumentation zeigt: http://peak.telecommunity.com/DevCenter/PkgResources#locating-plugins

Anwendungsbeispiel:

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)

Auf lange Sicht ist setuptools eine viel sicherere Wahl, da Plugins ohne Konflikte oder fehlende Anforderungen geladen werden können.

Ein weiterer Vorteil ist, dass die Plugins selbst mit demselben Mechanismus erweitert werden können, ohne dass sich die ursprünglichen Anwendungen darum kümmern müssen.

Ankostis
quelle
3

Als ein weiterer Ansatz für das Plugin-System können Sie das Extend Me-Projekt überprüfen .

Definieren wir zum Beispiel eine einfache Klasse und ihre Erweiterung

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

Und versuchen Sie es zu benutzen:

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

Und zeigen Sie, was sich hinter den Kulissen verbirgt:

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

Die Bibliothek " expand_me" manipuliert den Klassenerstellungsprozess über Metaklassen. Im obigen Beispiel erhalten MyCoolClasswir beim Erstellen einer neuen Instanz eine neue Klasse, die eine Unterklasse von beiden istMyCoolClassExtension und MyCoolClassdank der mehrfachen Vererbung von Python die Funktionalität beider

Zur besseren Kontrolle über die Klassenerstellung sind in dieser Bibliothek nur wenige Metaklassen definiert:

  • ExtensibleType - ermöglicht eine einfache Erweiterbarkeit durch Unterklassen

  • ExtensibleByHashType - Ähnlich wie ExtensibleType, jedoch mit der Fähigkeit, spezielle Klassenversionen zu erstellen, wodurch die globale Erweiterung der Basisklasse und die Erweiterung spezialisierter Klassenversionen ermöglicht werden

Diese Bibliothek wird in verwendet Bibliothek OpenERP Proxy Project verwendet und scheint gut genug zu funktionieren!

Ein echtes Anwendungsbeispiel finden Sie in der OpenERP-Proxy-Erweiterung 'field_datetime' :

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 hier ist extesible Objekt. RecordDateTimeist Erweiterung.

Um die Erweiterung zu aktivieren, importieren Sie einfach das Modul, das die Erweiterungsklasse enthält, und (im obigen Fall) alle Record Objekte, die danach erstellt wurden, haben die Erweiterungsklasse in Basisklassen und verfügen somit über alle Funktionen.

Der Hauptvorteil dieser Bibliothek besteht darin, dass Code, der erweiterbare Objekte betreibt, nicht über Erweiterungen Bescheid wissen muss und Erweiterungen alles in erweiterbaren Objekten ändern können.

Feuermagier
quelle
Ich denke, Sie wollen aus der Unterklasse instanziieren, dh my_cool_obj = MyCoolClassExtension1()anstelle vonmy_cool_obj = MyCoolClass()
Pylang
Nein, die erweiterbare Klasse hat die __new__Methode überschrieben , sodass sie automatisch alle Unterklassen findet und eine neue Klasse erstellt, dh eine Unterklasse von allen, und eine neue Instanz dieser erstellten Klasse zurückgibt. Daher muss die ursprüngliche Anwendung nicht über alle Erweiterungen Bescheid wissen. Dieser Ansatz ist beim Erstellen einer Bibliothek hilfreich, damit der Endbenutzer das Verhalten leicht ändern oder erweitern kann. Im obigen Beispiel kann MyCoolClass in der Bibliothek definiert und von dieser verwendet werden, und MyCoolClassExtension kann vom Endbenutzer definiert werden.
FireMage
Ein weiteres Beispiel wurde hinzugefügt, um zu antworten
FireMage
3

setuptools hat einen EntryPoint :

Einstiegspunkte sind eine einfache Möglichkeit für Distributionen, Python-Objekte (wie Funktionen oder Klassen) für die Verwendung durch andere Distributionen anzukündigen. Erweiterbare Anwendungen und Frameworks können nach Einstiegspunkten mit einem bestimmten Namen oder einer bestimmten Gruppe suchen, entweder aus einer bestimmten Distribution oder aus allen aktiven Distributionen auf sys.path, und dann die angekündigten Objekte nach Belieben überprüfen oder laden.

AFAIK Dieses Paket ist immer verfügbar, wenn Sie pip oder virtualenv verwenden.

guettli
quelle
2

Um die Antwort von @ edomaur zu erweitern, kann ich vorschlagen, einen Blick auf simple_plugins (schamloser Plug) zu werfen , ein einfaches Plugin-Framework, das von der Arbeit von Marty Alchin inspiriert wurde .

Ein kurzes Anwendungsbeispiel basierend auf der README-Datei des Projekts:

# 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ć
quelle
2

Ich habe Zeit damit verbracht, diesen Thread zu lesen, während ich ab und zu nach einem Plugin-Framework in Python gesucht habe. Ich habe einige verwendet, aber es gab Mängel bei ihnen. Folgendes habe ich mir für Ihre Prüfung im Jahr 2017 ausgedacht, ein schnittstellenfreies, lose gekoppeltes Plugin-Management-System: Laden Sie mich später . Hier finden Sie Tutorials zur Verwendung.

chfw
quelle
2

Sie können pluginlib verwenden .

Plugins sind einfach zu erstellen und können von anderen Paketen, Dateipfaden oder Einstiegspunkten geladen werden.

Erstellen Sie eine übergeordnete Plugin-Klasse und definieren Sie alle erforderlichen Methoden:

import pluginlib

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

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

Erstellen Sie ein Plugin, indem Sie eine übergeordnete Klasse erben:

import json

class JSON(Parser):
    _alias_ = 'json'

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

Laden Sie die Plugins:

loader = pluginlib.PluginLoader(modules=['sample_plugins'])
plugins = loader.plugins
parser = plugins.parser.json()
print(parser.parse('{"json": "test"}'))
aviso
quelle
1
Danke für das Beispiel. Ich habe mit 1 Frage gekämpft. Da Sie die Möglichkeit erwähnt haben, die Plugins aus verschiedenen Paketen zu laden, haben Sie vielleicht schon daran gedacht. Ich frage mich, wo die Elternklasse wohnen soll. Normalerweise wird empfohlen, es im Paket der Anwendung zu haben (vermutlich ein separates Quellcode-Repository), aber wie würden wir es dann in der Codebasis des Plugins erben? Importieren wir dafür die gesamte Anwendung? Oder ist es notwendig, den Schnittstellencode wie die Parser-Klasse oder ähnliche Abstraktionen in einem dritten Paket zu haben (das ein drittes Code-Repository wäre)?
JAponte
1
Die übergeordnete Klasse sollte sich in derselben Codebasis wie die Anwendung befinden, wahrscheinlich jedoch in ihrem eigenen Modul. Für ein aufgerufenes Paket foohaben Sie möglicherweise ein Modul namens, foo.parentsin dem Sie die übergeordneten Klassen definieren. Dann würden deine Plugins importieren foo.parents. Das funktioniert in den meisten Anwendungsfällen gut. Da 'foo' selbst auch importiert wird, um die Möglichkeit von zirkulären Importen zu vermeiden, lassen viele Projekte das Stammverzeichnis des Moduls leer und verwenden eine __main__.pyDatei oder Einstiegspunkte, um die Anwendung zu starten.
Aviso
1

Ich habe viel Zeit damit verbracht, ein kleines Plugin-System für Python zu finden, das meinen Anforderungen entspricht. Aber dann dachte ich nur, wenn es bereits eine Vererbung gibt, die natürlich und flexibel ist, warum nicht?

Das einzige Problem bei der Verwendung der Vererbung für Plugins besteht darin, dass Sie nicht wissen, welche Plugin-Klassen am spezifischsten sind (die niedrigsten im Vererbungsbaum).

Dies könnte jedoch mit einer Metaklasse gelöst werden, die die Vererbung der Basisklasse verfolgt, und möglicherweise eine Klasse erstellen, die von den meisten spezifischen Plugins erbt ('Root erweitert' in der folgenden Abbildung).

Geben Sie hier die Bildbeschreibung ein

Also kam ich mit einer Lösung, indem ich eine solche Metaklasse codierte:

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

Wenn Sie also eine Root-Basis haben, die mit einer Metaklasse erstellt wurde, und einen Baum von Plugins haben, die davon erben, können Sie automatisch eine Klasse erhalten, die von den spezifischsten Plugins erbt, indem Sie einfach Unterklassen erstellen:

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

Die Codebasis ist ziemlich klein (~ 30 Zeilen reiner Code) und so flexibel, wie es die Vererbung zulässt.

Wenn Sie interessiert sind, melden Sie sich unter https://github.com/thodnev/pluginlib

thodnev
quelle
1

Sie können sich auch Groundwork ansehen .

Die Idee ist, Anwendungen um wiederverwendbare Komponenten zu erstellen, die als Muster und Plugins bezeichnet werden. Plugins sind Klassen, die von abgeleitet sind GwBasePattern. Hier ist ein einfaches Beispiel:

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

Es gibt auch erweiterte Muster, um z. B. Befehlszeilenschnittstellen, Signalisierung oder gemeinsam genutzte Objekte zu handhaben.

Groundwork findet seine Plugins entweder durch programmgesteuertes Binden an eine App wie oben gezeigt oder automatisch über setuptools. Python-Pakete, die Plugins enthalten, müssen diese über einen speziellen Einstiegspunkt deklarieren groundwork.plugin.

Hier sind die Dokumente .

Haftungsausschluss : Ich bin einer der Autoren von Groundwork.

ub_marco
quelle
0

In unserem aktuellen Gesundheitsprodukt haben wir eine Plugin-Architektur mit Schnittstellenklasse implementiert. Unser Tech-Stack ist Django über Python für API und Nuxtjs über Nodejs für Frontend.

Wir haben eine Plugin-Manager-App für unser Produkt geschrieben, die im Grunde genommen ein Pip- und ein npm-Paket ist, das Django und Nuxtjs entspricht.

Für die Entwicklung neuer Plugins (pip und npm) haben wir den Plugin-Manager als Abhängigkeit festgelegt.

Im Pip-Paket: Mit Hilfe von setup.py können Sie den Einstiegspunkt des Plugins hinzufügen, um etwas mit dem Plugin-Manager (Registrierung, Initiierungen usw.) zu tun. Https://setuptools.readthedocs.io/en/latest/setuptools .html # automatische Skript-Erstellung

Im npm-Paket: Ähnlich wie bei pip gibt es Hooks in npm-Skripten, um die Installation durchzuführen. https://docs.npmjs.com/misc/scripts

Unser Anwendungsfall:

Das Plugin-Entwicklungsteam ist jetzt vom Kernteam für die Entwicklung getrennt. Der Umfang der Plugin-Entwicklung besteht in der Integration in Apps von Drittanbietern, die in einer der Produktkategorien definiert sind. Die Plugin-Schnittstellen sind kategorisiert nach: - Fax, Telefon, E-Mail ... usw. Der Plugin-Manager kann auf neue Kategorien erweitert werden.

In Ihrem Fall: Vielleicht können Sie ein Plugin schreiben lassen und dasselbe für Dinge wiederverwenden.

Wenn Plugin-Entwickler Kernobjekte wiederverwenden müssen, kann dieses Objekt durch eine Abstraktionsebene im Plugin-Manager verwendet werden, damit alle Plugins diese Methoden erben können.

Wenn Sie nur mitteilen, wie wir unser Produkt implementiert haben, erhalten Sie eine kleine Vorstellung davon.

Shankar Ganesh Jayaraman
quelle