Feature: 嵌套插件名称作用域优化 (#2665)

This commit is contained in:
Ju4tCode
2024-04-20 14:47:12 +08:00
committed by GitHub
parent e15d544341
commit 6bf10aafb7
16 changed files with 262 additions and 131 deletions

View File

@ -50,8 +50,8 @@ C = TypeVar("C", bound=BaseModel)
_plugins: dict[str, "Plugin"] = {}
_managers: list["PluginManager"] = []
_current_plugin_chain: ContextVar[tuple["Plugin", ...]] = ContextVar(
"_current_plugin_chain", default=()
_current_plugin: ContextVar[Optional["Plugin"]] = ContextVar(
"_current_plugin", default=None
)
@ -59,34 +59,87 @@ def _module_name_to_plugin_name(module_name: str) -> str:
return module_name.rsplit(".", 1)[-1]
def _controlled_modules() -> dict[str, str]:
return {
plugin_id: module_name
for manager in _managers
for plugin_id, module_name in manager.controlled_modules.items()
}
def _find_parent_plugin_id(
module_name: str, controlled_modules: Optional[dict[str, str]] = None
) -> Optional[str]:
if controlled_modules is None:
controlled_modules = _controlled_modules()
available = {
module_name: plugin_id for plugin_id, module_name in controlled_modules.items()
}
while "." in module_name:
module_name, _ = module_name.rsplit(".", 1)
if module_name in available:
return available[module_name]
def _module_name_to_plugin_id(
module_name: str, controlled_modules: Optional[dict[str, str]] = None
) -> str:
plugin_name = _module_name_to_plugin_name(module_name)
if parent_plugin_id := _find_parent_plugin_id(module_name, controlled_modules):
return f"{parent_plugin_id}:{plugin_name}"
return plugin_name
def _new_plugin(
module_name: str, module: ModuleType, manager: "PluginManager"
) -> "Plugin":
plugin_name = _module_name_to_plugin_name(module_name)
if plugin_name in _plugins:
raise RuntimeError("Plugin already exists! Check your plugin name.")
plugin = Plugin(plugin_name, module, module_name, manager)
_plugins[plugin_name] = plugin
plugin_id = _module_name_to_plugin_id(module_name)
if plugin_id in _plugins:
raise RuntimeError(
f"Plugin {plugin_id} already exists! Check your plugin name."
)
parent_plugin_id = _find_parent_plugin_id(module_name)
if parent_plugin_id is not None and parent_plugin_id not in _plugins:
raise RuntimeError(
f"Parent plugin {parent_plugin_id} must "
f"be loaded before loading {plugin_id}."
)
parent_plugin = _plugins[parent_plugin_id] if parent_plugin_id is not None else None
plugin = Plugin(
name=_module_name_to_plugin_name(module_name),
module=module,
module_name=module_name,
manager=manager,
parent_plugin=parent_plugin,
)
if parent_plugin:
parent_plugin.sub_plugins.add(plugin)
_plugins[plugin_id] = plugin
return plugin
def _revert_plugin(plugin: "Plugin") -> None:
if plugin.name not in _plugins:
if plugin.id_ not in _plugins:
raise RuntimeError("Plugin not found!")
del _plugins[plugin.name]
del _plugins[plugin.id_]
if parent_plugin := plugin.parent_plugin:
parent_plugin.sub_plugins.remove(plugin)
parent_plugin.sub_plugins.discard(plugin)
def get_plugin(name: str) -> Optional["Plugin"]:
def get_plugin(plugin_id: str) -> Optional["Plugin"]:
"""获取已经导入的某个插件。
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
如果为嵌套的子插件,标识符为 `父插件标识符:子插件文件(夹)名`。
参数:
name: 插件,即 {ref}`nonebot.plugin.model.Plugin.name`。
plugin_id: 插件标识符,即 {ref}`nonebot.plugin.model.Plugin.id_`。
"""
return _plugins.get(name)
return _plugins.get(plugin_id)
def get_plugin_by_module_name(module_name: str) -> Optional["Plugin"]:
@ -111,7 +164,7 @@ def get_loaded_plugins() -> set["Plugin"]:
def get_available_plugin_names() -> set[str]:
"""获取当前所有可用的插件(包含尚未加载的插件)。"""
"""获取当前所有可用的插件标识符(包含尚未加载的插件)。"""
return {*chain.from_iterable(manager.available_plugins for manager in _managers)}

View File

@ -15,7 +15,7 @@ from nonebot.utils import path_to_module_name
from .model import Plugin
from .manager import PluginManager
from . import _managers, get_plugin, _current_plugin_chain, _module_name_to_plugin_name
from . import _managers, get_plugin, _module_name_to_plugin_id
try: # pragma: py-gte-311
import tomllib # pyright: ignore[reportMissingImports]
@ -151,36 +151,40 @@ def load_builtin_plugins(*plugins: str) -> set[Plugin]:
def _find_manager_by_name(name: str) -> Optional[PluginManager]:
for manager in reversed(_managers):
if name in manager.plugins or name in manager.searched_plugins:
if (
name in manager.controlled_modules
or name in manager.controlled_modules.values()
):
return manager
def require(name: str) -> ModuleType:
"""获取一个插件的导出内容
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
"""声明依赖插件
参数:
name: 插件名,即 {ref}`nonebot.plugin.model.Plugin.name`
name: 插件模块名或插件标识符,仅在已声明插件的情况下可使用标识符
异常:
RuntimeError: 插件无法加载
"""
plugin = get_plugin(_module_name_to_plugin_name(name))
if "." in name:
# name is a module name
plugin = get_plugin(_module_name_to_plugin_id(name))
else:
# name is a plugin id or simple module name (equals to plugin id)
plugin = get_plugin(name)
# if plugin not loaded
if not plugin:
# plugin already declared
if plugin is None:
# plugin already declared, module name / plugin id
if manager := _find_manager_by_name(name):
plugin = manager.load_plugin(name)
# plugin not declared, try to declare and load it
else:
# clear current plugin chain, ensure plugin loaded in a new context
_t = _current_plugin_chain.set(())
try:
plugin = load_plugin(name)
finally:
_current_plugin_chain.reset(_t)
if not plugin:
plugin = load_plugin(name)
if plugin is None:
raise RuntimeError(f'Cannot load plugin "{name}"!')
return plugin.module
@ -200,9 +204,11 @@ def inherit_supported_adapters(*names: str) -> Optional[set[str]]:
final_supported: Optional[set[str]] = None
for name in names:
plugin = get_plugin(_module_name_to_plugin_name(name))
plugin = get_plugin(_module_name_to_plugin_id(name))
if plugin is None:
raise RuntimeError(f'Plugin "{name}" is not loaded!')
raise RuntimeError(
f'Plugin "{name}" is not loaded! You should require it first.'
)
meta = plugin.metadata
if meta is None:
raise ValueError(f'Plugin "{name}" has no metadata!')

View File

@ -26,8 +26,8 @@ from . import (
_managers,
_new_plugin,
_revert_plugin,
_current_plugin_chain,
_module_name_to_plugin_name,
_current_plugin,
_module_name_to_plugin_id,
)
@ -36,7 +36,7 @@ class PluginManager:
参数:
plugins: 独立插件模块名集合。
search_path: 插件搜索路径(文件夹)。
search_path: 插件搜索路径(文件夹),相对于当前工作目录
"""
def __init__(
@ -49,29 +49,38 @@ class PluginManager:
self.search_path: set[str] = set(search_path or [])
# cache plugins
self._third_party_plugin_names: dict[str, str] = {}
self._searched_plugin_names: dict[str, Path] = {}
self.prepare_plugins()
self._third_party_plugin_ids: dict[str, str] = {}
self._searched_plugin_ids: dict[str, str] = {}
self._prepare_plugins()
def __repr__(self) -> str:
return f"PluginManager(plugins={self.plugins}, search_path={self.search_path})"
return f"PluginManager(available_plugins={self.controlled_modules})"
@property
def third_party_plugins(self) -> set[str]:
"""返回所有独立插件名称"""
return set(self._third_party_plugin_names.keys())
"""返回所有独立插件标识符"""
return set(self._third_party_plugin_ids.keys())
@property
def searched_plugins(self) -> set[str]:
"""返回已搜索到的插件名称"""
return set(self._searched_plugin_names.keys())
"""返回已搜索到的插件标识符"""
return set(self._searched_plugin_ids.keys())
@property
def available_plugins(self) -> set[str]:
"""返回当前插件管理器中可用的插件名称"""
"""返回当前插件管理器中可用的插件标识符"""
return self.third_party_plugins | self.searched_plugins
def _previous_plugins(self) -> set[str]:
@property
def controlled_modules(self) -> dict[str, str]:
"""返回当前插件管理器中控制的插件标识符与模块路径映射字典。"""
return dict(
chain(
self._third_party_plugin_ids.items(), self._searched_plugin_ids.items()
)
)
def _previous_controlled_modules(self) -> dict[str, str]:
_pre_managers: list[PluginManager]
if self in _managers:
_pre_managers = _managers[: _managers.index(self)]
@ -79,26 +88,35 @@ class PluginManager:
_pre_managers = _managers[:]
return {
*chain.from_iterable(manager.available_plugins for manager in _pre_managers)
plugin_id: module_name
for manager in _pre_managers
for plugin_id, module_name in manager.controlled_modules.items()
}
def prepare_plugins(self) -> set[str]:
def _prepare_plugins(self) -> set[str]:
"""搜索插件并缓存插件名称。"""
# get all previous ready to load plugins
previous_plugins = self._previous_plugins()
searched_plugins: dict[str, Path] = {}
third_party_plugins: dict[str, str] = {}
previous_plugin_ids = self._previous_controlled_modules()
# if self not in global managers, merge self's controlled modules
def get_controlled_modules():
return (
previous_plugin_ids
if self in _managers
else {**previous_plugin_ids, **self.controlled_modules}
)
# check third party plugins
for plugin in self.plugins:
name = _module_name_to_plugin_name(plugin)
if name in third_party_plugins or name in previous_plugins:
plugin_id = _module_name_to_plugin_id(plugin, get_controlled_modules())
if (
plugin_id in self._third_party_plugin_ids
or plugin_id in previous_plugin_ids
):
raise RuntimeError(
f"Plugin already exists: {name}! Check your plugin name"
f"Plugin already exists: {plugin_id}! Check your plugin name"
)
third_party_plugins[name] = plugin
self._third_party_plugin_names = third_party_plugins
self._third_party_plugin_ids[plugin_id] = plugin
# check plugins in search path
for module_info in pkgutil.iter_modules(self.search_path):
@ -106,47 +124,55 @@ class PluginManager:
if module_info.name.startswith("_"):
continue
if (
module_info.name in searched_plugins
or module_info.name in previous_plugins
or module_info.name in third_party_plugins
):
raise RuntimeError(
f"Plugin already exists: {module_info.name}! Check your plugin name"
)
if not (
module_spec := module_info.module_finder.find_spec(
module_info.name, None
)
):
continue
if not (module_path := module_spec.origin):
continue
searched_plugins[module_info.name] = Path(module_path).resolve()
self._searched_plugin_names = searched_plugins
if not module_spec.origin:
continue
# get module name from path, pkgutil does not return the actual module name
module_path = Path(module_spec.origin).resolve()
module_name = path_to_module_name(module_path)
plugin_id = _module_name_to_plugin_id(module_name, get_controlled_modules())
if (
plugin_id in previous_plugin_ids
or plugin_id in self._third_party_plugin_ids
or plugin_id in self._searched_plugin_ids
):
raise RuntimeError(
f"Plugin already exists: {plugin_id}! Check your plugin name"
)
self._searched_plugin_ids[plugin_id] = module_name
return self.available_plugins
def load_plugin(self, name: str) -> Optional[Plugin]:
"""加载指定插件。
对于独立插件,可以使用完整插件模块名或者插件名称
可以使用完整插件模块名或者插件标识符加载
参数:
name: 插件名称。
name: 插件名称或插件标识符
"""
try:
if name in self.plugins:
# load using plugin id
if name in self._third_party_plugin_ids:
module = importlib.import_module(self._third_party_plugin_ids[name])
elif name in self._searched_plugin_ids:
module = importlib.import_module(self._searched_plugin_ids[name])
# load using module name
elif (
name in self._third_party_plugin_ids.values()
or name in self._searched_plugin_ids.values()
):
module = importlib.import_module(name)
elif name in self._third_party_plugin_names:
module = importlib.import_module(self._third_party_plugin_names[name])
elif name in self._searched_plugin_names:
module = importlib.import_module(
path_to_module_name(self._searched_plugin_names[name])
)
else:
raise RuntimeError(f"Plugin not found: {name}! Check your plugin name")
@ -155,13 +181,13 @@ class PluginManager:
) is None or not isinstance(plugin, Plugin):
raise RuntimeError(
f"Module {module.__name__} is not loaded as a plugin! "
"Make sure not to import it before loading."
f"Make sure not to import it before loading."
)
logger.opt(colors=True).success(
f'Succeeded to load plugin "<y>{escape_tag(plugin.name)}</y>"'
f'Succeeded to load plugin "<y>{escape_tag(plugin.id_)}</y>"'
+ (
f' from "<m>{escape_tag(plugin.module_name)}</m>"'
if plugin.module_name != plugin.name
if plugin.module_name != plugin.id_
else ""
)
)
@ -193,21 +219,16 @@ class PluginFinder(MetaPathFinder):
module_origin = module_spec.origin
if not module_origin:
return
module_path = Path(module_origin).resolve()
for manager in reversed(_managers):
# use path instead of name in case of submodule name conflict
if (
fullname in manager.plugins
or module_path in manager._searched_plugin_names.values()
):
if fullname in manager.controlled_modules.values():
module_spec.loader = PluginLoader(manager, fullname, module_origin)
return module_spec
return
class PluginLoader(SourceFileLoader):
def __init__(self, manager: PluginManager, fullname: str, path) -> None:
def __init__(self, manager: PluginManager, fullname: str, path: str) -> None:
self.manager = manager
self.loaded = False
super().__init__(fullname, path)
@ -227,17 +248,8 @@ class PluginLoader(SourceFileLoader):
plugin = _new_plugin(self.name, module, self.manager)
setattr(module, "__plugin__", plugin)
# detect parent plugin before entering current plugin context
parent_plugins = _current_plugin_chain.get()
for pre_plugin in reversed(parent_plugins):
# ensure parent plugin is declared before current plugin
if _managers.index(pre_plugin.manager) < _managers.index(self.manager):
plugin.parent_plugin = pre_plugin
pre_plugin.sub_plugins.add(plugin)
break
# enter plugin context
_plugin_token = _current_plugin_chain.set((*parent_plugins, plugin))
_plugin_token = _current_plugin.set(plugin)
try:
super().exec_module(module)
@ -246,7 +258,7 @@ class PluginLoader(SourceFileLoader):
raise
finally:
# leave plugin context
_current_plugin_chain.reset(_plugin_token)
_current_plugin.reset(_plugin_token)
# get plugin metadata
metadata: Optional[PluginMetadata] = getattr(module, "__plugin_meta__", None)

View File

@ -66,7 +66,7 @@ class Plugin:
"""存储插件信息"""
name: str
"""插件索引标识NoneBot 使用 文件/文件夹 名称作为标识符"""
"""插件名称NoneBot 使用 文件/文件夹 名称作为插件名称"""
module: ModuleType
"""插件模块对象"""
module_name: str
@ -80,3 +80,10 @@ class Plugin:
sub_plugins: set["Plugin"] = field(default_factory=set)
"""子插件集合"""
metadata: Optional[PluginMetadata] = None
@property
def id_(self) -> str:
"""插件索引标识"""
return (
f"{self.parent_plugin.id_}:{self.name}" if self.parent_plugin else self.name
)

View File

@ -31,8 +31,8 @@ from nonebot.rule import (
)
from .model import Plugin
from .manager import _current_plugin
from . import get_plugin_by_module_name
from .manager import _current_plugin_chain
def store_matcher(matcher: type[Matcher]) -> None:
@ -42,8 +42,8 @@ def store_matcher(matcher: type[Matcher]) -> None:
matcher: 事件响应器
"""
# only store the matcher defined when plugin loading
if plugin_chain := _current_plugin_chain.get():
plugin_chain[-1].matcher.add(matcher)
if plugin := _current_plugin.get():
plugin.matcher.add(matcher)
def get_matcher_plugin(depth: int = 1) -> Optional[Plugin]: # pragma: no cover
@ -96,16 +96,14 @@ def get_matcher_source(depth: int = 0) -> Optional[MatcherSource]:
module_name = (module := inspect.getmodule(frame)) and module.__name__
plugin: Optional["Plugin"] = None
# matcher defined when plugin loading
if plugin_chain := _current_plugin_chain.get():
plugin = plugin_chain[-1]
plugin: Optional["Plugin"] = _current_plugin.get()
# matcher defined when plugin running
elif module_name:
if plugin is None and module_name:
plugin = get_plugin_by_module_name(module_name)
return MatcherSource(
plugin_name=plugin and plugin.name,
plugin_id=plugin and plugin.id_,
module_name=module_name,
lineno=frame.f_lineno,
)