将Python应用打包为单个文件以支持插件或扩展?

7 投票
2 回答
1392 浏览
提问于 2025-04-15 22:57

有几种工具可以把一个Python包和它所有的依赖项打包成一个简单的可执行程序,这样就方便发给客户了。这些工具各有不同的操作步骤、限制和适用的操作系统:

我的情况更复杂一点:第三方开发者想要为我的应用程序编写插件、扩展或附加功能。用户在像Windows这样的系统上,最简单的安装插件或附加功能的方法是什么?这样我的应用程序才能轻松发现这些插件已经被安装了。除了这个基本问题,还有另一个问题:第三方开发者如何把他们的扩展和它所需的所有库(可能是一些二进制模块,比如lxml)打包在一起,这样插件的依赖项在插件可用时就能被导入。

这个问题该怎么解决呢?我的应用程序是否需要在磁盘上有自己的插件区域和插件注册表,以便处理这个问题?或者有没有一些通用的机制,我可以避免自己编写,这样一个作为单个可执行文件分发的应用程序就能找到同样作为单个文件安装的插件呢?

2 个回答

0

这里有一个关于Python应用程序使用插件的例子:OpenSTV。在这个例子中,插件只能是Python模块。

7

你可以创建一个插件目录,让你的应用在运行时(或者之后)扫描这个目录,导入相关的代码。下面是一个示例,它可以处理普通的 .py 或 .pyc 文件,甚至可以处理存放在 zip 文件里的插件(这样用户只需把 someplugin.zip 放到 'plugins' 目录里,就能神奇地工作):

import re, os, sys
class Plugin(object):
    """
    The base class from which all plugins are derived.  It is used by the
    plugin loading functions to find all the installed plugins.
    """
    def __init__(self, foo):
        self.foo = foo
    # Any useful base plugin methods would go in here.

def get_plugins(plugin_dir):
    """Adds plugins to sys.path and returns them as a list"""

    registered_plugins = []

    #check to see if a plugins directory exists and add any found plugins
    # (even if they're zipped)
    if os.path.exists(plugin_dir):
        plugins = os.listdir(plugin_dir)
        pattern = ".py$"
        for plugin in plugins:
            plugin_path = os.path.join(plugin_dir, plugin)
            if os.path.splitext(plugin)[1] == ".zip":
                sys.path.append(plugin_path)
                (plugin, ext) = os.path.splitext(plugin) # Get rid of the .zip extension
                registered_plugins.append(plugin)
            elif plugin != "__init__.py":
                if re.search(pattern, plugin):
                    (shortname, ext) = os.path.splitext(plugin)
                    registered_plugins.append(shortname)
            if os.path.isdir(plugin_path):
                plugins = os.listdir(plugin_path)
                for plugin in plugins:
                    if plugin != "__init__.py":
                        if re.search(pattern, plugin):
                            (shortname, ext) = os.path.splitext(plugin)
                            sys.path.append(plugin_path)
                            registered_plugins.append(shortname)
    return registered_plugins

def init_plugin_system(cfg):
    """
    Initializes the plugin system by appending all plugins into sys.path and
    then using load_plugins() to import them.

        cfg - A dictionary with two keys:
        plugin_path - path to the plugin directory (e.g. 'plugins')
        plugins - List of plugin names to import (e.g. ['foo', 'bar'])
    """
    if not cfg['plugin_path'] in sys.path:
        sys.path.insert(0, cfg['plugin_path'])
    load_plugins(cfg['plugins'])

def load_plugins(plugins):
    """
    Imports all plugins given a list.
    Note:  Assumes they're all in sys.path.
    """
    for plugin in plugins:
        __import__(plugin, None, None, [''])
        if plugin not in Plugin.__subclasses__():
            # This takes care of importing zipped plugins:
            __import__(plugin, None, None, [plugin])

假设我在一个叫 'plugins' 的目录里有一个名为 "foo.py" 的插件(这个目录在我的应用的根目录下),它会为我的应用添加一个新功能。这个文件的内容可能是这样的:

from plugin_stuff import Plugin

class Foo(Plugin):
    """An example plugin."""
    self.menu_entry = {'Tools': {'Foo': self.bar}}
    def bar(self):
        return "foo plugin!"

我可以在启动应用时这样初始化我的插件:

plugin_dir = "%s/plugins" % os.getcwd()
plugin_list = get_plugins(plugin_dir)
init_plugin_system({'plugin_path': plugin_dir, 'plugins': plugin_list})
plugins = find_plugins()
plugin_menu_entries = []
for plugin in plugins:
    print "Enabling plugin: %s" % plugin.__name__
    plugin_menu_entries.append(plugin.menu_entry))
add_menu_entries(plugin_menu_entries) # This is an imaginary function

只要插件是 .py 或 .pyc 文件(假设它已经为相关平台进行了字节编译),这个方法就能正常工作。插件可以是独立的文件,也可以放在一个包含 init.py 的目录里,或者放在一个 zip 文件里,规则都是一样的。

我怎么知道这个方法有效呢?因为我在 PyCI 中就是这样实现插件的。PyCI 是一个网页应用,但这个方法同样适用于普通的图形界面应用。上面的例子中,我选择使用一个虚构的 add_menu_entries() 函数,结合一个插件对象变量,用来将插件的方法添加到你的图形界面的菜单中。

希望这个回答能帮助你构建自己的插件系统。如果你想详细了解它是如何实现的,我建议你下载 PyCI 的源代码,查看 plugin_utils.py 和 plugins_enabled 目录中的示例插件。

撰写回答