From 6bf10aafb7e1efc5beea80449b76ed118de601b9 Mon Sep 17 00:00:00 2001
From: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
Date: Sat, 20 Apr 2024 14:47:12 +0800
Subject: [PATCH] =?UTF-8?q?:sparkles:=20Feature:=20=E5=B5=8C=E5=A5=97?=
=?UTF-8?q?=E6=8F=92=E4=BB=B6=E5=90=8D=E7=A7=B0=E4=BD=9C=E7=94=A8=E5=9F=9F?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=20(#2665)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
nonebot/__init__.py | 5 +-
nonebot/internal/matcher/matcher.py | 26 +++-
nonebot/plugin/__init__.py | 81 ++++++++--
nonebot/plugin/load.py | 42 ++---
nonebot/plugin/manager.py | 146 ++++++++++--------
nonebot/plugin/model.py | 9 +-
nonebot/plugin/on.py | 14 +-
.../__init__.py} | 0
.../dynamic/require_not_loaded/subplugin1.py | 0
.../dynamic/require_not_loaded/subplugin2.py | 0
tests/plugins/nested/__init__.py | 4 +-
tests/test_matcher/test_matcher.py | 4 +-
tests/test_plugin/test_get.py | 33 +++-
tests/test_plugin/test_load.py | 19 ++-
tests/test_plugin/test_manager.py | 8 +-
tests/test_plugin/test_on.py | 2 +
16 files changed, 262 insertions(+), 131 deletions(-)
rename tests/dynamic/{require_not_loaded.py => require_not_loaded/__init__.py} (100%)
create mode 100644 tests/dynamic/require_not_loaded/subplugin1.py
create mode 100644 tests/dynamic/require_not_loaded/subplugin2.py
diff --git a/nonebot/__init__.py b/nonebot/__init__.py
index de532a22..e413403e 100644
--- a/nonebot/__init__.py
+++ b/nonebot/__init__.py
@@ -266,11 +266,12 @@ def _resolve_combine_expr(obj_str: str) -> type[Driver]:
def _log_patcher(record: "loguru.Record"):
+ """使用插件标识优化日志展示"""
record["name"] = (
- plugin.name
+ plugin.id_
if (module_name := record["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])
)
diff --git a/nonebot/internal/matcher/matcher.py b/nonebot/internal/matcher/matcher.py
index 63791cac..e188d8e8 100644
--- a/nonebot/internal/matcher/matcher.py
+++ b/nonebot/internal/matcher/matcher.py
@@ -83,8 +83,8 @@ current_handler: ContextVar[Dependent] = ContextVar("current_handler")
class MatcherSource:
"""Matcher 源代码上下文信息"""
- plugin_name: Optional[str] = None
- """事件响应器所在插件名称"""
+ plugin_id: Optional[str] = None
+ """事件响应器所在插件标识符"""
module_name: Optional[str] = None
"""事件响应器所在插件模块的路径名"""
lineno: Optional[int] = None
@@ -95,8 +95,13 @@ class MatcherSource:
"""事件响应器所在插件"""
from nonebot.plugin import get_plugin
- if self.plugin_name is not None:
- return get_plugin(self.plugin_name)
+ if self.plugin_id is not None:
+ return get_plugin(self.plugin_id)
+
+ @property
+ def plugin_name(self) -> Optional[str]:
+ """事件响应器所在插件名"""
+ return self.plugin and self.plugin.name
@property
def module(self) -> Optional[ModuleType]:
@@ -245,7 +250,7 @@ class Matcher(metaclass=MatcherMeta):
)
source = source or (
MatcherSource(
- plugin_name=plugin and plugin.name,
+ plugin_id=plugin and plugin.id_,
module_name=module and module.__name__,
)
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
@classproperty
- def module(cls) -> Optional[ModuleType]:
- """事件响应器所在插件模块"""
- return cls._source and cls._source.module
+ def plugin_id(cls) -> Optional[str]:
+ """事件响应器所在插件标识符"""
+ return cls._source and cls._source.plugin_id
@classproperty
def plugin_name(cls) -> Optional[str]:
"""事件响应器所在插件名"""
return cls._source and cls._source.plugin_name
+ @classproperty
+ def module(cls) -> Optional[ModuleType]:
+ """事件响应器所在插件模块"""
+ return cls._source and cls._source.module
+
@classproperty
def module_name(cls) -> Optional[str]:
"""事件响应器所在插件模块路径"""
diff --git a/nonebot/plugin/__init__.py b/nonebot/plugin/__init__.py
index 20b0dcd1..81913529 100644
--- a/nonebot/plugin/__init__.py
+++ b/nonebot/plugin/__init__.py
@@ -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)}
diff --git a/nonebot/plugin/load.py b/nonebot/plugin/load.py
index e62b10b6..66f28d0d 100644
--- a/nonebot/plugin/load.py
+++ b/nonebot/plugin/load.py
@@ -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!')
diff --git a/nonebot/plugin/manager.py b/nonebot/plugin/manager.py
index db3461af..4f613c7c 100644
--- a/nonebot/plugin/manager.py
+++ b/nonebot/plugin/manager.py
@@ -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 "{escape_tag(plugin.name)}"'
+ f'Succeeded to load plugin "{escape_tag(plugin.id_)}"'
+ (
f' from "{escape_tag(plugin.module_name)}"'
- 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)
diff --git a/nonebot/plugin/model.py b/nonebot/plugin/model.py
index 9d8c01d9..3b6d8a01 100644
--- a/nonebot/plugin/model.py
+++ b/nonebot/plugin/model.py
@@ -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
+ )
diff --git a/nonebot/plugin/on.py b/nonebot/plugin/on.py
index 7c0934c5..5d1774d8 100644
--- a/nonebot/plugin/on.py
+++ b/nonebot/plugin/on.py
@@ -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,
)
diff --git a/tests/dynamic/require_not_loaded.py b/tests/dynamic/require_not_loaded/__init__.py
similarity index 100%
rename from tests/dynamic/require_not_loaded.py
rename to tests/dynamic/require_not_loaded/__init__.py
diff --git a/tests/dynamic/require_not_loaded/subplugin1.py b/tests/dynamic/require_not_loaded/subplugin1.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/dynamic/require_not_loaded/subplugin2.py b/tests/dynamic/require_not_loaded/subplugin2.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/plugins/nested/__init__.py b/tests/plugins/nested/__init__.py
index a62dbd0a..67c885b7 100644
--- a/tests/plugins/nested/__init__.py
+++ b/tests/plugins/nested/__init__.py
@@ -8,5 +8,5 @@ manager = PluginManager(
_managers.append(manager)
# test load nested plugin with require
-manager.load_plugin("nested_subplugin")
-manager.load_plugin("nested_subplugin2")
+manager.load_plugin("plugins.nested.plugins.nested_subplugin")
+manager.load_plugin("nested:nested_subplugin2")
diff --git a/tests/test_matcher/test_matcher.py b/tests/test_matcher/test_matcher.py
index 36bc36d0..5f4f13ff 100644
--- a/tests/test_matcher/test_matcher.py
+++ b/tests/test_matcher/test_matcher.py
@@ -29,8 +29,10 @@ async def test_matcher_info(app: App):
assert matcher.module is sys.modules["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.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 (
diff --git a/tests/test_plugin/test_get.py b/tests/test_plugin/test_get.py
index 629f7f8d..7d614e02 100644
--- a/tests/test_plugin/test_get.py
+++ b/tests/test_plugin/test_get.py
@@ -10,18 +10,43 @@ async def test_get_plugin():
# check simple plugin
plugin = nonebot.get_plugin("export")
assert plugin
+ assert plugin.id_ == "export"
+ assert plugin.name == "export"
assert plugin.module_name == "plugins.export"
# check sub plugin
- plugin = nonebot.get_plugin("nested_subplugin")
+ plugin = nonebot.get_plugin("nested:nested_subplugin")
assert plugin
+ assert plugin.id_ == "nested:nested_subplugin"
+ assert plugin.name == "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")
assert plugin
+ assert plugin.id_ == "nested"
+ assert plugin.name == "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
async def test_get_available_plugin():
@@ -31,8 +56,8 @@ async def test_get_available_plugin():
_managers.append(PluginManager(["plugins.export", "plugin.require"]))
# check get available plugins
- plugin_names = nonebot.get_available_plugin_names()
- assert plugin_names == {"export", "require"}
+ plugin_ids = nonebot.get_available_plugin_names()
+ assert plugin_ids == {"export", "require"}
finally:
_managers.clear()
_managers.extend(old_managers)
diff --git a/tests/test_plugin/test_load.py b/tests/test_plugin/test_load.py
index 16160eb9..b102c913 100644
--- a/tests/test_plugin/test_load.py
+++ b/tests/test_plugin/test_load.py
@@ -29,12 +29,13 @@ async def test_load_plugins(load_plugin: set[Plugin], load_builtin_plugin: set[P
# check simple plugin
assert "plugins.export" in sys.modules
+ assert "plugin._hidden" not in sys.modules
# check sub plugin
- plugin = nonebot.get_plugin("nested_subplugin")
+ plugin = nonebot.get_plugin("nested:nested_subplugin")
assert plugin
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
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
async def test_load_nested_plugin():
parent_plugin = nonebot.get_plugin("nested")
- sub_plugin = nonebot.get_plugin("nested_subplugin")
- sub_plugin2 = nonebot.get_plugin("nested_subplugin2")
+ sub_plugin = nonebot.get_plugin("nested:nested_subplugin")
+ sub_plugin2 = nonebot.get_plugin("nested:nested_subplugin2")
assert parent_plugin
assert sub_plugin
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)
+ # require use module name
nonebot.require("plugins.export")
+ # require use plugin id
+ nonebot.require("export")
+ nonebot.require("nested:nested_subplugin")
@pytest.mark.asyncio
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)
num_managers = len(_managers)
@@ -106,7 +111,11 @@ async def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(PluginManager, "load_plugin", _patched_load)
+ # require standalone plugin
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
diff --git a/tests/test_plugin/test_manager.py b/tests/test_plugin/test_manager.py
index 22dce1e1..7b9799f9 100644
--- a/tests/test_plugin/test_manager.py
+++ b/tests/test_plugin/test_manager.py
@@ -1,11 +1,17 @@
import pytest
-from nonebot.plugin import PluginManager
+from nonebot.plugin import PluginManager, _managers
@pytest.mark.asyncio
async def test_load_plugin_name():
m = PluginManager(plugins=["dynamic.manager"])
+ _managers.append(m)
+
+ # load by plugin id
module1 = m.load_plugin("manager")
+ # load by module name
module2 = m.load_plugin("dynamic.manager")
+ assert module1
+ assert module2
assert module1 is module2
diff --git a/tests/test_plugin/test_on.py b/tests/test_plugin/test_on.py
index 9aabbb14..7137c326 100644
--- a/tests/test_plugin/test_on.py
+++ b/tests/test_plugin/test_on.py
@@ -145,6 +145,7 @@ async def test_on(
assert matcher.plugin is plugin
assert matcher in plugin.matcher
assert matcher.module is module
+ assert matcher.plugin_id == "plugin"
assert matcher.plugin_name == "plugin"
assert matcher.module_name == "plugins.plugin.matchers"
@@ -163,6 +164,7 @@ async def test_runtime_on():
assert matcher.plugin is plugin
assert matcher not in plugin.matcher
assert matcher.module is module
+ assert matcher.plugin_id == "plugin"
assert matcher.plugin_name == "plugin"
assert matcher.module_name == "plugins.plugin.matchers"
finally: