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

This commit is contained in:
Ju4tCode 2024-04-20 14:47:12 +08:00 committed by GitHub
parent e15d544341
commit 6bf10aafb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 262 additions and 131 deletions

View File

@ -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])
) )

View File

@ -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]:
"""事件响应器所在插件模块路径""" """事件响应器所在插件模块路径"""

View File

@ -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)}

View File

@ -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!')

View File

@ -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)

View File

@ -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
)

View File

@ -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,
) )

View 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")

View File

@ -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 (

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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: