在Python中构build一个最小的插件体系结构
我有一个用Python编写的应用程序,由相当技术的读者(科学家)使用。
我正在寻找一个很好的方法来使应用程序可以扩展的用户,即脚本/插件架构。
我正在寻找一些非常轻量级的东西。 大多数脚本或插件不是由第三方开发和分发的,而是在几分钟内被用户激活,以自动执行重复任务,添加对文件格式的支持,所以插件应该有绝对最小的样板代码,除了复制到文件夹之外,不需要“安装”(所以像setuptools入口点,或者Zope插件架构看起来太多了)。
有没有像这样的系统,或者有什么项目实施类似的scheme,我应该看看的想法/灵感?
我的基本上是一个名为“插件”的目录,主应用程序可以轮询,然后使用imp.load_module来获取文件,使用模块级别的configuration参数寻找一个众所周知的入口点,然后从那里开始。 我使用文件监视的东西,其中插件是积极的一定的活力,但这是一个很好的。
当然,任何需要“我不需要(大而复杂的东西)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插件系统是多么简单..
你可能也想看看__import__
模块,虽然你可以用__import__
, os.listdir
和一些string操作做很多__import__
。
看看现有插件框架/库的概述 ,这是一个很好的起点。 我很喜欢yapsy ,但这取决于你的用例。
虽然这个问题真的很有趣,但我觉得这个问题很难回答,没有更多的细节。 这是什么样的应用? 它有一个GUI? 它是一个命令行工具吗? 一套脚本? 一个独特的入口点等程序…
鉴于我所掌握的信息很less,我将以非常通用的方式回答。
你有什么手段添加插件?
- 您可能需要添加一个configuration文件,该文件将列出要加载的path/目录。
- 另一种方式是说“该插件/目录中的任何文件将被加载”,但是它不便于要求用户移动文件。
- 最后一个中间选项是要求所有插件位于同一个插件/文件夹中,然后使用configuration文件中的相对path来激活/禁用它们。
在纯粹的代码/devise实践中,您必须清楚地确定您希望用户扩展的行为/特定操作。 确定将始终被覆盖的常用入口点/一组function,并确定这些操作中的组。 一旦完成,应该很容易扩展你的应用程序,
从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)),或者使用钩子类方法hooks.setup钩子。
但是,如果你想要更有用的答案,你将不得不缩小你的问题;)
马蒂Allchin的简单的插件框架是我用于我自己的需要的基础。 我真的build议去看看,如果你想要一些简单易行的东西,我认为这是一个很好的开始。 你可以find它也作为一个Django片段 。
我是一位退休的生物学家,他处理数字微图像,并发现自己必须编写一个image processing和分析软件包(不是技术上的库)才能在SGi机器上运行。 我用C编写了代码,并使用Tcl作为脚本语言。 GUI,比如它是用Tk完成的。 在Tcl中出现的命令的forms是“extensionName commandName arg0 arg1 … param0 param1 …”,即简单的空格分隔的单词和数字。 当Tcl看到“extensionName”子string时,控制权被传递给C程序包。 然后依次通过词法分析器(lexer / parser)(在lex / yacc中完成)运行命令,然后根据需要调用C例程。
运行程序包的命令可以通过GUI中的一个窗口逐个运行,但是通过编辑有效的Tcl脚本的文本文件来完成批量作业。 你会select你想做的那种文件级操作的模板,然后编辑一个副本来包含实际的目录和文件名以及包命令。 它像一个魅力工作。 直到 …
1)世界转向个人电脑和2)脚本超过500行左右,当Tcl的iffy组织能力开始成为一个真正的不便。 时间飞逝 …
我退休了,Python被发明了,它看起来像是Tcl的完美接class人。 现在,我从来没有做过端口,因为我从来没有面对在PC上编译(很大)C程序,使用C包扩展Python,在Python / Gt?/ Tk?/? ?。 然而,编辑模板脚本的旧想法似乎仍然可行。 而且,在本地Python表单中input包命令也不是太大的负担,例如:
packageName.command(arg0,arg1,…,param0,param1,…)
一些额外的点,parens和逗号,但那些不是showstoppers。
我记得看到有人已经在Python中完成了lex和yacc的版本(请尝试: http : //www.dabeaz.com/ply/ ),所以如果仍然需要这些版本的话,他们就在身边。
这种散漫的一点是,在我看来,Python本身就是科学家所期望的“轻量级”前端。 我很想知道你为什么认为它不是,我的意思是认真的。
稍后添加:应用程序gedit预计插件被添加,他们的网站有一个简单的插件程序,我在几分钟内find了一个简单的插件程序的最清晰的解释。 尝试:
https://wiki.gnome.org/Apps/Gedit/PythonPluginHowToOld
我仍然想更好地理解你的问题。 我不清楚你是否希望科学家能够以各种方式很简单地使用你的(Python)应用程序,或者2)想让科学家为你的应用程序增加新的function。 select#1是我们面对图像的情况,并导致我们使用我们修改的通用脚本以适应当下的需要。 它是select#2,它引导你的插件的想法,或者是你的应用程序的一些方面,使命令发布是不切实际的?
当我searchPython装饰,find一个简单但有用的代码片段。 它可能不适合你的需求,但非常鼓舞人心。
Scipy高级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上由Andre Roberge博士给出的各种插件体系结构的讨论。他从很简单的东西出发,对实现插件的不同方式进行了很好的概述。
它可以作为一个播客 (第二部分,解释猴子补丁)伴随着一系列的六个博客条目 。
我build议在做出决定之前快速听一下。
我到这里来寻找一个最小的插件架构,并发现了很多东西,似乎对我来说都是过度的。 所以,我已经实现了超级简单Python插件 。 要使用它,您需要创build一个或多个目录,并在每个目录中放置一个特殊的__init__.py
文件。 导入这些目录将导致所有其他Python文件被加载为子模块,并且它们的名字将被放置在__all__
列表中。 然后由您来validation/初始化/注册这些模块。 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是一个更安全的select,因为它可以加载插件没有冲突或缺less要求。
另一个好处是,插件本身可以使用相同的机制进行扩展,而原始应用程序不必关心它。
我可以build议看看@ edomaur的答案,看看simple_plugins (无耻的插件),这是一个受Marty Alchin工作启发的简单插件框架。
基于项目自述文件的简短使用示例:
# 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>
作为插件系统的另一种方法,您可以检查“ 扩展我”项目 。
例如,让我们定义简单的类及其扩展
# 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库通过元类操纵类的创build过程,因此在上面的例子中,当创buildMyCoolClass
新实例时,我们得到了MyCoolClassExtension
和MyCoolClass
子类的新类的实例,这两个类都具有相同的function,这要归功于Python的多重inheritance
为了更好地控制类的创build,在这个lib中定义了几个元类:
-
ExtensibleType
– 通过子类化允许简单的可扩展性 -
ExtensibleByHashType
– 类似于ExtensibleType,但有能力构build专门的类的版本,允许基类的全局扩展和类的特化版本的扩展
这个库在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
是可以解释的对象。 RecordDateTime
是扩展名。
要启用扩展,只需要包含扩展类的导入模块,以及(在上面的情况下)所有在基类中具有扩展类后创build的Record
对象,因此具有其所有function。
这个库的主要优点是,运行可扩展对象的代码不需要知道扩展和扩展就可以改变可扩展对象中的所有东西。
setuptools有一个EntryPoint :
入口点是发布“通告”Python对象(比如函数或类)供其他发行版使用的简单方法。 可扩展的应用程序和框架可以search具有特定名称或组的入口点,无论是从特定的分发还是从sys.path上的所有活动分发,然后随意检查或加载广告的对象。
AFAIK这个包总是可用的,如果你使用pip或virtualenv。
我花了很多时间去寻找Python的小插件系统,这将适合我的需要。 但是我只是想,如果已经有了一种自然而灵活的inheritance,为什么不使用它。
对插件使用inheritance的唯一问题是你不知道什么是最具体的(最低的inheritance树)插件类。
但是这可以通过元类来解决,元类跟踪基类的inheritance,并且可能会构buildinheritance自大多数特定插件的类(下图中的“根扩展”),
所以我通过编码这样一个元类来解决问题:
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
所以当你有根基,用metaclass创build,并且拥有从它inheritance的插件树时,你可以自动获得类,inheritance自最具体的插件只是inheritance:
class RootExtended(RootBase.PluginExtended): ... your code here ...
代码库相当小(约30行纯代码),并且像inheritance允许一样灵活。
如果你有兴趣,参与@ https://github.com/thodnev/pluginlib
当我在Python中search插件框架的时候,我已经花时间阅读了这个线程。 我用了一些,但是他们有缺点 。 下面是我在2017年提出的一个免费,松散耦合的插件pipe理系统: 稍后加载我 。 这里是如何使用它的教程 。