Compare commits

...

154 Commits

Author SHA1 Message Date
noneflow[bot]
f2b0b1752b 🔖 Release 2.0.1 2023-07-23 08:24:19 +00:00
Ju4tCode
81dcc65f99 🔖 bump version 2.0.1 (#2209) 2023-07-23 16:21:58 +08:00
noneflow[bot]
ac90df929e 📝 Update changelog 2023-07-21 14:38:47 +00:00
Tarrailt
555268239f 📝 Docs: 移动 Alconna 文档至最佳实践 (#2208)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-07-21 22:37:34 +08:00
noneflow[bot]
7009c8e8c1 📝 Update changelog 2023-07-21 12:53:17 +00:00
Jigsaw
2f3cc84f82 📝 Docs: 移除商店中不符合现规范的 tag (#2205) 2023-07-21 20:51:48 +08:00
noneflow[bot]
9444e01f0f 📝 Update changelog 2023-07-21 08:57:14 +00:00
NCBM
23b7a94b9a 🍻 publish plugin 方寸狭间 (#2206) 2023-07-21 08:56:06 +00:00
noneflow[bot]
70ece41b66 📝 Update changelog 2023-07-19 05:23:59 +00:00
Rockytkg
a5bb6e4220 🍻 publish plugin DALL-E绘图 (#2200) 2023-07-19 05:22:34 +00:00
noneflow[bot]
4fc99771c5 📝 Update changelog 2023-07-19 02:49:00 +00:00
canxin121
6601def5f7 🍻 publish plugin 指定戳一戳 (#2201) 2023-07-19 02:47:51 +00:00
noneflow[bot]
b2edea141e 📝 Update changelog 2023-07-19 02:15:58 +00:00
Ju4tCode
38886b9651 📝 Docs: 添加 scoped 插件配置指南 (#2198) 2023-07-19 10:14:36 +08:00
noneflow[bot]
1b225cbbca 📝 Update changelog 2023-07-18 05:36:22 +00:00
canxin121
b4f004c500 🍻 publish plugin templates_render (#2196) 2023-07-18 05:34:54 +00:00
noneflow[bot]
7a345714aa 📝 Update changelog 2023-07-17 12:43:28 +00:00
Ju4tCode
cb9fcae64c 🧑‍💻 Develop: 添加 Pyright 检查 (#2194) 2023-07-17 20:42:15 +08:00
noneflow[bot]
6ebeefed79 📝 Update changelog 2023-07-17 07:57:36 +00:00
Ju4tCode
6dc87a9455 use typing.override instead (#2193) 2023-07-17 15:56:27 +08:00
noneflow[bot]
7dd7c927bf 📝 Update changelog 2023-07-17 07:02:34 +00:00
Ju4tCode
e167865686 🐛 fix quart context error (#2192) 2023-07-17 15:01:21 +08:00
noneflow[bot]
29364679c4 📝 Update changelog 2023-07-16 13:43:30 +00:00
LambdaYH
ebbe8beec0 🍻 publish bot 米缸 (#2190) 2023-07-16 13:42:16 +00:00
noneflow[bot]
a04580e79e 📝 Update changelog 2023-07-14 16:28:15 +00:00
Well2333
bfe9e7e253 🍻 publish plugin MongoDB (#2188) 2023-07-14 16:26:50 +00:00
noneflow[bot]
720398198f 📝 Update changelog 2023-07-12 14:19:44 +00:00
Agnes4m
5ebf349886 🍻 publish plugin pjsk表情 (#2186) 2023-07-12 14:18:32 +00:00
noneflow[bot]
f8f5750c3b 📝 Update changelog 2023-07-11 15:16:11 +00:00
Q1351998764
8d9be61406 🍻 publish plugin nonebot-plugin-wenan (#2183) 2023-07-11 15:15:01 +00:00
noneflow[bot]
42ea650509 📝 Update changelog 2023-07-11 12:15:30 +00:00
mute23-code
a941a0f292 🍻 publish bot 林汐 (#2181) 2023-07-11 12:14:17 +00:00
noneflow[bot]
89f8745425 📝 Update changelog 2023-07-11 11:19:00 +00:00
Q1351998764
cc476528d8 🍻 publish plugin nonebot-plugin-picture-api (#2179) 2023-07-11 11:17:50 +00:00
noneflow[bot]
64f6c2dd4c 📝 Update changelog 2023-07-11 05:23:32 +00:00
MerCuJerry
81d9531b42 🍻 publish plugin Blocker (#2177) 2023-07-11 05:22:13 +00:00
noneflow[bot]
3512b0ab98 📝 Update changelog 2023-07-10 15:48:44 +00:00
Lptr-byte
ab3e916770 🍻 publish plugin nonebot-plugin-nobahpicture (#2175) 2023-07-10 15:47:23 +00:00
noneflow[bot]
21376a5bfa 📝 Update changelog 2023-07-08 07:35:00 +00:00
Akirami
5046b2a86e 📝 Docs: 钩子函数代码片段补充 (#2173) 2023-07-08 15:33:45 +08:00
noneflow[bot]
910c768910 📝 Update changelog 2023-07-08 07:27:52 +00:00
Akirami
5a526ddb40 📝 Docs: 格式化钩子函数中的代码片段 (#2172) 2023-07-08 15:26:30 +08:00
noneflow[bot]
4c5c97dca6 📝 Update changelog 2023-07-08 07:04:46 +00:00
Akirami
b3e0fb4830 ✏️ Plugin: 黑白名单添加标签 (#2170) 2023-07-08 15:03:35 +08:00
noneflow[bot]
258aa7d2d7 📝 Update changelog 2023-07-08 05:43:41 +00:00
A-kirami
5c72fd5ba7 🍻 publish plugin 过期事件过滤器 (#2168) 2023-07-08 05:42:28 +00:00
noneflow[bot]
26e4f23a67 📝 Update changelog 2023-07-08 03:41:39 +00:00
HuParry
28fc6c35f0 🍻 publish plugin 猫猫虫咖波图片发送 (#2166) 2023-07-08 03:40:25 +00:00
noneflow[bot]
3ef1d7d5d7 📝 Update changelog 2023-07-08 02:23:24 +00:00
Cypas
8474d8987e 🍻 publish plugin nonebot-plugin-splatoon3 (#2163) 2023-07-08 02:22:08 +00:00
noneflow[bot]
13ddfa1bdd 📝 Update changelog 2023-07-07 17:33:46 +00:00
coyude
ec8be10f26 🍻 publish plugin nonebot-plugin-cfassistant (#2162) 2023-07-07 17:32:16 +00:00
noneflow[bot]
511c521a68 📝 Update changelog 2023-07-07 13:28:22 +00:00
HuParry
0ef5940d0f 🍻 publish plugin 算法竞赛比赛查询 (#2158) 2023-07-07 13:26:51 +00:00
noneflow[bot]
eecc881cd8 📝 Update changelog 2023-07-07 03:29:19 +00:00
eya46
770141cf0a 📝 Docs: 补充 Message.only 文档 (#2155) 2023-07-07 11:28:08 +08:00
noneflow[bot]
b2b20ffc4a 📝 Update changelog 2023-07-06 07:29:30 +00:00
eya46
94a6067a4b Feature: 补充响应器组属性 (#2154)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-06 15:18:08 +08:00
noneflow[bot]
77220d9d1f 📝 Update changelog 2023-07-05 15:57:33 +00:00
djkcyl
647ad9ff8f 🍻 publish plugin nonebot-plugin-update (#2152) 2023-07-05 15:56:15 +00:00
pre-commit-ci[bot]
04182eefba ⬆️ auto update by pre-commit hooks (#2149)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-04 16:39:12 +08:00
noneflow[bot]
7b4aa08c54 📝 Update changelog 2023-07-04 02:47:02 +00:00
eya46
0033d7c686 🐛 Fix: 修复 dotenv 配置项为 None 将会跳过赋值 (#2143)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-07-04 10:45:55 +08:00
noneflow[bot]
c40b95f3e9 📝 Update changelog 2023-07-03 02:29:35 +00:00
Fireinsect
1fa44ca5c1 ✏️ Plugin: 修改 nonebot-plugin-ocgbot-v2 插件名称 (#2147) 2023-07-03 10:28:27 +08:00
noneflow[bot]
381f6633f6 📝 Update changelog 2023-07-03 02:18:44 +00:00
Agnes4m
d617508e32 🍻 publish plugin 远程同意好友 (#2145) 2023-07-03 02:17:36 +00:00
noneflow[bot]
8248e88686 📝 Update changelog 2023-07-02 14:28:26 +00:00
canxin
25649373a6 ✏️ Plugin: 更新 SparkGPT 插件描述 (#2144)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-02 22:27:13 +08:00
noneflow[bot]
3bee189598 📝 Update changelog 2023-07-01 07:41:39 +00:00
eya46
c1b1742b20 Feature: CommandGroup 支持命令别名添加前缀选项 (#2134)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-01 15:40:30 +08:00
noneflow[bot]
3e826cab72 📝 Update changelog 2023-07-01 05:55:19 +00:00
Fireinsect
4ef4bb0042 ✏️ Plugin: 修改 nonebot-plugin-ocgbot-v2 插件名称 (#2141) 2023-07-01 13:54:09 +08:00
Agnes4m
25ac653623 🍻 publish plugin 戳一戳事件 (#2138) 2023-06-30 12:45:13 +00:00
noneflow[bot]
b35bdfe6dc 📝 Update changelog 2023-06-30 12:44:37 +00:00
17TheWord
f06efca8cc 📝 Docs: 修复日志自定义文档 typo (#2140) 2023-06-30 20:43:26 +08:00
noneflow[bot]
a899523607 📝 Update changelog 2023-06-29 02:27:59 +00:00
lgc2333
2c162335cb 🍻 publish plugin EitherChoice (#2136) 2023-06-29 02:26:32 +00:00
noneflow[bot]
3a12984d4b 📝 Update changelog 2023-06-28 12:23:53 +00:00
MeetWq
7211f24a7d 🍻 publish plugin 用户信息 (#2132) 2023-06-28 12:22:35 +00:00
noneflow[bot]
649624ed80 📝 Update changelog 2023-06-27 08:26:35 +00:00
worldmozara
c03ff4e676 Feature: 添加用于动态继承支持适配器数据的方法 (#2127) 2023-06-27 16:25:27 +08:00
noneflow[bot]
0b5a18cb63 📝 Update changelog 2023-06-27 02:13:28 +00:00
wsdtl
518bf16082 🍻 publish bot web_bot (#2129) 2023-06-27 02:12:19 +00:00
noneflow[bot]
b625a5d19a 📝 Update changelog 2023-06-26 05:27:55 +00:00
kexue
acca22e179 ✏️ Plugin: 删除 nonebot-plugin-phlogo (#2128) 2023-06-26 13:26:26 +08:00
noneflow[bot]
a3009d45dc 📝 Update changelog 2023-06-25 15:02:27 +00:00
QBkira
fd3d1bb115 🍻 publish plugin Diablo4地狱狂潮boss提醒小助手 (#2121) 2023-06-25 15:01:22 +00:00
noneflow[bot]
7282da8b04 📝 Update changelog 2023-06-25 03:30:28 +00:00
eya46
7a3c7476fb 📝 Docs: 修复依赖注入文档 ArgStr 3.9+ 和 3.8+ 版本代码写反 (#2126)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-25 11:29:10 +08:00
noneflow[bot]
f1046cfb11 📝 Update changelog 2023-06-24 11:19:31 +00:00
eya46
8de25447b3 🐛 Fix: 修复 ArgParam 不支持 Annotated (#2124)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-24 19:18:24 +08:00
noneflow[bot]
3cdbf35dc6 📝 Update changelog 2023-06-24 09:06:04 +00:00
Agnes Digital
0228e255e1 ✏️ Plugin: 修改 nonebot-plugin-gw2 模块名 (#2123)
Co-authored-by: Agnes Digital <Z735803792@163.com>
2023-06-24 17:04:50 +08:00
noneflow[bot]
353d16ebfd 📝 Update changelog 2023-06-24 06:48:39 +00:00
Ju4tCode
3d5dd5969c 🚨 Develop: 添加 ruff linter (#2114) 2023-06-24 14:47:35 +08:00
noneflow[bot]
fe21cbfa1d 📝 Update changelog 2023-06-23 14:00:26 +00:00
fireinsect
c20f65636f 🍻 publish plugin nonbot-plugin-ocgbot-v2 (#2118) 2023-06-23 13:59:13 +00:00
noneflow[bot]
eade8face6 📝 Update changelog 2023-06-23 12:28:20 +00:00
worldmozara
ab75133e9d ✏️ Plugin: 更新 nonebot-plugin-msgbuf 插件的名称等信息 (#2119) 2023-06-23 20:27:08 +08:00
noneflow[bot]
89596fb708 📝 Update changelog 2023-06-22 02:42:23 +00:00
ssttkkl
eedcf0779d 🍻 publish plugin 错误告警 (#2116) 2023-06-22 02:41:06 +00:00
noneflow[bot]
05333260b7 📝 Update changelog 2023-06-21 05:28:42 +00:00
Agnes Digital
55fd447230 ✏️ Plugin: 修改插件信息和仓库地址 (#2115)
Co-authored-by: Agnes Digital <Z735803792@163.com>
2023-06-21 13:27:37 +08:00
noneflow[bot]
263e6b25e2 📝 Update changelog 2023-06-20 05:51:13 +00:00
Ju4tCode
e00890033e add plugin metadata to builtin plugins (#2113) 2023-06-20 13:50:05 +08:00
noneflow[bot]
20d3d62bd5 📝 Update changelog 2023-06-19 09:50:07 +00:00
Ju4tCode
080b876d93 👷 Test: 移除 httpbin 并整理测试 (#2110) 2023-06-19 17:48:59 +08:00
noneflow[bot]
27a3d1f0bb 📝 Update changelog 2023-06-19 08:48:59 +00:00
CMHopeSunshine
7a47985c2b 🍻 publish plugin follow_withdraw (#2111) 2023-06-19 08:47:51 +00:00
noneflow[bot]
8d97081948 📝 Update changelog 2023-06-18 13:17:46 +00:00
ThirdBlood
f4ffa07c8b 🍻 publish bot ReimeiBot-黎明机器人 (#2106) 2023-06-18 13:16:43 +00:00
noneflow[bot]
1b1ddc5c0f 📝 Update changelog 2023-06-14 09:07:00 +00:00
uy/sun
30dbd270a6 👷 CI: 缓存 NoneFlow 所需的 pre-commit hooks (#2104) 2023-06-14 17:05:52 +08:00
noneflow[bot]
7d3c7c4933 📝 Update changelog 2023-06-14 05:05:52 +00:00
0Neptune0
8c8436a94f 🍻 publish plugin 战雷查水表 (#2102) 2023-06-14 05:04:44 +00:00
noneflow[bot]
8601942ed3 📝 Update changelog 2023-06-13 13:46:01 +00:00
pre-commit-ci[bot]
4cc958ca17 🚨 auto fix by pre-commit hooks 2023-06-13 13:44:48 +00:00
SuperGuGuGu
472a2c7866 🍻 publish plugin bili_push (#2100) 2023-06-13 13:44:48 +00:00
noneflow[bot]
222609182e 📝 Update changelog 2023-06-12 13:19:50 +00:00
forchannot
dccf2f3ca8 🔥 Docs: 删除商店插件发布多余模块 (#2095)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-12 21:18:25 +08:00
noneflow[bot]
156807c365 📝 Update changelog 2023-06-12 12:40:57 +00:00
worldmozara
50941f5259 📝 Docs: 微调插件元数据的部分描述 (#2096) 2023-06-12 20:39:28 +08:00
noneflow[bot]
2de1524a89 📝 Update changelog 2023-06-11 15:49:49 +00:00
uy/sun
bdd17b62cc Feature: 插件商店适配最新的插件元数据 (#2094)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-11 23:48:37 +08:00
noneflow[bot]
3a9e800a58 📝 Update changelog 2023-06-11 15:42:26 +00:00
worldmozara
cb8d48c362 📝 Docs: 完成发布插件教程 (#2078)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
Co-authored-by: uy/sun <hmy0119@hotmail.com>
2023-06-11 23:41:16 +08:00
noneflow[bot]
a5981c05d5 📝 Update changelog 2023-06-11 13:06:02 +00:00
worldmozara
4cb87e596d 📝 Docs: 更新插件元数据的相关描述 (#2087) 2023-06-11 21:04:58 +08:00
noneflow[bot]
2725a0a324 📝 Update changelog 2023-06-11 07:34:45 +00:00
Ju4tCode
f6b0809e5f Feature: 依赖注入支持 Generic TypeVar 和 Matcher 重载 (#2089) 2023-06-11 15:33:33 +08:00
noneflow[bot]
6181c1760f 📝 Update changelog 2023-06-11 07:00:08 +00:00
Jigsaw
324277091c 🐛 Fix: aiohttp 请求时 data 和 file 不能同时存在 (#2088) 2023-06-11 14:59:05 +08:00
noneflow[bot]
6eef863b70 📝 Update changelog 2023-06-11 06:50:18 +00:00
Alpaca4610
7d52f5af4d 🍻 publish plugin AI作曲 (#2092) 2023-06-11 06:48:53 +00:00
noneflow[bot]
0a70721ec0 📝 Update changelog 2023-06-11 04:47:10 +00:00
reine-ishyanami
f430f061ec 🍻 publish plugin pcrjjc (#2090) 2023-06-11 04:46:06 +00:00
noneflow[bot]
572be1eb47 📝 Update changelog 2023-06-07 09:33:36 +00:00
惜月
29cf7de1a6 📝 add Villa adapter to README (#2086) 2023-06-07 17:32:31 +08:00
noneflow[bot]
c61e3cab90 📝 Update changelog 2023-06-06 16:23:08 +00:00
CMHopeSunshine
77bdc5ecba 🍻 publish adapter 大别野 (#2084) 2023-06-06 16:22:06 +00:00
pre-commit-ci[bot]
16054d18c6 ⬆️ auto update by pre-commit hooks (#2083)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-06 17:22:45 +08:00
noneflow[bot]
f0361295c3 📝 Update changelog 2023-06-05 09:18:27 +00:00
nek0us
9bd1964ae2 🍻 publish plugin twitter订阅 (#2081) 2023-06-05 09:17:09 +00:00
noneflow[bot]
9141c88f77 📝 Update changelog 2023-06-03 14:46:48 +00:00
DiheChen
491855876b 🐛 Fix: 修复因 loguru 更新导致的启动和关闭日志 name 不正常 (#2080)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-06-03 22:45:46 +08:00
noneflow[bot]
6df28dd2a8 📝 Update changelog 2023-06-03 03:28:06 +00:00
ssttkkl
142d0f4d95 🍻 publish plugin 链接防夹 (#2073) 2023-06-03 03:27:04 +00:00
noneflow[bot]
0127d765ae 📝 Update changelog 2023-06-02 10:14:15 +00:00
Bill Chen
207c6b3c15 ✏️ Plugin: 移除过时未更新的插件&Bot (#2072) 2023-06-02 18:13:04 +08:00
noneflow[bot]
d2e699a13a 📝 Update changelog 2023-06-02 10:10:24 +00:00
Agnes4m
ce9ba7dd9b 🍻 publish plugin 碧蓝航线攻略 (#2075) 2023-06-02 10:09:13 +00:00
noneflow[bot]
2af23c9d89 📝 Update changelog 2023-06-01 15:55:58 +00:00
BalconyJH
8ee0f5efc4 ✏️ Plugin: 删除插件 nonebot_plugin_r6s (#2071) 2023-06-01 23:54:46 +08:00
181 changed files with 20322 additions and 2917 deletions

View File

@@ -10,9 +10,11 @@
"settings": { "settings": {
"python.analysis.diagnosticMode": "workspace", "python.analysis.diagnosticMode": "workspace",
"python.analysis.typeCheckingMode": "basic", "python.analysis.typeCheckingMode": "basic",
"ruff.organizeImports": false,
"[python]": { "[python]": {
"editor.defaultFormatter": "ms-python.black-formatter", "editor.defaultFormatter": "ms-python.black-formatter",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.ruff": true,
"source.organizeImports": true "source.organizeImports": true
} }
}, },
@@ -44,6 +46,7 @@
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"ms-python.isort", "ms-python.isort",
"ms-python.black-formatter", "ms-python.black-formatter",
"charliermarsh.ruff",
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss" "bradlc.vscode-tailwindcss"

View File

@@ -3,22 +3,6 @@ title: "Plugin: {name}"
description: 发布插件到 NoneBot 官方商店 description: 发布插件到 NoneBot 官方商店
labels: ["Plugin"] labels: ["Plugin"]
body: body:
- type: input
id: name
attributes:
label: 插件名称
description: 插件名称
validations:
required: true
- type: input
id: description
attributes:
label: 插件描述
description: 插件描述
validations:
required: true
- type: input - type: input
id: pypi id: pypi
attributes: attributes:
@@ -37,15 +21,6 @@ body:
validations: validations:
required: true required: true
- type: input
id: homepage
attributes:
label: 插件项目仓库/主页链接
description: 插件项目仓库/主页链接
placeholder: e.g. https://github.com/xxx/xxx
validations:
required: true
- type: input - type: input
id: tags id: tags
attributes: attributes:
@@ -55,3 +30,14 @@ body:
value: "[]" value: "[]"
validations: validations:
required: true required: true
- type: textarea
id: config
attributes:
label: 插件配置项
description: 插件配置项
render: dotenv
placeholder: |
# e.g.
# KEY=VALUE
# KEY2=VALUE2

View File

@@ -12,7 +12,7 @@ on:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }} group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}
cancel-in-progress: true cancel-in-progress: false
jobs: jobs:
plugin_test: plugin_test:
@@ -40,6 +40,7 @@ jobs:
outputs: outputs:
result: ${{ steps.plugin-test.outputs.RESULT }} result: ${{ steps.plugin-test.outputs.RESULT }}
output: ${{ steps.plugin-test.outputs.OUTPUT }} output: ${{ steps.plugin-test.outputs.OUTPUT }}
metadata: ${{ steps.plugin-test.outputs.METADATA }}
steps: steps:
- name: Install Poetry - name: Install Poetry
if: ${{ !startsWith(github.event_name, 'pull_request') }} if: ${{ !startsWith(github.event_name, 'pull_request') }}
@@ -71,6 +72,12 @@ jobs:
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
- name: Cache pre-commit hooks
uses: actions/cache@v3
with:
path: .cache/.pre-commit
key: noneflow-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
- name: NoneFlow - name: NoneFlow
uses: docker://ghcr.io/nonebot/noneflow:latest uses: docker://ghcr.io/nonebot/noneflow:latest
with: with:
@@ -84,5 +91,10 @@ jobs:
env: env:
PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }} PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }}
PLUGIN_TEST_OUTPUT: ${{ needs.plugin_test.outputs.output }} PLUGIN_TEST_OUTPUT: ${{ needs.plugin_test.outputs.output }}
PLUGIN_TEST_METADATA: ${{ needs.plugin_test.outputs.metadata }}
APP_ID: ${{ secrets.APP_ID }} APP_ID: ${{ secrets.APP_ID }}
PRIVATE_KEY: ${{ secrets.APP_KEY }} PRIVATE_KEY: ${{ secrets.APP_KEY }}
PRE_COMMIT_HOME: /github/workspace/.cache/.pre-commit
- name: Fix permission
run: sudo chown -R $(whoami):$(id -ng) .cache/.pre-commit

26
.github/workflows/pyright.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Pyright Lint
on:
push:
branches:
- master
pull_request:
paths:
- "nonebot/**"
- "packages/**"
- "tests/**"
jobs:
pyright:
name: Pyright Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python environment
uses: ./.github/actions/setup-python
- run: echo "$(poetry env info --path)/bin" >> $GITHUB_PATH
- name: Run Pyright
uses: jakebailey/pyright-action@v1

21
.github/workflows/ruff.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Ruff Lint
on:
push:
branches:
- master
pull_request:
paths:
- "nonebot/**"
- "packages/**"
- "tests/**"
jobs:
ruff:
name: Ruff Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Ruff Lint
uses: chartboost/ruff-action@v1

View File

@@ -6,11 +6,12 @@ ci:
autoupdate_schedule: monthly autoupdate_schedule: monthly
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks" autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
repos: repos:
- repo: https://github.com/hadialqattan/pycln - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v2.1.3 rev: v0.0.276
hooks: hooks:
- id: pycln - id: ruff
args: [--config, pyproject.toml] args: [--fix, --exit-non-zero-on-fix]
stages: [commit]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
rev: 5.12.0 rev: 5.12.0

View File

@@ -19,9 +19,19 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
<img src="https://img.shields.io/github/license/nonebot/nonebot2" alt="license"> <img src="https://img.shields.io/github/license/nonebot/nonebot2" alt="license">
</a> </a>
<a href="https://pypi.python.org/pypi/nonebot2"> <a href="https://pypi.python.org/pypi/nonebot2">
<img src="https://img.shields.io/pypi/v/nonebot2" alt="pypi"> <img src="https://img.shields.io/pypi/v/nonebot2?logo=python&logoColor=edb641" alt="pypi">
</a> </a>
<img src="https://img.shields.io/badge/python-3.8+-blue" alt="python"> <img src="https://img.shields.io/badge/python-3.8+-blue?logo=python&logoColor=edb641" alt="python">
<a href="https://github.com/psf/black">
<img src="https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=edb641" alt="black">
</a>
<a href="https://github.com/Microsoft/pyright">
<img src="https://img.shields.io/badge/types-pyright-797952.svg?logo=python&logoColor=edb641" alt="pyright">
</a>
<a href="https://github.com/astral-sh/ruff">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="ruff">
</a>
<br />
<a href="https://codecov.io/gh/nonebot/nonebot2"> <a href="https://codecov.io/gh/nonebot/nonebot2">
<img src="https://codecov.io/gh/nonebot/nonebot2/branch/master/graph/badge.svg?token=2P0G0VS7N4" alt="codecov"/> <img src="https://codecov.io/gh/nonebot/nonebot2/branch/master/graph/badge.svg?token=2P0G0VS7N4" alt="codecov"/>
</a> </a>
@@ -31,6 +41,12 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
<a href="https://results.pre-commit.ci/latest/github/nonebot/nonebot2/master"> <a href="https://results.pre-commit.ci/latest/github/nonebot/nonebot2/master">
<img src="https://results.pre-commit.ci/badge/github/nonebot/nonebot2/master.svg" alt="pre-commit" /> <img src="https://results.pre-commit.ci/badge/github/nonebot/nonebot2/master.svg" alt="pre-commit" />
</a> </a>
<a href="https://github.com/nonebot/nonebot2/actions/workflows/pyright.yml">
<img src="https://github.com/nonebot/nonebot2/actions/workflows/pyright.yml/badge.svg?branch=master&event=push" alt="pyright">
</a>
<a href="https://github.com/nonebot/nonebot2/actions/workflows/ruff.yml">
<img src="https://github.com/nonebot/nonebot2/actions/workflows/ruff.yml/badge.svg?branch=master&event=push" alt="ruff">
</a>
<br /> <br />
<a href="https://onebot.dev/"> <a href="https://onebot.dev/">
<img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot"> <img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
@@ -109,6 +125,7 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
| MineCraft[仓库](https://github.com/17TheWord/nonebot-adapter-minecraft) | ↗️ | 由社区贡献 | | MineCraft[仓库](https://github.com/17TheWord/nonebot-adapter-minecraft) | ↗️ | 由社区贡献 |
| BiliBili Live[仓库](https://github.com/wwweww/adapter-bilibili) | ↗️ | 由社区贡献 | | BiliBili Live[仓库](https://github.com/wwweww/adapter-bilibili) | ↗️ | 由社区贡献 |
| Walle-Q[仓库](https://github.com/onebot-walle/nonebot_adapter_walleq) | ↗️ | QQ 协议,由社区贡献 | | Walle-Q[仓库](https://github.com/onebot-walle/nonebot_adapter_walleq) | ↗️ | QQ 协议,由社区贡献 |
| Villa[仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa) | ↗️ | 米游社大别野 Bot 协议,由社区贡献 |
- 坚实后盾:支持多种 web 框架,可自定义替换、组合 - 坚实后盾:支持多种 web 框架,可自定义替换、组合

View File

@@ -24,12 +24,17 @@
- `load_all_plugins` => {ref}``load_all_plugins` <nonebot.plugin.load.load_all_plugins>` - `load_all_plugins` => {ref}``load_all_plugins` <nonebot.plugin.load.load_all_plugins>`
- `load_from_json` => {ref}``load_from_json` <nonebot.plugin.load.load_from_json>` - `load_from_json` => {ref}``load_from_json` <nonebot.plugin.load.load_from_json>`
- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>` - `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`
- `load_builtin_plugin` => {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>` - `load_builtin_plugin` =>
- `load_builtin_plugins` => {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>` {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`
- `load_builtin_plugins` =>
{ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
- `get_plugin` => {ref}``get_plugin` <nonebot.plugin.get_plugin>` - `get_plugin` => {ref}``get_plugin` <nonebot.plugin.get_plugin>`
- `get_plugin_by_module_name` => {ref}``get_plugin_by_module_name` <nonebot.plugin.get_plugin_by_module_name>` - `get_plugin_by_module_name` =>
- `get_loaded_plugins` => {ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>` {ref}``get_plugin_by_module_name` <nonebot.plugin.get_plugin_by_module_name>`
- `get_available_plugin_names` => {ref}``get_available_plugin_names` <nonebot.plugin.get_available_plugin_names>` - `get_loaded_plugins` =>
{ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>`
- `get_available_plugin_names` =>
{ref}``get_available_plugin_names` <nonebot.plugin.get_available_plugin_names>`
- `require` => {ref}``require` <nonebot.plugin.load.require>` - `require` => {ref}``require` <nonebot.plugin.load.require>`
FrontMatter: FrontMatter:
@@ -69,7 +74,8 @@ def get_driver() -> Driver:
全局 {ref}`nonebot.drivers.Driver` 对象 全局 {ref}`nonebot.drivers.Driver` 对象
异常: 异常:
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用) ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
({ref}`nonebot.init <nonebot.init>` 尚未调用)
用法: 用法:
```python ```python
@@ -83,23 +89,33 @@ def get_driver() -> Driver:
@overload @overload
def get_adapter(name: str) -> Adapter: def get_adapter(name: str) -> Adapter:
... """
参数:
name: 适配器名称
返回:
指定名称的 {ref}`nonebot.adapters.Adapter` 对象
"""
@overload @overload
def get_adapter(name: Type[A]) -> A: def get_adapter(name: Type[A]) -> A:
... """
参数:
name: 适配器类型
返回:
指定类型的 {ref}`nonebot.adapters.Adapter` 对象
"""
def get_adapter(name: Union[str, Type[Adapter]]) -> Adapter: def get_adapter(name: Union[str, Type[Adapter]]) -> Adapter:
"""获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。 """获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。
返回:
指定名称或类型的 {ref}`nonebot.adapters.Adapter` 对象
异常: 异常:
ValueError: 指定的 {ref}`nonebot.adapters.Adapter` 未注册 ValueError: 指定的 {ref}`nonebot.adapters.Adapter` 未注册
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用) ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
({ref}`nonebot.init <nonebot.init>` 尚未调用)
用法: 用法:
```python ```python
@@ -121,7 +137,8 @@ def get_adapters() -> Dict[str, Adapter]:
所有 {ref}`nonebot.adapters.Adapter` 实例字典 所有 {ref}`nonebot.adapters.Adapter` 实例字典
异常: 异常:
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用) ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
({ref}`nonebot.init <nonebot.init>` 尚未调用)
用法: 用法:
```python ```python
@@ -139,7 +156,8 @@ def get_app() -> Any:
异常: 异常:
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ReverseDriver` 类型 AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ReverseDriver` 类型
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用) ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
({ref}`nonebot.init <nonebot.init>` 尚未调用)
用法: 用法:
```python ```python
@@ -154,14 +172,16 @@ def get_app() -> Any:
def get_asgi() -> Any: def get_asgi() -> Any:
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应 [ASGI](https://asgi.readthedocs.io/) 对象。 """获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应
[ASGI](https://asgi.readthedocs.io/) 对象。
返回: 返回:
ASGI 对象 ASGI 对象
异常: 异常:
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ReverseDriver` 类型 AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ReverseDriver` 类型
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用) ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
({ref}`nonebot.init <nonebot.init>` 尚未调用)
用法: 用法:
```python ```python
@@ -182,7 +202,8 @@ def get_bot(self_id: Optional[str] = None) -> Bot:
当不提供时,返回一个 {ref}`nonebot.adapters.Bot`。 当不提供时,返回一个 {ref}`nonebot.adapters.Bot`。
参数: 参数:
self_id: 用来识别 {ref}`nonebot.adapters.Bot` 的 {ref}`nonebot.adapters.Bot.self_id` 属性 self_id: 用来识别 {ref}`nonebot.adapters.Bot` 的
{ref}`nonebot.adapters.Bot.self_id` 属性
返回: 返回:
{ref}`nonebot.adapters.Bot` 对象 {ref}`nonebot.adapters.Bot` 对象
@@ -190,7 +211,8 @@ def get_bot(self_id: Optional[str] = None) -> Bot:
异常: 异常:
KeyError: 对应 self_id 的 Bot 不存在 KeyError: 对应 self_id 的 Bot 不存在
ValueError: 没有传入 self_id 且没有 Bot 可用 ValueError: 没有传入 self_id 且没有 Bot 可用
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用) ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
({ref}`nonebot.init <nonebot.init>` 尚未调用)
用法: 用法:
```python ```python
@@ -213,10 +235,12 @@ def get_bots() -> Dict[str, Bot]:
"""获取所有连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。 """获取所有连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。
返回: 返回:
一个以 {ref}`nonebot.adapters.Bot.self_id` 为键{ref}`nonebot.adapters.Bot` 对象为值的字典 一个以 {ref}`nonebot.adapters.Bot.self_id` 为键
{ref}`nonebot.adapters.Bot` 对象为值的字典
异常: 异常:
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用) ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
({ref}`nonebot.init <nonebot.init>` 尚未调用)
用法: 用法:
```python ```python

View File

@@ -1,19 +1,23 @@
"""本模块定义了 NoneBot 本身运行所需的配置项。 """本模块定义了 NoneBot 本身运行所需的配置项。
NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。 NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及
[`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。
配置项需符合特殊格式或 json 序列化格式。详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。 配置项需符合特殊格式或 json 序列化格式
详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。
FrontMatter: FrontMatter:
sidebar_position: 1 sidebar_position: 1
description: nonebot.config 模块 description: nonebot.config 模块
""" """
import os import os
from datetime import timedelta from datetime import timedelta
from ipaddress import IPv4Address from ipaddress import IPv4Address
from typing import TYPE_CHECKING, Any, Set, Dict, Tuple, Union, Mapping, Optional from typing import TYPE_CHECKING, Any, Set, Dict, Tuple, Union, Mapping, Optional
from pydantic.utils import deep_update from pydantic.utils import deep_update
from pydantic.fields import Undefined, UndefinedType
from pydantic import Extra, Field, BaseSettings, IPvAnyAddress from pydantic import Extra, Field, BaseSettings, IPvAnyAddress
from pydantic.env_settings import ( from pydantic.env_settings import (
DotenvType, DotenvType,
@@ -28,9 +32,8 @@ from nonebot.log import logger
class CustomEnvSettings(EnvSettingsSource): class CustomEnvSettings(EnvSettingsSource):
def __call__(self, settings: BaseSettings) -> Dict[str, Any]: def __call__(self, settings: BaseSettings) -> Dict[str, Any]:
""" """从环境变量和 dotenv 配置文件中读取配置项。"""
Build environment variables suitable for passing to the Model.
"""
d: Dict[str, Any] = {} d: Dict[str, Any] = {}
if settings.__config__.case_sensitive: if settings.__config__.case_sensitive:
@@ -42,21 +45,25 @@ class CustomEnvSettings(EnvSettingsSource):
env_vars = {**env_file_vars, **env_vars} env_vars = {**env_file_vars, **env_vars}
for field in settings.__fields__.values(): for field in settings.__fields__.values():
env_val: Optional[str] = None env_val: Union[str, None, UndefinedType] = Undefined
for env_name in field.field_info.extra["env_names"]: for env_name in field.field_info.extra["env_names"]:
env_val = env_vars.get(env_name) env_val = env_vars.get(env_name, Undefined)
if env_name in env_file_vars: if env_name in env_file_vars:
del env_file_vars[env_name] del env_file_vars[env_name]
if env_val is not None: if env_val is not Undefined:
break break
is_complex, allow_parse_failure = self.field_is_complex(field) is_complex, allow_parse_failure = self.field_is_complex(field)
if is_complex: if is_complex:
if env_val is None: if isinstance(env_val, UndefinedType):
# field is complex but no value found so far, try explode_env_vars
if env_val_built := self.explode_env_vars(field, env_vars): if env_val_built := self.explode_env_vars(field, env_vars):
d[field.alias] = env_val_built d[field.alias] = env_val_built
elif env_val is None:
d[field.alias] = env_val
else: else:
# field is complex and there's a value, decode that as JSON, then add explode_env_vars # field is complex and there's a value
# decode that as JSON, then add explode_env_vars
try: try:
env_val = settings.__config__.parse_env_var(field.name, env_val) env_val = settings.__config__.parse_env_var(field.name, env_val)
except ValueError as e: except ValueError as e:
@@ -71,8 +78,9 @@ class CustomEnvSettings(EnvSettingsSource):
) )
else: else:
d[field.alias] = env_val d[field.alias] = env_val
elif env_val is not None: elif not isinstance(env_val, UndefinedType):
# simplest case, field is not complex, we only need to add the value if it was found # simplest case, field is not complex
# we only need to add the value if it was found
d[field.alias] = env_val d[field.alias] = env_val
# remain user custom config # remain user custom config
@@ -82,7 +90,7 @@ class CustomEnvSettings(EnvSettingsSource):
# there's a value, decode that as JSON # there's a value, decode that as JSON
try: try:
env_val = settings.__config__.parse_env_var(env_name, val_striped) env_val = settings.__config__.parse_env_var(env_name, val_striped)
except ValueError as e: except ValueError:
logger.trace( logger.trace(
"Error while parsing JSON for " "Error while parsing JSON for "
f"{env_name!r}={val_striped!r}. " f"{env_name!r}={val_striped!r}. "
@@ -139,7 +147,7 @@ class BaseConfig(BaseSettings):
class Env(BaseConfig): class Env(BaseConfig):
"""运行环境配置。大小写不敏感。 """运行环境配置。大小写不敏感。
将会从 `环境变量` > `.env 环境配置文件` 的优先级读取环境信息。 将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。
""" """
environment: str = "prod" environment: str = "prod"
@@ -170,15 +178,17 @@ class Config(BaseConfig):
配置格式为 `<module>[:<Driver>][+<module>[:<Mixin>]]*`。 配置格式为 `<module>[:<Driver>][+<module>[:<Mixin>]]*`。
`~` 为 `nonebot.drivers.` 的缩写。 `~` 为 `nonebot.drivers.` 的缩写。
配置方法参考: [配置驱动器](https://nonebot.dev/docs/advanced/driver#%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E5%99%A8)
""" """
host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。""" """NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。"""
port: int = Field(default=8080, ge=1, le=65535) port: int = Field(default=8080, ge=1, le=65535)
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。""" """NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。"""
log_level: Union[int, str] = "INFO" log_level: Union[int, str] = "INFO"
"""NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称 """NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称
参考 [`loguru 日志等级`](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。 参考 [记录日志](https://nonebot.dev/docs/appendices/log)[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。
:::tip 提示 :::tip 提示
日志等级名称应为大写,如 `INFO`。 日志等级名称应为大写,如 `INFO`。
@@ -209,6 +219,8 @@ class Config(BaseConfig):
command_start: Set[str] = {"/"} command_start: Set[str] = {"/"}
"""命令的起始标记,用于判断一条消息是不是命令。 """命令的起始标记,用于判断一条消息是不是命令。
参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。
用法: 用法:
```conf ```conf
COMMAND_START=["/", ""] COMMAND_START=["/", ""]
@@ -217,6 +229,8 @@ class Config(BaseConfig):
command_sep: Set[str] = {"."} command_sep: Set[str] = {"."}
"""命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。 """命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。
参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。
用法: 用法:
```conf ```conf
COMMAND_SEP=["."] COMMAND_SEP=["."]

View File

@@ -4,6 +4,7 @@ FrontMatter:
sidebar_position: 9 sidebar_position: 9
description: nonebot.consts 模块 description: nonebot.consts 模块
""" """
import os import os
import sys import sys
from typing import Literal from typing import Literal

View File

@@ -82,8 +82,8 @@ class Dependent(Generic[R]):
""" """
call: _DependentCallable[R] call: _DependentCallable[R]
params: Tuple[ModelField] = field(default_factory=tuple) params: Tuple[ModelField, ...] = field(default_factory=tuple)
parameterless: Tuple[Param] = field(default_factory=tuple) parameterless: Tuple[Param, ...] = field(default_factory=tuple)
def __repr__(self) -> str: def __repr__(self) -> str:
if inspect.isfunction(self.call) or inspect.isclass(self.call): if inspect.isfunction(self.call) or inspect.isclass(self.call):
@@ -129,7 +129,8 @@ class Dependent(Generic[R]):
break break
else: else:
raise ValueError( raise ValueError(
f"Unknown parameter {param.name} for function {call} with type {param.annotation}" f"Unknown parameter {param.name} "
f"for function {call} with type {param.annotation}"
) )
default_value = field_info.default default_value = field_info.default
@@ -182,7 +183,7 @@ class Dependent(Generic[R]):
params = cls.parse_params(call, allow_types) params = cls.parse_params(call, allow_types)
parameterless_params = ( parameterless_params = (
tuple() ()
if parameterless is None if parameterless is None
else cls.parse_parameterless(tuple(parameterless), allow_types) else cls.parse_parameterless(tuple(parameterless), allow_types)
) )

View File

@@ -3,6 +3,7 @@ FrontMatter:
sidebar_position: 1 sidebar_position: 1
description: nonebot.dependencies.utils 模块 description: nonebot.dependencies.utils 模块
""" """
import inspect import inspect
from typing import Any, Dict, TypeVar, Callable, ForwardRef from typing import Any, Dict, TypeVar, Callable, ForwardRef
@@ -17,6 +18,7 @@ V = TypeVar("V")
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
"""获取可调用对象签名""" """获取可调用对象签名"""
signature = inspect.signature(call) signature = inspect.signature(call)
globalns = getattr(call, "__globals__", {}) globalns = getattr(call, "__globals__", {})
typed_params = [ typed_params = [
@@ -33,6 +35,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any: def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
"""获取参数的类型注解""" """获取参数的类型注解"""
annotation = param.annotation annotation = param.annotation
if isinstance(annotation, str): if isinstance(annotation, str):
annotation = ForwardRef(annotation) annotation = ForwardRef(annotation)
@@ -47,6 +50,8 @@ def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) ->
def check_field_type(field: ModelField, value: V) -> V: def check_field_type(field: ModelField, value: V) -> V:
"""检查字段类型是否匹配"""
_, errs_ = field.validate(value, {}, loc=()) _, errs_ = field.validate(value, {}, loc=())
if errs_: if errs_:
raise TypeMisMatch(field, value) raise TypeMisMatch(field, value)

View File

@@ -1,10 +1,11 @@
from typing_extensions import TypeAlias
from typing import Any, List, Union, Callable, Awaitable, cast from typing import Any, List, Union, Callable, Awaitable, cast
from nonebot.utils import run_sync, is_coroutine_callable from nonebot.utils import run_sync, is_coroutine_callable
SYNC_LIFESPAN_FUNC = Callable[[], Any] SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
ASYNC_LIFESPAN_FUNC = Callable[[], Awaitable[Any]] ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
LIFESPAN_FUNC = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC] LIFESPAN_FUNC: TypeAlias = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
class Lifespan: class Lifespan:

View File

@@ -15,10 +15,10 @@ FrontMatter:
description: nonebot.drivers.aiohttp 模块 description: nonebot.drivers.aiohttp 模块
""" """
from typing_extensions import override
from typing import Type, AsyncGenerator from typing import Type, AsyncGenerator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from nonebot.typing import overrides
from nonebot.drivers import Request, Response from nonebot.drivers import Request, Response
from nonebot.exception import WebSocketClosed from nonebot.exception import WebSocketClosed
from nonebot.drivers.none import Driver as NoneDriver from nonebot.drivers.none import Driver as NoneDriver
@@ -29,7 +29,8 @@ try:
import aiohttp import aiohttp
except ModuleNotFoundError as e: # pragma: no cover except ModuleNotFoundError as e: # pragma: no cover
raise ImportError( raise ImportError(
"Please install aiohttp first to use this driver. `pip install nonebot2[aiohttp]`" "Please install aiohttp first to use this driver. "
"Install with pip: `pip install nonebot2[aiohttp]`"
) from e ) from e
@@ -37,11 +38,11 @@ class Mixin(ForwardMixin):
"""AIOHTTP Mixin""" """AIOHTTP Mixin"""
@property @property
@overrides(ForwardMixin) @override
def type(self) -> str: def type(self) -> str:
return "aiohttp" return "aiohttp"
@overrides(ForwardMixin) @override
async def request(self, setup: Request) -> Response: async def request(self, setup: Request) -> Response:
if setup.version == HTTPVersion.H10: if setup.version == HTTPVersion.H10:
version = aiohttp.HttpVersion10 version = aiohttp.HttpVersion10
@@ -51,11 +52,12 @@ class Mixin(ForwardMixin):
raise RuntimeError(f"Unsupported HTTP version: {setup.version}") raise RuntimeError(f"Unsupported HTTP version: {setup.version}")
timeout = aiohttp.ClientTimeout(setup.timeout) timeout = aiohttp.ClientTimeout(setup.timeout)
files = None
data = setup.data
if setup.files: if setup.files:
files = aiohttp.FormData() data = aiohttp.FormData(data or {})
for name, file in setup.files: for name, file in setup.files:
files.add_field(name, file[1], content_type=file[2], filename=file[0]) data.add_field(name, file[1], content_type=file[2], filename=file[0])
cookies = { cookies = {
cookie.name: cookie.value for cookie in setup.cookies if cookie.value cookie.name: cookie.value for cookie in setup.cookies if cookie.value
@@ -66,7 +68,7 @@ class Mixin(ForwardMixin):
async with session.request( async with session.request(
setup.method, setup.method,
setup.url, setup.url,
data=setup.content or setup.data or files, data=setup.content or data,
json=setup.json, json=setup.json,
headers=setup.headers, headers=setup.headers,
timeout=timeout, timeout=timeout,
@@ -79,7 +81,7 @@ class Mixin(ForwardMixin):
request=setup, request=setup,
) )
@overrides(ForwardMixin) @override
@asynccontextmanager @asynccontextmanager
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]: async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
if setup.version == HTTPVersion.H10: if setup.version == HTTPVersion.H10:
@@ -115,15 +117,15 @@ class WebSocket(BaseWebSocket):
self.websocket = websocket self.websocket = websocket
@property @property
@overrides(BaseWebSocket) @override
def closed(self): def closed(self):
return self.websocket.closed return self.websocket.closed
@overrides(BaseWebSocket) @override
async def accept(self): async def accept(self):
raise NotImplementedError raise NotImplementedError
@overrides(BaseWebSocket) @override
async def close(self, code: int = 1000): async def close(self, code: int = 1000):
await self.websocket.close(code=code) await self.websocket.close(code=code)
await self.session.close() await self.session.close()
@@ -134,7 +136,7 @@ class WebSocket(BaseWebSocket):
raise WebSocketClosed(self.websocket.close_code or 1006) raise WebSocketClosed(self.websocket.close_code or 1006)
return msg return msg
@overrides(BaseWebSocket) @override
async def receive(self) -> str: async def receive(self) -> str:
msg = await self._receive() msg = await self._receive()
if msg.type not in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY): if msg.type not in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY):
@@ -143,7 +145,7 @@ class WebSocket(BaseWebSocket):
) )
return msg.data return msg.data
@overrides(BaseWebSocket) @override
async def receive_text(self) -> str: async def receive_text(self) -> str:
msg = await self._receive() msg = await self._receive()
if msg.type != aiohttp.WSMsgType.TEXT: if msg.type != aiohttp.WSMsgType.TEXT:
@@ -152,7 +154,7 @@ class WebSocket(BaseWebSocket):
) )
return msg.data return msg.data
@overrides(BaseWebSocket) @override
async def receive_bytes(self) -> bytes: async def receive_bytes(self) -> bytes:
msg = await self._receive() msg = await self._receive()
if msg.type != aiohttp.WSMsgType.BINARY: if msg.type != aiohttp.WSMsgType.BINARY:
@@ -161,11 +163,11 @@ class WebSocket(BaseWebSocket):
) )
return msg.data return msg.data
@overrides(BaseWebSocket) @override
async def send_text(self, data: str) -> None: async def send_text(self, data: str) -> None:
await self.websocket.send_str(data) await self.websocket.send_str(data)
@overrides(BaseWebSocket) @override
async def send_bytes(self, data: bytes) -> None: async def send_bytes(self, data: bytes) -> None:
await self.websocket.send_bytes(data) await self.websocket.send_bytes(data)

View File

@@ -19,12 +19,12 @@ FrontMatter:
import logging import logging
import contextlib import contextlib
from functools import wraps from functools import wraps
from typing_extensions import override
from typing import Any, Dict, List, Tuple, Union, Optional from typing import Any, Dict, List, Tuple, Union, Optional
from pydantic import BaseSettings from pydantic import BaseSettings
from nonebot.config import Env from nonebot.config import Env
from nonebot.typing import overrides
from nonebot.exception import WebSocketClosed from nonebot.exception import WebSocketClosed
from nonebot.internal.driver import FileTypes from nonebot.internal.driver import FileTypes
from nonebot.config import Config as NoneBotConfig from nonebot.config import Config as NoneBotConfig
@@ -41,7 +41,8 @@ try:
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
except ModuleNotFoundError as e: # pragma: no cover except ModuleNotFoundError as e: # pragma: no cover
raise ImportError( raise ImportError(
"Please install FastAPI by using `pip install nonebot2[fastapi]`" "Please install FastAPI first to use this driver. "
"Install with pip: `pip install nonebot2[fastapi]`"
) from e ) from e
@@ -90,7 +91,7 @@ class Driver(ReverseDriver):
"""FastAPI 驱动框架。""" """FastAPI 驱动框架。"""
def __init__(self, env: Env, config: NoneBotConfig): def __init__(self, env: Env, config: NoneBotConfig):
super(Driver, self).__init__(env, config) super().__init__(env, config)
self.fastapi_config: Config = Config(**config.dict()) self.fastapi_config: Config = Config(**config.dict())
@@ -105,30 +106,30 @@ class Driver(ReverseDriver):
) )
@property @property
@overrides(ReverseDriver) @override
def type(self) -> str: def type(self) -> str:
"""驱动名称: `fastapi`""" """驱动名称: `fastapi`"""
return "fastapi" return "fastapi"
@property @property
@overrides(ReverseDriver) @override
def server_app(self) -> FastAPI: def server_app(self) -> FastAPI:
"""`FastAPI APP` 对象""" """`FastAPI APP` 对象"""
return self._server_app return self._server_app
@property @property
@overrides(ReverseDriver) @override
def asgi(self) -> FastAPI: def asgi(self) -> FastAPI:
"""`FastAPI APP` 对象""" """`FastAPI APP` 对象"""
return self._server_app return self._server_app
@property @property
@overrides(ReverseDriver) @override
def logger(self) -> logging.Logger: def logger(self) -> logging.Logger:
"""fastapi 使用的 logger""" """fastapi 使用的 logger"""
return logging.getLogger("fastapi") return logging.getLogger("fastapi")
@overrides(ReverseDriver) @override
def setup_http_server(self, setup: HTTPServerSetup): def setup_http_server(self, setup: HTTPServerSetup):
async def _handle(request: Request) -> Response: async def _handle(request: Request) -> Response:
return await self._handle_http(request, setup) return await self._handle_http(request, setup)
@@ -141,7 +142,7 @@ class Driver(ReverseDriver):
include_in_schema=self.fastapi_config.fastapi_include_adapter_schema, include_in_schema=self.fastapi_config.fastapi_include_adapter_schema,
) )
@overrides(ReverseDriver) @override
def setup_websocket_server(self, setup: WebSocketServerSetup) -> None: def setup_websocket_server(self, setup: WebSocketServerSetup) -> None:
async def _handle(websocket: WebSocket) -> None: async def _handle(websocket: WebSocket) -> None:
await self._handle_ws(websocket, setup) await self._handle_ws(websocket, setup)
@@ -152,11 +153,11 @@ class Driver(ReverseDriver):
name=setup.name, name=setup.name,
) )
@overrides(ReverseDriver) @override
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
return self._lifespan.on_startup(func) return self._lifespan.on_startup(func)
@overrides(ReverseDriver) @override
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
return self._lifespan.on_shutdown(func) return self._lifespan.on_shutdown(func)
@@ -168,7 +169,7 @@ class Driver(ReverseDriver):
finally: finally:
await self._lifespan.shutdown() await self._lifespan.shutdown()
@overrides(ReverseDriver) @override
def run( def run(
self, self,
host: Optional[str] = None, host: Optional[str] = None,
@@ -267,30 +268,30 @@ class Driver(ReverseDriver):
class FastAPIWebSocket(BaseWebSocket): class FastAPIWebSocket(BaseWebSocket):
"""FastAPI WebSocket Wrapper""" """FastAPI WebSocket Wrapper"""
@overrides(BaseWebSocket) @override
def __init__(self, *, request: BaseRequest, websocket: WebSocket): def __init__(self, *, request: BaseRequest, websocket: WebSocket):
super().__init__(request=request) super().__init__(request=request)
self.websocket = websocket self.websocket = websocket
@property @property
@overrides(BaseWebSocket) @override
def closed(self) -> bool: def closed(self) -> bool:
return ( return (
self.websocket.client_state == WebSocketState.DISCONNECTED self.websocket.client_state == WebSocketState.DISCONNECTED
or self.websocket.application_state == WebSocketState.DISCONNECTED or self.websocket.application_state == WebSocketState.DISCONNECTED
) )
@overrides(BaseWebSocket) @override
async def accept(self) -> None: async def accept(self) -> None:
await self.websocket.accept() await self.websocket.accept()
@overrides(BaseWebSocket) @override
async def close( async def close(
self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = "" self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = ""
) -> None: ) -> None:
await self.websocket.close(code, reason) await self.websocket.close(code, reason)
@overrides(BaseWebSocket) @override
async def receive(self) -> Union[str, bytes]: async def receive(self) -> Union[str, bytes]:
# assert self.websocket.application_state == WebSocketState.CONNECTED # assert self.websocket.application_state == WebSocketState.CONNECTED
msg = await self.websocket.receive() msg = await self.websocket.receive()
@@ -298,21 +299,21 @@ class FastAPIWebSocket(BaseWebSocket):
raise WebSocketClosed(msg["code"]) raise WebSocketClosed(msg["code"])
return msg["text"] if "text" in msg else msg["bytes"] return msg["text"] if "text" in msg else msg["bytes"]
@overrides(BaseWebSocket) @override
@catch_closed @catch_closed
async def receive_text(self) -> str: async def receive_text(self) -> str:
return await self.websocket.receive_text() return await self.websocket.receive_text()
@overrides(BaseWebSocket) @override
@catch_closed @catch_closed
async def receive_bytes(self) -> bytes: async def receive_bytes(self) -> bytes:
return await self.websocket.receive_bytes() return await self.websocket.receive_bytes()
@overrides(BaseWebSocket) @override
async def send_text(self, data: str) -> None: async def send_text(self, data: str) -> None:
await self.websocket.send({"type": "websocket.send", "text": data}) await self.websocket.send({"type": "websocket.send", "text": data})
@overrides(BaseWebSocket) @override
async def send_bytes(self, data: bytes) -> None: async def send_bytes(self, data: bytes) -> None:
await self.websocket.send({"type": "websocket.send", "bytes": data}) await self.websocket.send({"type": "websocket.send", "bytes": data})

View File

@@ -14,10 +14,11 @@ FrontMatter:
sidebar_position: 3 sidebar_position: 3
description: nonebot.drivers.httpx 模块 description: nonebot.drivers.httpx 模块
""" """
from typing_extensions import override
from typing import Type, AsyncGenerator from typing import Type, AsyncGenerator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from nonebot.typing import overrides
from nonebot.drivers.none import Driver as NoneDriver from nonebot.drivers.none import Driver as NoneDriver
from nonebot.drivers import ( from nonebot.drivers import (
Request, Request,
@@ -33,7 +34,8 @@ try:
import httpx import httpx
except ModuleNotFoundError as e: # pragma: no cover except ModuleNotFoundError as e: # pragma: no cover
raise ImportError( raise ImportError(
"Please install httpx by using `pip install nonebot2[httpx]`" "Please install httpx first to use this driver. "
"Install with pip: `pip install nonebot2[httpx]`"
) from e ) from e
@@ -41,11 +43,11 @@ class Mixin(ForwardMixin):
"""HTTPX Mixin""" """HTTPX Mixin"""
@property @property
@overrides(ForwardMixin) @override
def type(self) -> str: def type(self) -> str:
return "httpx" return "httpx"
@overrides(ForwardMixin) @override
async def request(self, setup: Request) -> Response: async def request(self, setup: Request) -> Response:
async with httpx.AsyncClient( async with httpx.AsyncClient(
cookies=setup.cookies.jar, cookies=setup.cookies.jar,
@@ -70,7 +72,7 @@ class Mixin(ForwardMixin):
request=setup, request=setup,
) )
@overrides(ForwardMixin) @override
@asynccontextmanager @asynccontextmanager
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]: async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
async with super(Mixin, self).websocket(setup) as ws: async with super(Mixin, self).websocket(setup) as ws:

View File

@@ -9,14 +9,13 @@ FrontMatter:
description: nonebot.drivers.none 模块 description: nonebot.drivers.none 模块
""" """
import signal import signal
import asyncio import asyncio
import threading import threading
from typing_extensions import override
from nonebot.log import logger from nonebot.log import logger
from nonebot.consts import WINDOWS from nonebot.consts import WINDOWS
from nonebot.typing import overrides
from nonebot.config import Env, Config from nonebot.config import Env, Config
from nonebot.drivers import Driver as BaseDriver from nonebot.drivers import Driver as BaseDriver
@@ -42,28 +41,28 @@ class Driver(BaseDriver):
self.force_exit: bool = False self.force_exit: bool = False
@property @property
@overrides(BaseDriver) @override
def type(self) -> str: def type(self) -> str:
"""驱动名称: `none`""" """驱动名称: `none`"""
return "none" return "none"
@property @property
@overrides(BaseDriver) @override
def logger(self): def logger(self):
"""none driver 使用的 logger""" """none driver 使用的 logger"""
return logger return logger
@overrides(BaseDriver) @override
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""注册一个启动时执行的函数""" """注册一个启动时执行的函数"""
return self._lifespan.on_startup(func) return self._lifespan.on_startup(func)
@overrides(BaseDriver) @override
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""注册一个停止时执行的函数""" """注册一个停止时执行的函数"""
return self._lifespan.on_shutdown(func) return self._lifespan.on_shutdown(func)
@overrides(BaseDriver) @override
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
"""启动 none driver""" """启动 none driver"""
super().run(*args, **kwargs) super().run(*args, **kwargs)

View File

@@ -17,12 +17,23 @@ FrontMatter:
import asyncio import asyncio
from functools import wraps from functools import wraps
from typing import Any, Dict, List, Tuple, Union, TypeVar, Callable, Optional, Coroutine from typing_extensions import override
from typing import (
Any,
Dict,
List,
Tuple,
Union,
TypeVar,
Callable,
Optional,
Coroutine,
cast,
)
from pydantic import BaseSettings from pydantic import BaseSettings
from nonebot.config import Env from nonebot.config import Env
from nonebot.typing import overrides
from nonebot.exception import WebSocketClosed from nonebot.exception import WebSocketClosed
from nonebot.internal.driver import FileTypes from nonebot.internal.driver import FileTypes
from nonebot.config import Config as NoneBotConfig from nonebot.config import Config as NoneBotConfig
@@ -33,13 +44,15 @@ from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
try: try:
import uvicorn import uvicorn
from quart import request as _request from quart import request as _request
from quart import websocket as _websocket from quart.ctx import WebsocketContext
from quart.globals import websocket_ctx
from quart import Quart, Request, Response from quart import Quart, Request, Response
from quart.datastructures import FileStorage from quart.datastructures import FileStorage
from quart import Websocket as QuartWebSocket from quart import Websocket as QuartWebSocket
except ModuleNotFoundError as e: # pragma: no cover except ModuleNotFoundError as e: # pragma: no cover
raise ImportError( raise ImportError(
"Please install Quart by using `pip install nonebot2[quart]`" "Please install Quart first to use this driver. "
"Install with pip: `pip install nonebot2[quart]`"
) from e ) from e
_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine]) _AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine])
@@ -89,30 +102,30 @@ class Driver(ReverseDriver):
) )
@property @property
@overrides(ReverseDriver) @override
def type(self) -> str: def type(self) -> str:
"""驱动名称: `quart`""" """驱动名称: `quart`"""
return "quart" return "quart"
@property @property
@overrides(ReverseDriver) @override
def server_app(self) -> Quart: def server_app(self) -> Quart:
"""`Quart` 对象""" """`Quart` 对象"""
return self._server_app return self._server_app
@property @property
@overrides(ReverseDriver) @override
def asgi(self): def asgi(self):
"""`Quart` 对象""" """`Quart` 对象"""
return self._server_app return self._server_app
@property @property
@overrides(ReverseDriver) @override
def logger(self): def logger(self):
"""Quart 使用的 logger""" """Quart 使用的 logger"""
return self._server_app.logger return self._server_app.logger
@overrides(ReverseDriver) @override
def setup_http_server(self, setup: HTTPServerSetup): def setup_http_server(self, setup: HTTPServerSetup):
async def _handle() -> Response: async def _handle() -> Response:
return await self._handle_http(setup) return await self._handle_http(setup)
@@ -124,7 +137,7 @@ class Driver(ReverseDriver):
view_func=_handle, view_func=_handle,
) )
@overrides(ReverseDriver) @override
def setup_websocket_server(self, setup: WebSocketServerSetup) -> None: def setup_websocket_server(self, setup: WebSocketServerSetup) -> None:
async def _handle() -> None: async def _handle() -> None:
return await self._handle_ws(setup) return await self._handle_ws(setup)
@@ -135,17 +148,17 @@ class Driver(ReverseDriver):
view_func=_handle, view_func=_handle,
) )
@overrides(ReverseDriver) @override
def on_startup(self, func: _AsyncCallable) -> _AsyncCallable: def on_startup(self, func: _AsyncCallable) -> _AsyncCallable:
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)""" """参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
return self.server_app.before_serving(func) # type: ignore return self.server_app.before_serving(func) # type: ignore
@overrides(ReverseDriver) @override
def on_shutdown(self, func: _AsyncCallable) -> _AsyncCallable: def on_shutdown(self, func: _AsyncCallable) -> _AsyncCallable:
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)""" """参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
return self.server_app.after_serving(func) # type: ignore return self.server_app.after_serving(func) # type: ignore
@overrides(ReverseDriver) @override
def run( def run(
self, self,
host: Optional[str] = None, host: Optional[str] = None,
@@ -188,9 +201,7 @@ class Driver(ReverseDriver):
async def _handle_http(self, setup: HTTPServerSetup) -> Response: async def _handle_http(self, setup: HTTPServerSetup) -> Response:
request: Request = _request request: Request = _request
json = None json = await request.get_json() if request.is_json else None
if request.is_json:
json = await request.get_json()
data = await request.form data = await request.form
files_dict = await request.files files_dict = await request.files
@@ -223,7 +234,8 @@ class Driver(ReverseDriver):
) )
async def _handle_ws(self, setup: WebSocketServerSetup) -> None: async def _handle_ws(self, setup: WebSocketServerSetup) -> None:
websocket: QuartWebSocket = _websocket ctx = cast(WebsocketContext, websocket_ctx.copy())
websocket = websocket_ctx.websocket
http_request = BaseRequest( http_request = BaseRequest(
websocket.method, websocket.method,
@@ -233,7 +245,7 @@ class Driver(ReverseDriver):
version=websocket.http_version, version=websocket.http_version,
) )
ws = WebSocket(request=http_request, websocket=websocket) ws = WebSocket(request=http_request, websocket_ctx=ctx)
await setup.handle_func(ws) await setup.handle_func(ws)
@@ -241,30 +253,34 @@ class Driver(ReverseDriver):
class WebSocket(BaseWebSocket): class WebSocket(BaseWebSocket):
"""Quart WebSocket Wrapper""" """Quart WebSocket Wrapper"""
def __init__(self, *, request: BaseRequest, websocket: QuartWebSocket): def __init__(self, *, request: BaseRequest, websocket_ctx: WebsocketContext):
super().__init__(request=request) super().__init__(request=request)
self.websocket = websocket self.websocket_ctx = websocket_ctx
@property @property
@overrides(BaseWebSocket) def websocket(self) -> QuartWebSocket:
return self.websocket_ctx.websocket
@property
@override
def closed(self): def closed(self):
# FIXME # FIXME
return True return True
@overrides(BaseWebSocket) @override
async def accept(self): async def accept(self):
await self.websocket.accept() await self.websocket.accept()
@overrides(BaseWebSocket) @override
async def close(self, code: int = 1000, reason: str = ""): async def close(self, code: int = 1000, reason: str = ""):
await self.websocket.close(code, reason) await self.websocket.close(code, reason)
@overrides(BaseWebSocket) @override
@catch_closed @catch_closed
async def receive(self) -> Union[str, bytes]: async def receive(self) -> Union[str, bytes]:
return await self.websocket.receive() return await self.websocket.receive()
@overrides(BaseWebSocket) @override
@catch_closed @catch_closed
async def receive_text(self) -> str: async def receive_text(self) -> str:
msg = await self.websocket.receive() msg = await self.websocket.receive()
@@ -272,7 +288,7 @@ class WebSocket(BaseWebSocket):
raise TypeError("WebSocket received unexpected frame type: bytes") raise TypeError("WebSocket received unexpected frame type: bytes")
return msg return msg
@overrides(BaseWebSocket) @override
@catch_closed @catch_closed
async def receive_bytes(self) -> bytes: async def receive_bytes(self) -> bytes:
msg = await self.websocket.receive() msg = await self.websocket.receive()
@@ -280,11 +296,11 @@ class WebSocket(BaseWebSocket):
raise TypeError("WebSocket received unexpected frame type: str") raise TypeError("WebSocket received unexpected frame type: str")
return msg return msg
@overrides(BaseWebSocket) @override
async def send_text(self, data: str): async def send_text(self, data: str):
await self.websocket.send(data) await self.websocket.send(data)
@overrides(BaseWebSocket) @override
async def send_bytes(self, data: bytes): async def send_bytes(self, data: bytes):
await self.websocket.send(data) await self.websocket.send(data)

View File

@@ -14,12 +14,13 @@ FrontMatter:
sidebar_position: 4 sidebar_position: 4
description: nonebot.drivers.websockets 模块 description: nonebot.drivers.websockets 模块
""" """
import logging import logging
from functools import wraps from functools import wraps
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Type, Union, AsyncGenerator from typing_extensions import ParamSpec, override
from typing import Type, Union, TypeVar, Callable, Awaitable, AsyncGenerator
from nonebot.typing import overrides
from nonebot.log import LoguruHandler from nonebot.log import LoguruHandler
from nonebot.drivers import Request, Response from nonebot.drivers import Request, Response
from nonebot.exception import WebSocketClosed from nonebot.exception import WebSocketClosed
@@ -32,16 +33,20 @@ try:
from websockets.legacy.client import Connect, WebSocketClientProtocol from websockets.legacy.client import Connect, WebSocketClientProtocol
except ModuleNotFoundError as e: # pragma: no cover except ModuleNotFoundError as e: # pragma: no cover
raise ImportError( raise ImportError(
"Please install websockets by using `pip install nonebot2[websockets]`" "Please install websockets first to use this driver. "
"Install with pip: `pip install nonebot2[websockets]`"
) from e ) from e
T = TypeVar("T")
P = ParamSpec("P")
logger = logging.Logger("websockets.client", "INFO") logger = logging.Logger("websockets.client", "INFO")
logger.addHandler(LoguruHandler()) logger.addHandler(LoguruHandler())
def catch_closed(func): def catch_closed(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
@wraps(func) @wraps(func)
async def decorator(*args, **kwargs): async def decorator(*args: P.args, **kwargs: P.kwargs) -> T:
try: try:
return await func(*args, **kwargs) return await func(*args, **kwargs)
except ConnectionClosed as e: except ConnectionClosed as e:
@@ -57,15 +62,15 @@ class Mixin(ForwardMixin):
"""Websockets Mixin""" """Websockets Mixin"""
@property @property
@overrides(ForwardMixin) @override
def type(self) -> str: def type(self) -> str:
return "websockets" return "websockets"
@overrides(ForwardMixin) @override
async def request(self, setup: Request) -> Response: async def request(self, setup: Request) -> Response:
return await super(Mixin, self).request(setup) return await super(Mixin, self).request(setup)
@overrides(ForwardMixin) @override
@asynccontextmanager @asynccontextmanager
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]: async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
connection = Connect( connection = Connect(
@@ -80,30 +85,30 @@ class Mixin(ForwardMixin):
class WebSocket(BaseWebSocket): class WebSocket(BaseWebSocket):
"""Websockets WebSocket Wrapper""" """Websockets WebSocket Wrapper"""
@overrides(BaseWebSocket) @override
def __init__(self, *, request: Request, websocket: WebSocketClientProtocol): def __init__(self, *, request: Request, websocket: WebSocketClientProtocol):
super().__init__(request=request) super().__init__(request=request)
self.websocket = websocket self.websocket = websocket
@property @property
@overrides(BaseWebSocket) @override
def closed(self) -> bool: def closed(self) -> bool:
return self.websocket.closed return self.websocket.closed
@overrides(BaseWebSocket) @override
async def accept(self): async def accept(self):
raise NotImplementedError raise NotImplementedError
@overrides(BaseWebSocket) @override
async def close(self, code: int = 1000, reason: str = ""): async def close(self, code: int = 1000, reason: str = ""):
await self.websocket.close(code, reason) await self.websocket.close(code, reason)
@overrides(BaseWebSocket) @override
@catch_closed @catch_closed
async def receive(self) -> Union[str, bytes]: async def receive(self) -> Union[str, bytes]:
return await self.websocket.recv() return await self.websocket.recv()
@overrides(BaseWebSocket) @override
@catch_closed @catch_closed
async def receive_text(self) -> str: async def receive_text(self) -> str:
msg = await self.websocket.recv() msg = await self.websocket.recv()
@@ -111,7 +116,7 @@ class WebSocket(BaseWebSocket):
raise TypeError("WebSocket received unexpected frame type: bytes") raise TypeError("WebSocket received unexpected frame type: bytes")
return msg return msg
@overrides(BaseWebSocket) @override
@catch_closed @catch_closed
async def receive_bytes(self) -> bytes: async def receive_bytes(self) -> bytes:
msg = await self.websocket.recv() msg = await self.websocket.recv()
@@ -119,11 +124,11 @@ class WebSocket(BaseWebSocket):
raise TypeError("WebSocket received unexpected frame type: str") raise TypeError("WebSocket received unexpected frame type: str")
return msg return msg
@overrides(BaseWebSocket) @override
async def send_text(self, data: str) -> None: async def send_text(self, data: str) -> None:
await self.websocket.send(data) await self.websocket.send(data)
@overrides(BaseWebSocket) @override
async def send_bytes(self, data: bytes) -> None: async def send_bytes(self, data: bytes) -> None:
await self.websocket.send(data) await self.websocket.send(data)

View File

@@ -43,9 +43,9 @@ class NoneBotException(Exception):
# Rule Exception # Rule Exception
class ParserExit(NoneBotException): class ParserExit(NoneBotException):
"""{ref}`nonebot.rule.shell_command` 处理消息失败时返回的异常""" """{ref}`nonebot.rule.shell_command` 处理消息失败时返回的异常"""
def __init__(self, status: int = 0, message: Optional[str] = None): def __init__(self, status: int = 0, message: Optional[str] = None) -> None:
self.status = status self.status = status
self.message = message self.message = message
@@ -69,7 +69,7 @@ class IgnoredException(ProcessException):
reason: 忽略事件的原因 reason: 忽略事件的原因
""" """
def __init__(self, reason: Any): def __init__(self, reason: Any) -> None:
self.reason: Any = reason self.reason: Any = reason
def __repr__(self) -> str: def __repr__(self) -> str:
@@ -96,7 +96,7 @@ class SkippedException(ProcessException):
class TypeMisMatch(SkippedException): class TypeMisMatch(SkippedException):
"""当前 `Handler` 的参数类型不匹配。""" """当前 `Handler` 的参数类型不匹配。"""
def __init__(self, param: ModelField, value: Any): def __init__(self, param: ModelField, value: Any) -> None:
self.param: ModelField = param self.param: ModelField = param
self.value: Any = value self.value: Any = value
@@ -108,7 +108,8 @@ class TypeMisMatch(SkippedException):
class MockApiException(ProcessException): class MockApiException(ProcessException):
"""指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。可由 api hook 抛出。 """指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。
可由 api hook 抛出。
参数: 参数:
result: 返回的内容 result: 返回的内容
@@ -144,7 +145,8 @@ class MatcherException(NoneBotException):
class PausedException(MatcherException): class PausedException(MatcherException):
"""指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。可用于用户输入新信息。 """指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。
可用于用户输入新信息。
可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.pause` 抛出。 可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.pause` 抛出。
@@ -158,7 +160,8 @@ class PausedException(MatcherException):
class RejectedException(MatcherException): class RejectedException(MatcherException):
"""指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。可用于用户重新输入。 """指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。
可用于用户重新输入。
可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.reject` 抛出。 可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.reject` 抛出。
@@ -187,7 +190,7 @@ class FinishedException(MatcherException):
# Adapter Exceptions # Adapter Exceptions
class AdapterException(NoneBotException): class AdapterException(NoneBotException):
"""代表 `Adapter` 抛出的异常,所有的 `Adapter` 都要在内部继承自这个 `Exception` """代表 `Adapter` 抛出的异常,所有的 `Adapter` 都要在内部继承自这个 `Exception`
参数: 参数:
adapter_name: 标识 adapter adapter_name: 标识 adapter
@@ -210,7 +213,9 @@ class ApiNotAvailable(AdapterException):
class NetworkError(AdapterException): class NetworkError(AdapterException):
"""在网络出现问题时抛出,如: API 请求地址不正确, API 请求无返回或返回状态非正常等。""" """在网络出现问题时抛出,
如: API 请求地址不正确, API 请求无返回或返回状态非正常等。
"""
class ActionFailed(AdapterException): class ActionFailed(AdapterException):
@@ -219,13 +224,13 @@ class ActionFailed(AdapterException):
# Driver Exceptions # Driver Exceptions
class DriverException(NoneBotException): class DriverException(NoneBotException):
"""`Driver` 抛出的异常基类""" """`Driver` 抛出的异常基类"""
class WebSocketClosed(DriverException): class WebSocketClosed(DriverException):
"""WebSocket 连接已关闭""" """WebSocket 连接已关闭"""
def __init__(self, code: int, reason: Optional[str] = None): def __init__(self, code: int, reason: Optional[str] = None) -> None:
self.code = code self.code = code
self.reason = reason self.reason = reason

View File

@@ -44,10 +44,11 @@ class Event(abc.ABC, BaseModel):
def get_log_string(self) -> str: def get_log_string(self) -> str:
"""获取事件日志信息的方法。 """获取事件日志信息的方法。
通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,可以抛出 `NoLogException` 异常。 通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,
可以抛出 `NoLogException` 异常。
异常: 异常:
NoLogException: NoLogException: 希望 NoneBot 隐藏该事件日志
""" """
return f"[{self.get_event_name()}]: {self.get_event_description()}" return f"[{self.get_event_name()}]: {self.get_event_description()}"
@@ -58,7 +59,9 @@ class Event(abc.ABC, BaseModel):
@abc.abstractmethod @abc.abstractmethod
def get_session_id(self) -> str: def get_session_id(self) -> str:
"""获取会话 id 的方法,用于判断当前事件属于哪一个会话,通常是用户 id、群组 id 组合。""" """获取会话 id 的方法,用于判断当前事件属于哪一个会话,
通常是用户 id、群组 id 组合。
"""
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod

View File

@@ -98,7 +98,7 @@ class MessageSegment(abc.ABC, Generic[TM]):
class Message(List[TMS], abc.ABC): class Message(List[TMS], abc.ABC):
"""消息数组 """消息序列
参数: 参数:
message: 消息内容 message: 消息内容
@@ -124,9 +124,9 @@ class Message(List[TMS], abc.ABC):
def template(cls, format_string: Union[str, TM]) -> MessageTemplate[Self]: def template(cls, format_string: Union[str, TM]) -> MessageTemplate[Self]:
"""创建消息模板。 """创建消息模板。
用法和 `str.format` 大致相同, 但是可以输出消息对象, 并且支持以 `Message` 对象作为消息模板 用法和 `str.format` 大致相同支持以 `Message` 对象作为消息模板并输出消息对象。
并且提供了拓展的格式化控制符,
并且提供了拓展的格式化控制符, 可以用适用于该消息类型的 `MessageSegment` 工厂方法创建消息 可以通过该消息类型的 `MessageSegment` 工厂方法创建消息
参数: 参数:
format_string: 格式化模板 format_string: 格式化模板

View File

@@ -1,5 +1,6 @@
import functools import functools
from string import Formatter from string import Formatter
from typing_extensions import TypeAlias
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
@@ -25,7 +26,7 @@ if TYPE_CHECKING:
TM = TypeVar("TM", bound="Message") TM = TypeVar("TM", bound="Message")
TF = TypeVar("TF", str, "Message") TF = TypeVar("TF", str, "Message")
FormatSpecFunc = Callable[[Any], str] FormatSpecFunc: TypeAlias = Callable[[Any], str]
FormatSpecFunc_T = TypeVar("FormatSpecFunc_T", bound=FormatSpecFunc) FormatSpecFunc_T = TypeVar("FormatSpecFunc_T", bound=FormatSpecFunc)

View File

@@ -89,9 +89,7 @@ class Driver(abc.ABC):
@abc.abstractmethod @abc.abstractmethod
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
""" """启动驱动框架"""
启动驱动框架
"""
logger.opt(colors=True).debug( logger.opt(colors=True).debug(
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>" f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
) )
@@ -152,8 +150,10 @@ class Driver(abc.ABC):
await asyncio.gather(*coros) await asyncio.gather(*coros)
except Exception as e: except Exception as e:
logger.opt(colors=True, exception=e).error( logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketConnection hook. " "<r><bg #f8bbd0>"
"Running cancelled!</bg #f8bbd0></r>" "Error when running WebSocketConnection hook. "
"Running cancelled!"
"</bg #f8bbd0></r>"
) )
asyncio.create_task(_run_hook(bot)) asyncio.create_task(_run_hook(bot))
@@ -177,8 +177,10 @@ class Driver(abc.ABC):
await asyncio.gather(*coros) await asyncio.gather(*coros)
except Exception as e: except Exception as e:
logger.opt(colors=True, exception=e).error( logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. " "<r><bg #f8bbd0>"
"Running cancelled!</bg #f8bbd0></r>" "Error when running WebSocketDisConnection hook. "
"Running cancelled!"
"</bg #f8bbd0></r>"
) )
asyncio.create_task(_run_hook(bot)) asyncio.create_task(_run_hook(bot))
@@ -241,7 +243,7 @@ def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Dr
# check first # check first
assert issubclass(driver, Driver), "`driver` must be subclass of Driver" assert issubclass(driver, Driver), "`driver` must be subclass of Driver"
assert all( assert all(
map(lambda m: issubclass(m, ForwardMixin), mixins) issubclass(m, ForwardMixin) for m in mixins
), "`mixins` must be subclass of ForwardMixin" ), "`mixins` must be subclass of ForwardMixin"
if not mixins: if not mixins:
@@ -251,7 +253,9 @@ def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Dr
return ( return (
driver.type.__get__(self) driver.type.__get__(self)
+ "+" + "+"
+ "+".join(map(lambda x: x.type.__get__(self), mixins)) + "+".join(x.type.__get__(self) for x in mixins)
) )
return type("CombinedDriver", (*mixins, driver, ForwardDriver), {"type": property(type_)}) # type: ignore return type(
"CombinedDriver", (*mixins, driver, ForwardDriver), {"type": property(type_)}
) # type: ignore

View File

@@ -2,6 +2,7 @@ import abc
import urllib.request import urllib.request
from enum import Enum from enum import Enum
from dataclasses import dataclass from dataclasses import dataclass
from typing_extensions import TypeAlias
from http.cookiejar import Cookie, CookieJar from http.cookiejar import Cookie, CookieJar
from typing import ( from typing import (
IO, IO,
@@ -21,28 +22,30 @@ from typing import (
from yarl import URL as URL from yarl import URL as URL
from multidict import CIMultiDict from multidict import CIMultiDict
RawURL = Tuple[bytes, bytes, Optional[int], bytes] RawURL: TypeAlias = Tuple[bytes, bytes, Optional[int], bytes]
SimpleQuery = Union[str, int, float] SimpleQuery: TypeAlias = Union[str, int, float]
QueryVariable = Union[SimpleQuery, List[SimpleQuery]] QueryVariable: TypeAlias = Union[SimpleQuery, List[SimpleQuery]]
QueryTypes = Union[ QueryTypes: TypeAlias = Union[
None, str, Mapping[str, QueryVariable], List[Tuple[str, QueryVariable]] None, str, Mapping[str, QueryVariable], List[Tuple[str, QueryVariable]]
] ]
HeaderTypes = Union[ HeaderTypes: TypeAlias = Union[
None, None,
CIMultiDict[str], CIMultiDict[str],
Dict[str, str], Dict[str, str],
List[Tuple[str, str]], List[Tuple[str, str]],
] ]
CookieTypes = Union[None, "Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]] CookieTypes: TypeAlias = Union[
None, "Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]
]
ContentTypes = Union[str, bytes, None] ContentTypes: TypeAlias = Union[str, bytes, None]
DataTypes = Union[dict, None] DataTypes: TypeAlias = Union[dict, None]
FileContent = Union[IO[bytes], bytes] FileContent: TypeAlias = Union[IO[bytes], bytes]
FileType = Tuple[Optional[str], FileContent, Optional[str]] FileType: TypeAlias = Tuple[Optional[str], FileContent, Optional[str]]
FileTypes = Union[ FileTypes: TypeAlias = Union[
# file (or bytes) # file (or bytes)
FileContent, FileContent,
# (filename, file (or bytes)) # (filename, file (or bytes))
@@ -50,7 +53,7 @@ FileTypes = Union[
# (filename, file (or bytes), content_type) # (filename, file (or bytes), content_type)
FileType, FileType,
] ]
FilesTypes = Union[Dict[str, FileTypes], List[Tuple[str, FileTypes]], None] FilesTypes: TypeAlias = Union[Dict[str, FileTypes], List[Tuple[str, FileTypes]], None]
class HTTPVersion(Enum): class HTTPVersion(Enum):
@@ -160,7 +163,6 @@ class Response:
class WebSocket(abc.ABC): class WebSocket(abc.ABC):
def __init__(self, *, request: Request): def __init__(self, *, request: Request):
# request
self.request: Request = request self.request: Request = request
def __repr__(self) -> str: def __repr__(self) -> str:
@@ -169,9 +171,7 @@ class WebSocket(abc.ABC):
@property @property
@abc.abstractmethod @abc.abstractmethod
def closed(self) -> bool: def closed(self) -> bool:
""" """连接是否已经关闭"""
连接是否已经关闭
"""
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod

View File

@@ -29,6 +29,13 @@ from nonebot.internal.adapter import (
MessageSegment, MessageSegment,
MessageTemplate, MessageTemplate,
) )
from nonebot.typing import (
T_State,
T_Handler,
T_TypeUpdater,
T_DependencyCache,
T_PermissionUpdater,
)
from nonebot.consts import ( from nonebot.consts import (
ARG_KEY, ARG_KEY,
RECEIVE_KEY, RECEIVE_KEY,
@@ -36,14 +43,6 @@ from nonebot.consts import (
LAST_RECEIVE_KEY, LAST_RECEIVE_KEY,
REJECT_CACHE_TARGET, REJECT_CACHE_TARGET,
) )
from nonebot.typing import (
Any,
T_State,
T_Handler,
T_TypeUpdater,
T_DependencyCache,
T_PermissionUpdater,
)
from nonebot.exception import ( from nonebot.exception import (
PausedException, PausedException,
StopPropagation, StopPropagation,
@@ -376,7 +375,7 @@ class Matcher(metaclass=MatcherMeta):
return return
await matcher.reject() await matcher.reject()
_parameterless = (Depends(_receive), *(parameterless or tuple())) _parameterless = (Depends(_receive), *(parameterless or ()))
def _decorator(func: T_Handler) -> T_Handler: def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func: if cls.handlers and cls.handlers[-1].call is func:
@@ -406,7 +405,8 @@ class Matcher(metaclass=MatcherMeta):
) -> Callable[[T_Handler], T_Handler]: ) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来指示 NoneBot 获取一个参数 `key` """装饰一个函数来指示 NoneBot 获取一个参数 `key`
当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数,如果 `key` 已存在则直接继续运行 当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数,
如果 `key` 已存在则直接继续运行
参数: 参数:
key: 参数名 key: 参数名
@@ -423,7 +423,7 @@ class Matcher(metaclass=MatcherMeta):
return return
await matcher.reject(prompt) await matcher.reject(prompt)
_parameterless = (Depends(_key_getter), *(parameterless or tuple())) _parameterless = (Depends(_key_getter), *(parameterless or ()))
def _decorator(func: T_Handler) -> T_Handler: def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func: if cls.handlers and cls.handlers[-1].call is func:
@@ -454,7 +454,8 @@ class Matcher(metaclass=MatcherMeta):
参数: 参数:
message: 消息内容 message: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,请参考对应 adapter 的 bot 对象 api kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
请参考对应 adapter 的 bot 对象 api
""" """
bot = current_bot.get() bot = current_bot.get()
event = current_event.get() event = current_event.get()
@@ -475,7 +476,8 @@ class Matcher(metaclass=MatcherMeta):
参数: 参数:
message: 消息内容 message: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,请参考对应 adapter 的 bot 对象 api kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
请参考对应 adapter 的 bot 对象 api
""" """
if message is not None: if message is not None:
await cls.send(message, **kwargs) await cls.send(message, **kwargs)
@@ -491,7 +493,8 @@ class Matcher(metaclass=MatcherMeta):
参数: 参数:
prompt: 消息内容 prompt: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,请参考对应 adapter 的 bot 对象 api kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
请参考对应 adapter 的 bot 对象 api
""" """
if prompt is not None: if prompt is not None:
await cls.send(prompt, **kwargs) await cls.send(prompt, **kwargs)
@@ -508,7 +511,8 @@ class Matcher(metaclass=MatcherMeta):
参数: 参数:
prompt: 消息内容 prompt: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,请参考对应 adapter 的 bot 对象 api kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
请参考对应 adapter 的 bot 对象 api
""" """
if prompt is not None: if prompt is not None:
await cls.send(prompt, **kwargs) await cls.send(prompt, **kwargs)
@@ -527,7 +531,8 @@ class Matcher(metaclass=MatcherMeta):
参数: 参数:
key: 参数名 key: 参数名
prompt: 消息内容 prompt: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,请参考对应 adapter 的 bot 对象 api kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
请参考对应 adapter 的 bot 对象 api
""" """
matcher = current_matcher.get() matcher = current_matcher.get()
matcher.set_target(ARG_KEY.format(key=key)) matcher.set_target(ARG_KEY.format(key=key))
@@ -548,7 +553,8 @@ class Matcher(metaclass=MatcherMeta):
参数: 参数:
id: 消息 id id: 消息 id
prompt: 消息内容 prompt: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,请参考对应 adapter 的 bot 对象 api kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
请参考对应 adapter 的 bot 对象 api
""" """
matcher = current_matcher.get() matcher = current_matcher.get()
matcher.set_target(RECEIVE_KEY.format(id=id)) matcher.set_target(RECEIVE_KEY.format(id=id))

View File

@@ -63,7 +63,10 @@ def Depends(
finally: finally:
... ...
async def handler(param_name: Any = Depends(depend_func), gen: Any = Depends(depend_gen_func)): async def handler(
param_name: Any = Depends(depend_func),
gen: Any = Depends(depend_gen_func),
):
... ...
``` ```
""" """
@@ -317,7 +320,17 @@ class MatcherParam(Param):
# param type is Matcher(s) or subclass(es) of Matcher or None # param type is Matcher(s) or subclass(es) of Matcher or None
if generic_check_issubclass(param.annotation, Matcher): if generic_check_issubclass(param.annotation, Matcher):
return cls(Required) checker: Optional[ModelField] = None
if param.annotation is not Matcher:
checker = ModelField(
name=param.name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
)
return cls(Required, checker=checker)
# legacy: param is named "matcher" and has no type annotation # legacy: param is named "matcher" and has no type annotation
elif param.annotation == param.empty and param.name == "matcher": elif param.annotation == param.empty and param.name == "matcher":
return cls(Required) return cls(Required)
@@ -325,6 +338,10 @@ class MatcherParam(Param):
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any: async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
return matcher return matcher
async def _check(self, matcher: "Matcher", **kwargs: Any) -> Any:
if checker := self.extra.get("checker", None):
check_field_type(checker, matcher)
class ArgInner: class ArgInner:
def __init__( def __init__(
@@ -372,6 +389,10 @@ class ArgParam(Param):
return cls( return cls(
Required, key=param.default.key or param.name, type=param.default.type Required, key=param.default.key or param.name, type=param.default.type
) )
elif get_origin(param.annotation) is Annotated:
for arg in get_args(param.annotation):
if isinstance(arg, ArgInner):
return cls(Required, key=arg.key or param.name, type=arg.type)
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any: async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
key: str = self.extra["key"] key: str = self.extra["key"]

View File

@@ -54,7 +54,7 @@ class LoguruHandler(logging.Handler): # pragma: no cover
except ValueError: except ValueError:
level = record.levelno level = record.levelno
frame, depth = logging.currentframe(), 2 frame, depth = sys._getframe(6), 6
while frame and frame.f_code.co_filename == logging.__file__: while frame and frame.f_code.co_filename == logging.__file__:
frame = frame.f_back frame = frame.f_back
depth += 1 depth += 1
@@ -88,5 +88,6 @@ logger_id = logger.add(
filter=default_filter, filter=default_filter,
format=default_format, format=default_format,
) )
"""默认日志处理器 id"""
__autodoc__ = {"logger_id": False} __autodoc__ = {"logger_id": False}

View File

@@ -166,7 +166,7 @@ async def _apply_event_preprocessors(
for proc in _event_preprocessors for proc in _event_preprocessors
) )
) )
except IgnoredException as e: except IgnoredException:
logger.opt(colors=True).info( logger.opt(colors=True).info(
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>" f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
) )
@@ -293,7 +293,7 @@ async def _apply_run_postprocessors(
) -> None: ) -> None:
"""运行事件响应器运行后处理。 """运行事件响应器运行后处理。
Args: 参数:
bot: Bot 对象 bot: Bot 对象
event: Event 对象 event: Event 对象
matcher: 事件响应器 matcher: 事件响应器

View File

@@ -1,7 +1,8 @@
"""本模块是 {ref}`nonebot.matcher.Matcher.permission` 的类型定义。 """本模块是 {ref}`nonebot.matcher.Matcher.permission` 的类型定义。
每个 {ref}`nonebot.matcher.Matcher` 拥有一个 {ref}`nonebot.permission.Permission` 每个{ref}`事件响应器 <nonebot.matcher.Matcher>`
其中是 `PermissionChecker` 的集合,只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行 拥有一个 {ref}`nonebot.permission.Permission`,其中是 `PermissionChecker` 的集合
只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。
FrontMatter: FrontMatter:
sidebar_position: 6 sidebar_position: 6

View File

@@ -24,8 +24,10 @@
- `load_all_plugins` => {ref}``load_all_plugins` <nonebot.plugin.load.load_all_plugins>` - `load_all_plugins` => {ref}``load_all_plugins` <nonebot.plugin.load.load_all_plugins>`
- `load_from_json` => {ref}``load_from_json` <nonebot.plugin.load.load_from_json>` - `load_from_json` => {ref}``load_from_json` <nonebot.plugin.load.load_from_json>`
- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>` - `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`
- `load_builtin_plugin` => {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>` - `load_builtin_plugin` =>
- `load_builtin_plugins` => {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>` {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`
- `load_builtin_plugins` =>
{ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
- `require` => {ref}``require` <nonebot.plugin.load.require>` - `require` => {ref}``require` <nonebot.plugin.load.require>`
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.plugin.PluginMetadata>` - `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.plugin.PluginMetadata>`
@@ -42,7 +44,7 @@ from typing import Set, Dict, List, Tuple, Optional
_plugins: Dict[str, "Plugin"] = {} _plugins: Dict[str, "Plugin"] = {}
_managers: List["PluginManager"] = [] _managers: List["PluginManager"] = []
_current_plugin_chain: ContextVar[Tuple["Plugin", ...]] = ContextVar( _current_plugin_chain: ContextVar[Tuple["Plugin", ...]] = ContextVar(
"_current_plugin_chain", default=tuple() "_current_plugin_chain", default=()
) )
@@ -132,3 +134,4 @@ from .plugin import PluginMetadata as PluginMetadata
from .load import load_all_plugins as load_all_plugins from .load import load_all_plugins as load_all_plugins
from .load import load_builtin_plugin as load_builtin_plugin from .load import load_builtin_plugin as load_builtin_plugin
from .load import load_builtin_plugins as load_builtin_plugins from .load import load_builtin_plugins as load_builtin_plugins
from .load import inherit_supported_adapters as inherit_supported_adapters

View File

@@ -4,6 +4,7 @@ FrontMatter:
sidebar_position: 1 sidebar_position: 1
description: nonebot.plugin.load 模块 description: nonebot.plugin.load 模块
""" """
import json import json
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
@@ -25,7 +26,8 @@ def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。 """加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
参数: 参数:
module_path: 插件名称 `path.to.your.plugin` 或插件路径 `pathlib.Path(path/to/your/plugin)` module_path: 插件名称 `path.to.your.plugin`
或插件路径 `pathlib.Path(path/to/your/plugin)`
""" """
module_path = ( module_path = (
path_to_module_name(module_path) path_to_module_name(module_path)
@@ -63,7 +65,8 @@ def load_all_plugins(
def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]: def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
"""导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件,以 `_` 开头的插件不会被导入! """导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件
以 `_` 开头的插件不会被导入!
参数: 参数:
file_path: 指定 json 文件路径 file_path: 指定 json 文件路径
@@ -81,7 +84,7 @@ def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
nonebot.load_from_json("plugins.json") nonebot.load_from_json("plugins.json")
``` ```
""" """
with open(file_path, "r", encoding=encoding) as f: with open(file_path, encoding=encoding) as f:
data = json.load(f) data = json.load(f)
if not isinstance(data, dict): if not isinstance(data, dict):
raise TypeError("json file must contains a dict!") raise TypeError("json file must contains a dict!")
@@ -93,7 +96,9 @@ def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]: def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
"""导入指定 toml 文件 `[tool.nonebot]` 中的 `plugins` 以及 `plugin_dirs` 下多个插件,以 `_` 开头的插件不会被导入! """导入指定 toml 文件 `[tool.nonebot]` 中的
`plugins` 以及 `plugin_dirs` 下多个插件。
以 `_` 开头的插件不会被导入!
参数: 参数:
file_path: 指定 toml 文件路径 file_path: 指定 toml 文件路径
@@ -110,7 +115,7 @@ def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
nonebot.load_from_toml("pyproject.toml") nonebot.load_from_toml("pyproject.toml")
``` ```
""" """
with open(file_path, "r", encoding=encoding) as f: with open(file_path, encoding=encoding) as f:
data = tomllib.loads(f.read()) data = tomllib.loads(f.read())
nonebot_data = data.get("tool", {}).get("nonebot") nonebot_data = data.get("tool", {}).get("nonebot")
@@ -177,3 +182,39 @@ def require(name: str) -> ModuleType:
if not plugin: if not plugin:
raise RuntimeError(f'Cannot load plugin "{name}"!') raise RuntimeError(f'Cannot load plugin "{name}"!')
return plugin.module return plugin.module
def inherit_supported_adapters(*names: str) -> Optional[Set[str]]:
"""获取已加载插件的适配器支持状态集合。
如果传入了多个插件名称,返回值会自动取交集。
参数:
names: 插件名称列表。
异常:
RuntimeError: 插件未加载
ValueError: 插件缺少元数据
"""
final_supported: Optional[Set[str]] = None
for name in names:
plugin = get_plugin(_module_name_to_plugin_name(name))
if plugin is None:
raise RuntimeError(f'Plugin "{name}" is not loaded!')
meta = plugin.metadata
if meta is None:
raise ValueError(f'Plugin "{name}" has no metadata!')
support = meta.supported_adapters
if support is None:
continue
final_supported = (
support if final_supported is None else (final_supported & support)
)
return final_supported and {
f"nonebot.adapters.{adapter_name[1:]}"
if adapter_name.startswith("~")
else adapter_name
for adapter_name in final_supported
}

View File

@@ -6,6 +6,7 @@ FrontMatter:
sidebar_position: 5 sidebar_position: 5
description: nonebot.plugin.manager 模块 description: nonebot.plugin.manager 模块
""" """
import sys import sys
import pkgutil import pkgutil
import importlib import importlib

View File

@@ -4,6 +4,7 @@ FrontMatter:
sidebar_position: 2 sidebar_position: 2
description: nonebot.plugin.on 模块 description: nonebot.plugin.on 模块
""" """
import re import re
import inspect import inspect
from types import ModuleType from types import ModuleType
@@ -322,7 +323,8 @@ def on_shell_command(
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
并将用户输入的原始参数列表保存在 `state["argv"]`, `parser` 处理的参数保存在 `state["args"]` 中 可以通过 {ref}`nonebot.params.ShellCommandArgv` 获取原始参数列表,
通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典。
参数: 参数:
cmd: 指定命令内容 cmd: 指定命令内容
@@ -427,6 +429,7 @@ class CommandGroup(_Group):
参数: 参数:
cmd: 指定命令内容 cmd: 指定命令内容
prefix_aliases: 是否影响命令别名,给命令别名加前缀
rule: 事件响应规则 rule: 事件响应规则
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
@@ -437,11 +440,14 @@ class CommandGroup(_Group):
state: 默认 state state: 默认 state
""" """
def __init__(self, cmd: Union[str, Tuple[str, ...]], **kwargs): def __init__(
self, cmd: Union[str, Tuple[str, ...]], prefix_aliases: bool = False, **kwargs
):
"""命令前缀""" """命令前缀"""
super().__init__(**kwargs) super().__init__(**kwargs)
self.basecmd: Tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd self.basecmd: Tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd
self.base_kwargs.pop("aliases", None) self.base_kwargs.pop("aliases", None)
self.prefix_aliases = prefix_aliases
def __repr__(self) -> str: def __repr__(self) -> str:
return f"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})" return f"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})"
@@ -464,6 +470,11 @@ class CommandGroup(_Group):
""" """
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
cmd = self.basecmd + sub_cmd cmd = self.basecmd + sub_cmd
if self.prefix_aliases and (aliases := kwargs.get("aliases")):
kwargs["aliases"] = {
self.basecmd + ((alias,) if isinstance(alias, str) else alias)
for alias in aliases
}
matcher = on_command(cmd, **self._get_final_kwargs(kwargs)) matcher = on_command(cmd, **self._get_final_kwargs(kwargs))
self.matchers.append(matcher) self.matchers.append(matcher)
return matcher return matcher
@@ -488,6 +499,11 @@ class CommandGroup(_Group):
""" """
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
cmd = self.basecmd + sub_cmd cmd = self.basecmd + sub_cmd
if self.prefix_aliases and (aliases := kwargs.get("aliases")):
kwargs["aliases"] = {
self.basecmd + ((alias,) if isinstance(alias, str) else alias)
for alias in aliases
}
matcher = on_shell_command(cmd, **self._get_final_kwargs(kwargs)) matcher = on_shell_command(cmd, **self._get_final_kwargs(kwargs))
self.matchers.append(matcher) self.matchers.append(matcher)
return matcher return matcher
@@ -712,7 +728,8 @@ class MatcherGroup(_Group):
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
并将用户输入的原始参数列表保存在 `state["argv"]`, `parser` 处理的参数保存在 `state["args"]` 中 可以通过 {ref}`nonebot.params.ShellCommandArgv` 获取原始参数列表,
通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典。
参数: 参数:
cmd: 指定命令内容 cmd: 指定命令内容

View File

@@ -1,7 +1,7 @@
import re import re
from typing import Any
from types import ModuleType from types import ModuleType
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Set, List, Type, Tuple, Union, Optional
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
@@ -12,399 +12,409 @@ from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecke
from .plugin import Plugin from .plugin import Plugin
def store_matcher(matcher: Type[Matcher]) -> None: ... def store_matcher(matcher: type[Matcher]) -> None: ...
def get_matcher_plugin(depth: int = ...) -> Optional[Plugin]: ... def get_matcher_plugin(depth: int = ...) -> Plugin | None: ...
def get_matcher_module(depth: int = ...) -> Optional[ModuleType]: ... def get_matcher_module(depth: int = ...) -> ModuleType | None: ...
def on( def on(
type: str = "", type: str = "",
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
*, *,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_metaevent( def on_metaevent(
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
*, *,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_message( def on_message(
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
*, *,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_notice( def on_notice(
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
*, *,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_request( def on_request(
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
*, *,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_startswith( def on_startswith(
msg: Union[str, Tuple[str, ...]], msg: str | tuple[str, ...],
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
ignorecase: bool = ..., ignorecase: bool = ...,
*, *,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_endswith( def on_endswith(
msg: Union[str, Tuple[str, ...]], msg: str | tuple[str, ...],
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
ignorecase: bool = ..., ignorecase: bool = ...,
*, *,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_fullmatch( def on_fullmatch(
msg: Union[str, Tuple[str, ...]], msg: str | tuple[str, ...],
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
ignorecase: bool = ..., ignorecase: bool = ...,
*, *,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_keyword( def on_keyword(
keywords: Set[str], keywords: set[str],
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
*, *,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_command( def on_command(
cmd: Union[str, Tuple[str, ...]], cmd: str | tuple[str, ...],
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ..., aliases: set[str | tuple[str, ...]] | None = ...,
force_whitespace: Optional[Union[str, bool]] = ..., force_whitespace: str | bool | None = ...,
*, *,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_shell_command( def on_shell_command(
cmd: Union[str, Tuple[str, ...]], cmd: str | tuple[str, ...],
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ..., aliases: set[str | tuple[str, ...]] | None = ...,
parser: Optional[ArgumentParser] = ..., parser: ArgumentParser | None = ...,
*, *,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_regex( def on_regex(
pattern: str, pattern: str,
flags: Union[int, re.RegexFlag] = ..., flags: int | re.RegexFlag = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
*, *,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_type( def on_type(
types: Union[Type[Event], Tuple[Type[Event], ...]], types: type[Event] | tuple[type[Event], ...],
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
*, *,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
class CommandGroup: class _Group:
matchers: list[type[Matcher]] = ...
base_kwargs: dict[str, Any] = ...
def _get_final_kwargs(
self, update: dict[str, Any], *, exclude: set[str] | None = None
) -> dict[str, Any]: ...
class CommandGroup(_Group):
basecmd: tuple[str, ...] = ...
prefix_aliases: bool = ...
def __init__( def __init__(
self, self,
cmd: Union[str, Tuple[str, ...]], cmd: str | tuple[str, ...],
prefix_aliases: bool = ...,
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
): ... ): ...
def command( def command(
self, self,
cmd: Union[str, Tuple[str, ...]], cmd: str | tuple[str, ...],
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ..., aliases: set[str | tuple[str, ...]] | None = ...,
force_whitespace: Optional[Union[str, bool]] = ..., force_whitespace: str | bool | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def shell_command( def shell_command(
self, self,
cmd: Union[str, Tuple[str, ...]], cmd: str | tuple[str, ...],
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ..., aliases: set[str | tuple[str, ...]] | None = ...,
parser: Optional[ArgumentParser] = ..., parser: ArgumentParser | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
class MatcherGroup: class MatcherGroup(_Group):
def __init__( def __init__(
self, self,
*, *,
type: str = ..., type: str = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
): ... ): ...
def on( def on(
self, self,
*, *,
type: str = ..., type: str = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_metaevent( def on_metaevent(
self, self,
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_message( def on_message(
self, self,
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_notice( def on_notice(
self, self,
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_request( def on_request(
self, self,
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_startswith( def on_startswith(
self, self,
msg: Union[str, Tuple[str, ...]], msg: str | tuple[str, ...],
*, *,
ignorecase: bool = ..., ignorecase: bool = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_endswith( def on_endswith(
self, self,
msg: Union[str, Tuple[str, ...]], msg: str | tuple[str, ...],
*, *,
ignorecase: bool = ..., ignorecase: bool = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_fullmatch( def on_fullmatch(
self, self,
msg: Union[str, Tuple[str, ...]], msg: str | tuple[str, ...],
*, *,
ignorecase: bool = ..., ignorecase: bool = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_keyword( def on_keyword(
self, self,
keywords: Set[str], keywords: set[str],
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_command( def on_command(
self, self,
cmd: Union[str, Tuple[str, ...]], cmd: str | tuple[str, ...],
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ..., aliases: set[str | tuple[str, ...]] | None = ...,
force_whitespace: Optional[Union[str, bool]] = ..., force_whitespace: str | bool | None = ...,
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_shell_command( def on_shell_command(
self, self,
cmd: Union[str, Tuple[str, ...]], cmd: str | tuple[str, ...],
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ..., aliases: set[str | tuple[str, ...]] | None = ...,
parser: Optional[ArgumentParser] = ..., parser: ArgumentParser | None = ...,
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_regex( def on_regex(
self, self,
pattern: str, pattern: str,
flags: Union[int, re.RegexFlag] = ..., flags: int | re.RegexFlag = ...,
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...
def on_type( def on_type(
self, self,
types: Union[Type[Event], Tuple[Type[Event]]], types: type[Event] | tuple[type[Event]],
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Rule | T_RuleChecker | None = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Permission | T_PermissionChecker | None = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: list[T_Handler | Dependent] | None = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ..., expire_time: datetime | timedelta | None = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: T_State | None = ...,
) -> Type[Matcher]: ... ) -> type[Matcher]: ...

View File

@@ -1,7 +1,18 @@
from nonebot import on_command
from nonebot.rule import to_me from nonebot.rule import to_me
from nonebot.adapters import Message from nonebot.adapters import Message
from nonebot.params import CommandArg from nonebot.params import CommandArg
from nonebot.plugin import on_command from nonebot.plugin import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="echo",
description="重复你说的话",
usage="/echo [text]",
type="application",
homepage="https://github.com/nonebot/nonebot2/blob/master/nonebot/plugins/echo.py",
config=None,
supported_adapters=None,
)
echo = on_command("echo", to_me()) echo = on_command("echo", to_me())

View File

@@ -2,8 +2,19 @@ from typing import Dict, AsyncGenerator
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.params import Depends from nonebot.params import Depends
from nonebot.plugin import PluginMetadata
from nonebot.message import IgnoredException, event_preprocessor from nonebot.message import IgnoredException, event_preprocessor
__plugin_meta__ = PluginMetadata(
name="唯一会话",
description="限制同一会话内同时只能运行一个响应器",
usage="加载插件后自动生效",
type="application",
homepage="https://github.com/nonebot/nonebot2/blob/master/nonebot/plugins/single_session.py",
config=None,
supported_adapters=None,
)
_running_matcher: Dict[str, int] = {} _running_matcher: Dict[str, int] = {}
@@ -15,7 +26,7 @@ async def matcher_mutex(event: Event) -> AsyncGenerator[bool, None]:
yield result yield result
else: else:
current_event_id = id(event) current_event_id = id(event)
if event_id := _running_matcher.get(session_id, None): if event_id := _running_matcher.get(session_id):
result = event_id != current_event_id result = event_id != current_event_id
else: else:
_running_matcher[session_id] = current_event_id _running_matcher[session_id] = current_event_id

View File

@@ -1,7 +1,8 @@
"""本模块是 {ref}`nonebot.matcher.Matcher.rule` 的类型定义。 """本模块是 {ref}`nonebot.matcher.Matcher.rule` 的类型定义。
每个事件响应器 {ref}`nonebot.matcher.Matcher` 拥有一个匹配规则 {ref}`nonebot.rule.Rule` 每个{ref}`事件响应器 <nonebot.matcher.Matcher>`拥有一个
其中是 `RuleChecker` 的集合,只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行 {ref}`nonebot.rule.Rule`,其中是 `RuleChecker` 的集合
只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。
FrontMatter: FrontMatter:
sidebar_position: 5 sidebar_position: 5
@@ -60,20 +61,19 @@ from nonebot.consts import (
T = TypeVar("T") T = TypeVar("T")
CMD_RESULT = TypedDict(
"CMD_RESULT",
{
"command": Optional[Tuple[str, ...]],
"raw_command": Optional[str],
"command_arg": Optional[Message],
"command_start": Optional[str],
"command_whitespace": Optional[str],
},
)
TRIE_VALUE = NamedTuple( class CMD_RESULT(TypedDict):
"TRIE_VALUE", [("command_start", str), ("command", Tuple[str, ...])] command: Optional[Tuple[str, ...]]
) raw_command: Optional[str]
command_arg: Optional[Message]
command_start: Optional[str]
command_whitespace: Optional[str]
class TRIE_VALUE(NamedTuple):
command_start: str
command: Tuple[str, ...]
parser_message: ContextVar[str] = ContextVar("parser_message") parser_message: ContextVar[str] = ContextVar("parser_message")
@@ -406,7 +406,7 @@ def command(
force_whitespace: 是否强制命令后必须有指定空白符 force_whitespace: 是否强制命令后必须有指定空白符
用法: 用法:
使用默认 `command_start`, `command_sep` 配置 使用默认 `command_start`, `command_sep` 配置情况下:
命令 `("test",)` 可以匹配: `/test` 开头的消息 命令 `("test",)` 可以匹配: `/test` 开头的消息
命令 `("test", "sub")` 可以匹配: `/test.sub` 开头的消息 命令 `("test", "sub")` 可以匹配: `/test.sub` 开头的消息
@@ -441,6 +441,8 @@ def command(
class ArgumentParser(ArgParser): class ArgumentParser(ArgParser):
"""`shell_like` 命令参数解析器,解析出错时不会退出程序。 """`shell_like` 命令参数解析器,解析出错时不会退出程序。
支持 {ref}`nonebot.adapters.Message` 富文本解析。
用法: 用法:
用法与 `argparse.ArgumentParser` 相同, 用法与 `argparse.ArgumentParser` 相同,
参考文档: [argparse](https://docs.python.org/3/library/argparse.html) 参考文档: [argparse](https://docs.python.org/3/library/argparse.html)
@@ -588,10 +590,14 @@ def shell_command(
根据配置里提供的 {ref}``command_start` <nonebot.config.Config.command_start>`, 根据配置里提供的 {ref}``command_start` <nonebot.config.Config.command_start>`,
{ref}``command_sep` <nonebot.config.Config.command_sep>` 判断消息是否为命令。 {ref}``command_sep` <nonebot.config.Config.command_sep>` 判断消息是否为命令。
可以通过 {ref}`nonebot.params.Command` 获取匹配成功的命令(例: `("test",)` 可以通过 {ref}`nonebot.params.Command` 获取匹配成功的命令
通过 {ref}`nonebot.params.RawCommand` 获取匹配成功的原始命令文本(例: `"/test"` (例: `("test",)`
通过 {ref}`nonebot.params.ShellCommandArgv` 获取解析前的参数列表(例: `["arg", "-h"]` 通过 {ref}`nonebot.params.RawCommand` 获取匹配成功的原始命令文本
通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典(例: `{"arg": "arg", "h": True}` (例: `"/test"`
通过 {ref}`nonebot.params.ShellCommandArgv` 获取解析前的参数列表
(例: `["arg", "-h"]`
通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典
(例: `{"arg": "arg", "h": True}`)。
:::warning 警告 :::warning 警告
如果参数解析失败,则通过 {ref}`nonebot.params.ShellCommandArgs` 如果参数解析失败,则通过 {ref}`nonebot.params.ShellCommandArgs`
@@ -603,7 +609,8 @@ def shell_command(
parser: {ref}`nonebot.rule.ArgumentParser` 对象 parser: {ref}`nonebot.rule.ArgumentParser` 对象
用法: 用法:
使用默认 `command_start`, `command_sep` 配置,更多示例参考 `argparse` 标准库文档。 使用默认 `command_start`, `command_sep` 配置,更多示例参考
[argparse](https://docs.python.org/3/library/argparse.html) 标准库文档。
```python ```python
from nonebot.rule import ArgumentParser from nonebot.rule import ArgumentParser
@@ -698,7 +705,8 @@ def regex(regex: str, flags: Union[int, re.RegexFlag] = 0) -> Rule:
::: :::
:::tip 提示 :::tip 提示
正则表达式匹配使用 `EventMessage` 的 `str` 字符串,而非 `EventMessage` 的 `PlainText` 纯文本字符串 正则表达式匹配使用 `EventMessage` 的 `str` 字符串,
而非 `EventMessage` 的 `PlainText` 纯文本字符串
::: :::
""" """

View File

@@ -1,17 +1,17 @@
"""本模块定义了 NoneBot 模块中共享的一些类型。 """本模块定义了 NoneBot 模块中共享的一些类型。
下面的文档中,「类型」部分使用 Python 的 Type Hint 语法, 使用 Python 的 Type Hint 语法,
参考 [`PEP 484`](https://www.python.org/dev/peps/pep-0484/), 参考 [`PEP 484`](https://www.python.org/dev/peps/pep-0484/),
[`PEP 526`](https://www.python.org/dev/peps/pep-0526/) 和 [`PEP 526`](https://www.python.org/dev/peps/pep-0526/) 和
[`typing`](https://docs.python.org/3/library/typing.html)。 [`typing`](https://docs.python.org/3/library/typing.html)。
除了 Python 内置的类型,下面还出现了如下 NoneBot 自定类型,实际上它们是 Python 内置类型的别名。
FrontMatter: FrontMatter:
sidebar_position: 11 sidebar_position: 11
description: nonebot.typing 模块 description: nonebot.typing 模块
""" """
import warnings
from typing_extensions import ParamSpec, TypeAlias, override
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
@@ -30,28 +30,31 @@ if TYPE_CHECKING:
from nonebot.permission import Permission from nonebot.permission import Permission
T = TypeVar("T") T = TypeVar("T")
P = ParamSpec("P")
T_Wrapped = TypeVar("T_Wrapped", bound=Callable) T_Wrapped: TypeAlias = Callable[P, T]
def overrides(InterfaceClass: object) -> Callable[[T_Wrapped], T_Wrapped]: def overrides(InterfaceClass: object):
"""标记一个方法为父类 interface 的 implement""" """标记一个方法为父类 interface 的 implement"""
def overrider(func: T_Wrapped) -> T_Wrapped: warnings.warn(
assert func.__name__ in dir(InterfaceClass), f"Error method: {func.__name__}" "overrides is deprecated and will be removed in a future version, "
return func "use @typing_extensions.override instead. "
"See [PEP 698](https://peps.python.org/pep-0698/) for more details.",
return overrider DeprecationWarning,
)
return override
# state # state
T_State = Dict[Any, Any] T_State: TypeAlias = Dict[Any, Any]
"""事件处理状态 State 类型""" """事件处理状态 State 类型"""
_DependentCallable = Union[Callable[..., T], Callable[..., Awaitable[T]]] _DependentCallable: TypeAlias = Union[Callable[..., T], Callable[..., Awaitable[T]]]
# driver hooks # driver hooks
T_BotConnectionHook = _DependentCallable[Any] T_BotConnectionHook: TypeAlias = _DependentCallable[Any]
"""Bot 连接建立时钩子函数 """Bot 连接建立时钩子函数
依赖参数: 依赖参数:
@@ -60,7 +63,7 @@ T_BotConnectionHook = _DependentCallable[Any]
- BotParam: Bot 对象 - BotParam: Bot 对象
- DefaultParam: 带有默认值的参数 - DefaultParam: 带有默认值的参数
""" """
T_BotDisconnectionHook = _DependentCallable[Any] T_BotDisconnectionHook: TypeAlias = _DependentCallable[Any]
"""Bot 连接断开时钩子函数 """Bot 连接断开时钩子函数
依赖参数: 依赖参数:
@@ -71,15 +74,15 @@ T_BotDisconnectionHook = _DependentCallable[Any]
""" """
# api hooks # api hooks
T_CallingAPIHook = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]] T_CallingAPIHook: TypeAlias = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]]
"""`bot.call_api` 钩子函数""" """`bot.call_api` 钩子函数"""
T_CalledAPIHook = Callable[ T_CalledAPIHook: TypeAlias = Callable[
["Bot", Optional[Exception], str, Dict[str, Any], Any], Awaitable[Any] ["Bot", Optional[Exception], str, Dict[str, Any], Any], Awaitable[Any]
] ]
"""`bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result""" """`bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result"""
# event hooks # event hooks
T_EventPreProcessor = _DependentCallable[Any] T_EventPreProcessor: TypeAlias = _DependentCallable[Any]
"""事件预处理函数 EventPreProcessor 类型 """事件预处理函数 EventPreProcessor 类型
依赖参数: 依赖参数:
@@ -90,7 +93,7 @@ T_EventPreProcessor = _DependentCallable[Any]
- StateParam: State 对象 - StateParam: State 对象
- DefaultParam: 带有默认值的参数 - DefaultParam: 带有默认值的参数
""" """
T_EventPostProcessor = _DependentCallable[Any] T_EventPostProcessor: TypeAlias = _DependentCallable[Any]
"""事件预处理函数 EventPostProcessor 类型 """事件预处理函数 EventPostProcessor 类型
依赖参数: 依赖参数:
@@ -103,7 +106,7 @@ T_EventPostProcessor = _DependentCallable[Any]
""" """
# matcher run hooks # matcher run hooks
T_RunPreProcessor = _DependentCallable[Any] T_RunPreProcessor: TypeAlias = _DependentCallable[Any]
"""事件响应器运行前预处理函数 RunPreProcessor 类型 """事件响应器运行前预处理函数 RunPreProcessor 类型
依赖参数: 依赖参数:
@@ -115,7 +118,7 @@ T_RunPreProcessor = _DependentCallable[Any]
- MatcherParam: Matcher 对象 - MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数 - DefaultParam: 带有默认值的参数
""" """
T_RunPostProcessor = _DependentCallable[Any] T_RunPostProcessor: TypeAlias = _DependentCallable[Any]
"""事件响应器运行后后处理函数 RunPostProcessor 类型 """事件响应器运行后后处理函数 RunPostProcessor 类型
依赖参数: 依赖参数:
@@ -130,7 +133,7 @@ T_RunPostProcessor = _DependentCallable[Any]
""" """
# rule, permission # rule, permission
T_RuleChecker = _DependentCallable[bool] T_RuleChecker: TypeAlias = _DependentCallable[bool]
"""RuleChecker 即判断是否响应事件的处理函数。 """RuleChecker 即判断是否响应事件的处理函数。
依赖参数: 依赖参数:
@@ -141,7 +144,7 @@ T_RuleChecker = _DependentCallable[bool]
- StateParam: State 对象 - StateParam: State 对象
- DefaultParam: 带有默认值的参数 - DefaultParam: 带有默认值的参数
""" """
T_PermissionChecker = _DependentCallable[bool] T_PermissionChecker: TypeAlias = _DependentCallable[bool]
"""PermissionChecker 即判断事件是否满足权限的处理函数。 """PermissionChecker 即判断事件是否满足权限的处理函数。
依赖参数: 依赖参数:
@@ -152,10 +155,11 @@ T_PermissionChecker = _DependentCallable[bool]
- DefaultParam: 带有默认值的参数 - DefaultParam: 带有默认值的参数
""" """
T_Handler = _DependentCallable[Any] T_Handler: TypeAlias = _DependentCallable[Any]
"""Handler 处理函数。""" """Handler 处理函数。"""
T_TypeUpdater = _DependentCallable[str] T_TypeUpdater: TypeAlias = _DependentCallable[str]
"""TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。默认会更新为 `message`。 """TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。
默认会更新为 `message`。
依赖参数: 依赖参数:
@@ -166,8 +170,9 @@ T_TypeUpdater = _DependentCallable[str]
- MatcherParam: Matcher 对象 - MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数 - DefaultParam: 带有默认值的参数
""" """
T_PermissionUpdater = _DependentCallable["Permission"] T_PermissionUpdater: TypeAlias = _DependentCallable["Permission"]
"""PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新会话对象权限。默认会更新为当前事件的触发对象。 """PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新会话对象权限。
默认会更新为当前事件的触发对象。
依赖参数: 依赖参数:
@@ -178,5 +183,5 @@ T_PermissionUpdater = _DependentCallable["Permission"]
- MatcherParam: Matcher 对象 - MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数 - DefaultParam: 带有默认值的参数
""" """
T_DependencyCache = Dict[_DependentCallable[Any], "Task[Any]"] T_DependencyCache: TypeAlias = Dict[_DependentCallable[Any], "Task[Any]"]
"""依赖缓存, 用于存储依赖函数的返回值""" """依赖缓存, 用于存储依赖函数的返回值"""

View File

@@ -15,7 +15,7 @@ from pathlib import Path
from contextvars import copy_context from contextvars import copy_context
from functools import wraps, partial from functools import wraps, partial
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing_extensions import ParamSpec, get_args, get_origin from typing_extensions import ParamSpec, get_args, override, get_origin
from typing import ( from typing import (
Any, Any,
Type, Type,
@@ -33,7 +33,6 @@ from typing import (
from pydantic.typing import is_union, is_none_type from pydantic.typing import is_union, is_none_type
from nonebot.log import logger from nonebot.log import logger
from nonebot.typing import overrides
P = ParamSpec("P") P = ParamSpec("P")
R = TypeVar("R") R = TypeVar("R")
@@ -58,8 +57,13 @@ def generic_check_issubclass(
) -> bool: ) -> bool:
"""检查 cls 是否是 class_or_tuple 中的一个类型子类。 """检查 cls 是否是 class_or_tuple 中的一个类型子类。
特别的,如果 cls 是 `typing.Union` 或 `types.UnionType` 类型, 特别的
则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。
- 如果 cls 是 `typing.Union` 或 `types.UnionType` 类型,
则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。
- 如果 cls 是 `typing.TypeVar` 类型,
则会检查其 `__bound__` 或 `__constraints__`
是否是 class_or_tuple 中一个类型的子类或 None。
""" """
try: try:
return issubclass(cls, class_or_tuple) return issubclass(cls, class_or_tuple)
@@ -70,8 +74,18 @@ def generic_check_issubclass(
is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple) is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
for type_ in get_args(cls) for type_ in get_args(cls)
) )
# ensure generic List, Dict can be checked
elif origin: elif origin:
return issubclass(origin, class_or_tuple) return issubclass(origin, class_or_tuple)
elif isinstance(cls, TypeVar):
if cls.__constraints__:
return all(
is_none_type(type_)
or generic_check_issubclass(type_, class_or_tuple)
for type_ in cls.__constraints__
)
elif cls.__bound__:
return generic_check_issubclass(cls.__bound__, class_or_tuple)
return False return False
@@ -157,6 +171,17 @@ async def run_coro_with_catch(
exc: Tuple[Type[Exception], ...], exc: Tuple[Type[Exception], ...],
return_on_err: Optional[R] = None, return_on_err: Optional[R] = None,
) -> Optional[Union[T, R]]: ) -> Optional[Union[T, R]]:
"""运行协程并当遇到指定异常时返回指定值。
参数:
coro: 要运行的协程
exc: 要捕获的异常
return_on_err: 当发生异常时返回的值
返回:
协程的返回值或发生异常时的指定值
"""
try: try:
return await coro return await coro
except exc: except exc:
@@ -196,9 +221,9 @@ def resolve_dot_notation(
class DataclassEncoder(json.JSONEncoder): class DataclassEncoder(json.JSONEncoder):
"""在JSON序列化 {ref}`nonebot.adapters.Message` (List[Dataclass]) 时使用的 `JSONEncoder`""" """可以序列化 {ref}`nonebot.adapters.Message`(List[Dataclass]) 的 `JSONEncoder`"""
@overrides(json.JSONEncoder) @override
def default(self, o): def default(self, o):
if dataclasses.is_dataclass(o): if dataclasses.is_dataclass(o):
return {f.name: getattr(o, f.name) for f in dataclasses.fields(o)} return {f.name: getattr(o, f.name) for f in dataclasses.fields(o)}
@@ -214,9 +239,11 @@ def logger_wrapper(logger_name: str):
返回: 返回:
日志记录函数 日志记录函数
- level: 日志等级 日志记录函数的参数:
- message: 日志信息
- exception: 异常信息 - level: 日志等级
- message: 日志信息
- exception: 异常信息
""" """
def log(level: str, message: str, exception: Optional[Exception] = None): def log(level: str, message: str, exception: Optional[Exception] = None):

View File

@@ -11,10 +11,12 @@
"start": "yarn workspace nonebot start", "start": "yarn workspace nonebot start",
"serve": "yarn workspace nonebot serve", "serve": "yarn workspace nonebot serve",
"clear": "yarn workspace nonebot clear", "clear": "yarn workspace nonebot clear",
"prettier": "prettier --config ./.prettierrc --write \"./website/\"" "prettier": "prettier --config ./.prettierrc --write \"./website/\"",
"pyright": "pyright"
}, },
"devDependencies": { "devDependencies": {
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"prettier": "^2.5.0" "prettier": "^2.5.0",
"pyright": "^1.1.317"
} }
} }

View File

@@ -22,6 +22,6 @@ _✨ NoneBot 本地文档插件 ✨_
## 使用方式 ## 使用方式
加载插件并启动 Bot ,在浏览器内打开 `http://host:port/docs/` 加载插件并启动 Bot ,在浏览器内打开 `http://host:port/website/`
具体网址会在控制台内输出。 具体网址会在控制台内输出。

View File

@@ -2,6 +2,17 @@ import importlib
import nonebot import nonebot
from nonebot.log import logger from nonebot.log import logger
from nonebot.plugin import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="NoneBot 离线文档",
description="在本地查看 NoneBot 文档",
usage="启动机器人后访问 http://localhost:port/website/ 查看文档",
type="application",
homepage="https://github.com/nonebot/nonebot2/blob/master/packages/nonebot-plugin-docs",
config=None,
supported_adapters=None,
)
def init(): def init():
@@ -17,7 +28,7 @@ def init():
register_route(driver) register_route(driver)
host = str(driver.config.host) host = str(driver.config.host)
port = driver.config.port port = driver.config.port
if host in ["0.0.0.0", "127.0.0.1"]: if host in {"0.0.0.0", "127.0.0.1"}:
host = "localhost" host = "localhost"
logger.opt(colors=True).info( logger.opt(colors=True).info(
f"Nonebot docs will be running at: " f"Nonebot docs will be running at: "

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "nonebot-plugin-docs" name = "nonebot-plugin-docs"
version = "2.0.0-beta.1" version = "2.0.0"
description = "View NoneBot2 Docs Locally" description = "View NoneBot2 Docs Locally"
authors = ["yanyongyu <yyy@nonebot.dev>"] authors = ["yanyongyu <yyy@nonebot.dev>"]
license = "MIT" license = "MIT"
@@ -13,7 +13,7 @@ include = ["nonebot_plugin_docs/dist/**/*"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.8"
nonebot2 = "^2.0.0-beta.1" nonebot2 = "^2.0.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]

1242
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "nonebot2" name = "nonebot2"
version = "2.0.0" version = "2.0.1"
description = "An asynchronous python bot framework." description = "An asynchronous python bot framework."
authors = ["yanyongyu <yyy@nonebot.dev>"] authors = ["yanyongyu <yyy@nonebot.dev>"]
license = "MIT" license = "MIT"
@@ -43,18 +43,19 @@ httpx = { version = ">=0.20.0,<1.0.0", extras = ["http2"], optional = true }
uvicorn = { version = ">=0.20.0,<1.0.0", extras = ["standard"], optional = true } uvicorn = { version = ">=0.20.0,<1.0.0", extras = ["standard"], optional = true }
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pycln = "^2.1.2"
isort = "^5.10.1" isort = "^5.10.1"
black = "^23.1.0" black = "^23.1.0"
nonemoji = "^0.1.2" nonemoji = "^0.1.2"
pre-commit = "^3.0.0" pre-commit = "^3.0.0"
ruff = ">=0.0.272,<1.0.0"
[tool.poetry.group.test.dependencies] [tool.poetry.group.test.dependencies]
nonebug = "^0.3.0" nonebug = "^0.3.0"
werkzeug = "^2.3.6"
pytest-cov = "^4.0.0" pytest-cov = "^4.0.0"
pytest-xdist = "^3.0.2" pytest-xdist = "^3.0.2"
pytest-asyncio = "^0.21.0" pytest-asyncio = "^0.21.0"
coverage-conditional-plugin = "^0.8.0" coverage-conditional-plugin = "^0.9.0"
[tool.poetry.group.docs.dependencies] [tool.poetry.group.docs.dependencies]
nb-autodoc = "^1.0.0a5" nb-autodoc = "^1.0.0a5"
@@ -68,7 +69,7 @@ fastapi = ["fastapi", "uvicorn"]
all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"] all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_mode = "auto" asyncio_mode = "strict"
addopts = "--cov=nonebot --cov-append --cov-report=term-missing" addopts = "--cov=nonebot --cov-append --cov-report=term-missing"
filterwarnings = [ filterwarnings = [
"error", "error",
@@ -91,12 +92,18 @@ force_sort_within_sections = true
src_paths = ["nonebot", "tests"] src_paths = ["nonebot", "tests"]
extra_standard_library = ["typing_extensions"] extra_standard_library = ["typing_extensions"]
[tool.pycln] [tool.ruff]
path = "." select = ["E", "W", "F", "UP", "C", "T", "PYI", "PT", "Q"]
all = false ignore = ["E402", "C901"]
line-length = 88
target-version = "py38"
[tool.ruff.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = false
[tool.pyright] [tool.pyright]
reportShadowedImports = false
pythonVersion = "3.8" pythonVersion = "3.8"
pythonPlatform = "All" pythonPlatform = "All"
executionEnvironments = [ executionEnvironments = [
@@ -104,6 +111,9 @@ executionEnvironments = [
{ root = "./" }, { root = "./" },
] ]
typeCheckingMode = "basic"
reportShadowedImports = false
[build-system] [build-system]
requires = ["poetry_core>=1.0.0"] requires = ["poetry_core>=1.0.0"]

View File

@@ -1,6 +1,8 @@
LOG_LEVEL=TRACE LOG_LEVEL=TRACE
NICKNAME=["test"] NICKNAME=["test"]
SUPERUSERS=["test", "fake:faketest"] SUPERUSERS=["test", "fake:faketest"]
API_TIMEOUT
SIMPLE_NONE
COMMON_OVERRIDE=new COMMON_OVERRIDE=new
CONFIG_FROM_ENV= CONFIG_FROM_ENV=
CONFIG_OVERRIDE=old CONFIG_OVERRIDE=old

View File

@@ -1,11 +1,15 @@
import os import os
import threading
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Set from typing import TYPE_CHECKING, Set, Generator
import pytest import pytest
from nonebug import NONEBOT_INIT_KWARGS from nonebug import NONEBOT_INIT_KWARGS
from werkzeug.serving import BaseWSGIServer, make_server
import nonebot import nonebot
from nonebot.drivers import URL
from fake_server import request_handler
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}' os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
os.environ["CONFIG_OVERRIDE"] = "new" os.environ["CONFIG_OVERRIDE"] = "new"
@@ -28,3 +32,20 @@ def load_plugin(nonebug_init: None) -> Set["Plugin"]:
def load_builtin_plugin(nonebug_init: None) -> Set["Plugin"]: def load_builtin_plugin(nonebug_init: None) -> Set["Plugin"]:
# preload builtin plugins # preload builtin plugins
return nonebot.load_builtin_plugins("echo", "single_session") return nonebot.load_builtin_plugins("echo", "single_session")
@pytest.fixture(scope="session", autouse=True)
def server() -> Generator[BaseWSGIServer, None, None]:
server = make_server("127.0.0.1", 0, app=request_handler)
thread = threading.Thread(target=server.serve_forever)
thread.start()
try:
yield server
finally:
server.shutdown()
thread.join()
@pytest.fixture(scope="session")
def server_url(server: BaseWSGIServer) -> URL:
return URL(f"http://{server.host}:{server.port}")

69
tests/fake_server.py Normal file
View File

@@ -0,0 +1,69 @@
import json
import base64
from typing import Dict, List, Union, TypeVar
from werkzeug import Request, Response
from werkzeug.datastructures import MultiDict
K = TypeVar("K")
V = TypeVar("V")
def json_safe(string, content_type="application/octet-stream") -> str:
try:
string = string.decode("utf-8")
json.dumps(string)
return string
except (ValueError, TypeError):
return b"".join(
[
b"data:",
content_type.encode("utf-8"),
b";base64,",
base64.b64encode(string),
]
).decode("utf-8")
def flattern(d: "MultiDict[K, V]") -> Dict[K, Union[V, List[V]]]:
return {k: v[0] if len(v) == 1 else v for k, v in d.to_dict(flat=False).items()}
@Request.application
def request_handler(request: Request) -> Response:
try:
_json = json.loads(request.data.decode("utf-8"))
except (ValueError, TypeError):
_json = None
return Response(
json.dumps(
{
"url": request.url,
"method": request.method,
"origin": request.headers.get("X-Forwarded-For", request.remote_addr),
"headers": flattern(
MultiDict((k, v) for k, v in request.headers.items())
),
"args": flattern(request.args),
"form": flattern(request.form),
"data": json_safe(request.data),
"json": _json,
"files": flattern(
MultiDict(
(
k,
json_safe(
v.read(),
request.files[k].content_type
or "application/octet-stream",
),
)
for k, v in request.files.items()
)
),
}
),
status=200,
content_type="application/json",
)

View File

@@ -1 +1,3 @@
assert False import pytest
pytest.fail("should not be imported")

View File

@@ -2,6 +2,7 @@ from nonebot.matcher import Matcher
from nonebot.permission import USER, Permission from nonebot.permission import USER, Permission
default_permission = Permission() default_permission = Permission()
new_permission = Permission()
test_permission_updater = Matcher.new(permission=default_permission) test_permission_updater = Matcher.new(permission=default_permission)
@@ -14,4 +15,4 @@ test_custom_updater = Matcher.new(permission=default_permission)
@test_custom_updater.permission_updater @test_custom_updater.permission_updater
async def _() -> Permission: async def _() -> Permission:
return default_permission return new_permission

View File

@@ -0,0 +1,11 @@
from nonebot.plugin import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="测试插件2",
description="测试继承适配器",
usage="无法使用",
type="application",
homepage="https://nonebot.dev",
supported_adapters={"~onebot.v11", "~onebot.v12"},
extra={"author": "NoneBot"},
)

View File

@@ -1,6 +1,5 @@
from pathlib import Path from pathlib import Path
import nonebot
from nonebot.plugin import PluginManager, _managers from nonebot.plugin import PluginManager, _managers
manager = PluginManager( manager = PluginManager(

View File

@@ -1 +1 @@
from .nested_subplugin2 import a # nopycln: import from .nested_subplugin2 import a # noqa: F401

View File

@@ -1,4 +1,6 @@
from nonebot.adapters import Event, Message from typing_extensions import Annotated
from nonebot.adapters import Message
from nonebot.params import Arg, ArgStr, ArgPlainText from nonebot.params import Arg, ArgStr, ArgPlainText
@@ -12,3 +14,15 @@ async def arg_str(key: str = ArgStr()) -> str:
async def arg_plain_text(key: str = ArgPlainText()) -> str: async def arg_plain_text(key: str = ArgPlainText()) -> str:
return key return key
async def annotated_arg(key: Annotated[Message, Arg()]) -> Message:
return key
async def annotated_arg_str(key: Annotated[str, ArgStr()]) -> str:
return key
async def annotated_arg_plain_text(key: Annotated[str, ArgPlainText()]) -> str:
return key

View File

@@ -1,4 +1,4 @@
from typing import Union from typing import Union, TypeVar
from nonebot.adapters import Bot from nonebot.adapters import Bot
@@ -31,5 +31,19 @@ async def union_bot(b: Union[FooBot, BarBot]) -> Union[FooBot, BarBot]:
return b return b
B = TypeVar("B", bound=Bot)
async def generic_bot(b: B) -> B:
return b
CB = TypeVar("CB", Bot, None)
async def generic_bot_none(b: CB) -> CB:
return b
async def not_bot(b: Union[int, Bot]): async def not_bot(b: Union[int, Bot]):
... ...

View File

@@ -1,4 +1,4 @@
from typing import Union from typing import Union, TypeVar
from nonebot.adapters import Event, Message from nonebot.adapters import Event, Message
from nonebot.params import EventToMe, EventType, EventMessage, EventPlainText from nonebot.params import EventToMe, EventType, EventMessage, EventPlainText
@@ -32,6 +32,20 @@ async def union_event(e: Union[FooEvent, BarEvent]) -> Union[FooEvent, BarEvent]
return e return e
E = TypeVar("E", bound=Event)
async def generic_event(e: E) -> E:
return e
CE = TypeVar("CE", Event, None)
async def generic_event_none(e: CE) -> CE:
return e
async def not_event(e: Union[int, Event]): async def not_event(e: Union[int, Event]):
... ...

View File

@@ -1,3 +1,5 @@
from typing import Union, TypeVar
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.params import Received, LastReceived from nonebot.params import Received, LastReceived
@@ -7,6 +9,50 @@ async def matcher(m: Matcher) -> Matcher:
return m return m
async def legacy_matcher(matcher):
return matcher
async def not_legacy_matcher(matcher: int):
...
class FooMatcher(Matcher):
...
async def sub_matcher(m: FooMatcher) -> FooMatcher:
return m
class BarMatcher(Matcher):
...
async def union_matcher(
m: Union[FooMatcher, BarMatcher]
) -> Union[FooMatcher, BarMatcher]:
return m
M = TypeVar("M", bound=Matcher)
async def generic_matcher(m: M) -> M:
return m
CM = TypeVar("CM", Matcher, None)
async def generic_matcher_none(m: CM) -> CM:
return m
async def not_matcher(m: Union[int, Matcher]):
...
async def receive(e: Event = Received("test")) -> Event: async def receive(e: Event = Received("test")) -> Event:
return e return e

View File

@@ -1 +1 @@
from . import matchers from . import matchers as matchers

View File

@@ -220,7 +220,7 @@ matcher_on_type = on_type(
cmd_group = CommandGroup( cmd_group = CommandGroup(
"test", "prefix",
rule=rule, rule=rule,
permission=permission, permission=permission,
handlers=[handler], handlers=[handler],
@@ -230,8 +230,30 @@ cmd_group = CommandGroup(
block=True, block=True,
state=state, state=state,
) )
matcher_sub_cmd = cmd_group.command("sub") matcher_prefix_cmd = cmd_group.command("sub", aliases={"help", ("help", "foo")})
matcher_sub_shell_cmd = cmd_group.shell_command("sub") matcher_prefix_shell_cmd = cmd_group.shell_command(
"sub", aliases={"help", ("help", "foo")}
)
cmd_group_prefix_aliases = CommandGroup(
"prefix",
prefix_aliases=True,
rule=rule,
permission=permission,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_prefix_aliases_cmd = cmd_group_prefix_aliases.command(
"sub", aliases={"help", ("help", "foo")}
)
matcher_prefix_aliases_shell_cmd = cmd_group_prefix_aliases.shell_command(
"sub", aliases={"help", ("help", "foo")}
)
matcher_group = MatcherGroup( matcher_group = MatcherGroup(

View File

@@ -4,4 +4,5 @@ test_require = require("export").test
from plugins.export import test from plugins.export import test
assert test is test_require and test() == "export", "Export Require Error" assert test is test_require, "Export Require Error"
assert test() == "export", "Export Require Error"

View File

@@ -1,135 +1,136 @@
import pytest import pytest
from pydantic import ValidationError, parse_obj_as from pydantic import ValidationError, parse_obj_as
from utils import make_fake_message from nonebot.adapters import Message
from utils import FakeMessage, FakeMessageSegment
def test_segment_add(): def test_segment_data():
Message = make_fake_message() assert len(FakeMessageSegment.text("text")) == 4
MessageSegment = Message.get_segment_class() assert FakeMessageSegment.text("text").get("data") == {"text": "text"}
assert list(FakeMessageSegment.text("text").keys()) == ["type", "data"]
assert MessageSegment.text("text") + MessageSegment.text("text") == Message( assert list(FakeMessageSegment.text("text").values()) == ["text", {"text": "text"}]
[MessageSegment.text("text"), MessageSegment.text("text")] assert list(FakeMessageSegment.text("text").items()) == [
)
assert MessageSegment.text("text") + "text" == Message(
[MessageSegment.text("text"), MessageSegment.text("text")]
)
assert (
MessageSegment.text("text") + Message([MessageSegment.text("text")])
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
assert "text" + MessageSegment.text("text") == Message(
[MessageSegment.text("text"), MessageSegment.text("text")]
)
def test_segment_validate():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
assert parse_obj_as(
MessageSegment,
{"type": "text", "data": {"text": "text"}, "extra": "should be ignored"},
) == MessageSegment.text("text")
with pytest.raises(ValidationError):
parse_obj_as(MessageSegment, "some str")
with pytest.raises(ValidationError):
parse_obj_as(MessageSegment, {"data": {}})
def test_segment_join():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
seg = MessageSegment.text("test")
iterable = [
MessageSegment.text("first"),
Message([MessageSegment.text("second"), MessageSegment.text("third")]),
]
assert seg.join(iterable) == Message(
[
MessageSegment.text("first"),
MessageSegment.text("test"),
MessageSegment.text("second"),
MessageSegment.text("third"),
]
)
def test_segment():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
assert len(MessageSegment.text("text")) == 4
assert MessageSegment.text("text") != MessageSegment.text("other")
assert MessageSegment.text("text").get("data") == {"text": "text"}
assert list(MessageSegment.text("text").keys()) == ["type", "data"]
assert list(MessageSegment.text("text").values()) == ["text", {"text": "text"}]
assert list(MessageSegment.text("text").items()) == [
("type", "text"), ("type", "text"),
("data", {"text": "text"}), ("data", {"text": "text"}),
] ]
origin = MessageSegment.text("text")
def test_segment_equal():
assert FakeMessageSegment("text", {"text": "text"}) == FakeMessageSegment(
"text", {"text": "text"}
)
assert FakeMessageSegment("text", {"text": "text"}) != FakeMessageSegment(
"text", {"text": "other"}
)
assert FakeMessageSegment("text", {"text": "text"}) != FakeMessageSegment(
"other", {"text": "text"}
)
def test_segment_add():
assert FakeMessageSegment.text("text") + FakeMessageSegment.text(
"text"
) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")])
assert FakeMessageSegment.text("text") + "text" == FakeMessage(
[FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]
)
assert (
FakeMessageSegment.text("text") + FakeMessage([FakeMessageSegment.text("text")])
) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")])
assert "text" + FakeMessageSegment.text("text") == FakeMessage(
[FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]
)
def test_segment_validate():
assert parse_obj_as(
FakeMessageSegment,
{"type": "text", "data": {"text": "text"}, "extra": "should be ignored"},
) == FakeMessageSegment.text("text")
with pytest.raises(ValidationError):
parse_obj_as(FakeMessageSegment, "some str")
with pytest.raises(ValidationError):
parse_obj_as(FakeMessageSegment, {"data": {}})
def test_segment_join():
seg = FakeMessageSegment.text("test")
iterable = [
FakeMessageSegment.text("first"),
FakeMessage(
[FakeMessageSegment.text("second"), FakeMessageSegment.text("third")]
),
]
assert seg.join(iterable) == FakeMessage(
[
FakeMessageSegment.text("first"),
FakeMessageSegment.text("test"),
FakeMessageSegment.text("second"),
FakeMessageSegment.text("third"),
]
)
def test_segment_copy():
origin = FakeMessageSegment.text("text")
copy = origin.copy() copy = origin.copy()
assert origin is not copy assert origin is not copy
assert origin == copy assert origin == copy
def test_message_add(): def test_message_add():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
assert ( assert (
Message([MessageSegment.text("text")]) + MessageSegment.text("text") FakeMessage([FakeMessageSegment.text("text")]) + FakeMessageSegment.text("text")
) == Message([MessageSegment.text("text"), MessageSegment.text("text")]) ) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")])
assert Message([MessageSegment.text("text")]) + "text" == Message( assert FakeMessage([FakeMessageSegment.text("text")]) + "text" == FakeMessage(
[MessageSegment.text("text"), MessageSegment.text("text")] [FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]
) )
assert ( assert (
Message([MessageSegment.text("text")]) + Message([MessageSegment.text("text")]) FakeMessage([FakeMessageSegment.text("text")])
) == Message([MessageSegment.text("text"), MessageSegment.text("text")]) + FakeMessage([FakeMessageSegment.text("text")])
) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")])
assert "text" + Message([MessageSegment.text("text")]) == Message( assert "text" + FakeMessage([FakeMessageSegment.text("text")]) == FakeMessage(
[MessageSegment.text("text"), MessageSegment.text("text")] [FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]
) )
msg = Message([MessageSegment.text("text")]) msg = FakeMessage([FakeMessageSegment.text("text")])
msg += MessageSegment.text("text") msg += FakeMessageSegment.text("text")
assert msg == Message([MessageSegment.text("text"), MessageSegment.text("text")]) assert msg == FakeMessage(
[FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]
)
def test_message_getitem(): def test_message_getitem():
Message = make_fake_message() message = FakeMessage(
MessageSegment = Message.get_segment_class()
message = Message(
[ [
MessageSegment.text("test"), FakeMessageSegment.text("test"),
MessageSegment.image("test2"), FakeMessageSegment.image("test2"),
MessageSegment.image("test3"), FakeMessageSegment.image("test3"),
MessageSegment.text("test4"), FakeMessageSegment.text("test4"),
] ]
) )
assert message[0] == MessageSegment.text("test") assert message[0] == FakeMessageSegment.text("test")
assert message[:2] == Message( assert message[:2] == FakeMessage(
[MessageSegment.text("test"), MessageSegment.image("test2")] [FakeMessageSegment.text("test"), FakeMessageSegment.image("test2")]
) )
assert message["image"] == Message( assert message["image"] == FakeMessage(
[MessageSegment.image("test2"), MessageSegment.image("test3")] [FakeMessageSegment.image("test2"), FakeMessageSegment.image("test3")]
) )
assert message["image", 0] == MessageSegment.image("test2") assert message["image", 0] == FakeMessageSegment.image("test2")
assert message["image", 0:2] == message["image"] assert message["image", 0:2] == message["image"]
assert message.index(message[0]) == 0 assert message.index(message[0]) == 0
@@ -137,153 +138,137 @@ def test_message_getitem():
assert message.get("image") == message["image"] assert message.get("image") == message["image"]
assert message.get("image", 114514) == message["image"] assert message.get("image", 114514) == message["image"]
assert message.get("image", 1) == Message([message["image", 0]]) assert message.get("image", 1) == FakeMessage([message["image", 0]])
assert message.count("image") == 2 assert message.count("image") == 2
def test_message_validate(): def test_message_validate():
Message = make_fake_message() assert parse_obj_as(FakeMessage, FakeMessage([])) == FakeMessage([])
MessageSegment = Message.get_segment_class()
Message_ = make_fake_message()
assert parse_obj_as(Message, Message([])) == Message([])
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
parse_obj_as(Message, Message_([])) parse_obj_as(type("FakeMessage2", (Message,), {}), FakeMessage([]))
assert parse_obj_as(Message, "text") == Message([MessageSegment.text("text")]) assert parse_obj_as(FakeMessage, "text") == FakeMessage(
[FakeMessageSegment.text("text")]
assert parse_obj_as(Message, {"type": "text", "data": {"text": "text"}}) == Message(
[MessageSegment.text("text")]
) )
assert parse_obj_as( assert parse_obj_as(
Message, FakeMessage, {"type": "text", "data": {"text": "text"}}
[MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}], ) == FakeMessage([FakeMessageSegment.text("text")])
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
assert parse_obj_as(
FakeMessage,
[FakeMessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}],
) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")])
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
parse_obj_as(Message, object()) parse_obj_as(FakeMessage, object())
def test_message_contains(): def test_message_contains():
Message = make_fake_message() message = FakeMessage(
MessageSegment = Message.get_segment_class()
message = Message(
[ [
MessageSegment.text("test"), FakeMessageSegment.text("test"),
MessageSegment.image("test2"), FakeMessageSegment.image("test2"),
MessageSegment.image("test3"), FakeMessageSegment.image("test3"),
MessageSegment.text("test4"), FakeMessageSegment.text("test4"),
] ]
) )
assert message.has(MessageSegment.text("test")) is True assert message.has(FakeMessageSegment.text("test")) is True
assert MessageSegment.text("test") in message assert FakeMessageSegment.text("test") in message
assert message.has("image") is True assert message.has("image") is True
assert "image" in message assert "image" in message
assert message.has(MessageSegment.text("foo")) is False assert message.has(FakeMessageSegment.text("foo")) is False
assert MessageSegment.text("foo") not in message assert FakeMessageSegment.text("foo") not in message
assert message.has("foo") is False assert message.has("foo") is False
assert "foo" not in message assert "foo" not in message
def test_message_only(): def test_message_only():
Message = make_fake_message() message = FakeMessage(
MessageSegment = Message.get_segment_class()
message = Message(
[ [
MessageSegment.text("test"), FakeMessageSegment.text("test"),
MessageSegment.text("test2"), FakeMessageSegment.text("test2"),
] ]
) )
assert message.only("text") is True assert message.only("text") is True
assert message.only(MessageSegment.text("test")) is False assert message.only(FakeMessageSegment.text("test")) is False
message = Message( message = FakeMessage(
[ [
MessageSegment.text("test"), FakeMessageSegment.text("test"),
MessageSegment.image("test2"), FakeMessageSegment.image("test2"),
MessageSegment.image("test3"), FakeMessageSegment.image("test3"),
MessageSegment.text("test4"), FakeMessageSegment.text("test4"),
] ]
) )
assert message.only("text") is False assert message.only("text") is False
message = Message( message = FakeMessage(
[ [
MessageSegment.text("test"), FakeMessageSegment.text("test"),
MessageSegment.text("test"), FakeMessageSegment.text("test"),
] ]
) )
assert message.only(MessageSegment.text("test")) is True assert message.only(FakeMessageSegment.text("test")) is True
def test_message_join(): def test_message_join():
Message = make_fake_message() msg = FakeMessage([FakeMessageSegment.text("test")])
MessageSegment = Message.get_segment_class()
msg = Message([MessageSegment.text("test")])
iterable = [ iterable = [
MessageSegment.text("first"), FakeMessageSegment.text("first"),
Message([MessageSegment.text("second"), MessageSegment.text("third")]), FakeMessage(
[FakeMessageSegment.text("second"), FakeMessageSegment.text("third")]
),
] ]
assert msg.join(iterable) == Message( assert msg.join(iterable) == FakeMessage(
[ [
MessageSegment.text("first"), FakeMessageSegment.text("first"),
MessageSegment.text("test"), FakeMessageSegment.text("test"),
MessageSegment.text("second"), FakeMessageSegment.text("second"),
MessageSegment.text("third"), FakeMessageSegment.text("third"),
] ]
) )
def test_message_include(): def test_message_include():
Message = make_fake_message() message = FakeMessage(
MessageSegment = Message.get_segment_class()
message = Message(
[ [
MessageSegment.text("test"), FakeMessageSegment.text("test"),
MessageSegment.image("test2"), FakeMessageSegment.image("test2"),
MessageSegment.image("test3"), FakeMessageSegment.image("test3"),
MessageSegment.text("test4"), FakeMessageSegment.text("test4"),
] ]
) )
assert message.include("text") == Message( assert message.include("text") == FakeMessage(
[ [
MessageSegment.text("test"), FakeMessageSegment.text("test"),
MessageSegment.text("test4"), FakeMessageSegment.text("test4"),
] ]
) )
def test_message_exclude(): def test_message_exclude():
Message = make_fake_message() message = FakeMessage(
MessageSegment = Message.get_segment_class()
message = Message(
[ [
MessageSegment.text("test"), FakeMessageSegment.text("test"),
MessageSegment.image("test2"), FakeMessageSegment.image("test2"),
MessageSegment.image("test3"), FakeMessageSegment.image("test3"),
MessageSegment.text("test4"), FakeMessageSegment.text("test4"),
] ]
) )
assert message.exclude("image") == Message( assert message.exclude("image") == FakeMessage(
[ [
MessageSegment.text("test"), FakeMessageSegment.text("test"),
MessageSegment.text("test4"), FakeMessageSegment.text("test4"),
] ]
) )

View File

@@ -1,5 +1,5 @@
from nonebot.adapters import MessageTemplate from nonebot.adapters import MessageTemplate
from utils import escape_text, make_fake_message from utils import FakeMessage, FakeMessageSegment, escape_text
def test_template_basis(): def test_template_basis():
@@ -9,8 +9,7 @@ def test_template_basis():
def test_template_message(): def test_template_message():
Message = make_fake_message() template = FakeMessage.template("{a:custom}{b:text}{c:image}/{d}")
template = Message.template("{a:custom}{b:text}{c:image}/{d}")
@template.add_format_spec @template.add_format_spec
def custom(input: str) -> str: def custom(input: str) -> str:
@@ -37,29 +36,24 @@ def test_template_message():
def test_rich_template_message(): def test_rich_template_message():
Message = make_fake_message()
MS = Message.get_segment_class()
pic1, pic2, pic3 = ( pic1, pic2, pic3 = (
MS.image("file:///pic1.jpg"), FakeMessageSegment.image("file:///pic1.jpg"),
MS.image("file:///pic2.jpg"), FakeMessageSegment.image("file:///pic2.jpg"),
MS.image("file:///pic3.jpg"), FakeMessageSegment.image("file:///pic3.jpg"),
) )
template = Message.template("{}{}" + pic2 + "{}") template = FakeMessage.template("{}{}" + pic2 + "{}")
result = template.format(pic1, "[fake:image]", pic3) result = template.format(pic1, "[fake:image]", pic3)
assert result["image"] == Message([pic1, pic2, pic3]) assert result["image"] == FakeMessage([pic1, pic2, pic3])
assert str(result) == ( assert str(result) == (
"[fake:image]" + escape_text("[fake:image]") + "[fake:image]" + "[fake:image]" "[fake:image]" + escape_text("[fake:image]") + "[fake:image]" + "[fake:image]"
) )
def test_message_injection(): def test_message_injection():
Message = make_fake_message() template = FakeMessage.template("{name}Is Bad")
template = Message.template("{name}Is Bad")
message = template.format(name="[fake:image]") message = template.format(name="[fake:image]")
assert message.extract_plain_text() == escape_text("[fake:image]Is Bad") assert message.extract_plain_text() == escape_text("[fake:image]Is Bad")

View File

@@ -234,7 +234,7 @@ async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
bot = ctx.create_bot() bot = ctx.create_bot()
event = make_fake_event()() event = make_fake_event()()
ctx.receive_event(bot, event) ctx.receive_event(bot, event)
ctx.should_call_send(event, "test", True, bot) ctx.should_call_send(event, "test", True, bot=bot)
assert runned, "run_preprocessor should runned" assert runned, "run_preprocessor should runned"
@@ -346,7 +346,7 @@ async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
bot = ctx.create_bot() bot = ctx.create_bot()
event = make_fake_event()() event = make_fake_event()()
ctx.receive_event(bot, event) ctx.receive_event(bot, event)
ctx.should_call_send(event, "test", True, bot) ctx.should_call_send(event, "test", True, bot=bot)
assert runned, "run_postprocessor should runned" assert runned, "run_postprocessor should runned"

View File

@@ -1,6 +1,6 @@
import json import json
import asyncio import asyncio
from typing import Any, Set, cast from typing import Any, Set, Optional, cast
import pytest import pytest
from nonebug import App from nonebug import App
@@ -79,13 +79,37 @@ async def test_lifespan():
], ],
indirect=True, indirect=True,
) )
async def test_reverse_driver(app: App, driver: Driver): async def test_http_server(app: App, driver: Driver):
driver = cast(ReverseDriver, driver) driver = cast(ReverseDriver, driver)
async def _handle_http(request: Request) -> Response: async def _handle_http(request: Request) -> Response:
assert request.content in (b"test", "test") assert request.content in (b"test", "test")
return Response(200, content="test") return Response(200, content="test")
http_setup = HTTPServerSetup(URL("/http_test"), "POST", "http_test", _handle_http)
driver.setup_http_server(http_setup)
async with app.test_server(driver.asgi) as ctx:
client = ctx.get_client()
response = await client.post("/http_test", data="test")
assert response.status_code == 200
assert response.text == "test"
await asyncio.sleep(1)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"driver",
[
pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"),
pytest.param("nonebot.drivers.quart:Driver", id="quart"),
],
indirect=True,
)
async def test_websocket_server(app: App, driver: Driver):
driver = cast(ReverseDriver, driver)
async def _handle_ws(ws: WebSocket) -> None: async def _handle_ws(ws: WebSocket) -> None:
await ws.accept() await ws.accept()
data = await ws.receive() data = await ws.receive()
@@ -107,17 +131,11 @@ async def test_reverse_driver(app: App, driver: Driver):
with pytest.raises(WebSocketClosed): with pytest.raises(WebSocketClosed):
await ws.receive() await ws.receive()
http_setup = HTTPServerSetup(URL("/http_test"), "POST", "http_test", _handle_http)
driver.setup_http_server(http_setup)
ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws) ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws)
driver.setup_websocket_server(ws_setup) driver.setup_websocket_server(ws_setup)
async with app.test_server(driver.asgi) as ctx: async with app.test_server(driver.asgi) as ctx:
client = ctx.get_client() client = ctx.get_client()
response = await client.post("/http_test", data="test")
assert response.status_code == 200
assert response.text == "test"
async with client.websocket_connect("/ws_test") as ws: async with client.websocket_connect("/ws_test") as ws:
await ws.send_text("ping") await ws.send_text("ping")
@@ -136,6 +154,63 @@ async def test_reverse_driver(app: App, driver: Driver):
await asyncio.sleep(1) await asyncio.sleep(1)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"driver",
[
pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"),
pytest.param("nonebot.drivers.quart:Driver", id="quart"),
],
indirect=True,
)
async def test_cross_context(app: App, driver: Driver):
driver = cast(ReverseDriver, driver)
ws: Optional[WebSocket] = None
ws_ready = asyncio.Event()
ws_should_close = asyncio.Event()
async def background_task():
try:
await ws_ready.wait()
assert ws is not None
await ws.send("ping")
data = await ws.receive()
assert data == "pong"
finally:
ws_should_close.set()
task = asyncio.create_task(background_task())
async def _handle_ws(websocket: WebSocket) -> None:
nonlocal ws
await websocket.accept()
ws = websocket
ws_ready.set()
await ws_should_close.wait()
await websocket.close()
ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws)
driver.setup_websocket_server(ws_setup)
async with app.test_server(driver.asgi) as ctx:
client = ctx.get_client()
async with client.websocket_connect("/ws_test") as websocket:
try:
data = await websocket.receive_text()
assert data == "ping"
await websocket.send_text("pong")
except Exception as e:
if not e.args or "websocket.close" not in str(e.args[0]):
raise
await task
await asyncio.sleep(1)
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"driver", "driver",
@@ -145,43 +220,59 @@ async def test_reverse_driver(app: App, driver: Driver):
], ],
indirect=True, indirect=True,
) )
async def test_http_driver(driver: Driver): async def test_http_client(driver: Driver, server_url: URL):
driver = cast(ForwardDriver, driver) driver = cast(ForwardDriver, driver)
# simple post with query, headers, cookies and content
request = Request( request = Request(
"POST", "POST",
"https://httpbin.org/post", server_url,
params={"param": "test"}, params={"param": "test"},
headers={"X-Test": "test"}, headers={"X-Test": "test"},
cookies={"session": "test"}, cookies={"session": "test"},
content="test", content="test",
) )
response = await driver.request(request) response = await driver.request(request)
assert response.status_code == 200 and response.content assert response.status_code == 200
assert response.content
data = json.loads(response.content) data = json.loads(response.content)
assert data["method"] == "POST"
assert data["args"] == {"param": "test"} assert data["args"] == {"param": "test"}
assert data["headers"].get("X-Test") == "test" assert data["headers"].get("X-Test") == "test"
assert data["headers"].get("Cookie") == "session=test" assert data["headers"].get("Cookie") == "session=test"
assert data["data"] == "test" assert data["data"] == "test"
request = Request("POST", "https://httpbin.org/post", data={"form": "test"}) # post with data body
request = Request("POST", server_url, data={"form": "test"})
response = await driver.request(request) response = await driver.request(request)
assert response.status_code == 200 and response.content assert response.status_code == 200
assert response.content
data = json.loads(response.content) data = json.loads(response.content)
assert data["method"] == "POST"
assert data["form"] == {"form": "test"} assert data["form"] == {"form": "test"}
request = Request("POST", "https://httpbin.org/post", json={"json": "test"}) # post with json body
request = Request("POST", server_url, json={"json": "test"})
response = await driver.request(request) response = await driver.request(request)
assert response.status_code == 200 and response.content assert response.status_code == 200
assert response.content
data = json.loads(response.content) data = json.loads(response.content)
assert data["method"] == "POST"
assert data["json"] == {"json": "test"} assert data["json"] == {"json": "test"}
# post with files and form data
request = Request( request = Request(
"POST", "https://httpbin.org/post", files={"test": ("test.txt", b"test")} "POST",
server_url,
data={"form": "test"},
files={"test": ("test.txt", b"test")},
) )
response = await driver.request(request) response = await driver.request(request)
assert response.status_code == 200 and response.content assert response.status_code == 200
assert response.content
data = json.loads(response.content) data = json.loads(response.content)
assert data["method"] == "POST"
assert data["form"] == {"form": "test"}
assert data["files"] == {"test": "test"} assert data["files"] == {"test": "test"}
await asyncio.sleep(1) await asyncio.sleep(1)
@@ -189,7 +280,7 @@ async def test_http_driver(driver: Driver):
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"driver, driver_type", ("driver", "driver_type"),
[ [
pytest.param( pytest.param(
"nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin", "nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin",
@@ -232,7 +323,6 @@ async def test_bot_connect_hook(app: App, driver: Driver):
@driver.on_bot_connect @driver.on_bot_connect
async def conn_hook(foo: Bot, dep: int = Depends(dependency), default: int = 1): async def conn_hook(foo: Bot, dep: int = Depends(dependency), default: int = 1):
nonlocal conn_should_be_called nonlocal conn_should_be_called
conn_should_be_called = True
if foo is not bot: if foo is not bot:
pytest.fail("on_bot_connect hook called with wrong bot") pytest.fail("on_bot_connect hook called with wrong bot")
@@ -241,12 +331,13 @@ async def test_bot_connect_hook(app: App, driver: Driver):
if default != 1: if default != 1:
pytest.fail("on_bot_connect hook called with wrong default value") pytest.fail("on_bot_connect hook called with wrong default value")
conn_should_be_called = True
@driver.on_bot_disconnect @driver.on_bot_disconnect
async def disconn_hook( async def disconn_hook(
foo: Bot, dep: int = Depends(dependency), default: int = 1 foo: Bot, dep: int = Depends(dependency), default: int = 1
): ):
nonlocal disconn_should_be_called nonlocal disconn_should_be_called
disconn_should_be_called = True
if foo is not bot: if foo is not bot:
pytest.fail("on_bot_disconnect hook called with wrong bot") pytest.fail("on_bot_disconnect hook called with wrong bot")
@@ -255,6 +346,8 @@ async def test_bot_connect_hook(app: App, driver: Driver):
if default != 1: if default != 1:
pytest.fail("on_bot_connect hook called with wrong default value") pytest.fail("on_bot_connect hook called with wrong default value")
disconn_should_be_called = True
if conn_hook not in {hook.call for hook in conn_hooks}: if conn_hook not in {hook.call for hook in conn_hooks}:
pytest.fail("on_bot_connect hook not registered") pytest.fail("on_bot_connect hook not registered")
if disconn_hook not in {hook.call for hook in disconn_hooks}: if disconn_hook not in {hook.call for hook in disconn_hooks}:
@@ -264,6 +357,7 @@ async def test_bot_connect_hook(app: App, driver: Driver):
bot = ctx.create_bot() bot = ctx.create_bot()
await asyncio.sleep(1) await asyncio.sleep(1)
if not conn_should_be_called: if not conn_should_be_called:
pytest.fail("on_bot_connect hook not called") pytest.fail("on_bot_connect hook not called")
if not disconn_should_be_called: if not disconn_should_be_called:

View File

@@ -20,6 +20,11 @@ async def test_init():
assert env == "test" assert env == "test"
config = nonebot.get_driver().config config = nonebot.get_driver().config
assert config.nickname == {"test"}
assert config.superusers == {"test", "fake:faketest"}
assert config.api_timeout is None
assert config.simple_none is None
assert config.config_from_env == {"test": "test"} assert config.config_from_env == {"test": "test"}
assert config.config_override == "new" assert config.config_override == "new"
assert config.config_from_init == "init" assert config.config_from_init == "init"
@@ -31,17 +36,29 @@ async def test_init():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get(app: App, monkeypatch: pytest.MonkeyPatch): async def test_get_driver(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m: with monkeypatch.context() as m:
m.setattr(nonebot, "_driver", None) m.setattr(nonebot, "_driver", None)
with pytest.raises(ValueError): with pytest.raises(ValueError, match="initialized"):
get_driver() get_driver()
@pytest.mark.asyncio
async def test_get_asgi(app: App, monkeypatch: pytest.MonkeyPatch):
driver = get_driver() driver = get_driver()
assert isinstance(driver, ReverseDriver) assert isinstance(driver, ReverseDriver)
assert get_asgi() == driver.asgi assert get_asgi() == driver.asgi
@pytest.mark.asyncio
async def test_get_app(app: App, monkeypatch: pytest.MonkeyPatch):
driver = get_driver()
assert isinstance(driver, ReverseDriver)
assert get_app() == driver.server_app assert get_app() == driver.server_app
@pytest.mark.asyncio
async def test_get_adapter(app: App, monkeypatch: pytest.MonkeyPatch):
async with app.test_api() as ctx: async with app.test_api() as ctx:
adapter = ctx.create_adapter() adapter = ctx.create_adapter()
adapter_name = adapter.get_name() adapter_name = adapter.get_name()
@@ -51,24 +68,38 @@ async def test_get(app: App, monkeypatch: pytest.MonkeyPatch):
assert get_adapters() == {adapter_name: adapter} assert get_adapters() == {adapter_name: adapter}
assert get_adapter(adapter_name) is adapter assert get_adapter(adapter_name) is adapter
assert get_adapter(adapter.__class__) is adapter assert get_adapter(adapter.__class__) is adapter
with pytest.raises(ValueError): with pytest.raises(ValueError, match="registered"):
get_adapter("not exist") get_adapter("not exist")
@pytest.mark.asyncio
async def test_run(app: App, monkeypatch: pytest.MonkeyPatch):
runned = False runned = False
def mock_run(*args, **kwargs): def mock_run(*args, **kwargs):
nonlocal runned nonlocal runned
runned = True runned = True
assert args == ("arg",) and kwargs == {"kwarg": "kwarg"} assert args == ("arg",)
assert kwargs == {"kwarg": "kwarg"}
driver = get_driver()
with monkeypatch.context() as m:
m.setattr(driver, "run", mock_run)
nonebot.run("arg", kwarg="kwarg")
monkeypatch.setattr(driver, "run", mock_run)
nonebot.run("arg", kwarg="kwarg")
assert runned assert runned
with pytest.raises(ValueError):
@pytest.mark.asyncio
async def test_get_bot(app: App, monkeypatch: pytest.MonkeyPatch):
driver = get_driver()
with pytest.raises(ValueError, match="no bots"):
get_bot() get_bot()
monkeypatch.setattr(driver, "_bots", {"test": "test"}) with monkeypatch.context() as m:
assert get_bot() == "test" m.setattr(driver, "_bots", {"test": "test"})
assert get_bot("test") == "test" assert get_bot() == "test"
assert get_bots() == {"test": "test"} assert get_bot("test") == "test"
assert get_bots() == {"test": "test"}

View File

@@ -3,25 +3,16 @@ from nonebug import App
from nonebot.permission import User from nonebot.permission import User
from nonebot.matcher import Matcher, matchers from nonebot.matcher import Matcher, matchers
from utils import FakeMessage, make_fake_event
from nonebot.message import check_and_run_matcher from nonebot.message import check_and_run_matcher
from utils import make_fake_event, make_fake_message
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_matcher(app: App): async def test_matcher_handle(app: App):
from plugins.matcher.matcher_process import ( from plugins.matcher.matcher_process import test_handle
test_got,
test_handle,
test_preset,
test_combine,
test_receive,
test_overload,
)
message = make_fake_message()("text") message = FakeMessage("text")
event = make_fake_event(_message=message)() event = make_fake_event(_message=message)()
message_next = make_fake_message()("text_next")
event_next = make_fake_event(_message=message_next)()
assert len(test_handle.handlers) == 1 assert len(test_handle.handlers) == 1
async with app.test_matcher(test_handle) as ctx: async with app.test_matcher(test_handle) as ctx:
@@ -30,6 +21,16 @@ async def test_matcher(app: App):
ctx.should_call_send(event, "send", "result", at_sender=True) ctx.should_call_send(event, "send", "result", at_sender=True)
ctx.should_finished() ctx.should_finished()
@pytest.mark.asyncio
async def test_matcher_got(app: App):
from plugins.matcher.matcher_process import test_got
message = FakeMessage("text")
event = make_fake_event(_message=message)()
message_next = FakeMessage("text_next")
event_next = make_fake_event(_message=message_next)()
assert len(test_got.handlers) == 1 assert len(test_got.handlers) == 1
async with app.test_matcher(test_got) as ctx: async with app.test_matcher(test_got) as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
@@ -42,6 +43,14 @@ async def test_matcher(app: App):
ctx.should_rejected() ctx.should_rejected()
ctx.receive_event(bot, event_next) ctx.receive_event(bot, event_next)
@pytest.mark.asyncio
async def test_matcher_receive(app: App):
from plugins.matcher.matcher_process import test_receive
message = FakeMessage("text")
event = make_fake_event(_message=message)()
assert len(test_receive.handlers) == 1 assert len(test_receive.handlers) == 1
async with app.test_matcher(test_receive) as ctx: async with app.test_matcher(test_receive) as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
@@ -51,7 +60,17 @@ async def test_matcher(app: App):
ctx.should_call_send(event, "pause", "result", at_sender=True) ctx.should_call_send(event, "pause", "result", at_sender=True)
ctx.should_paused() ctx.should_paused()
assert len(test_receive.handlers) == 1
@pytest.mark.asyncio
async def test_matcher_(app: App):
from plugins.matcher.matcher_process import test_combine
message = FakeMessage("text")
event = make_fake_event(_message=message)()
message_next = FakeMessage("text_next")
event_next = make_fake_event(_message=message_next)()
assert len(test_combine.handlers) == 1
async with app.test_matcher(test_combine) as ctx: async with app.test_matcher(test_combine) as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
ctx.receive_event(bot, event) ctx.receive_event(bot, event)
@@ -64,6 +83,16 @@ async def test_matcher(app: App):
ctx.should_rejected() ctx.should_rejected()
ctx.receive_event(bot, event_next) ctx.receive_event(bot, event_next)
@pytest.mark.asyncio
async def test_matcher_preset(app: App):
from plugins.matcher.matcher_process import test_preset
message = FakeMessage("text")
event = make_fake_event(_message=message)()
message_next = FakeMessage("text_next")
event_next = make_fake_event(_message=message_next)()
assert len(test_preset.handlers) == 2 assert len(test_preset.handlers) == 2
async with app.test_matcher(test_preset) as ctx: async with app.test_matcher(test_preset) as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
@@ -72,6 +101,14 @@ async def test_matcher(app: App):
ctx.should_rejected() ctx.should_rejected()
ctx.receive_event(bot, event_next) ctx.receive_event(bot, event_next)
@pytest.mark.asyncio
async def test_matcher_overload(app: App):
from plugins.matcher.matcher_process import test_overload
message = FakeMessage("text")
event = make_fake_event(_message=message)()
assert len(test_overload.handlers) == 2 assert len(test_overload.handlers) == 2
async with app.test_matcher(test_overload) as ctx: async with app.test_matcher(test_overload) as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
@@ -83,7 +120,7 @@ async def test_matcher(app: App):
async def test_matcher_destroy(app: App): async def test_matcher_destroy(app: App):
from plugins.matcher.matcher_process import test_destroy from plugins.matcher.matcher_process import test_destroy
async with app.test_matcher(test_destroy) as ctx: async with app.test_matcher(test_destroy):
assert len(matchers) == 1 assert len(matchers) == 1
assert len(matchers[test_destroy.priority]) == 1 assert len(matchers[test_destroy.priority]) == 1
assert matchers[test_destroy.priority][0] is test_destroy assert matchers[test_destroy.priority][0] is test_destroy
@@ -115,12 +152,10 @@ async def test_type_updater(app: App):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_permission_updater(app: App): async def test_default_permission_updater(app: App):
from plugins.matcher.matcher_permission import ( from plugins.matcher.matcher_permission import (
default_permission, default_permission,
test_custom_updater,
test_permission_updater, test_permission_updater,
test_user_permission_updater,
) )
event = make_fake_event(_session_id="test")() event = make_fake_event(_session_id="test")()
@@ -136,6 +171,15 @@ async def test_permission_updater(app: App):
assert checker.users == ("test",) assert checker.users == ("test",)
assert checker.perm is default_permission assert checker.perm is default_permission
@pytest.mark.asyncio
async def test_user_permission_updater(app: App):
from plugins.matcher.matcher_permission import (
default_permission,
test_user_permission_updater,
)
event = make_fake_event(_session_id="test")()
user_permission = list(test_user_permission_updater.permission.checkers)[0].call user_permission = list(test_user_permission_updater.permission.checkers)[0].call
assert isinstance(user_permission, User) assert isinstance(user_permission, User)
assert user_permission.perm is default_permission assert user_permission.perm is default_permission
@@ -149,12 +193,22 @@ async def test_permission_updater(app: App):
assert checker.users == ("test",) assert checker.users == ("test",)
assert checker.perm is default_permission assert checker.perm is default_permission
@pytest.mark.asyncio
async def test_custom_permission_updater(app: App):
from plugins.matcher.matcher_permission import (
new_permission,
default_permission,
test_custom_updater,
)
event = make_fake_event(_session_id="test")()
assert test_custom_updater.permission is default_permission assert test_custom_updater.permission is default_permission
async with app.test_api() as ctx: async with app.test_api() as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
matcher = test_custom_updater() matcher = test_custom_updater()
new_perm = await matcher.update_permission(bot, event) new_perm = await matcher.update_permission(bot, event)
assert new_perm is default_permission assert new_perm is new_permission
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -189,12 +243,8 @@ async def test_run(app: App):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_expire(app: App): async def test_temp(app: App):
from plugins.matcher.matcher_expire import ( from plugins.matcher.matcher_expire import test_temp_matcher
test_temp_matcher,
test_datetime_matcher,
test_timedelta_matcher,
)
event = make_fake_event(_type="test")() event = make_fake_event(_type="test")()
async with app.test_api() as ctx: async with app.test_api() as ctx:
@@ -203,6 +253,11 @@ async def test_expire(app: App):
await check_and_run_matcher(test_temp_matcher, bot, event, {}) await check_and_run_matcher(test_temp_matcher, bot, event, {})
assert test_temp_matcher not in matchers[test_temp_matcher.priority] assert test_temp_matcher not in matchers[test_temp_matcher.priority]
@pytest.mark.asyncio
async def test_datetime_expire(app: App):
from plugins.matcher.matcher_expire import test_datetime_matcher
event = make_fake_event()() event = make_fake_event()()
async with app.test_api() as ctx: async with app.test_api() as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
@@ -210,6 +265,11 @@ async def test_expire(app: App):
await check_and_run_matcher(test_datetime_matcher, bot, event, {}) await check_and_run_matcher(test_datetime_matcher, bot, event, {})
assert test_datetime_matcher not in matchers[test_datetime_matcher.priority] assert test_datetime_matcher not in matchers[test_datetime_matcher.priority]
@pytest.mark.asyncio
async def test_timedelta_expire(app: App):
from plugins.matcher.matcher_expire import test_timedelta_matcher
event = make_fake_event()() event = make_fake_event()()
async with app.test_api() as ctx: async with app.test_api() as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()

View File

@@ -6,7 +6,7 @@ from nonebug import App
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.dependencies import Dependent from nonebot.dependencies import Dependent
from nonebot.exception import TypeMisMatch from nonebot.exception import TypeMisMatch
from utils import make_fake_event, make_fake_message from utils import FakeMessage, make_fake_event
from nonebot.params import ( from nonebot.params import (
ArgParam, ArgParam,
BotParam, BotParam,
@@ -33,6 +33,8 @@ from nonebot.consts import (
CMD_WHITESPACE_KEY, CMD_WHITESPACE_KEY,
) )
UNKNOWN_PARAM = "Unknown parameter"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_depend(app: App): async def test_depend(app: App):
@@ -50,7 +52,8 @@ async def test_depend(app: App):
async with app.test_dependent(depends, allow_types=[DependParam]) as ctx: async with app.test_dependent(depends, allow_types=[DependParam]) as ctx:
ctx.should_return(1) ctx.should_return(1)
assert len(runned) == 1 and runned[0] == 1 assert len(runned) == 1
assert runned[0] == 1
runned.clear() runned.clear()
@@ -59,7 +62,8 @@ async def test_depend(app: App):
event_next = make_fake_event()() event_next = make_fake_event()()
ctx.receive_event(bot, event_next) ctx.receive_event(bot, event_next)
assert len(runned) == 2 and runned[0] == runned[1] == 1 assert len(runned) == 2
assert runned[0] == runned[1] == 1
runned.clear() runned.clear()
@@ -90,7 +94,9 @@ async def test_bot(app: App):
sub_bot, sub_bot,
union_bot, union_bot,
legacy_bot, legacy_bot,
generic_bot,
not_legacy_bot, not_legacy_bot,
generic_bot_none,
) )
async with app.test_dependent(get_bot, allow_types=[BotParam]) as ctx: async with app.test_dependent(get_bot, allow_types=[BotParam]) as ctx:
@@ -103,16 +109,15 @@ async def test_bot(app: App):
ctx.pass_params(bot=bot) ctx.pass_params(bot=bot)
ctx.should_return(bot) ctx.should_return(bot)
with pytest.raises(ValueError): with pytest.raises(ValueError, match=UNKNOWN_PARAM):
async with app.test_dependent(not_legacy_bot, allow_types=[BotParam]) as ctx: app.test_dependent(not_legacy_bot, allow_types=[BotParam])
...
async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx: async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot(base=FooBot) bot = ctx.create_bot(base=FooBot)
ctx.pass_params(bot=bot) ctx.pass_params(bot=bot)
ctx.should_return(bot) ctx.should_return(bot)
with pytest.raises(TypeMisMatch): with pytest.raises(TypeMisMatch): # noqa: PT012
async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx: async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
ctx.pass_params(bot=bot) ctx.pass_params(bot=bot)
@@ -122,9 +127,18 @@ async def test_bot(app: App):
ctx.pass_params(bot=bot) ctx.pass_params(bot=bot)
ctx.should_return(bot) ctx.should_return(bot)
with pytest.raises(ValueError): async with app.test_dependent(generic_bot, allow_types=[BotParam]) as ctx:
async with app.test_dependent(not_bot, allow_types=[BotParam]) as ctx: bot = ctx.create_bot()
... ctx.pass_params(bot=bot)
ctx.should_return(bot)
async with app.test_dependent(generic_bot_none, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot()
ctx.pass_params(bot=bot)
ctx.should_return(bot)
with pytest.raises(ValueError, match=UNKNOWN_PARAM):
app.test_dependent(not_bot, allow_types=[BotParam])
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -139,11 +153,13 @@ async def test_event(app: App):
union_event, union_event,
legacy_event, legacy_event,
event_message, event_message,
generic_event,
event_plain_text, event_plain_text,
not_legacy_event, not_legacy_event,
generic_event_none,
) )
fake_message = make_fake_message()("text") fake_message = FakeMessage("text")
fake_event = make_fake_event(_message=fake_message)() fake_event = make_fake_event(_message=fake_message)()
fake_fooevent = make_fake_event(_base=FooEvent)() fake_fooevent = make_fake_event(_base=FooEvent)()
@@ -155,17 +171,14 @@ async def test_event(app: App):
ctx.pass_params(event=fake_event) ctx.pass_params(event=fake_event)
ctx.should_return(fake_event) ctx.should_return(fake_event)
with pytest.raises(ValueError): with pytest.raises(ValueError, match=UNKNOWN_PARAM):
async with app.test_dependent( app.test_dependent(not_legacy_event, allow_types=[EventParam])
not_legacy_event, allow_types=[EventParam]
) as ctx:
...
async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx: async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_fooevent) ctx.pass_params(event=fake_fooevent)
ctx.should_return(fake_fooevent) ctx.should_return(fake_fooevent)
with pytest.raises(TypeMisMatch): with pytest.raises(TypeMisMatch): # noqa: PT012
async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx: async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_event) ctx.pass_params(event=fake_event)
@@ -173,9 +186,16 @@ async def test_event(app: App):
ctx.pass_params(event=fake_fooevent) ctx.pass_params(event=fake_fooevent)
ctx.should_return(fake_event) ctx.should_return(fake_event)
with pytest.raises(ValueError): async with app.test_dependent(generic_event, allow_types=[EventParam]) as ctx:
async with app.test_dependent(not_event, allow_types=[EventParam]) as ctx: ctx.pass_params(event=fake_event)
... ctx.should_return(fake_event)
async with app.test_dependent(generic_event_none, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_event)
ctx.should_return(fake_event)
with pytest.raises(ValueError, match=UNKNOWN_PARAM):
app.test_dependent(not_event, allow_types=[EventParam])
async with app.test_dependent( async with app.test_dependent(
event_type, allow_types=[EventParam, DependParam] event_type, allow_types=[EventParam, DependParam]
@@ -225,7 +245,7 @@ async def test_state(app: App):
shell_command_argv, shell_command_argv,
) )
fake_message = make_fake_message()("text") fake_message = FakeMessage("text")
fake_matched = re.match(r"\[cq:(?P<type>.*?),(?P<arg>.*?)\]", "[cq:test,arg=value]") fake_matched = re.match(r"\[cq:(?P<type>.*?),(?P<arg>.*?)\]", "[cq:test,arg=value]")
fake_state = { fake_state = {
PREFIX_KEY: { PREFIX_KEY: {
@@ -252,11 +272,8 @@ async def test_state(app: App):
ctx.pass_params(state=fake_state) ctx.pass_params(state=fake_state)
ctx.should_return(fake_state) ctx.should_return(fake_state)
with pytest.raises(ValueError): with pytest.raises(ValueError, match=UNKNOWN_PARAM):
async with app.test_dependent( app.test_dependent(not_legacy_state, allow_types=[StateParam])
not_legacy_state, allow_types=[StateParam]
) as ctx:
...
async with app.test_dependent( async with app.test_dependent(
command, allow_types=[StateParam, DependParam] command, allow_types=[StateParam, DependParam]
@@ -351,14 +368,59 @@ async def test_state(app: App):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_matcher(app: App): async def test_matcher(app: App):
from plugins.param.param_matcher import matcher, receive, last_receive from plugins.param.param_matcher import (
FooMatcher,
matcher,
receive,
not_matcher,
sub_matcher,
last_receive,
union_matcher,
legacy_matcher,
generic_matcher,
not_legacy_matcher,
generic_matcher_none,
)
fake_matcher = Matcher() fake_matcher = Matcher()
foo_matcher = FooMatcher()
async with app.test_dependent(matcher, allow_types=[MatcherParam]) as ctx: async with app.test_dependent(matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=fake_matcher) ctx.pass_params(matcher=fake_matcher)
ctx.should_return(fake_matcher) ctx.should_return(fake_matcher)
async with app.test_dependent(legacy_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=fake_matcher)
ctx.should_return(fake_matcher)
with pytest.raises(ValueError, match=UNKNOWN_PARAM):
app.test_dependent(not_legacy_matcher, allow_types=[MatcherParam])
async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=foo_matcher)
ctx.should_return(foo_matcher)
with pytest.raises(TypeMisMatch): # noqa: PT012
async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=fake_matcher)
async with app.test_dependent(union_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=foo_matcher)
ctx.should_return(foo_matcher)
async with app.test_dependent(generic_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=fake_matcher)
ctx.should_return(fake_matcher)
async with app.test_dependent(
generic_matcher_none, allow_types=[MatcherParam]
) as ctx:
ctx.pass_params(matcher=fake_matcher)
ctx.should_return(fake_matcher)
with pytest.raises(ValueError, match=UNKNOWN_PARAM):
app.test_dependent(not_matcher, allow_types=[MatcherParam])
event = make_fake_event()() event = make_fake_event()()
fake_matcher.set_receive("test", event) fake_matcher.set_receive("test", event)
event_next = make_fake_event()() event_next = make_fake_event()()
@@ -379,10 +441,17 @@ async def test_matcher(app: App):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_arg(app: App): async def test_arg(app: App):
from plugins.param.param_arg import arg, arg_str, arg_plain_text from plugins.param.param_arg import (
arg,
arg_str,
annotated_arg,
arg_plain_text,
annotated_arg_str,
annotated_arg_plain_text,
)
matcher = Matcher() matcher = Matcher()
message = make_fake_message()("text") message = FakeMessage("text")
matcher.set_arg("key", message) matcher.set_arg("key", message)
async with app.test_dependent(arg, allow_types=[ArgParam]) as ctx: async with app.test_dependent(arg, allow_types=[ArgParam]) as ctx:
@@ -397,6 +466,20 @@ async def test_arg(app: App):
ctx.pass_params(matcher=matcher) ctx.pass_params(matcher=matcher)
ctx.should_return(message.extract_plain_text()) ctx.should_return(message.extract_plain_text())
async with app.test_dependent(annotated_arg, allow_types=[ArgParam]) as ctx:
ctx.pass_params(matcher=matcher)
ctx.should_return(message)
async with app.test_dependent(annotated_arg_str, allow_types=[ArgParam]) as ctx:
ctx.pass_params(matcher=matcher)
ctx.should_return(str(message))
async with app.test_dependent(
annotated_arg_plain_text, allow_types=[ArgParam]
) as ctx:
ctx.pass_params(matcher=matcher)
ctx.should_return(message.extract_plain_text())
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_exception(app: App): async def test_exception(app: App):

View File

@@ -47,15 +47,15 @@ async def test_permission(app: App):
async with app.test_api() as ctx: async with app.test_api() as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
assert await Permission(falsy)(bot, event) == False assert await Permission(falsy)(bot, event) is False
assert await Permission(truthy)(bot, event) == True assert await Permission(truthy)(bot, event) is True
assert await Permission(skipped)(bot, event) == False assert await Permission(skipped)(bot, event) is False
assert await Permission(truthy, falsy)(bot, event) == True assert await Permission(truthy, falsy)(bot, event) is True
assert await Permission(truthy, skipped)(bot, event) == True assert await Permission(truthy, skipped)(bot, event) is True
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("type, expected", [("message", True), ("notice", False)]) @pytest.mark.parametrize(("type", "expected"), [("message", True), ("notice", False)])
async def test_message(type: str, expected: bool): async def test_message(type: str, expected: bool):
dependent = list(MESSAGE.checkers)[0] dependent = list(MESSAGE.checkers)[0]
checker = dependent.call checker = dependent.call
@@ -67,7 +67,7 @@ async def test_message(type: str, expected: bool):
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("type, expected", [("message", False), ("notice", True)]) @pytest.mark.parametrize(("type", "expected"), [("message", False), ("notice", True)])
async def test_notice(type: str, expected: bool): async def test_notice(type: str, expected: bool):
dependent = list(NOTICE.checkers)[0] dependent = list(NOTICE.checkers)[0]
checker = dependent.call checker = dependent.call
@@ -79,7 +79,7 @@ async def test_notice(type: str, expected: bool):
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("type, expected", [("message", False), ("request", True)]) @pytest.mark.parametrize(("type", "expected"), [("message", False), ("request", True)])
async def test_request(type: str, expected: bool): async def test_request(type: str, expected: bool):
dependent = list(REQUEST.checkers)[0] dependent = list(REQUEST.checkers)[0]
checker = dependent.call checker = dependent.call
@@ -91,7 +91,9 @@ async def test_request(type: str, expected: bool):
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("type, expected", [("message", False), ("meta_event", True)]) @pytest.mark.parametrize(
("type", "expected"), [("message", False), ("meta_event", True)]
)
async def test_metaevent(type: str, expected: bool): async def test_metaevent(type: str, expected: bool):
dependent = list(METAEVENT.checkers)[0] dependent = list(METAEVENT.checkers)[0]
checker = dependent.call checker = dependent.call
@@ -104,7 +106,7 @@ async def test_metaevent(type: str, expected: bool):
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"type, user_id, expected", ("type", "user_id", "expected"),
[ [
("message", "test", True), ("message", "test", True),
("message", "foo", False), ("message", "foo", False),
@@ -128,7 +130,7 @@ async def test_superuser(app: App, type: str, user_id: str, expected: bool):
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"session_ids, session_id, expected", ("session_ids", "session_id", "expected"),
[ [
(("user", "foo"), "user", True), (("user", "foo"), "user", True),
(("user", "foo"), "bar", False), (("user", "foo"), "bar", False),

View File

@@ -6,7 +6,7 @@ from dataclasses import asdict
import pytest import pytest
import nonebot import nonebot
from nonebot.plugin import Plugin, PluginManager, _managers from nonebot.plugin import Plugin, PluginManager, _managers, inherit_supported_adapters
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -49,7 +49,9 @@ 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_subplugin")
sub_plugin2 = nonebot.get_plugin("nested_subplugin2") sub_plugin2 = nonebot.get_plugin("nested_subplugin2")
assert parent_plugin and sub_plugin and sub_plugin2 assert parent_plugin
assert sub_plugin
assert sub_plugin2
assert sub_plugin.parent_plugin is parent_plugin assert sub_plugin.parent_plugin is parent_plugin
assert sub_plugin2.parent_plugin is parent_plugin assert sub_plugin2.parent_plugin is parent_plugin
assert parent_plugin.sub_plugins == {sub_plugin, sub_plugin2} assert parent_plugin.sub_plugins == {sub_plugin, sub_plugin2}
@@ -67,7 +69,7 @@ async def test_load_json():
async def test_load_toml(): async def test_load_toml():
nonebot.load_from_toml("./plugins.toml") nonebot.load_from_toml("./plugins.toml")
with pytest.raises(ValueError): with pytest.raises(ValueError, match="Cannot find"):
nonebot.load_from_toml("./plugins.empty.toml") nonebot.load_from_toml("./plugins.empty.toml")
with pytest.raises(TypeError): with pytest.raises(TypeError):
@@ -145,3 +147,35 @@ async def test_plugin_metadata():
} }
assert plugin.metadata.get_supported_adapters() == {FakeAdapter} assert plugin.metadata.get_supported_adapters() == {FakeAdapter}
@pytest.mark.asyncio
async def test_inherit_supported_adapters():
with pytest.raises(RuntimeError):
inherit_supported_adapters("some_plugin_not_exist")
with pytest.raises(ValueError, match="has no metadata!"):
inherit_supported_adapters("export")
echo = nonebot.get_plugin("echo")
assert echo
assert echo.metadata
assert inherit_supported_adapters("echo") is None
plugin_1 = nonebot.get_plugin("metadata")
assert plugin_1
assert plugin_1.metadata
assert inherit_supported_adapters("metadata") == {
"nonebot.adapters.onebot.v11",
"plugins.metadata:FakeAdapter",
}
plugin_2 = nonebot.get_plugin("metadata_2")
assert plugin_2
assert plugin_2.metadata
assert inherit_supported_adapters("metadata", "metadata_2") == {
"nonebot.adapters.onebot.v11"
}
assert inherit_supported_adapters("metadata", "echo", "metadata_2") == {
"nonebot.adapters.onebot.v11"
}

View File

@@ -20,7 +20,7 @@ from nonebot.rule import (
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"matcher_name, pre_rule_factory, has_permission", ("matcher_name", "pre_rule_factory", "has_permission"),
[ [
pytest.param("matcher_on", None, True), pytest.param("matcher_on", None, True),
pytest.param("matcher_on_metaevent", None, False), pytest.param("matcher_on_metaevent", None, False),
@@ -41,10 +41,30 @@ from nonebot.rule import (
), ),
pytest.param("matcher_on_regex", lambda e: RegexRule("test"), True), pytest.param("matcher_on_regex", lambda e: RegexRule("test"), True),
pytest.param("matcher_on_type", lambda e: IsTypeRule(e), True), pytest.param("matcher_on_type", lambda e: IsTypeRule(e), True),
pytest.param("matcher_sub_cmd", lambda e: CommandRule([("test", "sub")]), True),
pytest.param( pytest.param(
"matcher_sub_shell_cmd", "matcher_prefix_cmd",
lambda e: ShellCommandRule([("test", "sub")], None), lambda e: CommandRule([("prefix", "sub"), ("help",), ("help", "foo")]),
True,
),
pytest.param(
"matcher_prefix_shell_cmd",
lambda e: ShellCommandRule(
[("prefix", "sub"), ("help",), ("help", "foo")], None
),
True,
),
pytest.param(
"matcher_prefix_aliases_cmd",
lambda e: CommandRule(
[("prefix", "sub"), ("prefix", "help"), ("prefix", "help", "foo")]
),
True,
),
pytest.param(
"matcher_prefix_aliases_shell_cmd",
lambda e: ShellCommandRule(
[("prefix", "sub"), ("prefix", "help"), ("prefix", "help", "foo")], None
),
True, True,
), ),
pytest.param("matcher_group_on", None, True), pytest.param("matcher_group_on", None, True),

View File

@@ -6,8 +6,8 @@ import pytest
from nonebug import App from nonebug import App
from nonebot.typing import T_State from nonebot.typing import T_State
from utils import make_fake_event, make_fake_message
from nonebot.exception import ParserExit, SkippedException from nonebot.exception import ParserExit, SkippedException
from utils import FakeMessage, FakeMessageSegment, make_fake_event
from nonebot.consts import ( from nonebot.consts import (
CMD_KEY, CMD_KEY,
PREFIX_KEY, PREFIX_KEY,
@@ -74,35 +74,32 @@ async def test_rule(app: App):
async with app.test_api() as ctx: async with app.test_api() as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
assert await Rule(falsy)(bot, event, {}) == False assert await Rule(falsy)(bot, event, {}) is False
assert await Rule(truthy)(bot, event, {}) == True assert await Rule(truthy)(bot, event, {}) is True
assert await Rule(skipped)(bot, event, {}) == False assert await Rule(skipped)(bot, event, {}) is False
assert await Rule(truthy, falsy)(bot, event, {}) == False assert await Rule(truthy, falsy)(bot, event, {}) is False
assert await Rule(truthy, skipped)(bot, event, {}) == False assert await Rule(truthy, skipped)(bot, event, {}) is False
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_trie(app: App): async def test_trie(app: App):
TrieRule.add_prefix("/fake-prefix", TRIE_VALUE("/", ("fake-prefix",))) TrieRule.add_prefix("/fake-prefix", TRIE_VALUE("/", ("fake-prefix",)))
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
async with app.test_api() as ctx: async with app.test_api() as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
message = Message("/fake-prefix some args") message = FakeMessage("/fake-prefix some args")
event = make_fake_event(_message=message)() event = make_fake_event(_message=message)()
state = {} state = {}
TrieRule.get_value(bot, event, state) TrieRule.get_value(bot, event, state)
assert state[PREFIX_KEY] == CMD_RESULT( assert state[PREFIX_KEY] == CMD_RESULT(
command=("fake-prefix",), command=("fake-prefix",),
raw_command="/fake-prefix", raw_command="/fake-prefix",
command_arg=Message("some args"), command_arg=FakeMessage("some args"),
command_start="/", command_start="/",
command_whitespace=" ", command_whitespace=" ",
) )
message = MessageSegment.text("/fake-prefix ") + MessageSegment.image( message = FakeMessageSegment.text("/fake-prefix ") + FakeMessageSegment.image(
"fake url" "fake url"
) )
event = make_fake_event(_message=message)() event = make_fake_event(_message=message)()
@@ -111,7 +108,7 @@ async def test_trie(app: App):
assert state[PREFIX_KEY] == CMD_RESULT( assert state[PREFIX_KEY] == CMD_RESULT(
command=("fake-prefix",), command=("fake-prefix",),
raw_command="/fake-prefix", raw_command="/fake-prefix",
command_arg=Message(MessageSegment.image("fake url")), command_arg=FakeMessage(FakeMessageSegment.image("fake url")),
command_start="/", command_start="/",
command_whitespace=" ", command_whitespace=" ",
) )
@@ -121,7 +118,7 @@ async def test_trie(app: App):
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"msg, ignorecase, type, text, expected", ("msg", "ignorecase", "type", "text", "expected"),
[ [
("prefix", False, "message", "prefix_", True), ("prefix", False, "message", "prefix_", True),
("prefix", False, "message", "Prefix_", False), ("prefix", False, "message", "Prefix_", False),
@@ -152,7 +149,7 @@ async def test_startswith(
assert checker.msg == msg assert checker.msg == msg
assert checker.ignorecase == ignorecase assert checker.ignorecase == ignorecase
message = text if text is None else make_fake_message()(text) message = text if text is None else FakeMessage(text)
event = make_fake_event(_type=type, _message=message)() event = make_fake_event(_type=type, _message=message)()
for prefix in msg: for prefix in msg:
state = {STARTSWITH_KEY: prefix} state = {STARTSWITH_KEY: prefix}
@@ -161,7 +158,7 @@ async def test_startswith(
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"msg, ignorecase, type, text, expected", ("msg", "ignorecase", "type", "text", "expected"),
[ [
("suffix", False, "message", "_suffix", True), ("suffix", False, "message", "_suffix", True),
("suffix", False, "message", "_Suffix", False), ("suffix", False, "message", "_Suffix", False),
@@ -192,7 +189,7 @@ async def test_endswith(
assert checker.msg == msg assert checker.msg == msg
assert checker.ignorecase == ignorecase assert checker.ignorecase == ignorecase
message = text if text is None else make_fake_message()(text) message = text if text is None else FakeMessage(text)
event = make_fake_event(_type=type, _message=message)() event = make_fake_event(_type=type, _message=message)()
for suffix in msg: for suffix in msg:
state = {ENDSWITH_KEY: suffix} state = {ENDSWITH_KEY: suffix}
@@ -201,7 +198,7 @@ async def test_endswith(
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"msg, ignorecase, type, text, expected", ("msg", "ignorecase", "type", "text", "expected"),
[ [
("fullmatch", False, "message", "fullmatch", True), ("fullmatch", False, "message", "fullmatch", True),
("fullmatch", False, "message", "Fullmatch", False), ("fullmatch", False, "message", "Fullmatch", False),
@@ -232,7 +229,7 @@ async def test_fullmatch(
assert checker.msg == msg assert checker.msg == msg
assert checker.ignorecase == ignorecase assert checker.ignorecase == ignorecase
message = text if text is None else make_fake_message()(text) message = text if text is None else FakeMessage(text)
event = make_fake_event(_type=type, _message=message)() event = make_fake_event(_type=type, _message=message)()
for full in msg: for full in msg:
state = {FULLMATCH_KEY: full} state = {FULLMATCH_KEY: full}
@@ -241,7 +238,7 @@ async def test_fullmatch(
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"kws, type, text, expected", ("kws", "type", "text", "expected"),
[ [
(("key",), "message", "_key_", True), (("key",), "message", "_key_", True),
(("key", "foo"), "message", "_foo_", True), (("key", "foo"), "message", "_foo_", True),
@@ -264,7 +261,7 @@ async def test_keyword(
assert isinstance(checker, KeywordsRule) assert isinstance(checker, KeywordsRule)
assert checker.keywords == kws assert checker.keywords == kws
message = text if text is None else make_fake_message()(text) message = text if text is None else FakeMessage(text)
event = make_fake_event(_type=type, _message=message)() event = make_fake_event(_type=type, _message=message)()
for kw in kws: for kw in kws:
state = {KEYWORD_KEY: kw} state = {KEYWORD_KEY: kw}
@@ -273,26 +270,26 @@ async def test_keyword(
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"cmds, force_whitespace, cmd, whitespace, arg_text, expected", ("cmds", "force_whitespace", "cmd", "whitespace", "arg_text", "expected"),
[ [
# command tests # command tests
[(("help",),), None, ("help",), None, None, True], ((("help",),), None, ("help",), None, None, True),
[(("help",),), None, ("foo",), None, None, False], ((("help",),), None, ("foo",), None, None, False),
[(("help", "foo"),), None, ("help", "foo"), None, None, True], ((("help", "foo"),), None, ("help", "foo"), None, None, True),
[(("help", "foo"),), None, ("help", "bar"), None, None, False], ((("help", "foo"),), None, ("help", "bar"), None, None, False),
[(("help",), ("foo",)), None, ("help",), None, None, True], ((("help",), ("foo",)), None, ("help",), None, None, True),
[(("help",), ("foo",)), None, ("bar",), None, None, False], ((("help",), ("foo",)), None, ("bar",), None, None, False),
# whitespace tests # whitespace tests
[(("help",),), True, ("help",), " ", "arg", True], ((("help",),), True, ("help",), " ", "arg", True),
[(("help",),), True, ("help",), None, "arg", False], ((("help",),), True, ("help",), None, "arg", False),
[(("help",),), True, ("help",), None, None, True], ((("help",),), True, ("help",), None, None, True),
[(("help",),), False, ("help",), " ", "arg", False], ((("help",),), False, ("help",), " ", "arg", False),
[(("help",),), False, ("help",), None, "arg", True], ((("help",),), False, ("help",), None, "arg", True),
[(("help",),), False, ("help",), None, None, True], ((("help",),), False, ("help",), None, None, True),
[(("help",),), " ", ("help",), " ", "arg", True], ((("help",),), " ", ("help",), " ", "arg", True),
[(("help",),), " ", ("help",), "\n", "arg", False], ((("help",),), " ", ("help",), "\n", "arg", False),
[(("help",),), " ", ("help",), None, "arg", False], ((("help",),), " ", ("help",), None, "arg", False),
[(("help",),), " ", ("help",), None, None, True], ((("help",),), " ", ("help",), None, None, True),
], ],
) )
async def test_command( async def test_command(
@@ -310,7 +307,7 @@ async def test_command(
assert isinstance(checker, CommandRule) assert isinstance(checker, CommandRule)
assert checker.cmds == cmds assert checker.cmds == cmds
arg = arg_text if arg_text is None else make_fake_message()(arg_text) arg = arg_text if arg_text is None else FakeMessage(arg_text)
state = { state = {
PREFIX_KEY: {CMD_KEY: cmd, CMD_WHITESPACE_KEY: whitespace, CMD_ARG_KEY: arg} PREFIX_KEY: {CMD_KEY: cmd, CMD_WHITESPACE_KEY: whitespace, CMD_ARG_KEY: arg}
} }
@@ -321,7 +318,7 @@ async def test_command(
async def test_shell_command(): async def test_shell_command():
state: T_State state: T_State
CMD = ("test",) CMD = ("test",)
Message = make_fake_message() Message = FakeMessage
MessageSegment = Message.get_segment_class() MessageSegment = Message.get_segment_class()
test_not_cmd = shell_command(CMD) test_not_cmd = shell_command(CMD)
@@ -427,7 +424,7 @@ async def test_shell_command():
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"pattern, type, text, expected, matched", ("pattern", "type", "text", "expected", "matched"),
[ [
( (
r"(?P<key>key\d)", r"(?P<key>key\d)",
@@ -455,7 +452,7 @@ async def test_regex(
assert isinstance(checker, RegexRule) assert isinstance(checker, RegexRule)
assert checker.regex == pattern assert checker.regex == pattern
message = text if text is None else make_fake_message()(text) message = text if text is None else FakeMessage(text)
event = make_fake_event(_type=type, _message=message)() event = make_fake_event(_type=type, _message=message)()
state = {} state = {}
assert await dependent(event=event, state=state) == expected assert await dependent(event=event, state=state) == expected

View File

@@ -16,21 +16,21 @@ async def test_matcher_mutex():
event_3 = make_fake_event(_session_id=None)() event_3 = make_fake_event(_session_id=None)()
async with am(event) as ctx: async with am(event) as ctx:
assert ctx == False assert ctx is False
assert not _running_matcher assert not _running_matcher
async with am(event) as ctx: async with am(event) as ctx:
async with am(event_1) as ctx_1: async with am(event_1) as ctx_1:
assert ctx == False assert ctx is False
assert ctx_1 == True assert ctx_1 is True
assert not _running_matcher assert not _running_matcher
async with am(event) as ctx: async with am(event) as ctx:
async with am(event_2) as ctx_2: async with am(event_2) as ctx_2:
assert ctx == False assert ctx is False
assert ctx_2 == False assert ctx_2 is False
assert not _running_matcher assert not _running_matcher
async with am(event_3) as ctx_3: async with am(event_3) as ctx_3:
assert ctx_3 == False assert ctx_3 is False
assert not _running_matcher assert not _running_matcher

View File

@@ -1,18 +1,126 @@
import json import json
from typing import Dict, List, Union, TypeVar
from utils import make_fake_message from utils import FakeMessage, FakeMessageSegment
from nonebot.utils import DataclassEncoder from nonebot.utils import (
DataclassEncoder,
escape_tag,
is_gen_callable,
is_async_gen_callable,
is_coroutine_callable,
generic_check_issubclass,
)
def test_loguru_escape_tag():
assert escape_tag("<red>red</red>") == r"\<red>red\</red>"
assert escape_tag("<fg #fff>white</fg #fff>") == r"\<fg #fff>white\</fg #fff>"
assert escape_tag("<fg\n#fff>white</fg\n#fff>") == "\\<fg\n#fff>white\\</fg\n#fff>"
assert escape_tag("<bg #fff>white</bg #fff>") == r"\<bg #fff>white\</bg #fff>"
assert escape_tag("<bg\n#fff>white</bg\n#fff>") == "\\<bg\n#fff>white\\</bg\n#fff>"
def test_generic_check_issubclass():
assert generic_check_issubclass(int, (int, float))
assert not generic_check_issubclass(str, (int, float))
assert generic_check_issubclass(Union[int, float, None], (int, float))
assert generic_check_issubclass(List[int], list)
assert generic_check_issubclass(Dict[str, int], dict)
assert generic_check_issubclass(TypeVar("T", int, float), (int, float))
assert generic_check_issubclass(TypeVar("T", bound=int), (int, float))
def test_is_coroutine_callable():
async def test1():
...
def test2():
...
class TestClass1:
async def __call__(self):
...
class TestClass2:
def __call__(self):
...
assert is_coroutine_callable(test1)
assert not is_coroutine_callable(test2)
assert not is_coroutine_callable(TestClass1)
assert is_coroutine_callable(TestClass1())
assert not is_coroutine_callable(TestClass2)
def test_is_gen_callable():
def test1():
yield
async def test2():
yield
def test3():
...
class TestClass1:
def __call__(self):
yield
class TestClass2:
async def __call__(self):
yield
class TestClass3:
def __call__(self):
...
assert is_gen_callable(test1)
assert not is_gen_callable(test2)
assert not is_gen_callable(test3)
assert is_gen_callable(TestClass1())
assert not is_gen_callable(TestClass2())
assert not is_gen_callable(TestClass3())
def test_is_async_gen_callable():
async def test1():
yield
def test2():
yield
async def test3():
...
class TestClass1:
async def __call__(self):
yield
class TestClass2:
def __call__(self):
yield
class TestClass3:
async def __call__(self):
...
assert is_async_gen_callable(test1)
assert not is_async_gen_callable(test2)
assert not is_async_gen_callable(test3)
assert is_async_gen_callable(TestClass1())
assert not is_async_gen_callable(TestClass2())
assert not is_async_gen_callable(TestClass3())
def test_dataclass_encoder(): def test_dataclass_encoder():
simple = json.dumps("123", cls=DataclassEncoder) simple = json.dumps("123", cls=DataclassEncoder)
assert simple == '"123"' assert simple == '"123"'
Message = make_fake_message() ms = FakeMessageSegment.nested(FakeMessage(FakeMessageSegment.text("text")))
MessageSegment = Message.get_segment_class()
ms = MessageSegment.nested(Message(MessageSegment.text("text")))
s = json.dumps(ms, cls=DataclassEncoder) s = json.dumps(ms, cls=DataclassEncoder)
assert ( assert s == (
s "{"
== '{"type": "node", "data": {"content": [{"type": "text", "data": {"text": "text"}}]}}' '"type": "node", '
'"data": {"content": [{"type": "text", "data": {"text": "text"}}]}'
"}"
) )

View File

@@ -1,6 +1,6 @@
from typing import Type, Union, Mapping, Iterable, Optional from typing import Type, Union, Mapping, Iterable, Optional
from pydantic import create_model from pydantic import Extra, create_model
from nonebot.adapters import Event, Message, MessageSegment from nonebot.adapters import Event, Message, MessageSegment
@@ -12,51 +12,49 @@ def escape_text(s: str, *, escape_comma: bool = True) -> str:
return s return s
def make_fake_message(): class FakeMessageSegment(MessageSegment["FakeMessage"]):
class FakeMessageSegment(MessageSegment["FakeMessage"]): @classmethod
@classmethod def get_message_class(cls):
def get_message_class(cls): return FakeMessage
return FakeMessage
def __str__(self) -> str: def __str__(self) -> str:
return self.data["text"] if self.type == "text" else f"[fake:{self.type}]" return self.data["text"] if self.type == "text" else f"[fake:{self.type}]"
@classmethod @classmethod
def text(cls, text: str): def text(cls, text: str):
return cls("text", {"text": text}) return cls("text", {"text": text})
@staticmethod @staticmethod
def image(url: str): def image(url: str):
return FakeMessageSegment("image", {"url": url}) return FakeMessageSegment("image", {"url": url})
@staticmethod @staticmethod
def nested(content: "FakeMessage"): def nested(content: "FakeMessage"):
return FakeMessageSegment("node", {"content": content}) return FakeMessageSegment("node", {"content": content})
def is_text(self) -> bool: def is_text(self) -> bool:
return self.type == "text" return self.type == "text"
class FakeMessage(Message[FakeMessageSegment]):
@classmethod
def get_segment_class(cls):
return FakeMessageSegment
@staticmethod class FakeMessage(Message[FakeMessageSegment]):
def _construct(msg: Union[str, Iterable[Mapping]]): @classmethod
if isinstance(msg, str): def get_segment_class(cls):
yield FakeMessageSegment.text(msg) return FakeMessageSegment
else:
for seg in msg:
yield FakeMessageSegment(**seg)
return
def __add__( @staticmethod
self, other: Union[str, FakeMessageSegment, Iterable[FakeMessageSegment]] def _construct(msg: Union[str, Iterable[Mapping]]):
): if isinstance(msg, str):
other = escape_text(other) if isinstance(other, str) else other yield FakeMessageSegment.text(msg)
return super().__add__(other) else:
for seg in msg:
yield FakeMessageSegment(**seg)
return
return FakeMessage def __add__(
self, other: Union[str, FakeMessageSegment, Iterable[FakeMessageSegment]]
):
other = escape_text(other) if isinstance(other, str) else other
return super().__add__(other)
def make_fake_event( def make_fake_event(
@@ -70,9 +68,9 @@ def make_fake_event(
_to_me: bool = True, _to_me: bool = True,
**fields, **fields,
) -> Type[Event]: ) -> Type[Event]:
_Fake = create_model("_Fake", __base__=_base or Event, **fields) Base = _base or Event
class FakeEvent(_Fake): class FakeEvent(Base, extra=Extra.forbid):
def get_type(self) -> str: def get_type(self) -> str:
return _type return _type
@@ -100,7 +98,4 @@ def make_fake_event(
def is_tome(self) -> bool: def is_tome(self) -> bool:
return _to_me return _to_me
class Config: return create_model("FakeEvent", __base__=FakeEvent, **fields)
extra = "forbid"
return FakeEvent

View File

@@ -71,7 +71,9 @@ async def _(foo: str = "bar"): ...
获取当前事件的 Bot 对象。 获取当前事件的 Bot 对象。
通过标注参数为 `Bot` 类型,或者一系列 `Bot` 类型,即可获取到当前事件的 Bot 对象。为兼容性考虑,如果参数名为 `bot` 且无类型注解,也会视为 `Bot` 依赖注入。 通过标注参数为 `Bot` 类型,或者一系列 `Bot` 类型,即可获取到当前事件的 Bot 对象。为兼容性考虑,如果参数名为 `bot` 且无类型注解,也会视为 Bot 依赖注入。
Bot 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。
<Tabs groupId="python"> <Tabs groupId="python">
<TabItem value="3.10" label="Python 3.10+" default> <TabItem value="3.10" label="Python 3.10+" default>
@@ -108,7 +110,9 @@ async def _(bot): ... # 兼容性处理
获取当前事件。 获取当前事件。
通过标注参数为 `Event` 类型,或者一系列 `Event` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `event` 且无类型注解,也会视为 `Event` 依赖注入。 通过标注参数为 `Event` 类型,或者一系列 `Event` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `event` 且无类型注解,也会视为 Event 依赖注入。
Event 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。
<Tabs groupId="python"> <Tabs groupId="python">
<TabItem value="3.10" label="Python 3.10+" default> <TabItem value="3.10" label="Python 3.10+" default>
@@ -143,6 +147,8 @@ async def _(event): ... # 兼容性处理
获取当前[会话状态](../appendices/session-state.md)。 获取当前[会话状态](../appendices/session-state.md)。
通过标注参数为 `T_State` 类型,即可获取到当前会话状态。为兼容性考虑,如果参数名为 `state` 且无类型注解,也会视为 State 依赖注入。
```python ```python
from nonebot.typing import T_State from nonebot.typing import T_State
@@ -153,10 +159,15 @@ async def _(foo: T_State): ...
获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。 获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。
通过标注参数为 `Matcher` 类型,或者一系列 `Matcher` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `matcher` 且无类型注解,也会视为 Matcher 依赖注入。
Matcher 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。
```python ```python
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
async def _(matcher: Matcher): ... async def _(foo: Matcher): ...
async def _(matcher): ... # 兼容性处理
``` ```
### Exception ### Exception
@@ -1155,8 +1166,8 @@ from typing import Annotated
from nonebot.params import ArgStr from nonebot.params import ArgStr
@matcher.got("key") @matcher.got("key")
async def _(key: str = ArgStr()): ... async def _(key: Annotated[str, ArgStr()]): ...
async def _(foo: str = ArgStr("key")): ... async def _(foo: Annotated[str, ArgStr("key")]): ...
``` ```
</TabItem> </TabItem>
@@ -1166,8 +1177,8 @@ async def _(foo: str = ArgStr("key")): ...
from nonebot.params import ArgStr from nonebot.params import ArgStr
@matcher.got("key") @matcher.got("key")
async def _(key: Annotated[str, ArgStr()]): ... async def _(key: str = ArgStr()): ...
async def _(foo: Annotated[str, ArgStr("key")]): ... async def _(foo: str = ArgStr("key")): ...
``` ```
</TabItem> </TabItem>

View File

@@ -12,6 +12,10 @@ options:
在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,内置的响应规则,与第三方响应规则拓展。 在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,内置的响应规则,与第三方响应规则拓展。
:::tip 提示
事件响应器允许继承,你可以通过直接继承 `Matcher` 类来创建一个新的事件响应器。
:::
## 事件响应器组成 ## 事件响应器组成
### 事件响应器类型 ### 事件响应器类型
@@ -287,6 +291,19 @@ sub_cmd = group.command("sub")
help_cmd = group.command("help") help_cmd = group.command("help")
``` ```
命令别名 aliases 默认不会添加 `CommandGroup` 设定的前缀,如果需要为 aliases 添加前缀,可以添加 `prefix_aliases=True` 参数:
```python
from nonebot import CommandGroup
group = CommandGroup("cmd", prefix_aliases=True)
cmd = group.command(tuple())
help_cmd = group.command("help", aliases={"帮助"})
```
这样就能成功匹配 `/cmd``/cmd.help``/cmd.帮助` 命令。如果未设置,将默认匹配 `/cmd``/cmd.help``/帮助` 命令。
### `MatcherGroup` ### `MatcherGroup`
`MatcherGroup` 可以用于管理一系列具有相同属性的响应器。 `MatcherGroup` 可以用于管理一系列具有相同属性的响应器。
@@ -307,114 +324,13 @@ matcher2 = group.on_message()
### Alconna ### Alconna
[`nonebot-plugin-alconna`](https://github.com/ArcletProject/nonebot-plugin-alconna) 是一类提供了拓展响应规则的插件。 [`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。
该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器, 该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器,
是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。
特点包括:
- 高效
- 直观的命令组件创建方式
- 强大的类型解析与类型转换功能
- 自定义的帮助信息格式
- 多语言支持
- 易用的快捷命令创建与使用
- 可创建命令补全会话, 以实现多轮连续的补全提示
- 可嵌套的多级子命令
- 正则匹配支持
该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。 该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。
同时,基于 [Annotated 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解 `AlcMatches``AlcResult`
该插件还可以通过 `handle(parameterless)` 来控制一个具体的响应函数是否在不满足条件时跳过响应:
- `pip.handle([Check(assign("add.name", "nb"))])` 表示仅在命令为 `role-group add` 并且 name 为 `nb` 时响应
- `pip.handle([Check(assign("list"))])` 表示仅在命令为 `role-group list` 时响应
- `pip.handle([Check(assign("add"))])` 表示仅在命令为 `role-group add` 时响应
基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。 基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。
标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。 标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。
#### 插件安装 详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna.md) 章节。
```shell
nb plugin install nonebot-plugin-alconna
```
```shell
pip install nonebot-plugin-alconna
```
#### 示例
```python
from nonebot_plugin_alconna.adapters import At
from nonebot.adapters.onebot.v12 import Message
from nonebot_plugin_alconna.adapters.onebot12 import Image
from nonebot_plugin_alconna import AlconnaMatches, on_alconna
from nonebot.adapters.onebot.v12 import MessageSegment as Ob12MS
from arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand
alc = Alconna(
"role-group",
Subcommand(
"add",
Args["name", str],
Option("member", Args["target", MultiVar(At)]),
),
Option("list"),
)
rg = on_alconna(alc, auto_send_output=True)
@rg.handle()
async def _(result: Arparma = AlconnaMatches()):
if result.find("list"):
img = await gen_role_group_list_image()
await rg.finish(Message([Image(img)]))
if result.find("add"):
group = await create_role_group(result["add.name"])
if result.find("add.member"):
ats: tuple[Ob12MS, ...] = result["add.member.target"]
group.extend(member.data["user_id"] for member in ats)
await rg.finish("添加成功")
```
我们可以看到主要的两大组件:`Option``Subcommand`
`Option` 可以传入一组别名,如 `Option("--foo|-F|--FOO|-f")``Option("--foo", alias=["-F"]`
`Subcommand` 则可以传入自己的 `Option``Subcommand`
他们拥有如下共同参数:
- `help_text`: 传入该组件的帮助信息
- `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name)
- `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换
- `default`: 默认值,在该组件未被解析时使用使用该值替换。
其次使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。
`on_alconna` 的所有参数如下:
- `command: Alconna | str`: Alconna 命令
- `skip_for_unmatch: bool = True`: 是否在命令不匹配时跳过该响应
- `auto_send_output: bool = False`: 是否自动发送输出信息并跳过响应
- `output_converter: TConvert | None = None`: 输出信息字符串转换为消息序列方法
- `aliases: set[str | tuple[str, ...]] | None = None`: 命令别名, 作用类似于 `on_command` 中的 aliases
- `comp_config: CompConfig | None = None`: 补全会话配置, 不传入则不启用补全会话
`AlconnaMatches` 是一个依赖注入函数,可注入 `Alconna` 命令解析结果。
#### 参考
插件文档: [📦 这里](https://github.com/ArcletProject/nonebot-plugin-alconna/blob/master/docs.md)
官方文档: [👉 指路](https://arclet.top/)
QQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH)
友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html)

View File

@@ -31,7 +31,7 @@ NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。
我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据,如下所示: 我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据,如下所示:
```python {1,5-11} title=example/__init__.py ```python {1,5-12} title=example/__init__.py
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from .config import Config from .config import Config
@@ -40,12 +40,19 @@ __plugin_meta__ = PluginMetadata(
name="示例插件", name="示例插件",
description="这是一个示例插件", description="这是一个示例插件",
usage="没什么用", usage="没什么用",
type="application",
config=Config, config=Config,
extra={}, extra={},
) )
``` ```
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有个可选的属性。`config` 属性用于指定插件的[配置类](../appendices/config.mdx#插件配置)`extra` 属性,它是一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。 我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#填写插件元数据)章节):
- `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能);
- `homepage`:插件项目主页,发布插件必填;
- `config`:插件的[配置类](../appendices/config.mdx#插件配置),如无配置类可不填;
- `supported_adapters`:支持的适配器模块名集合,若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写;
- `extra`:一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。
请注意,这里的**插件名称**是供使用者或机器人用户查看的,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。 请注意,这里的**插件名称**是供使用者或机器人用户查看的,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。

View File

@@ -25,6 +25,10 @@ options:
这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。 这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。
```python ```python
from nonebot import get_driver
driver = get_driver()
@driver.on_startup @driver.on_startup
async def do_something(): async def do_something():
pass pass
@@ -35,6 +39,10 @@ async def do_something():
这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。 这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。
```python ```python
from nonebot import get_driver
driver = get_driver()
@driver.on_shutdown @driver.on_shutdown
async def do_something(): async def do_something():
pass pass
@@ -45,6 +53,10 @@ async def do_something():
这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。 这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。
```python ```python
from nonebot import get_driver
driver = get_driver()
@driver.on_bot_connect @driver.on_bot_connect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -55,6 +67,10 @@ async def do_something(bot: Bot):
这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。 这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。
```python ```python
from nonebot import get_driver
driver = get_driver()
@driver.on_bot_disconnect @driver.on_bot_disconnect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -82,6 +98,7 @@ async def do_something(event: Event):
```python ```python
from nonebot.message import event_postprocessor from nonebot.message import event_postprocessor
@event_postprocessor @event_postprocessor
async def do_something(event: Event): async def do_something(event: Event):
pass pass
@@ -93,6 +110,7 @@ async def do_something(event: Event):
```python ```python
from nonebot.message import run_preprocessor from nonebot.message import run_preprocessor
@run_preprocessor @run_preprocessor
async def do_something(event: Event, matcher: Matcher): async def do_something(event: Event, matcher: Matcher):
pass pass

View File

@@ -224,6 +224,35 @@ weather = on_command(
发布插件应该为自身的事件响应器提供可配置的优先级,以便插件使用者可以自定义多个插件间的响应顺序。 发布插件应该为自身的事件响应器提供可配置的优先级,以便插件使用者可以自定义多个插件间的响应顺序。
::: :::
由于插件配置项是从全局配置中读取的,通常我们需要在配置项名称前面添加前缀名,以防止配置项冲突。例如在上方的示例中,我们就添加了配置项前缀 `weather_`。但是这样会导致在使用配置项时过长的变量名,因此我们可以使用 `pydantic` 的 `alias` 或者通过配置 scope 来简化配置项名称。这里我们以 scope 配置为例:
```python title=weather/config.py
from pydantic import BaseModel
class ScopedConfig(BaseModel):
api_key: str
command_priority: int = 10
plugin_enabled: bool = True
class Config(BaseModel):
weather: ScopedConfig
```
```python title=weather/__init__.py
from nonebot import get_driver
from .config import Config
plugin_config = Config.parse_obj(get_driver().config).weather
```
这样我们就可以省略插件配置项名称中的前缀 `weather_` 了。但需要注意的是,如果我们使用了 scope 配置,那么在配置文件中也需要使用 [`env_nested_delimiter` 格式](#配置项解析),例如:
```dotenv
WEATHER__API_KEY=123456
WEATHER__COMMAND_PRIORITY=10
```
## 内置配置项 ## 内置配置项
配置项 API 文档可以前往 [Config 类](../api/config.md#Config)查看。 配置项 API 文档可以前往 [Config 类](../api/config.md#Config)查看。

View File

@@ -75,7 +75,7 @@ logger.add(
sys.stdout, sys.stdout,
level=0, level=0,
diagnose=True, diagnose=True,
format="<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{full_name}</u></c> | {message}", format="<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}",
filter=default_filter filter=default_filter
) )
``` ```

View File

@@ -66,7 +66,7 @@ async def handle_onebot(bot: OneBot):
:::warning 注意 :::warning 注意
重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。 重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。
但 BotEvent 者的参数类型注解具有最高检查优先级,如果二者类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。 但 BotEvent 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。
::: :::
:::tip 提示 :::tip 提示

View File

@@ -0,0 +1,288 @@
---
sidebar_position: 6
description: Alconna 命令解析拓展
---
# Alconna 命令解析
[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。
该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器,
是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。
特点包括:
- 高效
- 直观的命令组件创建方式
- 强大的类型解析与类型转换功能
- 自定义的帮助信息格式
- 多语言支持
- 易用的快捷命令创建与使用
- 可创建命令补全会话, 以实现多轮连续的补全提示
- 可嵌套的多级子命令
- 正则匹配支持
该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。
同时,基于 [Annotated 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解 `AlcMatches``AlcResult`
该插件还可以通过 `handle(parameterless)` 来控制一个具体的响应函数是否在不满足条件时跳过响应:
- `pip.handle([Check(assign("add.name", "nb"))])` 表示仅在命令为 `role-group add` 并且 name 为 `nb` 时响应
- `pip.handle([Check(assign("list"))])` 表示仅在命令为 `role-group list` 时响应
- `pip.handle([Check(assign("add"))])` 表示仅在命令为 `role-group add` 时响应
基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。
标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。
## 安装插件
在使用前请先安装 `nonebot-plugin-alconna` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如:
在**项目目录**下执行以下命令:
```shell
nb plugin install nonebot-plugin-alconna
```
```shell
pip install nonebot-plugin-alconna
```
## 使用插件
以下为一个简单的使用示例:
```python
from nonebot_plugin_alconna.adapters import At
from nonebot.adapters.onebot.v12 import Message
from nonebot_plugin_alconna.adapters.onebot12 import Image
from nonebot_plugin_alconna import AlconnaMatches, on_alconna
from nonebot.adapters.onebot.v12 import MessageSegment as Ob12MS
from arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand
alc = Alconna(
["/", "!"],
"role-group",
Subcommand(
"add",
Args["name", str],
Option("member", Args["target", MultiVar(At)]),
),
Option("list"),
)
rg = on_alconna(alc, auto_send_output=True)
@rg.handle()
async def _(result: Arparma = AlconnaMatches()):
if result.find("list"):
img = await gen_role_group_list_image()
await rg.finish(Message([Image(img)]))
if result.find("add"):
group = await create_role_group(result["add.name"])
if result.find("add.member"):
ats: tuple[Ob12MS, ...] = result["add.member.target"]
group.extend(member.data["user_id"] for member in ats)
await rg.finish("添加成功")
```
### 导入插件
由于 `nonebot-plugin-alconna` 作为插件,因此需要在使用前对其进行**加载**并**导入**其中的 `on_alconna` 来使用命令拓展。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解。
```python
from nonebot import require
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna
```
### 命令编写
我们可以看到主要的两大组件:`Option``Subcommand`
`Option` 可以传入一组别名,如 `Option("--foo|-F|--FOO|-f")``Option("--foo", alias=["-F"]`
`Subcommand` 则可以传入自己的 `Option``Subcommand`
他们拥有如下共同参数:
- `help_text`: 传入该组件的帮助信息
- `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name)
- `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换
- `default`: 默认值,在该组件未被解析时使用使用该值替换。
然后是 `Args``MultiVar`,他们是用于解析参数的组件。
`Args` 是参数解析的基础组件,构造方法形如 `Args["foo", str]["bar", int]["baz", bool, False]`
与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。
`MultiVar` 则是一个特殊的标注,用于告知解析器该参数可以接受多个值,其构造方法形如 `MultiVar(str)`
同样的还有 `KeyWordVar`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。
:::tip
`MultiVar``KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,其构造方法形如 `MultiVar(KeyWordVar(str))`
`MultiVar``KeyWordVar` 也可以传入 `default` 参数,用于指定默认值。
`MultiVar` 不能在 `KeyWordVar` 之后传入。
:::
### 参数标注
`Args` 的参数类型表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例。
```python
from arclet.alconna import Args
from nepattern import BasePattern
# 表示 foo 参数需要匹配一个 @number 样式的字符串
args = Args["foo", BasePattern("@\d+")]
```
示例中传入的 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]`
默认支持的类型有:
- `str`: 匹配任意字符串
- `int`: 匹配整数
- `float`: 匹配浮点数
- `bool`: 匹配 `True``False` 以及他们小写形式
- `hex`: 匹配 `0x` 开头的十六进制字符串
- `url`: 匹配网址
- `email`: 匹配 `xxxx@xxx` 的字符串
- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串
- `list`: 匹配类似 `["foo","bar","baz"]` 的字符串
- `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串
- `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳
- `Any`: 匹配任意类型
- `AnyString`: 匹配任意类型,转为 `str`
- `Number`: 匹配 `int``float`,转为 `int`
同时可以使用 typing 中的类型:
- `Literal[X]`: 匹配其中的任意一个值
- `Union[X, Y]`: 匹配其中的任意一个类型
- `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值
- `List[X]`: 匹配一个列表,其中的元素为 `X` 类型
- `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型value 为 `Y` 类型
- ...
:::tip
几类特殊的传入标记:
- `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联)
- `RawStr("foo")`: 匹配字符串 "foo" (不会被 `BasePattern` 替换)
- `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz"
- `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型
- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值
- `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0]
- `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象
- `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值)
- ...
:::
### 消息段标注
示例中使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。
消息段标注会匹配特定的 `MessageSegment`
```python
...
ats: tuple[Ob12MS, ...] = result["add.member.target"]
group.extend(member.data["user_id"] for member in ats)
```
:::tip
通用标注与适配器标注的区别在于,通用标注会匹配多个适配器中相似类型的消息段。
通用标注返回的是 `nonebot_plugin_alconna.adapters` 中定义的 `Segment` 模型:
```python
class Segment:
"""基类标注"""
origin: MessageSegment
class At(Segment):
"""At对象, 表示一类提醒某用户的元素"""
target: str
class Emoji(Segment):
"""Emoji对象, 表示一类表情元素"""
id: str
name: Optional[str]
class Media(Segment):
url: Optional[str]
id: Optional[str]
class Image(Media):
"""Image对象, 表示一类图片元素"""
class Audio(Media):
"""Audio对象, 表示一类音频元素"""
class Voice(Media):
"""Voice对象, 表示一类语音元素"""
class Video(Media):
"""Video对象, 表示一类视频元素"""
class File(Segment):
"""File对象, 表示一类文件元素"""
id: str
name: Optional[str] = field(default=None)
```
:::
### 响应器使用
`on_alconna` 的所有参数如下:
- `command: Alconna | str`: Alconna 命令
- `skip_for_unmatch: bool = True`: 是否在命令不匹配时跳过该响应
- `auto_send_output: bool = False`: 是否自动发送输出信息并跳过响应
- `output_converter: TConvert | None = None`: 输出信息字符串转换为消息序列方法
- `aliases: set[str | tuple[str, ...]] | None = None`: 命令别名, 作用类似于 `on_command` 中的 aliases
- `comp_config: CompConfig | None = None`: 补全会话配置, 不传入则不启用补全会话
`AlconnaMatches` 是一个依赖注入函数,可注入 `Alconna` 命令解析结果。
### 配置项
#### alconna_auto_send_output
- **类型**: `bool`
- **默认值**: `False`
"是否全局启用输出信息自动发送,不启用则会在触特殊内置选项后仍然将解析结果传递至响应器。
#### alconna_use_command_start
- **类型**: `bool`
- **默认值**: `False`
是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀
#### alconna_auto_completion
- **类型**: `bool`
- **默认值**: `False`
是否全局启用命令自动补全,启用后会在参数缺失或触发 `--comp` 选项时自自动启用交互式补全。
## 文档参考
插件文档: [📦 这里](https://github.com/nonebot/plugin-alconna/blob/master/docs.md)
官方文档: [👉 指路](https://arclet.top/)
QQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH)
友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html)

View File

@@ -1,6 +0,0 @@
---
sidebar_position: 0
description: 在商店发布自己的插件
---
# 发布插件

View File

@@ -0,0 +1,202 @@
---
sidebar_position: 0
description: 在商店发布自己的插件
---
# 发布插件
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
NoneBot 为开发者提供了分享插件给大家使用的方式——商店。本章节将会介绍如何将我们写好的插件发布到商店。
:::tip 提示
本章节仅包含插件发布流程指导,插件开发请查阅前述章节。
:::
## 准备工作
### 插件命名规范
NoneBot 插件使用下述命名规范:
- 对于**项目名**,建议统一以 `nonebot-plugin-` 开头,之后为拟定的插件名字,词间用横杠 `-` 分隔;
- **项目名**用于代码仓库名称、PyPI 包的发布名称等;
- 本文使用 `nonebot-plugin-{your-plugin-name}` 为例。
- 对于**模块名**,建议与**项目名**一致,但词间用下划线 `_` 分隔,即统一以 `nonebot_plugin_` 开头,之后为拟定的插件名字;
- **模块名**用于程序导入使用,应为插件文件(夹)的名称;
- 本文使用 `nonebot_plugin_{your_plugin_name}` 为例。
### 项目结构
:::tip 提示
本段所述的项目结构仅作推荐,不做强制要求,保证实际可用性即可。
:::
插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。
插件项目的一种组织结构如下:
```tree
📦 nonebot-plugin-{your-plugin-name}
├── 📂 nonebot_plugin_{your_plugin_name}
│ ├── 📜 __init__.py
│ └── 📜 config.py
├── 📜 pyproject.toml
└── 📜 README.md
```
#### 第三方项目模板
一些社区用户可能会分享自己制作的项目模板方便大家使用,如:[A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template) 等。
:::tip 提示
本文档**不保证**第三方模板的适用性。
根据项目模板提供的使用指导补全/修改相应内容后上传到 GitHub 即可。
:::
### 插件依赖
本段指导填写插件依赖,避免不正确的依赖信息导致插件无法正常工作。
依赖填写的基本原则:程序直接导入了什么第三方库,就添加什么第三方包依赖;能用哪些第三方库的特性,就根据使用的特性锁定第三方包版本。
:::warning 注意
1. 插件需要添加 `nonebot2` 为依赖以避免“幽灵依赖”;
2. 插件需要将使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖;
3. 由于 `nonebot` 是指 `nonebot1` **而非** `nonebot2`,因此要注意**不要**将 `nonebot` 添加为插件的依赖,以免造成冲突;
4. 尽可能避免使用 `==` 锁定单一版本,增强与其它插件的兼容性。
:::
### 填写插件元数据
请注意,插件发布要求**必须**填写元数据才能通过审核。
下面是一个示例:
```python title=nonebot_plugin_{your_plugin_name}/__init__.py
from nonebot.plugin import PluginMetadata
from .config import Config
__plugin_meta__ = PluginMetadata(
name="{插件名称}",
description="{插件介绍}",
usage="{插件用法}",
type="{插件分类}",
# 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。
homepage="{项目主页}",
# 发布必填。
config=Config,
# 插件配置项类,如无需配置可不填写。
supported_adapters={"~onebot.v11", "~telegram"},
# 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。
# 若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写,否则应该列出插件支持的适配器。
)
```
:::warning 注意
`__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。
一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。
:::
:::tip 提示
带花括号 `{}` 的内容需要自行替换,注意**一定要把原有的花括号去掉**。
:::
### 准备项目主页
通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。
内容大致包括:
- 插件功能介绍
- 安装方法(建议至少有 `nb-cli` 方式安装,**不要**使用旧式的 `bot.py` 配置)
- 插件配置项(若无可跳过)
- 插件设置的触发规则(若无可跳过)
- 插件的其它用法(按需编写)
:::tip 提示
可以参考[第三方项目模板](#第三方项目模板)。
:::
### 发布至 [PyPI](https://pypi.org)
根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。
:::tip 提示
不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm.fming.dev/latest/),
[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)
构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。
:::
<Tabs groupId="publishMethod">
<TabItem value="poetry" label="Poetry" default>
```bash
poetry publish --build # 构建并发布
# 等效于以下两个命令
poetry build # 只构建
poetry publish # 只发布先前的构建
```
</TabItem>
<TabItem value="pdm" label="PDM" default>
```bash
pdm publish # 构建并发布
# 等效于以下两个命令
pdm build # 只构建
pdm publish --no-build # 只发布先前的构建
```
</TabItem>
<TabItem value="setuptools" label="Setuptools (PEP 517)" default>
```bash
pip install build twine # 安装通用构建与发布工具
python -m build --sdist --wheel . # 只构建
twine upload dist/* # 只发布先前的构建
```
</TabItem>
</Tabs>
:::tip 提示
发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。
:::
## 商店审核
### 提交申请
完成在 PyPI 的插件发布流程后,前往[商店](/store)页面,切换到插件页签,点击 **发布插件** 按钮。
在弹出的插件信息提交表单内,填入您所要发布的相应插件信息。请注意,如果插件需要必要配置项才能正常导入,请在“插件配置项”中填写必要的内容(请勿填写密钥等敏感信息)。
完成填写后,点击 **发布** 按钮,这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 `Submit new issue` 按钮进行最终提交即可。
### 等待插件审核
插件发布 Issue 创建后,将会经过 **NoneFlow Bot** 的自动前置检查,以确保插件信息正确无误、插件能被正确加载。
:::tip 提示
若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后在当前 Issue 追加任意内容的评论(如“已更新”等)即可重新触发插件检查。
:::
之后NoneBot 的维护者和一些插件开发者会初步检查插件代码,帮助减少该插件的问题。
完成这些步骤后,您的插件将会被自动合并到[商店](/store),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。

View File

@@ -148,6 +148,15 @@ MessageSegment.text("text") in message
"text" in message "text" in message
``` ```
我们还可以使用消息序列的 `only` 方法来检查消息中是否仅包含指定的消息段。
```python
# 是否都为指定消息段
message.only(MessageSegment.text("test"))
# 是否仅包含指定类型的消息段
message.only("text")
```
### 过滤、索引与切片 ### 过滤、索引与切片
消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。 消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。

View File

@@ -29,12 +29,9 @@ export default function Plugin(): JSX.Element {
const currentPlugins = filteredPlugins.slice(startIndex, endIndex + 1); const currentPlugins = filteredPlugins.slice(startIndex, endIndex + 1);
const [form, setForm] = useState<{ const [form, setForm] = useState<{
name: string;
desc: string;
projectLink: string; projectLink: string;
moduleName: string; moduleName: string;
homepage: string; }>({ projectLink: "", moduleName: "" });
}>({ name: "", desc: "", projectLink: "", moduleName: "", homepage: "" });
const ref = useRef<HTMLInputElement>(null); const ref = useRef<HTMLInputElement>(null);
const [tags, setTags] = useState<Tag[]>([]); const [tags, setTags] = useState<Tag[]>([]);
@@ -48,13 +45,13 @@ export default function Plugin(): JSX.Element {
setModalOpen(false); setModalOpen(false);
const queries: { key: string; value: string }[] = [ const queries: { key: string; value: string }[] = [
{ key: "template", value: "plugin_publish.yml" }, { key: "template", value: "plugin_publish.yml" },
{ key: "title", value: form.name && `Plugin: ${form.name}` }, {
key: "title",
value: form.projectLink && `Plugin: ${form.projectLink}`,
},
{ key: "labels", value: "Plugin" }, { key: "labels", value: "Plugin" },
{ key: "name", value: form.name },
{ key: "description", value: form.desc },
{ key: "pypi", value: form.projectLink }, { key: "pypi", value: form.projectLink },
{ key: "module", value: form.moduleName }, { key: "module", value: form.moduleName },
{ key: "homepage", value: form.homepage },
{ key: "tags", value: JSON.stringify(tags) }, { key: "tags", value: JSON.stringify(tags) },
]; ];
const urlQueries = queries const urlQueries = queries
@@ -95,10 +92,6 @@ export default function Plugin(): JSX.Element {
const delTag = (index: number) => { const delTag = (index: number) => {
setTags(tags.filter((_, i) => i !== index)); setTags(tags.filter((_, i) => i !== index));
}; };
const insertTagType = (text: string) => {
setLabel(text + label);
ref.current.value = text + label;
};
return ( return (
<> <>
@@ -137,25 +130,6 @@ export default function Plugin(): JSX.Element {
<ModalContent> <ModalContent>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<div className="grid grid-cols-1 gap-4 p-4"> <div className="grid grid-cols-1 gap-4 p-4">
<label className="flex flex-wrap">
<span className="mr-2">:</span>
<input
type="text"
name="name"
maxLength={20}
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
onChange={onChange}
/>
</label>
<label className="flex flex-wrap">
<span className="mr-2">:</span>
<input
type="text"
name="desc"
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
onChange={onChange}
/>
</label>
<label className="flex flex-wrap"> <label className="flex flex-wrap">
<span className="mr-2">PyPI :</span> <span className="mr-2">PyPI :</span>
<input <input
@@ -174,15 +148,6 @@ export default function Plugin(): JSX.Element {
onChange={onChange} onChange={onChange}
/> />
</label> </label>
<label className="flex flex-wrap">
<span className="mr-2">/:</span>
<input
type="text"
name="homepage"
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
onChange={onChange}
/>
</label>
</div> </div>
</form> </form>
<div className="px-4"> <div className="px-4">
@@ -211,21 +176,7 @@ export default function Plugin(): JSX.Element {
disableAlpha={true} disableAlpha={true}
onChangeComplete={onChangeColor} onChangeComplete={onChangeColor}
/> />
<div className="flex flex-wrap mt-2 items-center">
<span className="mr-2">Type:</span>
<button
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
onClick={() => insertTagType("a:")}
>
Adapter
</button>
<button
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
onClick={() => insertTagType("t:")}
>
Topic
</button>
</div>
<div className="flex mt-2"> <div className="flex mt-2">
<TagComponent label={label} color={color} /> <TagComponent label={label} color={color} />
<button <button

View File

@@ -5,6 +5,104 @@ toc_max_heading_level: 2
# 更新日志 # 更新日志
## v2.0.1
### 🚀 新功能
- Develop: 添加 Pyright 检查 [@yanyongyu](https://github.com/yanyongyu) ([#2194](https://github.com/nonebot/nonebot2/pull/2194))
- Feature: 使用 `typing.override` 标记 [@yanyongyu](https://github.com/yanyongyu) ([#2193](https://github.com/nonebot/nonebot2/pull/2193))
- Feature: 补充响应器组属性 [@eya46](https://github.com/eya46) ([#2154](https://github.com/nonebot/nonebot2/pull/2154))
- Feature: CommandGroup 支持命令别名添加前缀选项 [@eya46](https://github.com/eya46) ([#2134](https://github.com/nonebot/nonebot2/pull/2134))
- Feature: 添加用于动态继承支持适配器数据的方法 [@NCBM](https://github.com/NCBM) ([#2127](https://github.com/nonebot/nonebot2/pull/2127))
- Feature: 添加内置插件的插件元数据 [@yanyongyu](https://github.com/yanyongyu) ([#2113](https://github.com/nonebot/nonebot2/pull/2113))
- Feature: 插件商店适配最新的插件元数据 [@he0119](https://github.com/he0119) ([#2094](https://github.com/nonebot/nonebot2/pull/2094))
- Feature: 依赖注入支持 Generic TypeVar 和 Matcher 重载 [@yanyongyu](https://github.com/yanyongyu) ([#2089](https://github.com/nonebot/nonebot2/pull/2089))
### 🐛 Bug 修复
- Fix: 修复 Quart WS task 上下文错误 [@yanyongyu](https://github.com/yanyongyu) ([#2192](https://github.com/nonebot/nonebot2/pull/2192))
- Fix: 修复 dotenv 配置项为 None 将会跳过赋值 [@eya46](https://github.com/eya46) ([#2143](https://github.com/nonebot/nonebot2/pull/2143))
- Fix: 修复 `ArgParam` 不支持 `Annotated` [@eya46](https://github.com/eya46) ([#2124](https://github.com/nonebot/nonebot2/pull/2124))
- Fix: aiohttp 请求时 data 和 file 不能同时存在 [@j1g5awi](https://github.com/j1g5awi) ([#2088](https://github.com/nonebot/nonebot2/pull/2088))
- Fix: 修复因 loguru 更新导致的启动和关闭日志 name 不正常 [@DiheChen](https://github.com/DiheChen) ([#2080](https://github.com/nonebot/nonebot2/pull/2080))
### 📝 文档
- Docs: 移动 Alconna 文档至最佳实践 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2208](https://github.com/nonebot/nonebot2/pull/2208))
- Docs: 移除商店中不符合现规范的 tag [@j1g5awi](https://github.com/j1g5awi) ([#2205](https://github.com/nonebot/nonebot2/pull/2205))
- Docs: 添加 scoped 插件配置指南 [@yanyongyu](https://github.com/yanyongyu) ([#2198](https://github.com/nonebot/nonebot2/pull/2198))
- Docs: 钩子函数代码片段补充 [@A-kirami](https://github.com/A-kirami) ([#2173](https://github.com/nonebot/nonebot2/pull/2173))
- Docs: 格式化钩子函数中的代码片段 [@A-kirami](https://github.com/A-kirami) ([#2172](https://github.com/nonebot/nonebot2/pull/2172))
- Docs: 补充 Message.only 文档 [@eya46](https://github.com/eya46) ([#2155](https://github.com/nonebot/nonebot2/pull/2155))
- Docs: 修复日志自定义文档 typo [@17TheWord](https://github.com/17TheWord) ([#2140](https://github.com/nonebot/nonebot2/pull/2140))
- Docs: 修复依赖注入文档 `ArgStr` 3.9+ 和 3.8+ 版本代码写反 [@eya46](https://github.com/eya46) ([#2126](https://github.com/nonebot/nonebot2/pull/2126))
- Docs: 删除商店插件发布多余模块 [@forchannot](https://github.com/forchannot) ([#2095](https://github.com/nonebot/nonebot2/pull/2095))
- Docs: 微调插件元数据的部分描述 [@NCBM](https://github.com/NCBM) ([#2096](https://github.com/nonebot/nonebot2/pull/2096))
- Docs: 完成发布插件教程 [@NCBM](https://github.com/NCBM) ([#2078](https://github.com/nonebot/nonebot2/pull/2078))
- Docs: 更新插件元数据的相关描述 [@NCBM](https://github.com/NCBM) ([#2087](https://github.com/nonebot/nonebot2/pull/2087))
- Docs: 添加 Villa 适配器到 README [@CMHopeSunshine](https://github.com/CMHopeSunshine) ([#2086](https://github.com/nonebot/nonebot2/pull/2086))
### 💫 杂项
- Plugin: 黑白名单添加标签 [@A-kirami](https://github.com/A-kirami) ([#2170](https://github.com/nonebot/nonebot2/pull/2170))
- Plugin: 修改 nonebot-plugin-ocgbot-v2 插件名称 [@fireinsect](https://github.com/fireinsect) ([#2147](https://github.com/nonebot/nonebot2/pull/2147))
- Plugin: 更新 SparkGPT 插件描述 [@canxin121](https://github.com/canxin121) ([#2144](https://github.com/nonebot/nonebot2/pull/2144))
- Plugin: 修改 nonebot-plugin-ocgbot-v2 插件名称 [@fireinsect](https://github.com/fireinsect) ([#2141](https://github.com/nonebot/nonebot2/pull/2141))
- Plugin: 删除 nonebot-plugin-phlogo [@kexue-z](https://github.com/kexue-z) ([#2128](https://github.com/nonebot/nonebot2/pull/2128))
- Plugin: 修改 `nonebot-plugin-gw2` 模块名 [@Agnes4m](https://github.com/Agnes4m) ([#2123](https://github.com/nonebot/nonebot2/pull/2123))
- Develop: 添加 ruff linter [@yanyongyu](https://github.com/yanyongyu) ([#2114](https://github.com/nonebot/nonebot2/pull/2114))
- Plugin: 更新 `nonebot-plugin-msgbuf` 插件的名称等信息 [@NCBM](https://github.com/NCBM) ([#2119](https://github.com/nonebot/nonebot2/pull/2119))
- Plugin: 修改插件信息和仓库地址 [@Agnes4m](https://github.com/Agnes4m) ([#2115](https://github.com/nonebot/nonebot2/pull/2115))
- Test: 移除 httpbin 并整理测试 [@yanyongyu](https://github.com/yanyongyu) ([#2110](https://github.com/nonebot/nonebot2/pull/2110))
- CI: 缓存 NoneFlow 所需的 pre-commit hooks [@he0119](https://github.com/he0119) ([#2104](https://github.com/nonebot/nonebot2/pull/2104))
- Plugin: 移除过时未更新的插件\&Bot [@FYWinds](https://github.com/FYWinds) ([#2072](https://github.com/nonebot/nonebot2/pull/2072))
- Plugin: 删除插件 nonebot_plugin_r6s [@BalconyJH](https://github.com/BalconyJH) ([#2071](https://github.com/nonebot/nonebot2/pull/2071))
### 🍻 插件发布
- Plugin: 方寸狭间 [@noneflow](https://github.com/noneflow) ([#2207](https://github.com/nonebot/nonebot2/pull/2207))
- Plugin: DALL-E 绘图 [@noneflow](https://github.com/noneflow) ([#2204](https://github.com/nonebot/nonebot2/pull/2204))
- Plugin: 指定戳一戳 [@noneflow](https://github.com/noneflow) ([#2202](https://github.com/nonebot/nonebot2/pull/2202))
- Plugin: templates_render [@noneflow](https://github.com/noneflow) ([#2197](https://github.com/nonebot/nonebot2/pull/2197))
- Plugin: MongoDB [@noneflow](https://github.com/noneflow) ([#2189](https://github.com/nonebot/nonebot2/pull/2189))
- Plugin: pjsk 表情 [@noneflow](https://github.com/noneflow) ([#2187](https://github.com/nonebot/nonebot2/pull/2187))
- Plugin: nonebot-plugin-wenan [@noneflow](https://github.com/noneflow) ([#2184](https://github.com/nonebot/nonebot2/pull/2184))
- Plugin: nonebot-plugin-picture-api [@noneflow](https://github.com/noneflow) ([#2180](https://github.com/nonebot/nonebot2/pull/2180))
- Plugin: Blocker [@noneflow](https://github.com/noneflow) ([#2178](https://github.com/nonebot/nonebot2/pull/2178))
- Plugin: nonebot-plugin-nobahpicture [@noneflow](https://github.com/noneflow) ([#2176](https://github.com/nonebot/nonebot2/pull/2176))
- Plugin: 过期事件过滤器 [@noneflow](https://github.com/noneflow) ([#2169](https://github.com/nonebot/nonebot2/pull/2169))
- Plugin: 猫猫虫咖波图片发送 [@noneflow](https://github.com/noneflow) ([#2167](https://github.com/nonebot/nonebot2/pull/2167))
- Plugin: nonebot-plugin-splatoon3 [@noneflow](https://github.com/noneflow) ([#2165](https://github.com/nonebot/nonebot2/pull/2165))
- Plugin: nonebot-plugin-cfassistant [@noneflow](https://github.com/noneflow) ([#2164](https://github.com/nonebot/nonebot2/pull/2164))
- Plugin: 算法竞赛比赛查询 [@noneflow](https://github.com/noneflow) ([#2159](https://github.com/nonebot/nonebot2/pull/2159))
- Plugin: nonebot-plugin-update [@noneflow](https://github.com/noneflow) ([#2153](https://github.com/nonebot/nonebot2/pull/2153))
- Plugin: 远程同意好友 [@noneflow](https://github.com/noneflow) ([#2146](https://github.com/nonebot/nonebot2/pull/2146))
- Plugin: 戳一戳事件 [@noneflow](https://github.com/noneflow) ([#2139](https://github.com/nonebot/nonebot2/pull/2139))
- Plugin: EitherChoice [@noneflow](https://github.com/noneflow) ([#2137](https://github.com/nonebot/nonebot2/pull/2137))
- Plugin: 用户信息 [@noneflow](https://github.com/noneflow) ([#2133](https://github.com/nonebot/nonebot2/pull/2133))
- Plugin: Diablo4 地狱狂潮 boss 提醒小助手 [@noneflow](https://github.com/noneflow) ([#2122](https://github.com/nonebot/nonebot2/pull/2122))
- Plugin: nonbot-plugin-ocgbot-v2 [@noneflow](https://github.com/noneflow) ([#2120](https://github.com/nonebot/nonebot2/pull/2120))
- Plugin: 错误告警 [@noneflow](https://github.com/noneflow) ([#2117](https://github.com/nonebot/nonebot2/pull/2117))
- Plugin: follow_withdraw [@noneflow](https://github.com/noneflow) ([#2112](https://github.com/nonebot/nonebot2/pull/2112))
- Plugin: 战雷查水表 [@noneflow](https://github.com/noneflow) ([#2103](https://github.com/nonebot/nonebot2/pull/2103))
- Plugin: bili_push [@noneflow](https://github.com/noneflow) ([#2101](https://github.com/nonebot/nonebot2/pull/2101))
- Plugin: AI 作曲 [@noneflow](https://github.com/noneflow) ([#2093](https://github.com/nonebot/nonebot2/pull/2093))
- Plugin: pcrjjc [@noneflow](https://github.com/noneflow) ([#2091](https://github.com/nonebot/nonebot2/pull/2091))
- Plugin: twitter 订阅 [@noneflow](https://github.com/noneflow) ([#2082](https://github.com/nonebot/nonebot2/pull/2082))
- Plugin: 链接防夹 [@noneflow](https://github.com/noneflow) ([#2074](https://github.com/nonebot/nonebot2/pull/2074))
- Plugin: 碧蓝航线攻略 [@noneflow](https://github.com/noneflow) ([#2076](https://github.com/nonebot/nonebot2/pull/2076))
### 🍻 机器人发布
- Bot: 米缸 [@noneflow](https://github.com/noneflow) ([#2191](https://github.com/nonebot/nonebot2/pull/2191))
- Bot: 林汐 [@noneflow](https://github.com/noneflow) ([#2182](https://github.com/nonebot/nonebot2/pull/2182))
- Bot: web_bot [@noneflow](https://github.com/noneflow) ([#2131](https://github.com/nonebot/nonebot2/pull/2131))
- Bot: ReimeiBot-黎明机器人 [@noneflow](https://github.com/noneflow) ([#2107](https://github.com/nonebot/nonebot2/pull/2107))
### 🍻 适配器发布
- Adapter: 大别野 [@noneflow](https://github.com/noneflow) ([#2085](https://github.com/nonebot/nonebot2/pull/2085))
## v2.0.0 ## v2.0.0
### 💥 破坏性变更 ### 💥 破坏性变更

View File

@@ -153,5 +153,20 @@
} }
], ],
"is_official": false "is_official": false
},
{
"module_name": "nonebot.adapters.villa",
"project_link": "nonebot-adapter-villa",
"name": "大别野",
"desc": "米游社大别野官方Bot适配",
"author": "CMHopeSunshine",
"homepage": "https://github.com/CMHopeSunshine/nonebot-adapter-villa",
"tags": [
{
"label": "米哈游",
"color": "#e10909"
}
],
"is_official": false
} }
] ]

View File

@@ -71,14 +71,6 @@
"tags": [], "tags": [],
"is_official": false "is_official": false
}, },
{
"name": "Takker",
"desc": "综合了各种娱乐功能的Bot",
"author": "FYWinds",
"homepage": "https://github.com/FYWinds/takker",
"tags": [],
"is_official": false
},
{ {
"name": "剑网三bot", "name": "剑网三bot",
"desc": "网络游戏《剑侠情缘三》的群聊机器人数据使用www.jx3api.com", "desc": "网络游戏《剑侠情缘三》的群聊机器人数据使用www.jx3api.com",
@@ -163,10 +155,6 @@
{ {
"label": "AI", "label": "AI",
"color": "#ea5252" "color": "#ea5252"
},
{
"label": "a:onebot",
"color": "#000000"
} }
], ],
"is_official": false "is_official": false
@@ -261,12 +249,7 @@
"desc": "🐱🤖 一个以娱乐功能为主的缝合怪划掉QQ机器人包含一定Furry要素但是不会卖萌就是逊啦", "desc": "🐱🤖 一个以娱乐功能为主的缝合怪划掉QQ机器人包含一定Furry要素但是不会卖萌就是逊啦",
"author": "su226", "author": "su226",
"homepage": "https://github.com/su226/IdhagnBot", "homepage": "https://github.com/su226/IdhagnBot",
"tags": [ "tags": [],
{
"label": "a:OneBot",
"color": "#ea5252"
}
],
"is_official": false "is_official": false
}, },
{ {
@@ -339,15 +322,15 @@
"homepage": "https://github.com/Rinfair-CSP-A016/SuzunoBot-AGLAS", "homepage": "https://github.com/Rinfair-CSP-A016/SuzunoBot-AGLAS",
"tags": [ "tags": [
{ {
"label": "t:maimaiDX", "label": "maimaiDX",
"color": "#189ede" "color": "#189ede"
}, },
{ {
"label": "t:Arcaea", "label": "Arcaea",
"color": "#d551ef" "color": "#d551ef"
}, },
{ {
"label": "t:coc", "label": "coc",
"color": "#7fe4d0" "color": "#7fe4d0"
} }
], ],
@@ -434,10 +417,6 @@
{ {
"label": "maimai", "label": "maimai",
"color": "#52eaa5" "color": "#52eaa5"
},
{
"label": "a:onebot",
"color": "#000000"
} }
], ],
"is_official": false "is_official": false
@@ -447,12 +426,7 @@
"desc": "一个会拆家的高性能缝合萝卜子", "desc": "一个会拆家的高性能缝合萝卜子",
"author": "tkgs0", "author": "tkgs0",
"homepage": "https://github.com/tkgs0/Momoko", "homepage": "https://github.com/tkgs0/Momoko",
"tags": [ "tags": [],
{
"label": "a:onebot",
"color": "#000000"
}
],
"is_official": false "is_official": false
}, },
{ {
@@ -468,12 +442,7 @@
"desc": "简单的QQ功能型机器人", "desc": "简单的QQ功能型机器人",
"author": "This-is-XiaoDeng", "author": "This-is-XiaoDeng",
"homepage": "https://github.com/ITCraftDevelopmentTeam/XDbot2", "homepage": "https://github.com/ITCraftDevelopmentTeam/XDbot2",
"tags": [ "tags": [],
{
"label": "a:onebot",
"color": "#ea5252"
}
],
"is_official": false "is_official": false
}, },
{ {
@@ -521,14 +490,56 @@
"homepage": "https://github.com/bingqiu456/shouyun", "homepage": "https://github.com/bingqiu456/shouyun",
"tags": [ "tags": [
{ {
"label": "a:onebot", "label": "shouyun",
"color": "#ea5252"
},
{
"label": "t:shouyun",
"color": "#52ea7a" "color": "#52ea7a"
} }
], ],
"is_official": false "is_official": false
},
{
"name": "ReimeiBot-黎明机器人",
"desc": "流星飞逝,黎明终将到来。",
"author": "ThirdBlood",
"homepage": "https://github.com/3rdBit/ReimeiBot",
"tags": [],
"is_official": false
},
{
"name": "web_bot",
"desc": "把机器人搬到网络上",
"author": "wsdtl",
"homepage": "https://github.com/wsdtl/web_bot",
"tags": [
{
"label": "xiaonan",
"color": "#775151"
}
],
"is_official": false
},
{
"name": "林汐",
"desc": "多平台功能型Bot",
"author": "mute23-code",
"homepage": "https://github.com/netsora/SoraBot",
"tags": [
{
"label": "QQ频道",
"color": "#f47070"
},
{
"label": "OneBot v11",
"color": "#212121"
}
],
"is_official": false
},
{
"name": "米缸",
"desc": "基于nonebot2的米缸Bot",
"author": "LambdaYH",
"homepage": "https://github.com/LambdaYH/MigangBot",
"tags": [],
"is_official": false
} }
] ]

File diff suppressed because it is too large Load Diff

View File

@@ -1155,8 +1155,8 @@ from typing import Annotated
from nonebot.params import ArgStr from nonebot.params import ArgStr
@matcher.got("key") @matcher.got("key")
async def _(key: str = ArgStr()): ... async def _(key: Annotated[str, ArgStr()]): ...
async def _(foo: str = ArgStr("key")): ... async def _(foo: Annotated[str, ArgStr("key")]): ...
``` ```
</TabItem> </TabItem>
@@ -1166,8 +1166,8 @@ async def _(foo: str = ArgStr("key")): ...
from nonebot.params import ArgStr from nonebot.params import ArgStr
@matcher.got("key") @matcher.got("key")
async def _(key: Annotated[str, ArgStr()]): ... async def _(key: str = ArgStr()): ...
async def _(foo: Annotated[str, ArgStr("key")]): ... async def _(foo: str = ArgStr("key")): ...
``` ```
</TabItem> </TabItem>

View File

@@ -25,6 +25,10 @@ options:
这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。 这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。
```python ```python
from nonebot import get_driver
driver = get_driver()
@driver.on_startup @driver.on_startup
async def do_something(): async def do_something():
pass pass
@@ -35,6 +39,10 @@ async def do_something():
这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。 这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。
```python ```python
from nonebot import get_driver
driver = get_driver()
@driver.on_shutdown @driver.on_shutdown
async def do_something(): async def do_something():
pass pass
@@ -45,6 +53,10 @@ async def do_something():
这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。 这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。
```python ```python
from nonebot import get_driver
driver = get_driver()
@driver.on_bot_connect @driver.on_bot_connect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -55,6 +67,10 @@ async def do_something(bot: Bot):
这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。 这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。
```python ```python
from nonebot import get_driver
driver = get_driver()
@driver.on_bot_disconnect @driver.on_bot_disconnect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -82,6 +98,7 @@ async def do_something(event: Event):
```python ```python
from nonebot.message import event_postprocessor from nonebot.message import event_postprocessor
@event_postprocessor @event_postprocessor
async def do_something(event: Event): async def do_something(event: Event):
pass pass
@@ -93,6 +110,7 @@ async def do_something(event: Event):
```python ```python
from nonebot.message import run_preprocessor from nonebot.message import run_preprocessor
@run_preprocessor @run_preprocessor
async def do_something(event: Event, matcher: Matcher): async def do_something(event: Event, matcher: Matcher):
pass pass

View File

@@ -148,6 +148,15 @@ MessageSegment.text("text") in message
"text" in message "text" in message
``` ```
我们还可以使用消息序列的 `only` 方法来检查消息中是否仅包含指定的消息段。
```python
# 是否都为指定消息段
message.only(MessageSegment.text("test"))
# 是否仅包含指定类型的消息段
message.only("text")
```
### 过滤、索引与切片 ### 过滤、索引与切片
消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。 消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。

View File

@@ -1139,8 +1139,8 @@ from typing import Annotated
from nonebot.params import ArgStr from nonebot.params import ArgStr
@matcher.got("key") @matcher.got("key")
async def _(key: str = ArgStr()): ... async def _(key: Annotated[str, ArgStr()]): ...
async def _(foo: str = ArgStr("key")): ... async def _(foo: Annotated[str, ArgStr("key")]): ...
``` ```
</TabItem> </TabItem>
@@ -1150,8 +1150,8 @@ async def _(foo: str = ArgStr("key")): ...
from nonebot.params import ArgStr from nonebot.params import ArgStr
@matcher.got("key") @matcher.got("key")
async def _(key: Annotated[str, ArgStr()]): ... async def _(key: str = ArgStr()): ...
async def _(foo: Annotated[str, ArgStr("key")]): ... async def _(foo: str = ArgStr("key")): ...
``` ```
</TabItem> </TabItem>

View File

@@ -25,6 +25,10 @@ options:
这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。 这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。
```python ```python
from nonebot import get_driver
driver = get_driver()
@driver.on_startup @driver.on_startup
async def do_something(): async def do_something():
pass pass
@@ -35,6 +39,10 @@ async def do_something():
这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。 这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。
```python ```python
from nonebot import get_driver
driver = get_driver()
@driver.on_shutdown @driver.on_shutdown
async def do_something(): async def do_something():
pass pass
@@ -45,6 +53,10 @@ async def do_something():
这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。 这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。
```python ```python
from nonebot import get_driver
driver = get_driver()
@driver.on_bot_connect @driver.on_bot_connect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -55,6 +67,10 @@ async def do_something(bot: Bot):
这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。 这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。
```python ```python
from nonebot import get_driver
driver = get_driver()
@driver.on_bot_disconnect @driver.on_bot_disconnect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -82,6 +98,7 @@ async def do_something(event: Event):
```python ```python
from nonebot.message import event_postprocessor from nonebot.message import event_postprocessor
@event_postprocessor @event_postprocessor
async def do_something(event: Event): async def do_something(event: Event):
pass pass
@@ -93,6 +110,7 @@ async def do_something(event: Event):
```python ```python
from nonebot.message import run_preprocessor from nonebot.message import run_preprocessor
@run_preprocessor @run_preprocessor
async def do_something(event: Event, matcher: Matcher): async def do_something(event: Event, matcher: Matcher):
pass pass

View File

@@ -0,0 +1,49 @@
---
sidebar_position: 0
id: index
slug: /
---
# 概览
NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架(下称 NoneBot它基于 Python 的类型注解和异步优先特性兼容同步能够为你的需求实现提供便捷灵活的支持。同时NoneBot 拥有大量的开发者为其开发插件,用户无需编写任何代码,仅需完成环境配置及插件安装,就可以正常使用 NoneBot。
需要注意的是NoneBot 仅支持 **Python 3.8 以上版本**
## 特色
### 异步优先
NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。
### 完整的类型注解
NoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 PyrightPylance 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./editor-support))。
### 开箱即用
NoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。
### 插件系统
插件系统是 NoneBot 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。
### 依赖注入系统
NoneBot 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。
#### 什么是依赖注入
[**『依赖注入』**](https://zh.m.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**『依赖』**。
系统(在这里是指 NoneBot将负责做任何需要的事情为你的代码提供这些必要依赖即**『注入』**依赖性)
这在你有以下情形的需求时非常有用:
- 这部分代码拥有共享的逻辑(同样的代码逻辑多次重复)
- 共享数据库以及网络请求连接会话
- 比如 `httpx.AsyncClient``aiohttp.ClientSession``sqlalchemy.Session`
- 机器人用户权限检查以及认证
- 还有更多...
它在完成上述工作的同时,还能尽量减少代码的耦合和重复

View File

@@ -0,0 +1,161 @@
---
sidebar_position: 1
description: 注册适配器与指定平台交互
options:
menu:
weight: 20
category: advanced
---
# 使用适配器
适配器 (Adapter) 是机器人与平台交互的核心桥梁,它负责在驱动器和机器人插件之间转换与传递消息。
## 适配器功能与组成
适配器通常有两种功能,分别是**接收事件**和**调用平台接口**。其中,接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型,然后交由机器人插件处理;调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式,然后交由驱动器发送,并接收接口返回数据。
为了实现这两种功能,适配器通常由四个部分组成:
- **Adapter**:负责转换事件和调用接口,正确创建 Bot 对象并注册到 NoneBot 中。
- **Bot**:负责存储平台机器人相关信息,并提供回复事件的方法。
- **Event**:负责定义事件内容,以及事件主体对象。
- **Message**:负责正确序列化消息,以便机器人插件处理。
## 注册适配器
在使用适配器之前,我们需要先将适配器注册到驱动器中,这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例,来看看如何注册适配器:
```python {2,5} title=bot.py
import nonebot
from nonebot.adapters.console import Adapter
driver = nonebot.get_driver()
driver.register_adapter(Adapter)
```
我们首先需要从适配器模块中导入所需要的适配器类,然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。如果我们需要多平台支持,可以多次调用 `register_adapter` 方法来注册多个适配器。
## 获取已注册的适配器
NoneBot 提供了 `get_adapter` 方法来获取已注册的适配器,我们可以通过适配器的名称或类型来获取指定的适配器实例:
```python
import nonebot
from nonebot.adapters.console import Adapter
adapters = nonebot.get_adapters()
console_adapter = nonebot.get_adapter(Adapter)
console_adapter = nonebot.get_adapter(Adapter.get_name())
```
## 获取 Bot 对象
当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取,这是一个以机器人 ID 为键的字典:
```python
import nonebot
bots = nonebot.get_bots()
```
我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数,将会返回所有 Bot 中的第一个:
```python
import nonebot
bot = nonebot.get_bot("bot_id")
```
如果需要获取指定适配器连接的 Bot 对象,我们可以通过适配器的 `bots` 属性获取,这也是一个以机器人 ID 为键的字典:
```python
import nonebot
from nonebot.adapters.console import Adapter
console_adapter = nonebot.get_adapter(Adapter)
bots = console_adapter.bots
```
Bot 对象都具有一个 `self_id` 属性,它是机器人的唯一 ID由适配器填写通常为机器人的帐号 ID 或者 APP ID。
## 获取事件通用信息
适配器的所有事件模型均继承自 `Event` 基类,在[事件类型与重载](../appendices/overload.md)一节中,我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息:
### 事件类型
事件类型通常为 `meta_event`、`message`、`notice`、`request`。
```python
type: str = event.get_type()
```
### 事件名称
事件名称由适配器定义,通常用于日志记录。
```python
name: str = event.get_event_name()
```
### 事件描述
事件描述由适配器定义,通常用于日志记录。
```python
description: str = event.get_event_description()
```
### 事件日志字符串
事件日志字符串由事件名称和事件描述组成,用于日志记录。
```python
log: str = event.get_log_string()
```
### 事件主体 ID
事件主体 ID 通常为机器人用户 ID。
```python
user_id: str = event.get_user_id()
```
### 事件会话 ID
事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。
```python
session_id: str = event.get_session_id()
```
### 事件消息
如果事件包含消息,则可以通过该方法获取,否则会产生异常。
```python
message: Message = event.get_message()
```
### 事件纯文本消息
通常为事件消息的纯文本内容,如果事件不包含消息,则会产生异常。
```python
text: str = event.get_plaintext()
```
### 事件是否与机器人有关
由适配器实现的判断,通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。
```python
is_tome: bool = event.is_tome()
```
## 更多
官方支持的适配器和社区贡献的适配器均可在[商店](/store)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,286 @@
---
sidebar_position: 0
description: 选择合适的驱动器运行机器人
options:
menu:
weight: 10
category: advanced
---
# 选择驱动器
驱动器 (Driver) 是机器人运行的基石,它是机器人初始化的第一步,主要负责数据收发。
:::important 提示
驱动器的选择通常与机器人所使用的协议适配器相关,如果不知道该选择哪个驱动器,可以先阅读相关协议适配器文档说明。
:::
:::tip 提示
如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。
:::
## 驱动器类型
驱动器的类型有两种:
- `ForwardDriver`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。
- `ReverseDriver`:即服务端型驱动器,多用于使用 WebHook接收 WebSocket 客户端连接等情形。
客户端型驱动器具有以下两种功能:
1. 异步发送 HTTP 请求,自定义 `HTTP Method``URL``Header``Body``Cookie``Proxy``Timeout` 等。
2. 异步建立 WebSocket 连接上下文,自定义 `WebSocket URL``Header``Cookie``Proxy``Timeout` 等。
服务端型驱动器通常为 ASGI 应用框架,具有以下功能:
1. 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。
2. 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。
3. 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。
## 配置驱动器
驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍,这里将详细介绍驱动器配置的格式。
NoneBot 中的客户端和服务端型驱动器可以相互配合使用,但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类,即驱动器类,他可以作为驱动器单独运行。同时,客户端驱动器模块中还会提供一个 `Mixin` 子类,用于在与其他驱动器配合使用时加载。因此,驱动器配置格式采用特殊语法:`<module>[:<Driver>][+<module>[:<Mixin>]]*`
其中,`<module>` 代表**驱动器模块路径**`<Driver>` 代表**驱动器类名**,默认为 `Driver``<Mixin>` 代表**驱动器混入类名**,默认为 `Mixin`。即,我们需要选择一个主要驱动器,然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型,但混入类驱动器只能为客户端类型。
特别的,为了简化内置驱动器模块路径,我们可以使用 `~` 符号作为内置驱动器模块路径的前缀,如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配,但需要安装额外依赖才能使用,具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下:
```dotenv
DRIVER=~fastapi
DRIVER=~aiohttp
DRIVER=~httpx+~websockets
DRIVER=~fastapi+~httpx+~websockets
```
## 获取驱动器
在 NoneBot 框架初始化完成后,我们就可以通过 `get_driver()` 方法获取全局驱动器实例:
```python
from nonebot import get_driver
driver = get_driver()
```
## 内置驱动器
### None
**类型:**服务端驱动器
NoneBot 内置的空驱动器,不提供任何收发数据功能,可以在不需要外部网络连接时使用。
```env
DRIVER=~none
```
### FastAPI默认
**类型:**服务端驱动器
> FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
[FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架,具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能,也可以挂载其他 ASGI、WSGI 应用。
```env
DRIVER=~fastapi
```
#### FastAPI 配置项
##### `fastapi_openapi_url`
类型:`str | None`
默认值:`None`
说明:`FastAPI` 提供的 `OpenAPI` JSON 定义地址,如果为 `None`,则不提供 `OpenAPI` JSON 定义。
##### `fastapi_docs_url`
类型:`str | None`
默认值:`None`
说明:`FastAPI` 提供的 `Swagger` 文档地址,如果为 `None`,则不提供 `Swagger` 文档。
##### `fastapi_redoc_url`
类型:`str | None`
默认值:`None`
说明:`FastAPI` 提供的 `ReDoc` 文档地址,如果为 `None`,则不提供 `ReDoc` 文档。
##### `fastapi_include_adapter_schema`
类型:`bool`
默认值:`True`
说明:`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`
##### `fastapi_reload`
:::warning 警告
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
```bash
nb run --reload
```
开启该功能后,在 uvicorn 运行时FastAPI 提供的 ASGI 底层,即 reload 功能的实际来源asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`
> 相关信息参考 [uvicorn#529](https://github.com/encode/uvicorn/issues/529)[uvicorn#1070](https://github.com/encode/uvicorn/pull/1070)[uvicorn#1257](https://github.com/encode/uvicorn/pull/1257)
后者(`SelectorEventLoop`)在 Windows 平台的可使用性不如前者(`ProactorEventLoop`),包括但不限于
1. 不支持创建子进程
2. 最多只支持 512 个套接字
3. ...
> 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows)
所以,一些使用了 asyncio 的库因此可能无法正常工作,如:
1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows)
如果在开启该功能后,原本**正常运行**的代码报错,且打印的异常堆栈信息和 asyncio 有关(异常一般为 `NotImplementedError`
你可能就需要考虑相关库对事件循环的支持,以及是否启用该功能。
:::
类型:`bool`
默认值:`False`
说明:是否开启 `uvicorn``reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。
```python title=bot.py
app = nonebot.get_asgi()
nonebot.run(app="bot:app")
```
##### `fastapi_reload_dirs`
类型:`List[str] | None`
默认值:`None`
说明:重载监控文件夹列表,默认为 uvicorn 默认值
##### `fastapi_reload_delay`
类型:`float | None`
默认值:`None`
说明:重载延迟,默认为 uvicorn 默认值
##### `fastapi_reload_includes`
类型:`List[str] | None`
默认值:`None`
说明:要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值
##### `fastapi_reload_excludes`
类型:`List[str] | None`
默认值:`None`
说明:不要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值
##### `fastapi_extra`
类型:`Dist[str, Any]`
默认值:`{}`
说明:传递给 `FastAPI` 的其他参数
### Quart
**类型:**`ReverseDriver`
> Quart is an asyncio reimplementation of the popular Flask microframework API.
[Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本,拥有与 Flask 非常相似的接口和使用方法。
```env
DRIVER=~quart
```
#### Quart 配置项
##### `quart_reload`
:::warning 警告
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
```bash
nb run --reload
```
:::
类型:`bool`
默认值:`False`
说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。
```python title=bot.py
app = nonebot.get_asgi()
nonebot.run(app="bot:app")
```
##### `quart_reload_dirs`
类型:`List[str] | None`
默认值:`None`
说明:重载监控文件夹列表,默认为 uvicorn 默认值
##### `quart_reload_delay`
类型:`float | None`
默认值:`None`
说明:重载延迟,默认为 uvicorn 默认值
##### `quart_reload_includes`
类型:`List[str] | None`
默认值:`None`
说明:要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值
##### `quart_reload_excludes`
类型:`List[str] | None`
默认值:`None`
说明:不要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值
##### `quart_extra`
类型:`Dist[str, Any]`
默认值:`{}`
说明:传递给 `Quart` 的其他参数
### HTTPX
**类型:**`ForwardDriver`
:::warning 注意
本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。
:::
> [HTTPX](https://www.python-httpx.org/) is a fully featured HTTP client for Python 3, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2.
```env
DRIVER=~httpx
```
### websockets
**类型:**`ForwardDriver`
:::warning 注意
本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。
:::
> [websockets](https://websockets.readthedocs.io/) is a library for building WebSocket servers and clients in Python with a focus on correctness, simplicity, robustness, and performance.
```env
DRIVER=~websockets
```
### AIOHTTP
**类型:**`ForwardDriver`
> [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python.
```env
DRIVER=~aiohttp
```

Some files were not shown because too many files have changed in this diff Show More