Построение минимальной архитектуры плагинов в Python

У меня есть приложение, написанное на Python, которое используется довольно технической аудиторией (учеными).

Я ищу хороший способ сделать приложение расширяемым пользователями, т. е. архитектуру сценариев/плагинов.

Я что-то ищу чрезвычайно легкий. Большинство скриптов или плагинов не будут разработаны и распространены сторонними и установлены, но будут чем-то взбиты пользователем через несколько минуты для автоматизации повторяющейся задачи, добавления поддержки формата файла и т. д. Таким образом, Плагины должны иметь абсолютный минимальный шаблонный код и не требуют никакой "установки", кроме копирования в папку (так что что-то вроде точек входа setuptools или архитектуры плагина Zope кажется слишком большим.)

есть ли какие-либо системы, подобные этой, или какие-либо проекты, которые реализуют подобную схему, которую я должен искать для идей / вдохновения?

17 ответов


мой, в основном, каталог под названием "Плагины", который основное приложение может опросить, а затем использовать imp.load_module чтобы забрать файлы, найдите известную точку входа, возможно, с конфигурационными параметрами уровня модуля, и перейдите оттуда. Я использую файловый мониторинг для определенного динамизма, в котором Плагины активны, но это приятно иметь.

конечно, любое требование, которое приходит, говоря: "мне не нужно [большая, сложная вещь] X; я просто хочу что-то легкий " риск повторной реализации X одного обнаруженного требования за раз. Но это не значит, что вы не можете повеселиться, делая это в любом случае:)


module_example.py:

def plugin_main(*args, **kwargs):
    print args, kwargs

loader.py:

def load_plugin(name):
    mod = __import__("module_%s" % name)
    return mod

def call_plugin(name, *args, **kwargs):
    plugin = load_plugin(name)
    plugin.plugin_main(*args, **kwargs)

call_plugin("example", 1234)

это, конечно, "минимальный", он не имеет абсолютно никакой проверки ошибок, вероятно, бесчисленные проблемы безопасности, это не очень гибкий - но он должен показать вам, как простая система плагинов в Python может быть..

вы, вероятно, хотите посмотреть в ИМП модуль тоже, хотя вы можете сделать многое с помощью just __import__, os.listdir и некоторые манипуляции со строками.


посмотреть в этом обзоре по существующим фреймворкам / библиотекам плагинов, это хорошая отправная точка. Мне очень нравится yapsy, но это зависит от вашего использования.


хотя этот вопрос действительно интересен, я думаю, что на него довольно сложно ответить, без более подробной информации. Что это за приложение? У него есть графический интерфейс? Это инструмент командной строки? Набор сценариев? Программа с уникальной точки входа и т. д...

учитывая небольшую информацию, которую я имею, я отвечу очень обобщенно.

что означает, что вы должны добавить Плагины?

  • вы, вероятно, придется добавить файл конфигурации, который будет список путей / каталогов для загрузки.
  • другим способом было бы сказать: "любые файлы в этом плагине/ каталоге будут загружены", но неудобно требовать от ваших пользователей перемещения файлов.
  • последним промежуточным вариантом было бы требование, чтобы все плагины находились в одном плагине/ папке, а затем активировали/деактивировали их, используя относительные пути в файле конфигурации.

на чистой практике кода / дизайна, вы должны будете четко определить, что поведение / конкретные действия, которые необходимо расширить пользователям. Определите общую точку входа / набор функций, которые всегда будут переопределены, и определите группы в рамках этих действий. Как только это будет сделано, должно быть легко расширить ваше приложение,

пример использования крючки, вдохновленный MediaWiki (PHP, но действительно ли язык имеет значение?):

import hooks

# In your core code, on key points, you allow user to run actions:
def compute(...):
    try:
        hooks.runHook(hooks.registered.beforeCompute)
    except hooks.hookException:
        print('Error while executing plugin')

    # [compute main code] ...

    try:
        hooks.runHook(hooks.registered.afterCompute)
    except hooks.hookException:
        print('Error while executing plugin')

