Найпростіша plugin-архітектура на python

Так вже вийшло, що мені вдруге довелося писати модульну систему на пітоні. Причому вдруге довелося йти по одних і тих самих граблях, бо приклад минулої реалізації залишився на ноуті, який зараз лежить в сервісному центрі. Шлях нижче не являється оптимальним чи там найкращим, але головне, що він 100% робочий і задовольняє моїм потребам :) А потреби полягають в тому, що кожен плагін представляє собою реалізацію класу з певним набором функцій та має засіб для інстанціювання об’єкту цього класу не знаючи його імені. Отож…

Для початку створимо приблизно наступну систему підкаталогів з файлами, а потім почнемо її наповнювати:

+ / plugins
| |
| + / plugin1
| | |
| | - __init__.py
| |
| + / plugin2
| | |
| | - __init__.py
| |
| - __init__.py
|
- pluginmanager.py

Як неважко здогадатись, модуль plugingmanager.py – буде відповідати за завантаження плагінів. Папка plugins міститиме власне плагіни (кожен з яких буде займати окремий каталог) та файл ініціалізації пакету “plugins” (файл __init__.py) – його можна лишити порожнім, оскільки надпакет plugins використовується лише для групування.

#!/usr/bin/python
import os

plugins = []

def LoadModule(module):
    # build package name
    packagename = "plugins." + module
    # using built-in function __import__() to dynamically load modules; ensure that
    # directory containing "plugins" folder exists in python's sys.path list
    # if not, it can be specified by sys.path.append(path_to_plugins) command before calling __import__()
    mod = __import__(packagename, globals(), locals(), [])

    # if module import was successful
    if ( mod ):
        # try to get the plugin module itself
        components = packagename.split('.')
        for component in components[1:]:
            mod = getattr(mod, component)
        # add the instance of plugin-implemented class to the list
        plugins.append(mod.CreateInstance())

# dynamicaly load plugin packages
def LoadPlugins(pluginpath):
    # enum plugins directory
    for dir in os.listdir(pluginpath + "\\plugins"):
        # for each non-hidden file or directory
        if (dir[0] != "_") and (dir.endswith (".py")):
            # try to load corresponding plugin
            LoadModule(dir)

LoadPlugins(".")
for plugin in plugins:
	plugin.Say("Joey")

Сподіваюсь, коментарі зрозумілі (хоч і англомовні :) ). Отож, ми перебираємо папку з плагінами і для кожого файлу (тобто в нашому випадку каталогу) намагаємось імпортувати його по імені. В пітоні завдяки вищенаведеній структурі будується дерево пакетів. Тобто в нашому випадку у нас є пакет верхнього рівня plugins, який має два підпакети: plugins.plugin1 та plugins.plugin2. Для імпорту використовується вбудована функція __import__(). Вона повертає клас модуля верхнього рівня (в нашому випадку – “plugins”), але оскільки нам потрібно дійти до пакета нижнього рівня ми “занурюємось” далі використовуючи рекурсивно функцію attrib. В нашому випадку, звісно, можна було зробити простіше, оскільки вкладеність тут має лише один рівень і назва пакету будується в цій же функції, але код я трохи ускладнив спеціально, щоб дати більш загальну та гнучку реалізацію завантаження пакетів чи модулів.

Отримавши клас модуля ми викликаємо його глобальну функцію CreateInstance(), що поверне нам екземпляр реалізованого в модулі класу.

Тепер розглянемо реалізацію власне плагінів. Для нескладних випадків ми можемо запихнути її прямо в опис пакету plugin1 – файл __init__.py:

#!/usr/bin/python

class Plugin1():
	def Say(self, name):
		print "Hello, %s!" % name

def CreateInstance():
	return Plugin1()

По аналогії можна зробити і реалізацію plugin2. Тепер запустивши програму отримаємо наступне:

$ ./pluginmanager.py
Hello, Joey!
Konnichiwa, Joey!

Набір відповідних плагінів можна тепер збільшувати просто додаючи по аналогії інші підкаталоги-пакети. Звісно, можна переробити це все з підтримкою модулів, а не пакетів (чи не лише пакетів), але мені все ж така структура подобається більше.

Comments

Лучше сделать так.
def LoadPlugins(pluginpath):
# enum plugins directory
for dir in os.listdir(pluginpath + “\\plugins”):
# for each non-hidden file or directory
if (dir[0] != “_”) and (dir.endswith (“.py”)):
# try to load corresponding plugin
LoadModule(dir)

Leave a comment

(required)

(required)