Image

Играемся с twisted.plugins

Originally published at Pythy. You can comment here or there.

Продолжаем тему плагинов. Сегодня разговариваем о системе плагинов в Twisted.


Интерфейсы


Вспомним, что Twisted активно использует zope.interface, и twisted.plugin не стал исключением. Собственно работа по подготовке программы к работе с плагинами разбивается на два этапа:




  1. Создание интерфейса плагина


  2. Его реализация в виде конкретных плагинов


Нашим полигоном будет уже знакомый по прошлому посту 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. Для этого:




  1. При вызове getPlugins вторым параметром указываем lister.plugins</p>


  2. В __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 достаточно проблематично.