# The idea is to insert possibilities for users to extend the behavior 
# where it matters.
# If you need to, pass context parameters to runHook. Remember that
# runHook can be defined as a runHook(*args, **kwargs) function, not
# requiring you to define a common interface for *all* hooks. Quite flexible :)

# --------------------

# And in the plugin code:
# [...] plugin magic
def doStuff():
    # ....
# and register the functionalities in hooks

# doStuff will be called at the end of each core.compute() call
hooks.registered.afterCompute.append(doStuff)

еще один пример, вдохновленный mercurial. Здесь расширения только добавляют команды к hg исполняемый файл командной строки, расширяющий поведение.

def doStuff(ui, repo, *args, **kwargs):
    # when called, a extension function always receives:
    # * an ui object (user interface, prints, warnings, etc)
    # * a repository object (main object from which most operations are doable)
    # * command-line arguments that were not used by the core program

    doMoreMagicStuff()
    obj = maybeCreateSomeObjects()

# each extension defines a commands dictionary in the main extension file
commands = { 'newcommand': doStuff }

для обоих подходов, вам может понадобиться общие инициализации и завершить для расширения. Вы можете либо использовать общий интерфейс, который должен будет реализовать все ваше расширение(лучше подходит для второго подхода; mercurial использует reposetup (ui, repo), который вызывается для всех расширений), либо использовать подход типа крючка с крючками.крюк установки.

но опять же, если вам нужны более полезные ответы, вам придется сузить свой вопрос;)


простая структура плагинов Марти Алчина это база, которую я использую для своих нужд. Я очень рекомендую взглянуть на это, я думаю, что это очень хорошее начало, если вы хотите что-то простое и легко взломать. Вы можете найти его также как фрагменты Django.


Я отставной биолог, который занимался цифровыми микрографами и обнаружил, что ему нужно написать пакет обработки и анализа изображений (не технически библиотека) для работы на машине SGi. Я написал код на C и использовал Tcl для языка сценариев. GUI, как это было, было сделано с использованием Tk. Команды, которые появились в Tcl, имели форму " extensionName commandName arg0 arg1 ... param0 параметр1 ...-то есть простые слова и числа, разделенные пробелами. Когда Лок увидел "названиерасширения" подстроку, контроль был передан пакет c. Это, в свою очередь, запускало команду через лексер/парсер (сделано в lex/yacc), а затем вызывало подпрограммы C.

команды для работы с пакетом могут выполняться один за другим через окно в GUI, но пакетные задания выполнялись путем редактирования текстовых файлов, которые были действительными сценариями Tcl; вы бы выбрали шаблон, который выполнял операцию на уровне файла, которую вы хотели сделать, а затем отредактировали копию, чтобы содержать фактический каталог и имена файлов плюс команды пакета. Это сработало как заклинание. До...

1) мир обратился к ПК и 2) сценарии стали длиннее, чем около 500 строк, когда сомнительные организационные возможности Tcl начали становиться реальным неудобством. Время шло ...

Я вышел на пенсию, Python был изобретен, и он выглядел как идеальный преемник Tcl. Теперь я никогда не делал порт, потому что я никогда не сталкивался с проблемами компиляции (довольно больших) программ C на ПК, расширение Python с пакетом C и выполнение GUIs в Python/Gt?- ТК?/??. Однако, старые идеи редактируемый шаблон скрипты, кажется, еще работает. Кроме того, не должно быть слишком большой нагрузкой вводить команды пакета в собственной форме Python, например:

имя пакета.команды( параметры arg0, арг1, ..., param0, param1,...)

несколько дополнительных точек, скобок и запятых, но это не разрывные.

Я помню, что кто-то сделал версии lex и yacc в Python (попробуйте:http://www.dabeaz.com/ply/), поэтому, если они все еще нужны, они вокруг.

смысл этого бессвязного заключается в том, что мне показалось, что Python сам по себе является желаемым "легким" передним концом, используемым учеными. Мне любопытно знать, почему вы думаете, что это не так, и я серьезно.


добавлено позже: приложение gedit ожидает добавления плагинов, и их сайт имеет о самых четких объяснение простой процедуры плагина, которую я нашел за несколько минут просмотра. Попробуйте:

https://wiki.gnome.org/Apps/Gedit/PythonPluginHowToOld

Я все равно хотел бы лучше понять ваш вопрос. Я неясен, хотите ли вы 1), чтобы ученые могли использовать ваше (Python) приложение довольно просто по-разному или 2) хотите, чтобы ученые могли добавлять новые возможности в ваше приложение. Выбор № 1-это ситуация, с которой мы столкнулись с изображения и это привело нас к использованию общих сценариев, которые мы изменили в соответствии с потребностями момента. Это выбор #2, который приводит вас к идее плагинов, или это какой-то аспект вашего приложения, который делает выдачу команд ему непрактичным?


