diff --git a/nonebot/plugin.py b/nonebot/plugin/__init__.py
similarity index 97%
rename from nonebot/plugin.py
rename to nonebot/plugin/__init__.py
index e66672cd..540eac89 100644
--- a/nonebot/plugin.py
+++ b/nonebot/plugin/__init__.py
@@ -4,7 +4,6 @@
为 NoneBot 插件开发提供便携的定义函数。
"""
-
import re
import sys
import pkgutil
@@ -21,14 +20,17 @@ from nonebot.permission import Permission
from nonebot.typing import T_State, T_StateFactory, T_Handler, T_RuleChecker
from nonebot.rule import Rule, startswith, endswith, keyword, command, shell_command, ArgumentParser, regex
+from .manager import PluginManager
+
if TYPE_CHECKING:
- from nonebot.adapters import Bot, Event, MessageSegment
+ from nonebot.adapters import Bot, Event
plugins: Dict[str, "Plugin"] = {}
"""
:类型: ``Dict[str, Plugin]``
:说明: 已加载的插件
"""
+PLUGIN_NAMESPACE = "nonebot.loaded_plugins"
_export: ContextVar["Export"] = ContextVar("_export")
_tmp_matchers: ContextVar[Set[Type[Matcher]]] = ContextVar("_tmp_matchers")
@@ -950,7 +952,7 @@ def load_plugin(module_path: str) -> Optional[Plugin]:
"""
:说明:
- 使用 ``importlib`` 加载单个插件,可以是本地插件或是通过 ``pip`` 安装的插件。
+ 使用 ``PluginManager`` 加载单个插件,可以是本地插件或是通过 ``pip`` 安装的插件。
:参数:
@@ -1006,42 +1008,38 @@ def load_plugins(*plugin_dir: str) -> Set[Plugin]:
- ``Set[Plugin]``
"""
- def _load_plugin(module_info) -> Optional[Plugin]:
- _tmp_matchers.set(set())
- _export.set(Export())
- name = module_info.name
- if name.startswith("_"):
+ def _load_plugin(plugin_name: str) -> Optional[Plugin]:
+ if plugin_name.startswith("_"):
return None
- spec = module_info.module_finder.find_spec(name, None)
- if not spec:
- logger.warning(
- f"Module {name} cannot be loaded! Check module name first.")
- elif spec.name in plugins:
- return None
- elif spec.name in sys.modules:
- logger.warning(
- f"Module {spec.name} has been loaded by other plugin! Ignored")
+ _tmp_matchers.set(set())
+ _export.set(Export())
+
+ if plugin_name in plugins:
return None
try:
- module = _load(spec)
+ module = manager.load_plugin(plugin_name)
for m in _tmp_matchers.get():
- m.module = name
- plugin = Plugin(name, module, _tmp_matchers.get(), _export.get())
- plugins[name] = plugin
- logger.opt(colors=True).info(f'Succeeded to import "{name}"')
+ m.module = plugin_name
+ plugin = Plugin(plugin_name, module, _tmp_matchers.get(),
+ _export.get())
+ plugins[plugin_name] = plugin
+ logger.opt(
+ colors=True).info(f'Succeeded to import "{plugin_name}"')
return plugin
except Exception as e:
logger.opt(colors=True, exception=e).error(
- f'Failed to import "{name}"')
+ f'Failed to import "{plugin_name}"'
+ )
return None
loaded_plugins = set()
- for module_info in pkgutil.iter_modules(plugin_dir):
+ manager = PluginManager(PLUGIN_NAMESPACE, search_path=plugin_dir)
+ for plugin_name in manager.list_plugins():
context: Context = copy_context()
- result = context.run(_load_plugin, module_info)
+ result = context.run(_load_plugin, plugin_name)
if result:
loaded_plugins.add(result)
return loaded_plugins
diff --git a/nonebot/plugin.pyi b/nonebot/plugin/__init__.pyi
similarity index 100%
rename from nonebot/plugin.pyi
rename to nonebot/plugin/__init__.pyi
diff --git a/nonebot/plugin/manager.py b/nonebot/plugin/manager.py
new file mode 100644
index 00000000..7bdf0910
--- /dev/null
+++ b/nonebot/plugin/manager.py
@@ -0,0 +1,177 @@
+import sys
+import uuid
+import pkgutil
+import importlib
+from hashlib import md5
+from types import ModuleType
+from collections import Counter
+from importlib.abc import MetaPathFinder
+from importlib.machinery import PathFinder
+from typing import Set, List, Optional, Iterable
+
+_internal_space = ModuleType(__name__ + "._internal")
+_internal_space.__path__ = [] # type: ignore
+sys.modules[_internal_space.__name__] = _internal_space
+
+_manager_stack: List["PluginManager"] = []
+
+
+class _NamespaceModule(ModuleType):
+ """Simple namespace module to store plugins."""
+
+ @property
+ def __path__(self):
+ return []
+
+ def __getattr__(self, name: str):
+ try:
+ return super().__getattr__(name) # type: ignore
+ except AttributeError:
+ if name.startswith("__"):
+ raise
+ raise RuntimeError("Plugin manager not activated!")
+
+
+class _InternalModule(ModuleType):
+ """Internal module for each plugin manager."""
+
+ def __init__(self, plugin_manager: "PluginManager"):
+ super().__init__(
+ f"{_internal_space.__name__}.{plugin_manager.internal_id}")
+ self.__plugin_manager__ = plugin_manager
+
+ @property
+ def __path__(self) -> List[str]:
+ return list(self.__plugin_manager__.search_path)
+
+
+class PluginManager:
+
+ def __init__(self,
+ namespace: Optional[str] = None,
+ plugins: Optional[Iterable[str]] = None,
+ search_path: Optional[Iterable[str]] = None,
+ *,
+ id: Optional[str] = None):
+ self.namespace: Optional[str] = namespace
+ self.namespace_module: Optional[ModuleType] = self._setup_namespace(
+ namespace)
+
+ self.id: str = id or str(uuid.uuid4())
+ self.internal_id: str = md5(
+ ((self.namespace or "") + self.id).encode()).hexdigest()
+ self.internal_module = self._setup_internal_module(self.internal_id)
+
+ # simple plugin not in search path
+ self.plugins: Set[str] = set(plugins or [])
+ self.search_path: Set[str] = set(search_path or [])
+ # ensure can be loaded
+ self.list_plugins()
+
+ def _setup_namespace(self,
+ namespace: Optional[str] = None
+ ) -> Optional[ModuleType]:
+ if not namespace:
+ return None
+
+ try:
+ module = importlib.import_module(namespace)
+ except ImportError:
+ module = _NamespaceModule(namespace)
+ if "." in namespace:
+ parent = importlib.import_module(namespace.rsplit(".", 1)[0])
+ setattr(parent, namespace.rsplit(".", 1)[1], module)
+
+ sys.modules[namespace] = module
+ return module
+
+ def _setup_internal_module(self, internal_id: str) -> ModuleType:
+ if hasattr(_internal_space, internal_id):
+ raise RuntimeError("Plugin manager already exists!")
+ module = _InternalModule(self)
+ sys.modules[module.__name__] = module
+ setattr(_internal_space, internal_id, module)
+ return module
+
+ def __enter__(self):
+ if self in _manager_stack:
+ raise RuntimeError("Plugin manager already activated!")
+ _manager_stack.append(self)
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ try:
+ _manager_stack.pop()
+ except IndexError:
+ pass
+
+ def search_plugins(self) -> List[str]:
+ return [
+ module_info.name
+ for module_info in pkgutil.iter_modules(self.search_path)
+ ]
+
+ def list_plugins(self) -> Set[str]:
+ _pre_managers: List[PluginManager]
+ if self in _manager_stack:
+ _pre_managers = _manager_stack[:_manager_stack.index(self)]
+ else:
+ _pre_managers = _manager_stack[:]
+
+ _search_path: Set[str] = set()
+ for manager in _pre_managers:
+ _search_path |= manager.search_path
+ if _search_path & self.search_path:
+ raise RuntimeError("Duplicate plugin search path!")
+
+ _search_plugins = self.search_plugins()
+ c = Counter([*_search_plugins, *self.plugins])
+ conflict = [name for name, num in c.items() if num > 1]
+ if conflict:
+ raise RuntimeError(
+ f"More than one plugin named {' / '.join(conflict)}!")
+ return set(_search_plugins) | self.plugins
+
+ def load_plugin(self, name) -> ModuleType:
+ if name in self.plugins:
+ return importlib.import_module(name)
+
+ if "." in name:
+ raise ValueError("Plugin name cannot contain '.'")
+ with self:
+ return importlib.import_module(f"{self.namespace}.{name}")
+
+ def load_all_plugins(self) -> List[ModuleType]:
+ return [self.load_plugin(name) for name in self.list_plugins()]
+
+ def _rewrite_module_name(self, module_name) -> Optional[str]:
+ if module_name == self.namespace:
+ return self.internal_module.__name__
+ elif module_name.startswith(self.namespace + "."):
+ path = module_name.split(".")
+ length = self.namespace.count(".") + 1
+ return f"{self.internal_module.__name__}.{'.'.join(path[length:])}"
+ elif module_name in self.search_plugins():
+ return f"{self.internal_module.__name__}.{module_name}"
+ return None
+
+
+class PluginFinder(MetaPathFinder):
+
+ def find_spec(self, fullname: str, path, target):
+ if _manager_stack:
+ index = -1
+ while -index <= len(_manager_stack):
+ manager = _manager_stack[index]
+ newname = manager._rewrite_module_name(fullname)
+ if newname:
+ spec = PathFinder.find_spec(newname,
+ list(manager.search_path),
+ target)
+ if spec:
+ return spec
+ index -= 1
+ return None
+
+
+sys.meta_path.insert(0, PluginFinder())
diff --git a/tests/bot.py b/tests/bot.py
index 849aee27..b263bf49 100644
--- a/tests/bot.py
+++ b/tests/bot.py
@@ -31,8 +31,6 @@ nonebot.load_plugin("nonebot_plugin_test")
# load local plugins
nonebot.load_plugins("test_plugins")
-print(nonebot.require("test_export"))
-
# modify some config / config depends on loaded configs
config = driver.config
config.custom_config3 = config.custom_config1
diff --git a/tests/test_plugins/test_get_export.py b/tests/test_plugins/test_get_export.py
new file mode 100644
index 00000000..ec4437d1
--- /dev/null
+++ b/tests/test_plugins/test_get_export.py
@@ -0,0 +1,5 @@
+import nonebot
+
+from .test_export import export
+
+print(export, nonebot.require("test_export"))