Найпростіша 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!

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

This website uses IntenseDebate comments, but they are not currently loaded because either your browser doesn't support JavaScript, or they didn't load fast enough.

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)