когда я искал декораторы Python, нашел простой, но полезный фрагмент кода. Это может не соответствовать вашим потребностям, но очень вдохновляет.

Scipy Advanced Python#Система Регистрации Плагинов

class TextProcessor(object):
    PLUGINS = []

    def process(self, text, plugins=()):
        if plugins is ():
            for plugin in self.PLUGINS:
                text = plugin().process(text)
        else:
            for plugin in plugins:
                text = plugin().process(text)
        return text

    @classmethod
    def plugin(cls, plugin):
        cls.PLUGINS.append(plugin)
        return plugin


@TextProcessor.plugin
class CleanMarkdownBolds(object):
    def process(self, text):
        return text.replace('**', '')

использование:

processor = TextProcessor()
processed = processor.process(text="**foo bar**", plugins=(CleanMarkdownBolds, ))
processed = processor.process(text="**foo bar**")

Мне понравилось хорошее обсуждение различных архитектур плагинов, данное д-ром Андре Робержем в Pycon 2009. Он дает хороший обзор различных способов реализации плагинов, начиная от чего-то действительно простого.

его доступен как подкаст (вторая часть после объяснения исправления обезьяны) сопровождается серией шесть записей в блоге.

Я рекомендую дать ему быстрый слушать, прежде чем принимать решение.


Я приехал сюда в поисках минимальной архитектуры плагинов и нашел много вещей, которые мне показались излишними. Итак, я реализовал Супер Простые Плагины Python. Чтобы использовать его, вы создаете один или несколько каталогов и бросить __init__.py файл в каждом из них. Импорт этих каталогов приведет к загрузке всех других файлов Python в виде подмодулей, а их имена будут помещены в __all__ список. Затем это до вас, чтобы проверить/подключить/зарегистрировать модули. В файле README есть пример.


на самом деле setuptools работает с "каталогом плагинов", как показано в следующем примере из документации проекта: http://peak.telecommunity.com/DevCenter/PkgResources#locating-plugins

пример использования:

plugin_dirs = ['foo/plugins'] + sys.path
env = Environment(plugin_dirs)
distributions, errors = working_set.find_plugins(env)
map(working_set.add, distributions)  # add plugins+libs to sys.path
print("Couldn't load plugins due to: %s" % errors)

в конечном счете,setuptools является гораздо более безопасным выбором, так как он может загружать плагины без конфликтов или отсутствующих требований.

Другим преимуществом является то, что сами плагины могут быть расширенный с использованием того же механизма, без оригинальных приложений, которые должны заботиться о нем.


как один другой подход к системе плагинов, вы можете проверить расширить меня проект.

например, давайте определим простой класс и ее расширение

# Define base class for extensions (mount point)
class MyCoolClass(Extensible):
    my_attr_1 = 25
    def my_method1(self, arg1):
        print('Hello, %s' % arg1)

# Define extension, which implements some aditional logic
# or modifies existing logic of base class (MyCoolClass)
# Also any extension class maby be placed in any module You like,
# It just needs to be imported at start of app
class MyCoolClassExtension1(MyCoolClass):
    def my_method1(self, arg1):
        super(MyCoolClassExtension1, self).my_method1(arg1.upper())

    def my_method2(self, arg1):
        print("Good by, %s" % arg1)

и попробуйте использовать его:

>>> my_cool_obj = MyCoolClass()
>>> print(my_cool_obj.my_attr_1)
25
>>> my_cool_obj.my_method1('World')
Hello, WORLD
>>> my_cool_obj.my_method2('World')
Good by, World

и показать, что скрыто за сценой:

