Создание минимальной архитектуры плагинов в Python - PullRequest
176 голосов
/ 31 мая 2009

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

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

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

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

Ответы [ 18 ]

3 голосов
/ 09 октября 2014

В качестве одного из подходов к системе плагинов, вы можете выбрать Расширить проект .

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

# 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]

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

Для лучшего контроля над созданием классов в этой библиотеке определено несколько метаклассов:

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

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

Эта библиотека используется в OpenERP Proxy Project и, кажется, работает достаточно хорошо!

Для реального примера использования, посмотрите в Расширение '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 здесь это расширяемый объект. RecordDateTime это расширение.

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

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

3 голосов
/ 29 января 2013

setuptools имеет EntryPoint :

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

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

2 голосов
/ 31 мая 2017

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

2 голосов
/ 11 апреля 2016

В продолжение ответа @ 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>
1 голос
/ 25 сентября 2018

Вы также можете взглянуть на Основы .

Идея состоит в том, чтобы создавать приложения на основе повторно используемых компонентов, называемых шаблонами и плагинами. Плагины - это классы, производные от 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.

Вот документы .

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

1 голос
/ 17 августа 2018

Вы можете использовать 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"}'))
1 голос
/ 04 декабря 2016

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

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

Но это можно решить с помощью метакласса, который отслеживает наследование базового класса и, возможно, может создать класс, который наследуется от большинства конкретных плагинов («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

0 голосов
/ 08 октября 2018

В нашем текущем медицинском продукте у нас есть подключаемая архитектура с интерфейсом класса. Наш технический стек - это Django поверх Python для API и Nuxtjs поверх nodejs для внешнего интерфейса.

Для нашего продукта написано приложение менеджера плагинов, которое в основном представляет собой пакеты pip и npm в соответствии с Django и Nuxtjs.

Для разработки новых плагинов (pip и npm) мы сделали менеджер плагинов в качестве зависимости.

В пипс упаковке: С помощью setup.py вы можете добавить точку входа плагина, чтобы сделать что-то с менеджером плагинов (реестр, инициации, ... и т. Д.) https://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation

В упаковке npm: Как и в случае с pip, в скриптах npm есть хуки для установки. https://docs.npmjs.com/misc/scripts

Наш вариант использования:

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

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

Если разработчикам плагинов необходимо повторно использовать основные объекты, этот объект можно использовать, выполнив уровень абстракции в диспетчере плагинов, чтобы любые плагины могли наследовать эти методы.

Просто поделитесь, как мы внедрили в наш продукт, надеюсь, что это даст небольшую идею.

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