graywolf's lair

Inhuman being's diary…

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

| 3 Comments

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

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

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

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

#!/usr/bin/python
import os
import os.path
 
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(os.path.join(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!

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

3 Comments

  1. Лучше сделать так.
    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)

  2. pluginpath + "\\plugins"
    непітонічно. Треба os.path.join(pluginpath, 'plugins'), він вже розбереться зворотній там слеш чи ні.

Залишити відповідь

Required fields are marked *.