Plugin-Mechanismus zur registration von Quiz-Typen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
_Scaui
User
Beiträge: 11
Registriert: Montag 17. April 2023, 19:47
Kontaktdaten:

Hallo zusammen.
ich schreibe gerade an einem Quiz-Programm (https://github.com/scaui0/QuizEnchanter).
Das Programm soll über Plugins eigene Quiztypen hinzufügen können.
Ein Plugin besteht dabei aus mindestens zwei Dateien: Einer `extension.json`, in der die Informationen über das Plugin stehen und einer Python-Datei, die über die `extension.json` referenziert wird.
In dieser Datei sollte man Decorator `@QuizType` einen Quiz-Typ erstellen können.
Nun das Problem: Wie finde ich alle Quiztypen, die in der Datei definiert sind?

Viele Grüße
_Scaui
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das kann doch dein QuizType erledigen, der kennt doch die Klasse & kann sie zB in einer globalen Liste der deklarierten Plugins anmelden. Wenn du schon dabei bist, kannst du auch noch Redundanz entfernen, und den Parameter aus der uebergebenen Klasse holen.
_Scaui
User
Beiträge: 11
Registriert: Montag 17. April 2023, 19:47
Kontaktdaten:

Das Problem ist nur, dass der QuizType aus der extension.py in einem anderen Namespace liegt und somit keine zusammenhängenden globalen Variablen möglich sind.

Eine andere Idee, die mir aber nicht gefällt, wäre, in der extension.json anzugeben, welche Klasse aus der extension.py für welche ID verwendet werden soll. Diese Klasse müsste dann über dynamische Importe und getattr in die Liste der registrierten QuizTypes eingefügt werden.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das verstehe ich nicht. QuizType ist doch aus *deinem* Repository, oder schreibt sich jeder seinen eigenen Dekorator? Und damit kann der doch problemlos zB eine Klassenvariable mit allen registrierten Plugins haben.

Code: Alles auswählen

class QuizType:
    ALL_PLUGINS = []
    
    def __init__(self, name, identifier):
        self.name = name
        self.identifier = identifier

    def __call__(self, func_or_class):
        self.ALL_PLUGINS.append(func_or_class)
        @wraps(func_or_class)
        def helper(*args, **kwargs):
            return func_or_class(*args, **kwargs)

        return helper

    def __str__(self):
        return f"<QuizType {self.identifier!r}>"
Ich habe das jetzt nur geprototypt, mir ist die Semantik von helper hier nicht klar, aber die Idee wird hoffentlich klar.
_Scaui
User
Beiträge: 11
Registriert: Montag 17. April 2023, 19:47
Kontaktdaten:

Das Problem an diesem Prototyp ist, dass keine globale Variable möglich ist, da das eine Skript dynamisch importiert wird.
Ich hab mal kurz ein Beispiel gebaut:

main.py

Code: Alles auswählen

import importlib.util
from pathlib import Path


class QuizType:
    all = []

    def __init__(self, cls):
        self.cls = cls
        QuizType.all.append(cls)
        print(f"{QuizType.all}")  # [<class 'plugin.Test1'>]

    def __call__(self, *args, **kwargs):
        return self.cls(*args, **kwargs)


if __name__ == '__main__':
    spec = importlib.util.spec_from_file_location("plugin", Path(__file__).parent/"plugin.py")
    modul = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(modul)

    print(f"At the End: {QuizType.all}")  # At the End: []
plugin.py

Code: Alles auswählen

import main


@main.QuizType
class Test1:
    pass
Mit globalen Variablen hat es allerdings geklappt.
Ich würde es aktuell so lösen, dass die Python-Main-Datei des Plugins eine Konstante QUIZ_TYPES angeben muss, in der die Quiz-Typen gespeichert sind.
Was haltet ihr von dieser Idee?

Viele Grüße
_Scaui
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Natuerlich ist eine globale Variable moeglich, auch bei einem dynamischen Skript. Dein Plugin haengt doch nicht in der Luft. Du kannst *IMMER* auf "meinquizprogramm.REGISTRIERTE_PLUGINS" zugreifen. So wie du auch immer auf "sys.stdout" zugreifen kannst, einer globalen Variablen, die den Standardausgabestrom beinhaltet. Darum verstehe ich dein Proqblem nicht.

Die Konstante im plugin geht auch, aber wenn du den Dekorator schon vorsiehst, kannst du das eben auch damit machen.
_Scaui
User
Beiträge: 11
Registriert: Montag 17. April 2023, 19:47
Kontaktdaten:

Sorry, wenn ich gerade auf dem Schlauch stehe. Deine Begründung leuchet mir ein, aber warum liefert dann das obige Skript diese Ausgabe?
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Weil du hier nicht mit einem, sondern *zwei* Modulen arbeitest: "main", das du via import geholt hast, und "__main__", welches das an den Interpreter uebergebene Skript implizit heisst, und damit auch ein eigener Namensraum ist.

Aender das Skript so, dann wird's hoffentlich klar:

Code: Alles auswählen

import importlib.util
from pathlib import Path


class QuizType:
    all = []

    def __init__(self, cls):
        self.cls = cls
        QuizType.all.append(cls)
        print(f"{QuizType.all}")  # [<class 'plugin.Test1'>]

    def __call__(self, *args, **kwargs):
        return self.cls(*args, **kwargs)


if __name__ == '__main__':
    spec = importlib.util.spec_from_file_location("plugin", Path(__file__).parent/"plugin.py")
    modul = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(modul)
    print(f"At the End: {QuizType.all}")  # At the End: []
    import main
    print(f"At the End: {main.QuizType.all}")
Das sind also auch ZWEI Klassen QuizType, __main__.QuizType, und main.QuizType.

Ein vernuenftiges Projekt wird ja aber auch nicht so gestartet, sondern hat einen console-scripts entry-point, und damit ist diese Ambiguitaet aufgeloest. Oder du hast das main.py nur, um ein "import meinprogramm;meinprogramm.main()" auszufuehren.
_Scaui
User
Beiträge: 11
Registriert: Montag 17. April 2023, 19:47
Kontaktdaten:

Danke für die Erklärung!
Das das __main__ etwas anderes ist als das main.py-Modul wusste ich nicht. Die Datei hab ich wollte ich nur in der Entwicklung so starten, was wohl keine gute Idee war.
Ich werde das Programm in Zukunft so ändern, dass es mit globalen Plugins arbeitet die von einem PluginManager verwaltet werden können.
Benutzeravatar
grubenfox
User
Beiträge: 434
Registriert: Freitag 2. Dezember 2022, 15:49

aus gegebenem Anlass mal das minimale Plugin-System, welches ich vor Jahren im Web fand (als Metaklasse, ähnlich zu der obigen Dekorator-Klasse):

Code: Alles auswählen

class PluginMount(type):
    def __init__(cls, name, bases, attrs):
        if not hasattr(cls, 'plugins'):
            # This branch only executes when processing the mount point itself.
            # So, since this is a new plugin type, not an implementation, this
            # class shouldn't be registered as a plugin. Instead, it sets up a
            # list where plugins can be registered later.
            cls.plugins = []
        else:
            # This must be a plugin implementation, which should be registered.
            # Simply appending it to the list is all that's needed to keep
            # track of it later.
            cls.plugins.append(cls)
Half mir damals das mit den Metaklassen zu verstehen...
Von Marty Alchim aus dem Jahr 2008 http://web.archive.org/web/202209280042 ... framework/

Hier in der Anwendung (genau so wie hier drüber das Beispiel von __deets__, also mit dem import main im Main-Abschnitt) :
main.py

Code: Alles auswählen

import importlib.util
from pathlib import Path

class PluginMount(type):
    def __init__(cls, name, bases, attrs):
        if not hasattr(cls, 'plugins'):
            # This branch only executes when processing the mount point itself.
            # So, since this is a new plugin type, not an implementation, this
            # class shouldn't be registered as a plugin. Instead, it sets up a
            # list where plugins can be registered later.
            cls.plugins = []
        else:
            # This must be a plugin implementation, which should be registered.
            # Simply appending it to the list is all that's needed to keep
            # track of it later.
            cls.plugins.append(cls)


class QuizType(metaclass = PluginMount):
    pass


if __name__ == '__main__':
    spec = importlib.util.spec_from_file_location("plugin", Path(__file__).parent/"plugin.py")
    modul = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(modul)

    import main

    print(f"At the End: {main.QuizType.plugins}")  # At the End: [<class 'plugin.Test1'>]
das Modul mit einem Plugin ist so gut wie identisch mit dem Plugin-Modul etwas weiter oben von _Scaui
plugin.py

Code: Alles auswählen

import main

class Test1(main.QuizType):
    pass

Wenn ich das mal in der Vergangenheit selbst eingesetzt hatte, dann mit einer kleinen Änderung gegenüber dem Original. In der Klasse PluginMount wurden die Plugins nicht in einer Liste gespeichert, sondern mit Namen versehen landeten die Plugins in einem Dictionary.
So konnte ich über den jeweiligen Namen auch gezielt auf einzelne Plugins zugreifen und hatte nicht nur eine Liste von allen Plugins zur Verfügung...
Antworten