Budowanie minimalnej architektury wtyczek w Pythonie

190

Mam aplikację napisaną w języku Python, która jest używana przez dość technicznych odbiorców (naukowców).

Szukam dobrego sposobu na rozszerzenie aplikacji przez użytkowników, tj. Architekturę skryptów / wtyczek.

Szukam czegoś wyjątkowo lekkiego . Większość skryptów lub wtyczek nie będzie opracowywana i dystrybuowana przez firmę zewnętrzną i instalowana, ale za kilka minut zostanie wymyślona przez użytkownika w celu zautomatyzowania powtarzającego się zadania, dodania obsługi formatu pliku, itd. Tak więc wtyczki powinny mieć absolutnie minimalny kod typu „kocioł” i nie wymagają „instalacji” innej niż kopiowanie do folderu (więc coś w rodzaju punktów wejścia do setuptools lub architektura wtyczek Zope wydaje się zbyt duża).

Czy istnieją już takie systemy lub projekty, które wdrażają podobny schemat, na który powinienem szukać pomysłów / inspiracji?

dF.
źródło

Odpowiedzi:

150

Mój jest w zasadzie katalogiem zwanym „wtyczkami”, które główna aplikacja może odpytywać, a następnie używać imp.load_module do pobierania plików, szukania dobrze znanego punktu wejścia, ewentualnie z parametrami konfiguracji na poziomie modułu, i od tego momentu. Używam rzeczy do monitorowania plików dla pewnej dynamiki, w której wtyczki są aktywne, ale to miło mieć.

Oczywiście każdy wymóg, który pojawia się, mówiąc: „Nie potrzebuję [wielkiej, skomplikowanej rzeczy] X; Chcę tylko czegoś lekkiego”, wiąże się z ryzykiem ponownej implementacji X jednego wykrytego wymagania naraz. Ale to nie znaczy, że i tak nie możesz się dobrze bawić :)

TJG
źródło
26
Wielkie dzięki! Napisałem mały samouczek na podstawie twojego postu: lkubuntu.wordpress.com/2012/10/02/writing-a-python-plugin-api
MiJyn
9
impModuł jest zastąpiona importlibpocząwszy od Pythona 3.4
b0fh
1
W wielu przypadkach użycia możesz użyć importlib.import_module jako zamiennika 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)

Z pewnością jest „minimalny”, absolutnie nie ma sprawdzania błędów, prawdopodobnie niezliczone problemy bezpieczeństwa, nie jest zbyt elastyczny - ale powinien pokazać, jak prosty może być system wtyczek w Pythonie ..

Prawdopodobnie chcesz zajrzeć do imp modułu też, choć można zrobić wiele z zaledwie __import__, os.listdira niektóre łańcuchach.

dbr
źródło
4
Myślę, że może chcesz zmienić def call_plugin(name, *args)na def call_plugin(name, *args, **kwargs), a następnie plugin.plugin_main(*args)doplugin.plugin_main(*args, **kwargs)
Ron Klein
12
W python 3 impjest przestarzały na korzyśćimportlib
Adama Baxtera
25

Chociaż to pytanie jest naprawdę interesujące, myślę, że trudno jest odpowiedzieć bez dodatkowych szczegółów. Co to za aplikacja? Czy ma GUI? Czy to narzędzie wiersza polecenia? Zestaw skryptów? Program z unikalnym punktem wejścia itp.

Biorąc pod uwagę niewiele informacji, które posiadam, odpowiem w bardzo ogólny sposób.

Co oznacza, że ​​musisz dodać wtyczki?

  • Prawdopodobnie będziesz musiał dodać plik konfiguracyjny, który będzie zawierał ścieżki / katalogi do załadowania.
  • Innym sposobem byłoby powiedzenie „załadowane zostaną wszystkie pliki w tym katalogu wtyczki / katalogu”, ale niewygodne jest wymaganie od użytkowników przemieszczania się po plikach.
  • Ostatnią, pośrednią opcją byłoby wymaganie, aby wszystkie wtyczki znajdowały się w tym samym folderze wtyczka / folder, a następnie aktywować / dezaktywować je przy użyciu ścieżek względnych w pliku konfiguracyjnym.

W praktyce opartej na czystym kodzie / projektowaniu musisz jasno określić, jakie zachowania / konkretne działania chcesz rozszerzyć od użytkowników. Zidentyfikuj wspólny punkt wejścia / zestaw funkcji, które zawsze zostaną zastąpione, i określ grupy w ramach tych działań. Po wykonaniu tej czynności rozszerzenie aplikacji powinno być łatwe,

Przykład użycia hooków , zainspirowanych MediaWiki (PHP, ale czy język naprawdę ma znaczenie?):

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)

Kolejny przykład, inspirowany rtęcią. W tym przypadku rozszerzenia dodają tylko polecenia do pliku wykonywalnego wiersza polecenia hg , przedłużając zachowanie.

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 }

W przypadku obu podejść może być konieczne wspólne zainicjowanie i zakończenie rozszerzenia. Możesz albo użyć wspólnego interfejsu, który wszystkie rozszerzenia będą musiały zaimplementować (lepiej pasuje do drugiego podejścia; mercurial używa reposetup (interfejsu użytkownika, repo), który jest wywoływany dla wszystkich rozszerzeń), lub zastosować podejście typu hook, z hooks.setup hook.

Ale znowu, jeśli chcesz bardziej przydatnych odpowiedzi, musisz zawęzić swoje pytanie;)

Nicolas Dumazet
źródło
11

Prosta struktura wtyczek Marty'ego Allchina jest bazą, której używam do własnych potrzeb. Naprawdę polecam przyjrzeć się temu, myślę, że to naprawdę dobry początek, jeśli chcesz czegoś prostego i łatwego do włamania. Możesz go również znaleźć jako fragmenty Django .

edomaur
źródło
Próbuję zrobić coś takiego z pyduckiem jako podstawą.
edomaur,
Z tego, co mogę powiedzieć, jest on bardzo specyficzny dla Django.
Zoran Pavlovic
3
@ZoranPavlovic: wcale, niektóre linie standardowego Pythona, nie musisz używać Django.
edomaur
11

Jestem emerytowanym biologiem, który zajmował się cyfrowymi mikrografami i stwierdził, że musi napisać pakiet do przetwarzania obrazu i analizy (nie technicznie biblioteki), aby uruchomić go na maszynie SGi. Napisałem kod w C i użyłem Tcl jako języka skryptowego. GUI, tak jak było, zostało wykonane przy użyciu Tk. Polecenia, które pojawiły się w Tcl, miały postać „rozszerzenieNazwa nazwa polecenia arg0 arg1 ... param0 param1 ...”, to znaczy proste słowa i liczby oddzielone spacjami. Gdy Tcl zobaczył podciąg „extensionName”, kontrola została przekazana do pakietu C. To z kolei uruchomiło polecenie przez lexer / parser (wykonane w lex / yacc), a następnie w razie potrzeby wywołało procedury C.

Polecenia do obsługi pakietu można uruchamiać pojedynczo za pośrednictwem okna w graficznym interfejsie użytkownika, ale zadania wsadowe były wykonywane przez edycję plików tekstowych, które były poprawnymi skryptami Tcl; wybrałeś szablon, który wykonał rodzaj operacji na poziomie pliku, którą chcesz wykonać, a następnie edytowałeś kopię, aby zawierała rzeczywisty katalog i nazwy plików oraz polecenia pakietu. Działa jak urok. Aż do ...

1) Świat zwrócił się ku komputerom PC i 2) skrypty wydłużyły się do około 500 linii, gdy słabe możliwości organizacyjne Tcl stały się prawdziwą niedogodnością. Czas minął ...

