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: