我支持Roll Your Own方法。
所谓插件,是指:
插件:在运行时加载的模块或包,用于增强或修改主模块的行为。
我认为插件有两个要求:
1. 主模块能否在运行时加载插件?
2. 主模块和插件之间的数据是否可访问?
假设:
开发插件系统高度主观。有许多设计决策需要做出,没有一种真正的方法。通常的约束条件是时间、精力和经验。请注意,必须作出假设,并且实现通常定义术语(例如“插件”或“包”)。请友好地记录这些内容。
此插件实现假定:
A plugin is either a Python file or a directory (i.e. "plugin package")
A plugin package is a directory with structure:
plugin_package/
plugin_package.py <-- entry point
other_module.py <-+
some_subdir/ |- other files
icon.png <-+
Note that a Plugin package isn't necessarily a Python package. Whether or not a plugin package is a Python package depends on how you want to handle imports.
The plugin name is the same as the plugin entry point's file name, as well as the plugin package directory
The QApplication's QMainWindow is the primary source of data to share from the main module
1. 加载插件模块
加载插件模块需要两个信息:入口路径和插件名称。这些信息的获取方式因情况而异,通常需要进行字符串/路径解析。以下假设:
plugin_path
是插件入口的绝对路径,
plugin_name
是插件名称(根据上述假设,它是模块名称)。
Python 已经实现并重新实现了导入机制多次。在编写本文时(使用 Python 3.8),使用的导入方法是:
import importlib
from importlib.util import spec_from_loader, module_from_spec
from importlib.machinery import SourceFileLoader
loader = SourceFileLoader(plugin_name, plugin_path)
spec = spec_from_loader(plugin_name, loader)
plugin_module = module_from_spec(spec)
spec.loader.exec_module(plugin_module)
sys.modules[plugin_name] = plugin_module
为了简洁起见,以下内容省略了错误处理和已加载模块记录(例如通过将它们存储在字典中)。
2. 共享数据
数据可以在两个方向上进行共享:
- 主模块能否访问插件模块的数据?
- 插件模块能否访问主模块的数据?
主模块可以在导入后自由访问插件数据(根据定义)。只需访问插件模块的__dict__
即可:
some_data = plugin_module.__dict__.get('data')
从插件中访问主模块的数据是一个更棘手的问题。
如上所述,我通常认为QMainWindow与最终用户对“应用程序”的概念是同义词。 它是用户交互的主要小部件,并且通常包含大多数数据或可以轻松访问它。 挑战在于共享QMainWindow 实例。
共享QMainWindow实例数据的解决方案是将其设置为单例。 这迫使任何QMainWindow成为用户交互的唯一主窗口。 在Python中创建单例的方法有几种。 最常见的两种方法可能是使用元类或模块。 我在使用模块单例方法时取得了最大的成功。
将QMainWindow代码分解为单独的Python模块。 在模块级别,创建一个QMainWindow但不要初始化它。 创建一个模块级别的实例,以便其他模块可以将其作为模块属性访问。 不要初始化它,因为init需要一个QApplication(并且因为主窗口模块不是应用程序入口点)。
from PySide2 import QtWidgets
class MyMainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
main_window_instance = MyMainWindow.__new__(MyMainWindow)
使用单独的模块作为应用程序入口点,例如
main.py
。导入主窗口实例,创建QApplication并初始化主窗口。
import sys
from main_window import main_window_instance
from PySide2 import QtWidgets
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
main_window_instance.__init__()
main_window_instance.show()
sys.exit(app.exec_())
导入主窗口实例是使其成为单例的关键。在这里创建QApplication并不是必须的(它也是单例),但我认为这样更加清晰。
现在,在运行时加载插件时,它们可以导入main_window_instance。因为main.py入口点已经加载了main_window模块,所以它是主模块使用的主窗口(而不是新实例)。现在可以从插件模块访问主模块的数据。
from main_window import main_window_instance
main_window_instance.setWindowTitle('Changed from plugin')
评论
最小的设置需要三个文件:main.py
,main_window.py
和plugin.py
。 main_window.py
定义并实例化主窗口,main.py
使其成为单例并初始化它,plugin.py
导入并使用该实例。
许多细节被省略,希望主要组件能够显而易见。理想情况下,Qt和Python文档应足以填补空白......
还有进一步的考虑,例如如何分发和管理插件。插件可以远程托管,打包为(zip)存档,捆绑为适当的Python包等。插件可以像您想要的那样简单(或复杂)。希望这个答案为您提供了丰富的灵感,以便您设计插件系统的外观。