Przeszedłem na emeryturę, wymyśliłem Pythona i wyglądało to na idealnego następcę Tcl. Teraz nigdy nie robiłem portu, ponieważ nigdy nie stawiałem czoła wyzwaniom kompilacji (dość dużych) programów C na PC, rozszerzania Pythona za pomocą pakietu C i robienia GUI w Pythonie / Gt? / Tk? /? ? Jednak stary pomysł posiadania edytowalnych skryptów szablonów nadal wydaje się praktyczny. Ponadto nie powinno być zbyt wielkim obciążeniem wprowadzanie poleceń pakietu w natywnej formie Pythona, np .:

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

Kilka dodatkowych kropek, parenów i przecinków, ale to nie są wisiorki.

Pamiętam, że ktoś napisał wersje lex i yacc w Pythonie (spróbuj: http://www.dabeaz.com/ply/ ), więc jeśli są one nadal potrzebne, są w pobliżu.

Chodzi mi o to, że wydawało mi się, że sam Python JEST pożądanym „lekkim” frontem używanym przez naukowców. Jestem ciekawy, dlaczego uważasz, że tak nie jest, i mam na myśli to poważnie.


dodane później: Aplikacja gedit przewiduje dodawanie wtyczek, a ich witryna zawiera wyjaśnienie prostej procedury wtyczek, którą znalazłem w ciągu kilku minut rozglądania się. Próbować:

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

Nadal chciałbym lepiej zrozumieć twoje pytanie. Nie jestem pewien, czy 1) chcesz, aby naukowcy mogli korzystać z twojej aplikacji (Python) po prostu na różne sposoby, czy 2) chcesz, aby naukowcy mogli dodawać nowe możliwości do twojej aplikacji. Wybór nr 1 to sytuacja, w której mieliśmy do czynienia z obrazami, i to doprowadziło nas do użycia ogólnych skryptów, które zmodyfikowaliśmy, aby dopasować do potrzeb chwili. Czy to Choice # 2 prowadzi do idei wtyczek, czy jest to jakiś aspekt twojej aplikacji, który sprawia, że ​​wydawanie poleceń jest niewykonalne?

za opadaniem
źródło
2
Naprawa zgnilizny linków: wtyczka Gedit jest teraz - wiki.gnome.org/Apps/Gedit/PythonPluginHowTo
ohhorob
1
To jest piękny post, ponieważ pokazuje jasno i zwięźle, jakim szczęściem są nasi współcześni biolodzy. Dla niego / niej python jest modułowym językiem skryptowym używanym do nadania programistom modułów pewnej abstrakcji, aby nie musieli analizować głównego kodu C. Teraz jednak niewielu biologów nauczy się C, robiąc wszystko w Pythonie. W jaki sposób usuwamy złożoność naszych głównych programów w Pythonie podczas pisania modułów? Za 10 lat być może programy będą pisane w Emoji, a moduły będą tylko plikami audio zawierającymi serię pomruków. I może to jest OK.
JJ
10

Podczas wyszukiwania Dekoratorów Pythona znalazłem prosty, ale przydatny fragment kodu. To może nie pasować do twoich potrzeb, ale bardzo inspirujące.

Scipy Advanced Python # Plugin System rejestracji

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

Stosowanie:

processor = TextProcessor()
processed = processor.process(text="**foo bar**", plugins=(CleanMarkdownBolds, ))
processed = processor.process(text="**foo bar**")
guneysus
źródło
1
Uwaga: W tym przykładzie WordProcessor.pluginnic nie zwraca ( None), więc CleanMdashesExtensionpóźniej importowanie klasy jest po prostu importem None. Jeśli klasy wtyczek są przydatne same w sobie, utwórz .pluginmetodę class return plugin.
jkmacc
@jkmacc Masz rację. Zmodyfikowałem fragment 13 dni po Twoim komentarzu. Dziękuję Ci.
guneysus
7

Podobała mi się miła dyskusja na temat różnych architektur wtyczek wygłoszona przez dr Andre Roberge na Pycon 2009. Daje dobry przegląd różnych sposobów implementacji wtyczek, zaczynając od czegoś naprawdę prostego.

Jest dostępny jako podcast (druga część po wyjaśnieniu łatania małp) wraz z serią sześciu wpisów na blogu .

Zalecam szybkie posłuchanie, zanim podejmiesz decyzję.

Jon Mills
źródło
4

Przybyłem tutaj, szukając minimalnej architektury wtyczek i znalazłem wiele rzeczy, które wydawały mi się przesadne. Wdrożyłem więc Super Simple Python Plugins . Aby z niego skorzystać, tworzysz jeden lub więcej katalogów i upuszczasz specjalny __init__.pyplik w każdym z nich. Zaimportowanie tych katalogów spowoduje, że wszystkie inne pliki Pythona zostaną załadowane jako podmoduły, a ich nazwy zostaną umieszczone na __all__liście. Następnie do ciebie należy sprawdzenie / zainicjowanie / rejestracja tych modułów. W pliku README jest przykład.

samwyse
źródło
4

W rzeczywistości setuptools działa z „katalogiem wtyczek”, jak w poniższym przykładzie z dokumentacji projektu: http://peak.telecommunity.com/DevCenter/PkgResources#locating-plugins

Przykładowe użycie:

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)

W dłuższej perspektywie setuptools jest znacznie bezpieczniejszym wyborem, ponieważ może ładować wtyczki bez konfliktów i brakujących wymagań.

Kolejną zaletą jest to, że same wtyczki można rozszerzyć przy użyciu tego samego mechanizmu, bez konieczności dbania o to przez oryginalne aplikacje.

ankostis
źródło
3

Jako wzajemne podejście do systemu wtyczek możesz sprawdzić projekt Extend Me .

Na przykład zdefiniujmy prostą klasę i jej rozszerzenie

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

I spróbuj użyć:

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

I pokaż, co kryje się za sceną:

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

biblioteka ext_me manipuluje procesem tworzenia klas za pomocą metaklas, dlatego w powyższym przykładzie podczas tworzenia nowej instancji MyCoolClassotrzymaliśmy instancję nowej klasy, która jest podklasą obu MyCoolClassExtensioni MyCoolClassma funkcjonalność obu z nich, dzięki wielokrotnemu dziedziczeniu Pythona

Aby uzyskać lepszą kontrolę nad tworzeniem klas, w tej bibliotece zdefiniowano kilka metaklas:

  • ExtensibleType - umożliwia prostą rozszerzalność poprzez podklasowanie

  • ExtensibleByHashType - podobny do ExtensibleType, ale posiadający zdolność do budowania wyspecjalizowanych wersji klasy, umożliwiający globalne rozszerzenie klasy podstawowej i rozszerzenie specjalistycznych wersji klasy

Ta biblioteka jest używana w OpenERP Proxy Project i wydaje się, że działa wystarczająco dobrze!

Prawdziwy przykład użycia znajduje się w rozszerzeniu OpenERP Proxy „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

Recordtutaj jest obiekt rozciągliwy. RecordDateTimejest rozszerzeniem.

Aby włączyć rozszerzenie, wystarczy zaimportować moduł zawierający klasę rozszerzenia i (w powyższym przypadku) wszystkie Recordobiekty utworzone po nim będą miały klasę rozszerzenia w klasach podstawowych, a tym samym będą mieć całą swoją funkcjonalność.

Główną zaletą tej biblioteki jest to, że kod obsługujący obiekty rozszerzalne nie musi wiedzieć o rozszerzeniu, a rozszerzenia mogą zmieniać wszystko w obiektach rozszerzalnych.

Mag ognia
źródło
Myślę, że masz zamiar utworzyć instancję z podklasy, tj. my_cool_obj = MyCoolClassExtension1()Zamiastmy_cool_obj = MyCoolClass()
pylang
nie, klasa rozszerzalna ma nadpisaną __new__metodę, więc automatycznie wyszukuje wszystkie podklasy i buduje nową klasę, czyli podklasę wszystkich z nich, i powraca nowa instancja tej utworzonej klasy. Dlatego oryginalna aplikacja nie musi wiedzieć o wszystkich rozszerzeniach. takie podejście jest przydatne podczas budowania biblioteki, aby użytkownik końcowy mógł łatwo modyfikować lub rozszerzać swoje zachowanie. w powyższym przykładzie MyCoolClass może być zdefiniowany w bibliotece i przez niego używany, a MyCoolClassExtension może być zdefiniowany przez użytkownika końcowego.
FireMage
Do odpowiedzi dodano jeszcze jeden przykład
FireMage
3

setuptools ma EntryPoint :

Punkty wejścia są prostym sposobem dystrybucji dla „reklamowania” obiektów Pythona (takich jak funkcje lub klasy) do wykorzystania przez inne dystrybucje. Rozszerzalne aplikacje i frameworki mogą wyszukiwać punkty wejścia o określonej nazwie lub grupie, z określonej dystrybucji lub ze wszystkich aktywnych dystrybucji na sys.path, a następnie sprawdzać lub ładować reklamowane obiekty do woli.

AFAIK ten pakiet jest zawsze dostępny, jeśli używasz pip lub virtualenv.

guettli
źródło
2

Rozwijając odpowiedź @ edomaura, mogę zasugerować przyjrzenie się simple_plugins (bezwstydnej wtyczce), która jest prostym frameworkiem wtyczek zainspirowanym pracą Marty'ego Alchina .

Krótki przykład użycia na podstawie README projektu:

# 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ć
źródło
2

Możesz użyć pluginlib .

Wtyczki można łatwo tworzyć i można je ładować z innych pakietów, ścieżek plików lub punktów wejścia.

Utwórz klasę nadrzędną wtyczki, definiując wszelkie wymagane metody:

import pluginlib

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

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

Utwórz wtyczkę, dziedzicząc klasę nadrzędną:

import json

class JSON(Parser):
    _alias_ = 'json'

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

Załaduj wtyczki:

loader = pluginlib.PluginLoader(modules=['sample_plugins'])
plugins = loader.plugins
parser = plugins.parser.json()
print(parser.parse('{"json": "test"}'))
aviso
źródło
1
Dzięki za przykład. Walczę z 1 pytaniem. Ponieważ wspomniałeś o możliwości ładowania wtyczek z różnych pakietów, być może już o tym pomyślałeś. Zastanawiam się, gdzie powinna przebywać klasa rodzicielska. Zwykle zaleca się, aby mieć go w pakiecie aplikacji (przypuszczalnie oddzielne repozytorium kodu źródłowego), ale w takim razie w jaki sposób moglibyśmy odziedziczyć po nim w bazie kodu wtyczki? Czy importujemy w tym celu całą aplikację? Czy też konieczne jest posiadanie kodu interfejsu, takiego jak klasa Parser lub podobne abstrakcje, w trzecim pakiecie (który byłby repozytorium trzeciego kodu)?
JAponte
1
Klasa nadrzędna powinna znajdować się w tej samej bazie kodu co aplikacja, ale prawdopodobnie we własnym module. Tak więc w przypadku pakietu o nazwie foomożesz mieć moduł o nazwie, w foo.parentsktórym definiujesz klasy nadrzędne. Następnie wtyczki zostaną zaimportowane foo.parents. Działa to dobrze w większości przypadków użycia. Ponieważ samo „foo” również jest importowane, aby uniknąć możliwości importowania cyklicznego, wiele projektów pozostawia katalog główny modułu pusty i używa __main__.pypliku lub punktów wejścia do uruchomienia aplikacji.
aviso
1

Spędziłem dużo czasu próbując znaleźć mały system wtyczek dla Pythona, który pasowałby do moich potrzeb. Ale potem pomyślałem, że jeśli jest już dziedzictwo, które jest naturalne i elastyczne, dlaczego go nie użyć.

