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

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

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

Sergii Gulenok

Sergii Gulenok

View Comments