代码之家  ›  专栏  ›  技术社区  ›  dF.

在python中构建一个最小的插件架构

  •  175
  • dF.  · 技术社区  · 15 年前

    我有一个用Python编写的应用程序,它被相当专业的读者(科学家)使用。

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

    我在找东西 非常轻 . 大多数脚本或插件不会由第三方开发和分发并安装,但将是用户在几分钟内为自动执行重复任务、添加对文件格式的支持等而突然产生的东西。因此插件应该具有绝对最小的样板代码,除了复制外,不需要“安装”。到一个文件夹(因此,类似于SETUPTOOLS入口点或Zope插件架构的内容似乎太多。)

    有没有像这样的系统已经存在,或任何项目,实施类似的计划,我应该看看的想法/灵感?

    18 回复  |  直到 6 年前
        1
  •  144
  •   desolat Nathan MacInnes    9 年前

    我的基本上是一个名为“插件”的目录,主应用程序可以对其进行轮询,然后使用 imp.load_module 要获取文件,请使用模块级配置参数查找一个众所周知的入口点,然后从那里开始。我使用文件监控工具来进行一定程度的动态监控,插件是活动的,但这是一个很好的方法。

    当然,任何随附的“我不需要(大的,复杂的东西)x;我只需要一些轻量级的东西”的需求都有一次重新实现x-one发现的需求的风险。但这并不是说你做这件事也没什么乐趣:)

        2
  •  47
  •   Community Teyam    7 年前

    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 还有一些字符串操作。

        3
  •  30
  •   PhilS    15 年前

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

        4
  •  24
  •   Nicolas Dumazet    15 年前

    虽然这个问题真的很有趣,但如果没有更多的细节,我想很难回答。这是什么类型的应用程序?它有图形用户界面吗?它是命令行工具吗?一套脚本?具有唯一入口点等的程序…

    鉴于我掌握的信息很少,我将以非常一般的方式回答。

    你必须添加插件是什么意思?

    • 您可能需要添加一个配置文件,该文件将列出要加载的路径/目录。
    • 另一种方法是说“该插件/目录中的任何文件都将被加载”,但是要求用户移动文件并不方便。
    • 最后一个中间选项是要求所有插件都在同一个插件/文件夹中,然后使用配置文件中的相对路径激活/停用它们。

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

    实例使用 钩子 从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)
    

    另一个例子,灵感来自于反复无常。在这里,扩展只将命令添加到 命令行可执行文件,扩展行为。

    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使用调用所有扩展的repetup(ui,repo),也可以使用hook.setup hook的hook类方法。

    但同样,如果你想要更有用的答案,你必须缩小你的问题范围;)

        5
  •  11
  •   edomaur    14 年前

    Marty Allchin's simple plugin framework 是我用来满足自己需要的基础。我真的建议看一看,我认为如果你想要一些简单和容易破解的东西,这真的是一个很好的开始。你也可以找到它 as a Django Snippets .

        6
  •  11
  •   Mac Umer    10 年前

    我是一个退休的生物学家,他处理数字显微照片,发现自己必须写一个图像处理和分析包(技术上不是一个图书馆)运行在SGI机器上。我用C编写代码,并使用TCL编写脚本语言。图形用户界面,就像以前一样,是使用tk完成的。出现在tcl中的命令的格式为“extensionname commandname arg0 arg1…”param0 param1…,即简单的空格分隔的单词和数字。当tcl看到“extensionname”子字符串时,控制权被传递到c包。然后通过lexer/parser(在lex/yacc中完成)运行该命令,然后根据需要调用c例程。

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

    我退休了,巨蟒被发明了,它看起来是TCL的完美继承者。现在,我从来没有做过这个端口,因为我从来没有面对过在PC上编译(相当大的)C程序、用C包扩展python以及用python/gt执行gui的挑战。TK???然而,拥有可编辑模板脚本的旧想法似乎仍然可行。此外,以本机python形式输入package命令不应该太麻烦,例如:

    packagename.command(arg0,arg1,…,param0,param1,…)

    一些额外的点、parens和逗号,但它们不是ShowStopper。

    我记得有人在python中完成了lex和yacc的版本(尝试: http://www.dabeaz.com/ply/ ,所以如果仍然需要这些,它们就在附近。

    这种杂乱无章的观点是,在我看来,Python本身就是科学家们所期望的“轻量级”前端。我很好奇你为什么不这么认为,我是认真的。


    稍后添加:应用程序 吉迪特

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

    我还是想更好地理解你的问题。我不清楚您是1)希望科学家能够非常简单地以各种方式使用您的(python)应用程序,还是2)希望允许科学家向您的应用程序添加新功能。选择1是我们面对图像的情况,这导致我们使用通用脚本,我们对其进行了修改以适应当前的需要。是选择2导致您想到插件,还是应用程序的某个方面使向插件发出命令变得不可行?

        7
  •  10
  •   KeepCalmAndCarryOn    6 年前

    当我搜索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**")
    
        8
  •  7
  •   Tobias Kienzler    11 年前

    我很喜欢2009年Pycon的AndreRoberge博士关于不同插件架构的精彩讨论。他很好地概述了实现插件的不同方法,从一些非常简单的事情开始。

    可作为 podcast (第二部分是猴子修补的解释)伴随着一系列的 six blog entries .

    我建议你在做决定之前先快速听一下。

        9
  •  4
  •   samwyse    11 年前

    我来到这里是为了寻找一个最小的插件架构,并且发现了很多对我来说似乎都是多余的东西。所以,我已经实施了 Super Simple Python Plugins . 要使用它,您可以创建一个或多个目录并删除 __init__.py 在每个文件中。导入这些目录将导致所有其他python文件作为子模块加载,它们的名称将放置在 __all__ 名单。然后由您来验证/初始化/注册这些模块。自述文件中有一个例子。

        10
  •  4
  •   ankostis    7 年前

    事实上 设置工具 使用“插件目录”,如项目文档中的以下示例所示: 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)
    

    从长远来看, 设置工具 这是一个更安全的选择,因为它可以加载插件而不产生冲突或缺少需求。

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

        11
  •  3
  •   Alois Mahdal    8 年前

    作为插件系统的另一种方法,您可以检查 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]
    

    伸长肌 在上面的示例中,当创建新的 MyCoolClass 我们得到了新类的实例,它是这两个类的子类 MyCoolClassExtension MyCube类 多亏了python的 multiple inheritance

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

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

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

    此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 是延伸。

    要启用扩展,只需导入包含扩展类的模块,以及(在上面的情况下)所有 记录 在它之后创建的对象将在基类中具有扩展类,从而具有它的所有功能。

    这个库的主要优点是,操作可扩展对象的代码不需要知道扩展,扩展可以改变可扩展对象中的一切。

        12
  •  3
  •   jfs    8 年前

    setuptools has an EntryPoint :

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

    afaik如果使用pip或virtualenv,此软件包始终可用。

        13
  •  2
  •   Petar Marić    8 年前

    扩大@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>
    
        14
  •  2
  •   chfw    7 年前

    我花了很多时间阅读这个线程,而我不时地在Python中搜索插件框架。我有 used some but there were shortcomings 和他们在一起。以下是我在2017年为您提供的详细信息,这是一个无接口、松散耦合的插件管理系统: Load me later . 这里是 tutorials 关于如何使用它。

        15
  •  1
  •   thodnev    8 年前

    我花了很多时间为python寻找适合我需要的小插件系统。但是我只是想,如果已经有了一个自然而灵活的继承,为什么不使用它呢?

    为插件使用继承的唯一问题是,您不知道什么是最具体的(继承树上最低的)插件类。

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

    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

        16
  •  1
  •   aviso    6 年前

    你可以使用 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"}'))
    
        17
  •  1
  •   ub_marco    6 年前

    你也可以看看 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 .

    免责声明 :我是基础工作的作者之一。

        18
  •  0
  •   Shankar Ganesh Jayaraman    6 年前

    在我们当前的医疗保健产品中,我们有一个插件体系结构用接口类实现。我们的技术栈是jjango,位于python for api之上,nuxtjs位于nodejs的前端之上。

    我们为我们的产品编写了一个插件管理器应用程序,它基本上是符合Django和NuxtJS的PIP和NPM包。

    对于新的插件开发(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

    我们的用例:

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

    在您的例子中:也许您可以编写一个插件,并在做事情时重用它。

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

    只是分享一下我们在产品中的实现方式,希望它能给我们一些建议。