Compare commits

..

257 Commits

Author SHA1 Message Date
noneflow[bot]
5e86d53e0b 🔖 Release 2.4.0 2024-10-31 13:45:33 +00:00
Ju4tCode
a50a3398de 🔖 Release: v2.4.0 (#3097) 2024-10-31 21:35:08 +08:00
noneflow[bot]
950930a275 📝 Update changelog 2024-10-31 11:01:53 +00:00
DiaoDaiaChan
c7af169a94 🍻 publish plugin Comfyui绘图插件 (#3080) 2024-10-31 11:00:37 +00:00
noneflow[bot]
e17096d8d7 📝 Update changelog 2024-10-31 09:25:24 +00:00
tkgs0
00e9e74dfc 🍻 publish plugin 每日wife (#3093) 2024-10-31 09:24:19 +00:00
noneflow[bot]
934954d985 📝 Update changelog 2024-10-31 08:49:34 +00:00
Ju4tCode
a4a4991473 📝 Docs: 新增 nonebug 新版启动需要的配置 (#3087) 2024-10-31 16:48:22 +08:00
noneflow[bot]
60acb71033 📝 Update changelog 2024-10-31 07:38:50 +00:00
StarHeart
f8b4dfb1b1 📝 Docs: 修复侧边栏滚动 (#3062) 2024-10-31 15:37:23 +08:00
noneflow[bot]
ee5561046f 📝 Update changelog 2024-10-31 05:13:30 +00:00
YuuzukiRin
6660c3b471 🍻 publish plugin nonebot_plugin_impart (#3078) 2024-10-31 05:12:15 +00:00
noneflow[bot]
bd5ba84737 📝 Update changelog 2024-10-31 04:50:01 +00:00
Ju4tCode
15c5464069 Feature: 跳过部分非必要的 task group 创建 (#3095) 2024-10-31 12:47:29 +08:00
noneflow[bot]
7b136548a9 📝 Update changelog 2024-10-30 13:29:19 +00:00
wangyw15
36ed8030d3 🍻 publish bot CanrotBot (#3085) 2024-10-30 13:27:57 +00:00
noneflow[bot]
eff1fe455f 📝 Update changelog 2024-10-29 06:23:51 +00:00
Ju4tCode
e3cb4c7907 🐛 Fix: 修复结构化并发子依赖取消缓存问题 (#3084) 2024-10-29 14:22:41 +08:00
noneflow[bot]
be732cf9d8 📝 Update changelog 2024-10-28 11:31:31 +00:00
HibiKier
88a5966a40 🍻 publish plugin Pix图库 (#3082) 2024-10-28 11:30:21 +00:00
noneflow[bot]
bdde496332 📝 Update changelog 2024-10-26 15:04:43 +00:00
YuuzukiRin
a989a895e4 🍻 publish plugin nonebot_plugin_partner_join (#3048) 2024-10-26 15:03:23 +00:00
noneflow[bot]
7fc51e9227 📝 Update changelog 2024-10-26 08:14:26 +00:00
eya46
571fd007ba 🍻 publish plugin pong (#3065) 2024-10-26 08:13:17 +00:00
noneflow[bot]
599ef3b253 📝 Update changelog 2024-10-26 07:43:58 +00:00
eya46
c0c7d141ef 🍻 publish plugin Bot的消息也是消息 (#3063) 2024-10-26 07:42:44 +00:00
noneflow[bot]
3be68895e5 📝 Update changelog 2024-10-26 07:37:16 +00:00
Ju4tCode
ff21ceb946 Feature: 迁移至结构化并发框架 AnyIO (#3053) 2024-10-26 15:36:01 +08:00
noneflow[bot]
bd9befbb55 📝 Update changelog 2024-10-26 04:56:40 +00:00
Lonely-Sails
ed91ec9bf5 🍻 publish plugin BiliMusic Downloader (#3045) 2024-10-26 04:55:34 +00:00
noneflow[bot]
a00def5d86 📝 Update changelog 2024-10-26 04:48:33 +00:00
Ant1816
973282587e 🍻 publish bot 小安提Bot (#3060) 2024-10-26 04:47:16 +00:00
noneflow[bot]
8a6d209942 📝 Update changelog 2024-10-23 07:42:02 +00:00
zhongwen-4
fcd226031b 🍻 publish plugin 防撤回 (#3054) 2024-10-23 07:40:56 +00:00
noneflow[bot]
fb3f0d5e30 📝 Update changelog 2024-10-23 06:19:03 +00:00
YuuzukiRin
56518748d9 🍻 publish plugin nonebot_plugin_mai_arcade (#3044) 2024-10-23 06:17:44 +00:00
noneflow[bot]
474398ee3d 📝 Update changelog 2024-10-23 04:02:12 +00:00
gongfuture
597b104111 🍻 publish plugin DDNet 成绩查询 (#3030) 2024-10-23 04:01:06 +00:00
noneflow[bot]
acec8945ac 📝 Update changelog 2024-10-22 13:24:49 +00:00
ChenXu233
06b5f09371 🍻 publish plugin 省流 (#3041) 2024-10-22 13:23:38 +00:00
noneflow[bot]
15470cd3bb 📝 Update changelog 2024-10-22 02:35:06 +00:00
StarHeart
c1c5f57e0b 📝 Docs: 升级到 Docusaurus V3 (#2956) 2024-10-22 10:33:48 +08:00
noneflow[bot]
533e8794b2 📝 Update changelog 2024-10-21 10:03:18 +00:00
Cvandia
05c20a7a86 🍻 publish plugin FishSpeechTTS (#3049) 2024-10-21 10:02:12 +00:00
noneflow[bot]
edb416736b 📝 Update changelog 2024-10-20 12:36:27 +00:00
Onimaimai
2a68bb1b6e 🍻 publish plugin 语音点歌 (#3036) 2024-10-20 12:35:22 +00:00
noneflow[bot]
29ffbc630a 📝 Update changelog 2024-10-20 05:59:57 +00:00
snowykami
5cf6b93984 🍻 publish plugin Gotify (#3042) 2024-10-20 05:58:49 +00:00
noneflow[bot]
30011e3fb4 📝 Update changelog 2024-10-20 02:20:52 +00:00
zhongwen-4
36606ab05a 🍻 publish plugin 涩图插件 (#3038) 2024-10-20 02:19:49 +00:00
noneflow[bot]
0aba6b4bb4 📝 Update changelog 2024-10-20 02:15:59 +00:00
ssttkkl
fab51d9605 ✏️ Plugin: 移除不再维护的插件 (#3040) 2024-10-20 10:14:55 +08:00
noneflow[bot]
d7e2cc608b 📝 Update changelog 2024-10-19 16:08:07 +00:00
yeying-xingchen
b2c5ab3235 🍻 publish plugin boom (#3016) 2024-10-19 16:07:02 +00:00
noneflow[bot]
1668568d1a 📝 Update changelog 2024-10-19 08:49:11 +00:00
Agnes4m
4385934a6b 🍻 publish plugin 恶魔轮盘赌 (#3032) 2024-10-19 08:48:09 +00:00
noneflow[bot]
4830182050 📝 Update changelog 2024-10-18 15:09:49 +00:00
Onimaimai
d86a86d4b2 🍻 publish plugin 机厅 (#3028) 2024-10-18 15:08:38 +00:00
noneflow[bot]
f175bc9e80 📝 Update changelog 2024-10-18 14:40:56 +00:00
CM-Edelweiss
40c2bc636a 🍻 publish plugin PM帮助 (#3022) 2024-10-18 14:39:50 +00:00
noneflow[bot]
8ad5a8d4d1 📝 Update changelog 2024-10-18 13:51:38 +00:00
Refound-445
aed91dcc48 🍻 publish plugin NailongRemove (#2953) 2024-10-18 13:50:31 +00:00
noneflow[bot]
de8ffb6c97 📝 Update changelog 2024-10-18 12:59:02 +00:00
Onimaimai
990cf32304 🍻 publish plugin 团购 help (#3025) 2024-10-18 12:57:57 +00:00
noneflow[bot]
09b3f13e7e 📝 Update changelog 2024-10-14 13:22:17 +00:00
HibiKier
c39b13b782 🍻 publish plugin 真寻日报 (#3020) 2024-10-14 13:21:07 +00:00
noneflow[bot]
3ec4611a29 📝 Update changelog 2024-10-14 12:56:39 +00:00
zhongwen-4
9d6832303d 🍻 publish plugin 运行状态 (#3018) 2024-10-14 12:55:33 +00:00
noneflow[bot]
9fc9f7c384 📝 Update changelog 2024-10-10 06:26:10 +00:00
qllokirin
2021e81ed2 🍻 publish plugin 西工大翱翔门户成绩监控 (#3011) 2024-10-10 06:25:06 +00:00
noneflow[bot]
ada6e1ab64 📝 Update changelog 2024-10-10 06:18:05 +00:00
hanasa2023
2723a372da 🍻 publish plugin nb插件更新器 (#3014) 2024-10-10 06:17:01 +00:00
noneflow[bot]
a56c93cbcc 📝 Update changelog 2024-10-09 14:30:36 +00:00
ChenXu233
230476d8ae 🍻 publish plugin 涩涩保存器 (#2987) 2024-10-09 14:29:23 +00:00
noneflow[bot]
31a13551be 📝 Update changelog 2024-10-08 07:15:42 +00:00
swallow513
82dbacda83 🍻 publish plugin nonebot_plugin_BFVsearch (#3007) 2024-10-08 07:14:39 +00:00
noneflow[bot]
badd53b4bb 📝 Update changelog 2024-10-08 03:55:09 +00:00
yeying-xingchen
94052b5bf7 🍻 publish plugin lingyi_chat (#3005) 2024-10-08 03:54:07 +00:00
noneflow[bot]
72a6914980 📝 Update changelog 2024-10-08 02:25:52 +00:00
HibiKier
a2a604dd85 🍻 publish plugin ZXPM插件管理 (#3002) 2024-10-08 02:24:41 +00:00
noneflow[bot]
d3d0779d30 📝 Update changelog 2024-10-07 13:03:55 +00:00
Lonely-Sails
a45e7d3854 🍻 publish plugin MinecraftWatcher (#3009) 2024-10-07 13:02:44 +00:00
noneflow[bot]
4dadef3e51 📝 Update changelog 2024-10-06 12:29:31 +00:00
Lonely-Sails
ee643544f1 🍻 publish plugin BF5_grouptools (#3001) 2024-10-06 12:28:32 +00:00
noneflow[bot]
4598a3de9a 📝 Update changelog 2024-10-05 11:02:36 +00:00
Shadow403
59ad3d4b17 🍻 publish plugin lolinfo (#2996) 2024-10-05 11:01:23 +00:00
noneflow[bot]
ba78c3aef8 📝 Update changelog 2024-10-05 08:06:00 +00:00
Sevenyine
3ce2b69431 🍻 publish plugin osu! Match Monitor (#2982) 2024-10-05 08:04:56 +00:00
noneflow[bot]
d47722d87c 📝 Update changelog 2024-10-04 03:07:42 +00:00
Asankilp
c4ddfc3df1 🍻 publish plugin Marsho AI插件 (#2992) 2024-10-04 03:06:25 +00:00
hanasa2023
58c8879cbb 🍻 publish plugin nonechat (#2989) 2024-10-04 02:53:24 +00:00
noneflow[bot]
eb1342b78d 📝 Update changelog 2024-10-02 02:13:53 +00:00
STESmly
feb619a85c 🍻 publish plugin nonebot_plugin_SimpleToWrite (#2994) 2024-10-02 02:12:44 +00:00
noneflow[bot]
a9ec70e798 📝 Update changelog 2024-10-01 03:48:44 +00:00
qwq12738qwq
c96e9dbcb6 🍻 publish plugin Beat Saber查分器 (#2973) 2024-10-01 03:47:42 +00:00
noneflow[bot]
8742d867e8 📝 Update changelog 2024-09-30 02:29:48 +00:00
lyqgzbl
ed14dcd090 🍻 publish plugin githubmodels (#2944) 2024-09-30 02:28:43 +00:00
noneflow[bot]
7956b53530 📝 Update changelog 2024-09-29 13:17:13 +00:00
FrostN0v0
1140d668b6 🍻 publish plugin 给我点颜色瞧瞧 (#2983) 2024-09-29 13:16:03 +00:00
noneflow[bot]
9351b074b1 📝 Update changelog 2024-09-29 06:07:36 +00:00
yixinNB
61dc206935 📝 Docs: 修改文档示例代码与部分表述 (#2797)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Komorebi <110453675+KomoriDev@users.noreply.github.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2024-09-29 14:06:27 +08:00
noneflow[bot]
70f62bf4da 📝 Update changelog 2024-09-28 06:54:12 +00:00
Atr1ck
812c0cd624 🍻 publish plugin pjsk-helper (#2979) 2024-09-28 06:53:10 +00:00
noneflow[bot]
5107729290 📝 Update changelog 2024-09-26 08:44:18 +00:00
chsiyu
9b59c16b04 🍻 publish plugin 趣味内容插件 (#2978) 2024-09-26 08:43:13 +00:00
noneflow[bot]
fdc1dcace7 📝 Update changelog 2024-09-24 15:19:20 +00:00
Yurchiu
c6c22e3c29 🍻 publish plugin 计算器:游戏 (#2975) 2024-09-24 15:18:14 +00:00
noneflow[bot]
0291b10560 📝 Update changelog 2024-09-21 04:21:05 +00:00
yao-yun
6f07ce0060 🍻 publish plugin nonebot-plugin-yareminder (#2961) 2024-09-21 04:20:01 +00:00
noneflow[bot]
b375575792 📝 Update changelog 2024-09-20 12:29:42 +00:00
zhongwen-4
88765711f3 🍻 publish plugin 批量撤回 (#2965) 2024-09-20 12:28:40 +00:00
noneflow[bot]
17ba8d70e1 📝 Update changelog 2024-09-20 05:10:23 +00:00
RF-Tar-Railt
e373251092 🍻 publish plugin inspect (#2970) 2024-09-20 05:09:13 +00:00
noneflow[bot]
63dcc658da 📝 Update changelog 2024-09-20 04:31:23 +00:00
RF-Tar-Railt
3aaf86e9a9 🍻 publish plugin 通用信息 (#2968) 2024-09-20 04:30:17 +00:00
noneflow[bot]
2cabeb658e 📝 Update changelog 2024-09-19 04:07:57 +00:00
KiKi-XC
0c187cd8c3 🍻 publish plugin SSE日志输出流 (#2959) 2024-09-19 04:06:57 +00:00
noneflow[bot]
c0c8e1aa02 📝 Update changelog 2024-09-16 02:23:31 +00:00
TheChenXI
a8586d7990 🍻 publish plugin WITFF (#2940) 2024-09-16 02:22:28 +00:00
noneflow[bot]
91b3d3d5e0 📝 Update changelog 2024-09-15 11:40:22 +00:00
hanasa2023
8ef51154fd 🍻 publish plugin weather-rank (#2948) 2024-09-15 11:39:16 +00:00
noneflow[bot]
86c83064e4 📝 Update changelog 2024-09-14 04:23:30 +00:00
Funny1Potato
a4be2c465f 🍻 publish plugin 二维码生成器 (#2941) 2024-09-14 04:22:23 +00:00
noneflow[bot]
be4f36036c 📝 Update changelog 2024-09-09 14:42:24 +00:00
zouXH-god
f6c7fb6da6 🍻 publish plugin 次元星辰 (#2934) 2024-09-09 14:41:17 +00:00
noneflow[bot]
c9b4c3f3c0 📝 Update changelog 2024-09-08 09:42:53 +00:00
shoucandanghehe
4af4412cd7 🍻 publish plugin nonebot-plugin-tarina-lang-turbo (#2937) 2024-09-08 09:41:52 +00:00
noneflow[bot]
4e7f7fb722 📝 Update changelog 2024-09-04 15:38:25 +00:00
MeetWq
f01f692fde ✏️ Plugin: 删除不再维护的 simplemusic hikarisearch 插件 (#2933) 2024-09-04 23:37:21 +08:00
noneflow[bot]
6e94dade69 📝 Update changelog 2024-09-03 08:33:07 +00:00
wyf7685
eaef8dfc19 🍻 publish plugin 狼人杀 (#2931) 2024-09-03 08:32:00 +00:00
pre-commit-ci[bot]
dec8b26b89 ⬆️ auto update by pre-commit hooks (#2930)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-03 16:09:29 +08:00
noneflow[bot]
e8c39f9cc8 📝 Update changelog 2024-09-01 03:05:46 +00:00
SamuNatsu
2a8644de81 🍻 publish plugin 阿瓦隆 (#2914) 2024-09-01 03:04:44 +00:00
noneflow[bot]
5aaa0d3f12 📝 Update changelog 2024-08-29 04:00:46 +00:00
youlanan
22bb377fcf 🍻 publish plugin 消音器 (#2918) 2024-08-29 03:59:44 +00:00
noneflow[bot]
0c0ad0dd5e 📝 Update changelog 2024-08-27 15:20:56 +00:00
ChenXu233
c947bdfef5 🍻 publish plugin 悠悠 (#2927) 2024-08-27 15:19:49 +00:00
noneflow[bot]
b197802d9a 📝 Update changelog 2024-08-27 13:21:49 +00:00
kanbereina
8019a570cc 🍻 publish plugin LLOneBot-Master (#2924) 2024-08-27 13:20:45 +00:00
noneflow[bot]
25a85330a7 📝 Update changelog 2024-08-26 15:34:21 +00:00
鹿友-Reina
35fb4fc18d ✏️ Plugin: 删除插件 nonebot-plugin-ntqq-restart (#2926) 2024-08-26 23:33:13 +08:00
noneflow[bot]
ff50e997d0 📝 Update changelog 2024-08-26 15:26:03 +00:00
Funny1Potato
6cda981aa2 🍻 publish plugin nonebot-plugin-Send-API-picture (#2922) 2024-08-26 15:24:54 +00:00
noneflow[bot]
dcfbb32363 📝 Update changelog 2024-08-24 09:24:50 +00:00
KomoriDev
d8a1a0ab38 🍻 publish plugin maimai DX 查分 (#2920) 2024-08-24 09:23:52 +00:00
noneflow[bot]
6efd01a575 📝 Update changelog 2024-08-22 14:36:12 +00:00
molanp
b2f7846eb4 🍻 publish plugin Minecraft查服 (#2881) 2024-08-22 14:35:06 +00:00
noneflow[bot]
8d2a284fe2 📝 Update changelog 2024-08-22 13:29:57 +00:00
呵呵です
cce13f682d Feature: 添加 websockets 驱动器 proxy 连接警告 (#2916)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2024-08-22 21:28:51 +08:00
noneflow[bot]
6c1d7ad74b 📝 Update changelog 2024-08-21 15:25:11 +00:00
paro
4b837343ff 📝 Docs: 添加钩子函数 IgnoredException 用法 (#2912)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2024-08-21 23:23:56 +08:00
noneflow[bot]
d1904ba156 📝 Update changelog 2024-08-21 08:17:07 +00:00
Lonely-Sails
3ed1bde38a 🍻 publish plugin lagrange (#2897) 2024-08-21 08:16:16 +00:00
noneflow[bot]
5cd82df580 📝 Update changelog 2024-08-19 14:59:15 +00:00
Tarrailt
fdd0e82099 ✏️ Adapter: 移除社区版 mirai 适配器 (#2909) 2024-08-19 22:58:20 +08:00
noneflow[bot]
f540245aec 📝 Update changelog 2024-08-19 08:25:43 +00:00
KroMiose
7b3ca228ef 🍻 publish plugin nekro-agent (#2875) 2024-08-19 08:24:46 +00:00
noneflow[bot]
aa23adfd8a 📝 Update changelog 2024-08-18 14:42:21 +00:00
shengwang52005
95ee5d54e8 🍻 publish plugin nonebot_plugin_mute (#2892) 2024-08-18 14:41:35 +00:00
noneflow[bot]
eac0e7a656 📝 Update changelog 2024-08-18 07:08:21 +00:00
snowykami
e41ec29867 🍻 publish plugin LiteyukiBot(plugin) (#2904) 2024-08-18 07:07:26 +00:00
noneflow[bot]
42a922deb6 📝 Update changelog 2024-08-18 06:00:34 +00:00
tkgs0
8e70d55d77 🍻 publish plugin 复读6 (#2899) 2024-08-18 05:59:44 +00:00
noneflow[bot]
eeaf823ea9 🔖 Release 2.3.3 2024-08-18 03:57:17 +00:00
Ju4tCode
2195e07998 🔖 bump version 2.3.3 (#2903) 2024-08-18 11:53:33 +08:00
noneflow[bot]
70fb8fc8c6 📝 Update changelog 2024-08-18 03:03:33 +00:00
shoucandanghehe
b78ae1ef0d 🍻 publish plugin nonebot-plugin-wait-a-minute (#2901) 2024-08-18 03:02:40 +00:00
noneflow[bot]
6af5566466 📝 Update changelog 2024-08-18 02:58:50 +00:00
tkgs0
8ceca0a90d 🍻 publish plugin 你看我像 (#2894) 2024-08-18 02:57:59 +00:00
noneflow[bot]
08fa6dbfc8 📝 Update changelog 2024-08-18 02:56:16 +00:00
gsskk
967aa758d3 🍻 publish plugin dify插件 (#2888) 2024-08-18 02:55:19 +00:00
noneflow[bot]
e3b10fbdc2 📝 Update changelog 2024-08-17 15:15:03 +00:00
shengwang52005
2115e5c6ec 🍻 publish plugin mai2_pcount (#2890) 2024-08-17 15:14:05 +00:00
noneflow[bot]
41dc908032 📝 Update changelog 2024-08-17 02:49:42 +00:00
N791
2b2f24628d 🍻 publish plugin nonebot-plugin-ehentai-search (#2884) 2024-08-17 02:48:53 +00:00
noneflow[bot]
1cc5d1af33 📝 Update changelog 2024-08-16 13:27:05 +00:00
shengwang52005
88074cf5c3 🍻 publish plugin pokepoke_miss (#2878) 2024-08-16 13:25:54 +00:00
noneflow[bot]
5d637eed95 📝 Update changelog 2024-08-15 03:35:09 +00:00
lm175
362c43ce5f 🍻 publish plugin 聊天截图伪造 (#2879) 2024-08-15 03:34:08 +00:00
noneflow[bot]
622b8eb51e 📝 Update changelog 2024-08-13 11:23:45 +00:00
hanasa2023
c369dcf781 🍻 publish plugin ba-tools (#2863) 2024-08-13 11:22:46 +00:00
noneflow[bot]
53d1e1dee9 📝 Update changelog 2024-08-13 07:39:10 +00:00
Leo Q
75f5825cff 📝 Docs: 添加 Windows Powershell 设置环境变量方法 (#2874)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2024-08-13 15:38:17 +08:00
noneflow[bot]
d05c90787c 📝 Update changelog 2024-08-12 15:23:00 +00:00
BEISNWKZNAN
e07ba36a4a 🍻 publish plugin 精华消息管理 (#2872) 2024-08-12 15:22:10 +00:00
noneflow[bot]
f7c05d9a08 📝 Update changelog 2024-08-11 15:24:59 +00:00
kawaiior
59c5a1a35d 🍻 publish plugin B站收藏夹监视器 (#2868) 2024-08-11 15:24:07 +00:00
noneflow[bot]
3eb653821e 📝 Update changelog 2024-08-11 12:47:36 +00:00
Ju4tCode
214bc838c2 📝 Docs: 更新 localstore 插件文档 (#2871) 2024-08-11 20:46:41 +08:00
noneflow[bot]
79c7ea5bab 📝 Update changelog 2024-08-11 07:17:00 +00:00
Ju4tCode
b59b1be6ff Feature: 优化依赖注入在 pydantic v2 下的性能 (#2870) 2024-08-11 15:15:59 +08:00
noneflow[bot]
aeb75a6ce3 📝 Update changelog 2024-08-11 07:05:13 +00:00
iam57ao
847325a119 🍻 publish plugin Alist (#2864) 2024-08-11 07:04:22 +00:00
noneflow[bot]
26eabfaf6f 📝 Update changelog 2024-08-09 12:18:53 +00:00
月ヶ瀬
40a7b97220 ✏️ Plugin: 修改插件 system-command 信息 (#2862)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-09 20:18:01 +08:00
noneflow[bot]
91b40748c4 📝 Update changelog 2024-08-09 12:14:40 +00:00
SamuNatsu
013a2f94d6 🍻 publish plugin nonebot-plugin-deer-pipe (#2858) 2024-08-09 12:13:51 +00:00
noneflow[bot]
74d280ed75 📝 Update changelog 2024-08-08 09:04:11 +00:00
luosheng520qaq
b7d46de10e 🍻 publish plugin 漂流瓶 (#2860) 2024-08-08 09:03:15 +00:00
noneflow[bot]
c37b5bbbca 📝 Update changelog 2024-08-07 13:11:34 +00:00
zhongwen-4
5e08e73698 🍻 publish plugin 奇怪的小功能 (#2850) 2024-08-07 13:10:40 +00:00
pre-commit-ci[bot]
b27bb92d03 ⬆️ auto update by pre-commit hooks (#2857)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-06 14:43:44 +08:00
noneflow[bot]
6bf8858cc6 📝 Update changelog 2024-08-06 06:20:30 +00:00
Ju4tCode
c97a780645 Feature: 添加遗漏的类型标注 (#2856) 2024-08-06 14:19:17 +08:00
noneflow[bot]
976c1cd8e0 📝 Update changelog 2024-08-03 13:21:57 +00:00
ALittleBot
26fd6f8a6c ✏️ Plugin: 修改 nonebot-plugin-fishing 插件作者 (#2854) 2024-08-03 21:20:57 +08:00
noneflow[bot]
0020ad28ba 📝 Update changelog 2024-08-03 09:53:27 +00:00
CCYellowStar2
ba9ca63f10 🍻 publish plugin SunoAI音乐生成 (#2852) 2024-08-03 09:52:24 +00:00
noneflow[bot]
28b5b732c2 📝 Update changelog 2024-07-29 12:38:05 +00:00
KomoriDev
b944da8445 🍻 publish plugin 谁是卷王 (#2848) 2024-07-29 12:37:02 +00:00
noneflow[bot]
5cab166d6b 📝 Update changelog 2024-07-28 15:04:05 +00:00
zhaomaoniu
546cdb4229 🍻 publish plugin GPT-SoVITS 语音合成 (#2846) 2024-07-28 15:02:59 +00:00
noneflow[bot]
77790fad1f 📝 Update changelog 2024-07-28 12:41:01 +00:00
Alpaca4610
bcf849c98f 🍻 publish plugin 基于清影的AI视频生成 (#2842) 2024-07-28 12:40:00 +00:00
noneflow[bot]
f7b3c8af02 📝 Update changelog 2024-07-26 03:42:50 +00:00
tkgs0
cced60589c 🍻 publish plugin 命令行 (#2839) 2024-07-26 03:41:42 +00:00
noneflow[bot]
62adb32c94 📝 Update changelog 2024-07-22 07:10:44 +00:00
Lonely-Sails
6ab752dcdb ✏️ Bot: 更新 Minecraft QQBot 信息 (#2838) 2024-07-22 15:09:31 +08:00
noneflow[bot]
4d6f071739 📝 Update changelog 2024-07-21 14:58:12 +00:00
wyf7685
bd140c2ceb 🍻 publish plugin exe_code (#2834) 2024-07-21 14:57:09 +00:00
noneflow[bot]
59d9991aa4 📝 Update changelog 2024-07-21 12:56:40 +00:00
Lonely-Sails
55e7f59e40 🍻 publish bot Minecraft_QQBot (#2836) 2024-07-21 12:55:37 +00:00
noneflow[bot]
bb83483020 📝 Update changelog 2024-07-21 12:36:25 +00:00
This-is-XiaoDeng
5300ef5119 🍻 publish plugin nonebot-plugin-autopush (#2832) 2024-07-21 12:35:17 +00:00
noneflow[bot]
5a50d4203c 📝 Update changelog 2024-07-21 10:07:54 +00:00
SwedishDoveCooker
01a96f3086 🍻 publish plugin vv_helper (#2820) 2024-07-21 10:06:53 +00:00
noneflow[bot]
0570d779ee 📝 Update changelog 2024-07-21 04:51:31 +00:00
Cvandia
18d0bc2c81 🍻 publish plugin nonebot_plugin_game_torrent (#2826) 2024-07-21 04:50:31 +00:00
Ju4tCode
87e0d8148f 👷 Fix: preview alias and commit status (#2831) 2024-07-21 12:40:50 +08:00
Ju4tCode
53d8989145 🐛 Fix: website preview CD (#2830) 2024-07-21 12:14:18 +08:00
noneflow[bot]
5433b4ebdf 📝 Update changelog 2024-07-21 03:29:14 +00:00
wyeeeee
f10cecf16a 🍻 publish plugin 每日油价 (#2821) 2024-07-21 03:28:10 +00:00
Ju4tCode
60a3f6f4cc 👷 Security: 拆分 PR Website CI/CD (#2829) 2024-07-21 11:23:15 +08:00
noneflow[bot]
f70ae89098 📝 Update changelog 2024-07-20 06:04:34 +00:00
Ju4tCode
2f60c5e9b4 🚨 Fix: 错误的类型标注和 annotated 处理 (#2828) 2024-07-20 14:03:32 +08:00
noneflow[bot]
015ddd9517 📝 Update changelog 2024-07-18 15:18:51 +00:00
StarXinXin
f1539d9ec4 🍻 publish bot 星辰 Bot (#2823) 2024-07-18 15:17:48 +00:00
noneflow[bot]
2d0444ba75 📝 Update changelog 2024-07-18 13:35:13 +00:00
shiyihang2007
ed2c222e83 🍻 publish plugin wordle (#2817) 2024-07-18 13:34:08 +00:00
noneflow[bot]
ed048913a4 📝 Update changelog 2024-07-17 15:07:06 +00:00
SuperGuGuGu
121ba17698 ✏️ Plugin: 移除 kanonbot 插件 (#2819) 2024-07-17 23:05:55 +08:00
noneflow[bot]
d0f5a76c47 📝 Update changelog 2024-07-15 14:21:24 +00:00
NCBM
f809f1d089 🍻 publish plugin 再润 (#2815) 2024-07-15 14:20:12 +00:00
noneflow[bot]
070ad18781 📝 Update changelog 2024-07-13 13:10:45 +00:00
Shenyi Chen
56119ef1cc ✏️ Plugin: 更新插件 sparkapi 信息 (#2812) 2024-07-13 21:09:41 +08:00
noneflow[bot]
30195a35dc 📝 Update changelog 2024-07-13 12:07:10 +00:00
Asankilp
0500b7baab 🍻 publish plugin 漫展/展览查询 (#2810) 2024-07-13 12:06:04 +00:00
noneflow[bot]
08473a5c25 📝 Update changelog 2024-07-11 09:47:40 +00:00
Lumine
37ad14c277 ✏️ Plugin: 修改插件 miragetank & charpic 信息 (#2807) 2024-07-11 17:46:41 +08:00
noneflow[bot]
3e8c6ce541 📝 Update changelog 2024-07-11 09:41:38 +00:00
shi-yingyingjiang
3dd5539dc7 🍻 publish plugin 鸣潮wiki (#2801) 2024-07-11 09:40:36 +00:00
noneflow[bot]
559a0320a8 📝 Update changelog 2024-07-11 09:34:58 +00:00
1v7w
8646d885f0 🍻 publish plugin cloudfare R2 客服端 (#2805) 2024-07-11 09:33:53 +00:00
noneflow[bot]
84c008cdce 📝 Update changelog 2024-07-10 13:41:24 +00:00
QuickLAW
2671cb5b72 🍻 publish plugin AnyMate小助手 (#2760) 2024-07-10 13:40:12 +00:00
noneflow[bot]
379440708f 📝 Update changelog 2024-07-06 12:54:50 +00:00
315 changed files with 33915 additions and 10436 deletions

View File

@@ -9,13 +9,12 @@
"vscode": {
"settings": {
"python.analysis.diagnosticMode": "workspace",
"python.analysis.typeCheckingMode": "basic",
"ruff.organizeImports": false,
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.codeActionsOnSave": {
"source.fixAll.ruff": true,
"source.organizeImports": true
"source.fixAll.ruff": "explicit",
"source.organizeImports": "explicit"
}
},
"[javascript]": {

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,2 @@
open_collective: nonebot
custom: ["https://afdian.net/@nonebot"]
custom: ["https://afdian.com/@nonebot"]

View File

@@ -35,3 +35,12 @@ updates:
actions:
patterns:
- "*"
- package-ecosystem: devcontainers
directory: "/"
schedule:
interval: daily
groups:
devcontainers:
patterns:
- "*"

View File

@@ -40,7 +40,7 @@ jobs:
- name: Update Changelog
uses: docker://ghcr.io/nonebot/auto-changelog:master
with:
changelog_file: website/src/pages/changelog.md
changelog_file: website/src/changelog/changelog.md
latest_changes_position: '# 更新日志\n\n'
latest_changes_title: "## 最近更新"
replace_regex: '(?<=## 最近更新\n)[\s\S]*?(?=\n## )'

View File

@@ -32,7 +32,7 @@ jobs:
- name: Archive Changelog
uses: docker://ghcr.io/nonebot/auto-changelog:master
with:
changelog_file: website/src/pages/changelog.md
changelog_file: website/src/changelog/changelog.md
archive_regex: '(?<=## )最近更新(?=\n)'
archive_title: ${{ env.TAG_NAME }}
commit_and_push: false

View File

@@ -0,0 +1,99 @@
name: Site Deploy (Preview CD)
on:
workflow_run:
workflows: ["Site Deploy (Preview CI)"]
types:
- completed
jobs:
preview-cd:
runs-on: ubuntu-latest
concurrency:
group: pull-request-preview-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
if: ${{ github.event.workflow_run.conclusion == 'success' }}
environment: pull request
permissions:
actions: read
statuses: write
pull-requests: write
steps:
- name: Set Commit Status
uses: actions/github-script@v7
with:
script: |
github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.payload.workflow_run.head_sha,
context: 'Website Preview',
description: 'Deploying...',
state: 'pending',
})
- name: Download Artifact
uses: actions/download-artifact@v4
with:
name: website-preview
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Restore Context
run: |
cat action.env >> $GITHUB_ENV
- name: Set Deploy Name
run: |
echo "DEPLOY_NAME=deploy-preview-${{ env.PR_NUMBER }}" >> $GITHUB_ENV
- name: Deploy to Netlify
id: deploy
uses: nwtgck/actions-netlify@v3
with:
publish-dir: ./website/build
production-deploy: false
deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.event.workflow_run.head_sha }}"
alias: ${{ env.DEPLOY_NAME }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}
# action netlify has no pull request context, so we need to comment by ourselves
- name: Comment on Pull Request
uses: marocchino/sticky-pull-request-comment@v2
with:
header: website
number: ${{ env.PR_NUMBER }}
message: |
:rocket: Deployed to ${{ steps.deploy.outputs.deploy-url }}
- name: Set Commit Status
uses: actions/github-script@v7
if: always()
with:
script: |
if (`${{ job.status }}` === 'success') {
github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.payload.workflow_run.head_sha,
context: 'Website Preview',
description: `Deployed to ${{ steps.deploy.outputs.deploy-url }}`,
state: 'success',
target_url: `${{ steps.deploy.outputs.deploy-url }}`,
})
} else {
github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.payload.workflow_run.head_sha,
context: 'Website Preview',
description: `Deploy ${{ job.status }}`,
state: 'failure',
})
}

View File

@@ -0,0 +1,42 @@
name: Site Deploy (Preview CI)
on:
pull_request:
jobs:
preview-ci:
runs-on: ubuntu-latest
concurrency:
group: pull-request-preview-${{ github.event.number }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Setup Python Environment
uses: ./.github/actions/setup-python
- name: Setup Node Environment
uses: ./.github/actions/setup-node
- name: Build API Doc
uses: ./.github/actions/build-api-doc
- name: Build Doc
run: yarn build
- name: Export Context
run: |
echo "PR_NUMBER=${{ github.event.number }}" >> ./action.env
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: website-preview
path: |
./website/build
./action.env
retention-days: 1

View File

@@ -1,46 +0,0 @@
name: Site Deploy(Preview)
on:
pull_request_target:
jobs:
preview:
runs-on: ubuntu-latest
concurrency:
group: pull-request-preview-${{ github.event.number }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Setup Python Environment
uses: ./.github/actions/setup-python
- name: Setup Node Environment
uses: ./.github/actions/setup-node
- name: Build API Doc
uses: ./.github/actions/build-api-doc
- name: Build Doc
run: yarn build
- name: Get Deploy Name
run: |
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v3
with:
publish-dir: "./website/build"
production-deploy: false
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.sha }}"
enable-commit-comment: false
alias: ${{ env.DEPLOY_NAME }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@ docs_build/_build
!tests/.env
.docusaurus
website/docs/api/**/*.md
website/src/pages/changelog/**/*
# Created by https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux
# Edit at https://www.toptal.com/developers/gitignore?templates=python,node,visualstudiocode,jetbrains,macos,windows,linux

View File

@@ -7,30 +7,23 @@ ci:
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
rev: v0.7.1
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
stages: [commit]
stages: [pre-commit]
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
stages: [commit]
stages: [pre-commit]
- repo: https://github.com/psf/black
rev: 24.4.2
rev: 24.10.0
hooks:
- id: black
stages: [commit]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
stages: [commit]
stages: [pre-commit]
- repo: https://github.com/nonebot/nonemoji
rev: v0.1.4

View File

@@ -1,3 +1,3 @@
# Changelog
See [changelog.md](./website/src/pages/changelog.md) or <https://nonebot.dev/changelog>
See [changelog.md](./website/src/changelog/changelog.md) or <https://nonebot.dev/changelog>

View File

@@ -126,7 +126,6 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
| Mirai[仓库](https://github.com/nonebot/adapter-mirai)[协议](https://docs.mirai.mamoe.net/mirai-api-http/) | ✅ | QQ 协议 |
| 钉钉([仓库](https://github.com/nonebot/adapter-ding)[协议](https://open.dingtalk.com/document/) | 🤗 | 寻找 Maintainer暂不可用 |
| 开黑啦([仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila)[协议](https://developer.kookapp.cn/) | ↗️ | 由社区贡献 |
| Mirai[仓库](https://github.com/ieew/nonebot_adapter_mirai2)[协议](https://docs.mirai.mamoe.net/mirai-api-http/) | ↗️ | QQ 协议,由社区贡献 |
| Ntchat[仓库](https://github.com/JustUndertaker/adapter-ntchat) | ↗️ | 微信协议,由社区贡献 |
| MineCraft[仓库](https://github.com/17TheWord/nonebot-adapter-minecraft) | ↗️ | 由社区贡献 |
| BiliBili Live[仓库](https://github.com/wwweww/adapter-bilibili) | ↗️ | 由社区贡献 |

View File

@@ -69,16 +69,6 @@
"tags": [],
"is_official": true
},
{
"module_name": "nonebot.adapters.mirai2",
"project_link": "nonebot_adapter_mirai2",
"name": "mirai2",
"desc": "为 nonebot2 添加 mirai_api_http2.x的兼容适配器",
"author": "ieew",
"homepage": "https://github.com/ieew/nonebot_adapter_mirai2",
"tags": [],
"is_official": false
},
{
"module_name": "nonebot.adapters.onebot.v12",
"project_link": "nonebot-adapter-onebot",

View File

@@ -607,5 +607,55 @@
}
],
"is_official": false
},
{
"name": "星辰 Bot",
"desc": "欢迎使用 星辰 Bot 项目!这是一款基于 NoneBot2 打造的智能 QQ 机器人旨在为用户提供丰富的功能体验。无论是获取一言的灵感探索历史上的今天还是穿梭60s世界星辰 Bot 为您打开了全新的交流之门。快来尝试吧!",
"author": "StarXinXin",
"homepage": "https://github.com/StarXinXin/StarsBot",
"tags": [],
"is_official": false
},
{
"name": "Minecraft_QQBot",
"desc": "基于 NoneBot2 的 Minecraft 群服互联 QQ 机器人,支持多服务器多种方式连接。",
"author": "Lonely-Sails",
"homepage": "https://github.com/Minecraft-QQBot/BotServer",
"tags": [
{
"label": "Minecraft",
"color": "#ea5252"
},
{
"label": "娱乐",
"color": "#37a7e7"
}
],
"is_official": false
},
{
"name": "小安提Bot",
"desc": "服务于音游 舞萌DX 的多功能Bot",
"author": "Ant1816",
"homepage": "https://github.com/Ant1816/Ant1Bot",
"tags": [
{
"label": "maimaiDX",
"color": "#52ea9a"
},
{
"label": "音游",
"color": "#f74b18"
}
],
"is_official": false
},
{
"name": "CanrotBot",
"desc": "有很多实用功能的bot也有很多没什么用的娱乐功能接入了大模型并且有一部分功能可以被大模型调用。主打一个全都有",
"author": "wangyw15",
"homepage": "https://github.com/wangyw15/CanrotBot",
"tags": [],
"is_official": false
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1384
envs/test/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,11 @@ packages = [{ include = "nonebot-test.py" }]
[tool.poetry.dependencies]
python = "^3.9"
nonebug = "^0.3.7"
trio = "^0.27.0"
nonebug = "^0.4.1"
wsproto = "^1.2.0"
pytest-cov = "^5.0.0"
pytest-xdist = "^3.0.2"
pytest-asyncio = "^0.23.2"
werkzeug = ">=2.3.6,<4.0.0"
coverage-conditional-plugin = "^0.9.0"

View File

@@ -39,6 +39,8 @@
- `require` => {ref}``require` <nonebot.plugin.load.require>`
FrontMatter:
mdx:
format: md
sidebar_position: 0
description: nonebot 模块
"""

View File

@@ -3,6 +3,8 @@
使用 {ref}`nonebot.drivers.Driver.register_adapter` 注册适配器。
FrontMatter:
mdx:
format: md
sidebar_position: 0
description: nonebot.adapters 模块
"""

View File

@@ -3,22 +3,27 @@
为兼容 Pydantic V1 与 V2 版本,定义了一系列兼容函数与类供使用。
FrontMatter:
mdx:
format: md
sidebar_position: 16
description: nonebot.compat 模块
"""
from collections.abc import Generator
from functools import cached_property
from dataclasses import dataclass, is_dataclass
from typing_extensions import Self, get_args, get_origin, is_typeddict
from typing import (
TYPE_CHECKING,
Any,
Union,
Generic,
TypeVar,
Callable,
Optional,
Protocol,
Annotated,
overload,
)
from pydantic import VERSION, BaseModel
@@ -46,8 +51,8 @@ __all__ = (
"DEFAULT_CONFIG",
"FieldInfo",
"ModelField",
"TypeAdapter",
"extract_field_info",
"model_field_validate",
"model_fields",
"model_config",
"model_dump",
@@ -63,9 +68,10 @@ __autodoc__ = {
if PYDANTIC_V2: # pragma: pydantic-v2
from pydantic import GetCoreSchemaHandler
from pydantic import TypeAdapter as TypeAdapter
from pydantic_core import CoreSchema, core_schema
from pydantic._internal._repr import display_as_type
from pydantic import TypeAdapter, GetCoreSchemaHandler
from pydantic.fields import FieldInfo as BaseFieldInfo
Required = Ellipsis
@@ -125,6 +131,25 @@ if PYDANTIC_V2: # pragma: pydantic-v2
"""Construct a ModelField from given infos."""
return cls._construct(name, annotation, field_info or FieldInfo())
def __hash__(self) -> int:
# Each ModelField is unique for our purposes,
# to allow store them in a set.
return id(self)
@cached_property
def type_adapter(self) -> TypeAdapter:
"""TypeAdapter of the field.
Cache the TypeAdapter to avoid creating it multiple times.
Pydantic v2 uses too much cpu time to create TypeAdapter.
See: https://github.com/pydantic/pydantic/issues/9834
"""
return TypeAdapter(
Annotated[self.annotation, self.field_info],
config=None if self._annotation_has_config() else DEFAULT_CONFIG,
)
def _annotation_has_config(self) -> bool:
"""Check if the annotation has config.
@@ -152,10 +177,9 @@ if PYDANTIC_V2: # pragma: pydantic-v2
"""Get the display of the type of the field."""
return display_as_type(self.annotation)
def __hash__(self) -> int:
# Each ModelField is unique for our purposes,
# to allow store them in a set.
return id(self)
def validate_value(self, value: Any) -> Any:
"""Validate the value pass to the field."""
return self.type_adapter.validate_python(value)
def extract_field_info(field_info: BaseFieldInfo) -> dict[str, Any]:
"""Get FieldInfo init kwargs from a FieldInfo instance."""
@@ -164,15 +188,6 @@ if PYDANTIC_V2: # pragma: pydantic-v2
kwargs["annotation"] = field_info.rebuild_annotation()
return kwargs
def model_field_validate(
model_field: ModelField, value: Any, config: Optional[ConfigDict] = None
) -> Any:
"""Validate the value pass to the field."""
type: Any = Annotated[model_field.annotation, model_field.field_info]
return TypeAdapter(
type, config=None if model_field._annotation_has_config() else config
).validate_python(value)
def model_fields(model: type[BaseModel]) -> list[ModelField]:
"""Get field list of a model."""
@@ -305,6 +320,45 @@ else: # pragma: pydantic-v1
)
return cls._construct(name, annotation, field_info or FieldInfo())
def validate_value(self, value: Any) -> Any:
"""Validate the value pass to the field."""
v, errs_ = self.validate(value, {}, loc=())
if errs_:
raise ValueError(value, self)
return v
class TypeAdapter(Generic[T]):
@overload
def __init__(
self,
type: type[T],
*,
config: Optional[ConfigDict] = ...,
) -> None: ...
@overload
def __init__(
self,
type: Any,
*,
config: Optional[ConfigDict] = ...,
) -> None: ...
def __init__(
self,
type: Any,
*,
config: Optional[ConfigDict] = None,
) -> None:
self.type = type
self.config = config
def validate_python(self, value: Any) -> T:
return type_validate_python(self.type, value)
def validate_json(self, value: Union[str, bytes]) -> T:
return type_validate_json(self.type, value)
def extract_field_info(field_info: BaseFieldInfo) -> dict[str, Any]:
"""Get FieldInfo init kwargs from a FieldInfo instance."""
@@ -314,22 +368,6 @@ else: # pragma: pydantic-v1
kwargs.update(field_info.extra)
return kwargs
def model_field_validate(
model_field: ModelField, value: Any, config: Optional[type[ConfigDict]] = None
) -> Any:
"""Validate the value pass to the field.
Set config before validate to ensure validate correctly.
"""
if model_field.model_config is not config:
model_field.set_config(config or ConfigDict)
v, errs_ = model_field.validate(value, {}, loc=())
if errs_:
raise ValueError(value, model_field)
return v
def model_fields(model: type[BaseModel]) -> list[ModelField]:
"""Get field list of a model."""

View File

@@ -7,6 +7,8 @@ NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及
详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。
FrontMatter:
mdx:
format: md
sidebar_position: 1
description: nonebot.config 模块
"""

View File

@@ -1,6 +1,8 @@
"""本模块包含了 NoneBot 事件处理过程中使用到的常量。
FrontMatter:
mdx:
format: md
sidebar_position: 9
description: nonebot.consts 模块
"""

View File

@@ -1,22 +1,32 @@
"""本模块模块实现了依赖注入的定义与处理。
FrontMatter:
mdx:
format: md
sidebar_position: 0
description: nonebot.dependencies 模块
"""
import abc
import asyncio
import inspect
from functools import partial
from dataclasses import field, dataclass
from collections.abc import Iterable, Awaitable
from typing import Any, Generic, TypeVar, Callable, Optional, cast
import anyio
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.log import logger
from nonebot.typing import _DependentCallable
from nonebot.exception import SkippedException
from nonebot.utils import run_sync, is_coroutine_callable
from nonebot.compat import FieldInfo, ModelField, PydanticUndefined
from nonebot.utils import (
run_sync,
run_coro_with_shield,
is_coroutine_callable,
flatten_exception_group,
)
from .utils import check_field_type, get_typed_signature
@@ -82,7 +92,16 @@ class Dependent(Generic[R]):
)
async def __call__(self, **kwargs: Any) -> R:
try:
exception: Optional[BaseExceptionGroup[SkippedException]] = None
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
nonlocal exception
exception = exc_group
# raise one of the exceptions instead
excs = list(flatten_exception_group(exc_group))
logger.trace(f"{self} skipped due to {excs}")
with catch({SkippedException: _handle_skipped}):
# do pre-check
await self.check(**kwargs)
@@ -94,9 +113,8 @@ class Dependent(Generic[R]):
return await cast(Callable[..., Awaitable[R]], self.call)(**values)
else:
return await run_sync(cast(Callable[..., R], self.call))(**values)
except SkippedException as e:
logger.trace(f"{self} skipped due to {e}")
raise
raise exception
@staticmethod
def parse_params(
@@ -164,10 +182,17 @@ class Dependent(Generic[R]):
return cls(call, params, parameterless_params)
async def check(self, **params: Any) -> None:
await asyncio.gather(*(param._check(**params) for param in self.parameterless))
await asyncio.gather(
*(cast(Param, param.field_info)._check(**params) for param in self.params)
)
if self.parameterless:
async with anyio.create_task_group() as tg:
for param in self.parameterless:
tg.start_soon(partial(param._check, **params))
if self.params:
async with anyio.create_task_group() as tg:
for param in self.params:
tg.start_soon(
partial(cast(Param, param.field_info)._check, **params)
)
async def _solve_field(self, field: ModelField, params: dict[str, Any]) -> Any:
param = cast(Param, field.field_info)
@@ -183,10 +208,22 @@ class Dependent(Generic[R]):
await param._solve(**params)
# solve param values
values = await asyncio.gather(
*(self._solve_field(field, params) for field in self.params)
)
return {field.name: value for field, value in zip(self.params, values)}
result: dict[str, Any] = {}
if not self.params:
return result
async def _solve_field(field: ModelField, params: dict[str, Any]) -> None:
value = await self._solve_field(field, params)
result[field.name] = value
async with anyio.create_task_group() as tg:
for field in self.params:
# shield the task to prevent cancellation
# when one of the tasks raises an exception
# this will improve the dependency cache reusability
tg.start_soon(run_coro_with_shield, _solve_field(field, params))
return result
__autodoc__ = {"CustomConfig": False}

View File

@@ -1,5 +1,7 @@
"""
FrontMatter:
mdx:
format: md
sidebar_position: 1
description: nonebot.dependencies.utils 模块
"""
@@ -9,9 +11,9 @@ from typing import Any, Callable, ForwardRef
from loguru import logger
from nonebot.compat import ModelField
from nonebot.exception import TypeMisMatch
from nonebot.typing import evaluate_forwardref
from nonebot.compat import DEFAULT_CONFIG, ModelField, model_field_validate
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
@@ -51,6 +53,6 @@ def check_field_type(field: ModelField, value: Any) -> Any:
"""检查字段类型是否匹配"""
try:
return model_field_validate(field, value, DEFAULT_CONFIG)
return field.validate_value(value)
except ValueError:
raise TypeMisMatch(field, value)

View File

@@ -3,6 +3,8 @@
各驱动请继承以下基类。
FrontMatter:
mdx:
format: md
sidebar_position: 0
description: nonebot.drivers 模块
"""

View File

@@ -11,6 +11,8 @@ pip install nonebot2[aiohttp]
:::
FrontMatter:
mdx:
format: md
sidebar_position: 2
description: nonebot.drivers.aiohttp 模块
"""

View File

@@ -11,6 +11,8 @@ pip install nonebot2[fastapi]
:::
FrontMatter:
mdx:
format: md
sidebar_position: 1
description: nonebot.drivers.fastapi 模块
"""

View File

@@ -11,6 +11,8 @@ pip install nonebot2[httpx]
:::
FrontMatter:
mdx:
format: md
sidebar_position: 3
description: nonebot.drivers.httpx 模块
"""

View File

@@ -5,19 +5,25 @@
:::
FrontMatter:
mdx:
format: md
sidebar_position: 6
description: nonebot.drivers.none 模块
"""
import signal
import asyncio
import threading
from typing import Optional
from typing_extensions import override
import anyio
from anyio.abc import TaskGroup
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.log import logger
from nonebot.consts import WINDOWS
from nonebot.config import Env, Config
from nonebot.drivers import Driver as BaseDriver
from nonebot.utils import flatten_exception_group
HANDLED_SIGNALS = (
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
@@ -33,8 +39,8 @@ class Driver(BaseDriver):
def __init__(self, env: Env, config: Config):
super().__init__(env, config)
self.should_exit: asyncio.Event = asyncio.Event()
self.force_exit: bool = False
self.should_exit: anyio.Event = anyio.Event()
self.force_exit: anyio.Event = anyio.Event()
@property
@override
@@ -52,85 +58,98 @@ class Driver(BaseDriver):
def run(self, *args, **kwargs):
"""启动 none driver"""
super().run(*args, **kwargs)
loop = asyncio.get_event_loop()
loop.run_until_complete(self._serve())
anyio.run(self._serve)
async def _serve(self):
self._install_signal_handlers()
await self._startup()
if self.should_exit.is_set():
return
await self._main_loop()
await self._shutdown()
async with anyio.create_task_group() as driver_tg:
driver_tg.start_soon(self._handle_signals)
driver_tg.start_soon(self._listen_force_exit, driver_tg)
driver_tg.start_soon(self._handle_lifespan, driver_tg)
async def _startup(self):
async def _handle_signals(self):
try:
await self._lifespan.startup()
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Application startup failed. "
"Exiting.</bg #f8bbd0></r>"
)
self.should_exit.set()
return
logger.info("Application startup completed.")
async def _main_loop(self):
await self.should_exit.wait()
async def _shutdown(self):
logger.info("Shutting down")
logger.info("Waiting for application shutdown.")
try:
await self._lifespan.shutdown()
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running shutdown function. "
"Ignored!</bg #f8bbd0></r>"
)
for task in asyncio.all_tasks():
if task is not asyncio.current_task() and not task.done():
task.cancel()
await asyncio.sleep(0.1)
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
if tasks and not self.force_exit:
logger.info("Waiting for tasks to finish. (CTRL+C to force quit)")
while tasks and not self.force_exit:
await asyncio.sleep(0.1)
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
logger.info("Application shutdown complete.")
loop = asyncio.get_event_loop()
loop.stop()
def _install_signal_handlers(self) -> None:
if threading.current_thread() is not threading.main_thread():
# Signals can only be listened to from the main thread.
return
loop = asyncio.get_event_loop()
try:
for sig in HANDLED_SIGNALS:
loop.add_signal_handler(sig, self._handle_exit, sig, None)
with anyio.open_signal_receiver(*HANDLED_SIGNALS) as signal_receiver:
async for sig in signal_receiver:
self.exit(force=self.should_exit.is_set())
except NotImplementedError:
# Windows
for sig in HANDLED_SIGNALS:
signal.signal(sig, self._handle_exit)
signal.signal(sig, self._handle_legacy_signal)
def _handle_exit(self, sig, frame):
# backport for Windows signal handling
def _handle_legacy_signal(self, sig, frame):
self.exit(force=self.should_exit.is_set())
async def _handle_lifespan(self, tg: TaskGroup):
try:
await self._startup()
if self.should_exit.is_set():
return
await self._listen_exit()
await self._shutdown()
finally:
tg.cancel_scope.cancel()
async def _startup(self):
def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
self.should_exit.set()
for exc in flatten_exception_group(exc_group):
logger.opt(colors=True, exception=exc).error(
"<r><bg #f8bbd0>Error occurred while running startup hook."
"</bg #f8bbd0></r>"
)
logger.error(
"<r><bg #f8bbd0>Application startup failed. "
"Exiting.</bg #f8bbd0></r>"
)
with catch({Exception: handle_exception}):
await self._lifespan.startup()
if not self.should_exit.is_set():
logger.info("Application startup completed.")
async def _listen_exit(self, tg: Optional[TaskGroup] = None):
await self.should_exit.wait()
if tg is not None:
tg.cancel_scope.cancel()
async def _shutdown(self):
logger.info("Shutting down")
logger.info("Waiting for application shutdown. (CTRL+C to force quit)")
error_occurred: bool = False
def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
nonlocal error_occurred
error_occurred = True
for exc in flatten_exception_group(exc_group):
logger.opt(colors=True, exception=exc).error(
"<r><bg #f8bbd0>Error occurred while running shutdown hook."
"</bg #f8bbd0></r>"
)
logger.error(
"<r><bg #f8bbd0>Application shutdown failed. "
"Exiting.</bg #f8bbd0></r>"
)
with catch({Exception: handle_exception}):
await self._lifespan.shutdown()
if not error_occurred:
logger.info("Application shutdown complete.")
async def _listen_force_exit(self, tg: TaskGroup):
await self.force_exit.wait()
tg.cancel_scope.cancel()
def exit(self, force: bool = False):
"""退出 none driver
@@ -140,4 +159,4 @@ class Driver(BaseDriver):
if not self.should_exit.is_set():
self.should_exit.set()
if force:
self.force_exit = True
self.force_exit.set()

View File

@@ -11,6 +11,8 @@ pip install nonebot2[quart]
:::
FrontMatter:
mdx:
format: md
sidebar_position: 5
description: nonebot.drivers.quart 模块
"""

View File

@@ -11,6 +11,8 @@ pip install nonebot2[websockets]
:::
FrontMatter:
mdx:
format: md
sidebar_position: 4
description: nonebot.drivers.websockets 模块
"""
@@ -69,6 +71,8 @@ class Mixin(WebSocketClientMixin):
@override
@asynccontextmanager
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
if setup.proxy is not None:
logger.warning("proxy is not supported by websockets driver")
connection = Connect(
str(setup.url),
extra_headers={**setup.headers, **setup.cookies.as_header(setup)},

View File

@@ -25,6 +25,8 @@ NoneBotException
```
FrontMatter:
mdx:
format: md
sidebar_position: 10
description: nonebot.exception 模块
"""

View File

@@ -1,11 +1,14 @@
import abc
import asyncio
from functools import partial
from typing import TYPE_CHECKING, Any, Union, ClassVar, Optional, Protocol
import anyio
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.log import logger
from nonebot.config import Config
from nonebot.exception import MockApiException
from nonebot.utils import flatten_exception_group
from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook
if TYPE_CHECKING:
@@ -76,47 +79,98 @@ class Bot(abc.ABC):
skip_calling_api: bool = False
exception: Optional[Exception] = None
if coros := [hook(self, api, data) for hook in self._calling_api_hook]:
try:
logger.debug("Running CallingAPI hooks...")
await asyncio.gather(*coros)
except MockApiException as e:
if self._calling_api_hook:
logger.debug("Running CallingAPI hooks...")
def _handle_mock_api_exception(
exc_group: BaseExceptionGroup[MockApiException],
) -> None:
nonlocal skip_calling_api, result
excs = [
exc
for exc in flatten_exception_group(exc_group)
if isinstance(exc, MockApiException)
]
if not excs:
return
elif len(excs) > 1:
logger.warning(
"Multiple hooks want to mock API result. Use the first one."
)
skip_calling_api = True
result = e.result
result = excs[0].result
logger.debug(
f"Calling API {api} is cancelled. Return {result} instead."
)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running CallingAPI hook. "
"Running cancelled!</bg #f8bbd0></r>"
f"Calling API {api} is cancelled. Return {result!r} instead."
)
def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
for exc in flatten_exception_group(exc_group):
logger.opt(colors=True, exception=exc).error(
"<r><bg #f8bbd0>Error when running CallingAPI hook. "
"Running cancelled!</bg #f8bbd0></r>"
)
with catch(
{
MockApiException: _handle_mock_api_exception,
Exception: _handle_exception,
}
):
async with anyio.create_task_group() as tg:
for hook in self._calling_api_hook:
tg.start_soon(hook, self, api, data)
if not skip_calling_api:
try:
result = await self.adapter._call_api(self, api, **data)
except Exception as e:
exception = e
if coros := [
hook(self, exception, api, data, result) for hook in self._called_api_hook
]:
try:
logger.debug("Running CalledAPI hooks...")
await asyncio.gather(*coros)
except MockApiException as e:
# mock api result
result = e.result
# ignore exception
if self._called_api_hook:
logger.debug("Running CalledAPI hooks...")
def _handle_mock_api_exception(
exc_group: BaseExceptionGroup[MockApiException],
) -> None:
nonlocal result, exception
excs = [
exc
for exc in flatten_exception_group(exc_group)
if isinstance(exc, MockApiException)
]
if not excs:
return
elif len(excs) > 1:
logger.warning(
"Multiple hooks want to mock API result. Use the first one."
)
result = excs[0].result
exception = None
logger.debug(
f"Calling API {api} result is mocked. Return {result} instead."
)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running CalledAPI hook. "
"Running cancelled!</bg #f8bbd0></r>"
)
def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
for exc in flatten_exception_group(exc_group):
logger.opt(colors=True, exception=exc).error(
"<r><bg #f8bbd0>Error when running CalledAPI hook. "
"Running cancelled!</bg #f8bbd0></r>"
)
with catch(
{
MockApiException: _handle_mock_api_exception,
Exception: _handle_exception,
}
):
async with anyio.create_task_group() as tg:
for hook in self._called_api_hook:
tg.start_soon(hook, self, exception, api, data, result)
if exception:
raise exception

View File

@@ -1,6 +1,11 @@
from collections.abc import Awaitable
from types import TracebackType
from typing_extensions import TypeAlias
from typing import Any, Union, Callable, cast
from collections.abc import Iterable, Awaitable
from typing import Any, Union, Callable, Optional, cast
import anyio
from anyio.abc import TaskGroup
from exceptiongroup import suppress
from nonebot.utils import run_sync, is_coroutine_callable
@@ -11,10 +16,24 @@ LIFESPAN_FUNC: TypeAlias = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
class Lifespan:
def __init__(self) -> None:
self._task_group: Optional[TaskGroup] = None
self._startup_funcs: list[LIFESPAN_FUNC] = []
self._ready_funcs: list[LIFESPAN_FUNC] = []
self._shutdown_funcs: list[LIFESPAN_FUNC] = []
@property
def task_group(self) -> TaskGroup:
if self._task_group is None:
raise RuntimeError("Lifespan not started")
return self._task_group
@task_group.setter
def task_group(self, task_group: TaskGroup) -> None:
if self._task_group is not None:
raise RuntimeError("Lifespan already started")
self._task_group = task_group
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
self._startup_funcs.append(func)
return func
@@ -29,7 +48,7 @@ class Lifespan:
@staticmethod
async def _run_lifespan_func(
funcs: list[LIFESPAN_FUNC],
funcs: Iterable[LIFESPAN_FUNC],
) -> None:
for func in funcs:
if is_coroutine_callable(func):
@@ -38,18 +57,44 @@ class Lifespan:
await run_sync(cast(SYNC_LIFESPAN_FUNC, func))()
async def startup(self) -> None:
# create background task group
self.task_group = anyio.create_task_group()
await self.task_group.__aenter__()
# run startup funcs
if self._startup_funcs:
await self._run_lifespan_func(self._startup_funcs)
# run ready funcs
if self._ready_funcs:
await self._run_lifespan_func(self._ready_funcs)
async def shutdown(self) -> None:
async def shutdown(
self,
*,
exc_type: Optional[type[BaseException]] = None,
exc_val: Optional[BaseException] = None,
exc_tb: Optional[TracebackType] = None,
) -> None:
if self._shutdown_funcs:
await self._run_lifespan_func(self._shutdown_funcs)
# reverse shutdown funcs to ensure stack order
await self._run_lifespan_func(reversed(self._shutdown_funcs))
# shutdown background task group
self.task_group.cancel_scope.cancel()
with suppress(Exception):
await self.task_group.__aexit__(exc_type, exc_val, exc_tb)
self._task_group = None
async def __aenter__(self) -> None:
await self.startup()
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
await self.shutdown()
async def __aexit__(
self,
exc_type: Optional[type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
await self.shutdown(exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb)

View File

@@ -1,17 +1,20 @@
import abc
import asyncio
from types import TracebackType
from collections.abc import AsyncGenerator
from typing_extensions import Self, TypeAlias
from contextlib import AsyncExitStack, asynccontextmanager
from typing import TYPE_CHECKING, Any, Union, ClassVar, Optional
from anyio.abc import TaskGroup
from anyio import CancelScope, create_task_group
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.log import logger
from nonebot.config import Env, Config
from nonebot.dependencies import Dependent
from nonebot.exception import SkippedException
from nonebot.utils import escape_tag, run_coro_with_catch
from nonebot.internal.params import BotParam, DependParam, DefaultParam
from nonebot.utils import escape_tag, run_coro_with_catch, flatten_exception_group
from nonebot.typing import (
T_DependencyCache,
T_BotConnectionHook,
@@ -61,7 +64,6 @@ class Driver(abc.ABC):
self.config: Config = config
"""全局配置对象"""
self._bots: dict[str, "Bot"] = {}
self._bot_tasks: set[asyncio.Task] = set()
self._lifespan = Lifespan()
def __repr__(self) -> str:
@@ -75,6 +77,10 @@ class Driver(abc.ABC):
"""获取当前所有已连接的 Bot"""
return self._bots
@property
def task_group(self) -> TaskGroup:
return self._lifespan.task_group
def register_adapter(self, adapter: type["Adapter"], **kwargs) -> None:
"""注册一个协议适配器
@@ -112,8 +118,6 @@ class Driver(abc.ABC):
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
)
self.on_shutdown(self._cleanup)
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""注册一个启动时执行的函数"""
return self._lifespan.on_startup(func)
@@ -154,66 +158,63 @@ class Driver(abc.ABC):
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
self._bots[bot.self_id] = bot
if not self._bot_connection_hook:
return
def handle_exception(exc_group: BaseExceptionGroup) -> None:
for exc in flatten_exception_group(exc_group):
logger.opt(colors=True, exception=exc).error(
"<r><bg #f8bbd0>"
"Error when running WebSocketConnection hook:"
"</bg #f8bbd0></r>"
)
async def _run_hook(bot: "Bot") -> None:
dependency_cache: T_DependencyCache = {}
async with AsyncExitStack() as stack:
if coros := [
run_coro_with_catch(
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
(SkippedException,),
)
for hook in self._bot_connection_hook
]:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>"
"Error when running WebSocketConnection hook. "
"Running cancelled!"
"</bg #f8bbd0></r>"
with CancelScope(shield=True), catch({Exception: handle_exception}):
async with AsyncExitStack() as stack, create_task_group() as tg:
for hook in self._bot_connection_hook:
tg.start_soon(
run_coro_with_catch,
hook(
bot=bot, stack=stack, dependency_cache=dependency_cache
),
(SkippedException,),
)
task = asyncio.create_task(_run_hook(bot))
task.add_done_callback(self._bot_tasks.discard)
self._bot_tasks.add(task)
self.task_group.start_soon(_run_hook, bot)
def _bot_disconnect(self, bot: "Bot") -> None:
"""在连接断开后,调用该函数来注销 bot 对象"""
if bot.self_id in self._bots:
del self._bots[bot.self_id]
if not self._bot_disconnection_hook:
return
def handle_exception(exc_group: BaseExceptionGroup) -> None:
for exc in flatten_exception_group(exc_group):
logger.opt(colors=True, exception=exc).error(
"<r><bg #f8bbd0>"
"Error when running WebSocketDisConnection hook:"
"</bg #f8bbd0></r>"
)
async def _run_hook(bot: "Bot") -> None:
dependency_cache: T_DependencyCache = {}
async with AsyncExitStack() as stack:
if coros := [
run_coro_with_catch(
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
(SkippedException,),
)
for hook in self._bot_disconnection_hook
]:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>"
"Error when running WebSocketDisConnection hook. "
"Running cancelled!"
"</bg #f8bbd0></r>"
# shield cancellation to ensure bot disconnect hooks are always run
with CancelScope(shield=True), catch({Exception: handle_exception}):
async with create_task_group() as tg, AsyncExitStack() as stack:
for hook in self._bot_disconnection_hook:
tg.start_soon(
run_coro_with_catch,
hook(
bot=bot, stack=stack, dependency_cache=dependency_cache
),
(SkippedException,),
)
task = asyncio.create_task(_run_hook(bot))
task.add_done_callback(self._bot_tasks.discard)
self._bot_tasks.add(task)
async def _cleanup(self) -> None:
"""清理驱动器资源"""
if self._bot_tasks:
logger.opt(colors=True).debug(
"<y>Waiting for running bot connection hooks...</y>"
)
await asyncio.gather(*self._bot_tasks, return_exceptions=True)
self.task_group.start_soon(_run_hook, bot)
class Mixin(abc.ABC):

View File

@@ -15,7 +15,7 @@ def combine_driver(driver: type[D]) -> type[D]: ...
@overload
def combine_driver(
driver: type[D], _m: type[Mixin], *mixins: type[Mixin]
driver: type[D], __m: type[Mixin], /, *mixins: type[Mixin]
) -> type["CombinedDriver"]: ...

View File

@@ -22,11 +22,13 @@ from typing import ( # noqa: UP035
overload,
)
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.log import logger
from nonebot.internal.rule import Rule
from nonebot.utils import classproperty
from nonebot.dependencies import Param, Dependent
from nonebot.internal.permission import User, Permission
from nonebot.utils import classproperty, flatten_exception_group
from nonebot.internal.adapter import (
Bot,
Event,
@@ -76,7 +78,7 @@ T = TypeVar("T")
current_bot: ContextVar[Bot] = ContextVar("current_bot")
current_event: ContextVar[Event] = ContextVar("current_event")
current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
current_handler: ContextVar[Dependent] = ContextVar("current_handler")
current_handler: ContextVar[Dependent[Any]] = ContextVar("current_handler")
@dataclass
@@ -812,28 +814,34 @@ class Matcher(metaclass=MatcherMeta):
f"bot={bot}, event={event!r}, state={state!r}"
)
def _handle_stop_propagation(exc_group: BaseExceptionGroup[StopPropagation]):
self.block = True
with self.ensure_context(bot, event):
try:
# Refresh preprocess state
self.state.update(state)
with catch({StopPropagation: _handle_stop_propagation}):
# Refresh preprocess state
self.state.update(state)
while self.remain_handlers:
handler = self.remain_handlers.pop(0)
current_handler.set(handler)
logger.debug(f"Running handler {handler}")
try:
await handler(
matcher=self,
bot=bot,
event=event,
state=self.state,
stack=stack,
dependency_cache=dependency_cache,
)
except SkippedException:
logger.debug(f"Handler {handler} skipped")
except StopPropagation:
self.block = True
while self.remain_handlers:
handler = self.remain_handlers.pop(0)
current_handler.set(handler)
logger.debug(f"Running handler {handler}")
def _handle_skipped(
exc_group: BaseExceptionGroup[SkippedException],
):
logger.debug(f"Handler {handler} skipped")
with catch({SkippedException: _handle_skipped}):
await handler(
matcher=self,
bot=bot,
event=event,
state=self.state,
stack=stack,
dependency_cache=dependency_cache,
)
finally:
logger.info(f"{self} running complete")
@@ -846,10 +854,54 @@ class Matcher(metaclass=MatcherMeta):
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
):
try:
exc: Optional[Union[FinishedException, RejectedException, PausedException]] = (
None
)
def _handle_special_exception(
exc_group: BaseExceptionGroup[
Union[FinishedException, RejectedException, PausedException]
]
):
nonlocal exc
excs = list(flatten_exception_group(exc_group))
if len(excs) > 1:
logger.warning(
"Multiple session control exceptions occurred. "
"NoneBot will choose the proper one."
)
finished_exc = next(
(e for e in excs if isinstance(e, FinishedException)),
None,
)
rejected_exc = next(
(e for e in excs if isinstance(e, RejectedException)),
None,
)
paused_exc = next(
(e for e in excs if isinstance(e, PausedException)),
None,
)
exc = finished_exc or rejected_exc or paused_exc
elif isinstance(
excs[0], (FinishedException, RejectedException, PausedException)
):
exc = excs[0]
with catch(
{
(
FinishedException,
RejectedException,
PausedException,
): _handle_special_exception
}
):
await self.simple_run(bot, event, state, stack, dependency_cache)
except RejectedException:
if isinstance(exc, FinishedException):
pass
elif isinstance(exc, RejectedException):
await self.resolve_reject()
type_ = await self.update_type(bot, event, stack, dependency_cache)
permission = await self.update_permission(
@@ -870,7 +922,7 @@ class Matcher(metaclass=MatcherMeta):
default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater,
)
except PausedException:
elif isinstance(exc, PausedException):
type_ = await self.update_type(bot, event, stack, dependency_cache)
permission = await self.update_permission(
bot, event, stack, dependency_cache
@@ -890,5 +942,3 @@ class Matcher(metaclass=MatcherMeta):
default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater,
)
except FinishedException:
pass

View File

@@ -1,5 +1,5 @@
import asyncio
import inspect
from enum import Enum
from typing_extensions import Self, get_args, override, get_origin
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
from typing import (
@@ -13,8 +13,11 @@ from typing import (
cast,
)
import anyio
from exceptiongroup import BaseExceptionGroup, catch
from pydantic.fields import FieldInfo as PydanticFieldInfo
from nonebot.exception import SkippedException
from nonebot.dependencies import Param, Dependent
from nonebot.dependencies.utils import check_field_type
from nonebot.compat import FieldInfo, ModelField, PydanticUndefined, extract_field_info
@@ -93,6 +96,78 @@ def Depends(
return DependsInner(dependency, use_cache=use_cache, validate=validate)
class CacheState(str, Enum):
"""子依赖缓存状态"""
PENDING = "PENDING"
FINISHED = "FINISHED"
class DependencyCache:
"""子依赖结果缓存。
用于缓存子依赖的结果,以避免重复计算。
"""
def __init__(self):
self._state = CacheState.PENDING
self._result: Any = None
self._exception: Optional[BaseException] = None
self._waiter = anyio.Event()
def done(self) -> bool:
return self._state == CacheState.FINISHED
def result(self) -> Any:
"""获取子依赖结果"""
if self._state != CacheState.FINISHED:
raise RuntimeError("Result is not ready")
if self._exception is not None:
raise self._exception
return self._result
def exception(self) -> Optional[BaseException]:
"""获取子依赖异常"""
if self._state != CacheState.FINISHED:
raise RuntimeError("Result is not ready")
return self._exception
def set_result(self, result: Any) -> None:
"""设置子依赖结果"""
if self._state != CacheState.PENDING:
raise RuntimeError(f"Cache state invalid: {self._state}")
self._result = result
self._state = CacheState.FINISHED
self._waiter.set()
def set_exception(self, exception: BaseException) -> None:
"""设置子依赖异常"""
if self._state != CacheState.PENDING:
raise RuntimeError(f"Cache state invalid: {self._state}")
self._exception = exception
self._state = CacheState.FINISHED
self._waiter.set()
async def wait(self):
"""等待子依赖结果"""
await self._waiter.wait()
if self._state != CacheState.FINISHED:
raise RuntimeError("Invalid cache state")
if self._exception is not None:
raise self._exception
return self._result
class DependParam(Param):
"""子依赖注入参数。
@@ -102,7 +177,7 @@ class DependParam(Param):
"""
def __init__(
self, *args, dependent: Dependent, use_cache: bool, **kwargs: Any
self, *args, dependent: Dependent[Any], use_cache: bool, **kwargs: Any
) -> None:
super().__init__(*args, **kwargs)
self.dependent = dependent
@@ -114,7 +189,7 @@ class DependParam(Param):
@classmethod
def _from_field(
cls,
sub_dependent: Dependent,
sub_dependent: Dependent[Any],
use_cache: bool,
validate: Union[bool, PydanticFieldInfo],
) -> Self:
@@ -190,21 +265,31 @@ class DependParam(Param):
use_cache: bool = self.use_cache
dependency_cache = {} if dependency_cache is None else dependency_cache
sub_dependent: Dependent = self.dependent
sub_dependent = self.dependent
call = cast(Callable[..., Any], sub_dependent.call)
# solve sub dependency with current cache
sub_values = await sub_dependent.solve(
stack=stack,
dependency_cache=dependency_cache,
**kwargs,
)
exc: Optional[BaseExceptionGroup[SkippedException]] = None
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
nonlocal exc
exc = exc_group
with catch({SkippedException: _handle_skipped}):
sub_values = await sub_dependent.solve(
stack=stack,
dependency_cache=dependency_cache,
**kwargs,
)
if exc is not None:
raise exc
# run dependency function
task: asyncio.Task[Any]
if use_cache and call in dependency_cache:
return await dependency_cache[call]
elif is_gen_callable(call) or is_async_gen_callable(call):
return await dependency_cache[call].wait()
if is_gen_callable(call) or is_async_gen_callable(call):
assert isinstance(
stack, AsyncExitStack
), "Generator dependency should be called in context"
@@ -212,17 +297,28 @@ class DependParam(Param):
cm = run_sync_ctx_manager(contextmanager(call)(**sub_values))
else:
cm = asynccontextmanager(call)(**sub_values)
task = asyncio.create_task(stack.enter_async_context(cm))
dependency_cache[call] = task
return await task
target = stack.enter_async_context(cm)
elif is_coroutine_callable(call):
task = asyncio.create_task(call(**sub_values))
dependency_cache[call] = task
return await task
target = call(**sub_values)
else:
task = asyncio.create_task(run_sync(call)(**sub_values))
dependency_cache[call] = task
return await task
target = run_sync(call)(**sub_values)
dependency_cache[call] = cache = DependencyCache()
try:
result = await target
except Exception as e:
cache.set_exception(e)
raise
except BaseException as e:
cache.set_exception(e)
# remove cache when base exception occurs
# e.g. CancelledError
dependency_cache.pop(call, None)
raise
else:
cache.set_result(result)
return result
@override
async def _check(self, **kwargs: Any) -> None:

View File

@@ -1,8 +1,9 @@
import asyncio
from typing_extensions import Self
from contextlib import AsyncExitStack
from typing import Union, ClassVar, NoReturn, Optional
import anyio
from nonebot.dependencies import Dependent
from nonebot.utils import run_coro_with_catch
from nonebot.exception import SkippedException
@@ -70,22 +71,26 @@ class Permission:
"""
if not self.checkers:
return True
results = await asyncio.gather(
*(
run_coro_with_catch(
checker(
bot=bot,
event=event,
stack=stack,
dependency_cache=dependency_cache,
),
(SkippedException,),
False,
)
for checker in self.checkers
),
)
return any(results)
result = False
async def _run_checker(checker: Dependent[bool]) -> None:
nonlocal result
# calculate the result first to avoid data racing
is_passed = await run_coro_with_catch(
checker(
bot=bot, event=event, stack=stack, dependency_cache=dependency_cache
),
(SkippedException,),
False,
)
result |= is_passed
async with anyio.create_task_group() as tg:
for checker in self.checkers:
tg.start_soon(_run_checker, checker)
return result
def __and__(self, other: object) -> NoReturn:
raise RuntimeError("And operation between Permissions is not allowed.")

View File

@@ -1,7 +1,9 @@
import asyncio
from contextlib import AsyncExitStack
from typing import Union, ClassVar, NoReturn, Optional
import anyio
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.dependencies import Dependent
from nonebot.exception import SkippedException
from nonebot.typing import T_State, T_RuleChecker, T_DependencyCache
@@ -71,22 +73,33 @@ class Rule:
"""
if not self.checkers:
return True
try:
results = await asyncio.gather(
*(
checker(
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
)
for checker in self.checkers
)
result = True
def _handle_skipped_exception(
exc_group: BaseExceptionGroup[SkippedException],
) -> None:
nonlocal result
result = False
async def _run_checker(checker: Dependent[bool]) -> None:
nonlocal result
# calculate the result first to avoid data racing
is_passed = await checker(
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
)
except SkippedException:
return False
return all(results)
result &= is_passed
with catch({SkippedException: _handle_skipped_exception}):
async with anyio.create_task_group() as tg:
for checker in self.checkers:
tg.start_soon(_run_checker, checker)
return result
def __and__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
if other is None:

View File

@@ -8,6 +8,8 @@ NoneBot 使用 [`loguru`][loguru] 来记录日志信息。
[loguru]: https://github.com/Delgan/loguru
FrontMatter:
mdx:
format: md
sidebar_position: 7
description: nonebot.log 模块
"""

View File

@@ -1,6 +1,8 @@
"""本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。
FrontMatter:
mdx:
format: md
sidebar_position: 3
description: nonebot.matcher 模块
"""

View File

@@ -3,27 +3,36 @@
NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。
FrontMatter:
mdx:
format: md
sidebar_position: 2
description: nonebot.message 模块
"""
import asyncio
import contextlib
from datetime import datetime
from contextlib import AsyncExitStack
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Any, Callable, Optional
import anyio
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.log import logger
from nonebot.rule import TrieRule
from nonebot.dependencies import Dependent
from nonebot.matcher import Matcher, matchers
from nonebot.utils import escape_tag, run_coro_with_catch
from nonebot.exception import (
NoLogException,
StopPropagation,
IgnoredException,
SkippedException,
)
from nonebot.utils import (
escape_tag,
run_coro_with_catch,
run_coro_with_shield,
flatten_exception_group,
)
from nonebot.typing import (
T_State,
T_DependencyCache,
@@ -123,6 +132,21 @@ def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
return func
def _handle_ignored_exception(msg: str) -> Callable[[BaseExceptionGroup], None]:
def _handle(exc_group: BaseExceptionGroup[IgnoredException]) -> None:
logger.opt(colors=True).info(msg)
return _handle
def _handle_exception(msg: str) -> Callable[[BaseExceptionGroup], None]:
def _handle(exc_group: BaseExceptionGroup[Exception]) -> None:
for exc in flatten_exception_group(exc_group):
logger.opt(colors=True, exception=exc).error(msg)
return _handle
async def _apply_event_preprocessors(
bot: "Bot",
event: "Event",
@@ -150,10 +174,21 @@ async def _apply_event_preprocessors(
if show_log:
logger.debug("Running PreProcessors...")
try:
await asyncio.gather(
*(
run_coro_with_catch(
with catch(
{
IgnoredException: _handle_ignored_exception(
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
),
Exception: _handle_exception(
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
"Event ignored!</bg #f8bbd0></r>"
),
}
):
async with anyio.create_task_group() as tg:
for proc in _event_preprocessors:
tg.start_soon(
run_coro_with_catch,
proc(
bot=bot,
event=event,
@@ -163,22 +198,10 @@ async def _apply_event_preprocessors(
),
(SkippedException,),
)
for proc in _event_preprocessors
)
)
except IgnoredException:
logger.opt(colors=True).info(
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
)
return False
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
"Event ignored!</bg #f8bbd0></r>"
)
return False
return True
return True
return False
async def _apply_event_postprocessors(
@@ -205,10 +228,17 @@ async def _apply_event_postprocessors(
if show_log:
logger.debug("Running PostProcessors...")
try:
await asyncio.gather(
*(
run_coro_with_catch(
with catch(
{
Exception: _handle_exception(
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
)
}
):
async with anyio.create_task_group() as tg:
for proc in _event_postprocessors:
tg.start_soon(
run_coro_with_catch,
proc(
bot=bot,
event=event,
@@ -218,13 +248,6 @@ async def _apply_event_postprocessors(
),
(SkippedException,),
)
for proc in _event_postprocessors
)
)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
)
async def _apply_run_preprocessors(
@@ -252,35 +275,38 @@ async def _apply_run_preprocessors(
return True
# ensure matcher function can be correctly called
with matcher.ensure_context(bot, event):
try:
await asyncio.gather(
*(
run_coro_with_catch(
proc(
matcher=matcher,
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
),
(SkippedException,),
)
for proc in _run_preprocessors
with (
matcher.ensure_context(bot, event),
catch(
{
IgnoredException: _handle_ignored_exception(
f"{matcher} running is <b>cancelled</b>"
),
Exception: _handle_exception(
"<r><bg #f8bbd0>Error when running RunPreProcessors. "
"Running cancelled!</bg #f8bbd0></r>"
),
}
),
):
async with anyio.create_task_group() as tg:
for proc in _run_preprocessors:
tg.start_soon(
run_coro_with_catch,
proc(
matcher=matcher,
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
),
(SkippedException,),
)
)
except IgnoredException:
logger.opt(colors=True).info(f"{matcher} running is <b>cancelled</b>")
return False
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running RunPreProcessors. "
"Running cancelled!</bg #f8bbd0></r>"
)
return False
return True
return True
return False
async def _apply_run_postprocessors(
@@ -304,29 +330,32 @@ async def _apply_run_postprocessors(
if not _run_postprocessors:
return
with matcher.ensure_context(bot, event):
try:
await asyncio.gather(
*(
run_coro_with_catch(
proc(
matcher=matcher,
exception=exception,
bot=bot,
event=event,
state=matcher.state,
stack=stack,
dependency_cache=dependency_cache,
),
(SkippedException,),
)
for proc in _run_postprocessors
with (
matcher.ensure_context(bot, event),
catch(
{
Exception: _handle_exception(
"<r><bg #f8bbd0>Error when running RunPostProcessors"
"</bg #f8bbd0></r>"
)
}
),
):
async with anyio.create_task_group() as tg:
for proc in _run_postprocessors:
tg.start_soon(
run_coro_with_catch,
proc(
matcher=matcher,
exception=exception,
bot=bot,
event=event,
state=matcher.state,
stack=stack,
dependency_cache=dependency_cache,
),
(SkippedException,),
)
)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running RunPostProcessors</bg #f8bbd0></r>"
)
async def _check_matcher(
@@ -423,8 +452,9 @@ async def _run_matcher(
exception = None
logger.debug(f"Running {matcher}")
try:
logger.debug(f"Running {matcher}")
await matcher.run(bot, event, state, stack, dependency_cache)
except Exception as e:
logger.opt(colors=True, exception=e).error(
@@ -492,8 +522,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
用法:
```python
import asyncio
asyncio.create_task(handle_event(bot, event))
driver.task_group.start_soon(handle_event, bot, event)
```
"""
show_log = True
@@ -528,6 +557,13 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
)
break_flag = False
def _handle_stop_propagation(exc_group: BaseExceptionGroup) -> None:
nonlocal break_flag
break_flag = True
logger.debug("Stop event propagation")
# iterate through all priority until stop propagation
for priority in sorted(matchers.keys()):
if break_flag:
@@ -536,23 +572,30 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
if show_log:
logger.debug(f"Checking for matchers in priority {priority}...")
pending_tasks = [
check_and_run_matcher(
matcher, bot, event, state.copy(), stack, dependency_cache
)
for matcher in matchers[priority]
]
results = await asyncio.gather(*pending_tasks, return_exceptions=True)
for result in results:
if not isinstance(result, Exception):
continue
if isinstance(result, StopPropagation):
break_flag = True
logger.debug("Stop event propagation")
else:
logger.opt(colors=True, exception=result).error(
if not (priority_matchers := matchers[priority]):
continue
with catch(
{
StopPropagation: _handle_stop_propagation,
Exception: _handle_exception(
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
)
),
}
):
async with anyio.create_task_group() as tg:
for matcher in priority_matchers:
tg.start_soon(
run_coro_with_shield,
check_and_run_matcher(
matcher,
bot,
event,
state.copy(),
stack,
dependency_cache,
),
)
if show_log:
logger.debug("Checking for matchers completed")

View File

@@ -1,6 +1,8 @@
"""本模块定义了依赖注入的各类参数。
FrontMatter:
mdx:
format: md
sidebar_position: 4
description: nonebot.params 模块
"""

View File

@@ -5,6 +5,8 @@
只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。
FrontMatter:
mdx:
format: md
sidebar_position: 6
description: nonebot.permission 模块
"""

View File

@@ -32,6 +32,8 @@
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.model.PluginMetadata>`
FrontMatter:
mdx:
format: md
sidebar_position: 0
description: nonebot.plugin 模块
"""

View File

@@ -1,6 +1,8 @@
"""本模块定义插件加载接口。
FrontMatter:
mdx:
format: md
sidebar_position: 1
description: nonebot.plugin.load 模块
"""
@@ -20,7 +22,7 @@ from . import _managers, get_plugin, _module_name_to_plugin_id
try: # pragma: py-gte-311
import tomllib # pyright: ignore[reportMissingImports]
except ModuleNotFoundError: # pragma: py-lt-311
import tomli as tomllib
import tomli as tomllib # pyright: ignore[reportMissingImports]
def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:

View File

@@ -3,6 +3,8 @@
参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/)
FrontMatter:
mdx:
format: md
sidebar_position: 5
description: nonebot.plugin.manager 模块
"""

View File

@@ -1,6 +1,8 @@
"""本模块定义插件相关信息。
FrontMatter:
mdx:
format: md
sidebar_position: 3
description: nonebot.plugin.model 模块
"""

View File

@@ -1,6 +1,8 @@
"""本模块定义事件响应器便携定义函数。
FrontMatter:
mdx:
format: md
sidebar_position: 2
description: nonebot.plugin.on 模块
"""
@@ -114,7 +116,7 @@ def on(
rule: Optional[Union[Rule, T_RuleChecker]] = None,
permission: Optional[Union[Permission, T_PermissionChecker]] = None,
*,
handlers: Optional[list[Union[T_Handler, Dependent]]] = None,
handlers: Optional[list[Union[T_Handler, Dependent[Any]]]] = None,
temp: bool = False,
expire_time: Optional[Union[datetime, timedelta]] = None,
priority: int = 1,

View File

@@ -21,7 +21,7 @@ def on(
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
*,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -32,7 +32,7 @@ def on_metaevent(
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
*,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -43,7 +43,7 @@ def on_message(
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
*,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -54,7 +54,7 @@ def on_notice(
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
*,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -65,7 +65,7 @@ def on_request(
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
*,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -78,7 +78,7 @@ def on_startswith(
ignorecase: bool = ...,
*,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -91,7 +91,7 @@ def on_endswith(
ignorecase: bool = ...,
*,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -104,7 +104,7 @@ def on_fullmatch(
ignorecase: bool = ...,
*,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -116,7 +116,7 @@ def on_keyword(
rule: Rule | T_RuleChecker | None = ...,
*,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -130,7 +130,7 @@ def on_command(
force_whitespace: str | bool | None = ...,
*,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -144,7 +144,7 @@ def on_shell_command(
parser: ArgumentParser | None = ...,
*,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -157,7 +157,7 @@ def on_regex(
rule: Rule | T_RuleChecker | None = ...,
*,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -169,7 +169,7 @@ def on_type(
rule: Rule | T_RuleChecker | None = ...,
*,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -194,7 +194,7 @@ class CommandGroup(_Group):
*,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -209,7 +209,7 @@ class CommandGroup(_Group):
aliases: set[str | tuple[str, ...]] | None = ...,
force_whitespace: str | bool | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -224,7 +224,7 @@ class CommandGroup(_Group):
aliases: set[str | tuple[str, ...]] | None = ...,
parser: ArgumentParser | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -239,7 +239,7 @@ class MatcherGroup(_Group):
type: str = ...,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -252,7 +252,7 @@ class MatcherGroup(_Group):
type: str = ...,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -264,7 +264,7 @@ class MatcherGroup(_Group):
*,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -276,7 +276,7 @@ class MatcherGroup(_Group):
*,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -288,7 +288,7 @@ class MatcherGroup(_Group):
*,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -300,7 +300,7 @@ class MatcherGroup(_Group):
*,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -314,7 +314,7 @@ class MatcherGroup(_Group):
ignorecase: bool = ...,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -328,7 +328,7 @@ class MatcherGroup(_Group):
ignorecase: bool = ...,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -342,7 +342,7 @@ class MatcherGroup(_Group):
ignorecase: bool = ...,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -355,7 +355,7 @@ class MatcherGroup(_Group):
*,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -370,7 +370,7 @@ class MatcherGroup(_Group):
*,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -385,7 +385,7 @@ class MatcherGroup(_Group):
*,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -399,7 +399,7 @@ class MatcherGroup(_Group):
*,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,
@@ -412,7 +412,7 @@ class MatcherGroup(_Group):
*,
rule: Rule | T_RuleChecker | None = ...,
permission: Permission | T_PermissionChecker | None = ...,
handlers: list[T_Handler | Dependent] | None = ...,
handlers: list[T_Handler | Dependent[Any]] | None = ...,
temp: bool = ...,
expire_time: datetime | timedelta | None = ...,
priority: int = ...,

View File

@@ -5,6 +5,8 @@
只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。
FrontMatter:
mdx:
format: md
sidebar_position: 5
description: nonebot.rule 模块
"""

View File

@@ -6,6 +6,8 @@
[`typing`](https://docs.python.org/3/library/typing.html)。
FrontMatter:
mdx:
format: md
sidebar_position: 11
description: nonebot.typing 模块
"""
@@ -13,17 +15,15 @@ FrontMatter:
import sys
import types
import warnings
import contextlib
import typing as t
import typing_extensions as t_ext
from typing import TYPE_CHECKING, TypeVar
from typing_extensions import ParamSpec, TypeAlias, get_args, override, get_origin
if TYPE_CHECKING:
from asyncio import Task
from nonebot.adapters import Bot
from nonebot.permission import Permission
from nonebot.internal.params import DependencyCache
T = TypeVar("T")
P = ParamSpec("P")
@@ -86,9 +86,7 @@ def all_literal_values(type_: type[t.Any]) -> list[t.Any]:
def origin_is_annotated(origin: t.Optional[type[t.Any]]) -> bool:
"""判断是否是 Annotated 类型"""
with contextlib.suppress(TypeError):
return origin is not None and issubclass(origin, t_ext.Annotated)
return False
return origin is t_ext.Annotated
NONE_TYPES = {None, type(None), t.Literal[None], t_ext.Literal[None]}
@@ -259,5 +257,5 @@ T_PermissionUpdater: TypeAlias = _DependentCallable["Permission"]
- MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数
"""
T_DependencyCache: TypeAlias = dict[_DependentCallable[t.Any], "Task[t.Any]"]
T_DependencyCache: TypeAlias = dict[_DependentCallable[t.Any], "DependencyCache"]
"""依赖缓存, 用于存储依赖函数的返回值"""

View File

@@ -1,27 +1,30 @@
"""本模块包含了 NoneBot 的一些工具函数
FrontMatter:
mdx:
format: md
sidebar_position: 8
description: nonebot.utils 模块
"""
import re
import json
import asyncio
import inspect
import importlib
import contextlib
import dataclasses
from pathlib import Path
from collections import deque
from contextvars import copy_context
from functools import wraps, partial
from contextlib import AbstractContextManager, asynccontextmanager
from typing_extensions import ParamSpec, get_args, override, get_origin
from collections.abc import Mapping, Sequence, Coroutine, AsyncGenerator
from typing import Any, Union, Generic, TypeVar, Callable, Optional, overload
from collections.abc import Mapping, Sequence, Coroutine, Generator, AsyncGenerator
import anyio
import anyio.to_thread
from pydantic import BaseModel
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.log import logger
from nonebot.typing import (
@@ -37,6 +40,7 @@ R = TypeVar("R")
T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")
E = TypeVar("E", bound=BaseException)
def escape_tag(s: str) -> str:
@@ -176,11 +180,9 @@ def run_sync(call: Callable[P, R]) -> Callable[P, Coroutine[None, None, R]]:
@wraps(call)
async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
loop = asyncio.get_running_loop()
pfunc = partial(call, *args, **kwargs)
context = copy_context()
result = await loop.run_in_executor(None, partial(context.run, pfunc))
return result
return await anyio.to_thread.run_sync(
partial(call, *args, **kwargs), abandon_on_cancel=True
)
return _wrapper
@@ -232,10 +234,34 @@ async def run_coro_with_catch(
协程的返回值或发生异常时的指定值
"""
try:
with catch({exc: lambda exc_group: None}):
return await coro
except exc:
return return_on_err
return return_on_err
async def run_coro_with_shield(coro: Coroutine[Any, Any, T]) -> T:
"""运行协程并在取消时屏蔽取消异常。
参数:
coro: 要运行的协程
返回:
协程的返回值
"""
with anyio.CancelScope(shield=True):
return await coro
def flatten_exception_group(
exc_group: BaseExceptionGroup[E],
) -> Generator[E, None, None]:
for exc in exc_group.exceptions:
if isinstance(exc, BaseExceptionGroup):
yield from flatten_exception_group(exc)
else:
yield exc
def get_name(obj: Any) -> str:

2890
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "nonebot2"
version = "2.3.2"
version = "2.4.0"
description = "An asynchronous python bot framework."
authors = ["yanyongyu <yyy@nonebot.dev>"]
license = "MIT"
@@ -22,12 +22,14 @@ include = ["nonebot/py.typed"]
[tool.poetry.urls]
"Bug Tracker" = "https://github.com/nonebot/nonebot2/issues"
"Changelog" = "https://nonebot.dev/changelog"
"Funding" = "https://afdian.net/@nonebot"
"Funding" = "https://afdian.com/@nonebot"
[tool.poetry.dependencies]
python = "^3.9"
yarl = "^1.7.2"
anyio = "^4.4.0"
pygtrie = "^2.4.1"
exceptiongroup = "^1.2.2"
loguru = ">=0.6.0,<1.0.0"
python-dotenv = ">=0.21.0,<2.0.0"
typing-extensions = ">=4.4.0,<5.0.0"
@@ -44,7 +46,7 @@ uvicorn = { version = ">=0.20.0,<1.0.0", extras = [
], optional = true }
[tool.poetry.group.dev.dependencies]
ruff = "^0.4.0"
ruff = "^0.7.0"
isort = "^5.10.1"
black = "^24.0.0"
nonemoji = "^0.1.2"
@@ -65,7 +67,6 @@ fastapi = ["fastapi", "uvicorn"]
all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
[tool.pytest.ini_options]
asyncio_mode = "strict"
addopts = "--cov=nonebot --cov-report=term-missing"
filterwarnings = ["error", "ignore::DeprecationWarning"]

View File

@@ -1,8 +1,10 @@
import os
import threading
from pathlib import Path
from typing import TYPE_CHECKING
from functools import wraps
from collections.abc import Generator
from typing_extensions import ParamSpec
from typing import TYPE_CHECKING, TypeVar, Callable
import pytest
from nonebug import NONEBOT_INIT_KWARGS
@@ -20,6 +22,9 @@ os.environ["CONFIG_OVERRIDE"] = "new"
if TYPE_CHECKING:
from nonebot.plugin import Plugin
P = ParamSpec("P")
R = TypeVar("R")
collect_ignore = ["plugins/", "dynamic/", "bad_plugins/"]
@@ -38,14 +43,36 @@ def load_driver(request: pytest.FixtureRequest) -> Driver:
return DriverClass(Env(environment=global_driver.env), global_driver.config)
@pytest.fixture(scope="session", params=[pytest.param("asyncio"), pytest.param("trio")])
def anyio_backend(request: pytest.FixtureRequest):
return request.param
def run_once(func: Callable[P, R]) -> Callable[P, R]:
result = ...
@wraps(func)
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
nonlocal result
if result is not Ellipsis:
return result
result = func(*args, **kwargs)
return result
return _wrapper
@pytest.fixture(scope="session", autouse=True)
def load_plugin(nonebug_init: None) -> set["Plugin"]:
@run_once
def load_plugin(anyio_backend, nonebug_init: None) -> set["Plugin"]:
# preload global plugins
return nonebot.load_plugins(str(Path(__file__).parent / "plugins"))
@pytest.fixture(scope="session", autouse=True)
def load_builtin_plugin(nonebug_init: None) -> set["Plugin"]:
@run_once
def load_builtin_plugin(anyio_backend, nonebug_init: None) -> set["Plugin"]:
# preload builtin plugins
return nonebot.load_builtin_plugins("echo", "single_session")

View File

@@ -1,6 +1,7 @@
from typing import Annotated
from dataclasses import dataclass
import anyio
from pydantic import Field
from nonebot import on_message
@@ -105,3 +106,26 @@ async def validate_field(x: int = Depends(lambda: "1", validate=Field(gt=0))):
async def validate_field_fail(x: int = Depends(lambda: "0", validate=Field(gt=0))):
return x
async def _dep():
await anyio.sleep(1)
return 1
def _dep_mismatch():
return 1
async def cache_exception_func1(
dep: int = Depends(_dep),
mismatch: dict = Depends(_dep_mismatch),
):
raise RuntimeError("Never reach here")
async def cache_exception_func2(
dep: int = Depends(_dep),
match: int = Depends(_dep_mismatch),
):
return dep

View File

@@ -17,7 +17,7 @@ from nonebot.drivers import (
)
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_adapter_connect(app: App, driver: Driver):
last_connect_bot: Optional[Bot] = None
last_disconnect_bot: Optional[Bot] = None
@@ -45,7 +45,6 @@ async def test_adapter_connect(app: App, driver: Driver):
assert bot.self_id not in adapter.bots
@pytest.mark.asyncio
@pytest.mark.parametrize(
"driver",
[
@@ -75,7 +74,7 @@ async def test_adapter_connect(app: App, driver: Driver):
],
indirect=True,
)
async def test_adapter_server(driver: Driver):
def test_adapter_server(driver: Driver):
last_http_setup: Optional[HTTPServerSetup] = None
last_ws_setup: Optional[WebSocketServerSetup] = None
@@ -112,7 +111,7 @@ async def test_adapter_server(driver: Driver):
assert last_ws_setup is setup
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
"driver",
[
@@ -159,7 +158,7 @@ async def test_adapter_http_client(driver: Driver):
assert last_request is request
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
"driver",
[

View File

@@ -1,5 +1,6 @@
from typing import Any, Optional
import anyio
import pytest
from nonebug import App
@@ -7,7 +8,7 @@ from nonebot.adapters import Bot
from nonebot.exception import MockApiException
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_bot_call_api(app: App):
async with app.test_api() as ctx:
bot = ctx.create_bot()
@@ -23,7 +24,7 @@ async def test_bot_call_api(app: App):
await bot.call_api("test")
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_bot_calling_api_hook_simple(app: App):
runned: bool = False
@@ -49,7 +50,7 @@ async def test_bot_calling_api_hook_simple(app: App):
assert result is True
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_bot_calling_api_hook_mock(app: App):
runned: bool = False
@@ -76,7 +77,47 @@ async def test_bot_calling_api_hook_mock(app: App):
assert result is False
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_bot_calling_api_hook_multi_mock(app: App):
runned1: bool = False
runned2: bool = False
event = anyio.Event()
async def calling_api_hook1(bot: Bot, api: str, data: dict[str, Any]):
nonlocal runned1
runned1 = True
event.set()
raise MockApiException(1)
async def calling_api_hook2(bot: Bot, api: str, data: dict[str, Any]):
nonlocal runned2
runned2 = True
with anyio.fail_after(1):
await event.wait()
raise MockApiException(2)
hooks = set()
with pytest.MonkeyPatch.context() as m:
m.setattr(Bot, "_calling_api_hook", hooks)
Bot.on_calling_api(calling_api_hook1)
Bot.on_calling_api(calling_api_hook2)
assert hooks == {calling_api_hook1, calling_api_hook2}
async with app.test_api() as ctx:
bot = ctx.create_bot()
result = await bot.call_api("test")
assert runned1 is True
assert runned2 is True
assert result == 1
@pytest.mark.anyio
async def test_bot_called_api_hook_simple(app: App):
runned: bool = False
@@ -108,7 +149,7 @@ async def test_bot_called_api_hook_simple(app: App):
assert result is True
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_bot_called_api_hook_mock(app: App):
runned: bool = False
@@ -150,3 +191,56 @@ async def test_bot_called_api_hook_mock(app: App):
assert runned is True
assert result is False
@pytest.mark.anyio
async def test_bot_called_api_hook_multi_mock(app: App):
runned1: bool = False
runned2: bool = False
event = anyio.Event()
async def called_api_hook1(
bot: Bot,
exception: Optional[Exception],
api: str,
data: dict[str, Any],
result: Any,
):
nonlocal runned1
runned1 = True
event.set()
raise MockApiException(1)
async def called_api_hook2(
bot: Bot,
exception: Optional[Exception],
api: str,
data: dict[str, Any],
result: Any,
):
nonlocal runned2
runned2 = True
with anyio.fail_after(1):
await event.wait()
raise MockApiException(2)
hooks = set()
with pytest.MonkeyPatch.context() as m:
m.setattr(Bot, "_called_api_hook", hooks)
Bot.on_called_api(called_api_hook1)
Bot.on_called_api(called_api_hook2)
assert hooks == {called_api_hook1, called_api_hook2}
async with app.test_api() as ctx:
bot = ctx.create_bot()
ctx.should_call_api("test", {}, True)
result = await bot.call_api("test")
assert runned1 is True
assert runned2 is True
assert result == 1

View File

@@ -25,7 +25,7 @@ async def _dependency() -> int:
return 1
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_event_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(message, "_event_preprocessors", set())
@@ -58,7 +58,7 @@ async def test_event_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
assert runned, "event_preprocessor should runned"
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_event_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(message, "_event_preprocessors", set())
@@ -88,7 +88,7 @@ async def test_event_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPat
assert not runned, "matcher should not runned"
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_event_preprocessor_exception(
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
):
@@ -132,7 +132,7 @@ async def test_event_preprocessor_exception(
assert "RuntimeError: test" in capsys.readouterr().out
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_event_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(message, "_event_postprocessors", set())
@@ -165,7 +165,7 @@ async def test_event_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
assert runned, "event_postprocessor should runned"
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_event_postprocessor_exception(
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
):
@@ -202,7 +202,7 @@ async def test_event_postprocessor_exception(
assert "RuntimeError: test" in capsys.readouterr().out
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(message, "_run_preprocessors", set())
@@ -239,7 +239,7 @@ async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
assert runned, "run_preprocessor should runned"
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_run_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(message, "_run_preprocessors", set())
@@ -269,7 +269,7 @@ async def test_run_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch
assert not runned, "matcher should not runned"
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_run_preprocessor_exception(
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
):
@@ -313,7 +313,7 @@ async def test_run_preprocessor_exception(
assert "RuntimeError: test" in capsys.readouterr().out
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(message, "_run_postprocessors", set())
@@ -351,7 +351,7 @@ async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
assert runned, "run_postprocessor should runned"
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_run_postprocessor_exception(
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
):

View File

@@ -1,13 +1,14 @@
from typing import Any, Optional
from dataclasses import dataclass
from typing import Any, Optional, Annotated
import pytest
from pydantic import BaseModel
from pydantic import BaseModel, ValidationError
from nonebot.compat import (
DEFAULT_CONFIG,
Required,
FieldInfo,
TypeAdapter,
PydanticUndefined,
model_dump,
custom_validation,
@@ -16,14 +17,12 @@ from nonebot.compat import (
)
@pytest.mark.asyncio
async def test_default_config():
def test_default_config():
assert DEFAULT_CONFIG.get("extra") == "allow"
assert DEFAULT_CONFIG.get("arbitrary_types_allowed") is True
@pytest.mark.asyncio
async def test_field_info():
def test_field_info():
# required should be convert to PydanticUndefined
assert FieldInfo(Required).default is PydanticUndefined
@@ -31,8 +30,21 @@ async def test_field_info():
assert FieldInfo(test="test").extra["test"] == "test"
@pytest.mark.asyncio
async def test_model_dump():
def test_type_adapter():
t = TypeAdapter(Annotated[int, FieldInfo(ge=1)])
assert t.validate_python(2) == 2
with pytest.raises(ValidationError):
t.validate_python(0)
assert t.validate_json("2") == 2
with pytest.raises(ValidationError):
t.validate_json("0")
def test_model_dump():
class TestModel(BaseModel):
test1: int
test2: int
@@ -41,8 +53,7 @@ async def test_model_dump():
assert model_dump(TestModel(test1=1, test2=2), exclude={"test1"}) == {"test2": 2}
@pytest.mark.asyncio
async def test_custom_validation():
def test_custom_validation():
called = []
@custom_validation
@@ -69,8 +80,7 @@ async def test_custom_validation():
assert called == [1, 2]
@pytest.mark.asyncio
async def test_validate_json():
def test_validate_json():
class TestModel(BaseModel):
test1: int
test2: str

View File

@@ -50,16 +50,14 @@ class ExampleWithoutDelimiter(Example):
env_nested_delimiter = None
@pytest.mark.asyncio
async def test_config_no_env():
def test_config_no_env():
config = Example(_env_file=None)
assert config.simple == ""
with pytest.raises(AttributeError):
config.common_config
@pytest.mark.asyncio
async def test_config_with_env():
def test_config_with_env():
config = Example(_env_file=(".env", ".env.example"))
assert config.simple == "simple"
@@ -102,8 +100,7 @@ async def test_config_with_env():
config.other_nested_inner__b
@pytest.mark.asyncio
async def test_config_error_env():
def test_config_error_env():
with pytest.MonkeyPatch().context() as m:
m.setenv("COMPLEX", "not json")
@@ -111,8 +108,7 @@ async def test_config_error_env():
Example(_env_file=(".env", ".env.example"))
@pytest.mark.asyncio
async def test_config_without_delimiter():
def test_config_without_delimiter():
config = ExampleWithoutDelimiter()
assert config.nested.a == 1
assert config.nested.b == 0

View File

@@ -1,8 +1,8 @@
import json
import asyncio
from typing import Any, Optional
from http.cookies import SimpleCookie
import anyio
import pytest
from nonebug import App
@@ -25,7 +25,7 @@ from nonebot.drivers import (
)
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
"driver", [pytest.param("nonebot.drivers.none:Driver", id="none")], indirect=True
)
@@ -59,22 +59,22 @@ async def test_lifespan(driver: Driver):
@driver.on_shutdown
async def _shutdown1():
assert shutdown_log == []
assert shutdown_log == [2]
shutdown_log.append(1)
@driver.on_shutdown
async def _shutdown2():
assert shutdown_log == [1]
assert shutdown_log == []
shutdown_log.append(2)
async with driver._lifespan:
assert start_log == [1, 2]
assert ready_log == [1, 2]
assert shutdown_log == [1, 2]
assert shutdown_log == [2, 1]
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
"driver",
[
@@ -99,10 +99,10 @@ async def test_http_server(app: App, driver: Driver):
assert response.status_code == 200
assert response.text == "test"
await asyncio.sleep(1)
await anyio.sleep(1)
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
"driver",
[
@@ -155,10 +155,10 @@ async def test_websocket_server(app: App, driver: Driver):
await ws.close(code=1000)
await asyncio.sleep(1)
await anyio.sleep(1)
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
"driver",
[
@@ -171,9 +171,10 @@ async def test_cross_context(app: App, driver: Driver):
assert isinstance(driver, ASGIMixin)
ws: Optional[WebSocket] = None
ws_ready = asyncio.Event()
ws_should_close = asyncio.Event()
ws_ready = anyio.Event()
ws_should_close = anyio.Event()
# create a background task before the ws connection established
async def background_task():
try:
await ws_ready.wait()
@@ -185,8 +186,6 @@ async def test_cross_context(app: App, driver: Driver):
finally:
ws_should_close.set()
task = asyncio.create_task(background_task())
async def _handle_ws(websocket: WebSocket) -> None:
nonlocal ws
await websocket.accept()
@@ -199,7 +198,9 @@ async def test_cross_context(app: App, driver: Driver):
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:
async with anyio.create_task_group() as tg, app.test_server(driver.asgi) as ctx:
tg.start_soon(background_task)
client = ctx.get_client()
async with client.websocket_connect("/ws_test") as websocket:
@@ -211,11 +212,10 @@ async def test_cross_context(app: App, driver: Driver):
if not e.args or "websocket.close" not in str(e.args[0]):
raise
await task
await asyncio.sleep(1)
await anyio.sleep(1)
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
"driver",
[
@@ -304,10 +304,10 @@ async def test_http_client(driver: Driver, server_url: URL):
"test3": "test",
}, "file parsing error"
await asyncio.sleep(1)
await anyio.sleep(1)
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
"driver",
[
@@ -419,10 +419,10 @@ async def test_http_client_session(driver: Driver, server_url: URL):
"test3": "test",
}, "file parsing error"
await asyncio.sleep(1)
await anyio.sleep(1)
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
"driver",
[
@@ -452,10 +452,9 @@ async def test_websocket_client(driver: Driver, server_url: URL):
with pytest.raises(WebSocketClosed, match=r"code=1000"):
await ws.receive()
await asyncio.sleep(1)
await anyio.sleep(1)
@pytest.mark.asyncio
@pytest.mark.parametrize(
("driver", "driver_type"),
[
@@ -472,11 +471,11 @@ async def test_websocket_client(driver: Driver, server_url: URL):
],
indirect=["driver"],
)
async def test_combine_driver(driver: Driver, driver_type: str):
def test_combine_driver(driver: Driver, driver_type: str):
assert driver.type == driver_type
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_bot_connect_hook(app: App, driver: Driver):
with pytest.MonkeyPatch.context() as m:
conn_hooks: set[Dependent[Any]] = set()
@@ -533,7 +532,7 @@ async def test_bot_connect_hook(app: App, driver: Driver):
async with app.test_api() as ctx:
bot = ctx.create_bot()
await asyncio.sleep(1)
await anyio.sleep(1)
if not conn_should_be_called:
pytest.fail("on_bot_connect hook not called")

View File

@@ -4,7 +4,7 @@ from nonebug import App
from utils import FakeMessage, FakeMessageSegment, make_fake_event
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_echo(app: App):
from nonebot.plugins.echo import echo

View File

@@ -14,8 +14,7 @@ from nonebot import (
)
@pytest.mark.asyncio
async def test_init():
def test_init():
env = nonebot.get_driver().env
assert env == "test"
@@ -35,31 +34,28 @@ async def test_init():
assert config.not_nested == "some string"
@pytest.mark.asyncio
async def test_get_driver(app: App, monkeypatch: pytest.MonkeyPatch):
def test_get_driver(monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(nonebot, "_driver", None)
with pytest.raises(ValueError, match="initialized"):
get_driver()
@pytest.mark.asyncio
async def test_get_asgi(app: App, monkeypatch: pytest.MonkeyPatch):
def test_get_asgi():
driver = get_driver()
assert isinstance(driver, ReverseDriver)
assert isinstance(driver, ASGIMixin)
assert get_asgi() == driver.asgi
@pytest.mark.asyncio
async def test_get_app(app: App, monkeypatch: pytest.MonkeyPatch):
def test_get_app():
driver = get_driver()
assert isinstance(driver, ReverseDriver)
assert isinstance(driver, ASGIMixin)
assert get_app() == driver.server_app
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_get_adapter(app: App, monkeypatch: pytest.MonkeyPatch):
async with app.test_api() as ctx:
adapter = ctx.create_adapter()
@@ -74,8 +70,7 @@ async def test_get_adapter(app: App, monkeypatch: pytest.MonkeyPatch):
get_adapter("not exist")
@pytest.mark.asyncio
async def test_run(app: App, monkeypatch: pytest.MonkeyPatch):
def test_run(monkeypatch: pytest.MonkeyPatch):
runned = False
def mock_run(*args, **kwargs):
@@ -93,8 +88,7 @@ async def test_run(app: App, monkeypatch: pytest.MonkeyPatch):
assert runned
@pytest.mark.asyncio
async def test_get_bot(app: App, monkeypatch: pytest.MonkeyPatch):
def test_get_bot(app: App, monkeypatch: pytest.MonkeyPatch):
driver = get_driver()
with pytest.raises(ValueError, match="no bots"):

View File

@@ -12,8 +12,7 @@ from nonebot.permission import User, Permission
from nonebot.message import _check_matcher, check_and_run_matcher
@pytest.mark.asyncio
async def test_matcher_info(app: App):
def test_matcher_info(app: App):
from plugins.matcher.matcher_info import matcher
assert issubclass(matcher, Matcher)
@@ -43,7 +42,7 @@ async def test_matcher_info(app: App):
assert matcher._source.lineno == 3
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_matcher_check(app: App):
async def falsy():
return False
@@ -87,7 +86,7 @@ async def test_matcher_check(app: App):
assert await _check_matcher(test_rule_error, bot, event, {}) is False
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_matcher_handle(app: App):
from plugins.matcher.matcher_process import test_handle
@@ -102,7 +101,7 @@ async def test_matcher_handle(app: App):
ctx.should_finished()
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_matcher_got(app: App):
from plugins.matcher.matcher_process import test_got
@@ -124,7 +123,7 @@ async def test_matcher_got(app: App):
ctx.receive_event(bot, event_next)
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_matcher_receive(app: App):
from plugins.matcher.matcher_process import test_receive
@@ -141,7 +140,7 @@ async def test_matcher_receive(app: App):
ctx.should_paused()
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_matcher_combine(app: App):
from plugins.matcher.matcher_process import test_combine
@@ -164,7 +163,7 @@ async def test_matcher_combine(app: App):
ctx.receive_event(bot, event_next)
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_matcher_preset(app: App):
from plugins.matcher.matcher_process import test_preset
@@ -182,7 +181,7 @@ async def test_matcher_preset(app: App):
ctx.receive_event(bot, event_next)
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_matcher_overload(app: App):
from plugins.matcher.matcher_process import test_overload
@@ -196,7 +195,7 @@ async def test_matcher_overload(app: App):
ctx.should_finished()
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_matcher_destroy(app: App):
from plugins.matcher.matcher_process import test_destroy
@@ -210,7 +209,7 @@ async def test_matcher_destroy(app: App):
assert len(matchers[test_destroy.priority]) == 0
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_type_updater(app: App):
from plugins.matcher.matcher_type import test_type_updater, test_custom_updater
@@ -231,7 +230,7 @@ async def test_type_updater(app: App):
assert new_type == "custom"
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_default_permission_updater(app: App):
from plugins.matcher.matcher_permission import (
default_permission,
@@ -252,7 +251,7 @@ async def test_default_permission_updater(app: App):
assert checker.perm is default_permission
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_user_permission_updater(app: App):
from plugins.matcher.matcher_permission import (
default_permission,
@@ -274,7 +273,7 @@ async def test_user_permission_updater(app: App):
assert checker.perm is default_permission
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_custom_permission_updater(app: App):
from plugins.matcher.matcher_permission import (
new_permission,
@@ -291,7 +290,7 @@ async def test_custom_permission_updater(app: App):
assert new_perm is new_permission
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_run(app: App):
with app.provider.context({}):
assert not matchers
@@ -322,37 +321,46 @@ async def test_run(app: App):
assert len(matchers[0][0].handlers) == 0
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_temp(app: App):
from plugins.matcher.matcher_expire import test_temp_matcher
event = make_fake_event(_type="test")()
async with app.test_api() as ctx:
bot = ctx.create_bot()
assert test_temp_matcher in matchers[test_temp_matcher.priority]
await check_and_run_matcher(test_temp_matcher, bot, event, {})
assert test_temp_matcher not in matchers[test_temp_matcher.priority]
with app.provider.context({test_temp_matcher.priority: [test_temp_matcher]}):
async with app.test_api() as ctx:
bot = ctx.create_bot()
assert test_temp_matcher in matchers[test_temp_matcher.priority]
await check_and_run_matcher(test_temp_matcher, bot, event, {})
assert test_temp_matcher not in matchers[test_temp_matcher.priority]
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_datetime_expire(app: App):
from plugins.matcher.matcher_expire import test_datetime_matcher
event = make_fake_event()()
async with app.test_api() as ctx:
bot = ctx.create_bot()
assert test_datetime_matcher in matchers[test_datetime_matcher.priority]
await check_and_run_matcher(test_datetime_matcher, bot, event, {})
assert test_datetime_matcher not in matchers[test_datetime_matcher.priority]
with app.provider.context(
{test_datetime_matcher.priority: [test_datetime_matcher]}
):
async with app.test_matcher(test_datetime_matcher) as ctx:
bot = ctx.create_bot()
assert test_datetime_matcher in matchers[test_datetime_matcher.priority]
await check_and_run_matcher(test_datetime_matcher, bot, event, {})
assert test_datetime_matcher not in matchers[test_datetime_matcher.priority]
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_timedelta_expire(app: App):
from plugins.matcher.matcher_expire import test_timedelta_matcher
event = make_fake_event()()
async with app.test_api() as ctx:
bot = ctx.create_bot()
assert test_timedelta_matcher in matchers[test_timedelta_matcher.priority]
await check_and_run_matcher(test_timedelta_matcher, bot, event, {})
assert test_timedelta_matcher not in matchers[test_timedelta_matcher.priority]
with app.provider.context(
{test_timedelta_matcher.priority: [test_timedelta_matcher]}
):
async with app.test_api() as ctx:
bot = ctx.create_bot()
assert test_timedelta_matcher in matchers[test_timedelta_matcher.priority]
await check_and_run_matcher(test_timedelta_matcher, bot, event, {})
assert (
test_timedelta_matcher not in matchers[test_timedelta_matcher.priority]
)

View File

@@ -1,11 +1,9 @@
import pytest
from nonebug import App
from nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers
@pytest.mark.asyncio
async def test_manager(app: App):
def test_manager(app: App):
try:
default_provider = matchers.provider
matchers.set_provider(DEFAULT_PROVIDER_CLASS)

View File

@@ -2,6 +2,7 @@ import re
import pytest
from nonebug import App
from exceptiongroup import BaseExceptionGroup
from nonebot.matcher import Matcher
from nonebot.dependencies import Dependent
@@ -36,7 +37,7 @@ from nonebot.consts import (
UNKNOWN_PARAM = "Unknown parameter"
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_depend(app: App):
from plugins.param.param_depend import (
ClassDependency,
@@ -50,6 +51,8 @@ async def test_depend(app: App):
annotated_depend,
sub_type_mismatch,
validate_field_fail,
cache_exception_func1,
cache_exception_func2,
annotated_class_depend,
annotated_multi_depend,
annotated_prior_depend,
@@ -90,36 +93,67 @@ async def test_depend(app: App):
assert runned == [1, 1, 1]
runned.clear()
async with app.test_dependent(
annotated_class_depend, allow_types=[DependParam]
) as ctx:
ctx.should_return(ClassDependency(x=1, y=2))
with pytest.raises(TypeMisMatch): # noqa: PT012
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: # noqa: PT012
async with app.test_dependent(
sub_type_mismatch, allow_types=[DependParam, BotParam]
) as ctx:
bot = ctx.create_bot()
ctx.pass_params(bot=bot)
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
async with app.test_dependent(validate, allow_types=[DependParam]) as ctx:
ctx.should_return(1)
with pytest.raises(TypeMisMatch):
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:
async with app.test_dependent(validate_fail, allow_types=[DependParam]) as ctx:
...
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
async with app.test_dependent(validate_field, allow_types=[DependParam]) as ctx:
ctx.should_return(1)
with pytest.raises(TypeMisMatch):
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:
async with app.test_dependent(
validate_field_fail, allow_types=[DependParam]
) as ctx:
...
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
@pytest.mark.asyncio
# test cache reuse when exception raised
dependency_cache = {}
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:
async with app.test_dependent(
cache_exception_func1, allow_types=[DependParam]
) as ctx:
ctx.pass_params(dependency_cache=dependency_cache)
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
# dependency solve tasks should be shielded even if one of them raises an exception
assert len(dependency_cache) == 2
async with app.test_dependent(
cache_exception_func2, allow_types=[DependParam]
) as ctx:
ctx.pass_params(dependency_cache=dependency_cache)
ctx.should_return(1)
@pytest.mark.anyio
async def test_bot(app: App):
from plugins.param.param_bot import (
FooBot,
@@ -157,11 +191,14 @@ async def test_bot(app: App):
ctx.pass_params(bot=bot)
ctx.should_return(bot)
with pytest.raises(TypeMisMatch): # noqa: PT012
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: # noqa: PT012
async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot()
ctx.pass_params(bot=bot)
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
async with app.test_dependent(union_bot, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot(base=FooBot)
ctx.pass_params(bot=bot)
@@ -181,7 +218,7 @@ async def test_bot(app: App):
app.test_dependent(not_bot, allow_types=[BotParam])
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_event(app: App):
from plugins.param.param_event import (
FooEvent,
@@ -223,10 +260,13 @@ async def test_event(app: App):
ctx.pass_params(event=fake_fooevent)
ctx.should_return(fake_fooevent)
with pytest.raises(TypeMisMatch):
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:
async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_event)
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
async with app.test_dependent(union_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_fooevent)
ctx.should_return(fake_fooevent)
@@ -267,7 +307,7 @@ async def test_event(app: App):
ctx.should_return(fake_event.is_tome())
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_state(app: App):
from plugins.param.param_state import (
state,
@@ -418,7 +458,7 @@ async def test_state(app: App):
ctx.should_return(fake_state[KEYWORD_KEY])
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_matcher(app: App):
from plugins.param.param_matcher import (
FooMatcher,
@@ -457,10 +497,13 @@ async def test_matcher(app: App):
ctx.pass_params(matcher=foo_matcher)
ctx.should_return(foo_matcher)
with pytest.raises(TypeMisMatch):
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:
async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=fake_matcher)
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
async with app.test_dependent(union_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=foo_matcher)
ctx.should_return(foo_matcher)
@@ -496,7 +539,7 @@ async def test_matcher(app: App):
ctx.should_return(event_next)
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_arg(app: App):
from plugins.param.param_arg import (
arg,
@@ -548,7 +591,7 @@ async def test_arg(app: App):
ctx.should_return(message.extract_plain_text())
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_exception(app: App):
from plugins.param.param_exception import exc, legacy_exc
@@ -562,7 +605,7 @@ async def test_exception(app: App):
ctx.should_return(exception)
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_default(app: App):
from plugins.param.param_default import default
@@ -570,11 +613,10 @@ async def test_default(app: App):
ctx.should_return(1)
@pytest.mark.asyncio
async def test_priority():
def test_priority():
from plugins.param.priority import complex_priority
dependent = Dependent.parse(
dependent = Dependent[None].parse(
call=complex_priority,
allow_types=[
DependParam,

View File

@@ -22,7 +22,7 @@ from nonebot.permission import (
)
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_permission(app: App):
async def falsy():
return False
@@ -54,7 +54,7 @@ async def test_permission(app: App):
assert await Permission(truthy, skipped)(bot, event) is True
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(("type", "expected"), [("message", True), ("notice", False)])
async def test_message(type: str, expected: bool):
dependent = next(iter(MESSAGE.checkers))
@@ -66,7 +66,7 @@ async def test_message(type: str, expected: bool):
assert await dependent(event=event) == expected
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(("type", "expected"), [("message", False), ("notice", True)])
async def test_notice(type: str, expected: bool):
dependent = next(iter(NOTICE.checkers))
@@ -78,7 +78,7 @@ async def test_notice(type: str, expected: bool):
assert await dependent(event=event) == expected
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(("type", "expected"), [("message", False), ("request", True)])
async def test_request(type: str, expected: bool):
dependent = next(iter(REQUEST.checkers))
@@ -90,7 +90,7 @@ async def test_request(type: str, expected: bool):
assert await dependent(event=event) == expected
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
("type", "expected"), [("message", False), ("meta_event", True)]
)
@@ -104,7 +104,7 @@ async def test_metaevent(type: str, expected: bool):
assert await dependent(event=event) == expected
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
("type", "user_id", "expected"),
[
@@ -128,7 +128,7 @@ async def test_superuser(app: App, type: str, user_id: str, expected: bool):
assert await dependent(bot=bot, event=event) == expected
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
("session_ids", "session_id", "expected"),
[

View File

@@ -1,12 +1,10 @@
import pytest
from pydantic import BaseModel
import nonebot
from nonebot.plugin import PluginManager, _managers
@pytest.mark.asyncio
async def test_get_plugin():
def test_get_plugin():
# check simple plugin
plugin = nonebot.get_plugin("export")
assert plugin
@@ -22,8 +20,7 @@ async def test_get_plugin():
assert plugin.module_name == "plugins.nested.plugins.nested_subplugin"
@pytest.mark.asyncio
async def test_get_plugin_by_module_name():
def test_get_plugin_by_module_name():
# check get plugin by exact module name
plugin = nonebot.get_plugin_by_module_name("plugins.nested")
assert plugin
@@ -48,8 +45,7 @@ async def test_get_plugin_by_module_name():
assert plugin.module_name == "plugins.nested.plugins.nested_subplugin"
@pytest.mark.asyncio
async def test_get_available_plugin():
def test_get_available_plugin():
old_managers = _managers.copy()
_managers.clear()
try:
@@ -63,8 +59,7 @@ async def test_get_available_plugin():
_managers.extend(old_managers)
@pytest.mark.asyncio
async def test_get_plugin_config():
def test_get_plugin_config():
class Config(BaseModel):
plugin_config: int

View File

@@ -1,15 +1,44 @@
import sys
from pathlib import Path
from functools import wraps
from dataclasses import asdict
from typing import TypeVar, Callable
from typing_extensions import ParamSpec
import pytest
import nonebot
from nonebot.plugin import Plugin, PluginManager, _managers, inherit_supported_adapters
from nonebot.plugin import (
Plugin,
PluginManager,
_plugins,
_managers,
inherit_supported_adapters,
)
P = ParamSpec("P")
R = TypeVar("R")
@pytest.mark.asyncio
async def test_load_plugin():
def _recover(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
origin_managers = _managers.copy()
origin_plugins = _plugins.copy()
try:
return func(*args, **kwargs)
finally:
_managers.clear()
_managers.extend(origin_managers)
_plugins.clear()
_plugins.update(origin_plugins)
return _wrapper
@_recover
def test_load_plugin():
# check regular
assert nonebot.load_plugin("dynamic.simple")
@@ -20,8 +49,7 @@ async def test_load_plugin():
assert nonebot.load_plugin("some_plugin_not_exist") is None
@pytest.mark.asyncio
async def test_load_plugins(load_plugin: set[Plugin], load_builtin_plugin: set[Plugin]):
def test_load_plugins(load_plugin: set[Plugin], load_builtin_plugin: set[Plugin]):
loaded_plugins = {
plugin for plugin in nonebot.get_loaded_plugins() if not plugin.parent_plugin
}
@@ -44,8 +72,7 @@ async def test_load_plugins(load_plugin: set[Plugin], load_builtin_plugin: set[P
PluginManager(search_path=["plugins"]).load_all_plugins()
@pytest.mark.asyncio
async def test_load_nested_plugin():
def test_load_nested_plugin():
parent_plugin = nonebot.get_plugin("nested")
sub_plugin = nonebot.get_plugin("nested:nested_subplugin")
sub_plugin2 = nonebot.get_plugin("nested:nested_subplugin2")
@@ -57,16 +84,16 @@ async def test_load_nested_plugin():
assert parent_plugin.sub_plugins == {sub_plugin, sub_plugin2}
@pytest.mark.asyncio
async def test_load_json():
@_recover
def test_load_json():
nonebot.load_from_json("./plugins.json")
with pytest.raises(TypeError):
nonebot.load_from_json("./plugins.invalid.json")
@pytest.mark.asyncio
async def test_load_toml():
@_recover
def test_load_toml():
nonebot.load_from_toml("./plugins.toml")
with pytest.raises(ValueError, match="Cannot find"):
@@ -76,52 +103,54 @@ async def test_load_toml():
nonebot.load_from_toml("./plugins.invalid.toml")
@pytest.mark.asyncio
async def test_bad_plugin():
@_recover
def test_bad_plugin():
nonebot.load_plugins("bad_plugins")
assert nonebot.get_plugin("bad_plugin") is None
@pytest.mark.asyncio
async def test_require_loaded(monkeypatch: pytest.MonkeyPatch):
@_recover
def test_require_loaded(monkeypatch: pytest.MonkeyPatch):
def _patched_find(name: str):
pytest.fail("require existing plugin should not call find_manager_by_name")
monkeypatch.setattr("nonebot.plugin.load._find_manager_by_name", _patched_find)
with monkeypatch.context() as m:
m.setattr("nonebot.plugin.load._find_manager_by_name", _patched_find)
# require use module name
nonebot.require("plugins.export")
# require use plugin id
nonebot.require("export")
nonebot.require("nested:nested_subplugin")
# require use module name
nonebot.require("plugins.export")
# require use plugin id
nonebot.require("export")
nonebot.require("nested:nested_subplugin")
@pytest.mark.asyncio
async def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch):
m = PluginManager(["dynamic.require_not_loaded"], ["dynamic/require_not_loaded/"])
_managers.append(m)
@_recover
def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch):
pm = PluginManager(["dynamic.require_not_loaded"], ["dynamic/require_not_loaded/"])
_managers.append(pm)
num_managers = len(_managers)
origin_load = PluginManager.load_plugin
def _patched_load(self: PluginManager, name: str):
assert self is m
assert self is pm
return origin_load(self, name)
monkeypatch.setattr(PluginManager, "load_plugin", _patched_load)
with monkeypatch.context() as m:
m.setattr(PluginManager, "load_plugin", _patched_load)
# require standalone plugin
nonebot.require("dynamic.require_not_loaded")
# require searched plugin
nonebot.require("dynamic.require_not_loaded.subplugin1")
nonebot.require("require_not_loaded:subplugin2")
# require standalone plugin
nonebot.require("dynamic.require_not_loaded")
# require searched plugin
nonebot.require("dynamic.require_not_loaded.subplugin1")
nonebot.require("require_not_loaded:subplugin2")
assert len(_managers) == num_managers
@pytest.mark.asyncio
async def test_require_not_declared():
@_recover
def test_require_not_declared():
num_managers = len(_managers)
nonebot.require("dynamic.require_not_declared")
@@ -130,14 +159,13 @@ async def test_require_not_declared():
assert _managers[-1].plugins == {"dynamic.require_not_declared"}
@pytest.mark.asyncio
async def test_require_not_found():
@_recover
def test_require_not_found():
with pytest.raises(RuntimeError):
nonebot.require("some_plugin_not_exist")
@pytest.mark.asyncio
async def test_plugin_metadata():
def test_plugin_metadata():
from plugins.metadata import Config, FakeAdapter
plugin = nonebot.get_plugin("metadata")
@@ -157,8 +185,7 @@ async def test_plugin_metadata():
assert plugin.metadata.get_supported_adapters() == {FakeAdapter}
@pytest.mark.asyncio
async def test_inherit_supported_adapters_not_found():
def test_inherit_supported_adapters_not_found():
with pytest.raises(RuntimeError):
inherit_supported_adapters("some_plugin_not_exist")
@@ -166,7 +193,6 @@ async def test_inherit_supported_adapters_not_found():
inherit_supported_adapters("export")
@pytest.mark.asyncio
@pytest.mark.parametrize(
("inherit_plugins", "expected"),
[
@@ -233,7 +259,7 @@ async def test_inherit_supported_adapters_not_found():
),
],
)
async def test_inherit_supported_adapters_combine(
def test_inherit_supported_adapters_combine(
inherit_plugins: tuple[str], expected: set[str]
):
assert inherit_supported_adapters(*inherit_plugins) == expected

View File

@@ -1,17 +1,17 @@
import pytest
from nonebot.plugin import PluginManager, _managers
@pytest.mark.asyncio
async def test_load_plugin_name():
def test_load_plugin_name():
m = PluginManager(plugins=["dynamic.manager"])
_managers.append(m)
try:
_managers.append(m)
# load by plugin id
module1 = m.load_plugin("manager")
# load by module name
module2 = m.load_plugin("dynamic.manager")
assert module1
assert module2
assert module1 is module2
# load by plugin id
module1 = m.load_plugin("manager")
# load by module name
module2 = m.load_plugin("dynamic.manager")
assert module1
assert module2
assert module1 is module2
finally:
_managers.remove(m)

View File

@@ -18,7 +18,6 @@ from nonebot.rule import (
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
("matcher_name", "pre_rule_factory", "has_permission"),
[
@@ -102,7 +101,7 @@ from nonebot.rule import (
pytest.param("matcher_group_on_type", lambda e: IsTypeRule(e), True),
],
)
async def test_on(
def test_on(
matcher_name: str,
pre_rule_factory: Optional[Callable[[type[Event]], T_RuleChecker]],
has_permission: bool,
@@ -150,8 +149,7 @@ async def test_on(
assert matcher.module_name == "plugins.plugin.matchers"
@pytest.mark.asyncio
async def test_runtime_on():
def test_runtime_on():
import plugins.plugin.matchers as module
from plugins.plugin.matchers import matcher_on_factory

View File

@@ -49,7 +49,7 @@ from nonebot.rule import (
)
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_rule(app: App):
async def falsy():
return False
@@ -81,7 +81,7 @@ async def test_rule(app: App):
assert await Rule(truthy, skipped)(bot, event, {}) is False
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_trie(app: App):
TrieRule.add_prefix("/fake-prefix", TRIE_VALUE("/", ("fake-prefix",)))
@@ -146,7 +146,7 @@ async def test_trie(app: App):
del TrieRule.prefix["/fake-prefix"]
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
("msg", "ignorecase", "type", "text", "expected"),
[
@@ -186,7 +186,7 @@ async def test_startswith(
assert await dependent(event=event, state=state) == expected
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
("msg", "ignorecase", "type", "text", "expected"),
[
@@ -226,7 +226,7 @@ async def test_endswith(
assert await dependent(event=event, state=state) == expected
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
("msg", "ignorecase", "type", "text", "expected"),
[
@@ -266,7 +266,7 @@ async def test_fullmatch(
assert await dependent(event=event, state=state) == expected
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
("kws", "type", "text", "expected"),
[
@@ -298,7 +298,7 @@ async def test_keyword(
assert await dependent(event=event, state=state) == expected
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
("cmds", "force_whitespace", "cmd", "whitespace", "arg_text", "expected"),
[
@@ -344,7 +344,7 @@ async def test_command(
assert await dependent(state=state) == expected
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_shell_command():
state: T_State
CMD = ("test",)
@@ -451,7 +451,7 @@ async def test_shell_command():
assert state[SHELL_ARGS].status != 0
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize(
("pattern", "type", "text", "expected", "matched"),
[
@@ -494,7 +494,7 @@ async def test_regex(
assert result.span() == matched.span()
@pytest.mark.asyncio
@pytest.mark.anyio
@pytest.mark.parametrize("expected", [True, False])
async def test_to_me(expected: bool):
test_to_me = to_me()
@@ -507,7 +507,7 @@ async def test_to_me(expected: bool):
assert await dependent(event=event) == expected
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_is_type():
Event1 = make_fake_event()
Event2 = make_fake_event()

View File

@@ -5,7 +5,7 @@ import pytest
from utils import make_fake_event
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_matcher_mutex():
from nonebot.plugins.single_session import matcher_mutex, _running_matcher

View File

@@ -14,7 +14,7 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
### 异步优先
NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。
NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) / [trio](https://trio.readthedocs.io/en/stable/) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。
### 完整的类型注解

View File

@@ -77,6 +77,10 @@ NoneBot 为四种类型的事件响应器提供了五个基本的辅助函数:
## 内置响应规则
:::tip
响应规则的使用方法可以参考 [深入 - 响应规则](../appendices/rule.md)。
:::
### `startswith`
`startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`

View File

@@ -82,14 +82,16 @@ async def do_something(bot: Bot):
### 事件预处理
这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。
这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 会使 NoneBot 忽略该事件。
```python
from nonebot.exception import IgnoredException
from nonebot.message import event_preprocessor
@event_preprocessor
async def do_something(event: Event):
pass
if not event.is_tome():
raise IgnoredException("some reason")
```
### 事件后处理
@@ -106,14 +108,16 @@ async def do_something(event: Event):
### 运行预处理
这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态。
这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 也会使 NoneBot 忽略本次运行。
```python
from nonebot.message import run_preprocessor
from nonebot.exception import IgnoredException
@run_preprocessor
async def do_something(event: Event, matcher: Matcher):
pass
if not event.is_tome():
raise IgnoredException("some reason")
```
### 运行后处理

View File

@@ -73,10 +73,12 @@ CUSTOM_CONFIG=config in dotenv
同时,设置环境变量:
```bash
# windows
set CUSTOM_CONFIG "config in environment variables"
# windows cmd
set CUSTOM_CONFIG 'config in environment variables'
# windows powershell
$Env:CUSTOM_CONFIG='config in environment variables'
# linux/macOS
export CUSTOM_CONFIG="config in environment variables"
export CUSTOM_CONFIG='config in environment variables'
```
那最终 NoneBot 所读取的内容为环境变量中的内容,即 `config in environment variables`。
@@ -164,7 +166,7 @@ COMMON_CONFIG=common config # 这个配置项在任何环境中都会被加载
在生产环境中,可以通过设置环境变量 `ENVIRONMENT=prod` 来确保 NoneBot 读取正确的环境配置。
:::
#### .env.{ENVIRONMENT} 文件
#### .env.\{ENVIRONMENT\} 文件
`.env.{ENVIRONMENT}` 文件类似于预设,可以让我们在多套不同的配置方案中灵活切换,默认 NoneBot 会读取 `.env.prod` 配置。如果你使用了 `nb-cli` 创建 `simple` 项目,那么将含有两套预设配置:`.env.dev` 和 `.env.prod`。
@@ -176,7 +178,7 @@ nonebot.init(_env_file=".env.dev")
这将忽略在 `.env` 文件或环境变量中指定的 `ENVIRONMENT` 配置项。
## 读取配置项
## 读取全局配置项
NoneBot 的全局配置对象可以通过 `driver` 获取,如:
@@ -196,7 +198,7 @@ superusers = config.superusers
## 插件配置
在一个涉及大量配置项的项目中,通过直接读取配置项的方式显然并不高效。同时,由于额外的全局配置项没有预先定义,开发时编辑器将无法提示字段与类型,并且运行时没有对配置项直接进行合法性检查。那么就需要一种方式来规范定义插件配置项。
在一个涉及大量配置项的项目中,通过直接读取全局配置项的方式显然并不高效。同时,由于额外的全局配置项没有预先定义,开发时编辑器将无法提示字段与类型,并且运行时没有对配置项直接进行合法性检查。那么就需要一种方式来规范定义插件配置项。
在 NoneBot 中,我们使用强大高效的 `pydantic` 来定义配置模型,这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型:
@@ -218,7 +220,7 @@ class Config(BaseModel):
在 `config.py` 中,我们定义了一个 `Config` 类,它继承自 `pydantic.BaseModel`,并定义了一些配置项。在 `Config` 类中,我们还定义了一个 `check_priority` 方法,它用于检查 `weather_command_priority` 配置项的合法性。更多关于 `pydantic` 的编写方式,可以参考 [pydantic 官方文档](https://docs.pydantic.dev/)。
在定义好配置模型后,我们可以在插件加载时获取全局配置,导入插件自身的配置模型并使用
在定义好配置模型后,我们可以在插件加载时通过配置模型获取插件配置
```python {5,11} title=weather/__init__.py
from nonebot import get_plugin_config
@@ -295,8 +297,10 @@ DRIVER=~fastapi+~httpx+~websockets
<TabItem value="env" label="系统环境变量">
```bash
# windows
# windows cmd
set DRIVER '~fastapi+~httpx+~websockets'
# windows powershell
$Env:DRIVER='~fastapi+~httpx+~websockets'
# linux/macOS
export DRIVER='~fastapi+~httpx+~websockets'
```
@@ -331,8 +335,10 @@ HOST=127.0.0.1
<TabItem value="env" label="系统环境变量">
```bash
# windows
# windows cmd
set HOST '127.0.0.1'
# windows powershell
$Env:HOST='127.0.0.1'
# linux/macOS
export HOST='127.0.0.1'
```
@@ -367,8 +373,10 @@ PORT=8080
<TabItem value="env" label="系统环境变量">
```bash
# windows
# windows cmd
set PORT '8080'
# windows powershell
$Env:PORT='8080'
# linux/macOS
export PORT='8080'
```
@@ -407,8 +415,10 @@ LOG_LEVEL=DEBUG
<TabItem value="env" label="系统环境变量">
```bash
# windows
# windows cmd
set LOG_LEVEL 'DEBUG'
# windows powershell
$Env:LOG_LEVEL='DEBUG'
# linux/macOS
export LOG_LEVEL='DEBUG'
```
@@ -443,8 +453,10 @@ API_TIMEOUT=10.0
<TabItem value="env" label="系统环境变量">
```bash
# windows
# windows cmd
set API_TIMEOUT '10.0'
# windows powershell
$Env:API_TIMEOUT='10.0'
# linux/macOS
export API_TIMEOUT='10.0'
```
@@ -479,8 +491,10 @@ SUPERUSERS=["123123123"]
<TabItem value="env" label="系统环境变量">
```bash
# windows
# windows cmd
set SUPERUSERS '["123123123"]'
# windows powershell
$Env:SUPERUSERS='["123123123"]'
# linux/macOS
export SUPERUSERS='["123123123"]'
```
@@ -515,8 +529,10 @@ NICKNAME=["bot"]
<TabItem value="env" label="系统环境变量">
```bash
# windows
# windows cmd
set NICKNAME '["bot"]'
# windows powershell
$Env:NICKNAME='["bot"]'
# linux/macOS
export NICKNAME='["bot"]'
```
@@ -554,9 +570,12 @@ COMMAND_SEP=[".", " "]
<TabItem value="env" label="系统环境变量">
```bash
# windows
# windows cmd
set COMMAND_START '["/", ""]'
set COMMAND_SEP '[".", " "]'
# windows powershell
$Env:COMMAND_START='["/", ""]'
$Env:COMMAND_SEP='[".", " "]'
# linux/macOS
export COMMAND_START='["/", ""]'
export COMMAND_SEP='[".", " "]'
@@ -592,8 +611,10 @@ SESSION_EXPIRE_TIMEOUT=00:02:00
<TabItem value="env" label="系统环境变量">
```bash
# windows
# windows cmd
set SESSION_EXPIRE_TIMEOUT '00:02:00'
# windows powershell
$Env:SESSION_EXPIRE_TIMEOUT='00:02:00'
# linux/macOS
export SESSION_EXPIRE_TIMEOUT='00:02:00'
```

View File

@@ -29,8 +29,9 @@ import Messenger from "@site/src/components/Messenger";
例如,我们可以在 `weather` 插件中添加一个超级用户可用的指令:
```python {2,8} title=weather/__init__.py
```python {3,9} title=weather/__init__.py
from typing import Tuple
from nonebot.params import Command
from nonebot.permission import SUPERUSER
manage = on_command(

View File

@@ -20,7 +20,11 @@ options:
`RuleChecker` 是一个返回值为 `bool` 类型的依赖函数,即 `RuleChecker` 支持依赖注入。我们可以根据上一节中添加的[配置项](./config.mdx#插件配置),在 `weather` 插件目录中编写一个响应规则:
```python {3,4} title=weather/__init__.py
```python {7,8} title=weather/__init__.py
from nonebot import get_plugin_config
from .config import Config
plugin_config = get_plugin_config(Config)
async def is_enable() -> bool:
@@ -54,8 +58,11 @@ weather = on_command("天气", rule=rule)
在定义响应规则时,我们可以将规则进行细分,来更好地复用规则。而在使用时,我们需要合并多个规则。除了使用 `Rule` 对象来组合多个 `RuleChecker` 外,我们还可以对 `Rule` 对象进行合并。在原 `weather` 插件中,我们可以将 `rule=to_me()` 与 `rule=is_enable` 使用 `&` 运算符合并:
```python {10} title=weather/__init__.py
```python {13} title=weather/__init__.py
from nonebot.rule import to_me
from nonebot import get_plugin_config
from .config import Config
plugin_config = get_plugin_config(Config)
@@ -66,7 +73,7 @@ weather = on_command(
"天气",
rule=to_me() & is_enable,
aliases={"weather", "查天气"},
priority=plugin_config.weather_command_priority
priority=plugin_config.weather_command_priority,
block=True,
)
```

View File

@@ -71,14 +71,14 @@ alc = Alconna(".rd{roll:int}")
assert alc.parse(".rd123").header["roll"] == 123
```
Bracket Header 类似 python 里的 f-string 写法,通过 "{}" 声明匹配类型
Bracket Header 类似 python 里的 f-string 写法,通过 `"{}"` 声明匹配类型
"{}" 中的内容为 "name:type or pat"
`"{}"` 中的内容为 "name:type or pat"
- "{}", "{:}" ⇔ "(.+)", 占位符
- "{foo}" ⇔ "(?P&lt;foo&gt;.+)"
- "{:\d+}" ⇔ "(\d+)"
- "{foo:int}" ⇔ "(?P&lt;foo&gt;\d+)",其中 "int" 部分若能转为 `BasePattern` 则读取里面的表达式
- `"{}"`, `"{:}"``"(.+)"`, 占位符
- `"{foo}"``"(?P&lt;foo&gt;.+)"`
- `"{:\d+}"``"(\d+)"`
- `"{foo:int}"``"(?P&lt;foo&gt;\d+)"`,其中 `"int"` 部分若能转为 `BasePattern` 则读取里面的表达式
## 参数声明(Args)
@@ -321,7 +321,7 @@ opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1}))
- `keep_crlf`: 命令解析时是否保留换行字符
- `compact`: 命令是否允许第一个参数紧随头部
- `strict`: 命令是否严格匹配,若为 False 则未知参数将作为名为 $extra 的参数
- `context_style`: 命令上下文插值的风格None 为关闭bracket 为 {...}parentheses 为 $(...)
- `context_style`: 命令上下文插值的风格None 为关闭bracket 为 `{...}`parentheses 为 `$(...)`
- `extra`: 命令的自定义额外信息
元数据一定使用 `meta=...` 形式传入:

View File

@@ -96,7 +96,7 @@ class Other(Segment):
```
:::tips
:::tip
或许你注意到了 `Segment` 上有一个 `children` 属性。
@@ -291,7 +291,7 @@ msg.extend([Text("text")])
这里额外说明 `UniMessage.template` 的拓展控制符
相比 `Message`UniMessage 对于 {:XXX} 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行
相比 `Message`UniMessage 对于 `{:XXX}` 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行
以 At(...) 为例:
@@ -305,7 +305,7 @@ UniMessage(At("user", "123"))
UniMessage(At("user", "123"))
```
而在 `AlconnaMatcher` 中,{:XXX} 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能:
而在 `AlconnaMatcher` 中,`{:XXX}` 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能:
```python title=在AlconnaMatcher中使用通用消息段的拓展控制符
from arclet.alconna import Alconna, Args

View File

@@ -31,17 +31,17 @@ require("nonebot_plugin_localstore")
import nonebot_plugin_localstore as store
# 获取插件缓存目录
cache_dir = store.get_cache_dir("plugin_name")
cache_dir = store.get_plugin_cache_dir()
# 获取插件缓存文件
cache_file = store.get_cache_file("plugin_name", "file_name")
cache_file = store.get_plugin_cache_file("file_name")
# 获取插件数据目录
data_dir = store.get_data_dir("plugin_name")
data_dir = store.get_plugin_data_dir()
# 获取插件数据文件
data_file = store.get_data_file("plugin_name", "file_name")
data_file = store.get_plugin_data_file("file_name")
# 获取插件配置目录
config_dir = store.get_config_dir("plugin_name")
config_dir = store.get_plugin_config_dir()
# 获取插件配置文件
config_file = store.get_config_file("plugin_name", "file_name")
config_file = store.get_plugin_config_file("file_name")
```
:::danger 警告
@@ -53,9 +53,61 @@ config_file = store.get_config_file("plugin_name", "file_name")
```python
from pathlib import Path
data_file = store.get_data_file("plugin_name", "file_name")
data_file = store.get_plugin_data_file("file_name")
# 写入文件内容
data_file.write_text("Hello World!")
# 读取文件内容
data = data_file.read_text()
```
:::note 提示
对于嵌套插件,子插件的存储目录将位于父插件存储目录下。
:::
## 配置项
### localstore_cache_dir
自定义缓存目录
默认值:
- macOS: `~/Library/Caches/<AppName>`
- Unix: `~/.cache/<AppName>` (XDG default)
- Windows: `C:\Users\<username>\AppData\Local\<AppName>\Cache`
```dotenv
LOCALSTORE_CACHE_DIR=/tmp/cache
```
### localstore_data_dir
自定义数据目录
默认值:
- macOS: `~/Library/Application Support/<AppName>`
- Unix: `~/.local/share/<AppName>` or in $XDG_DATA_HOME, if defined
- Win XP (not roaming): `C:\Documents and Settings\<username>\Application Data\<AppName>`
- Win 7 (not roaming): `C:\Users\<username>\AppData\Local\<AppName>`
```dotenv
LOCALSTORE_DATA_DIR=/tmp/data
```
### localstore_config_dir
自定义配置目录
默认值:
- macOS: same as user_data_dir
- Unix: `~/.config/<AppName>`
- Win XP (roaming): `C:\Documents and Settings\<username>\Local Settings\Application Data\<AppName>`
- Win 7 (roaming): `C:\Users\<username>\AppData\Roaming\<AppName>`
```dotenv
LOCALSTORE_CONFIG_DIR=/tmp/config
```

View File

@@ -74,7 +74,17 @@ pip install pytest-asyncio
## 配置测试
在开始测试之前,我们需要对测试进行一些配置,以正确启动我们的机器人。在 `tests` 目录下新建 `conftest.py` 文件,添加以下内容:
在开始测试之前,我们需要对测试进行一些配置,以正确启动我们的机器人。
首先我们需要配置 pytest-asyncio在 `pyproject.toml` 的 pytest 配置部分添加:
```toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
```
然后,我们在 `tests` 目录下新建 `conftest.py` 文件,添加以下内容:
```python title=tests/conftest.py
import pytest
@@ -83,7 +93,7 @@ import nonebot
from nonebot.adapters.console import Adapter as ConsoleAdapter
@pytest.fixture(scope="session", autouse=True)
def load_bot():
async def after_nonebot_init(after_nonebot_init: None):
# 加载适配器
driver = nonebot.get_driver()
driver.register_adapter(ConsoleAdapter)
@@ -94,9 +104,10 @@ def load_bot():
这样,我们就可以在测试中使用机器人的插件了。通常,我们不需要自行初始化 NoneBotNoneBug 已经为我们运行了 `nonebot.init()`。如果需要自定义 NoneBot 初始化的参数,我们可以在 `conftest.py` 中添加 `pytest_configure` 钩子函数。例如,我们可以修改 NoneBot 配置环境为 `test` 并从环境变量中输入配置:
```python {3,5,7-9} title=tests/conftest.py
```python {4,6,8-10} title=tests/conftest.py
import os
import pytest
from nonebug import NONEBOT_INIT_KWARGS
os.environ["ENVIRONMENT"] = "test"
@@ -105,6 +116,16 @@ def pytest_configure(config: pytest.Config):
config.stash[NONEBOT_INIT_KWARGS] = {"secret": os.getenv("INPUT_SECRET")}
```
NoneBug 默认也会为我们管理 lifespan 的 startup 与 shutdown。如果不希望 NoneBug 管理 lifespan你可以在 `pytest_configure` 里添加以下配置:
```python
import pytest
from nonebug import NONEBOT_START_LIFESPAN
def pytest_configure(config: pytest.Config):
config.stash[NONEBOT_START_LIFESPAN] = False
```
## 编写插件测试
在配置完成插件加载后我们就可以在测试中使用插件了。NoneBug 通过 pytest fixture `app` 提供各种测试方法,我们可以在测试中使用它来测试插件。现在,我们创建一个测试脚本来测试[深入指南](../../appendices/session-control.mdx)中编写的天气插件。首先,我们先要导入我们需要的模块:

View File

@@ -1,13 +1,15 @@
---
sidebar_position: 0
description: 开源软件供应链点亮计划 - 暑期 2021
mdx:
format: md
---
# 暑期 2021
**开源软件供应链点亮计划 - 暑期 2021** 是**中国科学院软件研究所**与 **openEuler 社区**共同举办的一项面向高校学生的暑期活动,旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer.iscas.ac.cn/) 和 [帮助文档](https://summer.iscas.ac.cn/help/)。
NoneBot 社区有幸作为开源社区参与了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学在上面给出的活动官网报名,或通过 <contact@nonebot.dev> 联系我们。
NoneBot 社区有幸作为开源社区参与了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学在上面给出的活动官网报名,或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。
## NoneBot v1

View File

@@ -1,13 +1,15 @@
---
sidebar_position: 1
description: 开源之夏 - 暑期 2022
mdx:
format: md
---
# 暑期 2022
**开源之夏 - 暑期 2022** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of CodeGSoC旨在鼓励在校学生积极参与开源软件的开发维护促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。
NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/#/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a/) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学加入 QQ 群 [737131827](https://jq.qq.com/?_wv=1027&k=PEgyGeEu) 或通过 <contact@nonebot.dev> 联系我们。
NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/#/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a/) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学加入 QQ 群 [737131827](https://jq.qq.com/?_wv=1027&k=PEgyGeEu) 或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。
## NoneBot2 命令行 CLI 交互体验升级

View File

@@ -1,13 +1,15 @@
---
sidebar_position: 2
description: 开源之夏 - 暑期 2023
mdx:
format: md
---
# 暑期 2023
**开源之夏 - 暑期 2023** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of CodeGSoC旨在鼓励在校学生积极参与开源软件的开发维护促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。
NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 <contact@nonebot.dev> 联系我们。
NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。
## NoneBot 项目管理图形化面板

View File

@@ -1,13 +1,15 @@
---
sidebar_position: 3
description: 开源之夏 - 暑期 2024
mdx:
format: md
---
# 暑期 2024
**开源之夏 - 暑期 2024** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动,旨在鼓励高校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。活动联合各大开源社区,针对重要开源软件的开发与维护提供项目开发任务,并向全球高校学生开放报名。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。
NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 <contact@nonebot.dev> 联系我们。
NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。
## NonePress 官网组件库更新与优化

View File

@@ -107,4 +107,4 @@ if __name__ == "__main__":
python bot.py
```
如果你后续使用了 `nb-cli` ,你仍可以使用 `nb run` 命令来运行机器人,`nb-cli` 会自动检测入口文件 `bot.py` 是否存在并运行。
如果你后续使用了 `nb-cli` ,你仍可以使用 `nb run` 命令来运行机器人,`nb-cli` 会自动检测入口文件 `bot.py` 是否存在并运行。同时,你也可以使用 `nb run --reload` 来自动检测代码的更改并自动重新运行入口文件。

View File

@@ -51,8 +51,8 @@ from nonebot.rule import to_me
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
```
这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令,需要私聊或 `@bot` 时才会响应,优先级为 10 ,阻断事件传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。
这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。
:::tip 提示
需要注意的是,不同的辅助函数有不同的可选参数,在使用之前可以参考[事件响应器进阶](../advanced/matcher.md)或编辑器的提示
需要注意的是,不同的辅助函数有不同的可选参数,在使用之前可以参考[事件响应器进阶 - 基本辅助函数](../advanced/matcher.md#基本辅助函数)或 [API 文档](../api/plugin/on.md#on)。
:::

View File

@@ -1,317 +0,0 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
// color mode config
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["colorMode"]} */
const colorMode = {
defaultMode: "light",
respectPrefersColorScheme: true,
};
// navbar config
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["navbar"]} */
const navbar = {
title: "NoneBot",
logo: {
alt: "NoneBot",
src: "logo.png",
href: "/",
target: "_self",
height: 32,
width: 32,
},
hideOnScroll: false,
items: [
{
label: "指南",
type: "docsMenu",
category: "tutorial",
},
{
label: "深入",
type: "docsMenu",
category: "appendices",
},
{
label: "进阶",
type: "docsMenu",
category: "advanced",
},
{
label: "API",
type: "doc",
docId: "api/index",
},
{
label: "更多",
type: "dropdown",
to: "/store/plugins",
items: [
{
label: "最佳实践",
type: "doc",
docId: "best-practice/scheduler",
},
{
label: "开发者",
type: "doc",
docId: "developer/plugin-publishing",
},
{ label: "社区", type: "doc", docId: "community/contact" },
{ label: "开源之夏", type: "doc", docId: "ospp/2024" },
{ label: "商店", to: "/store/plugins" },
{ label: "更新日志", to: "/changelog" },
{ label: "论坛", href: "https://discussions.nonebot.dev" },
],
},
],
};
// footer config
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["footer"]} */
const footer = {
style: "light",
logo: {
alt: "NoneBot",
src: "logo.png",
href: "/",
target: "_self",
height: 32,
width: 32,
},
copyright: `Copyright © ${new Date().getFullYear()} NoneBot. All rights reserved.`,
links: [
{
title: "Learn",
items: [
{ label: "Introduction", to: "/docs/" },
{ label: "QuickStart", to: "/docs/quick-start" },
{ label: "Changelog", to: "/changelog" },
],
},
{
title: "NoneBot Team",
items: [
{
label: "Homepage",
href: "https://nonebot.dev",
},
{
label: "NoneBot V1",
href: "https://v1.nonebot.dev",
},
{ label: "NoneBot CLI", href: "https://cli.nonebot.dev" },
],
},
{
title: "Related",
items: [
{ label: "OneBot", href: "https://onebot.dev/" },
{ label: "go-cqhttp", href: "https://docs.go-cqhttp.org/" },
{ label: "Mirai", href: "https://mirai.mamoe.net/" },
],
},
],
};
// prism config
/** @type {import('prism-react-renderer').PrismTheme} */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line import/order
const lightCodeTheme = require("prism-react-renderer/themes/github");
/** @type {import('prism-react-renderer').PrismTheme} */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line import/order
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["prism"]} */
const prism = {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
additionalLanguages: ["docker", "ini"],
};
// algolia config
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["algolia"]} */
const algolia = {
appId: "X0X5UACHZQ",
apiKey: "ac03e1ac2bd0812e2ea38c0cc1ea38c5",
indexName: "nonebot",
contextualSearch: true,
};
// nonepress config
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["nonepress"]} */
const nonepress = {
tailwindConfig: require("./tailwind.config"),
navbar: {
docsVersionDropdown: {
dropdownItemsAfter: [
{
label: "1.x",
href: "https://v1.nonebot.dev/",
},
],
},
socialLinks: [
{
icon: ["fab", "github"],
href: "https://github.com/nonebot/nonebot2",
},
],
},
footer: {
socialLinks: [
{
icon: ["fab", "github"],
href: "https://github.com/nonebot/nonebot2",
},
{
icon: ["fab", "qq"],
href: "https://jq.qq.com/?_wv=1027&k=5OFifDh",
},
{
icon: ["fab", "telegram"],
href: "https://t.me/botuniverse",
},
{
icon: ["fab", "discord"],
href: "https://discord.gg/VKtE6Gdc4h",
},
],
},
};
// theme config
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig} */
const themeConfig = {
colorMode,
navbar,
footer,
prism,
algolia,
nonepress,
};
/** @type {import('@docusaurus/types').Config} */
const siteConfig = {
title: "NoneBot",
tagline: "跨平台 Python 异步机器人框架",
favicon: "icons/favicon.ico",
// Set the production url of your site here
url: "https://nonebot.dev",
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: process.env.BASE_URL || "/",
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: "nonebot", // Usually your GitHub org/user name.
projectName: "nonebot2", // Usually your repo name.
onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn",
// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
i18n: {
defaultLocale: "zh-Hans",
locales: ["zh-Hans"],
},
headTags: [
// 百度搜索资源平台
// https://ziyuan.baidu.com/
{
tagName: "meta",
attributes: {
name: "baidu-site-verification",
content: "codeva-0GTZpDnDrW",
},
},
],
scripts: [
// 百度统计
// https://tongji.baidu.com/
{
type: "text/javascript",
charset: "UTF-8",
src: "https://hm.baidu.com/hm.js?875efa50097818701ee681edd63eaac6",
async: true,
},
// 万维广告
// https://wwads.cn/
{
type: "text/javascript",
charset: "UTF-8",
src: "https://cdn.wwads.cn/js/makemoney.js",
async: true,
},
// uwu logo
{
type: "text/javascript",
charset: "UTF-8",
src: "/uwu.js",
},
],
presets: [
[
"@nullbot/docusaurus-preset-nonepress",
/** @type {import('@nullbot/docusaurus-preset-nonepress').Options} */
({
docs: {
sidebarPath: require.resolve("./sidebars.js"),
// Please change this to your repo.
editUrl: "https://github.com/nonebot/nonebot2/edit/master/website/",
showLastUpdateAuthor: true,
showLastUpdateTime: true,
// exclude: [
// "**/_*.{js,jsx,ts,tsx,md,mdx}",
// "**/_*/**",
// "**/*.test.{js,jsx,ts,tsx}",
// "**/__tests__/**",
// ],
// async sidebarItemsGenerator({
// isCategoryIndex: defaultCategoryIndexMatcher,
// defaultSidebarItemsGenerator,
// ...args
// }) {
// return defaultSidebarItemsGenerator({
// ...args,
// isCategoryIndex(doc) {
// // disable category index convention for generated API docs
// if (
// doc.directories.length > 0 &&
// doc.directories.at(-1) === "api"
// ) {
// return false;
// }
// return defaultCategoryIndexMatcher(doc);
// },
// });
// },
},
// theme: {
// customCss: require.resolve("./src/css/custom.css"),
// },
sitemap: {
changefreq: "daily",
priority: 0.5,
},
gtag: {
trackingID: "G-MRS1GMZG0F",
},
}),
],
],
plugins: [require("./src/plugins/webpack-plugin.cjs")],
themeConfig,
};
module.exports = siteConfig;

View File

@@ -0,0 +1,353 @@
import type { Config } from "@docusaurus/types";
import type { Options as ChangelogOptions } from "@nullbot/docusaurus-plugin-changelog";
import type * as Preset from "@nullbot/docusaurus-preset-nonepress";
import { themes } from "prism-react-renderer";
// By default, we use Docusaurus Faster
// DOCUSAURUS_SLOWER=true is useful for benchmarking faster against slower
// hyperfine --prepare 'yarn clear:website' --runs 3 'DOCUSAURUS_SLOWER=true yarn build:website:fast' 'yarn build:website:fast'
const isSlower = process.env.DOCUSAURUS_SLOWER === "true";
if (isSlower) {
console.log("🐢 Using slower Docusaurus build");
}
// color mode config
const colorMode: Preset.ThemeConfig["colorMode"] = {
defaultMode: "light",
respectPrefersColorScheme: true,
};
// navbar config
const navbar: Preset.ThemeConfig["navbar"] = {
title: "NoneBot",
logo: {
alt: "NoneBot",
src: "logo.png",
href: "/",
target: "_self",
height: 32,
width: 32,
},
hideOnScroll: false,
items: [
{
label: "指南",
type: "docsMenu",
category: "tutorial",
},
{
label: "深入",
type: "docsMenu",
category: "appendices",
},
{
label: "进阶",
type: "docsMenu",
category: "advanced",
},
{
label: "API",
type: "doc",
docId: "api/index",
},
{
label: "更多",
type: "dropdown",
to: "/store/plugins",
items: [
{
label: "最佳实践",
type: "doc",
docId: "best-practice/scheduler",
},
{
label: "开发者",
type: "doc",
docId: "developer/plugin-publishing",
},
{ label: "社区", type: "doc", docId: "community/contact" },
{ label: "开源之夏", type: "doc", docId: "ospp/2024" },
{ label: "商店", to: "/store/plugins" },
{ label: "更新日志", to: "/changelog" },
{ label: "论坛", href: "https://discussions.nonebot.dev" },
],
},
],
};
// footer config
const footer: Preset.ThemeConfig["footer"] = {
style: "light",
logo: {
alt: "NoneBot",
src: "logo.png",
href: "/",
target: "_self",
height: 32,
width: 32,
},
copyright: `Copyright © ${new Date().getFullYear()} NoneBot. All rights reserved.`,
links: [
{
title: "Learn",
items: [
{ label: "Introduction", to: "/docs/" },
{ label: "QuickStart", to: "/docs/quick-start" },
{ label: "Changelog", to: "/changelog" },
],
},
{
title: "NoneBot Team",
items: [
{
label: "Homepage",
href: "https://nonebot.dev",
},
{
label: "NoneBot V1",
href: "https://v1.nonebot.dev",
},
{ label: "NoneBot CLI", href: "https://cli.nonebot.dev" },
],
},
{
title: "Related",
items: [
{ label: "OneBot", href: "https://onebot.dev/" },
{ label: "go-cqhttp", href: "https://docs.go-cqhttp.org/" },
{ label: "Mirai", href: "https://mirai.mamoe.net/" },
],
},
],
};
// prism config
const lightCodeTheme = themes.github;
const darkCodeTheme = themes.dracula;
const prism: Preset.ThemeConfig["prism"] = {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
additionalLanguages: ["docker", "ini"],
};
// algolia config
const algolia: Preset.ThemeConfig["algolia"] = {
appId: "X0X5UACHZQ",
apiKey: "ac03e1ac2bd0812e2ea38c0cc1ea38c5",
indexName: "nonebot",
contextualSearch: true,
};
// nonepress config
const nonepress: Preset.ThemeConfig["nonepress"] = {
tailwindConfig: require("./tailwind.config"),
navbar: {
docsVersionDropdown: {
dropdownItemsAfter: [
{
label: "1.x",
href: "https://v1.nonebot.dev/",
},
],
},
socialLinks: [
{
icon: ["fab", "github"],
href: "https://github.com/nonebot/nonebot2",
},
],
},
footer: {
socialLinks: [
{
icon: ["fab", "github"],
href: "https://github.com/nonebot/nonebot2",
},
{
icon: ["fab", "qq"],
href: "https://jq.qq.com/?_wv=1027&k=5OFifDh",
},
{
icon: ["fab", "telegram"],
href: "https://t.me/botuniverse",
},
{
icon: ["fab", "discord"],
href: "https://discord.gg/VKtE6Gdc4h",
},
],
},
};
// theme config
const themeConfig: Preset.ThemeConfig = {
colorMode,
navbar,
footer,
prism,
algolia,
nonepress,
};
export default async function createConfigAsync() {
return {
title: "NoneBot",
tagline: "跨平台 Python 异步机器人框架",
favicon: "icons/favicon.ico",
// Set the production url of your site here
url: "https://nonebot.dev",
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: process.env.BASE_URL || "/",
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: "nonebot", // Usually your GitHub org/user name.
projectName: "nonebot2", // Usually your repo name.
onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn",
// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
i18n: {
defaultLocale: "zh-Hans",
locales: ["zh-Hans"],
},
headTags: [
// 百度搜索资源平台
// https://ziyuan.baidu.com/
{
tagName: "meta",
attributes: {
name: "baidu-site-verification",
content: "codeva-0GTZpDnDrW",
},
},
],
scripts: [
// 百度统计
// https://tongji.baidu.com/
{
type: "text/javascript",
charset: "UTF-8",
src: "https://hm.baidu.com/hm.js?875efa50097818701ee681edd63eaac6",
async: true,
},
// 万维广告
// https://wwads.cn/
{
type: "text/javascript",
charset: "UTF-8",
src: "https://cdn.wwads.cn/js/makemoney.js",
async: true,
},
// uwu logo
{
type: "text/javascript",
charset: "UTF-8",
src: "/uwu.js",
},
],
presets: [
[
"@nullbot/docusaurus-preset-nonepress",
/** @type {import('@nullbot/docusaurus-preset-nonepress').Options} */
{
docs: {
sidebarPath: require.resolve("./sidebars.js"),
// Please change this to your repo.
editUrl: "https://github.com/nonebot/nonebot2/edit/master/website/",
showLastUpdateAuthor: true,
showLastUpdateTime: true,
// exclude: [
// "**/_*.{js,jsx,ts,tsx,md,mdx}",
// "**/_*/**",
// "**/*.test.{js,jsx,ts,tsx}",
// "**/__tests__/**",
// ],
// async sidebarItemsGenerator({
// isCategoryIndex: defaultCategoryIndexMatcher,
// defaultSidebarItemsGenerator,
// ...args
// }) {
// return defaultSidebarItemsGenerator({
// ...args,
// isCategoryIndex(doc) {
// // disable category index convention for generated API docs
// if (
// doc.directories.length > 0 &&
// doc.directories.at(-1) === "api"
// ) {
// return false;
// }
// return defaultCategoryIndexMatcher(doc);
// },
// });
// },
},
// theme: {
// customCss: require.resolve("./src/css/custom.css"),
// },
sitemap: {
changefreq: "daily",
priority: 0.5,
},
gtag: {
trackingID: "G-MRS1GMZG0F",
},
},
],
],
webpack: {
jsLoader: (isServer) => ({
loader: require.resolve("swc-loader"),
options: {
jsc: {
parser: {
syntax: "typescript",
tsx: true,
},
transform: {
react: {
runtime: "automatic",
},
},
target: "es2017",
},
module: {
type: isServer ? "commonjs" : "es6",
},
},
}),
},
plugins: [
require("./src/plugins/webpack-plugin.ts"),
[
"@nullbot/docusaurus-plugin-changelog",
{
changelogPath: "src/changelog/changelog.md",
changelogHeader: `description: Changelog
toc_max_heading_level: 2
sidebar_custom_props:
sidebar_id: changelog`,
} satisfies ChangelogOptions,
],
],
markdown: {
mdx1Compat: {
headingIds: true,
},
},
themeConfig,
} satisfies Config;
}

View File

@@ -22,24 +22,27 @@
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "^2.4.1",
"@mdx-js/react": "^1.6.22",
"@nullbot/docusaurus-preset-nonepress": "^2.1.2",
"clsx": "^1.2.1",
"copy-text-to-clipboard": "^3.0.1",
"prism-react-renderer": "^1.3.5",
"@docusaurus/core": "^3.5.2",
"@mdx-js/react": "^3.0.0",
"@nullbot/docusaurus-plugin-changelog": "^3.0.0",
"@nullbot/docusaurus-preset-nonepress": "^3.0.0",
"@swc/core": "^1.7.26",
"clsx": "^2.0.0",
"copy-text-to-clipboard": "^3.2.0",
"prism-react-renderer": "^2.3.0",
"raw-loader": "^4.0.2",
"react": "^17.0.1",
"react": "^18.0.0",
"react-color": "^2.19.3",
"react-dom": "^17.0.1",
"react-use-pagination": "^2.0.1"
"react-dom": "^18.0.0",
"react-use-pagination": "^2.0.1",
"swc-loader": "^0.2.6"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^2.4.1",
"@tsconfig/docusaurus": "^1.0.5",
"@docusaurus/module-type-aliases": "^3.5.2",
"@nullbot/docusaurus-tsconfig": "^3.0.0",
"@types/react-color": "^3.0.10",
"asciinema-player": "^3.5.0",
"typescript": "^4.7.4"
"typescript": "~5.5.2"
},
"browserslist": {
"production": [
@@ -54,6 +57,6 @@
]
},
"engines": {
"node": ">=16.14"
"node": ">=18.0"
}
}

View File

@@ -8,11 +8,15 @@
Create as many sidebars as you want.
*/
import path from "path";
// @ts-check
import type { SidebarsConfig } from "@docusaurus/plugin-content-docs";
import { getChangelogItemsSync } from "@nullbot/docusaurus-plugin-changelog";
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
const changelogPath = path.join(__dirname, "src/changelog/changelog.md");
const changelogItems = getChangelogItemsSync(changelogPath, 10);
const sidebars: SidebarsConfig = {
tutorial: [
{
type: "category",
@@ -133,6 +137,22 @@ const sidebars = {
],
},
],
changelog: [
{
type: "category",
label: "更新日志",
collapsible: false,
items: changelogItems.map<{ type: "link"; label: string; href: string }>(
(chunk, index) => ({
type: "link",
label: chunk[0]!.title,
href: `/changelog/${
index > 0 ? encodeURIComponent(chunk[0]!.title) : ""
}`,
})
),
},
],
};
module.exports = sidebars;
export default sidebars;

View File

@@ -5,6 +5,170 @@ toc_max_heading_level: 2
# 更新日志
## v2.4.0
### 🚀 新功能
- Feature: 跳过部分非必要的 task group 创建 [@yanyongyu](https://github.com/yanyongyu) ([#3095](https://github.com/nonebot/nonebot2/pull/3095))
- Feature: 迁移至结构化并发框架 AnyIO [@yanyongyu](https://github.com/yanyongyu) ([#3053](https://github.com/nonebot/nonebot2/pull/3053))
- Feature: 添加 websockets 驱动器 proxy 连接警告 [@shoucandanghehe](https://github.com/shoucandanghehe) ([#2916](https://github.com/nonebot/nonebot2/pull/2916))
### 🐛 Bug 修复
- Fix: 修复结构化并发子依赖取消缓存问题 [@yanyongyu](https://github.com/yanyongyu) ([#3084](https://github.com/nonebot/nonebot2/pull/3084))
### 📝 文档
- Docs: 新增 nonebug 新版启动需要的配置 [@yanyongyu](https://github.com/yanyongyu) ([#3087](https://github.com/nonebot/nonebot2/pull/3087))
- Docs: 修复侧边栏滚动 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3062](https://github.com/nonebot/nonebot2/pull/3062))
- Docs: 升级到 Docusaurus V3 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2956](https://github.com/nonebot/nonebot2/pull/2956))
- Docs: 修改文档示例代码与部分表述 [@yixinNB](https://github.com/yixinNB) ([#2797](https://github.com/nonebot/nonebot2/pull/2797))
- Docs: 添加钩子函数 IgnoredException 用法 [@refparo](https://github.com/refparo) ([#2912](https://github.com/nonebot/nonebot2/pull/2912))
### 💫 杂项
- Plugin: 移除不再维护的插件 [@ssttkkl](https://github.com/ssttkkl) ([#3040](https://github.com/nonebot/nonebot2/pull/3040))
- Plugin: 删除不再维护的 simplemusic hikarisearch 插件 [@MeetWq](https://github.com/MeetWq) ([#2933](https://github.com/nonebot/nonebot2/pull/2933))
- Plugin: 删除插件 `nonebot-plugin-ntqq-restart` [@kanbereina](https://github.com/kanbereina) ([#2926](https://github.com/nonebot/nonebot2/pull/2926))
- Adapter: 移除社区版 mirai 适配器 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2909](https://github.com/nonebot/nonebot2/pull/2909))
### 🍻 插件发布
- Plugin: Comfyui绘图插件 [@noneflow](https://github.com/noneflow) ([#3081](https://github.com/nonebot/nonebot2/pull/3081))
- Plugin: 每日wife [@noneflow](https://github.com/noneflow) ([#3094](https://github.com/nonebot/nonebot2/pull/3094))
- Plugin: nonebot_plugin_impart [@noneflow](https://github.com/noneflow) ([#3079](https://github.com/nonebot/nonebot2/pull/3079))
- Plugin: Pix图库 [@noneflow](https://github.com/noneflow) ([#3083](https://github.com/nonebot/nonebot2/pull/3083))
- Plugin: nonebot_plugin_partner_join [@noneflow](https://github.com/noneflow) ([#3051](https://github.com/nonebot/nonebot2/pull/3051))
- Plugin: pong [@noneflow](https://github.com/noneflow) ([#3066](https://github.com/nonebot/nonebot2/pull/3066))
- Plugin: Bot的消息也是消息 [@noneflow](https://github.com/noneflow) ([#3064](https://github.com/nonebot/nonebot2/pull/3064))
- Plugin: BiliMusic Downloader [@noneflow](https://github.com/noneflow) ([#3046](https://github.com/nonebot/nonebot2/pull/3046))
- Plugin: 防撤回 [@noneflow](https://github.com/noneflow) ([#3055](https://github.com/nonebot/nonebot2/pull/3055))
- Plugin: nonebot_plugin_mai_arcade [@noneflow](https://github.com/noneflow) ([#3047](https://github.com/nonebot/nonebot2/pull/3047))
- Plugin: DDNet 成绩查询 [@noneflow](https://github.com/noneflow) ([#3031](https://github.com/nonebot/nonebot2/pull/3031))
- Plugin: 省流 [@noneflow](https://github.com/noneflow) ([#3052](https://github.com/nonebot/nonebot2/pull/3052))
- Plugin: FishSpeechTTS [@noneflow](https://github.com/noneflow) ([#3050](https://github.com/nonebot/nonebot2/pull/3050))
- Plugin: 语音点歌 [@noneflow](https://github.com/noneflow) ([#3037](https://github.com/nonebot/nonebot2/pull/3037))
- Plugin: Gotify [@noneflow](https://github.com/noneflow) ([#3043](https://github.com/nonebot/nonebot2/pull/3043))
- Plugin: 涩图插件 [@noneflow](https://github.com/noneflow) ([#3039](https://github.com/nonebot/nonebot2/pull/3039))
- Plugin: boom [@noneflow](https://github.com/noneflow) ([#3017](https://github.com/nonebot/nonebot2/pull/3017))
- Plugin: 恶魔轮盘赌 [@noneflow](https://github.com/noneflow) ([#3033](https://github.com/nonebot/nonebot2/pull/3033))
- Plugin: 机厅 [@noneflow](https://github.com/noneflow) ([#3029](https://github.com/nonebot/nonebot2/pull/3029))
- Plugin: PM帮助 [@noneflow](https://github.com/noneflow) ([#3023](https://github.com/nonebot/nonebot2/pull/3023))
- Plugin: NailongRemove [@noneflow](https://github.com/noneflow) ([#2972](https://github.com/nonebot/nonebot2/pull/2972))
- Plugin: 团购 [@noneflow](https://github.com/noneflow) ([#3027](https://github.com/nonebot/nonebot2/pull/3027))
- Plugin: 真寻日报 [@noneflow](https://github.com/noneflow) ([#3021](https://github.com/nonebot/nonebot2/pull/3021))
- Plugin: 运行状态 [@noneflow](https://github.com/noneflow) ([#3019](https://github.com/nonebot/nonebot2/pull/3019))
- Plugin: 西工大翱翔门户成绩监控 [@noneflow](https://github.com/noneflow) ([#3013](https://github.com/nonebot/nonebot2/pull/3013))
- Plugin: nb插件更新器 [@noneflow](https://github.com/noneflow) ([#3015](https://github.com/nonebot/nonebot2/pull/3015))
- Plugin: 涩涩保存器 [@noneflow](https://github.com/noneflow) ([#2988](https://github.com/nonebot/nonebot2/pull/2988))
- Plugin: nonebot_plugin_BFVsearch [@noneflow](https://github.com/noneflow) ([#3008](https://github.com/nonebot/nonebot2/pull/3008))
- Plugin: lingyi_chat [@noneflow](https://github.com/noneflow) ([#3006](https://github.com/nonebot/nonebot2/pull/3006))
- Plugin: ZXPM插件管理 [@noneflow](https://github.com/noneflow) ([#3003](https://github.com/nonebot/nonebot2/pull/3003))
- Plugin: MinecraftWatcher [@noneflow](https://github.com/noneflow) ([#3010](https://github.com/nonebot/nonebot2/pull/3010))
- Plugin: BF5_grouptools [@noneflow](https://github.com/noneflow) ([#3004](https://github.com/nonebot/nonebot2/pull/3004))
- Plugin: lolinfo [@noneflow](https://github.com/noneflow) ([#2997](https://github.com/nonebot/nonebot2/pull/2997))
- Plugin: osu! Match Monitor [@noneflow](https://github.com/noneflow) ([#2985](https://github.com/nonebot/nonebot2/pull/2985))
- Plugin: Marsho AI插件 [@noneflow](https://github.com/noneflow) ([#2993](https://github.com/nonebot/nonebot2/pull/2993))
- Plugin: nonechat [@noneflow](https://github.com/noneflow) ([#2990](https://github.com/nonebot/nonebot2/pull/2990))
- Plugin: nonebot_plugin_SimpleToWrite [@noneflow](https://github.com/noneflow) ([#2995](https://github.com/nonebot/nonebot2/pull/2995))
- Plugin: Beat Saber查分器 [@noneflow](https://github.com/noneflow) ([#2974](https://github.com/nonebot/nonebot2/pull/2974))
- Plugin: githubmodels [@noneflow](https://github.com/noneflow) ([#2945](https://github.com/nonebot/nonebot2/pull/2945))
- Plugin: 给我点颜色瞧瞧 [@noneflow](https://github.com/noneflow) ([#2984](https://github.com/nonebot/nonebot2/pull/2984))
- Plugin: pjsk-helper [@noneflow](https://github.com/noneflow) ([#2980](https://github.com/nonebot/nonebot2/pull/2980))
- Plugin: 趣味内容插件 [@noneflow](https://github.com/noneflow) ([#2981](https://github.com/nonebot/nonebot2/pull/2981))
- Plugin: 计算器:游戏 [@noneflow](https://github.com/noneflow) ([#2976](https://github.com/nonebot/nonebot2/pull/2976))
- Plugin: nonebot-plugin-yareminder [@noneflow](https://github.com/noneflow) ([#2964](https://github.com/nonebot/nonebot2/pull/2964))
- Plugin: 批量撤回 [@noneflow](https://github.com/noneflow) ([#2966](https://github.com/nonebot/nonebot2/pull/2966))
- Plugin: inspect [@noneflow](https://github.com/noneflow) ([#2971](https://github.com/nonebot/nonebot2/pull/2971))
- Plugin: 通用信息 [@noneflow](https://github.com/noneflow) ([#2969](https://github.com/nonebot/nonebot2/pull/2969))
- Plugin: SSE日志输出流 [@noneflow](https://github.com/noneflow) ([#2960](https://github.com/nonebot/nonebot2/pull/2960))
- Plugin: WITFF [@noneflow](https://github.com/noneflow) ([#2955](https://github.com/nonebot/nonebot2/pull/2955))
- Plugin: weather-rank [@noneflow](https://github.com/noneflow) ([#2949](https://github.com/nonebot/nonebot2/pull/2949))
- Plugin: 二维码生成器 [@noneflow](https://github.com/noneflow) ([#2942](https://github.com/nonebot/nonebot2/pull/2942))
- Plugin: 次元星辰 [@noneflow](https://github.com/noneflow) ([#2935](https://github.com/nonebot/nonebot2/pull/2935))
- Plugin: nonebot-plugin-tarina-lang-turbo [@noneflow](https://github.com/noneflow) ([#2938](https://github.com/nonebot/nonebot2/pull/2938))
- Plugin: 狼人杀 [@noneflow](https://github.com/noneflow) ([#2932](https://github.com/nonebot/nonebot2/pull/2932))
- Plugin: 阿瓦隆 [@noneflow](https://github.com/noneflow) ([#2915](https://github.com/nonebot/nonebot2/pull/2915))
- Plugin: 消音器 [@noneflow](https://github.com/noneflow) ([#2919](https://github.com/nonebot/nonebot2/pull/2919))
- Plugin: 悠悠 [@noneflow](https://github.com/noneflow) ([#2928](https://github.com/nonebot/nonebot2/pull/2928))
- Plugin: LLOneBot-Master [@noneflow](https://github.com/noneflow) ([#2925](https://github.com/nonebot/nonebot2/pull/2925))
- Plugin: 无情的发图姬 [@noneflow](https://github.com/noneflow) ([#2923](https://github.com/nonebot/nonebot2/pull/2923))
- Plugin: maimai DX 查分 [@noneflow](https://github.com/noneflow) ([#2921](https://github.com/nonebot/nonebot2/pull/2921))
- Plugin: Minecraft查服 [@noneflow](https://github.com/noneflow) ([#2882](https://github.com/nonebot/nonebot2/pull/2882))
- Plugin: lagrange [@noneflow](https://github.com/noneflow) ([#2898](https://github.com/nonebot/nonebot2/pull/2898))
- Plugin: nekro-agent [@noneflow](https://github.com/noneflow) ([#2896](https://github.com/nonebot/nonebot2/pull/2896))
- Plugin: nonebot_plugin_mute [@noneflow](https://github.com/noneflow) ([#2893](https://github.com/nonebot/nonebot2/pull/2893))
- Plugin: LiteyukiBot(plugin) [@noneflow](https://github.com/noneflow) ([#2905](https://github.com/nonebot/nonebot2/pull/2905))
- Plugin: 复读6 [@noneflow](https://github.com/noneflow) ([#2900](https://github.com/nonebot/nonebot2/pull/2900))
### 🍻 机器人发布
- Bot: CanrotBot [@noneflow](https://github.com/noneflow) ([#3086](https://github.com/nonebot/nonebot2/pull/3086))
- Bot: 小安提Bot [@noneflow](https://github.com/noneflow) ([#3061](https://github.com/nonebot/nonebot2/pull/3061))
## v2.3.3
### 🚀 新功能
- Feature: 优化依赖注入在 pydantic v2 下的性能 [@yanyongyu](https://github.com/yanyongyu) ([#2870](https://github.com/nonebot/nonebot2/pull/2870))
- Feature: 添加遗漏的类型标注 [@yanyongyu](https://github.com/yanyongyu) ([#2856](https://github.com/nonebot/nonebot2/pull/2856))
### 🐛 Bug 修复
- Fix: 错误的类型标注和 annotated 处理 [@yanyongyu](https://github.com/yanyongyu) ([#2828](https://github.com/nonebot/nonebot2/pull/2828))
### 📝 文档
- Docs: 添加 Windows Powershell 设置环境变量方法 [@LeoQuote](https://github.com/LeoQuote) ([#2874](https://github.com/nonebot/nonebot2/pull/2874))
- Docs: 更新 localstore 插件文档 [@yanyongyu](https://github.com/yanyongyu) ([#2871](https://github.com/nonebot/nonebot2/pull/2871))
### 💫 杂项
- Plugin: 修改插件 system-command 信息 [@tkgs0](https://github.com/tkgs0) ([#2862](https://github.com/nonebot/nonebot2/pull/2862))
- Plugin: 修改 nonebot-plugin-fishing 插件作者 [@ALittleBot](https://github.com/ALittleBot) ([#2854](https://github.com/nonebot/nonebot2/pull/2854))
- Bot: 更新 Minecraft QQBot 信息 [@Lonely-Sails](https://github.com/Lonely-Sails) ([#2838](https://github.com/nonebot/nonebot2/pull/2838))
- Plugin: 移除 kanonbot 插件 [@SuperGuGuGu](https://github.com/SuperGuGuGu) ([#2819](https://github.com/nonebot/nonebot2/pull/2819))
- Plugin: 更新插件 sparkapi 信息 [@CCLMSY](https://github.com/CCLMSY) ([#2812](https://github.com/nonebot/nonebot2/pull/2812))
- Plugin: 修改插件 miragetank \& charpic 信息 [@1umine](https://github.com/1umine) ([#2807](https://github.com/nonebot/nonebot2/pull/2807))
### 🍻 插件发布
- Plugin: nonebot-plugin-wait-a-minute [@noneflow](https://github.com/noneflow) ([#2902](https://github.com/nonebot/nonebot2/pull/2902))
- Plugin: 你看我像 [@noneflow](https://github.com/noneflow) ([#2895](https://github.com/nonebot/nonebot2/pull/2895))
- Plugin: dify插件 [@noneflow](https://github.com/noneflow) ([#2889](https://github.com/nonebot/nonebot2/pull/2889))
- Plugin: mai2_pcount [@noneflow](https://github.com/noneflow) ([#2891](https://github.com/nonebot/nonebot2/pull/2891))
- Plugin: nonebot-plugin-ehentai-search [@noneflow](https://github.com/noneflow) ([#2885](https://github.com/nonebot/nonebot2/pull/2885))
- Plugin: pokepoke_miss [@noneflow](https://github.com/noneflow) ([#2883](https://github.com/nonebot/nonebot2/pull/2883))
- Plugin: 聊天截图伪造 [@noneflow](https://github.com/noneflow) ([#2880](https://github.com/nonebot/nonebot2/pull/2880))
- Plugin: ba-tools [@noneflow](https://github.com/noneflow) ([#2867](https://github.com/nonebot/nonebot2/pull/2867))
- Plugin: 精华消息管理 [@noneflow](https://github.com/noneflow) ([#2873](https://github.com/nonebot/nonebot2/pull/2873))
- Plugin: B站收藏夹监视器 [@noneflow](https://github.com/noneflow) ([#2869](https://github.com/nonebot/nonebot2/pull/2869))
- Plugin: Alist [@noneflow](https://github.com/noneflow) ([#2865](https://github.com/nonebot/nonebot2/pull/2865))
- Plugin: 🦌管签到 [@noneflow](https://github.com/noneflow) ([#2859](https://github.com/nonebot/nonebot2/pull/2859))
- Plugin: 漂流瓶 [@noneflow](https://github.com/noneflow) ([#2861](https://github.com/nonebot/nonebot2/pull/2861))
- Plugin: 奇怪的小功能 [@noneflow](https://github.com/noneflow) ([#2851](https://github.com/nonebot/nonebot2/pull/2851))
- Plugin: SunoAI音乐生成 [@noneflow](https://github.com/noneflow) ([#2853](https://github.com/nonebot/nonebot2/pull/2853))
- Plugin: 谁是卷王 [@noneflow](https://github.com/noneflow) ([#2849](https://github.com/nonebot/nonebot2/pull/2849))
- Plugin: GPT-SoVITS 语音合成 [@noneflow](https://github.com/noneflow) ([#2847](https://github.com/nonebot/nonebot2/pull/2847))
- Plugin: 基于清影的AI视频生成 [@noneflow](https://github.com/noneflow) ([#2843](https://github.com/nonebot/nonebot2/pull/2843))
- Plugin: 命令行 [@noneflow](https://github.com/noneflow) ([#2840](https://github.com/nonebot/nonebot2/pull/2840))
- Plugin: exe_code [@noneflow](https://github.com/noneflow) ([#2835](https://github.com/nonebot/nonebot2/pull/2835))
- Plugin: nonebot-plugin-autopush [@noneflow](https://github.com/noneflow) ([#2833](https://github.com/nonebot/nonebot2/pull/2833))
- Plugin: vv_helper [@noneflow](https://github.com/noneflow) ([#2825](https://github.com/nonebot/nonebot2/pull/2825))
- Plugin: nonebot_plugin_game_torrent [@noneflow](https://github.com/noneflow) ([#2827](https://github.com/nonebot/nonebot2/pull/2827))
- Plugin: 每日油价 [@noneflow](https://github.com/noneflow) ([#2822](https://github.com/nonebot/nonebot2/pull/2822))
- Plugin: wordle [@noneflow](https://github.com/noneflow) ([#2818](https://github.com/nonebot/nonebot2/pull/2818))
- Plugin: 再润 [@noneflow](https://github.com/noneflow) ([#2816](https://github.com/nonebot/nonebot2/pull/2816))
- Plugin: 漫展/展览查询 [@noneflow](https://github.com/noneflow) ([#2811](https://github.com/nonebot/nonebot2/pull/2811))
- Plugin: 鸣潮wiki [@noneflow](https://github.com/noneflow) ([#2804](https://github.com/nonebot/nonebot2/pull/2804))
- Plugin: cloudfare R2 客服端 [@noneflow](https://github.com/noneflow) ([#2806](https://github.com/nonebot/nonebot2/pull/2806))
- Plugin: AnyMate小助手 [@noneflow](https://github.com/noneflow) ([#2761](https://github.com/nonebot/nonebot2/pull/2761))
### 🍻 机器人发布
- Bot: Minecraft_QQBot [@noneflow](https://github.com/noneflow) ([#2837](https://github.com/nonebot/nonebot2/pull/2837))
- Bot: 星辰 Bot [@noneflow](https://github.com/noneflow) ([#2824](https://github.com/nonebot/nonebot2/pull/2824))
## v2.3.2
### 🐛 Bug 修复

View File

@@ -1,7 +1,7 @@
import React from "react";
import { useDocsVersionCandidates } from "@docusaurus/plugin-content-docs/client";
import { PageMetadata } from "@docusaurus/theme-common";
import { useDocsVersionCandidates } from "@docusaurus/theme-common/internal";
import { useVersionedSidebar } from "@nullbot/docusaurus-plugin-getsidebar/client";
import { SidebarContentFiller } from "@nullbot/docusaurus-theme-nonepress/contexts";
@@ -25,7 +25,7 @@ function StorePage({ title, children }: Props): JSX.Element {
)!;
return (
<Page hideTableOfContents reduceContentWidth={false}>
<Page hideTableOfContents reduceContentWidth={false} sidebarId={SIDEBAR_ID}>
<SidebarContentFiller items={sidebarItems} />
<article className="prose max-w-full">
<h1 className="store-title">{title}</h1>

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