>>> my_cool_obj.__class__.__bases__
[MyCoolClassExtension1, MyCoolClass]

extend_me библиотека управляет процессом создания класса через метаклассы, таким образом, в примере выше, при создании нового экземпляра MyCoolClass мы получили экземпляр новый класс, который является подклассом как MyCoolClassExtension и MyCoolClass имея функциональность обоих из них, благодаря Python множественное наследование

для лучшего контроля над созданием класса существует несколько метаклассов, определенных в этой lib:

  • ExtensibleType - позволяет простую расширяемость путем подклассов

  • ExtensibleByHashType - подобный ExtensibleType, но имеющ способность строить специализированные версии класса, позволяющие глобальное расширение базового класса и расширения специализированных версий класса

этот lib используется в Прокси-Проект OpenERP, и, кажется, работает достаточно хорошо!

для реального примера использования, посмотрите в расширение Openerp Proxy 'field_datetime':

from ..orm.record import Record
import datetime

class RecordDateTime(Record):
    """ Provides auto conversion of datetime fields from
        string got from server to comparable datetime objects
    """

    def _get_field(self, ftype, name):
        res = super(RecordDateTime, self)._get_field(ftype, name)
        if res and ftype == 'date':
            return datetime.datetime.strptime(res, '%Y-%m-%d').date()
        elif res and ftype == 'datetime':
            return datetime.datetime.strptime(res, '%Y-%m-%d %H:%M:%S')
        return res

Record вот extesible объект. RecordDateTime это расширение.

чтобы включить расширение, просто импортируйте модуль, содержащий расширение класс, и (в случае выше) все Record объекты, созданные после него, будут иметь класс расширения в базовых классах, таким образом, имея всю свою функциональность.

основным преимуществом этой библиотеки является то, что код, который работает с расширяемыми объектами, не должен знать о расширении, и расширения могут изменить все в расширяемых объектах.


расширяя ответ @edomaur, могу ли я предложить взглянуть на simple_plugins (бесстыдный плагин), который представляет собой простую структуру плагинов, вдохновленную работа Марти Алчина.

краткий пример использования на основе README проекта:

# All plugin info
>>> BaseHttpResponse.plugins.keys()
['valid_ids', 'instances_sorted_by_id', 'id_to_class', 'instances',
 'classes', 'class_to_id', 'id_to_instance']

# Plugin info can be accessed using either dict...
>>> BaseHttpResponse.plugins['valid_ids']
set([304, 400, 404, 200, 301])

# ... or object notation
>>> BaseHttpResponse.plugins.valid_ids
set([304, 400, 404, 200, 301])

>>> BaseHttpResponse.plugins.classes
set([<class '__main__.NotFound'>, <class '__main__.OK'>,
     <class '__main__.NotModified'>, <class '__main__.BadRequest'>,
     <class '__main__.MovedPermanently'>])

>>> BaseHttpResponse.plugins.id_to_class[200]
<class '__main__.OK'>

>>> BaseHttpResponse.plugins.id_to_instance[200]
<OK: 200>

>>> BaseHttpResponse.plugins.instances_sorted_by_id
[<OK: 200>, <MovedPermanently: 301>, <NotModified: 304>, <BadRequest: 400>, <NotFound: 404>]

# Coerce the passed value into the right instance
>>> BaseHttpResponse.coerce(200)
<OK: 200>

setuptools имеет точку входа:

точки входа-это простой способ для дистрибутивов "рекламировать" Python объекты (такие как функции или классы) для использования другими дистрибутивами. Расширяемые приложения и фреймворки могут выполнять поиск точек входа с определенным именем или группой из определенного дистрибутива или из всех активных дистрибутивов на sys.путь, а затем проверить или загрузить объявленные объекты на будет.

AFAIK этот пакет всегда доступен, если вы используете pip или virtualenv.


Я потратил много времени, пытаясь найти небольшую систему плагинов для Python, которая соответствовала бы моим потребностям. Но тогда я просто подумал, если уже есть наследство, которое является естественным и гибким, почему бы не использовать его.

единственная проблема с использованием наследования для плагинов заключается в том, что вы не знаете, каковы наиболее конкретные(самые низкие по дереву наследования) классы плагинов.

