mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-06-08 21:35:39 +00:00
⚗️ change import hook
This commit is contained in:
parent
6cd6750729
commit
f26fb9d6fb
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
为 NoneBot 插件开发提供便携的定义函数。
|
为 NoneBot 插件开发提供便携的定义函数。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import pkgutil
|
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.typing import T_State, T_StateFactory, T_Handler, T_RuleChecker
|
||||||
from nonebot.rule import Rule, startswith, endswith, keyword, command, shell_command, ArgumentParser, regex
|
from nonebot.rule import Rule, startswith, endswith, keyword, command, shell_command, ArgumentParser, regex
|
||||||
|
|
||||||
|
from .manager import PluginManager
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from nonebot.adapters import Bot, Event, MessageSegment
|
from nonebot.adapters import Bot, Event
|
||||||
|
|
||||||
plugins: Dict[str, "Plugin"] = {}
|
plugins: Dict[str, "Plugin"] = {}
|
||||||
"""
|
"""
|
||||||
:类型: ``Dict[str, Plugin]``
|
:类型: ``Dict[str, Plugin]``
|
||||||
:说明: 已加载的插件
|
:说明: 已加载的插件
|
||||||
"""
|
"""
|
||||||
|
PLUGIN_NAMESPACE = "nonebot.loaded_plugins"
|
||||||
|
|
||||||
_export: ContextVar["Export"] = ContextVar("_export")
|
_export: ContextVar["Export"] = ContextVar("_export")
|
||||||
_tmp_matchers: ContextVar[Set[Type[Matcher]]] = ContextVar("_tmp_matchers")
|
_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]``
|
- ``Set[Plugin]``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _load_plugin(module_info) -> Optional[Plugin]:
|
def _load_plugin(plugin_name: str) -> Optional[Plugin]:
|
||||||
_tmp_matchers.set(set())
|
if plugin_name.startswith("_"):
|
||||||
_export.set(Export())
|
|
||||||
name = module_info.name
|
|
||||||
if name.startswith("_"):
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
spec = module_info.module_finder.find_spec(name, None)
|
_tmp_matchers.set(set())
|
||||||
if not spec:
|
_export.set(Export())
|
||||||
logger.warning(
|
|
||||||
f"Module {name} cannot be loaded! Check module name first.")
|
if plugin_name in plugins:
|
||||||
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")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
module = _load(spec)
|
module = manager.load_plugin(plugin_name)
|
||||||
|
|
||||||
for m in _tmp_matchers.get():
|
for m in _tmp_matchers.get():
|
||||||
m.module = name
|
m.module = plugin_name
|
||||||
plugin = Plugin(name, module, _tmp_matchers.get(), _export.get())
|
plugin = Plugin(plugin_name, module, _tmp_matchers.get(),
|
||||||
plugins[name] = plugin
|
_export.get())
|
||||||
logger.opt(colors=True).info(f'Succeeded to import "<y>{name}</y>"')
|
plugins[plugin_name] = plugin
|
||||||
|
logger.opt(
|
||||||
|
colors=True).info(f'Succeeded to import "<y>{plugin_name}</y>"')
|
||||||
return plugin
|
return plugin
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.opt(colors=True, exception=e).error(
|
logger.opt(colors=True, exception=e).error(
|
||||||
f'<r><bg #f8bbd0>Failed to import "{name}"</bg #f8bbd0></r>')
|
f'<r><bg #f8bbd0>Failed to import "{plugin_name}"</bg #f8bbd0></r>'
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
loaded_plugins = set()
|
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()
|
context: Context = copy_context()
|
||||||
result = context.run(_load_plugin, module_info)
|
result = context.run(_load_plugin, plugin_name)
|
||||||
if result:
|
if result:
|
||||||
loaded_plugins.add(result)
|
loaded_plugins.add(result)
|
||||||
return loaded_plugins
|
return loaded_plugins
|
177
nonebot/plugin/manager.py
Normal file
177
nonebot/plugin/manager.py
Normal file
@ -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())
|
@ -31,8 +31,6 @@ nonebot.load_plugin("nonebot_plugin_test")
|
|||||||
# load local plugins
|
# load local plugins
|
||||||
nonebot.load_plugins("test_plugins")
|
nonebot.load_plugins("test_plugins")
|
||||||
|
|
||||||
print(nonebot.require("test_export"))
|
|
||||||
|
|
||||||
# modify some config / config depends on loaded configs
|
# modify some config / config depends on loaded configs
|
||||||
config = driver.config
|
config = driver.config
|
||||||
config.custom_config3 = config.custom_config1
|
config.custom_config3 = config.custom_config1
|
||||||
|
5
tests/test_plugins/test_get_export.py
Normal file
5
tests/test_plugins/test_get_export.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import nonebot
|
||||||
|
|
||||||
|
from .test_export import export
|
||||||
|
|
||||||
|
print(export, nonebot.require("test_export"))
|
Loading…
x
Reference in New Issue
Block a user