Jedynym problemem związanym z używaniem dziedziczenia dla wtyczek jest to, że nie wiesz, jakie są najbardziej szczegółowe (najniższe na drzewie dziedziczenia) klasy wtyczek.

Można to jednak rozwiązać za pomocą metaklasy, która śledzi dziedziczenie klasy podstawowej, i ewentualnie mogłaby zbudować klasę, która dziedziczy z najbardziej określonych wtyczek („Root extension” na poniższym rysunku)

wprowadź opis zdjęcia tutaj

Więc znalazłem rozwiązanie, kodując taką metaklasę:

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

Więc jeśli masz bazę Root wykonaną z metaklasy i masz drzewo wtyczek, które z niej dziedziczą, możesz automatycznie uzyskać klasę, która dziedziczy z najbardziej specyficznych wtyczek po prostu poprzez podklasowanie:

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

Baza kodu jest dość mała (~ 30 linii czystego kodu) i tak elastyczna, jak pozwala na to dziedziczenie.

Jeśli jesteś zainteresowany, zaangażuj się @ https://github.com/thodnev/pluginlib

Thodnev
źródło
1

Możesz także rzucić okiem na Groundwork .

Chodzi o to, aby budować aplikacje wokół komponentów wielokrotnego użytku, zwanych wzorcami i wtyczkami. Wtyczki są klasami, które pochodzą GwBasePattern. Oto podstawowy przykład:

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

Istnieją również bardziej zaawansowane wzorce do obsługi np. Interfejsów linii poleceń, sygnalizacji lub współdzielonych obiektów.

Groundwork znajduje wtyczki albo programowo, wiążąc je z aplikacją, jak pokazano powyżej, lub automatycznie za pośrednictwem setuptools. Pakiety Python zawierające wtyczki muszą je deklarować przy użyciu specjalnego punktu wejścia groundwork.plugin.

Oto dokumenty .

Uwaga : Jestem jednym z autorów Groundwork.

ub_marco
źródło
0

W naszym obecnym produkcie opieki zdrowotnej mamy architekturę wtyczek zaimplementowaną z klasą interfejsu. Nasz stos technologii to Django na Pythonie dla API i Nuxtjs na nodejs dla frontendu.

Mamy aplikację menedżera wtyczek napisaną dla naszego produktu, która jest w zasadzie pakietem pip i npm, zgodnym z Django i Nuxtjs.

Do tworzenia nowych wtyczek (pip i npm) stworzyliśmy menedżera wtyczek jako zależność.

W pakiecie Pip: Za pomocą setup.py możesz dodać punkt wejścia wtyczki, aby zrobić coś z menedżerem wtyczek (rejestr, inicjacje, ... itp.) Https://setuptools.readthedocs.io/en/latest/setuptools .html # automatyczne tworzenie skryptów

W pakiecie npm: Podobnie do pipa, w skryptach npm znajdują się haki do obsługi instalacji. https://docs.npmjs.com/misc/scripts

Nasz przypadek użycia:

zespół programistów wtyczek jest teraz niezależny od zespołu programistów. Opracowywanie wtyczek ma na celu integrację z aplikacjami innych firm, które są zdefiniowane w dowolnej kategorii produktu. Interfejsy wtyczek są podzielone na następujące kategorie: - Faks, telefon, e-mail ... itd. Menedżer wtyczek można rozszerzyć o nowe kategorie.

W twoim przypadku: Może możesz napisać jedną wtyczkę i użyć jej ponownie do robienia rzeczy.

Jeśli twórcy wtyczek muszą użyć ponownie podstawowych obiektów, obiekt ten może zostać wykorzystany poprzez wykonanie poziomu abstrakcji w menedżerze wtyczek, aby wszelkie wtyczki mogły odziedziczyć te metody.

Po prostu dzieląc się tym, jak wdrożyliśmy nasz produkt, mamy nadzieję, że da to mały pomysł.

Shankar Ganesh Jayaraman
źródło