首页 文章

在Python中构建最小的插件体系结构

提问于
浏览
170

我有一个用Python编写的应用程序,由相当技术的读者(科学家)使用 .

我正在寻找一种让用户可扩展应用程序的好方法,即脚本/插件架构 .

我正在找东西 extremely lightweight . 大多数脚本或插件不会由第三方开发和分发并安装,但会在几分钟内被用户掀起,以自动执行重复任务,添加对文件格式的支持,所以插件应该具有绝对最小的样板代码,并且除了复制到文件夹之外不需要'installation'(因此类似于setuptools入口点,或者Zope插件架构似乎太多了 . )

是否有任何这样的系统,或者任何实施类似方案的项目,我应该考虑一下想法/灵感?

18 回答

  • 46

    我的基本上是一个名为"plugins"的目录,主应用程序可以轮询,然后使用imp.load_module来获取文件,查找可能带有模块级配置参数的着名入口点,并从那里开始 . 我使用文件监控的东西来获得一定程度的活力,其中插件是活跃的,但这是一个不错的选择 .

    当然,任何要求都说:“我不需要[大而复杂的东西] X;我只是想要一些轻量级的东西”冒着一次重新实现X一个被发现的要求的风险 . 但这并不是说你无论如何都无法做到这一点:)

  • 23

    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 和一些字符串操作来做很多事情 .

  • 10

    看看at this overview over existing plugin frameworks / libraries,这是一个很好的起点 . 我非常喜欢yapsy,但这取决于你的用例 .

  • 2

    虽然这个问题非常有趣,但我认为如果没有更多细节,就很难回答 . 这是什么类型的应用程序?它有GUI吗?它是一个命令行工具吗?一组脚本?具有唯一入口点等的程序......

    鉴于我的信息很少,我将以非常通用的方式回答 .

    你有什么方法可以添加插件?

    • 您可能需要添加一个配置文件,该文件将列出要加载的路径/目录 .

    • 另一种方式是说"any files in that plugin/ directory will be loaded",但是要求用户移动文件很不方便 .

    • 最后一个中间选项是要求所有插件位于同一个插件/文件夹中,然后使用配置文件中的相对路径激活/取消激活它们 .

    在纯粹的代码/设计实践中,您必须清楚地确定您希望用户扩展哪些行为/特定操作 . 确定将始终被覆盖的公共入口点/一组功能,并确定这些操作中的组 . 完成后,应该可以轻松扩展您的应用程序,

    使用钩子的例子,灵感来自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钩子 .

    但同样,如果你想要更多有用的答案,你将不得不缩小你的问题;)

  • 11

    Marty Allchin's simple plugin framework是我根据自己的需要使用的基础 . 我真的建议你去看看它,如果你想要简单易用的东西,我认为这是一个很好的开始 . 你也可以找到as a Django Snippets .

  • 0

    我是一名退休的生物学家,负责处理数字微图像,并发现自己必须编写一个图像处理和分析软件包(技术上不是库)才能在SGi机器上运行 . 我用C编写代码并使用Tcl作为脚本语言 . GUI就像使用Tk一样完成 . Tcl中出现的命令的格式为“extensionName commandName arg0 arg1 ... param0 param1 ...”,即简单的空格分隔的单词和数字 . 当Tcl看到“extensionName”子字符串时,控件被传递给C包 . 然后通过词法分析器/解析器(在lex / yacc中完成)运行命令,然后根据需要调用C例程 .

    操作包的命令可以通过GUI中的窗口逐个运行,但批处理作业是通过编辑有效Tcl脚本的文本文件完成的;您将选择执行您想要执行的文件级操作的模板,然后编辑包含实际的副本目录和文件名以及包命令 . 它就像一个魅力 . 直到 ...

    1)世界转向PC和2)脚本的时间长于500行,而Tcl的组织能力开始变得非常不便 . 时间飞逝 ...

    我退休了,Python被发明了,它看起来像是Tcl的完美继承者 . 现在,我从来没有完成过这个端口,因为我从来没有遇到过在PC上编译(相当大的)C程序,用C语言扩展Python,用Python / Gt做?GUI的挑战?/? ? . 但是,拥有可编辑模板脚本的旧想法似乎仍然可行 . 此外,以本机Python形式输入包命令不应该是太大的负担,例如:

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

    一些额外的点,parens和逗号,但那些不是showstoppers .

    我记得看到有人在Python中完成了lex和yacc的版本(尝试:http://www.dabeaz.com/ply/),所以如果仍然需要它们,那么它们就在身边 .

    这种漫无边际的观点是,在我看来,Python本身就是科学家可以使用的理想的“轻量级”前端 . 我很想知道为什么你认为它不是,我的意思是认真的 .


    后来添加:应用程序gedit预计插件被添加,他们的网站有一个简单的插件程序的最清楚的解释我在几分钟的环顾四周找到 . 尝试:

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

    我还是想更好地理解你的问题 . 我不清楚你是否希望科学家能够以各种方式简单地使用你的(Python)应用程序,或2)希望科学家能够为你的应用程序添加新的功能 . 选择#1是我们面对图像的情况,这导致我们使用我们修改的通用脚本以满足当下的需要 . 是选择#2引导您了解插件的概念,还是应用程序的某些方面使得向其发出命令是不切实际的?

  • 11

    当我搜索Python装饰器时,找到了一个简单但有用的代码片段 . 它可能不符合您的需求,但非常鼓舞人心 .

    Scipy Advanced Python#Plugin Registration System

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

    我非常喜欢Andre Roberge博士在Pycon 2009上给出的关于不同插件架构的精彩讨论 . 他很好地概述了实现插件的不同方法,从非常简单的方面开始 .

    它可以作为podcast(在修补猴子修补后的第二部分)伴随着一系列six blog entries .

    在你做出决定之前,我建议你快速听一听 .

  • 2

    我来到这里寻找一个最小的插件架构,发现很多东西对我来说都有些过分 . 所以,我已经实现了Super Simple Python Plugins . 要使用它,您需要创建一个或多个目录并在每个目录中删除一个特殊的 __init__.py 文件 . 导入这些目录将导致所有其他Python文件作为子模块加载,并且它们的名称将放在 __all__ 列表中 . 然后是's up to you to validate/initialize/register those modules. There'是README文件中的一个例子 .

  • 7

    实际上,setuptools与"plugins directory"一起使用,如以下示例取自项目的文档: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是一个更安全的选择,因为它可以加载插件而不会发生冲突或缺少需求 .

    另一个好处是插件本身可以使用相同的机制进行扩展,而原始应用程序不必关心它 .

  • 3

    作为插件系统的另一种方法,您可以检查Extend Me project .

    例如,让我们定义简单类及其扩展

    # 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 的新实例时,我们得到了新类的实例,它是 MyCoolClassExtensionMyCoolClass 的子类,具有两者的功能,这要归功于Python的multiple inheritance

    为了更好地控制类创建,在此lib中定义的元类很少:

    • ExtensibleType - 通过子类化允许简单的可扩展性

    • ExtensibleByHashType - 类似于ExtensibleType,但具有构建专用版本的类的能力,允许基类的全局扩展和类的专用版本的扩展

    这个lib在OpenERP 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 对象将在基类中具有扩展类,从而具有其所有功能 .

    这个库的主要优点是,操作可扩展对象的代码不需要知道扩展和扩展可能会改变可扩展对象中的所有内容 .

  • 140

    setuptools has an EntryPoint

    入口点是分发“广告”Python的简单方法其他发行版使用的对象(如函数或类) . 可扩展的应用程序和框架可以从特定的发行版或sys.path上的所有活动发行版中搜索具有特定名称或组的入口点,然后随意检查或加载广告对象 .

    如果您使用pip或virtualenv,AFAIK此套餐始终可用 .

  • 1

    扩展@ edomaur的答案我可以建议看一下simple_plugins(无耻插件),这是一个受work of 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>
    
  • 4

    当我在Python中搜索插件框架时,我花时间阅读这个帖子 . 我和他们一起used some but there were shortcomings . 以下是我在2017年提出的审查,一个免费的,松散耦合的插件管理系统:Load me later . 这里有tutorials如何使用它 .

  • 1

    我花了很多时间试图找到适合我需求的小插件系统 . 但后来我想,如果已经有一个自然而灵活的继承,为什么不使用它 .

    使用继承插件的唯一问题是你不知道什么是最具体的(继承树中最低的)插件类 .

    但这可以通过元类来解决,元类跟踪基类的继承,并且可能构建类,它继承自大多数特定的插件(下图中的“根扩展”)

    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
    

    因此,当你拥有使用元类创建的Root base并拥有从中继承的插件树时,你可以自动获取类,它只通过子类化继承自最特定的插件:

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

    代码库非常小(约30行纯代码),并且像继承一样灵活 .

    如果你有兴趣,请参与@ https://github.com/thodnev/pluginlib

  • 4

    你可以使用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"}'))
    
  • 3

    您也可以查看Groundwork .

    我们的想法是围绕可重用组件构建应用程序,称为模式和插件 . 插件是派生自 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 .

    Disclaimer :我是Groundwork的作者之一 .

  • 30

    在我们当前的医疗保健产品中,我们有一个使用接口类实现的插件架构 . 我们的技术堆栈是基于Python for API的Django和位于前端nodejs之上的Nuxtjs .

    我们有一个为我们的产品编写的插件管理器应用程序,基本上是pip和npm包,与Django和Nuxtjs一致 .

    对于新的插件开发(pip和npm),我们将插件管理器作为依赖 .

    在Pip包中:在setup.py的帮助下,您可以添加插件的入口点以使用插件管理器(注册表,启动等等)执行某些操作.https://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation

    在npm包中:与pip类似,npm脚本中有钩子来处理安装 . https://docs.npmjs.com/misc/scripts

    我们的用例:

    插件开发团队现在与核心开发团队分开 . 插件开发的范围是与第三方应用程序集成,这些应用程序在产品的任何类别中定义 . 插件接口分类为: - 传真,电话,电子邮件......等插件管理器可以增强到新的类别 .

    在你的情况下:也许你可以编写一个插件并重复使用它来做东西 .

    如果插件开发人员需要使用重用核心对象,则可以通过在插件管理器中执行抽象级别来使用该对象,以便任何插件都可以继承这些方法 .

    只是分享我们在产品中实施的方式,希望它能给出一点想法 .

相关问题