From 925886534cfef9fb3cb22424a0fd5bbc6e6e4766 Mon Sep 17 00:00:00 2001 From: AkiraXie Date: Mon, 14 Feb 2022 17:26:55 +0800 Subject: [PATCH] :memo: refactor dependency-injection documents (#791) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 update dependency-injection docs * :rotating_light: auto fix by pre-commit hooks * 📝 fix some indent * 📝 fix description * 📝 add create callable in DI docs * :rotating_light: auto fix by pre-commit hooks * 📝 delete unused params in docs * 📝 update di docs * :rotating_light: auto fix by pre-commit hooks * :memo: update di doc Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com> --- website/docs/advanced/README.md | 14 +- .../docs/advanced/di/dependency-injection.md | 260 +++++++++++++----- website/docs/advanced/di/overload.md | 6 +- website/docs/advanced/di/sync-support.md | 11 - website/docs/advanced/permission.md | 12 +- website/docs/advanced/runtime-hook.md | 12 +- 6 files changed, 220 insertions(+), 95 deletions(-) delete mode 100644 website/docs/advanced/di/sync-support.md diff --git a/website/docs/advanced/README.md b/website/docs/advanced/README.md index 56f7ca5d..7e375409 100644 --- a/website/docs/advanced/README.md +++ b/website/docs/advanced/README.md @@ -75,7 +75,7 @@ NoneBot2 是一个可以对机器人上报的事件进行处理并完成具体 ::: :::warning -`self-id` 是帐号的唯一识别 ID,这意味着不能出现相同的 `self-id`。 +`self-id` 是帐号的唯一识别 ID ,这意味着不能出现相同的 `self-id`。 ::: 2. 根据 `self-id` 实例化 `Adapter` 相应的 `Bot` 。 @@ -94,13 +94,13 @@ NoneBot2 是一个可以对机器人上报的事件进行处理并完成具体 #### 概念解释 -1. **hook**,或者说**钩子函数**,它们可以在 NoneBot 处理 `Event` 的不同时刻进行拦截,修改或者扩展,在 NoneBot 中,事件钩子函数分为`事件预处理 hook`、`运行预处理 hook`、`运行后处理 hook` 和`事件后处理 hook`。 +1. **hook** ,或者说**钩子函数**,它们可以在 NoneBot 处理 `Event` 的不同时刻进行拦截,修改或者扩展,在 NoneBot 中,事件钩子函数分为`事件预处理 hook`、`运行预处理 hook`、`运行后处理 hook` 和`事件后处理 hook`。 :::tip -关于 `hook` 的更多信息,可以查阅[这里](./runtime-hook.md) +关于 `hook` 的更多信息,可以查阅[这里](./runtime-hook.md)。 ::: -2. **Matcher** 与 **matcher**,在**指南**中,我们讲述了[如何注册事件响应器](../tutorial/plugin/create-matcher.md),这里的事件响应器或者说 `Matcher` 并不是一个具体的实例 `instance`,而是一个具有特定属性的类 `class`。只有当 `Matcher` **响应事件**时,才会实例化为具体的 `instance`,也就是 `matcher`。`matcher` 可以认为是 NoneBot 处理 `Event` 的基本单位,运行 `matcher` 是 NoneBot 工作的主要内容。 +2. **Matcher** 与 **matcher**,在**指南**中,我们讲述了[如何注册事件响应器](../tutorial/plugin/create-matcher.md),这里的事件响应器或者说 `Matcher` 并不是一个具体的实例 `instance`,而是一个具有特定属性的类 `class`。只有当 `Matcher` **响应事件**时,才会实例化为具体的 `instance`,也就是 `matcher` 。`matcher` 可以认为是 NoneBot 处理 `Event` 的基本单位,运行 `matcher` 是 NoneBot 工作的主要内容。 3. **handler**,或者说**事件处理函数**,它们可以认为是 NoneBot 处理 `Event` 的最小单位。在不考虑 `hook` 的情况下,**运行 matcher 就是顺序运行 matcher.handlers**,这句话换种表达方式就是,`handler` 只有添加到 `matcher.handlers` 时,才可以参与到 NoneBot 的工作中来。 @@ -115,7 +115,7 @@ NoneBot2 是一个可以对机器人上报的事件进行处理并完成具体 1. **执行事件预处理 hook**, NoneBot 接收到 `Event` 后,会传入到 `事件预处理 hook` 中进行处理。 :::warning -需要注意的是,执行多个 `事件预处理 hook` 时并无顺序可言,它们是**并行运行**的。这个原则同样适用于其他的 `hook`。 +需要注意的是,执行多个 `事件预处理 hook` 时并无顺序可言,它们是**并发运行**的。这个原则同样适用于其他的 `hook`。 ::: 2. **按优先级升序选出同一优先级的 Matcher**,NoneBot 提供了一个全局字典 `matchers`,这个字典的 `key` 是优先级 `priority`,`value` 是一个 `list`,里面存放着同一优先级的 `Matcher`。在注册 `Matcher` 时,它和优先级 `priority` 会添加到里面。 @@ -160,7 +160,7 @@ NoneBot2 是一个可以对机器人上报的事件进行处理并完成具体 这个异常可以在 `handler` 中由 `Matcher.pause` 抛出。 - 当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将后续的 `handler` 交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行后续的 `handler `。 + 当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将后续的 `handler` 交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行后续的 `handler`。 3. **RejectedException** @@ -178,7 +178,7 @@ NoneBot2 是一个可以对机器人上报的事件进行处理并完成具体 这个异常一般会在执行 `运行后处理 hook` 后抛出。 - 当 NoneBot 捕获到它时, 会停止传播当前 `Event`,不再寻找下一优先级的 `Matcher`,直接执行 `事件后处理 hook`。 + 当 NoneBot 捕获到它时, 会停止传播当前 `Event` ,不再寻找下一优先级的 `Matcher` ,直接执行 `事件后处理 hook` 。 ## 调用 API diff --git a/website/docs/advanced/di/dependency-injection.md b/website/docs/advanced/di/dependency-injection.md index 127c85e1..66645709 100644 --- a/website/docs/advanced/di/dependency-injection.md +++ b/website/docs/advanced/di/dependency-injection.md @@ -14,87 +14,81 @@ options: ## 什么是依赖注入? -~~交给 mix 了~~ +[依赖注入](https://zh.wikipedia.org/wiki/%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5) + +> 在软件工程中,**依赖注入**(dependency injection)的意思为,给予调用方它所需要的事物。 “依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接使用“依赖”,取而代之是“注入” 。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖。 传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。 + +依赖注入往往起到了分离依赖和调用方的作用,这样一方面能让代码更为整洁可读,一方面可以提升代码的复用性。 ## 使用依赖注入 以下通过一个简单的例子来说明依赖注入的使用方法: -### 编写依赖函数 - -这里我们编写了一个简单的函数 `depend` 作为依赖函数 - -```python {7-9} -from nonebot.log import logger -from nonebot.params import Depends -from nonebot import on_command, on_message +```python {2,7-8,11} +from nonebot import on_command +from nonebot.params import Depends # 1.引用 Depends +from nonebot.adapters.onebot.v11 import MessageEvent test = on_command("123") -def depend(state: dict): - # do something with state - return {**state, "depend": "depend"} +async def depend(event: MessageEvent): # 2.编写依赖函数 + return {"uid": event.get_user_id(), "nickname": event.sender.nickname} @test.handle() -async def _(x: dict = Depends(depend)): - print(x) +async def _(x: dict = Depends(depend)): # 3.在事件处理函数里声明依赖项 + print(x["uid"], x["nickname"]) ``` -它和普通的事件处理函数并无区别,同样可以接收 `bot`, `event` 等参数,你可以把它当作一个普通的事件处理函数,但是去除了装饰器(没有使用 `matcher.handle()` 等来装饰),并且可以返回任何类型的值。 +如注释所言,可以用三步来说明依赖注入的使用过程: -在这个例子中,依赖函数接受一个参数: +1. 引用 `Depends` 。 -- `state: dict`:当前事件处理状态字典。 +2. 编写依赖函数。依赖函数和普通的事件处理函数并无区别,同样可以接收 `bot`, `event`, `state` 等参数,你可以把它当作一个普通的事件处理函数,但是去除了装饰器(没有使用 `matcher.handle()` 等来装饰),并且可以返回任何类型的值。 -并且返回了一个 `state` 的复制以及一个附加的键值 `depend` 。 + 在这里我们接受了 `event`,并以 `onebot` 的 `MessageEvent` 作为类型标注,返回一个新的字典,包括 `uid` 和 `nickname` 两个键值。 -### 导入 `Depends` - -```python {2} -from nonebot.log import logger -from nonebot.params import Depends -from nonebot import on_command, on_message - -test = on_command("123") - -def depend(state: dict): - # do something with state - return {**state, "depend": "depend"} - -@test.handle() -async def _(x: dict = Depends(depend)): - print(x) -``` - -### 在事件处理函数里声明依赖函数 - -与 FastAPI 类似,你可以在函数中添加一个新的参数,并且使用 `Depends` 来声明它的依赖。 - -```python {12} -from nonebot.log import logger -from nonebot.params import Depends -from nonebot import on_command, on_message - -test = on_command("123") - -def depend(state: dict): - # do something with state - return {**state, "depend": "depend"} - -@test.handle() -async def _(x: dict = Depends(depend)): - print(x) -``` - -你需要给 `Depends` 指定一个依赖函数,这个依赖函数的返回值会被作为 `x` 的值。 - -`Depends` 的首个参数即是依赖函数,或者其他 `Callable` 对象,在之后会对更多形式的依赖对象进行介绍。 +3. 在事件处理函数中声明依赖项。依赖项必须要 `Depends` 包裹依赖函数作为默认值。 :::tip -参数 `x` 的类型标注并不会影响事件处理函数的运行,类型检查并不会对依赖函数的返回值以及类型标注进行检查。 +请注意,参数 `x` 的类型标注将会影响到事件处理函数的运行,与类型标注不符的值将会导致事件处理函数被跳过。 ::: -当接收到事件时,NoneBot 会进行以下处理: +:::tip +事实上,bot、event、state 它们本身只是依赖注入的一个特例,它们无需声明这是依赖即可注入。 +::: + +虽然声明依赖项的方式和其他参数如 `bot`, `event` 并无二样,但他的参数有一些限制,必须是**可调用对象**,函数自然是可调用对象,类和生成器也是,我们会在接下来的小节说明。 + +一般来说,当接收到事件时,`NoneBot2` 会进行以下处理: + +1. 准备依赖函数所需要的参数。 +2. 调用依赖函数并获得返回值。 +3. 将返回值作为事件处理函数中的参数值传入。 + +## 依赖缓存 + +在使用 `Depends` 包裹依赖函数时,有一个参数 `use_cache` ,它默认为 `True` ,这个参数会决定 `Nonebot2` 在依赖注入的处理中是否使用缓存。 + +```python {11} +import random +from nonebot import on_command +from nonebot.params import Depends + +test = on_command("123") + +async def always_run(): + return random.randint(1, 100) + +@test.handle() +async def _(x: int = Depends(always_run, use_cache=False)): + print(x) +``` + +:::tip +缓存是针对单次事件处理来说的,在事件处理中 `Depends` 第一次被调用时,结果存入缓存,在之后都会直接返回缓存中的值,在事件处理结束后缓存就会被清除。 +::: + +当使用缓存时,依赖注入会这样处理: 1. 查询缓存,如果缓存中有相应的值,则直接返回。 2. 准备依赖函数所需要的参数。 @@ -102,8 +96,148 @@ async def _(x: dict = Depends(depend)): 4. 将返回值存入缓存。 5. 将返回值作为事件处理函数中的参数值传入。 -## 依赖缓存 +## 同步支持 + +我们在编写依赖函数时,可以简单地用同步函数,`NoneBot2` 的内部流程会进行处理: + +```python {2,8-9,12} +from nonebot.log import logger +from nonebot.params import Depends # 1.引用 Depends +from nonebot import on_command, on_message +from nonebot.adapters.onebot.v11 import MessageEvent + +test = on_command("123") + +def depend(event: MessageEvent): # 2.编写同步依赖函数 + return {"uid": event.get_user_id(), "nickname": event.sender.nickname} + +@test.handle() +async def _(x: dict = Depends(depend)): # 3.在事件处理函数里声明依赖项 + print(x["uid"], x["nickname"]) +``` ## Class 作为依赖 -## Generator 作为依赖 +我们可以看下面的代码段: + +```python +class A: + def __init__(self): + pass +a = A() +``` + +在我们实例化类 `A` 的时候,其实我们就在**调用**它,类本身也是一个**可调用对象**,所以类可以被 `Depends` 包裹成为依赖项。 + +因此我们对第一节的代码段做一下改造: + +```python {2,7-10,13} +from nonebot import on_command +from nonebot.params import Depends # 1.引用 Depends +from nonebot.adapters.onebot.v11 import MessageEvent + +test = on_command("123") + +class DependClass: # 2.编写依赖类 + def __init__(self, event: MessageEvent): + self.uid = event.get_user_id() + self.nickname = event.sender.nickname + +@test.handle() +async def _(x: DependClass = Depends(DependClass)): # 3.在事件处理函数里声明依赖项 + print(x.uid, x.nickname) +``` + +依然可以用三步说明如何用类作为依赖项: + +1. 引用 `Depends` 。 +2. 编写依赖类。类的 `__init__` 函数可以接收 `bot`, `event`, `state` 等参数,在这里我们接受了 `event`,并以 `onebot` 的 `MessageEvent` 作为类型标注。 +3. 在事件处理函数中声明依赖项。当用类作为依赖项时,它会是一个对应的实例,在这里 `x` 就是 `DependClass` 实例。 + +### 另一种依赖项声明方式 + +当使用类作为依赖项时,`Depends` 的参数可以为空,`NoneBot2` 会根据参数的类型标注进行推断并进行依赖注入。 + +```python +@test.handle() +async def _(x: DependClass = Depends()): # 在事件处理函数里声明依赖项 + print(x.uid, x.nickname) +``` + +## 生成器作为依赖 + +:::warning +`yield` 语句只能写一次,否则会引发异常。 +如果对此有疑问并想探究原因,可以看 [contextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.contextmanager) 和 [asynccontextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager) 文档,实际上,`Nonebot2` 的内部就使用了这两个装饰器。 +::: + +:::tips +生成器是 `Python` 高级特性,如果你对此处文档感到疑惑那说明暂时你还用不上这个功能。 +::: + +与 `FastAPI` 一样,`NoneBot2` 的依赖注入支持依赖项在事件处理结束后进行一些额外的工作,比如数据库 session 或者网络 IO 的关闭,互斥锁的解锁等等。 + +要实现上述功能,我们可以用生成器函数作为依赖项,我们用 `yield` 关键字取代 `return` 关键字,并在 `yield` 之后进行额外的工作。 + +我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO: + +```python {3,7-10,13} +import httpx +from nonebot import on_command +from nonebot.params import Depends # 1.引用 Depends + +test = on_command("123") + +async def get_client(): # 2.编写异步生成器函数 + async with httpx.AsyncClient() as client: + yield client + print("调用结束") + +@test.handle() +async def _(x: httpx.AsyncClient = Depends(get_client)): # 3.在事件处理函数里声明依赖项 + resp = await x.get("https://v2.nonebot.dev") + # do something +``` + +我们用 `yield` 代码段作为生成器函数的“返回”,在事件处理函数里用返回出来的 `client` 做自己需要的工作。在 `NoneBot2` 结束事件处理时,会执行 `yield` 之后的代码。 + +## 创造可调用对象作为依赖 + +:::tips +魔法方法 `__call__` 是 `Python` 高级特性,如果你对此处文档感到疑惑那说明暂时你还用不上这个功能。 +::: + +在 `Python` 的里,类的 `__call__` 方法会让类的实例变成**可调用对象**,我们可以利用这个魔法方法做一个简单的尝试: + +```python{3,9-14,16,19} +from typing import Type +from nonebot.log import logger +from nonebot.params import Depends # 1.引用 Depends +from nonebot import on_command +from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent + +test = on_command("123") + +class EventChecker: # 2.编写需要的类 + def __init__(self, EventClass: Type[MessageEvent]): + self.event_class = EventClass + + def __call__(self, event: MessageEvent) -> bool: + return isinstance(event, self.event_class) + +checker = EventChecker(GroupMessageEvent) # 3.将类实例化 + +@test.handle() +async def _(x: bool = Depends(checker)): # 4.在事件处理函数里声明依赖项 + if x: + print("这是群聊消息") + else: + print("这不是群聊消息") +``` + +这是判断 `onebot` 的消息事件是不是群聊消息事件的一个例子,我们可以用四步来说明这个例子: + +1. 引用 `Depends` 。 +2. 编写需要的类。类的 `__init__` 函数接收参数 `EventClass`,它将接收事件类本身。类的 `__call__` 函数将接受消息事件对象,并返回一个 `bool` 类型的判定结果。 +3. 将类实例化。我们传入群聊消息事件作为参数实例化 `checker` 。 +4. 在事件处理函数里声明依赖项。`NoneBot2` 将会调用 `checker` 的 `__call__` 方法,返回给参数 `x` 相应的判断结果。 diff --git a/website/docs/advanced/di/overload.md b/website/docs/advanced/di/overload.md index b5dcf069..9ed910d2 100644 --- a/website/docs/advanced/di/overload.md +++ b/website/docs/advanced/di/overload.md @@ -56,7 +56,7 @@ async def _(bot: Bot, event: PrivateMessageEvent): ## 进阶 -事件处理函数重载机制同样支持被 `matcher.got` 等装饰器装饰的函数。 例如: +事件处理函数重载机制同样支持被 `matcher.got` 等装饰器装饰的函数。例如: ```python @matcher.got("key1", prompt="群事件提问") @@ -70,3 +70,7 @@ async def _(bot: Bot, event: PrivateMessageEvent): ``` 只有触发事件符合的函数才会触发装饰器。 + +:::warning 注意 +bot 和 event 参数具有最高的检查优先级,因此,如果参数类型不符合,所有的依赖项 `Depends` 等都不会被执行。 +::: diff --git a/website/docs/advanced/di/sync-support.md b/website/docs/advanced/di/sync-support.md deleted file mode 100644 index 47d6f4df..00000000 --- a/website/docs/advanced/di/sync-support.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -sidebar_position: 2 -description: 同步函数作为依赖 - -options: - menu: - weight: 61 - category: advanced ---- - -# 同步支持 diff --git a/website/docs/advanced/permission.md b/website/docs/advanced/permission.md index ac1b586d..ef9a0701 100644 --- a/website/docs/advanced/permission.md +++ b/website/docs/advanced/permission.md @@ -15,19 +15,18 @@ options: ```python from nonebot.permission import SUPERUSER -from nonebot.adapters import Bot from nonebot import on_command matcher = on_command("测试超管", permission=SUPERUSER) @matcher.handle() -async def _(bot: Bot): +async def _(): await matcher.send("超管命令测试成功") @matcher.got("key1", "超管提问") -async def _(bot: Bot, event: Event): +async def _(): await matcher.send("超管命令 got 成功") ``` @@ -43,17 +42,16 @@ async def _(bot: Bot, event: Event): ```python from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot from nonebot.adapters.onebot.v11 import GroupMessageEvent from nonebot.adapters.onebot.v11 import GROUP_ADMIN, GROUP_OWNER matcher = on_command("测试权限") @matcher.handle() -async def _(bot: Bot, event: GroupMessageEvent): - if await GROUP_ADMIN(bot, event): +async def _(event: GroupMessageEvent): + if await GROUP_ADMIN(event): await matcher.send("管理员测试成功") - elif await GROUP_OWNER(bot, event): + elif await GROUP_OWNER(event): await matcher.send("群主测试成功") else: await matcher.send("群员测试成功") diff --git a/website/docs/advanced/runtime-hook.md b/website/docs/advanced/runtime-hook.md index 77b1f86c..1b77a7b5 100644 --- a/website/docs/advanced/runtime-hook.md +++ b/website/docs/advanced/runtime-hook.md @@ -94,7 +94,7 @@ async def handle_api_result(bot: Bot, exception: Optional[Exception], api: str, ## 事件钩子函数 -这些钩子函数指的是影响 NoneBot2 进行**事件处理**的函数。 +这些钩子函数指的是影响 NoneBot2 进行**事件处理**的函数, 这些函数可以认为跟普通的事件处理函数一样,接受相应的参数。 :::tip 提示 关于**事件处理**的流程,可以在[这里](./README.md)查阅。 @@ -111,7 +111,7 @@ from nonebot.exception import IgnoredException @event_preprocessor -async def do_something(bot: Bot, event: Event, state: T_State): +async def do_something(): raise IgnoredException("reason") ``` @@ -127,7 +127,7 @@ async def do_something(bot: Bot, event: Event, state: T_State): from nonebot.message import event_preprocessor @event_preprocessor -async def do_something(bot: Bot, event: Event, state: T_State): +async def do_something(): pass ``` @@ -139,7 +139,7 @@ async def do_something(bot: Bot, event: Event, state: T_State): from nonebot.message import event_postprocessor @event_postprocessor -async def do_something(bot: Bot, event: Event, state: T_State): +async def do_something(): pass ``` @@ -151,7 +151,7 @@ async def do_something(bot: Bot, event: Event, state: T_State): from nonebot.message import run_preprocessor @run_preprocessor -async def do_something(matcher: Matcher, bot: Bot, event: Event, state: T_State): +async def do_something(): pass ``` @@ -163,6 +163,6 @@ async def do_something(matcher: Matcher, bot: Bot, event: Event, state: T_State) from nonebot.message import run_postprocessor @run_postprocessor -async def do_something(matcher: Matcher, exception: Optional[Exception], bot: Bot, event: Event, state: T_State): +async def do_something(): pass ```