Vorstellung eines einfachen Plugin Systems für Python

Geschrieben von Eric Scheibler am 28.02.2011

Plugins sind vereinfacht gesagt einzelne Abschnitte eines Programms, die nachträglich in das Hauptprogramm integriert werden. So kann der Funktionsumfang einer Application unkompliziert erweitert werden. Ein populäres Beispiel hierfür sind sicherlich die Firefox Addons. Da der Code des Plugins bei der Erstellung des Hauptprogramms noch nicht bekannt ist, muss er zur Laufzeit nachgeladen werden. Im folgenden möchte ich solch ein simples System zur Plugin Verwaltung vorstellen. Dies ist in Python geschrieben. Das Prinzip sollte sich aber auch auf andere Programmier - und Skriptsprachen anwenden lassen.

Vorbereitungen

Zuerst erstellt man sich einen Unterordner, in dem die Plugins liegen sollen:

mkdir plugins

Im gerade erstellten Plugins Ordner legt man die datei init.py an. Diese kann leer sein:

touch plugins/<u>_init_</u>.py

Fehlt diese Datei, so durchwandert das Hauptprogramm den Plugins Ordner nicht und die Klassen können nicht nachgeladen werden.

Die abstrakte Klasse

Als nächstes wird die abstrakte Klasse Plugin erzeugt. Diese definiert die Struktur der späteren Plugins. Die Funktionen, die in der abstrakten Klasse definiert werden, müssen in allen anderen Klassen dann implementiert werden

nano plugins/plugin.py
#!/usr/bin/env python

class Plugin(object):
"""
abstract plugin class
I only defined one function called help
"""
def help(self):
raise NotImplementedError

Die Klasse Plugin erbt von “Object” und definiert genau eine Funktion namens “help”. Die letzte Zeile sorgt dafür, dass eine Exception namens “NotImplementedError” ausgelöst wird, wenn das konkrete Plugin diese Funktion nicht implementiert hat (dazu gleich mehr).

In Python werden Blöcke nicht mit Klammern sondern durch Einrückungen abgegrenzt. Daher muss man beim Schreiben eines Python Programms besonders auf die richtige Zeileneinrückung achten. Ich rücke die Blöcke jeweils um 2 Leerzeichen ein, da der Platz auf einer 40 Zeichen langen Braillezeile begrenzt ist (Standrd sind 4 Leerzeichen). Demnach ist alles, was zur Klasse “Plugin” gehört um 2 Leerzeichen eingerückt und die Zeile “raise NotImplementedError” um 4, da sie zur Funktion “help” gehört.

Ein Beispielplugin

Nachdem die abstrakte Klasse definiert wurde, kann man jetzt ein konkretes Plugin von ihr ableiten:

nano plugins/example1.py
#!/usr/bin/env python

# import abstract class Plugin
# from foldername.filename (without extension) import classname
from plugins.plugin import Plugin

class ExamplePlugin1(Plugin):
def help(self):
return "This is the help text of the first plugin."

Die Klasse ExamplePlugin1 erbt von der gerade definierten abstrakten Klasse Plugin. Damit Plugin auch gefunden wird, muss man die Klasse importieren, was bei der “import …” Anweisung geschieht. Anschließend wird die geforderte Funktion “help” implementiert. In diesem Beispiel reicht es, wenn sie einen kurzen String zurückgibt.

Hauptprogramm

Jetzt fehlt noch der Hauptteil, der die Plugins zur Laufzeit nachlädt:

nano main.py
#!/usr/bin/env python

import os
from plugins.plugin import Plugin

# at first, define the function which imports the classes in the plugins folder
def find_subclasses(path, cls):
"""
Find all subclass of cls in py files located below path
(does look in sub directories)

@param path: the path to the top level folder to walk
@type path: str
@param cls: the base class that all subclasses should inherit from
@type cls: class
@rtype: list
@return: a list if classes that are subclasses of cls
"""

subclasses=[]

def look_for_subclass(modulename):
module=<u>_import_</u>(modulename)

#walk the dictionaries to get to the last one
d=module.<u>_dict_</u>
for m in modulename.split('.')[1:]:
d=d[m].<u>_dict_</u>

#look through this dictionary for things
#that are subclass of Job
#but are not Job itself
for key, entry in d.items():
if key == cls.<u>_name_</u>:
continue

try:
if issubclass(entry, cls):
print("Found subclass: "+key)
subclasses.append(entry)
except TypeError:
#this happens when a non-type is passed in to issubclass. We
#don't care as it can't be a subclass of Job if it isn't a
#type
continue

# walk through the given path and take all the .py files
# the plugins are loaded in alphabetical order
for root, dirs, files in os.walk(path):
sorted_files = []
for filename in files:
# collect the files in the plugins folder
sorted_files.append(filename)
sorted_files.sort()
for name in sorted_files:
if name.endswith(".py") and not name.startswith("__"):
path = os.path.join(root, name)
modulename = path.rsplit('.', 1)[0].replace('/', '.')
look_for_subclass(modulename)

return subclasses

# here is the beginning of the program
# the find_subclasses function gets the path to the plugins folder and the name of the abstract class Plugin which I imported above
imported_modules = find_subclasses("plugins", Plugin)

# lets create an instance of every found class and call the help funktion
for i in range(len(imported_modules)):
print("Help: " + imported_modules[i]().help())

Nach den import Anweisungen wird die Funktion “find_subclasses” definiert. Diese bekommt den Ordner, in dem die Plugins liegen übergeben und durchwandert ihn auf der Suche nach .py Dateien. Dabei werden auch mögliche Unterordner mit einbezogen. Da ich die Plugins gern alphabetisch importieren möchte wird die Dateiliste vorher noch sortiert. Nachdem alle Dateien in dem Ordner abgearbeitet wurden, gibt die Funktion die gefundenen Klassen in einem Array zurück. Jeder Eintrag beinhaltet die Instanz eines Plugins. Zum Schluss wandert man mit der For - Schleife einmal durch das Array, ruft jeweils die “help” Funktion auf und gibt diese in der Konsole aus.

Python installieren

Um das Beispiel zu starten, benötigt man Python. Unter Debian kann man es mit folgendem Befehl installieren:

# apt-get install python

Hat man bereits das Upgrade auf Debian Squeeze durchgeführt, wird Python 2.6.6 installiert. Sollte man eine neuere Version benötigen oder die Installation aus anderen Gründen nicht funktionieren, so kann man sich Python auch von http://www.python.org/download/ herunterladen. Für Windows und Mac OS gibts fertige Installer. Linux:

tar xfvz Python-2.7.1.tgz
cd Python-2.7.1
./configure
make
# make install

Das Beispiel ausführen

Nach der Installation wechselt man wieder in das Verzeichnis, in dem die main.py liegt und startet sie via:

python main.py

Nimmt man mein an diesen Beitrag angehängtes Beispiel, so müsste der Output in etwa so aussehen:

Found subclass: ExamplePlugin1
Found subclass: ExamplePlugin2
Help: This is the help text of the first plugin.
Help: The second plugin returns this example text.

Download des gesamten Beispiels: pythonplugins.zip

Quellen