diff --git a/.github/workflows/site_deploy.yml b/.github/workflows/site_deploy.yml index 5c6f081d..48db972f 100644 --- a/.github/workflows/site_deploy.yml +++ b/.github/workflows/site_deploy.yml @@ -10,6 +10,12 @@ jobs: steps: - uses: actions/checkout@v2 + if: github.event_name == 'push' + + - uses: actions/checkout@v2 + if: github.event_name != 'push' + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Get yarn cache directory path id: yarn-cache-dir-path @@ -46,7 +52,7 @@ jobs: publish-dir: './docs/.vuepress/dist' production-branch: master github-token: ${{ secrets.GITHUB_TOKEN }} - deploy-message: 'Deploy ${{ env.BRANCH_NAME }}@${{ github.sha }}' + deploy-message: 'Deploy ${{ env.DEPLOY_NAME }}@${{ github.sha }}' enable-commit-comment: false alias: ${{ env.DEPLOY_NAME }} env: diff --git a/docs/api/adapters/README.md b/docs/api/adapters/README.md index ee6edaa1..b6d728d5 100644 --- a/docs/api/adapters/README.md +++ b/docs/api/adapters/README.md @@ -17,6 +17,16 @@ sidebarDepth: 0 Bot 基类。用于处理上报消息,并提供 API 调用接口。 +### `driver` + +Driver 对象 + + +### `config` + +Config 配置对象 + + ### _abstract_ `__init__(connection_type, self_id, *, websocket=None)` @@ -33,34 +43,35 @@ Bot 基类。用于处理上报消息,并提供 API 调用接口。 -### _abstract async_ `call_api(api, **data)` +### `connection_type` + +连接类型 + + +### `self_id` + +机器人 ID + + +### `websocket` + +Websocket 连接对象 + + +### _abstract property_ `type` + +Adapter 类型 + + +### _classmethod_ `register(driver, config)` * **说明** - 调用机器人 API 接口,可以通过该函数或直接通过 bot 属性进行调用 + register 方法会在 driver.register_adapter 时被调用,用于初始化相关配置 -* **参数** - - - * `api: str`: API 名称 - - - * `**data`: API 数据 - - - -* **示例** - - -```python -await bot.call_api("send_msg", message="hello world") -await bot.send_msg(message="hello world") -``` - - ### _abstract async classmethod_ `check_permission(driver, connection_type, headers, body)` @@ -116,15 +127,34 @@ await bot.send_msg(message="hello world") -### _classmethod_ `register(driver, config)` +### _abstract async_ `call_api(api, **data)` * **说明** - register 方法会在 driver.register_adapter 时被调用,用于初始化相关配置 + 调用机器人 API 接口,可以通过该函数或直接通过 bot 属性进行调用 +* **参数** + + + * `api: str`: API 名称 + + + * `**data`: API 数据 + + + +* **示例** + + +```python +await bot.call_api("send_msg", message="hello world") +await bot.send_msg(message="hello world") +``` + + ### _abstract async_ `send(event, message, **kwargs)` @@ -147,167 +177,29 @@ await bot.send_msg(message="hello world") -### _abstract property_ `type` +## _class_ `MessageSegment` -Adapter 类型 +基类:`abc.ABC` +消息段基类 -## _class_ `Event` -基类:`abc.ABC`, `pydantic.main.BaseModel` +### `type` -Event 基类。提供获取关键信息的方法,其余信息可直接获取。 +* 类型: `str` -### _abstract_ `get_event_description()` +* 说明: 消息段类型 -* **说明** - 获取事件描述的方法,通常为事件具体内容。 +### `data` +* 类型: `Dict[str, Union[str, list]]` -* **返回** - - - * `str` - - - -### _abstract_ `get_event_name()` - - -* **说明** - - 获取事件名称的方法。 - - - -* **返回** - - - * `str` - - - -### `get_log_string()` - - -* **说明** - - 获取事件日志信息的方法,通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,可以抛出 `NoLogException` 异常。 - - - -* **返回** - - - * `str` - - - -* **异常** - - - * `NoLogException` - - - -### _abstract_ `get_message()` - - -* **说明** - - 获取事件消息内容的方法。 - - - -* **返回** - - - * `Message` - - - -### `get_plaintext()` - - -* **说明** - - 获取消息纯文本的方法,通常不需要修改,默认通过 `get_message().extract_plain_text` 获取。 - - - -* **返回** - - - * `str` - - - -### _abstract_ `get_session_id()` - - -* **说明** - - 获取会话 id 的方法,用于判断当前事件属于哪一个会话,通常是用户 id、群组 id 组合。 - - - -* **返回** - - - * `str` - - - -### _abstract_ `get_type()` - - -* **说明** - - 获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。 - - - -* **返回** - - - * `Literal["message", "notice", "request", "meta_event"]` - - - -### _abstract_ `get_user_id()` - - -* **说明** - - 获取事件主体 id 的方法,通常是用户 id 。 - - - -* **返回** - - - * `str` - - - -### _abstract_ `is_tome()` - - -* **说明** - - 获取事件是否与机器人有关的方法。 - - - -* **返回** - - - * `bool` +* 说明: 消息段数据 ## _class_ `Message` @@ -359,15 +251,6 @@ Event 基类。提供获取关键信息的方法,其余信息可直接获取 -### `extract_plain_text()` - - -* **说明** - - 提取消息内纯文本消息 - - - ### `reduce()` @@ -377,8 +260,170 @@ Event 基类。提供获取关键信息的方法,其余信息可直接获取 -## _class_ `MessageSegment` +### `extract_plain_text()` -基类:`abc.ABC` -消息段基类 +* **说明** + + 提取消息内纯文本消息 + + + +## _class_ `Event` + +基类:`abc.ABC`, `pydantic.main.BaseModel` + +Event 基类。提供获取关键信息的方法,其余信息可直接获取。 + + +### _abstract_ `get_type()` + + +* **说明** + + 获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。 + + + +* **返回** + + + * `Literal["message", "notice", "request", "meta_event"]` + + + * `str` + + + +### _abstract_ `get_event_name()` + + +* **说明** + + 获取事件名称的方法。 + + + +* **返回** + + + * `str` + + + +### _abstract_ `get_event_description()` + + +* **说明** + + 获取事件描述的方法,通常为事件具体内容。 + + + +* **返回** + + + * `str` + + + +### `get_log_string()` + + +* **说明** + + 获取事件日志信息的方法,通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,可以抛出 `NoLogException` 异常。 + + + +* **返回** + + + * `str` + + + +* **异常** + + + * `NoLogException` + + + +### _abstract_ `get_user_id()` + + +* **说明** + + 获取事件主体 id 的方法,通常是用户 id 。 + + + +* **返回** + + + * `str` + + + +### _abstract_ `get_session_id()` + + +* **说明** + + 获取会话 id 的方法,用于判断当前事件属于哪一个会话,通常是用户 id、群组 id 组合。 + + + +* **返回** + + + * `str` + + + +### _abstract_ `get_message()` + + +* **说明** + + 获取事件消息内容的方法。 + + + +* **返回** + + + * `Message` + + + +### `get_plaintext()` + + +* **说明** + + 获取消息纯文本的方法,通常不需要修改,默认通过 `get_message().extract_plain_text` 获取。 + + + +* **返回** + + + * `str` + + + +### _abstract_ `is_tome()` + + +* **说明** + + 获取事件是否与机器人有关的方法。 + + + +* **返回** + + + * `bool` diff --git a/docs/api/adapters/cqhttp.md b/docs/api/adapters/cqhttp.md index 5982e72e..104b6981 100644 --- a/docs/api/adapters/cqhttp.md +++ b/docs/api/adapters/cqhttp.md @@ -193,7 +193,7 @@ CQHTTP 配置类 ## _class_ `Bot` -基类:[`nonebot.adapters.Bot`](README.md#nonebot.adapters.Bot) +基类:[`nonebot.adapters._base.Bot`](README.md#nonebot.adapters._base.Bot) CQHTTP 协议 Bot 适配。继承属性参考 [BaseBot](./#class-basebot) 。 @@ -307,14 +307,14 @@ CQHTTP 协议 Bot 适配。继承属性参考 [BaseBot](./#class-basebot) 。 ## _class_ `MessageSegment` -基类:[`nonebot.adapters.MessageSegment`](README.md#nonebot.adapters.MessageSegment) +基类:[`nonebot.adapters._base.MessageSegment`](README.md#nonebot.adapters._base.MessageSegment) CQHTTP 协议 MessageSegment 适配。具体方法参考协议消息段类型或源码。 ## _class_ `Message` -基类:[`nonebot.adapters.Message`](README.md#nonebot.adapters.Message) +基类:[`nonebot.adapters._base.Message`](README.md#nonebot.adapters._base.Message) CQHTTP 协议 Message 适配。 @@ -377,7 +377,7 @@ CQHTTP 协议 Message 适配。 ## _class_ `Event` -基类:[`nonebot.adapters.Event`](README.md#nonebot.adapters.Event) +基类:[`nonebot.adapters._base.Event`](README.md#nonebot.adapters._base.Event) CQHTTP 协议事件,字段与 CQHTTP 一致。各事件字段参考 [CQHTTP 文档](https://github.com/howmanybots/onebot/blob/master/README.md) diff --git a/docs/api/adapters/ding.md b/docs/api/adapters/ding.md index ed2a2d38..93d6519e 100644 --- a/docs/api/adapters/ding.md +++ b/docs/api/adapters/ding.md @@ -94,7 +94,7 @@ sidebarDepth: 0 ## _class_ `Bot` -基类:[`nonebot.adapters.Bot`](README.md#nonebot.adapters.Bot) +基类:[`nonebot.adapters._base.Bot`](README.md#nonebot.adapters._base.Bot) 钉钉 协议 Bot 适配。继承属性参考 [BaseBot](./#class-basebot) 。 @@ -199,7 +199,7 @@ sidebarDepth: 0 ## _class_ `MessageSegment` -基类:[`nonebot.adapters.MessageSegment`](README.md#nonebot.adapters.MessageSegment) +基类:[`nonebot.adapters._base.MessageSegment`](README.md#nonebot.adapters._base.MessageSegment) 钉钉 协议 MessageSegment 适配。具体方法参考协议消息段类型或源码。 @@ -283,7 +283,7 @@ message += MessageSegment.atDingtalkIds(event.senderId) ## _class_ `Message` -基类:[`nonebot.adapters.Message`](README.md#nonebot.adapters.Message) +基类:[`nonebot.adapters._base.Message`](README.md#nonebot.adapters._base.Message) 钉钉 协议 Message 适配。 @@ -292,7 +292,7 @@ message += MessageSegment.atDingtalkIds(event.senderId) ## _class_ `Event` -基类:[`nonebot.adapters.Event`](README.md#nonebot.adapters.Event) +基类:[`nonebot.adapters._base.Event`](README.md#nonebot.adapters._base.Event) 钉钉协议事件。各事件字段参考 [钉钉文档](https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p) diff --git a/docs/api/adapters/mirai.md b/docs/api/adapters/mirai.md index 89ee9c0f..0a304f8f 100644 --- a/docs/api/adapters/mirai.md +++ b/docs/api/adapters/mirai.md @@ -117,7 +117,7 @@ Bot会话管理器, 提供API主动调用接口 ## _class_ `Bot` -基类:[`nonebot.adapters.Bot`](README.md#nonebot.adapters.Bot) +基类:[`nonebot.adapters._base.Bot`](README.md#nonebot.adapters._base.Bot) mirai-api-http 协议 Bot 适配。 @@ -722,7 +722,7 @@ mirai-api-http 正向 Websocket 协议 Bot 适配。 ## _class_ `MessageSegment` -基类:[`nonebot.adapters.MessageSegment`](README.md#nonebot.adapters.MessageSegment) +基类:[`nonebot.adapters._base.MessageSegment`](README.md#nonebot.adapters._base.MessageSegment) CQHTTP 协议 MessageSegment 适配。具体方法参考 [mirai-api-http 消息类型](https://github.com/project-mirai/mirai-api-http/blob/master/docs/MessageType.md) @@ -963,7 +963,7 @@ CQHTTP 协议 MessageSegment 适配。具体方法参考 [mirai-api-http 消息 ## _class_ `MessageChain` -基类:[`nonebot.adapters.Message`](README.md#nonebot.adapters.Message) +基类:[`nonebot.adapters._base.Message`](README.md#nonebot.adapters._base.Message) Mirai 协议 Message 适配 @@ -1060,7 +1060,7 @@ Mirai 协议 Message 适配 ## _class_ `Event` -基类:[`nonebot.adapters.Event`](README.md#nonebot.adapters.Event) +基类:[`nonebot.adapters._base.Event`](README.md#nonebot.adapters._base.Event) mirai-api-http 协议事件,字段与 mirai-api-http 一致。各事件字段参考 [mirai-api-http 事件类型](https://github.com/project-mirai/mirai-api-http/blob/master/docs/EventType.md) @@ -1486,7 +1486,7 @@ Bot被邀请入群申请 ## _class_ `Event` -基类:[`nonebot.adapters.Event`](README.md#nonebot.adapters.Event) +基类:[`nonebot.adapters._base.Event`](README.md#nonebot.adapters._base.Event) mirai-api-http 协议事件,字段与 mirai-api-http 一致。各事件字段参考 [mirai-api-http 事件类型](https://github.com/project-mirai/mirai-api-http/blob/master/docs/EventType.md) diff --git a/docs/api/nonebot.md b/docs/api/nonebot.md index e3b80445..c11cf2be 100644 --- a/docs/api/nonebot.md +++ b/docs/api/nonebot.md @@ -52,6 +52,9 @@ sidebarDepth: 0 * `load_plugins` => `nonebot.plugin.load_plugins` +* `load_all_plugins` => `nonebot.plugin.load_all_plugins` + + * `load_builtin_plugins` => `nonebot.plugin.load_builtin_plugins` diff --git a/docs/api/plugin.md b/docs/api/plugin.md index 1d134c52..8b16427a 100644 --- a/docs/api/plugin.md +++ b/docs/api/plugin.md @@ -1267,7 +1267,7 @@ def something_else(): * **说明** - 使用 `importlib` 加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。 + 使用 `PluginManager` 加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。 @@ -1308,6 +1308,32 @@ def something_else(): +## `load_all_plugins(module_path, plugin_dir)` + + +* **说明** + + 导入指定列表中的插件以及指定目录下多个插件,以 `_` 开头的插件不会被导入! + + + +* **参数** + + + * `module_path: Set[str]`: 指定插件集合 + + + * `plugin_dir: Set[str]`: 指定插件路径集合 + + + +* **返回** + + + * `Set[Plugin]` + + + ## `load_builtin_plugins(name='echo')` diff --git a/docs_build/adapters/README.rst b/docs_build/adapters/README.rst index e6e0d24e..ec75d8b0 100644 --- a/docs_build/adapters/README.rst +++ b/docs_build/adapters/README.rst @@ -6,7 +6,7 @@ sidebarDepth: 0 NoneBot.adapters 模块 ===================== -.. automodule:: nonebot.adapters +.. automodule:: nonebot.adapters._base :members: :private-members: :special-members: __init__ diff --git a/nonebot/__init__.py b/nonebot/__init__.py index db0d8277..efe6ed81 100644 --- a/nonebot/__init__.py +++ b/nonebot/__init__.py @@ -18,6 +18,7 @@ - ``Matchergroup`` => ``nonebot.plugin.MatcherGroup`` - ``load_plugin`` => ``nonebot.plugin.load_plugin`` - ``load_plugins`` => ``nonebot.plugin.load_plugins`` +- ``load_all_plugins`` => ``nonebot.plugin.load_all_plugins`` - ``load_builtin_plugins`` => ``nonebot.plugin.load_builtin_plugins`` - ``get_plugin`` => ``nonebot.plugin.get_plugin`` - ``get_loaded_plugins`` => ``nonebot.plugin.get_loaded_plugins`` @@ -221,5 +222,5 @@ def run(host: Optional[str] = None, from nonebot.plugin import on_message, on_notice, on_request, on_metaevent, CommandGroup, MatcherGroup from nonebot.plugin import on_startswith, on_endswith, on_keyword, on_command, on_shell_command, on_regex -from nonebot.plugin import load_plugin, load_plugins, load_builtin_plugins +from nonebot.plugin import load_plugin, load_plugins, load_all_plugins, load_builtin_plugins from nonebot.plugin import export, require, get_plugin, get_loaded_plugins diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index c279a65d..5bcc2b02 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -1,24 +1,4 @@ -""" -协议适配基类 -============ - -各协议请继承以下基类,并使用 ``driver.register_adapter`` 注册适配器 -""" - -import abc -from copy import copy -from typing_extensions import Literal -from functools import reduce, partial -from dataclasses import dataclass, field -from typing import Any, Dict, Union, TypeVar, Mapping, Optional, Callable, Iterable, Iterator, Awaitable, TYPE_CHECKING - -from pydantic import BaseModel - -from nonebot.utils import DataclassEncoder - -if TYPE_CHECKING: - from nonebot.config import Config - from nonebot.drivers import Driver, WebSocket +from typing import Iterable try: import pkg_resources @@ -31,454 +11,4 @@ except ImportError: except Exception: pass - -class Bot(abc.ABC): - """ - Bot 基类。用于处理上报消息,并提供 API 调用接口。 - """ - - driver: "Driver" - """Driver 对象""" - config: "Config" - """Config 配置对象""" - - @abc.abstractmethod - def __init__(self, - connection_type: str, - self_id: str, - *, - websocket: Optional["WebSocket"] = None): - """ - :参数: - - * ``connection_type: str``: http 或者 websocket - * ``self_id: str``: 机器人 ID - * ``websocket: Optional[WebSocket]``: Websocket 连接对象 - """ - self.connection_type = connection_type - """连接类型""" - self.self_id = self_id - """机器人 ID""" - self.websocket = websocket - """Websocket 连接对象""" - - def __getattr__(self, name: str) -> Callable[..., Awaitable[Any]]: - return partial(self.call_api, name) - - @property - @abc.abstractmethod - def type(self) -> str: - """Adapter 类型""" - raise NotImplementedError - - @classmethod - def register(cls, driver: "Driver", config: "Config"): - """ - :说明: - - `register` 方法会在 `driver.register_adapter` 时被调用,用于初始化相关配置 - """ - cls.driver = driver - cls.config = config - - @classmethod - @abc.abstractmethod - async def check_permission(cls, driver: "Driver", connection_type: str, - headers: dict, body: Optional[dict]) -> str: - """ - :说明: - - 检查连接请求是否合法的函数,如果合法则返回当前连接 ``唯一标识符``,通常为机器人 ID;如果不合法则抛出 ``RequestDenied`` 异常。 - - :参数: - - * ``driver: Driver``: Driver 对象 - * ``connection_type: str``: 连接类型 - * ``headers: dict``: 请求头 - * ``body: Optional[dict]``: 请求数据,WebSocket 连接该部分为空 - - :返回: - - - ``str``: 连接唯一标识符 - - :异常: - - - ``RequestDenied``: 请求非法 - """ - raise NotImplementedError - - @abc.abstractmethod - async def handle_message(self, message: dict): - """ - :说明: - - 处理上报消息的函数,转换为 ``Event`` 事件后调用 ``nonebot.message.handle_event`` 进一步处理事件。 - - :参数: - - * ``message: dict``: 收到的上报消息 - """ - raise NotImplementedError - - @abc.abstractmethod - async def call_api(self, api: str, **data): - """ - :说明: - - 调用机器人 API 接口,可以通过该函数或直接通过 bot 属性进行调用 - - :参数: - - * ``api: str``: API 名称 - * ``**data``: API 数据 - - :示例: - - .. code-block:: python - - await bot.call_api("send_msg", message="hello world") - await bot.send_msg(message="hello world") - """ - raise NotImplementedError - - @abc.abstractmethod - async def send(self, event: "Event", - message: Union[str, "Message", "MessageSegment"], **kwargs): - """ - :说明: - - 调用机器人基础发送消息接口 - - :参数: - - * ``event: Event``: 上报事件 - * ``message: Union[str, Message, MessageSegment]``: 要发送的消息 - * ``**kwargs`` - """ - raise NotImplementedError - - -T_Message = TypeVar("T_Message", bound="Message") -T_MessageSegment = TypeVar("T_MessageSegment", bound="MessageSegment") - - -@dataclass -class MessageSegment(abc.ABC): - """消息段基类""" - type: str - """ - - 类型: ``str`` - - 说明: 消息段类型 - """ - data: Dict[str, Any] = field(default_factory=lambda: {}) - """ - - 类型: ``Dict[str, Union[str, list]]`` - - 说明: 消息段数据 - """ - - @abc.abstractmethod - def __str__(self: T_MessageSegment) -> str: - """该消息段所代表的 str,在命令匹配部分使用""" - raise NotImplementedError - - @abc.abstractmethod - def __add__(self: T_MessageSegment, other: Union[str, T_MessageSegment, - T_Message]) -> T_Message: - """你需要在这里实现不同消息段的合并: - 比如: - if isinstance(other, str): - ... - elif isinstance(other, MessageSegment): - ... - 注意:需要返回一个新生成的对象 - """ - raise NotImplementedError - - @abc.abstractmethod - def __radd__( - self: T_MessageSegment, other: Union[str, dict, list, T_MessageSegment, - T_Message]) -> "T_Message": - """你需要在这里实现不同消息段的合并: - 比如: - if isinstance(other, str): - ... - elif isinstance(other, MessageSegment): - ... - 注意:需要返回一个新生成的对象 - """ - raise NotImplementedError - - def __getitem__(self, key): - return getattr(self, key) - - def __setitem__(self, key, value): - return setattr(self, key, value) - - def get(self, key, default=None): - return getattr(self, key, default) - - def copy(self: T_MessageSegment) -> T_MessageSegment: - return copy(self) - - @abc.abstractmethod - def is_text(self) -> bool: - raise NotImplementedError - - -class Message(list, abc.ABC): - """消息数组""" - - def __init__(self, - message: Union[str, None, Mapping, Iterable[Mapping], - T_MessageSegment, T_Message, Any] = None, - *args, - **kwargs): - """ - :参数: - - * ``message: Union[str, list, dict, MessageSegment, Message, Any]``: 消息内容 - """ - super().__init__(*args, **kwargs) - if message is None: - return - elif isinstance(message, Message): - self.extend(message) - elif isinstance(message, MessageSegment): - self.append(message) - else: - self.extend(self._construct(message)) - - def __str__(self): - return ''.join((str(seg) for seg in self)) - - @classmethod - def __get_validators__(cls): - yield cls._validate - - @classmethod - def _validate(cls, value): - return cls(value) - - @staticmethod - @abc.abstractmethod - def _construct( - msg: Union[str, Mapping, Iterable[Mapping], Any] - ) -> Iterable[T_MessageSegment]: - raise NotImplementedError - - def __add__(self: T_Message, other: Union[str, T_MessageSegment, - T_Message]) -> T_Message: - result = self.__class__(self) - if isinstance(other, str): - result.extend(self._construct(other)) - elif isinstance(other, MessageSegment): - result.append(other) - elif isinstance(other, Message): - result.extend(other) - return result - - def __radd__(self: T_Message, other: Union[str, T_MessageSegment, - T_Message]) -> T_Message: - result = self.__class__(other) - return result.__add__(self) - - def __iadd__(self: T_Message, other: Union[str, T_MessageSegment, - T_Message]) -> T_Message: - if isinstance(other, str): - self.extend(self._construct(other)) - elif isinstance(other, MessageSegment): - self.append(other) - elif isinstance(other, Message): - self.extend(other) - return self - - def append(self: T_Message, obj: Union[str, T_MessageSegment]) -> T_Message: - """ - :说明: - - 添加一个消息段到消息数组末尾 - - :参数: - - * ``obj: Union[str, MessageSegment]``: 要添加的消息段 - """ - if isinstance(obj, MessageSegment): - super().append(obj) - elif isinstance(obj, str): - self.extend(self._construct(obj)) - else: - raise ValueError(f"Unexpected type: {type(obj)} {obj}") - return self - - def extend(self: T_Message, - obj: Union[T_Message, Iterable[T_MessageSegment]]) -> T_Message: - """ - :说明: - - 拼接一个消息数组或多个消息段到消息数组末尾 - - :参数: - - * ``obj: Union[Message, Iterable[MessageSegment]]``: 要添加的消息数组 - """ - for segment in obj: - self.append(segment) - return self - - def reduce(self: T_Message) -> None: - """ - :说明: - - 缩减消息数组,即按 MessageSegment 的实现拼接相邻消息段 - """ - index = 0 - while index < len(self): - if index > 0 and self[index - - 1].is_text() and self[index].is_text(): - self[index - 1] += self[index] - del self[index] - else: - index += 1 - - def extract_plain_text(self: T_Message) -> str: - """ - :说明: - - 提取消息内纯文本消息 - """ - - def _concat(x: str, y: T_MessageSegment) -> str: - return f"{x} {y}" if y.is_text() else x - - plain_text = reduce(_concat, self, "") - return plain_text[1:] if plain_text else plain_text - - -class Event(abc.ABC, BaseModel): - """Event 基类。提供获取关键信息的方法,其余信息可直接获取。""" - - class Config: - extra = "allow" - json_encoders = {Message: DataclassEncoder} - - @abc.abstractmethod - def get_type(self) -> Literal["message", "notice", "request", "meta_event"]: - """ - :说明: - - 获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。 - - :返回: - - * ``Literal["message", "notice", "request", "meta_event"]`` - """ - raise NotImplementedError - - @abc.abstractmethod - def get_event_name(self) -> str: - """ - :说明: - - 获取事件名称的方法。 - - :返回: - - * ``str`` - """ - raise NotImplementedError - - @abc.abstractmethod - def get_event_description(self) -> str: - """ - :说明: - - 获取事件描述的方法,通常为事件具体内容。 - - :返回: - - * ``str`` - """ - raise NotImplementedError - - def __str__(self) -> str: - return f"[{self.get_event_name()}]: {self.get_event_description()}" - - def get_log_string(self) -> str: - """ - :说明: - - 获取事件日志信息的方法,通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,可以抛出 ``NoLogException`` 异常。 - - :返回: - - * ``str`` - - :异常: - - - ``NoLogException`` - """ - return f"[{self.get_event_name()}]: {self.get_event_description()}" - - @abc.abstractmethod - def get_user_id(self) -> str: - """ - :说明: - - 获取事件主体 id 的方法,通常是用户 id 。 - - :返回: - - * ``str`` - """ - raise NotImplementedError - - @abc.abstractmethod - def get_session_id(self) -> str: - """ - :说明: - - 获取会话 id 的方法,用于判断当前事件属于哪一个会话,通常是用户 id、群组 id 组合。 - - :返回: - - * ``str`` - """ - raise NotImplementedError - - @abc.abstractmethod - def get_message(self) -> "Message": - """ - :说明: - - 获取事件消息内容的方法。 - - :返回: - - * ``Message`` - """ - raise NotImplementedError - - def get_plaintext(self) -> str: - """ - :说明: - - 获取消息纯文本的方法,通常不需要修改,默认通过 ``get_message().extract_plain_text`` 获取。 - - :返回: - - * ``str`` - """ - return self.get_message().extract_plain_text() - - @abc.abstractmethod - def is_tome(self) -> bool: - """ - :说明: - - 获取事件是否与机器人有关的方法。 - - :返回: - - * ``bool`` - """ - raise NotImplementedError +from ._base import Bot, Event, Message, MessageSegment diff --git a/nonebot/adapters/_base.py b/nonebot/adapters/_base.py new file mode 100644 index 00000000..328883c8 --- /dev/null +++ b/nonebot/adapters/_base.py @@ -0,0 +1,474 @@ +""" +协议适配基类 +============ + +各协议请继承以下基类,并使用 ``driver.register_adapter`` 注册适配器 +""" + +import abc +from copy import copy +from typing_extensions import Literal +from functools import reduce, partial +from dataclasses import dataclass, field +from typing import Any, Dict, Union, TypeVar, Mapping, Optional, Callable, Iterable, Iterator, Awaitable, TYPE_CHECKING + +from pydantic import BaseModel + +from nonebot.utils import DataclassEncoder + +if TYPE_CHECKING: + from nonebot.config import Config + from nonebot.drivers import Driver, WebSocket + + +class Bot(abc.ABC): + """ + Bot 基类。用于处理上报消息,并提供 API 调用接口。 + """ + + driver: "Driver" + """Driver 对象""" + config: "Config" + """Config 配置对象""" + + @abc.abstractmethod + def __init__(self, + connection_type: str, + self_id: str, + *, + websocket: Optional["WebSocket"] = None): + """ + :参数: + + * ``connection_type: str``: http 或者 websocket + * ``self_id: str``: 机器人 ID + * ``websocket: Optional[WebSocket]``: Websocket 连接对象 + """ + self.connection_type = connection_type + """连接类型""" + self.self_id = self_id + """机器人 ID""" + self.websocket = websocket + """Websocket 连接对象""" + + def __getattr__(self, name: str) -> Callable[..., Awaitable[Any]]: + return partial(self.call_api, name) + + @property + @abc.abstractmethod + def type(self) -> str: + """Adapter 类型""" + raise NotImplementedError + + @classmethod + def register(cls, driver: "Driver", config: "Config"): + """ + :说明: + + `register` 方法会在 `driver.register_adapter` 时被调用,用于初始化相关配置 + """ + cls.driver = driver + cls.config = config + + @classmethod + @abc.abstractmethod + async def check_permission(cls, driver: "Driver", connection_type: str, + headers: dict, body: Optional[dict]) -> str: + """ + :说明: + + 检查连接请求是否合法的函数,如果合法则返回当前连接 ``唯一标识符``,通常为机器人 ID;如果不合法则抛出 ``RequestDenied`` 异常。 + + :参数: + + * ``driver: Driver``: Driver 对象 + * ``connection_type: str``: 连接类型 + * ``headers: dict``: 请求头 + * ``body: Optional[dict]``: 请求数据,WebSocket 连接该部分为空 + + :返回: + + - ``str``: 连接唯一标识符 + + :异常: + + - ``RequestDenied``: 请求非法 + """ + raise NotImplementedError + + @abc.abstractmethod + async def handle_message(self, message: dict): + """ + :说明: + + 处理上报消息的函数,转换为 ``Event`` 事件后调用 ``nonebot.message.handle_event`` 进一步处理事件。 + + :参数: + + * ``message: dict``: 收到的上报消息 + """ + raise NotImplementedError + + @abc.abstractmethod + async def call_api(self, api: str, **data): + """ + :说明: + + 调用机器人 API 接口,可以通过该函数或直接通过 bot 属性进行调用 + + :参数: + + * ``api: str``: API 名称 + * ``**data``: API 数据 + + :示例: + + .. code-block:: python + + await bot.call_api("send_msg", message="hello world") + await bot.send_msg(message="hello world") + """ + raise NotImplementedError + + @abc.abstractmethod + async def send(self, event: "Event", + message: Union[str, "Message", "MessageSegment"], **kwargs): + """ + :说明: + + 调用机器人基础发送消息接口 + + :参数: + + * ``event: Event``: 上报事件 + * ``message: Union[str, Message, MessageSegment]``: 要发送的消息 + * ``**kwargs`` + """ + raise NotImplementedError + + +T_Message = TypeVar("T_Message", bound="Message") +T_MessageSegment = TypeVar("T_MessageSegment", bound="MessageSegment") + + +@dataclass +class MessageSegment(abc.ABC): + """消息段基类""" + type: str + """ + - 类型: ``str`` + - 说明: 消息段类型 + """ + data: Dict[str, Any] = field(default_factory=lambda: {}) + """ + - 类型: ``Dict[str, Union[str, list]]`` + - 说明: 消息段数据 + """ + + @abc.abstractmethod + def __str__(self: T_MessageSegment) -> str: + """该消息段所代表的 str,在命令匹配部分使用""" + raise NotImplementedError + + @abc.abstractmethod + def __add__(self: T_MessageSegment, other: Union[str, T_MessageSegment, + T_Message]) -> T_Message: + """你需要在这里实现不同消息段的合并: + 比如: + if isinstance(other, str): + ... + elif isinstance(other, MessageSegment): + ... + 注意:需要返回一个新生成的对象 + """ + raise NotImplementedError + + @abc.abstractmethod + def __radd__( + self: T_MessageSegment, other: Union[str, dict, list, T_MessageSegment, + T_Message]) -> "T_Message": + """你需要在这里实现不同消息段的合并: + 比如: + if isinstance(other, str): + ... + elif isinstance(other, MessageSegment): + ... + 注意:需要返回一个新生成的对象 + """ + raise NotImplementedError + + def __getitem__(self, key): + return getattr(self, key) + + def __setitem__(self, key, value): + return setattr(self, key, value) + + def get(self, key, default=None): + return getattr(self, key, default) + + def copy(self: T_MessageSegment) -> T_MessageSegment: + return copy(self) + + @abc.abstractmethod + def is_text(self) -> bool: + raise NotImplementedError + + +class Message(list, abc.ABC): + """消息数组""" + + def __init__(self, + message: Union[str, None, Mapping, Iterable[Mapping], + T_MessageSegment, T_Message, Any] = None, + *args, + **kwargs): + """ + :参数: + + * ``message: Union[str, list, dict, MessageSegment, Message, Any]``: 消息内容 + """ + super().__init__(*args, **kwargs) + if message is None: + return + elif isinstance(message, Message): + self.extend(message) + elif isinstance(message, MessageSegment): + self.append(message) + else: + self.extend(self._construct(message)) + + def __str__(self): + return ''.join((str(seg) for seg in self)) + + @classmethod + def __get_validators__(cls): + yield cls._validate + + @classmethod + def _validate(cls, value): + return cls(value) + + @staticmethod + @abc.abstractmethod + def _construct( + msg: Union[str, Mapping, Iterable[Mapping], Any] + ) -> Iterable[T_MessageSegment]: + raise NotImplementedError + + def __add__(self: T_Message, other: Union[str, T_MessageSegment, + T_Message]) -> T_Message: + result = self.__class__(self) + if isinstance(other, str): + result.extend(self._construct(other)) + elif isinstance(other, MessageSegment): + result.append(other) + elif isinstance(other, Message): + result.extend(other) + return result + + def __radd__(self: T_Message, other: Union[str, T_MessageSegment, + T_Message]) -> T_Message: + result = self.__class__(other) + return result.__add__(self) + + def __iadd__(self: T_Message, other: Union[str, T_MessageSegment, + T_Message]) -> T_Message: + if isinstance(other, str): + self.extend(self._construct(other)) + elif isinstance(other, MessageSegment): + self.append(other) + elif isinstance(other, Message): + self.extend(other) + return self + + def append(self: T_Message, obj: Union[str, T_MessageSegment]) -> T_Message: + """ + :说明: + + 添加一个消息段到消息数组末尾 + + :参数: + + * ``obj: Union[str, MessageSegment]``: 要添加的消息段 + """ + if isinstance(obj, MessageSegment): + super().append(obj) + elif isinstance(obj, str): + self.extend(self._construct(obj)) + else: + raise ValueError(f"Unexpected type: {type(obj)} {obj}") + return self + + def extend(self: T_Message, + obj: Union[T_Message, Iterable[T_MessageSegment]]) -> T_Message: + """ + :说明: + + 拼接一个消息数组或多个消息段到消息数组末尾 + + :参数: + + * ``obj: Union[Message, Iterable[MessageSegment]]``: 要添加的消息数组 + """ + for segment in obj: + self.append(segment) + return self + + def reduce(self: T_Message) -> None: + """ + :说明: + + 缩减消息数组,即按 MessageSegment 的实现拼接相邻消息段 + """ + index = 0 + while index < len(self): + if index > 0 and self[index - + 1].is_text() and self[index].is_text(): + self[index - 1] += self[index] + del self[index] + else: + index += 1 + + def extract_plain_text(self: T_Message) -> str: + """ + :说明: + + 提取消息内纯文本消息 + """ + + def _concat(x: str, y: T_MessageSegment) -> str: + return f"{x} {y}" if y.is_text() else x + + plain_text = reduce(_concat, self, "") + return plain_text[1:] if plain_text else plain_text + + +class Event(abc.ABC, BaseModel): + """Event 基类。提供获取关键信息的方法,其余信息可直接获取。""" + + class Config: + extra = "allow" + json_encoders = {Message: DataclassEncoder} + + @abc.abstractmethod + def get_type(self) -> str: + """ + :说明: + + 获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。 + + :返回: + + * ``Literal["message", "notice", "request", "meta_event"]`` + * ``str`` + """ + raise NotImplementedError + + @abc.abstractmethod + def get_event_name(self) -> str: + """ + :说明: + + 获取事件名称的方法。 + + :返回: + + * ``str`` + """ + raise NotImplementedError + + @abc.abstractmethod + def get_event_description(self) -> str: + """ + :说明: + + 获取事件描述的方法,通常为事件具体内容。 + + :返回: + + * ``str`` + """ + raise NotImplementedError + + def __str__(self) -> str: + return f"[{self.get_event_name()}]: {self.get_event_description()}" + + def get_log_string(self) -> str: + """ + :说明: + + 获取事件日志信息的方法,通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,可以抛出 ``NoLogException`` 异常。 + + :返回: + + * ``str`` + + :异常: + + - ``NoLogException`` + """ + return f"[{self.get_event_name()}]: {self.get_event_description()}" + + @abc.abstractmethod + def get_user_id(self) -> str: + """ + :说明: + + 获取事件主体 id 的方法,通常是用户 id 。 + + :返回: + + * ``str`` + """ + raise NotImplementedError + + @abc.abstractmethod + def get_session_id(self) -> str: + """ + :说明: + + 获取会话 id 的方法,用于判断当前事件属于哪一个会话,通常是用户 id、群组 id 组合。 + + :返回: + + * ``str`` + """ + raise NotImplementedError + + @abc.abstractmethod + def get_message(self) -> "Message": + """ + :说明: + + 获取事件消息内容的方法。 + + :返回: + + * ``Message`` + """ + raise NotImplementedError + + def get_plaintext(self) -> str: + """ + :说明: + + 获取消息纯文本的方法,通常不需要修改,默认通过 ``get_message().extract_plain_text`` 获取。 + + :返回: + + * ``str`` + """ + return self.get_message().extract_plain_text() + + @abc.abstractmethod + def is_tome(self) -> bool: + """ + :说明: + + 获取事件是否与机器人有关的方法。 + + :返回: + + * ``bool`` + """ + raise NotImplementedError diff --git a/nonebot/plugin.py b/nonebot/plugin/__init__.py similarity index 93% rename from nonebot/plugin.py rename to nonebot/plugin/__init__.py index e66672cd..439067af 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") @@ -946,11 +948,37 @@ class MatcherGroup: return matcher +def _load_plugin(manager: PluginManager, plugin_name: str) -> Optional[Plugin]: + if plugin_name.startswith("_"): + return None + + _tmp_matchers.set(set()) + _export.set(Export()) + + if plugin_name in plugins: + return None + + try: + module = manager.load_plugin(plugin_name) + + for m in _tmp_matchers.get(): + 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 "{plugin_name}"') + return None + + def load_plugin(module_path: str) -> Optional[Plugin]: """ :说明: - 使用 ``importlib`` 加载单个插件,可以是本地插件或是通过 ``pip`` 安装的插件。 + 使用 ``PluginManager`` 加载单个插件,可以是本地插件或是通过 ``pip`` 安装的插件。 :参数: @@ -961,34 +989,9 @@ def load_plugin(module_path: str) -> Optional[Plugin]: - ``Optional[Plugin]`` """ - def _load_plugin(module_path: str) -> Optional[Plugin]: - try: - _tmp_matchers.set(set()) - _export.set(Export()) - if module_path in plugins: - return plugins[module_path] - elif module_path in sys.modules: - logger.warning( - f"Module {module_path} has been loaded by other plugins! Ignored" - ) - return None - module = importlib.import_module(module_path) - for m in _tmp_matchers.get(): - m.module = module_path - plugin = Plugin(module_path, module, _tmp_matchers.get(), - _export.get()) - plugins[module_path] = plugin - logger.opt( - colors=True).info(f'Succeeded to import "{module_path}"') - return plugin - except Exception as e: - logger.opt(colors=True, exception=e).error( - f'Failed to import "{module_path}"' - ) - return None - context: Context = copy_context() - return context.run(_load_plugin, module_path) + manager = PluginManager(PLUGIN_NAMESPACE, plugins=[module_path]) + return context.run(_load_plugin, manager, module_path) def load_plugins(*plugin_dir: str) -> Set[Plugin]: @@ -1005,43 +1008,65 @@ def load_plugins(*plugin_dir: str) -> Set[Plugin]: - ``Set[Plugin]`` """ + loaded_plugins = set() + manager = PluginManager(PLUGIN_NAMESPACE, search_path=plugin_dir) + for plugin_name in manager.list_plugins(): + context: Context = copy_context() + result = context.run(_load_plugin, manager, plugin_name) + if result: + loaded_plugins.add(result) + return loaded_plugins + + +def load_all_plugins(module_path: Set[str], + plugin_dir: Set[str]) -> Set[Plugin]: + """ + :说明: + + 导入指定列表中的插件以及指定目录下多个插件,以 ``_`` 开头的插件不会被导入! + + :参数: + + - ``module_path: Set[str]``: 指定插件集合 + - ``plugin_dir: Set[str]``: 指定插件路径集合 + + :返回: + + - ``Set[Plugin]`` + """ + + def _load_plugin(plugin_name: str) -> Optional[Plugin]: + if plugin_name.startswith("_"): + return None - def _load_plugin(module_info) -> Optional[Plugin]: _tmp_matchers.set(set()) _export.set(Export()) - name = module_info.name - if 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") + 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, module_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 99% rename from nonebot/plugin.pyi rename to nonebot/plugin/__init__.pyi index 409043d5..9549d45e 100644 --- a/nonebot/plugin.pyi +++ b/nonebot/plugin/__init__.pyi @@ -176,6 +176,11 @@ def load_plugins(*plugin_dir: str) -> Set[Plugin]: ... +def load_all_plugins(module_path: Set[str], + plugin_dir: Set[str]) -> Set[Plugin]: + ... + + def load_builtin_plugins(name: str = ...): ... diff --git a/nonebot/plugin/manager.py b/nonebot/plugin/manager.py new file mode 100644 index 00000000..a184d956 --- /dev/null +++ b/nonebot/plugin/manager.py @@ -0,0 +1,181 @@ +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, prefix: str, plugin_manager: "PluginManager"): + super().__init__(f"{prefix}.{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!") + + prefix = sys._getframe(2).f_globals.get( + "__name__") or _internal_space.__name__ + if not prefix.startswith(_internal_space.__name__): + prefix = _internal_space.__name__ + module = _InternalModule(prefix, 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/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/event.py b/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/event.py index 2037577b..53497870 100644 --- a/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/event.py +++ b/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/event.py @@ -25,10 +25,10 @@ class Event(BaseEvent): __event__ = "" time: int self_id: int - post_type: Literal["message", "notice", "request", "meta_event"] + post_type: str @overrides(BaseEvent) - def get_type(self) -> Literal["message", "notice", "request", "meta_event"]: + def get_type(self) -> str: return self.post_type @overrides(BaseEvent) diff --git a/pages/changelog.md b/pages/changelog.md index dac7d888..71a8c679 100644 --- a/pages/changelog.md +++ b/pages/changelog.md @@ -4,6 +4,16 @@ sidebar: auto # 更新日志 +## v2.0.0a11 + +- 修改 `nonebot` 项目结构,分离所有 `adapter` +- 修改插件加载逻辑,使用 `import hook` (PEP 302) + +## v2.0.0a10 + +- 新增 `Quart Driver` 支持 +- 修复 `mirai` 协议适配命令处理以及消息转义 + ## v2.0.0a9 - 修复 `Message` 消息为 `None` 时的处理错误 diff --git a/tests/bot.py b/tests/bot.py index 849aee27..e4ec39e4 100644 --- a/tests/bot.py +++ b/tests/bot.py @@ -25,13 +25,10 @@ driver.register_adapter("mirai", MiraiBot) # load builtin plugin nonebot.load_builtin_plugins() -nonebot.load_plugin("nonebot_plugin_apscheduler") -nonebot.load_plugin("nonebot_plugin_test") -# load local plugins -nonebot.load_plugins("test_plugins") - -print(nonebot.require("test_export")) +# load all plugins +nonebot.load_all_plugins({"nonebot_plugin_apscheduler", "nonebot_plugin_test"}, + {"test_plugins"}) # modify some config / config depends on loaded configs config = driver.config 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"))