Играемся с twisted.plugins
Originally published at Pythy. You can comment here or there.
Продолжаем тему плагинов. Сегодня разговариваем о системе плагинов в Twisted.
Интерфейсы
Вспомним, что Twisted активно использует zope.interface, и twisted.plugin не стал исключением. Собственно работа по подготовке программы к работе с плагинами разбивается на два этапа:
Создание интерфейса плагина
Его реализация в виде конкретных плагинов
Нашим полигоном будет уже знакомый по прошлому посту lister. Мы с минимальными переделками в коде (__init__.py, input.py, output.py вообще не трогались) будем приспосабливать его к twisted.plugin. Итак, описание интерфейсов плагинов (напомню, у нас два типа плагинов: плагины ввода и плагины вывода):
from zope import interface
class IInputPlugin(interface.Interface):
"""
An input plugin interface
"""
name = interface.Attribute("Name of plugin")
desc = interface.Attribute("Plugin's description")
def __call__():
"""
Returns the iterator
"""
class IOutputPlugin(interface.Interface):
"""
An output plugin interface
"""
name = interface.Attribute("Name of plugin")
desc = interface.Attribute("Plugin's description")
def __call__(xiter):
"""
Iterates over xiter and prints it in some format
"""
Как вы заметили, здесь мы декларируем не только наличие точек расширения нашей программы, но и их интерфейс. В данном случае - тот факт, что плагины должны быть выполняемыми (callable) объектами, иметь атрибуты name и desc.
Управление плагинами
По умолчанию плагины кладутся в twisted/plugins. Нам же хочется, чтобы они были в lister/plugins. Для этого:
При вызовеgetPluginsвторым параметром указываемlister.plugins</p>
В__init__.pyпод-пакетаlister.pluginsуказываем где искать плагины
Итак, первый пункт:
import lister.plugins # required by getPlugins
def get_input_plugins(name=None):
"""
Returns iterator over available input plugins
name - show only plugins with such name, all if None
"""
if name is None:
res = plugin.getPlugins(IInputPlugin, lister.plugins)
else:
res = (p for p in get_input_plugins() if p.name == name)
return res
def get_output_plugins(name=None):
"""
Returns iterator over available output plugins
name - show only plugins with such name, all if None
"""
if name is None:
res = plugin.getPlugins(IOutputPlugin, lister.plugins)
else:
res = (p for p in get_output_plugins() if p.name == name)
return res
Здесь видно зачем в интерфейсах плагинов прописан атрибут name - в twisted.plugin нет способа идентификации плагинов, поэтому нам нужно каким то образом отличать их (напомню, что у нас они вызываются по имени) - для этого и используем имя плагина.
И второй пункт:
import os, sys
__path__ = [os.path.abspath(os.path.join(x, 'lister', 'plugins'))
for x in sys.path]
__all__ = []
Таким образом, плагины ищутся в lister/plugins в каждом из каталогов, указанных в sys.path.
По идее, уже можно переходить к реализации плагинов. Но я не хочу спешить. Зато я хочу оставить без изменения код __init__.py, input.py, output.py и минимально затронуть command.py. Поэтому, я делаю "обертку" для наших "старых плагинов":
class PluginWrapper(object):
"""
Wrapper for making Twisted plugins for lister be easier
"""
def __init__(self, name, action):
"""
Making plugins to be twisted
name - name of plugin
action - actioner
"""
self.name = name
self.action = action
self.desc = action.__doc__
def __call__(self):
"""
Run the action
"""
return self.action()
class InputPluginWrapper(PluginWrapper):
"""
Wrapper for input plugins
"""
interface.implements(plugin.IPlugin, IInputPlugin)
class OutputPluginWrapper(PluginWrapper):
"""
Wrapper for output plugins
"""
interface.implements(plugin.IPlugin, IOutputPlugin)
def __call__(self, xiter):
"""
Run the action for output plugin
"""
return self.action(xiter)
Реализация плагинов
Всё, теперь можно приступать ко второму этапу - реализации плагинов. Итак, в каталоге plugins нашего пакета создаем builtin.py в котором будут "встроенные" плагины:
from lister.input import dir_list
from lister.output import raw_list
from lister.plug import InputPluginWrapper, OutputPluginWrapper
dir_list_plugin = InputPluginWrapper('dir', dir_list)
output_list_plugin = OutputPluginWrapper('raw', raw_list)
Соответствующим образом изменяем command.py (изменения не принципиальны, так что я не привожу их здесь, любопытные могут посмотреть на code.google.com).
listersyspath
Теперь попробуем написать новый плагин в Twisted-стиле.
import sys
from zope.interface import implements
from twisted.plugin import IPlugin
from lister.plug import IInputPlugin
class SysPathLister(object):
"""
syspath input plugin for lister, twisted style
"""
implements(IInputPlugin, IPlugin)
name = "syspath"
desc = """
Lists sys.path
"""
def __call__(self):
return sys.path
syspath_list_plugin = SysPathLister()
Что здесь… Во-первых, необходимые импорты (поддержка интерфейсов zope.interface, поддержка Twisted-плагинов IPlugin, интерфейс наших плагинов IInputPlugin). Во-вторых класс SysPathLister, реализующий интерфейсы IPlugin и IInputPlugin. И в-третьих, сам объект плагина syspath_list_plugin, предоставляющий реализацию этих интерфейсов.
Проба
Теперь нужно, чтобы плагины лежали в lister/plugins в одном из каталогов, указанных в sys.path. Например, текущем каталоге. Итак, сделав пакет lister доступным для импорта, переходим в каталог, где лежат плагины и пробуем…
$ listit -l
Input plugins:
syspath
Lists sys.path
dir
Lists current dir
Output plugins:
raw
Prints list 'AS IS'
$ ls -lR
.:
итого 0
drwxr-xr-x 3 pythy pythy 20 2007-03-02 18:20 lister
./lister:
итого 0
drwxr-xr-x 2 pythy pythy 60 2007-03-03 23:22 plugins
./lister/plugins:
итого 12
-rw-r--r-- 1 pythy pythy 409 2007-03-03 23:22 dropin.cache
-rw-r--r-- 1 pythy pythy 340 2007-03-02 19:00 syspath.py
-rw-r--r-- 1 pythy pythy 823 2007-03-03 23:22 syspath.pyc
Кстати говоря, появление dropin.cache (либо сообщение о невозможности его создать) - явный признак того, что Twisted "подцепил" плагины.
Код, как всегда - на code.google.com
egg’s entrypoints vs twisted.plugin
Подведем итоги.
Egg’s entrypoints:
Работает по pull-схеме (расширяемая программа сама опрашивает наличие плагинов)
Указывает только точку расширения
В качестве плагина служит любой Python-объект (функция, класс, экземпляр класса)
Код плагина полностью не зависим от расширяемой программы
Точка расширения указывается в мета-информации
Twisted plugin:
Работает по pull-схеме (расширяемая программа сама опрашивает наличие плагинов)
Указывает не только точки расширения, но и интерфейсы
В качестве плагина служит только экземпляр класса
Код плагина зависит от кода интерфейса (импортирует интерфейсы)
Точка расширения явно указывается в классе
Резюме таково - twisted.plugin годится только для программ, сделанных на основе Twisted. В остальных случаях резона использовать именно twisted.plugin нет. Тем более, "оторвать" подсистему плагинов от Twisted достаточно проблематично.