но это можно решить с помощью metaclass, который отслеживает наследование базового класса, и возможно, можно построить класс, который наследуется от большинства конкретных плагинов ("Root extended" на рисунке ниже)

enter image description here

Итак, я пришел с решением, кодируя такой метакласс:

class PluginBaseMeta(type):
    def __new__(mcls, name, bases, namespace):
        cls = super(PluginBaseMeta, mcls).__new__(mcls, name, bases, namespace)
        if not hasattr(cls, '__pluginextensions__'):  # parent class
            cls.__pluginextensions__ = {cls}  # set reflects lowest plugins
            cls.__pluginroot__ = cls
            cls.__pluginiscachevalid__ = False
        else:  # subclass
            assert not set(namespace) & {'__pluginextensions__',
                                         '__pluginroot__'}     # only in parent
            exts = cls.__pluginextensions__
            exts.difference_update(set(bases))  # remove parents
            exts.add(cls)  # and add current
            cls.__pluginroot__.__pluginiscachevalid__ = False
        return cls

    @property
    def PluginExtended(cls):
        # After PluginExtended creation we'll have only 1 item in set
        # so this is used for caching, mainly not to create same PluginExtended
        if cls.__pluginroot__.__pluginiscachevalid__:
            return next(iter(cls.__pluginextensions__))  # only 1 item in set
        else:
            name = cls.__pluginroot__.__name__ + 'PluginExtended'
            extended = type(name, tuple(cls.__pluginextensions__), {})
            cls.__pluginroot__.__pluginiscachevalid__ = True
return extended

поэтому, когда у вас есть корневая база, сделанная с метаклассом, и есть дерево плагинов, которые наследуют от него, вы можете автоматически получить класс, который наследует от самых конкретных плагинов, просто подклассом:

class RootExtended(RootBase.PluginExtended):
    ... your code here ...

база кода довольно небольшие (~30 строк чистого кода) и настолько гибкие, насколько позволяет наследование.

Если вы заинтересованы, принять участие @ https://github.com/thodnev/pluginlib


Я потратил время на чтение этой темы, пока искал фреймворк плагина в Python время от времени. У меня использовал некоторые, но были недостатки С ними. Вот что я придумал для вашего изучения в 2017 году, бесплатный интерфейс, слабо связанная система управления плагинами:загрузите меня позже. Вот уроки о том, как использовать его.


можно использовать pluginlib.

Плагины легко создавать и могут быть загружены из других пакетов, путей к файлам или точек входа.

создайте родительский класс плагина, определив любые необходимые методы:

import pluginlib

@pluginlib.Parent('parser')
class Parser(object):

    @pluginlib.abstractmethod
    def parse(self, string):
        pass

создайте плагин, наследовав родительский класс:

import json

class JSON(Parser):
    _alias_ = 'json'

    def parse(self, string):
        return json.loads(string)

загрузить Плагины:

loader = pluginlib.PluginLoader(modules=['sample_plugins'])
plugins = loader.plugins
parser = plugins.parser.json()
print(parser.parse('{"json": "test"}'))

вы также можете посмотреть на задел.

идея заключается в создании приложений вокруг многоразовых компонентов, называемых шаблонами и плагинами. Плагины-это классы, производные от GwBasePattern. Вот простой пример:

from groundwork import App
from groundwork.patterns import GwBasePattern

class MyPlugin(GwBasePattern):
    def __init__(self, app, **kwargs):
        self.name = "My Plugin"
        super().__init__(app, **kwargs)

    def activate(self): 
        pass

    def deactivate(self):
        pass

my_app = App(plugins=[MyPlugin])       # register plugin
my_app.plugins.activate(["My Plugin"]) # activate it

есть также более продвинутые шаблоны для обработки, например, интерфейсов командной строки, сигнализации или общих объектов.

Groundwork находит свои плагины либо программно привязывая их к приложению, как показано выше, либо автоматически через setuptools. Пакеты Python, содержащие Плагины, должны объявлять их с помощью специальной точки входа groundwork.plugin.

здесь docs.

отказ от ответственности: я один из авторов Groundwork.