mirror of
https://github.com/nonebot/nonebot2.git
synced 2024-11-27 18:45:05 +08:00
✨ Feature: 嵌套插件名称作用域优化 (#2665)
This commit is contained in:
parent
e15d544341
commit
6bf10aafb7
@ -266,11 +266,12 @@ def _resolve_combine_expr(obj_str: str) -> type[Driver]:
|
|||||||
|
|
||||||
|
|
||||||
def _log_patcher(record: "loguru.Record"):
|
def _log_patcher(record: "loguru.Record"):
|
||||||
|
"""使用插件标识优化日志展示"""
|
||||||
record["name"] = (
|
record["name"] = (
|
||||||
plugin.name
|
plugin.id_
|
||||||
if (module_name := record["name"])
|
if (module_name := record["name"])
|
||||||
and (plugin := get_plugin_by_module_name(module_name))
|
and (plugin := get_plugin_by_module_name(module_name))
|
||||||
else (module_name and module_name.split(".")[0])
|
else (module_name and module_name.split(".", maxsplit=1)[0])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,8 +83,8 @@ current_handler: ContextVar[Dependent] = ContextVar("current_handler")
|
|||||||
class MatcherSource:
|
class MatcherSource:
|
||||||
"""Matcher 源代码上下文信息"""
|
"""Matcher 源代码上下文信息"""
|
||||||
|
|
||||||
plugin_name: Optional[str] = None
|
plugin_id: Optional[str] = None
|
||||||
"""事件响应器所在插件名称"""
|
"""事件响应器所在插件标识符"""
|
||||||
module_name: Optional[str] = None
|
module_name: Optional[str] = None
|
||||||
"""事件响应器所在插件模块的路径名"""
|
"""事件响应器所在插件模块的路径名"""
|
||||||
lineno: Optional[int] = None
|
lineno: Optional[int] = None
|
||||||
@ -95,8 +95,13 @@ class MatcherSource:
|
|||||||
"""事件响应器所在插件"""
|
"""事件响应器所在插件"""
|
||||||
from nonebot.plugin import get_plugin
|
from nonebot.plugin import get_plugin
|
||||||
|
|
||||||
if self.plugin_name is not None:
|
if self.plugin_id is not None:
|
||||||
return get_plugin(self.plugin_name)
|
return get_plugin(self.plugin_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin_name(self) -> Optional[str]:
|
||||||
|
"""事件响应器所在插件名"""
|
||||||
|
return self.plugin and self.plugin.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def module(self) -> Optional[ModuleType]:
|
def module(self) -> Optional[ModuleType]:
|
||||||
@ -245,7 +250,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
)
|
)
|
||||||
source = source or (
|
source = source or (
|
||||||
MatcherSource(
|
MatcherSource(
|
||||||
plugin_name=plugin and plugin.name,
|
plugin_id=plugin and plugin.id_,
|
||||||
module_name=module and module.__name__,
|
module_name=module and module.__name__,
|
||||||
)
|
)
|
||||||
if plugin is not None or module is not None
|
if plugin is not None or module is not None
|
||||||
@ -328,15 +333,20 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
return cls._source and cls._source.plugin
|
return cls._source and cls._source.plugin
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def module(cls) -> Optional[ModuleType]:
|
def plugin_id(cls) -> Optional[str]:
|
||||||
"""事件响应器所在插件模块"""
|
"""事件响应器所在插件标识符"""
|
||||||
return cls._source and cls._source.module
|
return cls._source and cls._source.plugin_id
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def plugin_name(cls) -> Optional[str]:
|
def plugin_name(cls) -> Optional[str]:
|
||||||
"""事件响应器所在插件名"""
|
"""事件响应器所在插件名"""
|
||||||
return cls._source and cls._source.plugin_name
|
return cls._source and cls._source.plugin_name
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def module(cls) -> Optional[ModuleType]:
|
||||||
|
"""事件响应器所在插件模块"""
|
||||||
|
return cls._source and cls._source.module
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def module_name(cls) -> Optional[str]:
|
def module_name(cls) -> Optional[str]:
|
||||||
"""事件响应器所在插件模块路径"""
|
"""事件响应器所在插件模块路径"""
|
||||||
|
@ -50,8 +50,8 @@ C = TypeVar("C", bound=BaseModel)
|
|||||||
|
|
||||||
_plugins: dict[str, "Plugin"] = {}
|
_plugins: dict[str, "Plugin"] = {}
|
||||||
_managers: list["PluginManager"] = []
|
_managers: list["PluginManager"] = []
|
||||||
_current_plugin_chain: ContextVar[tuple["Plugin", ...]] = ContextVar(
|
_current_plugin: ContextVar[Optional["Plugin"]] = ContextVar(
|
||||||
"_current_plugin_chain", default=()
|
"_current_plugin", default=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -59,34 +59,87 @@ def _module_name_to_plugin_name(module_name: str) -> str:
|
|||||||
return module_name.rsplit(".", 1)[-1]
|
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(
|
def _new_plugin(
|
||||||
module_name: str, module: ModuleType, manager: "PluginManager"
|
module_name: str, module: ModuleType, manager: "PluginManager"
|
||||||
) -> "Plugin":
|
) -> "Plugin":
|
||||||
plugin_name = _module_name_to_plugin_name(module_name)
|
plugin_id = _module_name_to_plugin_id(module_name)
|
||||||
if plugin_name in _plugins:
|
if plugin_id in _plugins:
|
||||||
raise RuntimeError("Plugin already exists! Check your plugin name.")
|
raise RuntimeError(
|
||||||
plugin = Plugin(plugin_name, module, module_name, manager)
|
f"Plugin {plugin_id} already exists! Check your plugin name."
|
||||||
_plugins[plugin_name] = plugin
|
)
|
||||||
|
|
||||||
|
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
|
return plugin
|
||||||
|
|
||||||
|
|
||||||
def _revert_plugin(plugin: "Plugin") -> None:
|
def _revert_plugin(plugin: "Plugin") -> None:
|
||||||
if plugin.name not in _plugins:
|
if plugin.id_ not in _plugins:
|
||||||
raise RuntimeError("Plugin not found!")
|
raise RuntimeError("Plugin not found!")
|
||||||
del _plugins[plugin.name]
|
del _plugins[plugin.id_]
|
||||||
if parent_plugin := plugin.parent_plugin:
|
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` 文件夹导入的插件,则为文件(夹)名。
|
如果为 `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"]:
|
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]:
|
def get_available_plugin_names() -> set[str]:
|
||||||
"""获取当前所有可用的插件名(包含尚未加载的插件)。"""
|
"""获取当前所有可用的插件标识符(包含尚未加载的插件)。"""
|
||||||
return {*chain.from_iterable(manager.available_plugins for manager in _managers)}
|
return {*chain.from_iterable(manager.available_plugins for manager in _managers)}
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from nonebot.utils import path_to_module_name
|
|||||||
|
|
||||||
from .model import Plugin
|
from .model import Plugin
|
||||||
from .manager import PluginManager
|
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
|
try: # pragma: py-gte-311
|
||||||
import tomllib # pyright: ignore[reportMissingImports]
|
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]:
|
def _find_manager_by_name(name: str) -> Optional[PluginManager]:
|
||||||
for manager in reversed(_managers):
|
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
|
return manager
|
||||||
|
|
||||||
|
|
||||||
def require(name: str) -> ModuleType:
|
def require(name: str) -> ModuleType:
|
||||||
"""获取一个插件的导出内容。
|
"""声明依赖插件。
|
||||||
|
|
||||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
name: 插件名,即 {ref}`nonebot.plugin.model.Plugin.name`。
|
name: 插件模块名或插件标识符,仅在已声明插件的情况下可使用标识符。
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
RuntimeError: 插件无法加载
|
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 plugin not loaded
|
||||||
if not plugin:
|
if plugin is None:
|
||||||
# plugin already declared
|
# plugin already declared, module name / plugin id
|
||||||
if manager := _find_manager_by_name(name):
|
if manager := _find_manager_by_name(name):
|
||||||
plugin = manager.load_plugin(name)
|
plugin = manager.load_plugin(name)
|
||||||
|
|
||||||
# plugin not declared, try to declare and load it
|
# plugin not declared, try to declare and load it
|
||||||
else:
|
else:
|
||||||
# clear current plugin chain, ensure plugin loaded in a new context
|
|
||||||
_t = _current_plugin_chain.set(())
|
|
||||||
try:
|
|
||||||
plugin = load_plugin(name)
|
plugin = load_plugin(name)
|
||||||
finally:
|
|
||||||
_current_plugin_chain.reset(_t)
|
if plugin is None:
|
||||||
if not plugin:
|
|
||||||
raise RuntimeError(f'Cannot load plugin "{name}"!')
|
raise RuntimeError(f'Cannot load plugin "{name}"!')
|
||||||
return plugin.module
|
return plugin.module
|
||||||
|
|
||||||
@ -200,9 +204,11 @@ def inherit_supported_adapters(*names: str) -> Optional[set[str]]:
|
|||||||
final_supported: Optional[set[str]] = None
|
final_supported: Optional[set[str]] = None
|
||||||
|
|
||||||
for name in names:
|
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:
|
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
|
meta = plugin.metadata
|
||||||
if meta is None:
|
if meta is None:
|
||||||
raise ValueError(f'Plugin "{name}" has no metadata!')
|
raise ValueError(f'Plugin "{name}" has no metadata!')
|
||||||
|
@ -26,8 +26,8 @@ from . import (
|
|||||||
_managers,
|
_managers,
|
||||||
_new_plugin,
|
_new_plugin,
|
||||||
_revert_plugin,
|
_revert_plugin,
|
||||||
_current_plugin_chain,
|
_current_plugin,
|
||||||
_module_name_to_plugin_name,
|
_module_name_to_plugin_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ class PluginManager:
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
plugins: 独立插件模块名集合。
|
plugins: 独立插件模块名集合。
|
||||||
search_path: 插件搜索路径(文件夹)。
|
search_path: 插件搜索路径(文件夹),相对于当前工作目录。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -49,29 +49,38 @@ class PluginManager:
|
|||||||
self.search_path: set[str] = set(search_path or [])
|
self.search_path: set[str] = set(search_path or [])
|
||||||
|
|
||||||
# cache plugins
|
# cache plugins
|
||||||
self._third_party_plugin_names: dict[str, str] = {}
|
self._third_party_plugin_ids: dict[str, str] = {}
|
||||||
self._searched_plugin_names: dict[str, Path] = {}
|
self._searched_plugin_ids: dict[str, str] = {}
|
||||||
self.prepare_plugins()
|
self._prepare_plugins()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"PluginManager(plugins={self.plugins}, search_path={self.search_path})"
|
return f"PluginManager(available_plugins={self.controlled_modules})"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def third_party_plugins(self) -> set[str]:
|
def third_party_plugins(self) -> set[str]:
|
||||||
"""返回所有独立插件名称。"""
|
"""返回所有独立插件标识符。"""
|
||||||
return set(self._third_party_plugin_names.keys())
|
return set(self._third_party_plugin_ids.keys())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def searched_plugins(self) -> set[str]:
|
def searched_plugins(self) -> set[str]:
|
||||||
"""返回已搜索到的插件名称。"""
|
"""返回已搜索到的插件标识符。"""
|
||||||
return set(self._searched_plugin_names.keys())
|
return set(self._searched_plugin_ids.keys())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available_plugins(self) -> set[str]:
|
def available_plugins(self) -> set[str]:
|
||||||
"""返回当前插件管理器中可用的插件名称。"""
|
"""返回当前插件管理器中可用的插件标识符。"""
|
||||||
return self.third_party_plugins | self.searched_plugins
|
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]
|
_pre_managers: list[PluginManager]
|
||||||
if self in _managers:
|
if self in _managers:
|
||||||
_pre_managers = _managers[: _managers.index(self)]
|
_pre_managers = _managers[: _managers.index(self)]
|
||||||
@ -79,26 +88,35 @@ class PluginManager:
|
|||||||
_pre_managers = _managers[:]
|
_pre_managers = _managers[:]
|
||||||
|
|
||||||
return {
|
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
|
# get all previous ready to load plugins
|
||||||
previous_plugins = self._previous_plugins()
|
previous_plugin_ids = self._previous_controlled_modules()
|
||||||
searched_plugins: dict[str, Path] = {}
|
|
||||||
third_party_plugins: dict[str, str] = {}
|
# 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
|
# check third party plugins
|
||||||
for plugin in self.plugins:
|
for plugin in self.plugins:
|
||||||
name = _module_name_to_plugin_name(plugin)
|
plugin_id = _module_name_to_plugin_id(plugin, get_controlled_modules())
|
||||||
if name in third_party_plugins or name in previous_plugins:
|
if (
|
||||||
|
plugin_id in self._third_party_plugin_ids
|
||||||
|
or plugin_id in previous_plugin_ids
|
||||||
|
):
|
||||||
raise RuntimeError(
|
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_ids[plugin_id] = plugin
|
||||||
|
|
||||||
self._third_party_plugin_names = third_party_plugins
|
|
||||||
|
|
||||||
# check plugins in search path
|
# check plugins in search path
|
||||||
for module_info in pkgutil.iter_modules(self.search_path):
|
for module_info in pkgutil.iter_modules(self.search_path):
|
||||||
@ -106,47 +124,55 @@ class PluginManager:
|
|||||||
if module_info.name.startswith("_"):
|
if module_info.name.startswith("_"):
|
||||||
continue
|
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 (
|
if not (
|
||||||
module_spec := module_info.module_finder.find_spec(
|
module_spec := module_info.module_finder.find_spec(
|
||||||
module_info.name, None
|
module_info.name, None
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
continue
|
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
|
return self.available_plugins
|
||||||
|
|
||||||
def load_plugin(self, name: str) -> Optional[Plugin]:
|
def load_plugin(self, name: str) -> Optional[Plugin]:
|
||||||
"""加载指定插件。
|
"""加载指定插件。
|
||||||
|
|
||||||
对于独立插件,可以使用完整插件模块名或者插件名称。
|
可以使用完整插件模块名或者插件标识符加载。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
name: 插件名称。
|
name: 插件名称或插件标识符。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
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)
|
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:
|
else:
|
||||||
raise RuntimeError(f"Plugin not found: {name}! Check your plugin name")
|
raise RuntimeError(f"Plugin not found: {name}! Check your plugin name")
|
||||||
|
|
||||||
@ -155,13 +181,13 @@ class PluginManager:
|
|||||||
) is None or not isinstance(plugin, Plugin):
|
) is None or not isinstance(plugin, Plugin):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Module {module.__name__} is not loaded as a plugin! "
|
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(
|
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>"'
|
f' from "<m>{escape_tag(plugin.module_name)}</m>"'
|
||||||
if plugin.module_name != plugin.name
|
if plugin.module_name != plugin.id_
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -193,21 +219,16 @@ class PluginFinder(MetaPathFinder):
|
|||||||
module_origin = module_spec.origin
|
module_origin = module_spec.origin
|
||||||
if not module_origin:
|
if not module_origin:
|
||||||
return
|
return
|
||||||
module_path = Path(module_origin).resolve()
|
|
||||||
|
|
||||||
for manager in reversed(_managers):
|
for manager in reversed(_managers):
|
||||||
# use path instead of name in case of submodule name conflict
|
if fullname in manager.controlled_modules.values():
|
||||||
if (
|
|
||||||
fullname in manager.plugins
|
|
||||||
or module_path in manager._searched_plugin_names.values()
|
|
||||||
):
|
|
||||||
module_spec.loader = PluginLoader(manager, fullname, module_origin)
|
module_spec.loader = PluginLoader(manager, fullname, module_origin)
|
||||||
return module_spec
|
return module_spec
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class PluginLoader(SourceFileLoader):
|
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.manager = manager
|
||||||
self.loaded = False
|
self.loaded = False
|
||||||
super().__init__(fullname, path)
|
super().__init__(fullname, path)
|
||||||
@ -227,17 +248,8 @@ class PluginLoader(SourceFileLoader):
|
|||||||
plugin = _new_plugin(self.name, module, self.manager)
|
plugin = _new_plugin(self.name, module, self.manager)
|
||||||
setattr(module, "__plugin__", plugin)
|
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
|
# enter plugin context
|
||||||
_plugin_token = _current_plugin_chain.set((*parent_plugins, plugin))
|
_plugin_token = _current_plugin.set(plugin)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
super().exec_module(module)
|
super().exec_module(module)
|
||||||
@ -246,7 +258,7 @@ class PluginLoader(SourceFileLoader):
|
|||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
# leave plugin context
|
# leave plugin context
|
||||||
_current_plugin_chain.reset(_plugin_token)
|
_current_plugin.reset(_plugin_token)
|
||||||
|
|
||||||
# get plugin metadata
|
# get plugin metadata
|
||||||
metadata: Optional[PluginMetadata] = getattr(module, "__plugin_meta__", None)
|
metadata: Optional[PluginMetadata] = getattr(module, "__plugin_meta__", None)
|
||||||
|
@ -66,7 +66,7 @@ class Plugin:
|
|||||||
"""存储插件信息"""
|
"""存储插件信息"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
"""插件索引标识,NoneBot 使用 文件/文件夹 名称作为标识符"""
|
"""插件名称,NoneBot 使用 文件/文件夹 名称作为插件名称"""
|
||||||
module: ModuleType
|
module: ModuleType
|
||||||
"""插件模块对象"""
|
"""插件模块对象"""
|
||||||
module_name: str
|
module_name: str
|
||||||
@ -80,3 +80,10 @@ class Plugin:
|
|||||||
sub_plugins: set["Plugin"] = field(default_factory=set)
|
sub_plugins: set["Plugin"] = field(default_factory=set)
|
||||||
"""子插件集合"""
|
"""子插件集合"""
|
||||||
metadata: Optional[PluginMetadata] = None
|
metadata: Optional[PluginMetadata] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id_(self) -> str:
|
||||||
|
"""插件索引标识"""
|
||||||
|
return (
|
||||||
|
f"{self.parent_plugin.id_}:{self.name}" if self.parent_plugin else self.name
|
||||||
|
)
|
||||||
|
@ -31,8 +31,8 @@ from nonebot.rule import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .model import Plugin
|
from .model import Plugin
|
||||||
|
from .manager import _current_plugin
|
||||||
from . import get_plugin_by_module_name
|
from . import get_plugin_by_module_name
|
||||||
from .manager import _current_plugin_chain
|
|
||||||
|
|
||||||
|
|
||||||
def store_matcher(matcher: type[Matcher]) -> None:
|
def store_matcher(matcher: type[Matcher]) -> None:
|
||||||
@ -42,8 +42,8 @@ def store_matcher(matcher: type[Matcher]) -> None:
|
|||||||
matcher: 事件响应器
|
matcher: 事件响应器
|
||||||
"""
|
"""
|
||||||
# only store the matcher defined when plugin loading
|
# only store the matcher defined when plugin loading
|
||||||
if plugin_chain := _current_plugin_chain.get():
|
if plugin := _current_plugin.get():
|
||||||
plugin_chain[-1].matcher.add(matcher)
|
plugin.matcher.add(matcher)
|
||||||
|
|
||||||
|
|
||||||
def get_matcher_plugin(depth: int = 1) -> Optional[Plugin]: # pragma: no cover
|
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__
|
module_name = (module := inspect.getmodule(frame)) and module.__name__
|
||||||
|
|
||||||
plugin: Optional["Plugin"] = None
|
|
||||||
# matcher defined when plugin loading
|
# matcher defined when plugin loading
|
||||||
if plugin_chain := _current_plugin_chain.get():
|
plugin: Optional["Plugin"] = _current_plugin.get()
|
||||||
plugin = plugin_chain[-1]
|
|
||||||
# matcher defined when plugin running
|
# matcher defined when plugin running
|
||||||
elif module_name:
|
if plugin is None and module_name:
|
||||||
plugin = get_plugin_by_module_name(module_name)
|
plugin = get_plugin_by_module_name(module_name)
|
||||||
|
|
||||||
return MatcherSource(
|
return MatcherSource(
|
||||||
plugin_name=plugin and plugin.name,
|
plugin_id=plugin and plugin.id_,
|
||||||
module_name=module_name,
|
module_name=module_name,
|
||||||
lineno=frame.f_lineno,
|
lineno=frame.f_lineno,
|
||||||
)
|
)
|
||||||
|
0
tests/dynamic/require_not_loaded/subplugin1.py
Normal file
0
tests/dynamic/require_not_loaded/subplugin1.py
Normal file
0
tests/dynamic/require_not_loaded/subplugin2.py
Normal file
0
tests/dynamic/require_not_loaded/subplugin2.py
Normal file
@ -8,5 +8,5 @@ manager = PluginManager(
|
|||||||
_managers.append(manager)
|
_managers.append(manager)
|
||||||
|
|
||||||
# test load nested plugin with require
|
# test load nested plugin with require
|
||||||
manager.load_plugin("nested_subplugin")
|
manager.load_plugin("plugins.nested.plugins.nested_subplugin")
|
||||||
manager.load_plugin("nested_subplugin2")
|
manager.load_plugin("nested:nested_subplugin2")
|
||||||
|
@ -29,8 +29,10 @@ async def test_matcher_info(app: App):
|
|||||||
assert matcher.module is sys.modules["plugins.matcher.matcher_info"]
|
assert matcher.module is sys.modules["plugins.matcher.matcher_info"]
|
||||||
assert matcher.module_name == "plugins.matcher.matcher_info"
|
assert matcher.module_name == "plugins.matcher.matcher_info"
|
||||||
|
|
||||||
|
assert matcher._source.plugin_id == "matcher:matcher_info"
|
||||||
assert matcher._source.plugin_name == "matcher_info"
|
assert matcher._source.plugin_name == "matcher_info"
|
||||||
assert matcher.plugin is get_plugin("matcher_info")
|
assert matcher.plugin is get_plugin("matcher:matcher_info")
|
||||||
|
assert matcher.plugin_id == "matcher:matcher_info"
|
||||||
assert matcher.plugin_name == "matcher_info"
|
assert matcher.plugin_name == "matcher_info"
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
|
@ -10,18 +10,43 @@ async def test_get_plugin():
|
|||||||
# check simple plugin
|
# check simple plugin
|
||||||
plugin = nonebot.get_plugin("export")
|
plugin = nonebot.get_plugin("export")
|
||||||
assert plugin
|
assert plugin
|
||||||
|
assert plugin.id_ == "export"
|
||||||
|
assert plugin.name == "export"
|
||||||
assert plugin.module_name == "plugins.export"
|
assert plugin.module_name == "plugins.export"
|
||||||
|
|
||||||
# check sub plugin
|
# check sub plugin
|
||||||
plugin = nonebot.get_plugin("nested_subplugin")
|
plugin = nonebot.get_plugin("nested:nested_subplugin")
|
||||||
assert plugin
|
assert plugin
|
||||||
|
assert plugin.id_ == "nested:nested_subplugin"
|
||||||
|
assert plugin.name == "nested_subplugin"
|
||||||
assert plugin.module_name == "plugins.nested.plugins.nested_subplugin"
|
assert plugin.module_name == "plugins.nested.plugins.nested_subplugin"
|
||||||
|
|
||||||
# check get plugin by module name
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_plugin_by_module_name():
|
||||||
|
# check get plugin by exact module name
|
||||||
|
plugin = nonebot.get_plugin_by_module_name("plugins.nested")
|
||||||
|
assert plugin
|
||||||
|
assert plugin.id_ == "nested"
|
||||||
|
assert plugin.name == "nested"
|
||||||
|
assert plugin.module_name == "plugins.nested"
|
||||||
|
|
||||||
|
# check get plugin by sub module name
|
||||||
plugin = nonebot.get_plugin_by_module_name("plugins.nested.utils")
|
plugin = nonebot.get_plugin_by_module_name("plugins.nested.utils")
|
||||||
assert plugin
|
assert plugin
|
||||||
|
assert plugin.id_ == "nested"
|
||||||
|
assert plugin.name == "nested"
|
||||||
assert plugin.module_name == "plugins.nested"
|
assert plugin.module_name == "plugins.nested"
|
||||||
|
|
||||||
|
# check get plugin by sub plugin exact module name
|
||||||
|
plugin = nonebot.get_plugin_by_module_name(
|
||||||
|
"plugins.nested.plugins.nested_subplugin"
|
||||||
|
)
|
||||||
|
assert plugin
|
||||||
|
assert plugin.id_ == "nested:nested_subplugin"
|
||||||
|
assert plugin.name == "nested_subplugin"
|
||||||
|
assert plugin.module_name == "plugins.nested.plugins.nested_subplugin"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_available_plugin():
|
async def test_get_available_plugin():
|
||||||
@ -31,8 +56,8 @@ async def test_get_available_plugin():
|
|||||||
_managers.append(PluginManager(["plugins.export", "plugin.require"]))
|
_managers.append(PluginManager(["plugins.export", "plugin.require"]))
|
||||||
|
|
||||||
# check get available plugins
|
# check get available plugins
|
||||||
plugin_names = nonebot.get_available_plugin_names()
|
plugin_ids = nonebot.get_available_plugin_names()
|
||||||
assert plugin_names == {"export", "require"}
|
assert plugin_ids == {"export", "require"}
|
||||||
finally:
|
finally:
|
||||||
_managers.clear()
|
_managers.clear()
|
||||||
_managers.extend(old_managers)
|
_managers.extend(old_managers)
|
||||||
|
@ -29,12 +29,13 @@ async def test_load_plugins(load_plugin: set[Plugin], load_builtin_plugin: set[P
|
|||||||
|
|
||||||
# check simple plugin
|
# check simple plugin
|
||||||
assert "plugins.export" in sys.modules
|
assert "plugins.export" in sys.modules
|
||||||
|
assert "plugin._hidden" not in sys.modules
|
||||||
|
|
||||||
# check sub plugin
|
# check sub plugin
|
||||||
plugin = nonebot.get_plugin("nested_subplugin")
|
plugin = nonebot.get_plugin("nested:nested_subplugin")
|
||||||
assert plugin
|
assert plugin
|
||||||
assert "plugins.nested.plugins.nested_subplugin" in sys.modules
|
assert "plugins.nested.plugins.nested_subplugin" in sys.modules
|
||||||
assert plugin.parent_plugin == nonebot.get_plugin("nested")
|
assert plugin.parent_plugin is nonebot.get_plugin("nested")
|
||||||
|
|
||||||
# check load again
|
# check load again
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
@ -46,8 +47,8 @@ async def test_load_plugins(load_plugin: set[Plugin], load_builtin_plugin: set[P
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_load_nested_plugin():
|
async def test_load_nested_plugin():
|
||||||
parent_plugin = nonebot.get_plugin("nested")
|
parent_plugin = nonebot.get_plugin("nested")
|
||||||
sub_plugin = nonebot.get_plugin("nested_subplugin")
|
sub_plugin = nonebot.get_plugin("nested:nested_subplugin")
|
||||||
sub_plugin2 = nonebot.get_plugin("nested_subplugin2")
|
sub_plugin2 = nonebot.get_plugin("nested:nested_subplugin2")
|
||||||
assert parent_plugin
|
assert parent_plugin
|
||||||
assert sub_plugin
|
assert sub_plugin
|
||||||
assert sub_plugin2
|
assert sub_plugin2
|
||||||
@ -89,12 +90,16 @@ async def test_require_loaded(monkeypatch: pytest.MonkeyPatch):
|
|||||||
|
|
||||||
monkeypatch.setattr("nonebot.plugin.load._find_manager_by_name", _patched_find)
|
monkeypatch.setattr("nonebot.plugin.load._find_manager_by_name", _patched_find)
|
||||||
|
|
||||||
|
# require use module name
|
||||||
nonebot.require("plugins.export")
|
nonebot.require("plugins.export")
|
||||||
|
# require use plugin id
|
||||||
|
nonebot.require("export")
|
||||||
|
nonebot.require("nested:nested_subplugin")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch):
|
async def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch):
|
||||||
m = PluginManager(["dynamic.require_not_loaded"])
|
m = PluginManager(["dynamic.require_not_loaded"], ["dynamic/require_not_loaded/"])
|
||||||
_managers.append(m)
|
_managers.append(m)
|
||||||
num_managers = len(_managers)
|
num_managers = len(_managers)
|
||||||
|
|
||||||
@ -106,7 +111,11 @@ async def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch):
|
|||||||
|
|
||||||
monkeypatch.setattr(PluginManager, "load_plugin", _patched_load)
|
monkeypatch.setattr(PluginManager, "load_plugin", _patched_load)
|
||||||
|
|
||||||
|
# require standalone plugin
|
||||||
nonebot.require("dynamic.require_not_loaded")
|
nonebot.require("dynamic.require_not_loaded")
|
||||||
|
# require searched plugin
|
||||||
|
nonebot.require("dynamic.require_not_loaded.subplugin1")
|
||||||
|
nonebot.require("require_not_loaded:subplugin2")
|
||||||
|
|
||||||
assert len(_managers) == num_managers
|
assert len(_managers) == num_managers
|
||||||
|
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nonebot.plugin import PluginManager
|
from nonebot.plugin import PluginManager, _managers
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_load_plugin_name():
|
async def test_load_plugin_name():
|
||||||
m = PluginManager(plugins=["dynamic.manager"])
|
m = PluginManager(plugins=["dynamic.manager"])
|
||||||
|
_managers.append(m)
|
||||||
|
|
||||||
|
# load by plugin id
|
||||||
module1 = m.load_plugin("manager")
|
module1 = m.load_plugin("manager")
|
||||||
|
# load by module name
|
||||||
module2 = m.load_plugin("dynamic.manager")
|
module2 = m.load_plugin("dynamic.manager")
|
||||||
|
assert module1
|
||||||
|
assert module2
|
||||||
assert module1 is module2
|
assert module1 is module2
|
||||||
|
@ -145,6 +145,7 @@ async def test_on(
|
|||||||
assert matcher.plugin is plugin
|
assert matcher.plugin is plugin
|
||||||
assert matcher in plugin.matcher
|
assert matcher in plugin.matcher
|
||||||
assert matcher.module is module
|
assert matcher.module is module
|
||||||
|
assert matcher.plugin_id == "plugin"
|
||||||
assert matcher.plugin_name == "plugin"
|
assert matcher.plugin_name == "plugin"
|
||||||
assert matcher.module_name == "plugins.plugin.matchers"
|
assert matcher.module_name == "plugins.plugin.matchers"
|
||||||
|
|
||||||
@ -163,6 +164,7 @@ async def test_runtime_on():
|
|||||||
assert matcher.plugin is plugin
|
assert matcher.plugin is plugin
|
||||||
assert matcher not in plugin.matcher
|
assert matcher not in plugin.matcher
|
||||||
assert matcher.module is module
|
assert matcher.module is module
|
||||||
|
assert matcher.plugin_id == "plugin"
|
||||||
assert matcher.plugin_name == "plugin"
|
assert matcher.plugin_name == "plugin"
|
||||||
assert matcher.module_name == "plugins.plugin.matchers"
|
assert matcher.module_name == "plugins.plugin.matchers"
|
||||||
finally:
|
finally:
|
||||||
|
Loading…
Reference in New Issue
Block a user