Создание минимальной архитектуры плагина в Python

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

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

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

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

Ответ 1

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

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

Ответ 2

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.

Возможно, вам захочется заглянуть в модуль imp, хотя вы можете сделать многое с помощью только __import__, os.listdir и некоторые манипуляции с строкой.

Ответ 4

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

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

Что означает, что вам нужно добавлять плагины?

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

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

Пример использования крючков, вдохновленный 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)

Другой пример, вдохновленный меркуриальным. Здесь расширения только добавляют команды к исполняемому файлу командной строки 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), который вызывается для всех расширений) или использует подход с крючкообразным видом, с hooks.setup hook.

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

Ответ 5

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

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

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

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

packageName.command(arg0, arg1,..., param0, param1,...)

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

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

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


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

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

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

Ответ 6

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

Ответ 7

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

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**")

Ответ 8

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

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

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

Ответ 9

Фактически 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 является гораздо более безопасным выбором, поскольку он может загружать плагины без конфликтов или отсутствующих требований.

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

Ответ 10

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

Ответ 11

setuptools имеет EntryPoint:

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

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

Ответ 12

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

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

# 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 множественное наследование

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

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

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

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

Для реального примера использования посмотрите OpenERP Proxy 'field_datetime' extension:

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

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

Ответ 13

Развернувшись на ответе @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>

Ответ 14

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

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

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

введите описание изображения здесь

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

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

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

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

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

Если вам интересно, включите @https://github.com/thodnev/pluginlib

Ответ 15

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