我是 Roll Your Own 方法的粉丝。
插件,我的意思是:
-
插件:在运行时加载的模块或包,用于增强或修改主模块的行为
我看到插件有两个要求:
- 主模块可以在运行时加载插件吗?
- 主模块和插件之间是否可以访问数据?
假设
开发插件系统是非常主观的。有许多设计决策需要做出,并且没有一种真正的方式。通常的限制是时间、精力和经验。请注意,必须做出假设,并且实现通常会定义术语(例如“插件”或“包”)。善待并尽可能记录这些内容。
此插件实现假设:
-
插件可以是 Python 文件或目录(即“插件包”)
-
插件包是一个目录结构:
plugin_package/
plugin_package.py <-- entry point
other_module.py <-+
some_subdir/ |- other files
icon.png <-+
请注意,插件包不一定是 Python 包。插件包是否是 Python 包取决于您要如何处理导入。
-
插件名称与插件入口点的文件名相同,插件包目录也一样
-
QApplication 的 QMainWindow 是从主模块共享的主要数据源
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)
# Required for reloading a module
sys.modules[plugin_name] = plugin_module
# # This is how to reload a module
# importlib.reload(plugin_module)
包含错误处理和已加载模块的记录可能是个好主意(例如,通过将它们存储在字典中)。为简洁起见,此处排除了这些详细信息。
2.共享数据
数据共享有两个方向:
- 主模块可以访问插件模块的数据吗?
- 插件模块可以访问主模块的数据吗?
主模块可以在导入后免费访问插件数据(根据定义)。只需访问插件模块的__dict__:
# Accessing plugin data from the main module
some_data = plugin_module.__dict__.get('data')
从插件中访问主模块的数据是一个棘手的问题。
如上所述,我通常将 QMainWindow 视为最终用户的“应用程序”概念的同义词。它是用户与之交互的主要小部件,因此通常包含大部分数据或可以轻松访问它。挑战在于共享 QMainWindow 实例。
共享 QMainWindow 实例数据的解决方案是使其成为单例。 这会强制任何 QMainWindow 成为用户与之交互的主窗口。在 Python 中有几种创建单例的方法。两种最常见的方法可能是使用metaclasses 或模块。我使用模块单例方法取得了最大的成功。
将 QMainWindow 代码分解为一个单独的 Python 模块。在模块级别,创建但不初始化 QMainWindow。创建一个模块级实例,以便其他模块可以作为模块属性访问该实例。不要初始化它,因为 init 需要一个 QApplication(并且因为主窗口模块不是应用程序入口点)。
# main_window.py
from PySide2 import QtWidgets
class MyMainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
main_window_instance = MyMainWindow.__new__(MyMainWindow)
为应用程序入口点使用单独的模块,例如 main.py。导入主窗口实例,创建QApplication,初始化主窗口。
# main.py
import sys
# Importing the instance makes it a module singleton
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_window 模块已经被main.py 入口点加载,所以它是主模块使用的主窗口(而不是新实例)。现在可以从插件模块访问主模块中的数据。
# plugin.py
# The plugin can now access the main module's data
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 包等。插件可以像您想要的一样简单(或复杂)。希望这个答案为您希望插件系统的外观提供充足的灵感。