Построение минимальной архитектуры плагинов в 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>
точки входа-это простой способ для дистрибутивов "рекламировать" Python объекты (такие как функции или классы) для использования другими дистрибутивами. Расширяемые приложения и фреймворки могут выполнять поиск точек входа с определенным именем или группой из определенного дистрибутива или из всех активных дистрибутивов на sys.путь, а затем проверить или загрузить объявленные объекты на будет.
AFAIK этот пакет всегда доступен, если вы используете pip или virtualenv.
Я потратил много времени, пытаясь найти небольшую систему плагинов для Python, которая соответствовала бы моим потребностям. Но тогда я просто подумал, если уже есть наследство, которое является естественным и гибким, почему бы не использовать его.
единственная проблема с использованием наследования для плагинов заключается в том, что вы не знаете, каковы наиболее конкретные(самые низкие по дереву наследования) классы плагинов.
но это можно решить с помощью metaclass, который отслеживает наследование базового класса, и возможно, можно построить класс, который наследуется от большинства конкретных плагинов ("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
поэтому, когда у вас есть корневая база, сделанная с метаклассом, и есть дерево плагинов, которые наследуют от него, вы можете автоматически получить класс, который наследует от самых конкретных плагинов, просто подклассом:
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.