Fork me on GitHub
3/3/2007

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

Продолжаем тему плагинов. Сегодня разговариваем о системе плагинов в 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
  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 достаточно проблематично.

Комментарии

Все статьи