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

在Python中构建最小插件体系结构,python,architecture,plugins,Python,Architecture,Plugins,我有一个用Python编写的应用程序,它被相当技术性的受众(科学家)使用 我正在寻找一种让用户可以扩展应用程序的好方法,即脚本/插件架构 我正在寻找一种非常轻便的东西。大多数脚本或插件不会由第三方开发和分发并安装,而是由用户在几分钟内启动,以自动执行重复任务,添加对文件格式的支持等。因此插件应具有绝对最小的样板代码,除了复制到文件夹之外,不需要任何“安装”(因此像setuptools入口点或Zope插件架构之类的东西似乎太多了。) 有没有像这样的系统已经存在,或者有任何项目实现了类似的方案,我应

我有一个用Python编写的应用程序,它被相当技术性的受众(科学家)使用

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

我正在寻找一种非常轻便的东西。大多数脚本或插件不会由第三方开发和分发并安装,而是由用户在几分钟内启动,以自动执行重复任务,添加对文件格式的支持等。因此插件应具有绝对最小的样板代码,除了复制到文件夹之外,不需要任何“安装”(因此像setuptools入口点或Zope插件架构之类的东西似乎太多了。)


有没有像这样的系统已经存在,或者有任何项目实现了类似的方案,我应该看看它们的想法/灵感?

我的基本上是一个名为“插件”的目录,主应用程序可以轮询它,然后用它来获取文件,寻找一个著名的入口点,可能带有模块级配置参数,从那里开始。我使用文件监控工具来实现一定程度的动态性,插件处于活动状态,但这是一个很好的选择


当然,任何说“我不需要[大的,复杂的东西]X;我只想要一些轻量级的东西”的需求都有可能一次重新实现一个发现的需求。但这并不是说你不能从中获得乐趣:)

看看,这是一个很好的起点。我很喜欢,但这取决于您的用例。

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

鉴于我掌握的信息很少,我将以非常笼统的方式回答

你必须添加插件的方法是什么

  • 您可能需要添加一个配置文件,该文件将列出要加载的路径/目录
  • 另一种方式是说“该插件/目录中的任何文件都将被加载”,但要求用户在文件之间移动会带来不便
  • 最后一个中间选项是要求所有插件位于同一个插件/文件夹中,然后使用配置文件中的相对路径激活/停用它们
在纯代码/设计实践中,您必须清楚地确定希望用户扩展哪些行为/特定操作。确定将始终被覆盖的公共入口点/一组功能,并确定这些操作中的组。一旦这样做了,就可以很容易地扩展您的应用程序

使用挂钩的示例,灵感来自MediaWiki(PHP,但语言真的重要吗?)

另一个例子,灵感来自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钩子


但是,如果你想得到更多有用的答案,你必须缩小你的问题范围;)

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

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

1) 全世界都转向了个人电脑,2)当Tcl的组织能力开始变得非常不方便时,脚本的长度超过了500行。时光流逝

我退休了,Python被发明了,它看起来像是Tcl的完美继承者。现在,我从来没有做过移植,因为我从来没有在PC上编译(相当大的)C程序,用C包扩展Python,用Python/Gt?/Tk?/?做GUI??。然而,拥有可编辑模板脚本的旧想法似乎仍然可行。此外,以本机Python形式输入包命令也不会造成太大的负担,例如:

命令(arg0,arg1,…,param0,param1,…)

还有一些额外的点、括号和逗号,但这些都不是停止符

我记得看到有人用Python编写了lex和yacc版本(try:),所以如果仍然需要这些版本,它们就可以使用了

这种漫无边际的观点是,在我看来,Python本身就是科学家所期望的“轻量级”前端。我很想知道你为什么认为不是,我是认真的


后来添加:应用程序gedit预计插件将被添加,他们的站点对我在几分钟的浏览中发现的一个简单插件过程有了最清晰的解释。尝试:

我还是想更好地理解你的问题。我是
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 }
def plugin_main(*args, **kwargs):
    print args, kwargs
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)
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**")
# 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]
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
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)
# 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>
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 ...
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"}'))
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