Compare commits

...

719 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
noneflow[bot]
4d070f5b48 🔖 Release 2.3.2 2024-07-06 12:34:00 +00:00
Ju4tCode
82138454bc 🔖 bump version 2.3.2 (#2799) 2024-07-06 20:24:00 +08:00
noneflow[bot]
d98fe53d56 📝 Update changelog 2024-07-05 12:49:09 +00:00
Komorebi
278b9e92c2 📝 Docs: 修改导航栏开源之夏链接 (#2798) 2024-07-05 20:48:06 +08:00
noneflow[bot]
45418ccfae 📝 Update changelog 2024-07-04 15:06:15 +00:00
fallllllllsleep
2ad2922565 📝 Docs: on_keyword 参数类型错误 (#2795)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2024-07-04 23:04:58 +08:00
pre-commit-ci[bot]
84ebcb4ce6 ⬆️ auto update by pre-commit hooks (#2794)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-02 21:50:54 +08:00
noneflow[bot]
6a0caacfd6 📝 Update changelog 2024-07-02 13:30:22 +00:00
tianyisama
a8f3940cbc 🍻 publish plugin 指令更新NapCat (#2790) 2024-07-02 13:29:12 +00:00
noneflow[bot]
15d3910462 📝 Update changelog 2024-07-01 13:18:15 +00:00
Autuamn
edfd0eb887 🍻 publish plugin QQ群-Discord 互通 (#2787) 2024-07-01 13:17:10 +00:00
noneflow[bot]
fe63717848 📝 Update changelog 2024-06-28 12:30:26 +00:00
Dongyanmio
63424bc3ac 🍻 publish plugin nonebot_plugin_obastatus (#2779) 2024-06-28 12:29:23 +00:00
noneflow[bot]
99b1d0ed96 📝 Update changelog 2024-06-25 15:18:44 +00:00
BraveCowardp
90c7fd4747 🍻 publish plugin b站消息转发 (#2784) 2024-06-25 15:17:40 +00:00
noneflow[bot]
c1a9758a18 📝 Update changelog 2024-06-25 15:06:02 +00:00
WMGray
17e7a0c029 🍻 publish plugin Daily Task (#2768) 2024-06-25 15:04:50 +00:00
noneflow[bot]
df6a948c08 📝 Update changelog 2024-06-25 14:05:42 +00:00
zifox666
9f19eb7a96 🍻 publish plugin EVE ONLINE 多功能机器人 (#2781) 2024-06-25 14:04:38 +00:00
noneflow[bot]
2b68428526 📝 Update changelog 2024-06-25 13:45:54 +00:00
kanbereina
d62c6561c2 🍻 publish plugin NTQQ自动登录/断连重启 (#2783) 2024-06-25 13:44:51 +00:00
noneflow[bot]
fc3bb5ff1f 📝 Update changelog 2024-06-25 13:13:53 +00:00
CCYellowStar2
76b1bbb443 🍻 publish plugin asmr (#2774) 2024-06-25 13:12:48 +00:00
noneflow[bot]
7b724925ba 📝 Update changelog 2024-06-20 12:50:21 +00:00
uy/sun
62dc2574c7 🐛 Fix: 修复 ForwardRef eval 时参数 recursive_guard 缺失 (#2778) 2024-06-20 20:49:17 +08:00
noneflow[bot]
ea40ae3a18 📝 Update changelog 2024-06-19 18:08:39 +00:00
ElainaFanBoy
f94e7d9b5b 🍻 publish plugin 日麻猜手牌小游戏 (#2776) 2024-06-19 18:07:12 +00:00
noneflow[bot]
c8ba973280 📝 Update changelog 2024-06-19 10:08:21 +00:00
SherkeyXD
35e062c588 🍻 publish plugin 绝地潜兵信息查询小助手 (#2771) 2024-06-19 10:07:19 +00:00
noneflow[bot]
53724487d3 📝 Update changelog 2024-06-19 08:56:41 +00:00
LiLuo-B
a3003b0ff6 🍻 publish plugin MCSM小助手 (#2770) 2024-06-19 08:55:32 +00:00
noneflow[bot]
96ecd415cd 📝 Update changelog 2024-06-18 21:42:46 +00:00
syagina
e8ef4735ea 🍻 publish plugin 多模态AI工具 (#2754) 2024-06-18 21:41:43 +00:00
noneflow[bot]
b78b08ed81 📝 Update changelog 2024-06-14 14:23:26 +00:00
phquathi
e11ea52276 🍻 publish plugin nonebot-plugin-easymarkdown (#2766) 2024-06-14 14:22:20 +00:00
noneflow[bot]
819e7334b2 📝 Update changelog 2024-06-14 14:08:12 +00:00
AzideCupric
1ebafaa9a5 🍻 publish plugin 峯驰外包 (#2764) 2024-06-14 14:07:04 +00:00
noneflow[bot]
3554292d5f 📝 Update changelog 2024-06-12 11:08:37 +00:00
BraveCowardp
ec9ef9a760 🍻 publish plugin 鸣潮抽卡记录分析 (#2762) 2024-06-12 11:07:23 +00:00
noneflow[bot]
74663c7c5e 📝 Update changelog 2024-06-11 13:48:36 +00:00
ajdgg
cbc99be031 🍻 publish plugin nonebot-plugin-xjie-weather (#2755) 2024-06-11 13:47:34 +00:00
noneflow[bot]
81e9bdd7ec 📝 Update changelog 2024-06-08 04:39:27 +00:00
KomoriDev
323038ecc6 🍻 publish plugin 颜值评分 (#2751) 2024-06-08 04:38:23 +00:00
noneflow[bot]
7091beb809 📝 Update changelog 2024-06-07 09:42:23 +00:00
ikarosf
010c48d30f 🍻 publish plugin 学园偶像大师算分插件 (#2749) 2024-06-07 09:41:19 +00:00
noneflow[bot]
a5b2dd38d5 📝 Update changelog 2024-06-05 13:38:04 +00:00
050644zf
fa5f295fe7 🍻 publish plugin nonebot-plugin-lynchpined (#2747) 2024-06-05 13:36:59 +00:00
pre-commit-ci[bot]
7f7b23bd2f ⬆️ auto update by pre-commit hooks (#2746)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-04 10:40:10 +08:00
noneflow[bot]
0434e12b8a 📝 Update changelog 2024-06-03 04:39:22 +00:00
yejue
425d140161 🍻 publish plugin QQShell (#2744) 2024-06-03 04:38:08 +00:00
noneflow[bot]
64d8f7843a 📝 Update changelog 2024-06-02 14:31:34 +00:00
CCYellowStar2
a0a6427540 🍻 publish plugin ai唱歌 (#2742) 2024-06-02 14:30:24 +00:00
noneflow[bot]
31fe8e6582 📝 Update changelog 2024-05-30 06:14:11 +00:00
mobyw
38e42919b7 📝 Docs: 修复单元测试示例代码 (#2741) 2024-05-30 14:13:11 +08:00
noneflow[bot]
c769f95688 📝 Update changelog 2024-05-29 12:53:55 +00:00
yejue
d642897a5b 🍻 publish plugin 复读姬+1 PlusOne (#2731) 2024-05-29 12:52:45 +00:00
noneflow[bot]
d7931f8ec2 📝 Update changelog 2024-05-27 13:22:56 +00:00
Sclock
8a0b989718 🍻 publish plugin 高优先级关闭信号钩子插件 (#2736) 2024-05-27 13:21:55 +00:00
noneflow[bot]
4fbbb646c3 📝 Update changelog 2024-05-25 14:17:07 +00:00
cubstaryow
75856e63f6 🍻 publish plugin 插件响应鉴权 (#2726) 2024-05-25 14:16:03 +00:00
noneflow[bot]
98213f50db 📝 Update changelog 2024-05-25 04:21:01 +00:00
Weltolk
5bce1db24e 📝 Docs: 修改依赖注入定义链接 (#2733) 2024-05-25 12:19:48 +08:00
noneflow[bot]
380ace5780 📝 Update changelog 2024-05-21 20:14:18 +00:00
Ljzd-PRO
6e5b01a3d4 🍻 publish plugin DG-Lab-Play (#2728) 2024-05-21 20:13:16 +00:00
noneflow[bot]
622e8e8af3 🔖 Release 2.3.1 2024-05-20 14:19:50 +00:00
Ju4tCode
2bbb83d3f2 🔖 bump version 2.3.1 (#2725) 2024-05-20 22:09:51 +08:00
noneflow[bot]
54756134d4 📝 Update changelog 2024-05-20 13:31:40 +00:00
Alpaca4610
932b212e04 🍻 publish plugin 自定义人格和AI绘图的混合聊天BOT (#2723) 2024-05-20 13:30:32 +00:00
noneflow[bot]
3b40e5b20c 📝 Update changelog 2024-05-18 15:32:15 +00:00
ajdgg
f594db207d 🍻 publish plugin nonebot-plugin-calc24 (#2720) 2024-05-18 15:31:14 +00:00
noneflow[bot]
70e23427e8 📝 Update changelog 2024-05-18 14:47:10 +00:00
WindowsSov8forUs
c1a303fd3d 🍻 publish plugin nonebot-plugin-tsugu-bangdream-bot (#2718) 2024-05-18 14:46:02 +00:00
noneflow[bot]
a62b9a5e1a 📝 Update changelog 2024-05-18 03:44:44 +00:00
Komorebi
36eece311a 📝 Docs: 修正 匹配扩展 中的示例 (#2722) 2024-05-18 11:43:39 +08:00
noneflow[bot]
29ea5f5787 📝 Update changelog 2024-05-17 05:58:08 +00:00
CCLMSY
c00e3aacfc 🍻 publish plugin 科大讯飞星火大语言模型官方API聊天机器人插件 (#2716) 2024-05-17 05:57:07 +00:00
noneflow[bot]
cf9f78528c 📝 Update changelog 2024-05-15 02:03:18 +00:00
Tarrailt
68d4795de6 📝 Docs: 更新 Mirai 适配器说明 (#2715)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-15 10:02:12 +08:00
noneflow[bot]
e689d7f7d2 📝 Update changelog 2024-05-14 17:06:21 +00:00
RF-Tar-Railt
a607f868c2 🍻 publish adapter Mirai (#2713) 2024-05-14 17:05:19 +00:00
noneflow[bot]
84ac1c4bad 📝 Update changelog 2024-05-14 15:27:34 +00:00
worldmozara
e11ff528e2 🗑️ 移除已在 PyPI 上删除的 covid 插件和 molar-mass 插件 (#2712) 2024-05-14 23:26:26 +08:00
noneflow[bot]
047f4d1878 📝 Update changelog 2024-05-14 04:28:53 +00:00
LiLuo-B
0294c33baf 🍻 publish plugin nonebot_plugin_valve_server_query (#2710) 2024-05-14 04:27:43 +00:00
noneflow[bot]
11a8b6e40b 📝 Update changelog 2024-05-13 14:06:07 +00:00
ConcyWee
cade86b62a 🍻 publish plugin 库洛游戏信息 (#2702) 2024-05-13 14:04:58 +00:00
noneflow[bot]
df836ec1c6 📝 Update changelog 2024-05-12 09:46:03 +00:00
zhaomaoniu
12cc00a3d3 🍻 publish plugin BanG Dream! Tsugu Frontend (#2707) 2024-05-12 09:45:00 +00:00
noneflow[bot]
24aa81f0be 📝 Update changelog 2024-05-12 09:33:27 +00:00
Yan-Zero
339706a3a6 🍻 publish plugin 神秘学助手 (#2699) 2024-05-12 09:32:25 +00:00
noneflow[bot]
b43c9adb7a 📝 Update changelog 2024-05-12 02:42:57 +00:00
Ekac00
c2783039d4 🍻 publish plugin nonebot-plugin-furryfusion (#2704) 2024-05-12 02:41:50 +00:00
noneflow[bot]
c4706e4123 📝 Update changelog 2024-05-12 02:30:10 +00:00
Ekac00
8a997540b3 🍻 publish plugin nonebot-plugin-RanFurryPic (#2701) 2024-05-12 02:28:59 +00:00
noneflow[bot]
045022b22a 📝 Update changelog 2024-05-09 07:10:05 +00:00
Ju4tCode
723fa4b3d8 🐛 Fix: State ForwardRef 检测错误 (#2698) 2024-05-09 15:08:49 +08:00
noneflow[bot]
41b59cff06 📝 Update changelog 2024-05-09 02:38:47 +00:00
yejue
bed1b46527 🍻 publish plugin with_ai_agents (#2696) 2024-05-09 02:37:44 +00:00
pre-commit-ci[bot]
ad695ca6e8 ⬆️ auto update by pre-commit hooks (#2695)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-07 13:35:53 +08:00
noneflow[bot]
33e997708c 📝 Update changelog 2024-05-05 05:48:08 +00:00
zhaomaoniu
56b6ee1d38 🍻 publish plugin nonebot_plugin_anime_downloader (#2690) 2024-05-05 05:47:03 +00:00
noneflow[bot]
27b2cf52a5 📝 Update changelog 2024-05-05 04:56:01 +00:00
eya46
b532130f6e 📝 Docs: 添加 Tailchat 适配器说明 (#2694)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2024-05-05 12:54:51 +08:00
noneflow[bot]
d16b8594ad 📝 Update changelog 2024-05-05 04:42:08 +00:00
eya46
ad8442c6de 🍻 publish adapter Tailchat (#2692) 2024-05-05 04:40:57 +00:00
noneflow[bot]
4edf7e2c2c 📝 Update changelog 2024-05-01 14:29:42 +00:00
StarHeart
ea49318809 📝 Docs: 添加 uwu logo (#2689) 2024-05-01 22:28:36 +08:00
noneflow[bot]
a9a86aba61 🔖 Release 2.3.0 2024-05-01 09:01:32 +00:00
Ju4tCode
6e95d5366c 🔖 bump version 2.3.0 (#2688) 2024-05-01 16:55:17 +08:00
noneflow[bot]
445711e1cb 📝 Update changelog 2024-05-01 08:17:06 +00:00
Bryan不可思议
dfd2096fe5 📝 Docs: 数据库最佳实践 (#2545)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2024-05-01 16:16:02 +08:00
noneflow[bot]
d469c6f287 📝 Update changelog 2024-05-01 05:39:50 +00:00
Ju4tCode
9655b941f3 🐛 Fix: none 系列驱动器启动失败时未退出应用 (#2687) 2024-05-01 13:38:47 +08:00
noneflow[bot]
4254fdfd8c 📝 Update changelog 2024-05-01 03:35:49 +00:00
Tarrailt
1b3cd7e2e2 📝 Docs: 更新最佳实践的 Alconna 部分 (#2686) 2024-05-01 11:34:33 +08:00
noneflow[bot]
897498b7f5 📝 Update changelog 2024-04-29 12:27:53 +00:00
Ju4tCode
34770e4463 🧑‍💻 CI: 修复 NoneFlow reaction 范围 (#2685) 2024-04-29 20:26:35 +08:00
noneflow[bot]
9d14f72249 📝 Update changelog 2024-04-27 09:58:35 +00:00
colasama
87f6e81ffc 🍻 publish plugin 表情包保存器 (#2683) 2024-04-27 09:57:30 +00:00
noneflow[bot]
c3373e141a 📝 Update changelog 2024-04-26 03:12:24 +00:00
StarHeart
a5f2d97b04 👷 CI: 修复测试 (#2682) 2024-04-26 11:11:13 +08:00
noneflow[bot]
80ac6a5ae9 📝 Update changelog 2024-04-25 03:13:26 +00:00
cubstaryow
496475e0ca 🍻 publish plugin HelpWithPic (#2680) 2024-04-25 03:12:15 +00:00
noneflow[bot]
982dbbccdf 📝 Update changelog 2024-04-24 06:57:06 +00:00
cubstaryow
3f9c20c60b 🍻 publish plugin cyberfurry (#2678) 2024-04-24 06:55:56 +00:00
noneflow[bot]
cabb3c6c45 📝 Update changelog 2024-04-23 04:37:41 +00:00
afterow
03bf1fdcfe 🍻 publish plugin 三爻易数 (#2674) 2024-04-23 04:36:36 +00:00
noneflow[bot]
f36f8d1bcc 📝 Update changelog 2024-04-22 07:59:57 +00:00
Ju4tCode
5c2c1770a2 👷 CI: NoneFlow 添加 reaction 响应提示 (#2677) 2024-04-22 15:58:48 +08:00
noneflow[bot]
6810af1e1d 📝 Update changelog 2024-04-22 07:27:58 +00:00
Ju4tCode
78ba6ce973 📝 Docs: 添加 OSPP 2024 项目说明 (#2676) 2024-04-22 15:26:48 +08:00
noneflow[bot]
15bcb7e374 📝 Update changelog 2024-04-22 06:35:01 +00:00
shi-yingyingjiang
7dd7ccbff5 🍻 publish plugin 战双表情 (#2668) 2024-04-22 06:33:57 +00:00
noneflow[bot]
5b17c8de71 📝 Update changelog 2024-04-21 02:39:49 +00:00
Autuamn
5cf4ff66a3 🍻 publish plugin QQ频道-Discord 互通 (#2666) 2024-04-21 02:38:40 +00:00
noneflow[bot]
b6be8a178e 📝 Update changelog 2024-04-20 15:41:30 +00:00
YuxiCN
b77c3b2d0c 🍻 publish plugin Yinying-Chat (#2653) 2024-04-20 15:40:30 +00:00
noneflow[bot]
e4a210b47c 📝 Update changelog 2024-04-20 06:48:21 +00:00
Ju4tCode
6bf10aafb7 Feature: 嵌套插件名称作用域优化 (#2665) 2024-04-20 14:47:12 +08:00
noneflow[bot]
e15d544341 📝 Update changelog 2024-04-19 03:49:38 +00:00
惜月
acdb5787db 📝 Docs: 更新 Villa 适配器说明 (#2661) 2024-04-19 11:48:34 +08:00
noneflow[bot]
18f0c9b500 📝 Update changelog 2024-04-19 02:56:24 +00:00
shi-yingyingjiang
b36e721274 🍻 publish plugin 淫语 (#2649) 2024-04-19 02:55:16 +00:00
noneflow[bot]
9fdc50cd0e 📝 Update changelog 2024-04-19 02:52:46 +00:00
Tarrailt
41abf077bc 📝 Docs: 添加 Kritor 适配器说明 (#2660)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2024-04-19 10:51:37 +08:00
noneflow[bot]
27a4e5a55b 📝 Update changelog 2024-04-19 02:43:27 +00:00
Ju4tCode
65f6a104e9 Update assets/adapters.json 2024-04-19 02:42:17 +00:00
RF-Tar-Railt
415bd07c0d 🍻 publish adapter Kritor (#2658) 2024-04-19 02:42:17 +00:00
noneflow[bot]
3fd26dd937 📝 Update changelog 2024-04-18 10:51:15 +00:00
Azide
f5f5d93b64 🐛 Bug: inherit_supported_adapters 在展开缩写前取交集 (#2654)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-18 18:50:11 +08:00
noneflow[bot]
b497bb8c83 📝 Update changelog 2024-04-18 10:30:35 +00:00
Tarrailt
b0d554eacb 📝 Docs: 更新最佳实践的 Alconna 部分 (#2656)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-18 18:28:31 +08:00
noneflow[bot]
cbecc7b930 📝 Update changelog 2024-04-18 06:11:20 +00:00
baiqwerdvd
5e0921aca9 🍻 publish plugin 飞花令 (#2647) 2024-04-18 06:10:11 +00:00
noneflow[bot]
7e8015e828 📝 Update changelog 2024-04-17 11:14:50 +00:00
huanxin996
bef5bdf0bf 🍻 publish plugin Hx_YinYing (#2645) 2024-04-17 11:13:39 +00:00
noneflow[bot]
c04cd5e83e 📝 Update changelog 2024-04-17 09:25:33 +00:00
Ju4tCode
30d3c1bbce Feature: 优化调用栈识别 (#2644) 2024-04-17 17:24:38 +08:00
noneflow[bot]
5e72461391 📝 Update changelog 2024-04-16 12:52:07 +00:00
KarisAya
54fdf71d91 🍻 publish plugin clovers插件框架 (#2642) 2024-04-16 12:51:10 +00:00
noneflow[bot]
420d0cfdc4 📝 Update changelog 2024-04-16 06:45:23 +00:00
zhulinyv
84bfba7a82 🍻 publish plugin nonebot-plugin-nai3 (#2638) 2024-04-16 06:44:27 +00:00
noneflow[bot]
9fd89a6822 📝 Update changelog 2024-04-15 16:34:44 +00:00
Ju4tCode
4a02dde83f 💥 Remove: 移除 Python 3.8 支持 (#2641) 2024-04-16 00:33:48 +08:00
noneflow[bot]
e93ee1ffec 📝 Update changelog 2024-04-14 14:27:20 +00:00
Ju4tCode
e2b6fb12c7 📝 Docs: 添加 RocketChat 适配器说明 (#2640)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-14 22:26:23 +08:00
noneflow[bot]
7836073c7e 📝 Update changelog 2024-04-11 05:33:11 +00:00
IllTamer
3119626d89 🍻 publish adapter RocketChat (#2636) 2024-04-11 05:32:21 +00:00
noneflow[bot]
19bebdd923 📝 Update changelog 2024-04-09 03:28:08 +00:00
Well2333
0b0dd8b552 🍻 publish plugin nonebot-plugin-auto-bot-selector (#2634) 2024-04-09 03:27:14 +00:00
noneflow[bot]
47ce7a633f 📝 Update changelog 2024-04-08 12:23:18 +00:00
StarHeart
ca32f68787 📝 Docs: 商店卡片样式调整 (#2633) 2024-04-08 20:22:29 +08:00
noneflow[bot]
0b972ad302 📝 Update changelog 2024-04-08 12:17:53 +00:00
Ju4tCode
9b4b1526b1 🐛 Bug: 添加 HTTP 客户端会话上下文检查 (#2632) 2024-04-08 20:17:03 +08:00
noneflow[bot]
7a232c7a4a 📝 Update changelog 2024-04-06 12:08:18 +00:00
Azide
983351f0b7 📝 Docs: 为商店插件卡片添加更多展示内容 (#2626) 2024-04-06 20:07:09 +08:00
noneflow[bot]
16fb5ac121 📝 Update changelog 2024-04-05 13:44:11 +00:00
mrqx0195
bb1fbca4a7 🍻 publish plugin Chikari_economy (#2630) 2024-04-05 13:43:17 +00:00
noneflow[bot]
b7c0b6b8e0 📝 Update changelog 2024-04-05 13:11:57 +00:00
Ju4tCode
485aa62755 Feature: 支持 HTTP 客户端会话 (#2627) 2024-04-05 21:11:05 +08:00
noneflow[bot]
53e2a86dd9 📝 Update changelog 2024-04-05 03:36:21 +00:00
zhzhongshi
312095d1df 🍻 publish plugin diffsinger (#2624) 2024-04-05 03:35:33 +00:00
noneflow[bot]
b498be1092 📝 Update changelog 2024-04-04 08:13:35 +00:00
Akirami
211ea8427f 📝 Docs: 修复 RegexMatched 文档类型标注错误 (#2629) 2024-04-04 16:12:39 +08:00
noneflow[bot]
407eb69568 📝 Update changelog 2024-04-04 07:30:23 +00:00
Akirami
8a44b4d6ee 📝 Docs: 修复 RegexMatched​ 文档高亮行错误 (#2628) 2024-04-04 15:29:33 +08:00
noneflow[bot]
bc58fbb741 📝 Update changelog 2024-04-02 14:55:01 +00:00
Azide
0c977f5fd7 📝 Docs: 为商店的详情卡片添加跳转链接 (#2623) 2024-04-02 22:54:10 +08:00
noneflow[bot]
7eeccbcb14 📝 Update changelog 2024-04-02 02:04:37 +00:00
Jigsaw
020d2a5687 🐛 Fix: 将 aiohttp 的 quote_fields 默认设为 False (#2619) 2024-04-02 10:03:46 +08:00
pre-commit-ci[bot]
83d61fcffd ⬆️ auto update by pre-commit hooks (#2620)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-02 09:55:19 +08:00
noneflow[bot]
c0b222a5fa 📝 Update changelog 2024-04-02 01:38:09 +00:00
Akirami
236e4ea9aa 📝 Docs: 添加 RegexMatched 依赖注入文档 (#2618)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2024-04-02 09:37:02 +08:00
noneflow[bot]
0622e16d18 📝 Update changelog 2024-04-02 01:21:36 +00:00
Lipraty
159ca84e46 🍻 publish plugin ghtiles (#2621) 2024-04-02 01:20:19 +00:00
noneflow[bot]
142a61ce5c 📝 Update changelog 2024-03-24 13:17:39 +00:00
QuanhuZeYu
7f226af541 🍻 publish plugin 人类友好数据配置 (#2615) 2024-03-24 13:16:37 +00:00
noneflow[bot]
8bf912499a 📝 Update changelog 2024-03-23 12:20:18 +00:00
Redmomn
a55b10cfa3 🍻 publish plugin nonebot-plugin-pallas-repeater (#2613) 2024-03-23 12:19:13 +00:00
noneflow[bot]
1a4f889b40 📝 Update changelog 2024-03-23 05:24:00 +00:00
Redmomn
f9bc2de4e4 🍻 publish plugin nonebot-plugin-duel (#2611) 2024-03-23 05:22:54 +00:00
noneflow[bot]
5a1c635083 📝 Update changelog 2024-03-23 05:14:56 +00:00
Agnes4m
76e8567f1e 🍻 publish plugin Sekai Stickers (#2609) 2024-03-23 05:13:57 +00:00
noneflow[bot]
9bd349d933 📝 Update changelog 2024-03-17 12:58:31 +00:00
GLDYM
5e8a67b605 🍻 publish plugin 100orangejuice (#2600) 2024-03-17 12:57:28 +00:00
noneflow[bot]
e16799d500 📝 Update changelog 2024-03-15 14:36:09 +00:00
zhaomaoniu
a189846194 🍻 publish plugin Steam Info (#2607) 2024-03-15 14:35:03 +00:00
noneflow[bot]
fcd536aada 📝 Update changelog 2024-03-13 03:32:46 +00:00
KroMiose
c89bafc2c9 🍻 publish plugin nonebot-plugin-dice-narrator (#2604) 2024-03-13 03:31:42 +00:00
dependabot[bot]
f5855a9f9a ⬆️ Bump the actions group with 1 update (#2605)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 13:56:57 +08:00
noneflow[bot]
a49b4bccc6 📝 Update changelog 2024-03-11 05:27:16 +00:00
NanakaNeko
b434da29b1 🍻 publish plugin a2s查询 (#2602) 2024-03-11 05:26:16 +00:00
noneflow[bot]
514b3a5afe 📝 Update changelog 2024-03-11 02:35:56 +00:00
student_2333
0d30f81ddb ✏️ Plugin: 移除不维护的插件 eitherchoice (#2599) 2024-03-11 10:34:44 +08:00
noneflow[bot]
9a86c00f62 📝 Update changelog 2024-03-09 05:54:47 +00:00
noneflow[bot]
7648138902 🍻 publish plugin 赛博钓鱼 (#2596)
Co-authored-by: C14H22O <C14H22O@users.noreply.github.com>
2024-03-09 13:53:45 +08:00
noneflow[bot]
2055f092f2 📝 Update changelog 2024-03-07 06:58:32 +00:00
uy/sun
9ff7f4baba 🧑‍💻 Develop: 添加 ruff RUF 规则 (#2598)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2024-03-07 14:57:26 +08:00
noneflow[bot]
92ba99c34c 📝 Update changelog 2024-03-05 05:42:03 +00:00
XTxiaoting14332
876cff4daf 🍻 publish plugin 人性化的ChatGLM (#2591) 2024-03-05 05:40:55 +00:00
noneflow[bot]
1ac6a612b0 📝 Update changelog 2024-03-04 09:12:55 +00:00
Redmomn
15ecad9f87 🍻 publish plugin nonebot-plugin-vits-tts (#2594) 2024-03-04 09:11:47 +00:00
noneflow[bot]
587d3f7c7e 📝 Update changelog 2024-03-02 09:49:10 +00:00
Ju4tCode
10e4ea6743 📝 Docs: 添加百度搜索资源验证 (#2590)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-03-02 17:48:04 +08:00
noneflow[bot]
d1601bf2fe 🔖 Release 2.2.1 2024-02-24 04:35:38 +00:00
Ju4tCode
2994945c64 🔖 bump version 2.2.1 (#2589) 2024-02-24 12:25:17 +08:00
noneflow[bot]
c9e3cad738 📝 Update changelog 2024-02-23 08:03:12 +00:00
XTxiaoting14332
7c36964812 ✏️ Plugin: 修改 phigros 相关内容 (#2578) 2024-02-23 16:02:00 +08:00
noneflow[bot]
0e02d13c67 📝 Update changelog 2024-02-23 07:55:58 +00:00
KomoriDev
f7aeea2f3d 🍻 publish plugin 运行状态 (#2586) 2024-02-23 07:54:48 +00:00
noneflow[bot]
b2da7d4cae 📝 Update changelog 2024-02-23 02:33:43 +00:00
SAFEluren
239f9769c2 🍻 publish plugin nonebot-plugin-bf1marneserverlist (#2583) 2024-02-23 02:32:43 +00:00
noneflow[bot]
f5947518b1 📝 Update changelog 2024-02-22 06:01:08 +00:00
MingxuanGame
1a4afa406b 📝 Docs: 更新 Session Expire Timeout​ 文档 (#2585) 2024-02-22 13:59:57 +08:00
noneflow[bot]
412b879f39 📝 Update changelog 2024-02-17 15:18:51 +00:00
MingxuanGame
a830346545 Feature: 优化 pydantic 兼容函数 model_dumptype_validate_json (#2579)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-02-17 23:18:00 +08:00
noneflow[bot]
fbb8320a25 📝 Update changelog 2024-02-17 04:07:01 +00:00
Cypas
14f4a0f701 🍻 publish plugin splatoon3游戏nso查询 (#2575) 2024-02-17 04:06:04 +00:00
noneflow[bot]
e82e2817d5 📝 Update changelog 2024-02-16 13:17:36 +00:00
Ju4tCode
ffbd1f9aeb 🐛 Fix: 修改遗漏的过时 Pydantic 方法 (#2577) 2024-02-16 21:16:46 +08:00
noneflow[bot]
5ab418a3cf 📝 Update changelog 2024-02-14 03:14:08 +00:00
mrqx0195
a58e00b206 🍻 publish plugin Chikari_yinpa (#2571) 2024-02-14 03:13:14 +00:00
noneflow[bot]
a74682bbf6 📝 Update changelog 2024-02-12 09:54:54 +00:00
student_2333
11142253fb 🐛 Fix: Message.__contains__() 未考虑 bool(MessageSegment) 存在 False 情况导致的异常结果 (#2572)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2024-02-12 17:53:50 +08:00
noneflow[bot]
ef7782167f 📝 Update changelog 2024-02-10 09:53:22 +00:00
Ju4tCode
f4a2682e6c 📝 Docs: 添加适配器测试注意事项 (#2570) 2024-02-10 17:52:29 +08:00
noneflow[bot]
35cee22cf6 🔖 Release 2.2.0 2024-02-09 08:21:37 +00:00
Ju4tCode
fbb55228f2 🔖 bump version 2.2.0 (#2569) 2024-02-09 16:14:15 +08:00
noneflow[bot]
391ac00d81 📝 Update changelog 2024-02-09 06:49:39 +00:00
lengmianzz
277b744ca3 📝 Docs: 更新 Alconna 文档 (#2568)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-02-09 14:48:48 +08:00
noneflow[bot]
a89c67a50e 📝 Update changelog 2024-02-07 08:00:35 +00:00
Ju4tCode
26b30a7b22 📝 Docs: 添加产品赞助列表 (#2566) 2024-02-07 15:59:36 +08:00
pre-commit-ci[bot]
4dae23d3bb ⬆️ auto update by pre-commit hooks (#2565)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2024-02-06 12:48:23 +08:00
noneflow[bot]
07e6c3f977 📝 Update changelog 2024-02-05 06:01:56 +00:00
Ju4tCode
dace63d9d2 Feature: 添加插件 Pydantic 相关使用方法 (#2563) 2024-02-05 14:00:49 +08:00
dependabot[bot]
2ebf956599 ⬆️ Bump the actions group with 1 update (#2564)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-05 13:35:34 +08:00
noneflow[bot]
b20793c67a 📝 Update changelog 2024-02-04 05:40:22 +00:00
velor2012
47e9f59cc8 🍻 publish plugin 定时提醒 (#2557) 2024-02-04 05:39:12 +00:00
noneflow[bot]
e27cac7fef 📝 Update changelog 2024-02-04 02:16:43 +00:00
Johnny Hsieh
5bfda6e2bc ✏️ Plugin: 移除不再维护的几款插件 (#2561) 2024-02-04 10:15:27 +08:00
noneflow[bot]
ef2ab7df48 📝 Update changelog 2024-02-02 02:48:11 +00:00
bingqiu456
ac1d9147d2 🍻 publish plugin 黑名单插件 (#2553) 2024-02-02 02:47:02 +00:00
dependabot[bot]
f2350909d2 ⬆️ Bump the actions group with 1 update (#2560)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2024-02-01 13:59:22 +08:00
noneflow[bot]
f14ef93808 📝 Update changelog 2024-02-01 02:42:50 +00:00
nek0us
45bd4252bf 🍻 publish plugin ChatGPT 聊天 (#2555) 2024-02-01 02:41:43 +00:00
noneflow[bot]
6b4456bf0e 📝 Update changelog 2024-02-01 02:30:11 +00:00
StarHeart
c5e114dc7f 🐛 Docs: 修复表单标签状态更新 (#2558) 2024-02-01 10:29:03 +08:00
noneflow[bot]
30ceea4287 📝 Update changelog 2024-01-29 02:40:32 +00:00
lengmianzz
380f9ff013 🍻 publish plugin BA模拟抽卡 (#2549) 2024-01-29 02:39:21 +00:00
noneflow[bot]
19ac119714 📝 Update changelog 2024-01-27 09:40:31 +00:00
HuParry
236f70183c 🍻 publish plugin 随机发送图片 (#2547) 2024-01-27 09:39:24 +00:00
noneflow[bot]
117bc35653 📝 Update changelog 2024-01-27 02:56:19 +00:00
eya46
4fcaa8d3d6 🍻 publish plugin 哪吒监控插件 (#2551) 2024-01-27 02:55:17 +00:00
noneflow[bot]
536889d3df 📝 Update changelog 2024-01-26 03:14:04 +00:00
Ju4tCode
bbd13c04cc Feature: 兼容 Pydantic v2 (#2544)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-26 11:12:57 +08:00
noneflow[bot]
82e4ccb227 📝 Update changelog 2024-01-24 07:25:25 +00:00
StarHeart
626cfa474f 👷 CI: 更新 prettier 配置 (#2546)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-24 15:24:21 +08:00
noneflow[bot]
18e9a9afd3 📝 Update changelog 2024-01-21 11:49:59 +00:00
pre-commit-ci[bot]
41b7d5a3a0 🚨 auto fix by pre-commit hooks 2024-01-21 11:48:50 +00:00
eya46
16fcd4c639 🍻 publish plugin SakuraFrp (#2542) 2024-01-21 11:48:50 +00:00
noneflow[bot]
ef3641efa6 📝 Update changelog 2024-01-21 11:32:33 +00:00
boxie123
8d95a32672 🍻 publish plugin haruka_bot_red (#2540) 2024-01-21 11:31:24 +00:00
dependabot[bot]
3a3a718779 ⬆️ Bump the actions group with 2 updates (#2539)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-18 13:58:14 +08:00
noneflow[bot]
3d1955211a 📝 Update changelog 2024-01-17 08:48:23 +00:00
zhaomaoniu
8d87715d6f 🍻 publish plugin nonebot-plugin-gemini (#2526) 2024-01-17 08:47:26 +00:00
noneflow[bot]
3c535b8e99 📝 Update changelog 2024-01-17 08:40:30 +00:00
Ju4tCode
2c6affecea 🐛 Fix: websockets 驱动器连接关闭 code 获取错误 (#2537) 2024-01-17 16:39:35 +08:00
noneflow[bot]
c2d2169a9f 📝 Update changelog 2024-01-15 05:25:48 +00:00
Ju4tCode
1153c5ff17 Feature: 使用自定义配置加载替代 pydantic-settings (#2521)
Co-authored-by: uy/sun <hmy0119@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-15 13:24:55 +08:00
noneflow[bot]
6c532f5926 📝 Update changelog 2024-01-15 03:52:20 +00:00
Perseus037
7083394bc9 🍻 publish plugin 最终台词 (#2522) 2024-01-15 03:51:29 +00:00
noneflow[bot]
7c58410868 📝 Update changelog 2024-01-15 02:50:11 +00:00
pk5ls20
00c3e3b713 🍻 publish plugin nonebot-plugin-nekoimage (#2532) 2024-01-15 02:49:14 +00:00
noneflow[bot]
9d4a72766d 📝 Update changelog 2024-01-15 02:38:31 +00:00
Alpaca4610
82e16b4438 🍻 publish plugin 谷歌Bard聊天 (#2528) 2024-01-15 02:37:37 +00:00
noneflow[bot]
56353f2d0a 📝 Update changelog 2024-01-15 02:31:11 +00:00
tianyisama
4d0eb94a6f 🍻 publish plugin nonebot-plugin-mypower (#2530) 2024-01-15 02:30:14 +00:00
noneflow[bot]
e1a494ecbd 📝 Update changelog 2024-01-14 06:50:02 +00:00
Ju4tCode
6b1e34da63 🐛 Fix: 修复 echo 发送空消息 (#2525) 2024-01-14 14:49:05 +08:00
noneflow[bot]
ccf9597102 📝 Update changelog 2024-01-11 03:53:02 +00:00
Bryan不可思议
5a6f4b9e1c Feature: 带参数的 RegexStr() (#2499) 2024-01-11 11:52:07 +08:00
noneflow[bot]
9b09b42f97 📝 Update changelog 2024-01-10 14:48:29 +00:00
Ju4tCode
854345e16f 📝 Docs: 添加 CITATION 文件 (#2520) 2024-01-10 22:47:27 +08:00
noneflow[bot]
e0ee865b87 📝 Update changelog 2024-01-10 02:34:49 +00:00
Pasumao
dad0c01335 🍻 publish plugin 文心一言4适配 (#2515) 2024-01-10 02:33:56 +00:00
noneflow[bot]
79ef5af19b 📝 Update changelog 2024-01-10 02:17:24 +00:00
lgc2333
b349959f93 🍻 publish plugin 最佳平替 (#2518) 2024-01-10 02:16:11 +00:00
noneflow[bot]
2e7f9612af 📝 Update changelog 2024-01-08 03:11:55 +00:00
wlm3201
8ff2303b22 🍻 publish plugin 随机MC图 (#2511) 2024-01-08 03:10:43 +00:00
noneflow[bot]
b681fdd6d6 📝 Update changelog 2024-01-04 03:12:42 +00:00
Johnny Hsieh
b65b3b438c 🐛 Fix: MessageTemplate 禁止访问私有属性 (#2509)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-04 11:11:37 +08:00
pre-commit-ci[bot]
580d6bab36 ⬆️ auto update by pre-commit hooks (#2510)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-02 11:01:26 +08:00
noneflow[bot]
90349ddd7d 📝 Update changelog 2023-12-29 08:46:49 +00:00
Perseus037
dcac421bc0 🍻 publish plugin nonebot_plugin_nikke (#2507) 2023-12-29 08:45:51 +00:00
noneflow[bot]
b4f643577f 📝 Update changelog 2023-12-29 08:25:46 +00:00
phquathi
411e7168b3 🍻 publish plugin nonebot-plugin-imagemaster (#2503) 2023-12-29 08:24:39 +00:00
noneflow[bot]
fef072a62a 📝 Update changelog 2023-12-28 08:19:24 +00:00
RF-Tar-Railt
f529e9cb23 🍻 publish plugin Waiter 插件 (#2505) 2023-12-28 08:18:34 +00:00
noneflow[bot]
cfa3bfd88c 📝 Update changelog 2023-12-28 06:51:55 +00:00
student_2333
321c99f12b ✏️ Plugin: 恢复删除的插件 nonebot-plugin-eitherchoice (#2502) 2023-12-28 14:50:54 +08:00
noneflow[bot]
73ad4992ee 📝 Update changelog 2023-12-27 02:09:54 +00:00
phquathi
ddbf37c1be 🍻 publish plugin AntiMonkey (#2500) 2023-12-27 02:08:54 +00:00
noneflow[bot]
b9392371c7 🔖 Release 2.1.3 2023-12-25 03:57:43 +00:00
Ju4tCode
d3c26a1548 🔖 bump version 2.1.3 (#2498) 2023-12-25 11:51:10 +08:00
noneflow[bot]
31c2a61cce 📝 Update changelog 2023-12-23 05:56:49 +00:00
XTxiaoting14332
f84ba9768b 🍻 publish plugin Phigros查分器(Adapter-qq) (#2496) 2023-12-23 05:55:59 +00:00
dependabot[bot]
1faa935527 ⬆️ Bump aiohttp from 3.9.0b1 to 3.9.0 (#2495)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-21 11:31:38 +08:00
noneflow[bot]
5f940ff309 📝 Update changelog 2023-12-18 07:30:21 +00:00
Cypas_Nya
4c4c0ea0ba ✏️ Plugin: 更新 splatoon3 插件地址 (#2494) 2023-12-18 15:29:23 +08:00
noneflow[bot]
787b40a99e 📝 Update changelog 2023-12-16 06:47:19 +00:00
lgc2333
fd6a0ae747 🍻 publish plugin Riffusion (#2492) 2023-12-16 06:46:29 +00:00
noneflow[bot]
298a32c096 📝 Update changelog 2023-12-15 02:53:22 +00:00
student_2333
aecff5ffd6 ✏️ Plugin: 删除不维护的插件 (#2491) 2023-12-15 10:52:30 +08:00
noneflow[bot]
c1a6b7b787 📝 Update changelog 2023-12-12 03:06:41 +00:00
Perseus037
0903f19f9c 🍻 publish plugin nonebot_plugin_longtu (#2489) 2023-12-12 03:05:45 +00:00
noneflow[bot]
51aa23817a 📝 Update changelog 2023-12-10 10:13:05 +00:00
Bryan不可思议
8f3f385cb6 🐛 Fix: 新增 Lifespan._on_ready() 供适配器使用 (#2483)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-12-10 18:12:10 +08:00
noneflow[bot]
915274081d 📝 Update changelog 2023-12-09 03:23:48 +00:00
lgc2333
a388c52b3f 🍻 publish plugin CNRail (#2487) 2023-12-09 03:22:56 +00:00
noneflow[bot]
b4d3cd4d4d 📝 Update changelog 2023-12-08 07:04:59 +00:00
Ju4tCode
50c03b0675 🚨 make pyright happy (#2486) 2023-12-08 15:03:59 +08:00
dependabot[bot]
fa3bb96417 ⬆️ Bump the actions group (#2484)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-08 13:26:19 +08:00
dependabot[bot]
09bde57835 ⬆️ Bump the actions group with 1 update (#2485)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-08 13:22:29 +08:00
noneflow[bot]
76ac2a8843 📝 Update changelog 2023-12-05 14:12:53 +00:00
Perseus037
f6ec6962ab 🍻 publish plugin ba塔罗牌,运势与魔法占卜! (#2480) 2023-12-05 14:11:43 +00:00
pre-commit-ci[bot]
28ad6829cd ⬆️ auto update by pre-commit hooks (#2482)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-12-05 14:02:02 +08:00
noneflow[bot]
7f4b002a87 📝 Update changelog 2023-12-03 11:37:26 +00:00
iyume
7e073b6ff4 🍻 publish plugin 群聊 NSFW 图片检测 (#2476) 2023-12-03 11:36:22 +00:00
noneflow[bot]
fa3781efe5 📝 Update changelog 2023-11-30 08:10:45 +00:00
StarHeart
bec74d85cd 📝 Docs: 商店详情卡片添加宽度限制与文本省略 (#2473) 2023-11-30 16:09:29 +08:00
noneflow[bot]
abc3829c64 📝 Update changelog 2023-11-30 05:53:17 +00:00
Jigsaw
18f5d6eab9 ✏️ Plugin: 移除不再维护的插件 (#2474) 2023-11-30 13:52:17 +08:00
noneflow[bot]
00f3e30930 📝 Update changelog 2023-11-29 12:42:03 +00:00
worldmozara
97cd21d004 ✏️ Plugin: 移除不再维护的插件 (#2472) 2023-11-29 20:40:55 +08:00
noneflow[bot]
09b4d44f23 📝 Update changelog 2023-11-29 07:52:04 +00:00
MeetWq
3536bf56bd ✏️ Plugin: 移除不再维护的插件 (#2471) 2023-11-29 15:51:01 +08:00
noneflow[bot]
f8eaf5def0 📝 Update changelog 2023-11-24 02:24:57 +00:00
mobyw
6077f85e52 🍻 publish plugin sm.ms图床 (#2469) 2023-11-24 02:23:50 +00:00
noneflow[bot]
e2976a3859 📝 Update changelog 2023-11-24 02:11:57 +00:00
Ju4tCode
1e25fde22e Update assets/plugins.json 2023-11-24 02:10:51 +00:00
mnixry
55d88b7dae 🍻 publish plugin 文件托管支持 (#2467) 2023-11-24 02:10:51 +00:00
noneflow[bot]
de30f8917f 📝 Update changelog 2023-11-23 10:52:02 +00:00
StarHeartHunt
52653fa005 🍻 publish plugin 短链接服务支持 (#2465) 2023-11-23 10:50:57 +00:00
noneflow[bot]
4628358add 📝 Update changelog 2023-11-22 08:33:21 +00:00
StarHeart
117b08a73e 📝 Docs: 修复商店发布 上一步 按钮显示问题 (#2464) 2023-11-22 16:32:14 +08:00
noneflow[bot]
700888a8e0 📝 Update changelog 2023-11-22 06:05:31 +00:00
StarHeart
ef882927f3 📝 Docs: 添加商店表单支持 (#2460)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-11-22 14:04:22 +08:00
noneflow[bot]
af9327de14 📝 Update changelog 2023-11-22 02:48:30 +00:00
he0119
2881d42bf5 🍻 publish plugin 用户 (#2462) 2023-11-22 02:47:21 +00:00
noneflow[bot]
dc3a49fe57 📝 Update changelog 2023-11-20 02:22:18 +00:00
student_2333
addabd6396 📝 Docs: 修复事件后处理函数类型 docstring 错误 (#2459) 2023-11-20 10:21:10 +08:00
noneflow[bot]
3341c641cc 📝 Update changelog 2023-11-12 04:18:10 +00:00
bingyue
363413e1e6 📝 Docs: 修改 QQ 频道为 QQ (#2457)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-11-12 12:17:05 +08:00
noneflow[bot]
b675d27a30 📝 Update changelog 2023-11-10 12:05:29 +00:00
pre-commit-ci[bot]
796023408a 🚨 auto fix by pre-commit hooks 2023-11-10 12:03:46 +00:00
惜月
983a8512b2 📝 更新README 2023-11-10 12:03:46 +00:00
CMHopeSunshine
6593102632 🍻 publish adapter DoDo (#2455) 2023-11-10 12:03:46 +00:00
noneflow[bot]
65fff13150 📝 Update changelog 2023-11-09 06:14:11 +00:00
Alpaca4610
edd1a140d7 🍻 publish plugin DALL-E 3绘图 (#2451) 2023-11-09 06:12:43 +00:00
noneflow[bot]
18070baad4 📝 Update changelog 2023-11-08 02:47:58 +00:00
tiehu
acf729f6e7 🍻 publish plugin 局域网唤醒 (#2448) 2023-11-08 02:46:59 +00:00
noneflow[bot]
6dbc8eac03 📝 Update changelog 2023-11-08 02:31:28 +00:00
StarHeart
35944bcbdc 👷 CI: 测试矩阵添加 Python 3.12 (#2441) 2023-11-08 10:30:00 +08:00
noneflow[bot]
3f919f91c1 📝 Update changelog 2023-11-07 06:40:21 +00:00
Tarrailt
443a20d83d 📝 Docs: 更新最佳实践的 Alconna 部分 (#2443)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: StarHeartHunt <starheart233@gmail.com>
2023-11-07 14:39:05 +08:00
noneflow[bot]
2fca26eaae 📝 Update changelog 2023-11-07 06:29:54 +00:00
jiangyuxiaoxiao
ebc8141971 🍻 publish plugin nonebot-plugin-bertvits2 (#2445) 2023-11-07 06:28:15 +00:00
pre-commit-ci[bot]
5d6bcc9b9b ⬆️ auto update by pre-commit hooks (#2447)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-07 14:14:49 +08:00
noneflow[bot]
55fca332ba 📝 Update changelog 2023-11-05 09:08:57 +00:00
MelodyYuuka
6b65c5fe69 🍻 publish plugin Nonebot2 Any 多平台服务 (#2440) 2023-11-05 09:07:36 +00:00
noneflow[bot]
3e4dbe1015 📝 Update changelog 2023-11-05 03:23:34 +00:00
zhaomaoniu
20197e64b2 🍻 publish bot Sakiko (#2438) 2023-11-05 03:22:31 +00:00
noneflow[bot]
94eecaf448 🔖 Release 2.1.2 2023-10-31 10:05:03 +00:00
Ju4tCode
fa91e0e79b 🔖 bump version 2.1.2 (#2437) 2023-10-31 17:55:31 +08:00
noneflow[bot]
891adc38fc 📝 Update changelog 2023-10-31 09:27:26 +00:00
Ju4tCode
af6cc63db2 ⬆️ upgrade pytest-asyncio and fix test (#2436) 2023-10-31 17:26:06 +08:00
noneflow[bot]
af73e14b64 📝 Update changelog 2023-10-27 15:11:11 +00:00
Ju4tCode
9305fe7875 🐛 修复依赖注入对 Literal 检查报错 (#2433) 2023-10-27 23:09:32 +08:00
noneflow[bot]
613fde4639 📝 Update changelog 2023-10-25 02:57:15 +00:00
T0nyX1ang
61db2c898b 🍻 publish plugin 定时广播插件 (#2431) 2023-10-25 02:55:43 +00:00
dependabot[bot]
acf313c420 ⬆️ Bump the actions group (#2430)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-24 13:47:28 +08:00
noneflow[bot]
15fca08641 📝 Update changelog 2023-10-22 06:29:50 +00:00
StarHeart
e2cbe3c1f8 📝 Docs: 修复 Alconna 文档 typo (#2429) 2023-10-22 14:28:29 +08:00
noneflow[bot]
d3883ea3ae 📝 Update changelog 2023-10-21 12:25:27 +00:00
SherkeyXD
8b2c4b3e60 🍻 publish plugin 选择困难症 (#2427) 2023-10-21 12:23:54 +00:00
noneflow[bot]
65d0d00591 📝 Update changelog 2023-10-18 07:56:54 +00:00
RainEggplant
97a57c2f6e Feature: 添加多消息段命令解析支持 (#2419)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-10-18 15:55:09 +08:00
noneflow[bot]
6559b2ff27 📝 Update changelog 2023-10-17 10:51:13 +00:00
StarHeartHunt
4c1deeb899 🍻 publish bot 芙芙 (#2425) 2023-10-17 10:49:41 +00:00
noneflow[bot]
a65ea6805d 📝 Update changelog 2023-10-17 02:13:27 +00:00
hanasa2023
effe65b034 🍻 publish plugin nonebot-plugin-getbapics (#2420) 2023-10-17 02:11:40 +00:00
noneflow[bot]
37296cf048 📝 Update changelog 2023-10-16 14:31:07 +00:00
StarHeart
1b597c1301 📝 add baidu statistic script (#2424) 2023-10-16 09:29:48 -05:00
noneflow[bot]
c2454d0689 📝 Update changelog 2023-10-15 07:33:10 +00:00
Yuri-YuzuChaN
9b60b44554 🍻 publish plugin nonebot-plugin-maimaidx (#2421) 2023-10-15 07:31:48 +00:00
noneflow[bot]
75516bdafb 📝 Update changelog 2023-10-14 12:33:47 +00:00
MerCuJerry
12f5a487c1 🍻 publish plugin BlueArchive Title Generator (#2417) 2023-10-14 12:32:20 +00:00
noneflow[bot]
8d128d5035 📝 Update changelog 2023-10-13 06:13:24 +00:00
Agnes4m
cfa7117e64 🍻 publish plugin VRChat查询 (#2410) 2023-10-13 06:12:10 +00:00
noneflow[bot]
7880bf0dc1 📝 Update changelog 2023-10-13 05:50:53 +00:00
influ3nza
0054041829 🍻 publish plugin nonebot_plugin_fgoavatarguess (#2415) 2023-10-13 05:49:26 +00:00
noneflow[bot]
99931f785a 📝 Update changelog 2023-10-10 12:35:55 +00:00
jiangyuxiaoxiao
5e121269f0 🍻 publish bot 妃爱 (#2412) 2023-10-10 12:34:36 +00:00
noneflow[bot]
38ced0243f 📝 Update changelog 2023-10-10 06:28:40 +00:00
EuDs63
869db878e1 🍻 publish plugin nonebot-plugin-yesman​ (#2408) 2023-10-10 06:27:23 +00:00
noneflow[bot]
e6c6e355e1 📝 Update changelog 2023-10-09 02:19:46 +00:00
ninthseason
6221b9a5fd 🍻 publish plugin morep-finder (#2406) 2023-10-09 02:18:19 +00:00
noneflow[bot]
5f2c9c935b 📝 Update changelog 2023-10-08 10:11:53 +00:00
Ju4tCode
76559b253c Update assets/adapters.json 2023-10-08 10:10:28 +00:00
pre-commit-ci[bot]
3c54655c39 🚨 auto fix by pre-commit hooks 2023-10-08 10:10:28 +00:00
Ju4tCode
7a851ac199 Apply suggestions from code review 2023-10-08 10:10:28 +00:00
pre-commit-ci[bot]
b2ba5dfcd1 🚨 auto fix by pre-commit hooks 2023-10-08 10:10:28 +00:00
Tarrailt
4a4fae8f8c Update README.md 2023-10-08 10:10:28 +00:00
RF-Tar-Railt
de894ce7b2 🍻 publish adapter Satori (#2404) 2023-10-08 10:10:28 +00:00
noneflow[bot]
09c4a955c9 📝 Update changelog 2023-10-07 07:31:56 +00:00
ninthseason
db1581a0a2 🍻 publish plugin op-finder (#2402) 2023-10-07 07:30:07 +00:00
noneflow[bot]
db9d7b3060 📝 Update changelog 2023-10-07 04:07:34 +00:00
Tarrailt
7e0c29472e 📝 Docs: 更新最佳实践 Alconna (#2401)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-07 12:06:07 +08:00
noneflow[bot]
d13492070d 📝 Update changelog 2023-10-06 10:04:18 +00:00
Ohdmire
695ede51ea 🍻 publish plugin nonebot-plugin-playercheck (#2399) 2023-10-06 10:02:52 +00:00
noneflow[bot]
168f382aa6 📝 Update changelog 2023-10-06 06:23:20 +00:00
nikissXI
5bd433318d ✏️ Plugin: 移除 nonebot-plugin-nya-music 插件 (#2398) 2023-10-06 14:21:51 +08:00
noneflow[bot]
d1cd2a793e 📝 Update changelog 2023-10-06 03:17:45 +00:00
nikissXI
5a4464f338 🍻 publish plugin talk with eop ai (#2396) 2023-10-06 03:16:15 +00:00
noneflow[bot]
561d25320b 📝 Update changelog 2023-10-04 10:21:51 +00:00
Komorebi
b225c2dd3b 📝 Docs: 修改商店发布的跳转链接 (#2387)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-10-04 18:20:06 +08:00
noneflow[bot]
2a2e357513 📝 Update changelog 2023-10-04 10:13:30 +00:00
zhuhiki
28bfe1ecb8 🍻 publish plugin 算法比赛查询和今日比赛自动提醒 (#2394) 2023-10-04 10:12:01 +00:00
pre-commit-ci[bot]
cc12f0af7e ⬆️ auto update by pre-commit hooks (#2393)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-03 21:49:16 +08:00
noneflow[bot]
da831a1b08 📝 Update changelog 2023-10-03 11:59:10 +00:00
MelodyKnit
eb97be17dd 🍻 publish plugin 屏蔽词插件 (#2386) 2023-10-03 11:57:53 +00:00
noneflow[bot]
2dd1c9b2ad 📝 Update changelog 2023-10-03 11:38:02 +00:00
MingxuanGame
41191db863 🐛 Docs: 修复文档主页 Features 不居中 (#2390) 2023-10-03 19:36:40 +08:00
noneflow[bot]
ee20204b22 📝 Update changelog 2023-10-03 10:53:54 +00:00
Dobiichi-Origami
f1032804bb 🍻 publish plugin Nonebot Agent (#2388) 2023-10-03 10:52:35 +00:00
noneflow[bot]
ba1540d75b 📝 Update changelog 2023-10-02 15:04:27 +00:00
uy/sun
f5c87f80e1 🧑‍💻 CI: 调整商店数据存放位置与内容 (#2385) 2023-10-02 23:03:05 +08:00
noneflow[bot]
d2d7603ff5 📝 Update changelog 2023-10-02 06:44:11 +00:00
KomoriDev
56013dca48 🍻 publish plugin 聚能环 (#2383) 2023-10-02 06:42:55 +00:00
noneflow[bot]
d33ed4a69f 📝 Update changelog 2023-10-02 06:38:57 +00:00
Ju4tCode
ed753b5564 ✏️ Adapter: 修改频道适配器为 QQ 适配器 (#2382)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-02 14:37:28 +08:00
Ju4tCode
7e65552d01 👷 fix doc package publish ci 2023-10-01 07:31:27 +00:00
600 changed files with 64712 additions and 20595 deletions

View File

@@ -4,18 +4,17 @@
"features": {
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
},
"postCreateCommand": "poetry config virtualenvs.in-project true && poetry install -E all && poetry run pre-commit install && yarn install",
"postCreateCommand": "./scripts/setup-envs.sh",
"customizations": {
"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

@@ -4,7 +4,7 @@ description: Setup Node
runs:
using: "composite"
steps:
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: "18"
cache: "yarn"

View File

@@ -6,6 +6,14 @@ inputs:
description: Python version
required: false
default: "3.10"
env-dir:
description: Environment directory
required: false
default: "."
no-root:
description: Do not install package in the environment
required: false
default: "false"
runs:
using: "composite"
@@ -14,11 +22,19 @@ runs:
run: pipx install poetry
shell: bash
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
architecture: "x64"
cache: "poetry"
cache-dependency-path: |
./poetry.lock
${{ inputs.env-dir }}/poetry.lock
- run: poetry install -E all
- run: |
cd ${{ inputs.env-dir }}
if [ "${{ inputs.no-root }}" = "true" ]; then
poetry install --all-extras --no-root
else
poetry install --all-extras
fi
shell: bash

View File

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

View File

@@ -6,25 +6,32 @@ on:
- master
pull_request:
paths:
- "envs/**"
- "nonebot/**"
- "packages/**"
- "tests/**"
- ".github/actions/setup-python/**"
- ".github/workflows/codecov.yml"
- "pyproject.toml"
- "poetry.lock"
jobs:
test:
name: Test Coverage
runs-on: ${{ matrix.os }}
concurrency:
group: test-coverage-${{ github.ref }}-${{ matrix.os }}-${{ matrix.python-version }}
group: test-coverage-${{ github.ref }}-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.env }}
cancel-in-progress: true
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
os: [ubuntu-latest, windows-latest, macos-latest]
env: [pydantic-v1, pydantic-v2]
env:
OS: ${{ matrix.os }}
PYTHON_VERSION: ${{ matrix.python-version }}
PYDANTIC_VERSION: ${{ matrix.env }}
steps:
- uses: actions/checkout@v4
@@ -33,15 +40,19 @@ jobs:
uses: ./.github/actions/setup-python
with:
python-version: ${{ matrix.python-version }}
env-dir: ./envs/${{ matrix.env }}
no-root: true
- name: Run Pytest
run: |
cd tests/
poetry run pytest -n auto --cov-report xml
cd ./envs/${{ matrix.env }}
poetry run bash "../../scripts/run-tests.sh"
- name: Upload coverage report
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
env_vars: OS,PYTHON_VERSION
env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION
files: ./tests/coverage.xml
flags: unittests
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -15,9 +15,10 @@ concurrency:
cancel-in-progress: false
jobs:
plugin_test:
check:
runs-on: ubuntu-latest
name: nonebot2 plugin test
name: check
# do not run on forked PRs, do not run on not related issues, do not run on pr comments
if: |
!(
(
@@ -35,6 +36,46 @@ jobs:
github.event_name == 'issue_comment' && github.event.issue.pull_request
)
)
steps:
- run: echo "Check passed"
reaction:
runs-on: ubuntu-latest
name: reaction
needs: check
if: |
(
github.event_name == 'issue_comment' &&
github.event.action == 'created'
) ||
(
github.event_name == 'issues' &&
github.event.action == 'opened'
)
steps:
- name: Generate token
id: generate-token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_KEY }}
- name: Reaction on issue
if: github.event_name == 'issues'
run: |
gh api --method POST /repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/reactions -f "content=rocket"
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
- name: Reaction on issue comment
if: github.event_name == 'issue_comment'
run: |
gh api --method POST /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions -f "content=rocket"
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
plugin_test:
runs-on: ubuntu-latest
name: nonebot2 plugin test
needs: check
permissions:
issues: read
outputs:
@@ -47,7 +88,7 @@ jobs:
run: pipx install poetry
- name: Setup Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.x"
@@ -73,7 +114,7 @@ jobs:
token: ${{ steps.generate-token.outputs.token }}
- name: Cache pre-commit hooks
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: .cache/.pre-commit
key: noneflow-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
@@ -84,9 +125,9 @@ jobs:
config: >
{
"base": "master",
"plugin_path": "website/static/plugins.json",
"bot_path": "website/static/bots.json",
"adapter_path": "website/static/adapters.json"
"plugin_path": "assets/plugins.json",
"bot_path": "assets/bots.json",
"adapter_path": "assets/adapters.json"
}
env:
PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }}

View File

@@ -6,21 +6,42 @@ on:
- master
pull_request:
paths:
- "envs/**"
- "nonebot/**"
- "packages/**"
- "tests/**"
- ".github/actions/setup-python/**"
- ".github/workflows/pyright.yml"
- "pyproject.toml"
- "poetry.lock"
jobs:
pyright:
name: Pyright Lint
runs-on: ubuntu-latest
concurrency:
group: pyright-${{ github.ref }}-${{ matrix.env }}
cancel-in-progress: true
strategy:
matrix:
env: [pydantic-v1, pydantic-v2]
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Setup Python environment
uses: ./.github/actions/setup-python
with:
env-dir: ./envs/${{ matrix.env }}
no-root: true
- run: echo "$(poetry env info --path)/bin" >> $GITHUB_PATH
- run: |
(cd ./envs/${{ matrix.env }} && echo "$(poetry env info --path)/bin" >> $GITHUB_PATH)
if [ "${{ matrix.env }}" = "pydantic-v1" ]; then
sed -i 's/PYDANTIC_V2 = true/PYDANTIC_V2 = false/g' ./pyproject.toml
fi
shell: bash
- name: Run Pyright
uses: jakebailey/pyright-action@v1
uses: jakebailey/pyright-action@v2

View File

@@ -32,7 +32,7 @@ jobs:
- name: Setup Node Environment
uses: ./.github/actions/setup-node
- uses: release-drafter/release-drafter@v5
- uses: release-drafter/release-drafter@v6
id: release-drafter
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
@@ -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## )'
@@ -92,7 +92,7 @@ jobs:
if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION
run: exit 1
- uses: release-drafter/release-drafter@v5
- uses: release-drafter/release-drafter@v6
with:
name: Release ${{ steps.version.outputs.TAG_NAME }} 🌈
tag: ${{ steps.version.outputs.TAG_NAME }}
@@ -123,7 +123,7 @@ jobs:
- name: Publish Doc Package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: packages/nonebot-plugin-docs/
packages-dir: packages/nonebot-plugin-docs/dist/
- name: Publish Doc Package to GitHub
run: |

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

@@ -6,14 +6,23 @@ on:
- master
pull_request:
paths:
- "envs/**"
- "nonebot/**"
- "packages/**"
- "tests/**"
- ".github/actions/setup-python/**"
- ".github/workflows/ruff.yml"
- "pyproject.toml"
- "poetry.lock"
jobs:
ruff:
name: Ruff Lint
runs-on: ubuntu-latest
concurrency:
group: pyright-${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v4

View File

@@ -33,7 +33,7 @@ jobs:
run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v2
uses: nwtgck/actions-netlify@v3
with:
publish-dir: "./website/build"
production-deploy: true

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@v2
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.0.291
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.12.0
rev: 5.13.2
hooks:
- id: isort
stages: [commit]
stages: [pre-commit]
- repo: https://github.com/psf/black
rev: 23.9.1
rev: 24.10.0
hooks:
- id: black
stages: [commit]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.3
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

@@ -5,5 +5,17 @@
"arrowParens": "always",
"singleQuote": false,
"trailingComma": "es5",
"semi": true
"semi": true,
"overrides": [
{
"files": [
"**/devcontainer.json",
"**/tsconfig.json",
"**/tsconfig.*.json"
],
"options": {
"parser": "json"
}
}
]
}

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>

26
CITATION.cff Normal file
View File

@@ -0,0 +1,26 @@
# This CITATION.cff file was generated with cffinit.
# Visit https://bit.ly/cffinit to generate yours today!
cff-version: 1.2.0
title: NoneBot
message: >-
If you use this software, please cite it using the
metadata from this file.
type: software
authors:
- given-names: Yongyu
family-names: Yan
email: yyy@nonebot.dev
- name: NoneBot Team
email: contact@nonebot.dev
website: 'https://github.com/nonebot'
repository-code: 'https://github.com/nonebot/nonebot2'
url: 'https://nonebot.dev/'
abstract: >-
NoneBot, an asynchronous multi-platform chatbot framework
written in Python
keywords:
- nonebot
- chatbot
- pydantic
license: MIT

View File

@@ -21,7 +21,7 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
<a href="https://pypi.python.org/pypi/nonebot2">
<img src="https://img.shields.io/pypi/v/nonebot2?logo=python&logoColor=edb641" alt="pypi">
</a>
<img src="https://img.shields.io/badge/python-3.8+-blue?logo=python&logoColor=edb641" alt="python">
<img src="https://img.shields.io/badge/python-3.9+-blue?logo=python&logoColor=edb641" alt="python">
<a href="https://github.com/psf/black">
<img src="https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=edb641" alt="black">
</a>
@@ -54,6 +54,9 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
<a href="https://onebot.dev/">
<img src="https://img.shields.io/badge/OneBot-v12-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
</a>
<a href="https://bot.q.qq.com/wiki/">
<img src="https://img.shields.io/badge/QQ-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ">
</a>
<a href="https://core.telegram.org/bots/api">
<img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram" alt="telegram">
</a>
@@ -63,9 +66,6 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
<a href="https://docs.github.com/en/developers/apps">
<img src="https://img.shields.io/badge/GitHub-Bot-181717?style=social&logo=github" alt="github"/>
</a>
<a href="https://bot.q.qq.com/wiki/">
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ频道">
</a>
<!-- <a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p">
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="dingtalk"> -->
</a>
@@ -94,7 +94,7 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
<p align="center">
<a href="https://asciinema.org/a/569440">
<img src="https://nonebot.dev/img/setup.svg">
<img src="https://nonebot.dev/img/setup.svg" alt="setup" >
</a>
</p>
@@ -110,24 +110,29 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
- 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源))
- 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
| 协议名称 | 状态 | 注释 |
| :--------------------------------------------------------------------------------------------------------------------------: | :--: | :-----------------------------------------------------------------------: |
| OneBot[仓库](https://github.com/nonebot/adapter-onebot)[协议](https://onebot.dev/) | ✅ | 支持 QQ、TG、微信公众号、KOOK 等[平台](https://onebot.dev/ecosystem.html) |
| Telegram[仓库](https://github.com/nonebot/adapter-telegram)[协议](https://core.telegram.org/bots/api) | ✅ | |
| 飞书([仓库](https://github.com/nonebot/adapter-feishu)[协议](https://open.feishu.cn/document/home/index) | ✅ | |
| GitHub[仓库](https://github.com/nonebot/adapter-github)[协议](https://docs.github.com/en/apps) | ✅ | GitHub APP & OAuth APP |
| QQ 频道[仓库](https://github.com/nonebot/adapter-qqguild)[协议](https://bot.q.qq.com/wiki/) | ✅ | 官方接口调整较多 |
| 钉钉[仓库](https://github.com/nonebot/adapter-ding)[协议](https://open.dingtalk.com/document/) | 🤗 | 寻找 Maintainer暂不可用 |
| Console[仓库](https://github.com/nonebot/adapter-console) | ✅ | 控制台交互 |
| Red [仓库](https://github.com/nonebot/adapter-red)[协议](https://chrononeko.github.io/QQNTRedProtocol/) | ✅ | QQ 协议 |
| Discord [仓库](https://github.com/nonebot/adapter-discord)[协议](https://discord.com/developers/docs/intro) | ✅ | Discord Bot 协议 |
| 开黑啦[仓库](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) | ↗️ | 由社区贡献 |
| Walle-Q[仓库](https://github.com/onebot-walle/nonebot_adapter_walleq) | ↗️ | QQ 协议,由社区贡献 |
| Villa[仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa)[协议](https://webstatic.mihoyo.com/vila/bot/doc/) | ↗️ | 米游社大别野 Bot 协议,由社区贡献 |
| 协议名称 | 状态 | 注释 |
| :-------------------------------------------------------------------------------------------------------------------: | :--: | :-----------------------------------------------------------------------: |
| OneBot[仓库](https://github.com/nonebot/adapter-onebot)[协议](https://onebot.dev/) | ✅ | 支持 QQ、TG、微信公众号、KOOK 等[平台](https://onebot.dev/ecosystem.html) |
| Telegram[仓库](https://github.com/nonebot/adapter-telegram)[协议](https://core.telegram.org/bots/api) | ✅ | |
| 飞书([仓库](https://github.com/nonebot/adapter-feishu)[协议](https://open.feishu.cn/document/home/index) | ✅ | |
| GitHub[仓库](https://github.com/nonebot/adapter-github)[协议](https://docs.github.com/en/apps) | ✅ | GitHub APP & OAuth APP |
| QQ[仓库](https://github.com/nonebot/adapter-qq)[协议](https://bot.q.qq.com/wiki/) | ✅ | QQ 官方接口调整较多 |
| Console[仓库](https://github.com/nonebot/adapter-console) | | 控制台交互 |
| Red[仓库](https://github.com/nonebot/adapter-red)[协议](https://chrononeko.github.io/QQNTRedProtocol/) | ✅ | QQ 协议 |
| Satori[仓库](https://github.com/nonebot/adapter-satori)[协议](https://satori.js.org/zh-CN) | ✅ | 支持 Onebot、TG、飞书、微信公众号、Koishi 等 |
| Discord[仓库](https://github.com/nonebot/adapter-discord)[协议](https://discord.com/developers/docs/intro) | ✅ | Discord Bot 协议 |
| DoDo[仓库](https://github.com/nonebot/adapter-dodo)[协议](https://open.imdodo.com/) | ✅ | DoDo Bot 协议 |
| Kritor[仓库](https://github.com/nonebot/adapter-kritor)[协议](https://github.com/KarinJS/kritor) | | Kritor (OnebotX) 协议QQ 机器人接口标准 |
| 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/) | ↗️ | 由社区贡献 |
| Ntchat[仓库](https://github.com/JustUndertaker/adapter-ntchat) | ↗️ | 微信协议,由社区贡献 |
| MineCraft[仓库](https://github.com/17TheWord/nonebot-adapter-minecraft) | ↗️ | 由社区贡献 |
| BiliBili Live[仓库](https://github.com/wwweww/adapter-bilibili) | ↗️ | 由社区贡献 |
| Walle-Q[仓库](https://github.com/onebot-walle/nonebot_adapter_walleq) | ↗️ | QQ 协议,由社区贡献 |
| Villa[仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa) | ❌ | 米游社大别野 Bot 协议,官方已下线 |
| Rocket.Chat[仓库](https://github.com/IUnlimit/nonebot-adapter-rocketchat)[协议](https://developer.rocket.chat/) | ↗️ | Rocket.Chat Bot 协议,由社区贡献 |
| Tailchat[仓库](https://github.com/eya46/nonebot-adapter-tailchat)[协议](https://tailchat.msgbyte.com/) | ↗️ | Tailchat 开放平台 Bot 协议,由社区贡献 |
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
@@ -230,10 +235,52 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### 赞助者
感谢以下产品对 NoneBot 项目提供的赞助:
<p align="center">
<a href="https://github.com/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.nonebot.dev/github-dark.png">
<img src="https://assets.nonebot.dev/github-light.png" height="50" alt="GitHub">
</picture>
</a>&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.netlify.com/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.nonebot.dev/netlify-dark.svg">
<img src="https://assets.nonebot.dev/netlify-light.svg" height="50" alt="netlify">
</picture>
</a>&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://sentry.io/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.nonebot.dev/sentry-dark.svg">
<img src="https://assets.nonebot.dev/sentry-light.svg" height="50" alt="sentry">
</picture>
</a>
</p>
<p align="center">
<a href="https://www.docker.com/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.nonebot.dev/docker-dark.svg">
<img src="https://assets.nonebot.dev/docker-light.svg" height="50" alt="docker">
</picture>
</a>&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.algolia.com/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.nonebot.dev/algolia-dark.svg">
<img src="https://assets.nonebot.dev/algolia-light.svg" height="50" alt="algolia">
</picture>
</a>
</p>
<p align="center">
<a href="https://www.jetbrains.com/">
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg" height="80" alt="JetBrains" >
</a>
</p>
感谢以下赞助者对 NoneBot 项目提供的资金支持:
<a href="https://assets.nonebot.dev/sponsors.svg">
<img src='https://assets.nonebot.dev/sponsors.svg'/>
<img src="https://assets.nonebot.dev/sponsors.svg" alt="sponsors" />
</a>
### 开发者
@@ -241,5 +288,5 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
感谢以下开发者对 NoneBot2 作出的贡献:
<a href="https://github.com/nonebot/nonebot2/graphs/contributors">
<img src="https://contrib.rocks/image?repo=nonebot/nonebot2&max=1000" />
<img src="https://contrib.rocks/image?repo=nonebot/nonebot2&max=1000" alt="contributors" />
</a>

View File

@@ -40,12 +40,12 @@
"is_official": true
},
{
"module_name": "nonebot.adapters.qqguild",
"project_link": "nonebot-adapter-qqguild",
"name": "QQ 频道",
"desc": "QQ 频道官方机器人",
"module_name": "nonebot.adapters.qq",
"project_link": "nonebot-adapter-qq",
"name": "QQ",
"desc": "QQ 官方机器人",
"author": "yanyongyu",
"homepage": "https://github.com/nonebot/adapter-qqguild",
"homepage": "https://github.com/nonebot/adapter-qq",
"tags": [],
"is_official": true
},
@@ -60,14 +60,14 @@
"is_official": false
},
{
"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",
"module_name": "nonebot.adapters.mirai",
"project_link": "nonebot-adapter-mirai",
"name": "Mirai",
"desc": "mirai-api-http v2 协议适配",
"author": "RF-Tar-Railt",
"homepage": "https://github.com/nonebot/adapter-mirai",
"tags": [],
"is_official": false
"is_official": true
},
{
"module_name": "nonebot.adapters.onebot.v12",
@@ -188,5 +188,65 @@
"homepage": "https://github.com/nonebot/adapter-discord",
"tags": [],
"is_official": true
},
{
"module_name": "nonebot.adapters.satori",
"project_link": "nonebot-adapter-satori",
"name": "Satori",
"desc": "Satori 协议适配器",
"author": "RF-Tar-Railt",
"homepage": "https://github.com/nonebot/adapter-satori",
"tags": [
{
"label": "跨平台",
"color": "#bf40bf"
}
],
"is_official": true
},
{
"module_name": "nonebot.adapters.dodo",
"project_link": "nonebot-adapter-dodo",
"name": "DoDo",
"desc": "DoDo Bot 协议适配器",
"author": "CMHopeSunshine",
"homepage": "https://github.com/nonebot/adapter-dodo",
"tags": [],
"is_official": true
},
{
"module_name": "nonebot.adapters.rocketchat",
"project_link": "nonebot-adapter-rocketchat",
"name": "RocketChat",
"desc": "RocketChat adapter for nonebot2",
"author": "IllTamer",
"homepage": "https://github.com/IUnlimit/nonebot-adapter-rocketchat",
"tags": [],
"is_official": false
},
{
"module_name": "nonebot.adapters.kritor",
"project_link": "nonebot-adapter-kritor",
"name": "Kritor",
"desc": "Kritor 协议适配",
"author": "RF-Tar-Railt",
"homepage": "https://github.com/nonebot/adapter-kritor",
"tags": [
{
"label": "QQNT",
"color": "#35a7c9"
}
],
"is_official": true
},
{
"module_name": "nonebot_adapter_tailchat",
"project_link": "nonebot-adapter-tailchat",
"name": "Tailchat",
"desc": "Tailchat 适配器",
"author": "eya46",
"homepage": "https://github.com/eya46/nonebot-adapter-tailchat",
"tags": [],
"is_official": false
}
]

View File

@@ -574,5 +574,88 @@
}
],
"is_official": false
},
{
"name": "妃爱",
"desc": "超可爱的妃爱QQ群聊机器人",
"author": "jiangyuxiaoxiao",
"homepage": "https://github.com/jiangyuxiaoxiao/Hiyori",
"tags": [],
"is_official": false
},
{
"name": "芙芙",
"desc": "供 Mooncell Wiki 协作使用的跨平台机器人",
"author": "StarHeartHunt",
"homepage": "https://github.com/MooncellWiki/BotFooChan",
"tags": [],
"is_official": false
},
{
"name": "Sakiko",
"desc": "基于 LiteLoaderBDS 的 Minecraft 基岩版 Bot",
"author": "zhaomaoniu",
"homepage": "https://github.com/zhaomaoniu/Sakiko",
"tags": [
{
"label": "Minecraft",
"color": "#6cc349"
},
{
"label": "BanGDream",
"color": "#e70050"
}
],
"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
}
]

7573
assets/plugins.json Normal file

File diff suppressed because it is too large Load Diff

2485
envs/pydantic-v1/poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
[tool.poetry]
name = "nonebot-pydantic-v1"
version = "0.1.0"
description = "Private pydantic v1 test env for nonebot"
authors = ["yanyongyu <yyy@nonebot.dev>"]
license = "MIT"
[tool.poetry.dependencies]
python = "^3.9"
[tool.poetry.group.dev.dependencies]
pydantic = "^1.0.0"
nonebot-test = { path = "../test/", develop = false }
nonebot2 = { path = "../../", extras = ["all"], develop = true }
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

2561
envs/pydantic-v2/poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
[tool.poetry]
name = "nonebot-pydantic-v2"
version = "0.1.0"
description = "Private pydantic v2 test env for nonebot"
authors = ["yanyongyu <yyy@nonebot.dev>"]
license = "MIT"
[tool.poetry.dependencies]
python = "^3.9"
[tool.poetry.group.dev.dependencies]
pydantic = "^2.0.0"
nonebot-test = { path = "../test/", develop = false }
nonebot2 = { path = "../../", extras = ["all"], develop = true }
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -0,0 +1 @@
# fake file to make project installable

1329
envs/test/poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

21
envs/test/pyproject.toml Normal file
View File

@@ -0,0 +1,21 @@
[tool.poetry]
name = "nonebot-test"
version = "0.1.0"
description = "Private test env for nonebot"
authors = ["yanyongyu <yyy@nonebot.dev>"]
license = "MIT"
packages = [{ include = "nonebot-test.py" }]
[tool.poetry.dependencies]
python = "^3.9"
trio = "^0.27.0"
nonebug = "^0.4.1"
wsproto = "^1.2.0"
pytest-cov = "^5.0.0"
pytest-xdist = "^3.0.2"
werkzeug = ">=2.3.6,<4.0.0"
coverage-conditional-plugin = "^0.9.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -35,23 +35,26 @@
{ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>`
- `get_available_plugin_names` =>
{ref}``get_available_plugin_names` <nonebot.plugin.get_available_plugin_names>`
- `get_plugin_config` => {ref}``get_plugin_config` <nonebot.plugin.get_plugin_config>`
- `require` => {ref}``require` <nonebot.plugin.load.require>`
FrontMatter:
mdx:
format: md
sidebar_position: 0
description: nonebot 模块
"""
import os
from importlib.metadata import version
from typing import Any, Dict, Type, Union, TypeVar, Optional, overload
from typing import Any, Union, TypeVar, Optional, overload
import loguru
from pydantic.env_settings import DotenvType
from nonebot.config import Env, Config
from nonebot.compat import model_dump
from nonebot.log import logger as logger
from nonebot.adapters import Bot, Adapter
from nonebot.config import DOTENV_TYPE, Env, Config
from nonebot.utils import escape_tag, resolve_dot_notation
from nonebot.drivers import Driver, ASGIMixin, combine_driver
@@ -99,7 +102,7 @@ def get_adapter(name: str) -> Adapter:
@overload
def get_adapter(name: Type[A]) -> A:
def get_adapter(name: type[A]) -> A:
"""
参数:
name: 适配器类型
@@ -109,7 +112,7 @@ def get_adapter(name: Type[A]) -> A:
"""
def get_adapter(name: Union[str, Type[Adapter]]) -> Adapter:
def get_adapter(name: Union[str, type[Adapter]]) -> Adapter:
"""获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。
异常:
@@ -130,7 +133,7 @@ def get_adapter(name: Union[str, Type[Adapter]]) -> Adapter:
return adapters[target]
def get_adapters() -> Dict[str, Adapter]:
def get_adapters() -> dict[str, Adapter]:
"""获取所有已注册的 {ref}`nonebot.adapters.Adapter` 实例。
返回:
@@ -229,7 +232,7 @@ def get_bot(self_id: Optional[str] = None) -> Bot:
raise ValueError("There are no bots to get.")
def get_bots() -> Dict[str, Bot]:
def get_bots() -> dict[str, Bot]:
"""获取所有连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。
返回:
@@ -248,7 +251,7 @@ def get_bots() -> Dict[str, Bot]:
return get_driver().bots
def _resolve_combine_expr(obj_str: str) -> Type[Driver]:
def _resolve_combine_expr(obj_str: str) -> type[Driver]:
drivers = obj_str.split("+")
DriverClass = resolve_dot_notation(
drivers[0], "Driver", default_prefix="nonebot.drivers."
@@ -265,15 +268,16 @@ def _resolve_combine_expr(obj_str: str) -> Type[Driver]:
def _log_patcher(record: "loguru.Record"):
"""使用插件标识优化日志展示"""
record["name"] = (
plugin.name
plugin.id_
if (module_name := record["name"])
and (plugin := get_plugin_by_module_name(module_name))
else (module_name and module_name.split(".")[0])
else (module_name and module_name.split(".", maxsplit=1)[0])
)
def init(*, _env_file: Optional[DotenvType] = None, **kwargs: Any) -> None:
def init(*, _env_file: Optional[DOTENV_TYPE] = None, **kwargs: Any) -> None:
"""初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。
NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。
@@ -296,9 +300,11 @@ def init(*, _env_file: Optional[DotenvType] = None, **kwargs: Any) -> None:
_env_file = _env_file or f".env.{env.environment}"
config = Config(
**kwargs,
_env_file=(".env", _env_file)
if isinstance(_env_file, (str, os.PathLike))
else _env_file,
_env_file=(
(".env", _env_file)
if isinstance(_env_file, (str, os.PathLike))
else _env_file
),
)
logger.configure(
@@ -308,7 +314,7 @@ def init(*, _env_file: Optional[DotenvType] = None, **kwargs: Any) -> None:
f"Current <y><b>Env: {escape_tag(env.environment)}</b></y>"
)
logger.opt(colors=True).debug(
f"Loaded <y><b>Config</b></y>: {escape_tag(str(config.dict()))}"
f"Loaded <y><b>Config</b></y>: {escape_tag(str(model_dump(config)))}"
)
DriverClass = _resolve_combine_expr(config.driver)
@@ -353,10 +359,9 @@ from nonebot.plugin import load_from_json as load_from_json
from nonebot.plugin import load_from_toml as load_from_toml
from nonebot.plugin import load_all_plugins as load_all_plugins
from nonebot.plugin import on_shell_command as on_shell_command
from nonebot.plugin import get_plugin_config as get_plugin_config
from nonebot.plugin import get_loaded_plugins as get_loaded_plugins
from nonebot.plugin import load_builtin_plugin as load_builtin_plugin
from nonebot.plugin import load_builtin_plugins as load_builtin_plugins
from nonebot.plugin import get_plugin_by_module_name as get_plugin_by_module_name
from nonebot.plugin import get_available_plugin_names as get_available_plugin_names
__autodoc__ = {"internal": False}

View File

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

418
nonebot/compat.py Normal file
View File

@@ -0,0 +1,418 @@
"""本模块为 Pydantic 版本兼容层模块
为兼容 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
from nonebot.typing import origin_is_annotated
T = TypeVar("T")
PYDANTIC_V2 = int(VERSION.split(".", 1)[0]) == 2
if TYPE_CHECKING:
class _CustomValidationClass(Protocol):
@classmethod
def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]: ...
CVC = TypeVar("CVC", bound=_CustomValidationClass)
__all__ = (
"Required",
"PydanticUndefined",
"PydanticUndefinedType",
"ConfigDict",
"DEFAULT_CONFIG",
"FieldInfo",
"ModelField",
"TypeAdapter",
"extract_field_info",
"model_fields",
"model_config",
"model_dump",
"type_validate_python",
"type_validate_json",
"custom_validation",
)
__autodoc__ = {
"PydanticUndefined": "Pydantic Undefined object",
"PydanticUndefinedType": "Pydantic Undefined type",
}
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.fields import FieldInfo as BaseFieldInfo
Required = Ellipsis
"""Alias of Ellipsis for compatibility with pydantic v1"""
# Export undefined type
from pydantic_core import PydanticUndefined as PydanticUndefined
from pydantic_core import PydanticUndefinedType as PydanticUndefinedType
# isort: split
# Export model config dict
from pydantic import ConfigDict as ConfigDict
DEFAULT_CONFIG = ConfigDict(extra="allow", arbitrary_types_allowed=True)
"""Default config for validations"""
class FieldInfo(BaseFieldInfo):
"""FieldInfo class with extra property for compatibility with pydantic v1"""
# make default can be positional argument
def __init__(self, default: Any = PydanticUndefined, **kwargs: Any) -> None:
super().__init__(default=default, **kwargs)
@property
def extra(self) -> dict[str, Any]:
"""Extra data that is not part of the standard pydantic fields.
For compatibility with pydantic v1.
"""
# extract extra data from attributes set except used slots
# we need to call super in advance due to
# comprehension not inlined in cpython < 3.12
# https://peps.python.org/pep-0709/
slots = super().__slots__
return {k: v for k, v in self._attributes_set.items() if k not in slots}
@dataclass
class ModelField:
"""ModelField class for compatibility with pydantic v1"""
name: str
"""The name of the field."""
annotation: Any
"""The annotation of the field."""
field_info: FieldInfo
"""The FieldInfo of the field."""
@classmethod
def _construct(cls, name: str, annotation: Any, field_info: FieldInfo) -> Self:
return cls(name, annotation, field_info)
@classmethod
def construct(
cls, name: str, annotation: Any, field_info: Optional[FieldInfo] = None
) -> Self:
"""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.
TypeAdapter raise error when annotation has config
and given config is not None.
"""
type_is_annotated = origin_is_annotated(get_origin(self.annotation))
inner_type = (
get_args(self.annotation)[0] if type_is_annotated else self.annotation
)
try:
return (
issubclass(inner_type, BaseModel)
or is_dataclass(inner_type)
or is_typeddict(inner_type)
)
except TypeError:
return False
def get_default(self) -> Any:
"""Get the default value of the field."""
return self.field_info.get_default(call_default_factory=True)
def _type_display(self):
"""Get the display of the type of the field."""
return display_as_type(self.annotation)
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."""
kwargs = field_info._attributes_set.copy()
kwargs["annotation"] = field_info.rebuild_annotation()
return kwargs
def model_fields(model: type[BaseModel]) -> list[ModelField]:
"""Get field list of a model."""
return [
ModelField._construct(
name=name,
annotation=field_info.rebuild_annotation(),
field_info=FieldInfo(**extract_field_info(field_info)),
)
for name, field_info in model.model_fields.items()
]
def model_config(model: type[BaseModel]) -> Any:
"""Get config of a model."""
return model.model_config
def model_dump(
model: BaseModel,
include: Optional[set[str]] = None,
exclude: Optional[set[str]] = None,
by_alias: bool = False,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
) -> dict[str, Any]:
return model.model_dump(
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
def type_validate_python(type_: type[T], data: Any) -> T:
"""Validate data with given type."""
return TypeAdapter(type_).validate_python(data)
def type_validate_json(type_: type[T], data: Union[str, bytes]) -> T:
"""Validate JSON with given type."""
return TypeAdapter(type_).validate_json(data)
def __get_pydantic_core_schema__(
cls: type["_CustomValidationClass"],
source_type: Any,
handler: GetCoreSchemaHandler,
) -> CoreSchema:
validators = list(cls.__get_validators__())
if len(validators) == 1:
return core_schema.no_info_plain_validator_function(validators[0])
return core_schema.chain_schema(
[core_schema.no_info_plain_validator_function(func) for func in validators]
)
def custom_validation(class_: type["CVC"]) -> type["CVC"]:
"""Use pydantic v1 like validator generator in pydantic v2"""
setattr(
class_,
"__get_pydantic_core_schema__",
classmethod(__get_pydantic_core_schema__),
)
return class_
else: # pragma: pydantic-v1
from pydantic import Extra
from pydantic import parse_obj_as, parse_raw_as
from pydantic import BaseConfig as PydanticConfig
from pydantic.fields import FieldInfo as BaseFieldInfo
from pydantic.fields import ModelField as BaseModelField
from pydantic.schema import get_annotation_from_field_info
# isort: split
from pydantic.fields import Required as Required
# isort: split
from pydantic.fields import Undefined as PydanticUndefined
from pydantic.fields import UndefinedType as PydanticUndefinedType
class ConfigDict(PydanticConfig):
"""Config class that allow get value with default value."""
@classmethod
def get(cls, field: str, default: Any = None) -> Any:
"""Get a config value."""
return getattr(cls, field, default)
class DEFAULT_CONFIG(ConfigDict):
extra = Extra.allow
arbitrary_types_allowed = True
class FieldInfo(BaseFieldInfo):
def __init__(self, default: Any = PydanticUndefined, **kwargs: Any):
# preprocess default value to make it compatible with pydantic v2
# when default is Required, set it to PydanticUndefined
if default is Required:
default = PydanticUndefined
super().__init__(default, **kwargs)
class ModelField(BaseModelField):
@classmethod
def _construct(cls, name: str, annotation: Any, field_info: FieldInfo) -> Self:
return cls(
name=name,
type_=annotation,
class_validators=None,
model_config=DEFAULT_CONFIG,
default=field_info.default,
default_factory=field_info.default_factory,
required=(
field_info.default is PydanticUndefined
and field_info.default_factory is None
),
field_info=field_info,
)
@classmethod
def construct(
cls, name: str, annotation: Any, field_info: Optional[FieldInfo] = None
) -> Self:
"""Construct a ModelField from given infos.
Field annotation is preprocessed with field_info.
"""
if field_info is not None:
annotation = get_annotation_from_field_info(
annotation, field_info, name
)
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."""
kwargs = {
s: getattr(field_info, s) for s in field_info.__slots__ if s != "extra"
}
kwargs.update(field_info.extra)
return kwargs
def model_fields(model: type[BaseModel]) -> list[ModelField]:
"""Get field list of a model."""
# construct the model field without preprocess to avoid error
return [
ModelField._construct(
name=model_field.name,
annotation=model_field.annotation,
field_info=FieldInfo(
**extract_field_info(model_field.field_info),
),
)
for model_field in model.__fields__.values()
]
def model_config(model: type[BaseModel]) -> Any:
"""Get config of a model."""
return model.__config__
def model_dump(
model: BaseModel,
include: Optional[set[str]] = None,
exclude: Optional[set[str]] = None,
by_alias: bool = False,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
) -> dict[str, Any]:
return model.dict(
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
def type_validate_python(type_: type[T], data: Any) -> T:
"""Validate data with given type."""
return parse_obj_as(type_, data)
def type_validate_json(type_: type[T], data: Union[str, bytes]) -> T:
"""Validate JSON with given type."""
return parse_raw_as(type_, data)
def custom_validation(class_: type["CVC"]) -> type["CVC"]:
"""Do nothing in pydantic v1"""
return class_

View File

@@ -7,81 +7,253 @@ 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 模块
"""
import os
import abc
import json
from pathlib import Path
from datetime import timedelta
from ipaddress import IPv4Address
from typing import TYPE_CHECKING, Any, Set, Dict, Tuple, Union, Mapping, Optional
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Union, Optional
from typing_extensions import TypeAlias, get_args, get_origin
from pydantic.utils import deep_update
from pydantic.fields import Undefined, UndefinedType
from pydantic import Extra, Field, BaseSettings, IPvAnyAddress
from pydantic.env_settings import (
DotenvType,
SettingsError,
EnvSettingsSource,
InitSettingsSource,
SettingsSourceCallable,
)
from dotenv import dotenv_values
from pydantic import Field, BaseModel
from pydantic.networks import IPvAnyAddress
from nonebot.log import logger
from nonebot.typing import origin_is_union
from nonebot.utils import deep_update, type_is_complex, lenient_issubclass
from nonebot.compat import (
PYDANTIC_V2,
ConfigDict,
ModelField,
PydanticUndefined,
PydanticUndefinedType,
model_config,
model_fields,
)
DOTENV_TYPE: TypeAlias = Union[
Path, str, list[Union[Path, str]], tuple[Union[Path, str], ...]
]
ENV_FILE_SENTINEL = Path("")
class CustomEnvSettings(EnvSettingsSource):
def __call__(self, settings: BaseSettings) -> Dict[str, Any]:
class SettingsError(ValueError): ...
class BaseSettingsSource(abc.ABC):
def __init__(self, settings_cls: type["BaseSettings"]) -> None:
self.settings_cls = settings_cls
@property
def config(self) -> "SettingsConfig":
return model_config(self.settings_cls)
@abc.abstractmethod
def __call__(self) -> dict[str, Any]:
raise NotImplementedError
class InitSettingsSource(BaseSettingsSource):
__slots__ = ("init_kwargs",)
def __init__(
self, settings_cls: type["BaseSettings"], init_kwargs: dict[str, Any]
) -> None:
self.init_kwargs = init_kwargs
super().__init__(settings_cls)
def __call__(self) -> dict[str, Any]:
return self.init_kwargs
def __repr__(self) -> str:
return f"InitSettingsSource(init_kwargs={self.init_kwargs!r})"
class DotEnvSettingsSource(BaseSettingsSource):
def __init__(
self,
settings_cls: type["BaseSettings"],
env_file: Optional[DOTENV_TYPE] = ENV_FILE_SENTINEL,
env_file_encoding: Optional[str] = None,
case_sensitive: Optional[bool] = None,
env_nested_delimiter: Optional[str] = None,
) -> None:
super().__init__(settings_cls)
self.env_file = (
env_file
if env_file is not ENV_FILE_SENTINEL
else self.config.get("env_file", (".env",))
)
self.env_file_encoding = (
env_file_encoding
if env_file_encoding is not None
else self.config.get("env_file_encoding", "utf-8")
)
self.case_sensitive = (
case_sensitive
if case_sensitive is not None
else self.config.get("case_sensitive", False)
)
self.env_nested_delimiter = (
env_nested_delimiter
if env_nested_delimiter is not None
else self.config.get("env_nested_delimiter", None)
)
def _apply_case_sensitive(self, var_name: str) -> str:
return var_name if self.case_sensitive else var_name.lower()
def _field_is_complex(self, field: ModelField) -> tuple[bool, bool]:
if type_is_complex(field.annotation):
return True, False
elif origin_is_union(get_origin(field.annotation)) and any(
type_is_complex(arg) for arg in get_args(field.annotation)
):
return True, True
return False, False
def _parse_env_vars(
self, env_vars: Mapping[str, Optional[str]]
) -> dict[str, Optional[str]]:
return {
self._apply_case_sensitive(key): value for key, value in env_vars.items()
}
def _read_env_file(self, file_path: Path) -> dict[str, Optional[str]]:
file_vars = dotenv_values(file_path, encoding=self.env_file_encoding)
return self._parse_env_vars(file_vars)
def _read_env_files(self) -> dict[str, Optional[str]]:
env_files = self.env_file
if env_files is None:
return {}
if isinstance(env_files, (str, os.PathLike)):
env_files = [env_files]
dotenv_vars: dict[str, Optional[str]] = {}
for env_file in env_files:
env_path = Path(env_file).expanduser()
if env_path.is_file():
dotenv_vars.update(self._read_env_file(env_path))
return dotenv_vars
def _next_field(
self, field: Optional[ModelField], key: str
) -> Optional[ModelField]:
if not field or origin_is_union(get_origin(field.annotation)):
return None
elif field.annotation and lenient_issubclass(field.annotation, BaseModel):
for field in model_fields(field.annotation):
if field.name == key:
return field
return None
def _explode_env_vars(
self,
field: ModelField,
env_vars: dict[str, Optional[str]],
env_file_vars: dict[str, Optional[str]],
) -> dict[str, Any]:
if self.env_nested_delimiter is None:
return {}
prefix = f"{field.name}{self.env_nested_delimiter}"
result: dict[str, Any] = {}
for env_name, env_val in env_vars.items():
if not env_name.startswith(prefix):
continue
# delete from file vars when used
if env_name in env_file_vars:
del env_file_vars[env_name]
_, *keys, last_key = env_name.split(self.env_nested_delimiter)
env_var = result
target_field: Optional[ModelField] = field
for key in keys:
target_field = self._next_field(target_field, key)
env_var = env_var.setdefault(key, {})
target_field = self._next_field(target_field, last_key)
if target_field and env_val:
is_complex, allow_parse_failure = self._field_is_complex(target_field)
if is_complex:
try:
env_val = json.loads(env_val)
except ValueError as e:
if not allow_parse_failure:
raise SettingsError(
f'error parsing env var "{env_name}"'
) from e
env_var[last_key] = env_val
return result
def __call__(self) -> dict[str, Any]:
"""从环境变量和 dotenv 配置文件中读取配置项。"""
d: Dict[str, Any] = {}
d: dict[str, Any] = {}
if settings.__config__.case_sensitive:
env_vars: Mapping[str, Optional[str]] = os.environ # pragma: no cover
else:
env_vars = {k.lower(): v for k, v in os.environ.items()}
env_file_vars = self._read_env_files(settings.__config__.case_sensitive)
env_vars = self._parse_env_vars(os.environ)
env_file_vars = self._read_env_files()
env_vars = {**env_file_vars, **env_vars}
for field in settings.__fields__.values():
env_val: Union[str, None, UndefinedType] = Undefined
for env_name in field.field_info.extra["env_names"]:
env_val = env_vars.get(env_name, Undefined)
if env_name in env_file_vars:
del env_file_vars[env_name]
if env_val is not Undefined:
break
for field in model_fields(self.settings_cls):
field_name = field.name
env_name = self._apply_case_sensitive(field_name)
is_complex, allow_parse_failure = self.field_is_complex(field)
# try get values from env vars
env_val = env_vars.get(env_name, PydanticUndefined)
# delete from file vars when used
if env_name in env_file_vars:
del env_file_vars[env_name]
is_complex, allow_parse_failure = self._field_is_complex(field)
if is_complex:
if isinstance(env_val, UndefinedType):
if isinstance(env_val, PydanticUndefinedType):
# field is complex but no value found so far, try explode_env_vars
if env_val_built := self.explode_env_vars(field, env_vars):
d[field.alias] = env_val_built
if env_val_built := self._explode_env_vars(
field, env_vars, env_file_vars
):
d[field_name] = env_val_built
elif env_val is None:
d[field.alias] = env_val
d[field_name] = env_val
else:
# field is complex and there's a value
# decode that as JSON, then add explode_env_vars
try:
env_val = settings.__config__.parse_env_var(field.name, env_val)
env_val = json.loads(env_val)
except ValueError as e:
if not allow_parse_failure:
raise SettingsError(
f'error parsing env var "{env_name}"' # type: ignore
f'error parsing env var "{env_name}"'
) from e
if isinstance(env_val, dict):
d[field.alias] = deep_update(
env_val, self.explode_env_vars(field, env_vars)
# field value is a dict
# try explode_env_vars to find more sub-values
d[field_name] = deep_update(
env_val,
self._explode_env_vars(field, env_vars, env_file_vars),
)
else:
d[field.alias] = env_val
elif not isinstance(env_val, UndefinedType):
d[field_name] = env_val
elif env_val is not PydanticUndefined:
# simplest case, field is not complex
# we only need to add the value if it was found
d[field.alias] = env_val
d[field_name] = env_val
# remain user custom config
for env_name in env_file_vars:
@@ -89,7 +261,7 @@ class CustomEnvSettings(EnvSettingsSource):
if env_val and (val_striped := env_val.strip()):
# there's a value, decode that as JSON
try:
env_val = settings.__config__.parse_env_var(env_name, val_striped)
env_val = json.loads(val_striped)
except ValueError:
logger.trace(
"Error while parsing JSON for "
@@ -113,38 +285,80 @@ class CustomEnvSettings(EnvSettingsSource):
return d
class BaseConfig(BaseSettings):
if PYDANTIC_V2: # pragma: pydantic-v2
class SettingsConfig(ConfigDict, total=False):
env_file: Optional[DOTENV_TYPE]
env_file_encoding: str
case_sensitive: bool
env_nested_delimiter: Optional[str]
else: # pragma: pydantic-v1
class SettingsConfig(ConfigDict):
env_file: Optional[DOTENV_TYPE]
env_file_encoding: str
case_sensitive: bool
env_nested_delimiter: Optional[str]
class BaseSettings(BaseModel):
if TYPE_CHECKING:
# dummy getattr for pylance checking, actually not used
def __getattr__(self, name: str) -> Any: # pragma: no cover
return self.__dict__.get(name)
class Config:
extra = Extra.allow
env_nested_delimiter = "__"
if PYDANTIC_V2: # pragma: pydantic-v2
model_config = SettingsConfig(
extra="allow",
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
env_nested_delimiter="__",
)
else: # pragma: pydantic-v1
@classmethod
def customise_sources(
cls,
init_settings: InitSettingsSource,
env_settings: EnvSettingsSource,
file_secret_settings: SettingsSourceCallable,
) -> Tuple[SettingsSourceCallable, ...]:
common_config = init_settings.init_kwargs.pop("_common_config", {})
return (
init_settings,
CustomEnvSettings(
env_settings.env_file,
env_settings.env_file_encoding,
env_settings.env_nested_delimiter,
env_settings.env_prefix_len,
),
InitSettingsSource(common_config),
file_secret_settings,
class Config(SettingsConfig):
extra = "allow" # type: ignore
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = False
env_nested_delimiter = "__"
def __init__(
__settings_self__, # pyright: ignore[reportSelfClsParameterName]
_env_file: Optional[DOTENV_TYPE] = ENV_FILE_SENTINEL,
_env_file_encoding: Optional[str] = None,
_env_nested_delimiter: Optional[str] = None,
**values: Any,
) -> None:
super().__init__(
**__settings_self__._settings_build_values(
values,
env_file=_env_file,
env_file_encoding=_env_file_encoding,
env_nested_delimiter=_env_nested_delimiter,
)
)
def _settings_build_values(
self,
init_kwargs: dict[str, Any],
env_file: Optional[DOTENV_TYPE] = None,
env_file_encoding: Optional[str] = None,
env_nested_delimiter: Optional[str] = None,
) -> dict[str, Any]:
init_settings = InitSettingsSource(self.__class__, init_kwargs=init_kwargs)
env_settings = DotEnvSettingsSource(
self.__class__,
env_file=env_file,
env_file_encoding=env_file_encoding,
env_nested_delimiter=env_nested_delimiter,
)
return deep_update(env_settings(), init_settings())
class Env(BaseConfig):
class Env(BaseSettings):
"""运行环境配置。大小写不敏感。
将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。
@@ -156,11 +370,8 @@ class Env(BaseConfig):
NoneBot 将从 `.env.{environment}` 文件中加载配置。
"""
class Config:
env_file = ".env"
class Config(BaseConfig):
class Config(BaseSettings):
"""NoneBot 主要配置。大小写不敏感。
除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。
@@ -169,7 +380,8 @@ class Config(BaseConfig):
配置方法参考: [配置](https://nonebot.dev/docs/appendices/config)
"""
_env_file: DotenvType = ".env", ".env.prod"
if TYPE_CHECKING:
_env_file: Optional[DOTENV_TYPE] = ".env", ".env.prod"
# nonebot configs
driver: str = "~fastapi"
@@ -206,7 +418,7 @@ class Config(BaseConfig):
"""API 请求超时时间,单位: 秒。"""
# bot runtime configs
superusers: Set[str] = set()
superusers: set[str] = set()
"""机器人超级用户。
用法:
@@ -214,9 +426,9 @@ class Config(BaseConfig):
SUPERUSERS=["12345789"]
```
"""
nickname: Set[str] = set()
nickname: set[str] = set()
"""机器人昵称。"""
command_start: Set[str] = {"/"}
command_start: set[str] = {"/"}
"""命令的起始标记,用于判断一条消息是不是命令。
参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。
@@ -226,7 +438,7 @@ class Config(BaseConfig):
COMMAND_START=["/", ""]
```
"""
command_sep: Set[str] = {"."}
command_sep: set[str] = {"."}
"""命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。
参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。
@@ -241,9 +453,8 @@ class Config(BaseConfig):
用法:
```conf
SESSION_EXPIRE_TIMEOUT=120 # 单位: 秒
SESSION_EXPIRE_TIMEOUT=[DD ][HH:MM]SS[.ffffff]
SESSION_EXPIRE_TIMEOUT=P[DD]DT[HH]H[MM]M[SS]S # ISO 8601
SESSION_EXPIRE_TIMEOUT=[-][DD]D[,][HH:MM:]SS[.ffffff]
SESSION_EXPIRE_TIMEOUT=[±]P[DD]DT[HH]H[MM]M[SS]S # ISO 8601
```
"""
@@ -254,11 +465,21 @@ class Config(BaseConfig):
# custom configs can be assigned during nonebot.init
# or from env file using json loads
class Config:
env_file = ".env", ".env.prod"
if PYDANTIC_V2: # pragma: pydantic-v2
model_config = SettingsConfig(env_file=(".env", ".env.prod"))
else: # pragma: pydantic-v1
class Config( # pyright: ignore[reportIncompatibleVariableOverride]
SettingsConfig
):
env_file = ".env", ".env.prod"
__autodoc__ = {
"CustomEnvSettings": False,
"BaseConfig": False,
"SettingsError": False,
"BaseSettingsSource": False,
"InitSettingsSource": False,
"DotEnvSettingsSource": False,
"SettingsConfig": False,
"BaseSettings": False,
}

View File

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

View File

@@ -1,37 +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 typing import (
Any,
Dict,
List,
Type,
Tuple,
Generic,
TypeVar,
Callable,
Iterable,
Optional,
Awaitable,
cast,
)
from collections.abc import Iterable, Awaitable
from typing import Any, Generic, TypeVar, Callable, Optional, cast
from pydantic import BaseConfig
from pydantic.schema import get_annotation_from_field_info
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
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
@@ -51,13 +46,13 @@ class Param(abc.ABC, FieldInfo):
@classmethod
def _check_param(
cls, param: inspect.Parameter, allow_types: Tuple[Type["Param"], ...]
cls, param: inspect.Parameter, allow_types: tuple[type["Param"], ...]
) -> Optional["Param"]:
return
@classmethod
def _check_parameterless(
cls, value: Any, allow_types: Tuple[Type["Param"], ...]
cls, value: Any, allow_types: tuple[type["Param"], ...]
) -> Optional["Param"]:
return
@@ -69,10 +64,6 @@ class Param(abc.ABC, FieldInfo):
return
class CustomConfig(BaseConfig):
arbitrary_types_allowed = True
@dataclass(frozen=True)
class Dependent(Generic[R]):
"""依赖注入容器
@@ -86,8 +77,8 @@ class Dependent(Generic[R]):
"""
call: _DependentCallable[R]
params: Tuple[ModelField, ...] = field(default_factory=tuple)
parameterless: Tuple[Param, ...] = field(default_factory=tuple)
params: tuple[ModelField, ...] = field(default_factory=tuple)
parameterless: tuple[Param, ...] = field(default_factory=tuple)
def __repr__(self) -> str:
if inspect.isfunction(self.call) or inspect.isclass(self.call):
@@ -101,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)
@@ -113,24 +113,19 @@ 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(
call: _DependentCallable[R], allow_types: Tuple[Type[Param], ...]
) -> Tuple[ModelField, ...]:
fields: List[ModelField] = []
call: _DependentCallable[R], allow_types: tuple[type[Param], ...]
) -> tuple[ModelField, ...]:
fields: list[ModelField] = []
params = get_typed_signature(call).parameters.values()
for param in params:
default_value = Required
if param.default != param.empty:
default_value = param.default
if isinstance(default_value, Param):
field_info = default_value
if isinstance(param.default, Param):
field_info = param.default
else:
for allow_type in allow_types:
if field_info := allow_type._check_param(param, allow_types):
@@ -141,25 +136,13 @@ class Dependent(Generic[R]):
f"for function {call} with type {param.annotation}"
)
default_value = field_info.default
annotation: Any = Any
required = default_value == Required
if param.annotation != param.empty:
if param.annotation is not param.empty:
annotation = param.annotation
annotation = get_annotation_from_field_info(
annotation, field_info, param.name
)
fields.append(
ModelField(
name=param.name,
type_=annotation,
class_validators=None,
model_config=CustomConfig,
default=None if required else default_value,
required=required,
field_info=field_info,
ModelField.construct(
name=param.name, annotation=annotation, field_info=field_info
)
)
@@ -167,9 +150,9 @@ class Dependent(Generic[R]):
@staticmethod
def parse_parameterless(
parameterless: Tuple[Any, ...], allow_types: Tuple[Type[Param], ...]
) -> Tuple[Param, ...]:
parameterless_params: List[Param] = []
parameterless: tuple[Any, ...], allow_types: tuple[type[Param], ...]
) -> tuple[Param, ...]:
parameterless_params: list[Param] = []
for value in parameterless:
for allow_type in allow_types:
if param := allow_type._check_parameterless(value, allow_types):
@@ -185,7 +168,7 @@ class Dependent(Generic[R]):
*,
call: _DependentCallable[R],
parameterless: Optional[Iterable[Any]] = None,
allow_types: Iterable[Type[Param]],
allow_types: Iterable[type[Param]],
) -> "Dependent[R]":
allow_types = tuple(allow_types)
@@ -199,29 +182,48 @@ 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))
async def _solve_field(self, field: ModelField, params: Dict[str, Any]) -> Any:
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)
value = await param._solve(**params)
if value is Undefined:
if value is PydanticUndefined:
value = field.get_default()
v = check_field_type(field, value)
return v if param.validate else value
async def solve(self, **params: Any) -> Dict[str, Any]:
async def solve(self, **params: Any) -> dict[str, Any]:
# solve parameterless
for param in self.parameterless:
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,17 +1,19 @@
"""
FrontMatter:
mdx:
format: md
sidebar_position: 1
description: nonebot.dependencies.utils 模块
"""
import inspect
from typing import Any, Dict, Callable, ForwardRef
from typing import Any, Callable, ForwardRef
from loguru import logger
from pydantic.fields import ModelField
from pydantic.typing import evaluate_forwardref
from nonebot.compat import ModelField
from nonebot.exception import TypeMisMatch
from nonebot.typing import evaluate_forwardref
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
@@ -31,7 +33,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
return inspect.Signature(typed_params)
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
def get_typed_annotation(param: inspect.Parameter, globalns: dict[str, Any]) -> Any:
"""获取参数的类型注解"""
annotation = param.annotation
@@ -50,7 +52,7 @@ def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) ->
def check_field_type(field: ModelField, value: Any) -> Any:
"""检查字段类型是否匹配"""
v, errs_ = field.validate(value, {}, loc=())
if errs_:
try:
return field.validate_value(value)
except ValueError:
raise TypeMisMatch(field, value)
return v

View File

@@ -3,6 +3,8 @@
各驱动请继承以下基类。
FrontMatter:
mdx:
format: md
sidebar_position: 0
description: nonebot.drivers 模块
"""
@@ -23,6 +25,7 @@ from nonebot.internal.driver import ReverseDriver as ReverseDriver
from nonebot.internal.driver import combine_driver as combine_driver
from nonebot.internal.driver import HTTPClientMixin as HTTPClientMixin
from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup
from nonebot.internal.driver import HTTPClientSession as HTTPClientSession
from nonebot.internal.driver import WebSocketClientMixin as WebSocketClientMixin
from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup

View File

@@ -1,46 +0,0 @@
from typing_extensions import TypeAlias
from typing import Any, List, Union, Callable, Awaitable, cast
from nonebot.utils import run_sync, is_coroutine_callable
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
LIFESPAN_FUNC: TypeAlias = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
class Lifespan:
def __init__(self) -> None:
self._startup_funcs: List[LIFESPAN_FUNC] = []
self._shutdown_funcs: List[LIFESPAN_FUNC] = []
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
self._startup_funcs.append(func)
return func
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
self._shutdown_funcs.append(func)
return func
@staticmethod
async def _run_lifespan_func(
funcs: List[LIFESPAN_FUNC],
) -> None:
for func in funcs:
if is_coroutine_callable(func):
await cast(ASYNC_LIFESPAN_FUNC, func)()
else:
await run_sync(cast(SYNC_LIFESPAN_FUNC, func))()
async def startup(self) -> None:
if self._startup_funcs:
await self._run_lifespan_func(self._startup_funcs)
async def shutdown(self) -> None:
if self._shutdown_funcs:
await self._run_lifespan_func(self._shutdown_funcs)
async def __aenter__(self) -> None:
await self.startup()
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
await self.shutdown()

View File

@@ -11,21 +11,28 @@ pip install nonebot2[aiohttp]
:::
FrontMatter:
mdx:
format: md
sidebar_position: 2
description: nonebot.drivers.aiohttp 模块
"""
from typing_extensions import override
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, AsyncGenerator
from typing import TYPE_CHECKING, Union, Optional
from multidict import CIMultiDict
from nonebot.drivers import Request, Response
from nonebot.exception import WebSocketClosed
from nonebot.drivers import URL, Request, Response
from nonebot.drivers.none import Driver as NoneDriver
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.internal.driver import Cookies, QueryTypes, CookieTypes, HeaderTypes
from nonebot.drivers import (
HTTPVersion,
HTTPClientMixin,
HTTPClientSession,
WebSocketClientMixin,
combine_driver,
)
@@ -39,6 +46,107 @@ except ModuleNotFoundError as e: # pragma: no cover
) from e
class Session(HTTPClientSession):
@override
def __init__(
self,
params: QueryTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
version: Union[str, HTTPVersion] = HTTPVersion.H11,
timeout: Optional[float] = None,
proxy: Optional[str] = None,
):
self._client: Optional[aiohttp.ClientSession] = None
self._params = URL.build(query=params).query if params is not None else None
self._headers = CIMultiDict(headers) if headers is not None else None
self._cookies = tuple(
(cookie.name, cookie.value)
for cookie in Cookies(cookies)
if cookie.value is not None
)
version = HTTPVersion(version)
if version == HTTPVersion.H10:
self._version = aiohttp.HttpVersion10
elif version == HTTPVersion.H11:
self._version = aiohttp.HttpVersion11
else:
raise RuntimeError(f"Unsupported HTTP version: {version}")
self._timeout = timeout
self._proxy = proxy
@property
def client(self) -> aiohttp.ClientSession:
if self._client is None:
raise RuntimeError("Session is not initialized")
return self._client
@override
async def request(self, setup: Request) -> Response:
if self._params:
params = self._params.copy()
params.update(setup.url.query)
url = setup.url.with_query(params)
else:
url = setup.url
data = setup.data
if setup.files:
data = aiohttp.FormData(data or {}, quote_fields=False)
for name, file in setup.files:
data.add_field(name, file[1], content_type=file[2], filename=file[0])
cookies = (
(cookie.name, cookie.value)
for cookie in setup.cookies
if cookie.value is not None
)
timeout = aiohttp.ClientTimeout(setup.timeout)
async with await self.client.request(
setup.method,
url,
data=setup.content or data,
json=setup.json,
cookies=cookies,
headers=setup.headers,
proxy=setup.proxy or self._proxy,
timeout=timeout,
) as response:
return Response(
response.status,
headers=response.headers.copy(),
content=await response.read(),
request=setup,
)
@override
async def setup(self) -> None:
if self._client is not None:
raise RuntimeError("Session has already been initialized")
self._client = aiohttp.ClientSession(
cookies=self._cookies,
headers=self._headers,
version=self._version,
timeout=self._timeout,
trust_env=True,
)
await self._client.__aenter__()
@override
async def close(self) -> None:
try:
if self._client is not None:
await self._client.close()
finally:
self._client = None
class Mixin(HTTPClientMixin, WebSocketClientMixin):
"""AIOHTTP Mixin"""
@@ -49,42 +157,8 @@ class Mixin(HTTPClientMixin, WebSocketClientMixin):
@override
async def request(self, setup: Request) -> Response:
if setup.version == HTTPVersion.H10:
version = aiohttp.HttpVersion10
elif setup.version == HTTPVersion.H11:
version = aiohttp.HttpVersion11
else:
raise RuntimeError(f"Unsupported HTTP version: {setup.version}")
timeout = aiohttp.ClientTimeout(setup.timeout)
data = setup.data
if setup.files:
data = aiohttp.FormData(data or {})
for name, file in setup.files:
data.add_field(name, file[1], content_type=file[2], filename=file[0])
cookies = {
cookie.name: cookie.value for cookie in setup.cookies if cookie.value
}
async with aiohttp.ClientSession(
cookies=cookies, version=version, trust_env=True
) as session:
async with session.request(
setup.method,
setup.url,
data=setup.content or data,
json=setup.json,
headers=setup.headers,
timeout=timeout,
proxy=setup.proxy,
) as response:
return Response(
response.status,
headers=response.headers.copy(),
content=await response.read(),
request=setup,
)
async with self.get_session() as session:
return await session.request(setup)
@override
@asynccontextmanager
@@ -106,6 +180,25 @@ class Mixin(HTTPClientMixin, WebSocketClientMixin):
) as ws:
yield WebSocket(request=setup, session=session, websocket=ws)
@override
def get_session(
self,
params: QueryTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
version: Union[str, HTTPVersion] = HTTPVersion.H11,
timeout: Optional[float] = None,
proxy: Optional[str] = None,
) -> Session:
return Session(
params=params,
headers=headers,
cookies=cookies,
version=version,
timeout=timeout,
proxy=proxy,
)
class WebSocket(BaseWebSocket):
"""AIOHTTP Websocket Wrapper"""
@@ -131,8 +224,8 @@ class WebSocket(BaseWebSocket):
raise NotImplementedError
@override
async def close(self, code: int = 1000):
await self.websocket.close(code=code)
async def close(self, code: int = 1000, reason: str = ""):
await self.websocket.close(code=code, message=reason.encode("utf-8"))
await self.session.close()
async def _receive(self) -> aiohttp.WSMessage:
@@ -179,8 +272,7 @@ class WebSocket(BaseWebSocket):
if TYPE_CHECKING:
class Driver(Mixin, NoneDriver):
...
class Driver(Mixin, NoneDriver): ...
else:
Driver = combine_driver(NoneDriver, Mixin)

View File

@@ -11,18 +11,19 @@ pip install nonebot2[fastapi]
:::
FrontMatter:
mdx:
format: md
sidebar_position: 1
description: nonebot.drivers.fastapi 模块
"""
import logging
import contextlib
from functools import wraps
from typing_extensions import override
from typing import Any, Dict, List, Tuple, Union, Optional
from typing import Any, Union, Optional
from pydantic import BaseSettings
from pydantic import BaseModel
from nonebot.config import Env
from nonebot.drivers import ASGIMixin
@@ -32,10 +33,9 @@ from nonebot.drivers import Driver as BaseDriver
from nonebot.config import Config as NoneBotConfig
from nonebot.drivers import Request as BaseRequest
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.compat import model_dump, type_validate_python
from nonebot.drivers import HTTPServerSetup, WebSocketServerSetup
from ._lifespan import LIFESPAN_FUNC, Lifespan
try:
import uvicorn
from fastapi.responses import Response
@@ -61,7 +61,7 @@ def catch_closed(func):
return decorator
class Config(BaseSettings):
class Config(BaseModel):
"""FastAPI 驱动框架设置,详情参考 FastAPI 文档"""
fastapi_openapi_url: Optional[str] = None
@@ -74,20 +74,17 @@ class Config(BaseSettings):
"""是否包含适配器路由的 schema默认为 `True`"""
fastapi_reload: bool = False
"""开启/关闭冷重载"""
fastapi_reload_dirs: Optional[List[str]] = None
fastapi_reload_dirs: Optional[list[str]] = None
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
fastapi_reload_delay: float = 0.25
"""重载延迟,默认为 uvicorn 默认值"""
fastapi_reload_includes: Optional[List[str]] = None
fastapi_reload_includes: Optional[list[str]] = None
"""要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
fastapi_reload_excludes: Optional[List[str]] = None
fastapi_reload_excludes: Optional[list[str]] = None
"""不要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
fastapi_extra: Dict[str, Any] = {}
fastapi_extra: dict[str, Any] = {}
"""传递给 `FastAPI` 的其他参数。"""
class Config:
extra = "ignore"
class Driver(BaseDriver, ASGIMixin):
"""FastAPI 驱动框架。"""
@@ -95,9 +92,7 @@ class Driver(BaseDriver, ASGIMixin):
def __init__(self, env: Env, config: NoneBotConfig):
super().__init__(env, config)
self.fastapi_config: Config = Config(**config.dict())
self._lifespan = Lifespan()
self.fastapi_config: Config = type_validate_python(Config, model_dump(config))
self._server_app = FastAPI(
lifespan=self._lifespan_manager,
@@ -155,14 +150,6 @@ class Driver(BaseDriver, ASGIMixin):
name=setup.name,
)
@override
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
return self._lifespan.on_startup(func)
@override
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
return self._lifespan.on_shutdown(func)
@contextlib.asynccontextmanager
async def _lifespan_manager(self, app: FastAPI):
await self._lifespan.startup()
@@ -176,7 +163,7 @@ class Driver(BaseDriver, ASGIMixin):
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
*args,
app: Optional[str] = None,
**kwargs,
):
@@ -221,7 +208,7 @@ class Driver(BaseDriver, ASGIMixin):
json = await request.json()
data: Optional[dict] = None
files: Optional[List[Tuple[str, FileTypes]]] = None
files: Optional[list[tuple[str, FileTypes]]] = None
with contextlib.suppress(Exception):
form = await request.form()
data = {}

View File

@@ -11,19 +11,26 @@ pip install nonebot2[httpx]
:::
FrontMatter:
mdx:
format: md
sidebar_position: 3
description: nonebot.drivers.httpx 模块
"""
from typing import TYPE_CHECKING
from typing_extensions import override
from typing import TYPE_CHECKING, Union, Optional
from multidict import CIMultiDict
from nonebot.drivers.none import Driver as NoneDriver
from nonebot.internal.driver import Cookies, QueryTypes, CookieTypes, HeaderTypes
from nonebot.drivers import (
URL,
Request,
Response,
HTTPVersion,
HTTPClientMixin,
HTTPClientSession,
combine_driver,
)
@@ -36,6 +43,79 @@ except ModuleNotFoundError as e: # pragma: no cover
) from e
class Session(HTTPClientSession):
@override
def __init__(
self,
params: QueryTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
version: Union[str, HTTPVersion] = HTTPVersion.H11,
timeout: Optional[float] = None,
proxy: Optional[str] = None,
):
self._client: Optional[httpx.AsyncClient] = None
self._params = (
tuple(URL.build(query=params).query.items()) if params is not None else None
)
self._headers = (
tuple(CIMultiDict(headers).items()) if headers is not None else None
)
self._cookies = Cookies(cookies)
self._version = HTTPVersion(version)
self._timeout = timeout
self._proxy = proxy
@property
def client(self) -> httpx.AsyncClient:
if self._client is None:
raise RuntimeError("Session is not initialized")
return self._client
@override
async def request(self, setup: Request) -> Response:
response = await self.client.request(
setup.method,
str(setup.url),
content=setup.content,
data=setup.data,
files=setup.files,
json=setup.json,
headers=tuple(setup.headers.items()),
cookies=setup.cookies.jar,
timeout=setup.timeout,
)
return Response(
response.status_code,
headers=response.headers.multi_items(),
content=response.content,
request=setup,
)
@override
async def setup(self) -> None:
if self._client is not None:
raise RuntimeError("Session has already been initialized")
self._client = httpx.AsyncClient(
params=self._params,
headers=self._headers,
cookies=self._cookies.jar,
http2=self._version == HTTPVersion.H2,
proxies=self._proxy,
follow_redirects=True,
)
await self._client.__aenter__()
@override
async def close(self) -> None:
try:
if self._client is not None:
await self._client.aclose()
finally:
self._client = None
class Mixin(HTTPClientMixin):
"""HTTPX Mixin"""
@@ -46,34 +126,34 @@ class Mixin(HTTPClientMixin):
@override
async def request(self, setup: Request) -> Response:
async with httpx.AsyncClient(
cookies=setup.cookies.jar,
http2=setup.version == HTTPVersion.H2,
proxies=setup.proxy,
follow_redirects=True,
) as client:
response = await client.request(
setup.method,
str(setup.url),
content=setup.content,
data=setup.data,
json=setup.json,
files=setup.files,
headers=tuple(setup.headers.items()),
timeout=setup.timeout,
)
return Response(
response.status_code,
headers=response.headers.multi_items(),
content=response.content,
request=setup,
)
async with self.get_session(
version=setup.version, proxy=setup.proxy
) as session:
return await session.request(setup)
@override
def get_session(
self,
params: QueryTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
version: Union[str, HTTPVersion] = HTTPVersion.H11,
timeout: Optional[float] = None,
proxy: Optional[str] = None,
) -> Session:
return Session(
params=params,
headers=headers,
cookies=cookies,
version=version,
timeout=timeout,
proxy=proxy,
)
if TYPE_CHECKING:
class Driver(Mixin, NoneDriver):
...
class Driver(Mixin, NoneDriver): ...
else:
Driver = combine_driver(NoneDriver, Mixin)

View File

@@ -5,21 +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 ._lifespan import LIFESPAN_FUNC, Lifespan
from nonebot.utils import flatten_exception_group
HANDLED_SIGNALS = (
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
@@ -35,10 +39,8 @@ class Driver(BaseDriver):
def __init__(self, env: Env, config: Config):
super().__init__(env, config)
self._lifespan = Lifespan()
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,97 +54,102 @@ class Driver(BaseDriver):
"""none driver 使用的 logger"""
return logger
@override
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""注册一个启动时执行的函数"""
return self._lifespan.on_startup(func)
@override
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""注册一个停止时执行的函数"""
return self._lifespan.on_shutdown(func)
@override
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>Error when running startup function. "
"Ignored!</bg #f8bbd0></r>"
)
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
@@ -152,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 模块
"""
@@ -18,20 +20,9 @@ FrontMatter:
import asyncio
from functools import wraps
from typing_extensions import override
from typing import (
Any,
Dict,
List,
Tuple,
Union,
TypeVar,
Callable,
Optional,
Coroutine,
cast,
)
from typing import Any, Union, Optional, cast
from pydantic import BaseSettings
from pydantic import BaseModel
from nonebot.config import Env
from nonebot.drivers import ASGIMixin
@@ -41,6 +32,7 @@ from nonebot.drivers import Driver as BaseDriver
from nonebot.config import Config as NoneBotConfig
from nonebot.drivers import Request as BaseRequest
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.compat import model_dump, type_validate_python
from nonebot.drivers import HTTPServerSetup, WebSocketServerSetup
try:
@@ -57,8 +49,6 @@ except ModuleNotFoundError as e: # pragma: no cover
"Install with pip: `pip install nonebot2[quart]`"
) from e
_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine])
def catch_closed(func):
@wraps(func)
@@ -71,25 +61,22 @@ def catch_closed(func):
return decorator
class Config(BaseSettings):
class Config(BaseModel):
"""Quart 驱动框架设置"""
quart_reload: bool = False
"""开启/关闭冷重载"""
quart_reload_dirs: Optional[List[str]] = None
quart_reload_dirs: Optional[list[str]] = None
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
quart_reload_delay: float = 0.25
"""重载延迟,默认为 uvicorn 默认值"""
quart_reload_includes: Optional[List[str]] = None
quart_reload_includes: Optional[list[str]] = None
"""要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
quart_reload_excludes: Optional[List[str]] = None
quart_reload_excludes: Optional[list[str]] = None
"""不要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
quart_extra: Dict[str, Any] = {}
quart_extra: dict[str, Any] = {}
"""传递给 `Quart` 的其他参数。"""
class Config:
extra = "ignore"
class Driver(BaseDriver, ASGIMixin):
"""Quart 驱动框架"""
@@ -97,11 +84,13 @@ class Driver(BaseDriver, ASGIMixin):
def __init__(self, env: Env, config: NoneBotConfig):
super().__init__(env, config)
self.quart_config = Config(**config.dict())
self.quart_config = type_validate_python(Config, model_dump(config))
self._server_app = Quart(
self.__class__.__qualname__, **self.quart_config.quart_extra
)
self._server_app.before_serving(self._lifespan.startup)
self._server_app.after_serving(self._lifespan.shutdown)
@property
@override
@@ -150,22 +139,12 @@ class Driver(BaseDriver, ASGIMixin):
view_func=_handle,
)
@override
def on_startup(self, func: _AsyncCallable) -> _AsyncCallable:
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
return self.server_app.before_serving(func) # type: ignore
@override
def on_shutdown(self, func: _AsyncCallable) -> _AsyncCallable:
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
return self.server_app.after_serving(func) # type: ignore
@override
def run(
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
*args,
app: Optional[str] = None,
**kwargs,
):
@@ -207,7 +186,7 @@ class Driver(BaseDriver, ASGIMixin):
data = await request.form
files_dict = await request.files
files: List[Tuple[str, FileTypes]] = []
files: list[tuple[str, FileTypes]] = []
key: str
value: FileStorage
for key, value in files_dict.items():

View File

@@ -11,6 +11,8 @@ pip install nonebot2[websockets]
:::
FrontMatter:
mdx:
format: md
sidebar_position: 4
description: nonebot.drivers.websockets 模块
"""
@@ -19,7 +21,8 @@ import logging
from functools import wraps
from contextlib import asynccontextmanager
from typing_extensions import ParamSpec, override
from typing import TYPE_CHECKING, Union, TypeVar, Callable, Awaitable, AsyncGenerator
from collections.abc import Coroutine, AsyncGenerator
from typing import TYPE_CHECKING, Any, Union, TypeVar, Callable
from nonebot.drivers import Request
from nonebot.log import LoguruHandler
@@ -44,16 +47,15 @@ logger = logging.Logger("websockets.client", "INFO")
logger.addHandler(LoguruHandler())
def catch_closed(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
def catch_closed(
func: Callable[P, Coroutine[Any, Any, T]]
) -> Callable[P, Coroutine[Any, Any, T]]:
@wraps(func)
async def decorator(*args: P.args, **kwargs: P.kwargs) -> T:
try:
return await func(*args, **kwargs)
except ConnectionClosed as e:
if e.rcvd_then_sent:
raise WebSocketClosed(e.rcvd.code, e.rcvd.reason) # type: ignore
else:
raise WebSocketClosed(e.sent.code, e.sent.reason) # type: ignore
raise WebSocketClosed(e.code, e.reason)
return decorator
@@ -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)},
@@ -131,8 +135,7 @@ class WebSocket(BaseWebSocket):
if TYPE_CHECKING:
class Driver(Mixin, NoneDriver):
...
class Driver(Mixin, NoneDriver): ...
else:
Driver = combine_driver(NoneDriver, Mixin)

View File

@@ -25,13 +25,15 @@ NoneBotException
```
FrontMatter:
mdx:
format: md
sidebar_position: 10
description: nonebot.exception 模块
"""
from typing import Any, Optional
from pydantic.fields import ModelField
from nonebot.compat import ModelField
class NoneBotException(Exception):

View File

@@ -1,8 +1,10 @@
import abc
from typing import Any
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from typing import Any, Dict, AsyncGenerator
from nonebot.config import Config
from nonebot.internal.driver._lifespan import LIFESPAN_FUNC
from nonebot.internal.driver import (
Driver,
Request,
@@ -31,7 +33,7 @@ class Adapter(abc.ABC):
def __init__(self, driver: Driver, **kwargs: Any):
self.driver: Driver = driver
"""{ref}`nonebot.drivers.Driver` 实例"""
self.bots: Dict[str, Bot] = {}
self.bots: dict[str, Bot] = {}
"""本协议适配器已建立连接的 {ref}`nonebot.adapters.Bot` 实例"""
def __repr__(self) -> str:
@@ -97,6 +99,9 @@ class Adapter(abc.ABC):
async with self.driver.websocket(setup) as ws:
yield ws
def on_ready(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
return self.driver._lifespan.on_ready(func)
@abc.abstractmethod
async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:
"""`Adapter` 实际调用 api 的逻辑实现函数,实现该方法以调用 api。

View File

@@ -1,11 +1,14 @@
import abc
import asyncio
from functools import partial
from typing import TYPE_CHECKING, Any, Set, Union, Optional, Protocol
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:
@@ -14,8 +17,7 @@ if TYPE_CHECKING:
from .message import Message, MessageSegment
class _ApiCall(Protocol):
async def __call__(self, **kwargs: Any) -> Any:
...
async def __call__(self, **kwargs: Any) -> Any: ...
class Bot(abc.ABC):
@@ -28,9 +30,9 @@ class Bot(abc.ABC):
self_id: 机器人 ID
"""
_calling_api_hook: Set[T_CallingAPIHook] = set()
_calling_api_hook: ClassVar[set[T_CallingAPIHook]] = set()
"""call_api 时执行的函数"""
_called_api_hook: Set[T_CalledAPIHook] = set()
_called_api_hook: ClassVar[set[T_CalledAPIHook]] = set()
"""call_api 后执行的函数"""
def __init__(self, adapter: "Adapter", self_id: str):
@@ -77,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,9 +1,10 @@
import abc
from typing import Any, Type, TypeVar
from typing import Any, TypeVar
from pydantic import BaseModel
from nonebot.utils import DataclassEncoder
from nonebot.compat import PYDANTIC_V2, ConfigDict
from .message import Message
@@ -13,15 +14,21 @@ E = TypeVar("E", bound="Event")
class Event(abc.ABC, BaseModel):
"""Event 基类。提供获取关键信息的方法,其余信息可直接获取。"""
class Config:
extra = "allow"
json_encoders = {Message: DataclassEncoder}
if PYDANTIC_V2: # pragma: pydantic-v2
model_config = ConfigDict(extra="allow")
else: # pragma: pydantic-v1
@classmethod
def validate(cls: Type["E"], value: Any) -> "E":
if isinstance(value, Event) and not isinstance(value, cls):
raise TypeError(f"{value} is incompatible with Event type {cls}")
return super().validate(value)
class Config(ConfigDict):
extra = "allow" # type: ignore
json_encoders = {Message: DataclassEncoder} # noqa: RUF012
if not PYDANTIC_V2: # pragma: pydantic-v1
@classmethod
def validate(cls: type["E"], value: Any) -> "E":
if isinstance(value, Event) and not isinstance(value, cls):
raise TypeError(f"{value} is incompatible with Event type {cls}")
return super().validate(value)
@abc.abstractmethod
def get_type(self) -> str:

View File

@@ -1,23 +1,20 @@
import abc
from copy import deepcopy
from typing_extensions import Self
from collections.abc import Iterable
from dataclasses import field, asdict, dataclass
from typing import (
from typing import ( # noqa: UP035
Any,
Dict,
List,
Type,
Tuple,
Union,
Generic,
TypeVar,
Iterable,
Optional,
SupportsIndex,
overload,
)
from pydantic import parse_obj_as
from nonebot.compat import custom_validation, type_validate_python
from .template import MessageTemplate
@@ -25,18 +22,19 @@ TMS = TypeVar("TMS", bound="MessageSegment")
TM = TypeVar("TM", bound="Message")
@custom_validation
@dataclass
class MessageSegment(abc.ABC, Generic[TM]):
"""消息段基类"""
type: str
"""消息段类型"""
data: Dict[str, Any] = field(default_factory=dict)
data: dict[str, Any] = field(default_factory=dict)
"""消息段数据"""
@classmethod
@abc.abstractmethod
def get_message_class(cls) -> Type[TM]:
def get_message_class(cls) -> Type[TM]: # noqa: UP006
"""获取消息数组类型"""
raise NotImplementedError
@@ -48,7 +46,9 @@ class MessageSegment(abc.ABC, Generic[TM]):
def __len__(self) -> int:
return len(str(self))
def __ne__(self, other: Self) -> bool:
def __ne__( # pyright: ignore[reportIncompatibleMethodOverride]
self, other: Self
) -> bool:
return not self == other
def __add__(self: TMS, other: Union[str, TMS, Iterable[TMS]]) -> TM:
@@ -65,6 +65,8 @@ class MessageSegment(abc.ABC, Generic[TM]):
def _validate(cls, value) -> Self:
if isinstance(value, cls):
return value
if isinstance(value, MessageSegment):
raise ValueError(f"Type {type(value)} can not be converted to {cls}")
if not isinstance(value, dict):
raise ValueError(f"Expected dict for MessageSegment, got {type(value)}")
if "type" not in value:
@@ -97,7 +99,8 @@ class MessageSegment(abc.ABC, Generic[TM]):
raise NotImplementedError
class Message(List[TMS], abc.ABC):
@custom_validation
class Message(list[TMS], abc.ABC):
"""消息序列
参数:
@@ -138,7 +141,7 @@ class Message(List[TMS], abc.ABC):
@classmethod
@abc.abstractmethod
def get_segment_class(cls) -> Type[TMS]:
def get_segment_class(cls) -> type[TMS]:
"""获取消息段类型"""
raise NotImplementedError
@@ -158,9 +161,9 @@ class Message(List[TMS], abc.ABC):
elif isinstance(value, str):
pass
elif isinstance(value, dict):
value = parse_obj_as(cls.get_segment_class(), value)
value = type_validate_python(cls.get_segment_class(), value)
elif isinstance(value, Iterable):
value = [parse_obj_as(cls.get_segment_class(), v) for v in value]
value = [type_validate_python(cls.get_segment_class(), v) for v in value]
else:
raise ValueError(
f"Expected str, dict or iterable for Message, got {type(value)}"
@@ -173,7 +176,9 @@ class Message(List[TMS], abc.ABC):
"""构造消息数组"""
raise NotImplementedError
def __add__(self, other: Union[str, TMS, Iterable[TMS]]) -> Self:
def __add__( # pyright: ignore[reportIncompatibleMethodOverride]
self, other: Union[str, TMS, Iterable[TMS]]
) -> Self:
result = self.copy()
result += other
return result
@@ -205,7 +210,7 @@ class Message(List[TMS], abc.ABC):
"""
@overload
def __getitem__(self, args: Tuple[str, int]) -> TMS:
def __getitem__(self, args: tuple[str, int]) -> TMS:
"""索引指定类型的消息段
参数:
@@ -216,7 +221,7 @@ class Message(List[TMS], abc.ABC):
"""
@overload
def __getitem__(self, args: Tuple[str, slice]) -> Self:
def __getitem__(self, args: tuple[str, slice]) -> Self:
"""切片指定类型的消息段
参数:
@@ -248,12 +253,12 @@ class Message(List[TMS], abc.ABC):
消息切片 `args`
"""
def __getitem__(
def __getitem__( # pyright: ignore[reportIncompatibleMethodOverride]
self,
args: Union[
str,
Tuple[str, int],
Tuple[str, slice],
tuple[str, int],
tuple[str, slice],
int,
slice,
],
@@ -272,7 +277,9 @@ class Message(List[TMS], abc.ABC):
else:
raise ValueError("Incorrect arguments to slice") # pragma: no cover
def __contains__(self, value: Union[TMS, str]) -> bool:
def __contains__( # pyright: ignore[reportIncompatibleMethodOverride]
self, value: Union[TMS, str]
) -> bool:
"""检查消息段是否存在
参数:
@@ -281,7 +288,7 @@ class Message(List[TMS], abc.ABC):
消息内是否存在给定消息段或给定类型的消息段
"""
if isinstance(value, str):
return bool(next((seg for seg in self if seg.type == value), None))
return next((seg for seg in self if seg.type == value), None) is not None
return super().__contains__(value)
def has(self, value: Union[TMS, str]) -> bool:
@@ -355,7 +362,9 @@ class Message(List[TMS], abc.ABC):
return all(seg.type == value for seg in self)
return all(seg == value for seg in self)
def append(self, obj: Union[str, TMS]) -> Self:
def append( # pyright: ignore[reportIncompatibleMethodOverride]
self, obj: Union[str, TMS]
) -> Self:
"""添加一个消息段到消息数组末尾。
参数:
@@ -369,7 +378,9 @@ class Message(List[TMS], abc.ABC):
raise ValueError(f"Unexpected type: {type(obj)} {obj}") # pragma: no cover
return self
def extend(self, obj: Union[Self, Iterable[TMS]]) -> Self:
def extend( # pyright: ignore[reportIncompatibleMethodOverride]
self, obj: Union[Self, Iterable[TMS]]
) -> Self:
"""拼接一个消息数组或多个消息段到消息数组末尾。
参数:

View File

@@ -1,28 +1,29 @@
import functools
from string import Formatter
from typing_extensions import TypeAlias
from collections.abc import Mapping, Sequence
from typing import (
TYPE_CHECKING,
Any,
Set,
Dict,
List,
Type,
Tuple,
Union,
Generic,
Mapping,
TypeVar,
Callable,
Optional,
Sequence,
cast,
overload,
)
from _string import formatter_field_name_split # type: ignore
if TYPE_CHECKING:
from .message import Message, MessageSegment
def formatter_field_name_split(
field_name: str,
) -> tuple[str, list[tuple[bool, str]]]: ...
TM = TypeVar("TM", bound="Message")
TF = TypeVar("TF", str, "Message")
@@ -36,26 +37,35 @@ class MessageTemplate(Formatter, Generic[TF]):
参数:
template: 模板
factory: 消息类型工厂,默认为 `str`
private_getattr: 是否允许在模板中访问私有属性,默认为 `False`
"""
@overload
def __init__(
self: "MessageTemplate[str]", template: str, factory: Type[str] = str
) -> None:
...
self: "MessageTemplate[str]",
template: str,
factory: type[str] = str,
private_getattr: bool = False,
) -> None: ...
@overload
def __init__(
self: "MessageTemplate[TM]", template: Union[str, TM], factory: Type[TM]
) -> None:
...
self: "MessageTemplate[TM]",
template: Union[str, TM],
factory: type[TM],
private_getattr: bool = False,
) -> None: ...
def __init__(
self, template: Union[str, TM], factory: Union[Type[str], Type[TM]] = str
self,
template: Union[str, TM],
factory: Union[type[str], type[TM]] = str,
private_getattr: bool = False,
) -> None:
self.template: TF = template # type: ignore
self.factory: Type[TF] = factory # type: ignore
self.format_specs: Dict[str, FormatSpecFunc] = {}
self.factory: type[TF] = factory # type: ignore
self.format_specs: dict[str, FormatSpecFunc] = {}
self.private_getattr = private_getattr
def __repr__(self) -> str:
return f"MessageTemplate({self.template!r}, factory={self.factory!r})"
@@ -69,7 +79,9 @@ class MessageTemplate(Formatter, Generic[TF]):
self.format_specs[name] = spec
return spec
def format(self, *args, **kwargs):
def format( # pyright: ignore[reportIncompatibleMethodOverride]
self, *args, **kwargs
) -> TF:
"""根据传入参数和模板生成消息对象"""
return self._format(args, kwargs)
@@ -102,7 +114,7 @@ class MessageTemplate(Formatter, Generic[TF]):
self.check_unused_args(used_args, args, kwargs)
return cast(TF, full_message)
def vformat(
def vformat( # pyright: ignore[reportIncompatibleMethodOverride]
self,
format_string: str,
args: Sequence[Any],
@@ -110,15 +122,15 @@ class MessageTemplate(Formatter, Generic[TF]):
) -> TF:
raise NotImplementedError("`vformat` has merged into `_format`")
def _vformat(
def _vformat( # pyright: ignore[reportIncompatibleMethodOverride]
self,
format_string: str,
args: Sequence[Any],
kwargs: Mapping[str, Any],
used_args: Set[Union[int, str]],
used_args: set[Union[int, str]],
auto_arg_index: int = 0,
) -> Tuple[TF, int]:
results: List[Any] = [self.factory()]
) -> tuple[TF, int]:
results: list[Any] = [self.factory()]
for literal_text, field_name, format_spec, conversion in self.parse(
format_string
@@ -167,10 +179,23 @@ class MessageTemplate(Formatter, Generic[TF]):
return functools.reduce(self._add, results), auto_arg_index
def get_field(
self, field_name: str, args: Sequence[Any], kwargs: Mapping[str, Any]
) -> tuple[Any, Union[int, str]]:
first, rest = formatter_field_name_split(field_name)
obj = self.get_value(first, args, kwargs)
for is_attr, value in rest:
if not self.private_getattr and value.startswith("_"):
raise ValueError("Cannot access private attribute")
obj = getattr(obj, value) if is_attr else obj[value]
return obj, first
def format_field(self, value: Any, format_spec: str) -> Any:
formatter: Optional[FormatSpecFunc] = self.format_specs.get(format_spec)
if formatter is None and not issubclass(self.factory, str):
segment_class: Type["MessageSegment"] = self.factory.get_segment_class()
segment_class: type["MessageSegment"] = self.factory.get_segment_class()
method = getattr(segment_class, format_spec, None)
if callable(method) and not cast(str, method.__name__).startswith("_"):
formatter = getattr(segment_class, format_spec)

View File

@@ -26,5 +26,6 @@ from .abstract import ReverseDriver as ReverseDriver
from .combine import combine_driver as combine_driver
from .model import HTTPServerSetup as HTTPServerSetup
from .abstract import HTTPClientMixin as HTTPClientMixin
from .abstract import HTTPClientSession as HTTPClientSession
from .model import WebSocketServerSetup as WebSocketServerSetup
from .abstract import WebSocketClientMixin as WebSocketClientMixin

View File

@@ -0,0 +1,100 @@
from types import TracebackType
from typing_extensions import TypeAlias
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
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
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
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
self._shutdown_funcs.append(func)
return func
def on_ready(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
self._ready_funcs.append(func)
return func
@staticmethod
async def _run_lifespan_func(
funcs: Iterable[LIFESPAN_FUNC],
) -> None:
for func in funcs:
if is_coroutine_callable(func):
await cast(ASYNC_LIFESPAN_FUNC, func)()
else:
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,
*,
exc_type: Optional[type[BaseException]] = None,
exc_val: Optional[BaseException] = None,
exc_tb: Optional[TracebackType] = None,
) -> None:
if 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: 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,22 +1,38 @@
import abc
import asyncio
from typing_extensions import TypeAlias
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, Set, Dict, Type, Callable, AsyncGenerator
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,
T_BotDisconnectionHook,
)
from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup
from ._lifespan import LIFESPAN_FUNC, Lifespan
from .model import (
Request,
Response,
WebSocket,
QueryTypes,
CookieTypes,
HeaderTypes,
HTTPVersion,
HTTPServerSetup,
WebSocketServerSetup,
)
if TYPE_CHECKING:
from nonebot.internal.adapter import Bot, Adapter
@@ -35,11 +51,11 @@ class Driver(abc.ABC):
config: 包含配置信息的 Config 对象
"""
_adapters: Dict[str, "Adapter"] = {}
_adapters: ClassVar[dict[str, "Adapter"]] = {}
"""已注册的适配器列表"""
_bot_connection_hook: Set[Dependent[Any]] = set()
_bot_connection_hook: ClassVar[set[Dependent[Any]]] = set()
"""Bot 连接建立时执行的函数"""
_bot_disconnection_hook: Set[Dependent[Any]] = set()
_bot_disconnection_hook: ClassVar[set[Dependent[Any]]] = set()
"""Bot 连接断开时执行的函数"""
def __init__(self, env: Env, config: Config):
@@ -47,8 +63,8 @@ class Driver(abc.ABC):
"""环境名称"""
self.config: Config = config
"""全局配置对象"""
self._bots: Dict[str, "Bot"] = {}
self._bot_tasks: Set[asyncio.Task] = set()
self._bots: dict[str, "Bot"] = {}
self._lifespan = Lifespan()
def __repr__(self) -> str:
return (
@@ -57,11 +73,15 @@ class Driver(abc.ABC):
)
@property
def bots(self) -> Dict[str, "Bot"]:
def bots(self) -> dict[str, "Bot"]:
"""获取当前所有已连接的 Bot"""
return self._bots
def register_adapter(self, adapter: Type["Adapter"], **kwargs) -> None:
@property
def task_group(self) -> TaskGroup:
return self._lifespan.task_group
def register_adapter(self, adapter: type["Adapter"], **kwargs) -> None:
"""注册一个协议适配器
参数:
@@ -98,17 +118,13 @@ 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)
@abc.abstractmethod
def on_startup(self, func: Callable) -> Callable:
"""注册一个在驱动器启动时执行的函数"""
raise NotImplementedError
@abc.abstractmethod
def on_shutdown(self, func: Callable) -> Callable:
"""注册一个在驱动器停止时执行的函数"""
raise NotImplementedError
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""注册一个停止时执行的函数"""
return self._lifespan.on_shutdown(func)
@classmethod
def on_bot_connect(cls, func: T_BotConnectionHook) -> T_BotConnectionHook:
@@ -142,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):
@@ -222,6 +235,49 @@ class ReverseMixin(Mixin):
"""服务端混入基类。"""
class HTTPClientSession(abc.ABC):
"""HTTP 客户端会话基类。"""
@abc.abstractmethod
def __init__(
self,
params: QueryTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
version: Union[str, HTTPVersion] = HTTPVersion.H11,
timeout: Optional[float] = None,
proxy: Optional[str] = None,
):
raise NotImplementedError
@abc.abstractmethod
async def request(self, setup: Request) -> Response:
"""发送一个 HTTP 请求"""
raise NotImplementedError
@abc.abstractmethod
async def setup(self) -> None:
"""初始化会话"""
raise NotImplementedError
@abc.abstractmethod
async def close(self) -> None:
"""关闭会话"""
raise NotImplementedError
async def __aenter__(self) -> Self:
await self.setup()
return self
async def __aexit__(
self,
exc_type: Optional[type[BaseException]],
exc: Optional[BaseException],
tb: Optional[TracebackType],
) -> None:
await self.close()
class HTTPClientMixin(ForwardMixin):
"""HTTP 客户端混入基类。"""
@@ -230,6 +286,19 @@ class HTTPClientMixin(ForwardMixin):
"""发送一个 HTTP 请求"""
raise NotImplementedError
@abc.abstractmethod
def get_session(
self,
params: QueryTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
version: Union[str, HTTPVersion] = HTTPVersion.H11,
timeout: Optional[float] = None,
proxy: Optional[str] = None,
) -> HTTPClientSession:
"""获取一个 HTTP 会话"""
raise NotImplementedError
class WebSocketClientMixin(ForwardMixin):
"""WebSocket 客户端混入基类。"""

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Type, Union, TypeVar, overload
from typing import TYPE_CHECKING, Union, TypeVar, overload
from .abstract import Mixin, Driver
@@ -6,23 +6,22 @@ D = TypeVar("D", bound="Driver")
if TYPE_CHECKING:
class CombinedDriver(Driver, Mixin):
...
class CombinedDriver(Driver, Mixin): ...
@overload
def combine_driver(driver: Type[D]) -> Type[D]:
...
def combine_driver(driver: type[D]) -> type[D]: ...
@overload
def combine_driver(driver: Type[D], *mixins: Type[Mixin]) -> Type["CombinedDriver"]:
...
def combine_driver(
driver: type[D], __m: type[Mixin], /, *mixins: type[Mixin]
) -> type["CombinedDriver"]: ...
def combine_driver(
driver: Type[D], *mixins: Type[Mixin]
) -> Union[Type[D], Type["CombinedDriver"]]:
driver: type[D], *mixins: type[Mixin]
) -> Union[type[D], type["CombinedDriver"]]:
"""将一个驱动器和多个混入类合并。"""
# check first
if not issubclass(driver, Driver):
@@ -35,9 +34,9 @@ def combine_driver(
def type_(self: "CombinedDriver") -> str:
return (
driver.type.__get__(self)
driver.type.__get__(self) # type: ignore
+ "+"
+ "+".join(x.type.__get__(self) for x in mixins)
+ "+".join(x.type.__get__(self) for x in mixins) # type: ignore
)
return type(

View File

@@ -4,56 +4,44 @@ from enum import Enum
from dataclasses import dataclass
from typing_extensions import TypeAlias
from http.cookiejar import Cookie, CookieJar
from typing import (
IO,
Any,
Dict,
List,
Tuple,
Union,
Mapping,
Callable,
Iterator,
Optional,
Awaitable,
MutableMapping,
)
from typing import IO, Any, Union, Callable, Optional
from collections.abc import Mapping, Iterator, Awaitable, MutableMapping
from yarl import URL as URL
from multidict import CIMultiDict
RawURL: TypeAlias = Tuple[bytes, bytes, Optional[int], bytes]
RawURL: TypeAlias = tuple[bytes, bytes, Optional[int], bytes]
SimpleQuery: TypeAlias = Union[str, int, float]
QueryVariable: TypeAlias = Union[SimpleQuery, List[SimpleQuery]]
QueryVariable: TypeAlias = Union[SimpleQuery, list[SimpleQuery]]
QueryTypes: TypeAlias = Union[
None, str, Mapping[str, QueryVariable], List[Tuple[str, QueryVariable]]
None, str, Mapping[str, QueryVariable], list[tuple[str, SimpleQuery]]
]
HeaderTypes: TypeAlias = Union[
None,
CIMultiDict[str],
Dict[str, str],
List[Tuple[str, str]],
dict[str, str],
list[tuple[str, str]],
]
CookieTypes: TypeAlias = Union[
None, "Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]
None, "Cookies", CookieJar, dict[str, str], list[tuple[str, str]]
]
ContentTypes: TypeAlias = Union[str, bytes, None]
DataTypes: TypeAlias = Union[dict, None]
FileContent: TypeAlias = Union[IO[bytes], bytes]
FileType: TypeAlias = Tuple[Optional[str], FileContent, Optional[str]]
FileType: TypeAlias = tuple[Optional[str], FileContent, Optional[str]]
FileTypes: TypeAlias = Union[
# file (or bytes)
FileContent,
# (filename, file (or bytes))
Tuple[Optional[str], FileContent],
tuple[Optional[str], FileContent],
# (filename, file (or bytes), content_type)
FileType,
]
FilesTypes: TypeAlias = Union[Dict[str, FileTypes], List[Tuple[str, FileTypes]], None]
FilesTypes: TypeAlias = Union[dict[str, FileTypes], list[tuple[str, FileTypes]], None]
class HTTPVersion(Enum):
@@ -119,7 +107,7 @@ class Request:
self.content: ContentTypes = content
self.data: DataTypes = data
self.json: Any = json
self.files: Optional[List[Tuple[str, FileType]]] = None
self.files: Optional[list[tuple[str, FileType]]] = None
if files:
self.files = []
files_ = files.items() if isinstance(files, dict) else files
@@ -257,7 +245,7 @@ class Cookies(MutableMapping):
)
self.jar.set_cookie(cookie)
def get(
def get( # pyright: ignore[reportIncompatibleMethodOverride]
self,
name: str,
default: Optional[str] = None,
@@ -298,12 +286,14 @@ class Cookies(MutableMapping):
def clear(self, domain: Optional[str] = None, path: Optional[str] = None) -> None:
self.jar.clear(domain, path)
def update(self, cookies: CookieTypes = None) -> None:
def update( # pyright: ignore[reportIncompatibleMethodOverride]
self, cookies: CookieTypes = None
) -> None:
cookies = Cookies(cookies)
for cookie in cookies.jar:
self.jar.set_cookie(cookie)
def as_header(self, request: Request) -> Dict[str, str]:
def as_header(self, request: Request) -> dict[str, str]:
urllib_request = self._CookieCompatRequest(request)
self.jar.add_cookie_header(urllib_request)
return urllib_request.added_headers
@@ -341,9 +331,11 @@ class Cookies(MutableMapping):
method=request.method,
)
self.request = request
self.added_headers: Dict[str, str] = {}
self.added_headers: dict[str, str] = {}
def add_unredirected_header(self, key: str, value: str) -> None:
def add_unredirected_header( # pyright: ignore[reportIncompatibleMethodOverride]
self, key: str, value: str
) -> None:
super().add_unredirected_header(key, value)
self.added_headers[key] = value

View File

@@ -1,18 +1,5 @@
from typing import (
TYPE_CHECKING,
List,
Type,
Tuple,
Union,
TypeVar,
Iterator,
KeysView,
Optional,
ItemsView,
ValuesView,
MutableMapping,
overload,
)
from typing import TYPE_CHECKING, Union, TypeVar, Optional, overload
from collections.abc import Iterator, KeysView, ItemsView, ValuesView, MutableMapping
from .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider
@@ -22,7 +9,7 @@ if TYPE_CHECKING:
T = TypeVar("T")
class MatcherManager(MutableMapping[int, List[Type["Matcher"]]]):
class MatcherManager(MutableMapping[int, list[type["Matcher"]]]):
"""事件响应器管理器
实现了常用字典操作,用于管理事件响应器。
@@ -43,10 +30,10 @@ class MatcherManager(MutableMapping[int, List[Type["Matcher"]]]):
def __len__(self) -> int:
return len(self.provider)
def __getitem__(self, key: int) -> List[Type["Matcher"]]:
def __getitem__(self, key: int) -> list[type["Matcher"]]:
return self.provider[key]
def __setitem__(self, key: int, value: List[Type["Matcher"]]) -> None:
def __setitem__(self, key: int, value: list[type["Matcher"]]) -> None:
self.provider[key] = value
def __delitem__(self, key: int) -> None:
@@ -58,43 +45,45 @@ class MatcherManager(MutableMapping[int, List[Type["Matcher"]]]):
def keys(self) -> KeysView[int]:
return self.provider.keys()
def values(self) -> ValuesView[List[Type["Matcher"]]]:
def values(self) -> ValuesView[list[type["Matcher"]]]:
return self.provider.values()
def items(self) -> ItemsView[int, List[Type["Matcher"]]]:
def items(self) -> ItemsView[int, list[type["Matcher"]]]:
return self.provider.items()
@overload
def get(self, key: int) -> Optional[List[Type["Matcher"]]]:
...
def get(self, key: int) -> Optional[list[type["Matcher"]]]: ...
@overload
def get(self, key: int, default: T) -> Union[List[Type["Matcher"]], T]:
...
def get(self, key: int, default: T) -> Union[list[type["Matcher"]], T]: ...
def get(
self, key: int, default: Optional[T] = None
) -> Optional[Union[List[Type["Matcher"]], T]]:
) -> Optional[Union[list[type["Matcher"]], T]]:
return self.provider.get(key, default)
def pop(self, key: int) -> List[Type["Matcher"]]:
def pop( # pyright: ignore[reportIncompatibleMethodOverride]
self, key: int
) -> list[type["Matcher"]]:
return self.provider.pop(key)
def popitem(self) -> Tuple[int, List[Type["Matcher"]]]:
def popitem(self) -> tuple[int, list[type["Matcher"]]]:
return self.provider.popitem()
def clear(self) -> None:
self.provider.clear()
def update(self, __m: MutableMapping[int, List[Type["Matcher"]]]) -> None:
def update( # pyright: ignore[reportIncompatibleMethodOverride]
self, __m: MutableMapping[int, list[type["Matcher"]]]
) -> None:
self.provider.update(__m)
def setdefault(
self, key: int, default: List[Type["Matcher"]]
) -> List[Type["Matcher"]]:
self, key: int, default: list[type["Matcher"]]
) -> list[type["Matcher"]]:
return self.provider.setdefault(key, default)
def set_provider(self, provider_class: Type[MatcherProvider]) -> None:
def set_provider(self, provider_class: type[MatcherProvider]) -> None:
"""设置事件响应器存储器
参数:

View File

@@ -6,29 +6,29 @@ from types import ModuleType
from dataclasses import dataclass
from contextvars import ContextVar
from typing_extensions import Self
from collections.abc import Iterable
from datetime import datetime, timedelta
from contextlib import AsyncExitStack, contextmanager
from typing import (
from typing import ( # noqa: UP035
TYPE_CHECKING,
Any,
List,
Type,
Tuple,
Union,
TypeVar,
Callable,
ClassVar,
Iterable,
NoReturn,
Optional,
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,
@@ -78,15 +78,15 @@ 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
class MatcherSource:
"""Matcher 源代码上下文信息"""
plugin_name: Optional[str] = None
"""事件响应器所在插件名称"""
plugin_id: Optional[str] = None
"""事件响应器所在插件标识符"""
module_name: Optional[str] = None
"""事件响应器所在插件模块的路径名"""
lineno: Optional[int] = None
@@ -97,8 +97,13 @@ class MatcherSource:
"""事件响应器所在插件"""
from nonebot.plugin import get_plugin
if self.plugin_name is not None:
return get_plugin(self.plugin_name)
if self.plugin_id is not None:
return get_plugin(self.plugin_id)
@property
def plugin_name(self) -> Optional[str]:
"""事件响应器所在插件名"""
return self.plugin and self.plugin.name
@property
def module(self) -> Optional[ModuleType]:
@@ -141,7 +146,7 @@ class Matcher(metaclass=MatcherMeta):
"""事件响应器匹配规则"""
permission: ClassVar[Permission] = Permission()
"""事件响应器触发权限"""
handlers: List[Dependent[Any]] = []
handlers: ClassVar[list[Dependent[Any]]] = []
"""事件响应器拥有的事件处理函数列表"""
priority: ClassVar[int] = 1
"""事件响应器优先级"""
@@ -160,7 +165,7 @@ class Matcher(metaclass=MatcherMeta):
_default_permission_updater: ClassVar[Optional[Dependent[Permission]]] = None
"""事件响应器权限更新函数"""
HANDLER_PARAM_TYPES: ClassVar[Tuple[Type[Param], ...]] = (
HANDLER_PARAM_TYPES: ClassVar[tuple[Type[Param], ...]] = ( # noqa: UP006
DependParam,
BotParam,
EventParam,
@@ -171,7 +176,7 @@ class Matcher(metaclass=MatcherMeta):
)
def __init__(self):
self.handlers = self.handlers.copy()
self.remain_handlers: list[Dependent[Any]] = self.handlers.copy()
self.state = self._default_state.copy()
def __repr__(self) -> str:
@@ -192,7 +197,7 @@ class Matcher(metaclass=MatcherMeta):
type_: str = "",
rule: Optional[Rule] = None,
permission: Optional[Permission] = None,
handlers: Optional[List[Union[T_Handler, Dependent[Any]]]] = None,
handlers: Optional[list[Union[T_Handler, Dependent[Any]]]] = None,
temp: bool = False,
priority: int = 1,
block: bool = False,
@@ -206,7 +211,7 @@ class Matcher(metaclass=MatcherMeta):
default_permission_updater: Optional[
Union[T_PermissionUpdater, Dependent[Permission]]
] = None,
) -> Type[Self]:
) -> Type[Self]: # noqa: UP006
"""
创建一个新的事件响应器,并存储至 `matchers <#matchers>`_
@@ -247,7 +252,7 @@ class Matcher(metaclass=MatcherMeta):
)
source = source or (
MatcherSource(
plugin_name=plugin and plugin.name,
plugin_id=plugin and plugin.id_,
module_name=module and module.__name__,
)
if plugin is not None or module is not None
@@ -262,16 +267,20 @@ class Matcher(metaclass=MatcherMeta):
"type": type_,
"rule": rule or Rule(),
"permission": permission or Permission(),
"handlers": [
handler
if isinstance(handler, Dependent)
else Dependent[Any].parse(
call=handler, allow_types=cls.HANDLER_PARAM_TYPES
)
for handler in handlers
]
if handlers
else [],
"handlers": (
[
(
handler
if isinstance(handler, Dependent)
else Dependent[Any].parse(
call=handler, allow_types=cls.HANDLER_PARAM_TYPES
)
)
for handler in handlers
]
if handlers
else []
),
"temp": temp,
"expire_time": (
expire_time
@@ -313,7 +322,7 @@ class Matcher(metaclass=MatcherMeta):
matchers[priority].append(NewMatcher)
return NewMatcher
return NewMatcher # type: ignore
@classmethod
def destroy(cls) -> None:
@@ -326,15 +335,20 @@ class Matcher(metaclass=MatcherMeta):
return cls._source and cls._source.plugin
@classproperty
def module(cls) -> Optional[ModuleType]:
"""事件响应器所在插件模块"""
return cls._source and cls._source.module
def plugin_id(cls) -> Optional[str]:
"""事件响应器所在插件标识符"""
return cls._source and cls._source.plugin_id
@classproperty
def plugin_name(cls) -> Optional[str]:
"""事件响应器所在插件名"""
return cls._source and cls._source.plugin_name
@classproperty
def module(cls) -> Optional[ModuleType]:
"""事件响应器所在插件模块"""
return cls._source and cls._source.module
@classproperty
def module_name(cls) -> Optional[str]:
"""事件响应器所在插件模块路径"""
@@ -453,7 +467,7 @@ class Matcher(metaclass=MatcherMeta):
parameterless: 非参数类型依赖列表
"""
async def _receive(event: Event, matcher: "Matcher") -> Union[None, NoReturn]:
async def _receive(event: Event, matcher: "Matcher") -> None:
matcher.set_target(RECEIVE_KEY.format(id=id))
if matcher.get_target() == RECEIVE_KEY.format(id=id):
matcher.set_receive(id, event)
@@ -658,12 +672,10 @@ class Matcher(metaclass=MatcherMeta):
raise SkippedException
@overload
def get_receive(self, id: str) -> Union[Event, None]:
...
def get_receive(self, id: str) -> Union[Event, None]: ...
@overload
def get_receive(self, id: str, default: T) -> Union[Event, T]:
...
def get_receive(self, id: str, default: T) -> Union[Event, T]: ...
def get_receive(
self, id: str, default: Optional[T] = None
@@ -680,12 +692,10 @@ class Matcher(metaclass=MatcherMeta):
self.state[LAST_RECEIVE_KEY] = event
@overload
def get_last_receive(self) -> Union[Event, None]:
...
def get_last_receive(self) -> Union[Event, None]: ...
@overload
def get_last_receive(self, default: T) -> Union[Event, T]:
...
def get_last_receive(self, default: T) -> Union[Event, T]: ...
def get_last_receive(
self, default: Optional[T] = None
@@ -697,12 +707,10 @@ class Matcher(metaclass=MatcherMeta):
return self.state.get(LAST_RECEIVE_KEY, default)
@overload
def get_arg(self, key: str) -> Union[Message, None]:
...
def get_arg(self, key: str) -> Union[Message, None]: ...
@overload
def get_arg(self, key: str, default: T) -> Union[Message, T]:
...
def get_arg(self, key: str, default: T) -> Union[Message, T]: ...
def get_arg(
self, key: str, default: Optional[T] = None
@@ -724,12 +732,10 @@ class Matcher(metaclass=MatcherMeta):
self.state[REJECT_TARGET] = target
@overload
def get_target(self) -> Union[str, None]:
...
def get_target(self) -> Union[str, None]: ...
@overload
def get_target(self, default: T) -> Union[str, T]:
...
def get_target(self, default: T) -> Union[str, T]: ...
def get_target(self, default: Optional[T] = None) -> Optional[Union[str, T]]:
return self.state.get(REJECT_TARGET, default)
@@ -779,7 +785,7 @@ class Matcher(metaclass=MatcherMeta):
async def resolve_reject(self):
handler = current_handler.get()
self.handlers.insert(0, handler)
self.remain_handlers.insert(0, handler)
if REJECT_CACHE_TARGET in self.state:
self.state[REJECT_TARGET] = self.state[REJECT_CACHE_TARGET]
@@ -808,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.handlers:
handler = self.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")
@@ -842,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(
@@ -856,7 +912,7 @@ class Matcher(metaclass=MatcherMeta):
type_,
Rule(),
permission,
self.handlers,
self.remain_handlers,
temp=True,
priority=0,
block=True,
@@ -866,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
@@ -876,7 +932,7 @@ class Matcher(metaclass=MatcherMeta):
type_,
Rule(),
permission,
self.handlers,
self.remain_handlers,
temp=True,
priority=0,
block=True,
@@ -886,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,12 +1,13 @@
import abc
from typing import TYPE_CHECKING
from collections import defaultdict
from typing import TYPE_CHECKING, List, Type, Mapping, MutableMapping
from collections.abc import Mapping, MutableMapping
if TYPE_CHECKING:
from .matcher import Matcher
class MatcherProvider(abc.ABC, MutableMapping[int, List[Type["Matcher"]]]):
class MatcherProvider(abc.ABC, MutableMapping[int, list[type["Matcher"]]]):
"""事件响应器存储器基类
参数:
@@ -14,12 +15,12 @@ class MatcherProvider(abc.ABC, MutableMapping[int, List[Type["Matcher"]]]):
"""
@abc.abstractmethod
def __init__(self, matchers: Mapping[int, List[Type["Matcher"]]]):
def __init__(self, matchers: Mapping[int, list[type["Matcher"]]]):
raise NotImplementedError
class _DictProvider(defaultdict, MatcherProvider):
def __init__(self, matchers: Mapping[int, List[Type["Matcher"]]]):
def __init__(self, matchers: Mapping[int, list[type["Matcher"]]]):
super().__init__(list, matchers)

View File

@@ -1,25 +1,33 @@
import asyncio
import inspect
from typing_extensions import Self, Annotated, override
from enum import Enum
from typing_extensions import Self, get_args, override, get_origin
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
from typing import (
TYPE_CHECKING,
Any,
Type,
Tuple,
Union,
Literal,
Callable,
Optional,
Annotated,
cast,
)
from pydantic.typing import get_args, get_origin
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
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.dependencies import Param, Dependent, CustomConfig
from nonebot.typing import T_State, T_Handler, T_DependencyCache
from nonebot.compat import FieldInfo, ModelField, PydanticUndefined, extract_field_info
from nonebot.typing import (
_STATE_FLAG,
T_State,
T_Handler,
T_DependencyCache,
origin_is_annotated,
)
from nonebot.utils import (
get_name,
run_sync,
@@ -34,23 +42,6 @@ if TYPE_CHECKING:
from nonebot.matcher import Matcher
from nonebot.adapters import Bot, Event
EXTRA_FIELD_INFO = (
"gt",
"lt",
"ge",
"le",
"multiple_of",
"allow_inf_nan",
"max_digits",
"decimal_places",
"min_items",
"max_items",
"unique_items",
"min_length",
"max_length",
"regex",
)
class DependsInner:
def __init__(
@@ -58,7 +49,7 @@ class DependsInner:
dependency: Optional[T_Handler] = None,
*,
use_cache: bool = True,
validate: Union[bool, FieldInfo] = False,
validate: Union[bool, PydanticFieldInfo] = False,
) -> None:
self.dependency = dependency
self.use_cache = use_cache
@@ -75,7 +66,7 @@ def Depends(
dependency: Optional[T_Handler] = None,
*,
use_cache: bool = True,
validate: Union[bool, FieldInfo] = False,
validate: Union[bool, PydanticFieldInfo] = False,
) -> Any:
"""子依赖装饰器
@@ -105,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):
"""子依赖注入参数。
@@ -113,29 +176,37 @@ class DependParam(Param):
本注入应该具有最高优先级,因此应该在其他参数之前检查。
"""
def __init__(
self, *args, dependent: Dependent[Any], use_cache: bool, **kwargs: Any
) -> None:
super().__init__(*args, **kwargs)
self.dependent = dependent
self.use_cache = use_cache
def __repr__(self) -> str:
return f"Depends({self.extra['dependent']})"
return f"Depends({self.dependent}, use_cache={self.use_cache})"
@classmethod
def _from_field(
cls, sub_dependent: Dependent, use_cache: bool, validate: Union[bool, FieldInfo]
cls,
sub_dependent: Dependent[Any],
use_cache: bool,
validate: Union[bool, PydanticFieldInfo],
) -> Self:
kwargs = {}
if isinstance(validate, FieldInfo):
kwargs.update((k, getattr(validate, k)) for k in EXTRA_FIELD_INFO)
if isinstance(validate, PydanticFieldInfo):
kwargs.update(extract_field_info(validate))
return cls(
Required,
validate=bool(validate),
**kwargs,
dependent=sub_dependent,
use_cache=use_cache,
)
kwargs["validate"] = bool(validate)
kwargs["dependent"] = sub_dependent
kwargs["use_cache"] = use_cache
return cls(**kwargs)
@classmethod
@override
def _check_param(
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
) -> Optional[Self]:
type_annotation, depends_inner = param.annotation, None
# extract type annotation and dependency from Annotated
@@ -175,7 +246,7 @@ class DependParam(Param):
@classmethod
@override
def _check_parameterless(
cls, value: Any, allow_types: Tuple[Type[Param], ...]
cls, value: Any, allow_types: tuple[type[Param], ...]
) -> Optional["Param"]:
if isinstance(value, DependsInner):
assert value.dependency, "Dependency cannot be empty"
@@ -191,24 +262,34 @@ class DependParam(Param):
dependency_cache: Optional[T_DependencyCache] = None,
**kwargs: Any,
) -> Any:
use_cache: bool = self.extra["use_cache"]
use_cache: bool = self.use_cache
dependency_cache = {} if dependency_cache is None else dependency_cache
sub_dependent: Dependent = self.extra["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"
@@ -216,23 +297,33 @@ 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:
# run sub dependent pre-checkers
sub_dependent: Dependent = self.extra["dependent"]
await sub_dependent.check(**kwargs)
await self.dependent.check(**kwargs)
class BotParam(Param):
@@ -243,21 +334,23 @@ class BotParam(Param):
为保证兼容性,本注入还会解析名为 `bot` 且没有类型注解的参数。
"""
def __init__(
self, *args, checker: Optional[ModelField] = None, **kwargs: Any
) -> None:
super().__init__(*args, **kwargs)
self.checker = checker
def __repr__(self) -> str:
return (
"BotParam("
+ (
repr(cast(ModelField, checker).type_)
if (checker := self.extra.get("checker"))
else ""
)
+ (repr(self.checker.annotation) if self.checker is not None else "")
+ ")"
)
@classmethod
@override
def _check_param(
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
) -> Optional[Self]:
from nonebot.adapters import Bot
@@ -265,27 +358,26 @@ class BotParam(Param):
if generic_check_issubclass(param.annotation, Bot):
checker: Optional[ModelField] = None
if param.annotation is not Bot:
checker = ModelField(
name=param.name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
checker = ModelField.construct(
name=param.name, annotation=param.annotation, field_info=FieldInfo()
)
return cls(Required, checker=checker)
return cls(checker=checker)
# legacy: param is named "bot" and has no type annotation
elif param.annotation == param.empty and param.name == "bot":
return cls(Required)
return cls()
@override
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
async def _solve( # pyright: ignore[reportIncompatibleMethodOverride]
self, bot: "Bot", **kwargs: Any
) -> Any:
return bot
@override
async def _check(self, bot: "Bot", **kwargs: Any) -> None:
if checker := self.extra.get("checker"):
check_field_type(checker, bot)
async def _check( # pyright: ignore[reportIncompatibleMethodOverride]
self, bot: "Bot", **kwargs: Any
) -> None:
if self.checker is not None:
check_field_type(self.checker, bot)
class EventParam(Param):
@@ -296,21 +388,23 @@ class EventParam(Param):
为保证兼容性,本注入还会解析名为 `event` 且没有类型注解的参数。
"""
def __init__(
self, *args, checker: Optional[ModelField] = None, **kwargs: Any
) -> None:
super().__init__(*args, **kwargs)
self.checker = checker
def __repr__(self) -> str:
return (
"EventParam("
+ (
repr(cast(ModelField, checker).type_)
if (checker := self.extra.get("checker"))
else ""
)
+ (repr(self.checker.annotation) if self.checker is not None else "")
+ ")"
)
@classmethod
@override
def _check_param(
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
) -> Optional[Self]:
from nonebot.adapters import Event
@@ -318,27 +412,26 @@ class EventParam(Param):
if generic_check_issubclass(param.annotation, Event):
checker: Optional[ModelField] = None
if param.annotation is not Event:
checker = ModelField(
name=param.name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
checker = ModelField.construct(
name=param.name, annotation=param.annotation, field_info=FieldInfo()
)
return cls(Required, checker=checker)
return cls(checker=checker)
# legacy: param is named "event" and has no type annotation
elif param.annotation == param.empty and param.name == "event":
return cls(Required)
return cls()
@override
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
async def _solve( # pyright: ignore[reportIncompatibleMethodOverride]
self, event: "Event", **kwargs: Any
) -> Any:
return event
@override
async def _check(self, event: "Event", **kwargs: Any) -> Any:
if checker := self.extra.get("checker", None):
check_field_type(checker, event)
async def _check( # pyright: ignore[reportIncompatibleMethodOverride]
self, event: "Event", **kwargs: Any
) -> Any:
if self.checker is not None:
check_field_type(self.checker, event)
class StateParam(Param):
@@ -355,17 +448,21 @@ class StateParam(Param):
@classmethod
@override
def _check_param(
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
) -> Optional[Self]:
# param type is T_State
if param.annotation is T_State:
return cls(Required)
if origin_is_annotated(
get_origin(param.annotation)
) and _STATE_FLAG in get_args(param.annotation):
return cls()
# legacy: param is named "state" and has no type annotation
elif param.annotation == param.empty and param.name == "state":
return cls(Required)
return cls()
@override
async def _solve(self, state: T_State, **kwargs: Any) -> Any:
async def _solve( # pyright: ignore[reportIncompatibleMethodOverride]
self, state: T_State, **kwargs: Any
) -> Any:
return state
@@ -377,13 +474,23 @@ class MatcherParam(Param):
为保证兼容性,本注入还会解析名为 `matcher` 且没有类型注解的参数。
"""
def __init__(
self, *args, checker: Optional[ModelField] = None, **kwargs: Any
) -> None:
super().__init__(*args, **kwargs)
self.checker = checker
def __repr__(self) -> str:
return "MatcherParam()"
return (
"MatcherParam("
+ (repr(self.checker.annotation) if self.checker is not None else "")
+ ")"
)
@classmethod
@override
def _check_param(
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
) -> Optional[Self]:
from nonebot.matcher import Matcher
@@ -391,35 +498,34 @@ class MatcherParam(Param):
if generic_check_issubclass(param.annotation, Matcher):
checker: Optional[ModelField] = None
if param.annotation is not Matcher:
checker = ModelField(
name=param.name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
checker = ModelField.construct(
name=param.name, annotation=param.annotation, field_info=FieldInfo()
)
return cls(Required, checker=checker)
return cls(checker=checker)
# legacy: param is named "matcher" and has no type annotation
elif param.annotation == param.empty and param.name == "matcher":
return cls(Required)
return cls()
@override
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
async def _solve( # pyright: ignore[reportIncompatibleMethodOverride]
self, matcher: "Matcher", **kwargs: Any
) -> Any:
return matcher
@override
async def _check(self, matcher: "Matcher", **kwargs: Any) -> Any:
if checker := self.extra.get("checker", None):
check_field_type(checker, matcher)
async def _check( # pyright: ignore[reportIncompatibleMethodOverride]
self, matcher: "Matcher", **kwargs: Any
) -> Any:
if self.checker is not None:
check_field_type(self.checker, matcher)
class ArgInner:
def __init__(
self, key: Optional[str], type: Literal["message", "str", "plaintext"]
) -> None:
self.key = key
self.type = type
self.key: Optional[str] = key
self.type: Literal["message", "str", "plaintext"] = type
def __repr__(self) -> str:
return f"ArgInner(key={self.key!r}, type={self.type!r})"
@@ -449,31 +555,41 @@ class ArgParam(Param):
留空则会根据参数名称获取。
"""
def __init__(
self,
*args,
key: str,
type: Literal["message", "str", "plaintext"],
**kwargs: Any,
) -> None:
super().__init__(*args, **kwargs)
self.key = key
self.type = type
def __repr__(self) -> str:
return f"ArgParam(key={self.extra['key']!r}, type={self.extra['type']!r})"
return f"ArgParam(key={self.key!r}, type={self.type!r})"
@classmethod
@override
def _check_param(
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
) -> Optional[Self]:
if isinstance(param.default, ArgInner):
return cls(
Required, key=param.default.key or param.name, type=param.default.type
)
return cls(key=param.default.key or param.name, type=param.default.type)
elif get_origin(param.annotation) is Annotated:
for arg in get_args(param.annotation)[:0:-1]:
if isinstance(arg, ArgInner):
return cls(Required, key=arg.key or param.name, type=arg.type)
return cls(key=arg.key or param.name, type=arg.type)
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
key: str = self.extra["key"]
message = matcher.get_arg(key)
async def _solve( # pyright: ignore[reportIncompatibleMethodOverride]
self, matcher: "Matcher", **kwargs: Any
) -> Any:
message = matcher.get_arg(self.key)
if message is None:
return message
if self.extra["type"] == "message":
if self.type == "message":
return message
elif self.extra["type"] == "str":
elif self.type == "str":
return str(message)
else:
return message.extract_plain_text()
@@ -493,14 +609,14 @@ class ExceptionParam(Param):
@classmethod
@override
def _check_param(
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
) -> Optional[Self]:
# param type is Exception(s) or subclass(es) of Exception or None
if generic_check_issubclass(param.annotation, Exception):
return cls(Required)
return cls()
# legacy: param is named "exception" and has no type annotation
elif param.annotation == param.empty and param.name == "exception":
return cls(Required)
return cls()
@override
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
@@ -521,14 +637,14 @@ class DefaultParam(Param):
@classmethod
@override
def _check_param(
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
) -> Optional[Self]:
if param.default != param.empty:
return cls(param.default)
return cls(default=param.default)
@override
async def _solve(self, **kwargs: Any) -> Any:
return Undefined
return PydanticUndefined
__autodoc__ = {

View File

@@ -1,7 +1,8 @@
import asyncio
from typing_extensions import Self
from contextlib import AsyncExitStack
from typing import Set, Tuple, Union, NoReturn, Optional
from typing import Union, ClassVar, NoReturn, Optional
import anyio
from nonebot.dependencies import Dependent
from nonebot.utils import run_coro_with_catch
@@ -9,7 +10,7 @@ from nonebot.exception import SkippedException
from nonebot.typing import T_DependencyCache, T_PermissionChecker
from .adapter import Bot, Event
from .params import BotParam, EventParam, DependParam, DefaultParam
from .params import Param, BotParam, EventParam, DependParam, DefaultParam
class Permission:
@@ -30,7 +31,7 @@ class Permission:
__slots__ = ("checkers",)
HANDLER_PARAM_TYPES = [
HANDLER_PARAM_TYPES: ClassVar[list[type[Param]]] = [
DependParam,
BotParam,
EventParam,
@@ -38,11 +39,13 @@ class Permission:
]
def __init__(self, *checkers: Union[T_PermissionChecker, Dependent[bool]]) -> None:
self.checkers: Set[Dependent[bool]] = {
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
self.checkers: set[Dependent[bool]] = {
(
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
)
)
for checker in checkers
}
@@ -68,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.")
@@ -120,7 +127,7 @@ class User:
__slots__ = ("users", "perm")
def __init__(
self, users: Tuple[str, ...], perm: Optional[Permission] = None
self, users: tuple[str, ...], perm: Optional[Permission] = None
) -> None:
self.users = users
self.perm = perm
@@ -144,7 +151,7 @@ class User:
@classmethod
def _clean_permission(cls, perm: Permission) -> Optional[Permission]:
if len(perm.checkers) == 1 and isinstance(
user_perm := tuple(perm.checkers)[0].call, cls
user_perm := next(iter(perm.checkers)).call, cls
):
return user_perm.perm
return perm

View File

@@ -1,13 +1,15 @@
import asyncio
from contextlib import AsyncExitStack
from typing import Set, Union, NoReturn, Optional
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
from .adapter import Bot, Event
from .params import BotParam, EventParam, StateParam, DependParam, DefaultParam
from .params import Param, BotParam, EventParam, StateParam, DependParam, DefaultParam
class Rule:
@@ -28,7 +30,7 @@ class Rule:
__slots__ = ("checkers",)
HANDLER_PARAM_TYPES = [
HANDLER_PARAM_TYPES: ClassVar[list[type[Param]]] = [
DependParam,
BotParam,
EventParam,
@@ -37,11 +39,13 @@ class Rule:
]
def __init__(self, *checkers: Union[T_RuleChecker, Dependent[bool]]) -> None:
self.checkers: Set[Dependent[bool]] = {
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
self.checkers: set[Dependent[bool]] = {
(
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
)
)
for checker in checkers
}
@@ -69,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,11 +8,14 @@ NoneBot 使用 [`loguru`][loguru] 来记录日志信息。
[loguru]: https://github.com/Delgan/loguru
FrontMatter:
mdx:
format: md
sidebar_position: 7
description: nonebot.log 模块
"""
import sys
import inspect
import logging
from typing import TYPE_CHECKING
@@ -45,6 +48,7 @@ logger: "Logger" = loguru.logger
# logger.addHandler(default_handler)
# https://loguru.readthedocs.io/en/stable/overview.html#entirely-compatible-with-standard-logging
class LoguruHandler(logging.Handler): # pragma: no cover
"""logging 与 loguru 之间的桥梁,将 logging 的日志转发到 loguru。"""
@@ -54,8 +58,8 @@ class LoguruHandler(logging.Handler): # pragma: no cover
except ValueError:
level = record.levelno
frame, depth = sys._getframe(6), 6
while frame and frame.f_code.co_filename == logging.__file__:
frame, depth = inspect.currentframe(), 0
while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__):
frame = frame.f_back
depth += 1

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, Set, Dict, Type, 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,
@@ -46,10 +55,10 @@ from nonebot.internal.params import (
if TYPE_CHECKING:
from nonebot.adapters import Bot, Event
_event_preprocessors: Set[Dependent[Any]] = set()
_event_postprocessors: Set[Dependent[Any]] = set()
_run_preprocessors: Set[Dependent[Any]] = set()
_run_postprocessors: Set[Dependent[Any]] = set()
_event_preprocessors: set[Dependent[Any]] = set()
_event_postprocessors: set[Dependent[Any]] = set()
_run_preprocessors: set[Dependent[Any]] = set()
_run_postprocessors: set[Dependent[Any]] = set()
EVENT_PCS_PARAMS = (
DependParam,
@@ -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,33 +330,36 @@ 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(
Matcher: Type[Matcher],
Matcher: type[Matcher],
bot: "Bot",
event: "Event",
state: T_State,
@@ -381,7 +410,7 @@ async def _check_matcher(
async def _run_matcher(
Matcher: Type[Matcher],
Matcher: type[Matcher],
bot: "Bot",
event: "Event",
state: T_State,
@@ -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(
@@ -446,7 +476,7 @@ async def _run_matcher(
async def check_and_run_matcher(
Matcher: Type[Matcher],
Matcher: type[Matcher],
bot: "Bot",
event: "Event",
state: T_State,
@@ -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
@@ -505,7 +534,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
if show_log:
logger.opt(colors=True).success(log_msg)
state: Dict[Any, Any] = {}
state: dict[Any, Any] = {}
dependency_cache: T_DependencyCache = {}
# create event scope context
@@ -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,11 +1,14 @@
"""本模块定义了依赖注入的各类参数。
FrontMatter:
mdx:
format: md
sidebar_position: 4
description: nonebot.params 模块
"""
from typing import Any, Dict, List, Match, Tuple, Union, Optional
from re import Match
from typing import Any, Union, Literal, Callable, Optional, overload
from nonebot.typing import T_State
from nonebot.matcher import Matcher
@@ -79,7 +82,7 @@ def _command(state: T_State) -> Message:
return state[PREFIX_KEY][CMD_KEY]
def Command() -> Tuple[str, ...]:
def Command() -> tuple[str, ...]:
"""消息命令元组"""
return Depends(_command)
@@ -129,7 +132,7 @@ def ShellCommandArgs() -> Any:
return Depends(_shell_command_args, use_cache=False)
def _shell_command_argv(state: T_State) -> List[Union[str, MessageSegment]]:
def _shell_command_argv(state: T_State) -> list[Union[str, MessageSegment]]:
return state[SHELL_ARGV]
@@ -147,29 +150,50 @@ def RegexMatched() -> Match[str]:
return Depends(_regex_matched, use_cache=False)
def _regex_str(state: T_State) -> str:
return _regex_matched(state).group()
def _regex_str(
groups: tuple[Union[str, int], ...]
) -> Callable[[T_State], Union[str, tuple[Union[str, Any], ...], Any]]:
def _regex_str_dependency(
state: T_State,
) -> Union[str, tuple[Union[str, Any], ...], Any]:
return _regex_matched(state).group(*groups)
return _regex_str_dependency
def RegexStr() -> str:
@overload
def RegexStr(__group: Literal[0] = 0) -> str: ...
@overload
def RegexStr(__group: Union[str, int]) -> Union[str, Any]: ...
@overload
def RegexStr(
__group1: Union[str, int], __group2: Union[str, int], *groups: Union[str, int]
) -> tuple[Union[str, Any], ...]: ...
def RegexStr(*groups: Union[str, int]) -> Union[str, tuple[Union[str, Any], ...], Any]:
"""正则匹配结果文本"""
return Depends(_regex_str, use_cache=False)
return Depends(_regex_str(groups), use_cache=False)
def _regex_group(state: T_State) -> Tuple[Any, ...]:
def _regex_group(state: T_State) -> tuple[Any, ...]:
return _regex_matched(state).groups()
def RegexGroup() -> Tuple[Any, ...]:
def RegexGroup() -> tuple[Any, ...]:
"""正则匹配结果 group 元组"""
return Depends(_regex_group, use_cache=False)
def _regex_dict(state: T_State) -> Dict[str, Any]:
def _regex_dict(state: T_State) -> dict[str, Any]:
return _regex_matched(state).groupdict()
def RegexDict() -> Dict[str, Any]:
def RegexDict() -> dict[str, Any]:
"""正则匹配结果 group 字典"""
return Depends(_regex_dict, use_cache=False)

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 模块
"""
@@ -39,12 +41,19 @@ FrontMatter:
from itertools import chain
from types import ModuleType
from contextvars import ContextVar
from typing import Set, Dict, List, Tuple, Optional
from typing import TypeVar, Optional
_plugins: Dict[str, "Plugin"] = {}
_managers: List["PluginManager"] = []
_current_plugin_chain: ContextVar[Tuple["Plugin", ...]] = ContextVar(
"_current_plugin_chain", default=()
from pydantic import BaseModel
from nonebot import get_driver
from nonebot.compat import model_dump, type_validate_python
C = TypeVar("C", bound=BaseModel)
_plugins: dict[str, "Plugin"] = {}
_managers: list["PluginManager"] = []
_current_plugin: ContextVar[Optional["Plugin"]] = ContextVar(
"_current_plugin", default=None
)
@@ -52,34 +61,87 @@ def _module_name_to_plugin_name(module_name: str) -> str:
return module_name.rsplit(".", 1)[-1]
def _controlled_modules() -> dict[str, str]:
return {
plugin_id: module_name
for manager in _managers
for plugin_id, module_name in manager.controlled_modules.items()
}
def _find_parent_plugin_id(
module_name: str, controlled_modules: Optional[dict[str, str]] = None
) -> Optional[str]:
if controlled_modules is None:
controlled_modules = _controlled_modules()
available = {
module_name: plugin_id for plugin_id, module_name in controlled_modules.items()
}
while "." in module_name:
module_name, _ = module_name.rsplit(".", 1)
if module_name in available:
return available[module_name]
def _module_name_to_plugin_id(
module_name: str, controlled_modules: Optional[dict[str, str]] = None
) -> str:
plugin_name = _module_name_to_plugin_name(module_name)
if parent_plugin_id := _find_parent_plugin_id(module_name, controlled_modules):
return f"{parent_plugin_id}:{plugin_name}"
return plugin_name
def _new_plugin(
module_name: str, module: ModuleType, manager: "PluginManager"
) -> "Plugin":
plugin_name = _module_name_to_plugin_name(module_name)
if plugin_name in _plugins:
raise RuntimeError("Plugin already exists! Check your plugin name.")
plugin = Plugin(plugin_name, module, module_name, manager)
_plugins[plugin_name] = plugin
plugin_id = _module_name_to_plugin_id(module_name)
if plugin_id in _plugins:
raise RuntimeError(
f"Plugin {plugin_id} already exists! Check your plugin name."
)
parent_plugin_id = _find_parent_plugin_id(module_name)
if parent_plugin_id is not None and parent_plugin_id not in _plugins:
raise RuntimeError(
f"Parent plugin {parent_plugin_id} must "
f"be loaded before loading {plugin_id}."
)
parent_plugin = _plugins[parent_plugin_id] if parent_plugin_id is not None else None
plugin = Plugin(
name=_module_name_to_plugin_name(module_name),
module=module,
module_name=module_name,
manager=manager,
parent_plugin=parent_plugin,
)
if parent_plugin:
parent_plugin.sub_plugins.add(plugin)
_plugins[plugin_id] = plugin
return plugin
def _revert_plugin(plugin: "Plugin") -> None:
if plugin.name not in _plugins:
if plugin.id_ not in _plugins:
raise RuntimeError("Plugin not found!")
del _plugins[plugin.name]
del _plugins[plugin.id_]
if parent_plugin := plugin.parent_plugin:
parent_plugin.sub_plugins.remove(plugin)
parent_plugin.sub_plugins.discard(plugin)
def get_plugin(name: str) -> Optional["Plugin"]:
def get_plugin(plugin_id: str) -> Optional["Plugin"]:
"""获取已经导入的某个插件。
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
如果为嵌套的子插件,标识符为 `父插件标识符:子插件文件(夹)名`。
参数:
name: 插件,即 {ref}`nonebot.plugin.model.Plugin.name`。
plugin_id: 插件标识符,即 {ref}`nonebot.plugin.model.Plugin.id_`。
"""
return _plugins.get(name)
return _plugins.get(plugin_id)
def get_plugin_by_module_name(module_name: str) -> Optional["Plugin"]:
@@ -98,16 +160,21 @@ def get_plugin_by_module_name(module_name: str) -> Optional["Plugin"]:
module_name, *has_parent = module_name.rsplit(".", 1)
def get_loaded_plugins() -> Set["Plugin"]:
def get_loaded_plugins() -> set["Plugin"]:
"""获取当前已导入的所有插件。"""
return set(_plugins.values())
def get_available_plugin_names() -> Set[str]:
"""获取当前所有可用的插件(包含尚未加载的插件)。"""
def get_available_plugin_names() -> set[str]:
"""获取当前所有可用的插件标识符(包含尚未加载的插件)。"""
return {*chain.from_iterable(manager.available_plugins for manager in _managers)}
def get_plugin_config(config: type[C]) -> C:
"""从全局配置获取当前插件需要的配置项。"""
return type_validate_python(config, model_dump(get_driver().config))
from .on import on as on
from .manager import PluginManager
from .on import on_type as on_type

View File

@@ -1,6 +1,8 @@
"""本模块定义插件加载接口。
FrontMatter:
mdx:
format: md
sidebar_position: 1
description: nonebot.plugin.load 模块
"""
@@ -8,18 +10,19 @@ FrontMatter:
import json
from pathlib import Path
from types import ModuleType
from typing import Set, Union, Iterable, Optional
from typing import Union, Optional
from collections.abc import Iterable
from nonebot.utils import path_to_module_name
from .model import Plugin
from .manager import PluginManager
from . import _managers, get_plugin, _current_plugin_chain, _module_name_to_plugin_name
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]:
@@ -39,7 +42,7 @@ def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
return manager.load_plugin(module_path)
def load_plugins(*plugin_dir: str) -> Set[Plugin]:
def load_plugins(*plugin_dir: str) -> set[Plugin]:
"""导入文件夹下多个插件,以 `_` 开头的插件不会被导入!
参数:
@@ -52,7 +55,7 @@ def load_plugins(*plugin_dir: str) -> Set[Plugin]:
def load_all_plugins(
module_path: Iterable[str], plugin_dir: Iterable[str]
) -> Set[Plugin]:
) -> set[Plugin]:
"""导入指定列表中的插件以及指定目录下多个插件,以 `_` 开头的插件不会被导入!
参数:
@@ -64,7 +67,7 @@ def load_all_plugins(
return manager.load_all_plugins()
def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
def load_from_json(file_path: str, encoding: str = "utf-8") -> set[Plugin]:
"""导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件。
以 `_` 开头的插件不会被导入!
@@ -95,7 +98,7 @@ def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
return load_all_plugins(set(plugins), set(plugin_dirs))
def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
def load_from_toml(file_path: str, encoding: str = "utf-8") -> set[Plugin]:
"""导入指定 toml 文件 `[tool.nonebot]` 中的
`plugins` 以及 `plugin_dirs` 下多个插件。
以 `_` 开头的插件不会被导入!
@@ -139,7 +142,7 @@ def load_builtin_plugin(name: str) -> Optional[Plugin]:
return load_plugin(f"nonebot.plugins.{name}")
def load_builtin_plugins(*plugins: str) -> Set[Plugin]:
def load_builtin_plugins(*plugins: str) -> set[Plugin]:
"""导入多个 NoneBot 内置插件。
参数:
@@ -150,41 +153,45 @@ def load_builtin_plugins(*plugins: str) -> Set[Plugin]:
def _find_manager_by_name(name: str) -> Optional[PluginManager]:
for manager in reversed(_managers):
if name in manager.plugins or name in manager.searched_plugins:
if (
name in manager.controlled_modules
or name in manager.controlled_modules.values()
):
return manager
def require(name: str) -> ModuleType:
"""获取一个插件的导出内容
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
"""声明依赖插件
参数:
name: 插件名,即 {ref}`nonebot.plugin.model.Plugin.name`
name: 插件模块名或插件标识符,仅在已声明插件的情况下可使用标识符
异常:
RuntimeError: 插件无法加载
"""
plugin = get_plugin(_module_name_to_plugin_name(name))
if "." in name:
# name is a module name
plugin = get_plugin(_module_name_to_plugin_id(name))
else:
# name is a plugin id or simple module name (equals to plugin id)
plugin = get_plugin(name)
# if plugin not loaded
if not plugin:
# plugin already declared
if plugin is None:
# plugin already declared, module name / plugin id
if manager := _find_manager_by_name(name):
plugin = manager.load_plugin(name)
# plugin not declared, try to declare and load it
else:
# clear current plugin chain, ensure plugin loaded in a new context
_t = _current_plugin_chain.set(())
try:
plugin = load_plugin(name)
finally:
_current_plugin_chain.reset(_t)
if not plugin:
plugin = load_plugin(name)
if plugin is None:
raise RuntimeError(f'Cannot load plugin "{name}"!')
return plugin.module
def inherit_supported_adapters(*names: str) -> Optional[Set[str]]:
def inherit_supported_adapters(*names: str) -> Optional[set[str]]:
"""获取已加载插件的适配器支持状态集合。
如果传入了多个插件名称,返回值会自动取交集。
@@ -196,25 +203,28 @@ def inherit_supported_adapters(*names: str) -> Optional[Set[str]]:
RuntimeError: 插件未加载
ValueError: 插件缺少元数据
"""
final_supported: Optional[Set[str]] = None
final_supported: Optional[set[str]] = None
for name in names:
plugin = get_plugin(_module_name_to_plugin_name(name))
plugin = get_plugin(_module_name_to_plugin_id(name))
if plugin is None:
raise RuntimeError(f'Plugin "{name}" is not loaded!')
raise RuntimeError(
f'Plugin "{name}" is not loaded! You should require it first.'
)
meta = plugin.metadata
if meta is None:
raise ValueError(f'Plugin "{name}" has no metadata!')
support = meta.supported_adapters
if support is None:
if (raw := meta.supported_adapters) is None:
continue
support = {
f"nonebot.adapters.{adapter[1:]}" if adapter.startswith("~") else adapter
for adapter in raw
}
final_supported = (
support if final_supported is None else (final_supported & support)
)
return final_supported and {
f"nonebot.adapters.{adapter_name[1:]}"
if adapter_name.startswith("~")
else adapter_name
for adapter_name in final_supported
}
return final_supported

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 模块
"""
@@ -12,10 +14,11 @@ import pkgutil
import importlib
from pathlib import Path
from itertools import chain
from typing import Optional
from types import ModuleType
from importlib.abc import MetaPathFinder
from collections.abc import Iterable, Sequence
from importlib.machinery import PathFinder, SourceFileLoader
from typing import Set, Dict, List, Iterable, Optional, Sequence
from nonebot.log import logger
from nonebot.utils import escape_tag, path_to_module_name
@@ -25,8 +28,8 @@ from . import (
_managers,
_new_plugin,
_revert_plugin,
_current_plugin_chain,
_module_name_to_plugin_name,
_current_plugin,
_module_name_to_plugin_id,
)
@@ -35,7 +38,7 @@ class PluginManager:
参数:
plugins: 独立插件模块名集合。
search_path: 插件搜索路径(文件夹)。
search_path: 插件搜索路径(文件夹),相对于当前工作目录
"""
def __init__(
@@ -44,60 +47,78 @@ class PluginManager:
search_path: Optional[Iterable[str]] = None,
):
# simple plugin not in search path
self.plugins: Set[str] = set(plugins or [])
self.search_path: Set[str] = set(search_path or [])
self.plugins: set[str] = set(plugins or [])
self.search_path: set[str] = set(search_path or [])
# cache plugins
self._third_party_plugin_names: Dict[str, str] = {}
self._searched_plugin_names: Dict[str, Path] = {}
self.prepare_plugins()
self._third_party_plugin_ids: dict[str, str] = {}
self._searched_plugin_ids: dict[str, str] = {}
self._prepare_plugins()
def __repr__(self) -> str:
return f"PluginManager(plugins={self.plugins}, search_path={self.search_path})"
return f"PluginManager(available_plugins={self.controlled_modules})"
@property
def third_party_plugins(self) -> Set[str]:
"""返回所有独立插件名称"""
return set(self._third_party_plugin_names.keys())
def third_party_plugins(self) -> set[str]:
"""返回所有独立插件标识符"""
return set(self._third_party_plugin_ids.keys())
@property
def searched_plugins(self) -> Set[str]:
"""返回已搜索到的插件名称"""
return set(self._searched_plugin_names.keys())
def searched_plugins(self) -> set[str]:
"""返回已搜索到的插件标识符"""
return set(self._searched_plugin_ids.keys())
@property
def available_plugins(self) -> Set[str]:
"""返回当前插件管理器中可用的插件名称"""
def available_plugins(self) -> set[str]:
"""返回当前插件管理器中可用的插件标识符"""
return self.third_party_plugins | self.searched_plugins
def _previous_plugins(self) -> Set[str]:
_pre_managers: List[PluginManager]
@property
def controlled_modules(self) -> dict[str, str]:
"""返回当前插件管理器中控制的插件标识符与模块路径映射字典。"""
return dict(
chain(
self._third_party_plugin_ids.items(), self._searched_plugin_ids.items()
)
)
def _previous_controlled_modules(self) -> dict[str, str]:
_pre_managers: list[PluginManager]
if self in _managers:
_pre_managers = _managers[: _managers.index(self)]
else:
_pre_managers = _managers[:]
return {
*chain.from_iterable(manager.available_plugins for manager in _pre_managers)
plugin_id: module_name
for manager in _pre_managers
for plugin_id, module_name in manager.controlled_modules.items()
}
def prepare_plugins(self) -> Set[str]:
def _prepare_plugins(self) -> set[str]:
"""搜索插件并缓存插件名称。"""
# get all previous ready to load plugins
previous_plugins = self._previous_plugins()
searched_plugins: Dict[str, Path] = {}
third_party_plugins: Dict[str, str] = {}
previous_plugin_ids = self._previous_controlled_modules()
# if self not in global managers, merge self's controlled modules
def get_controlled_modules():
return (
previous_plugin_ids
if self in _managers
else {**previous_plugin_ids, **self.controlled_modules}
)
# check third party plugins
for plugin in self.plugins:
name = _module_name_to_plugin_name(plugin)
if name in third_party_plugins or name in previous_plugins:
plugin_id = _module_name_to_plugin_id(plugin, get_controlled_modules())
if (
plugin_id in self._third_party_plugin_ids
or plugin_id in previous_plugin_ids
):
raise RuntimeError(
f"Plugin already exists: {name}! Check your plugin name"
f"Plugin already exists: {plugin_id}! Check your plugin name"
)
third_party_plugins[name] = plugin
self._third_party_plugin_names = third_party_plugins
self._third_party_plugin_ids[plugin_id] = plugin
# check plugins in search path
for module_info in pkgutil.iter_modules(self.search_path):
@@ -105,47 +126,55 @@ class PluginManager:
if module_info.name.startswith("_"):
continue
if (
module_info.name in searched_plugins
or module_info.name in previous_plugins
or module_info.name in third_party_plugins
):
raise RuntimeError(
f"Plugin already exists: {module_info.name}! Check your plugin name"
)
if not (
module_spec := module_info.module_finder.find_spec(
module_info.name, None
)
):
continue
if not (module_path := module_spec.origin):
continue
searched_plugins[module_info.name] = Path(module_path).resolve()
self._searched_plugin_names = searched_plugins
if not module_spec.origin:
continue
# get module name from path, pkgutil does not return the actual module name
module_path = Path(module_spec.origin).resolve()
module_name = path_to_module_name(module_path)
plugin_id = _module_name_to_plugin_id(module_name, get_controlled_modules())
if (
plugin_id in previous_plugin_ids
or plugin_id in self._third_party_plugin_ids
or plugin_id in self._searched_plugin_ids
):
raise RuntimeError(
f"Plugin already exists: {plugin_id}! Check your plugin name"
)
self._searched_plugin_ids[plugin_id] = module_name
return self.available_plugins
def load_plugin(self, name: str) -> Optional[Plugin]:
"""加载指定插件。
对于独立插件,可以使用完整插件模块名或者插件名称
可以使用完整插件模块名或者插件标识符加载
参数:
name: 插件名称。
name: 插件名称或插件标识符
"""
try:
if name in self.plugins:
# load using plugin id
if name in self._third_party_plugin_ids:
module = importlib.import_module(self._third_party_plugin_ids[name])
elif name in self._searched_plugin_ids:
module = importlib.import_module(self._searched_plugin_ids[name])
# load using module name
elif (
name in self._third_party_plugin_ids.values()
or name in self._searched_plugin_ids.values()
):
module = importlib.import_module(name)
elif name in self._third_party_plugin_names:
module = importlib.import_module(self._third_party_plugin_names[name])
elif name in self._searched_plugin_names:
module = importlib.import_module(
path_to_module_name(self._searched_plugin_names[name])
)
else:
raise RuntimeError(f"Plugin not found: {name}! Check your plugin name")
@@ -154,13 +183,13 @@ class PluginManager:
) is None or not isinstance(plugin, Plugin):
raise RuntimeError(
f"Module {module.__name__} is not loaded as a plugin! "
"Make sure not to import it before loading."
f"Make sure not to import it before loading."
)
logger.opt(colors=True).success(
f'Succeeded to load plugin "<y>{escape_tag(plugin.name)}</y>"'
f'Succeeded to load plugin "<y>{escape_tag(plugin.id_)}</y>"'
+ (
f' from "<m>{escape_tag(plugin.module_name)}</m>"'
if plugin.module_name != plugin.name
if plugin.module_name != plugin.id_
else ""
)
)
@@ -170,7 +199,7 @@ class PluginManager:
f'<r><bg #f8bbd0>Failed to import "{escape_tag(name)}"</bg #f8bbd0></r>'
)
def load_all_plugins(self) -> Set[Plugin]:
def load_all_plugins(self) -> set[Plugin]:
"""加载所有可用插件。"""
return set(
@@ -192,21 +221,16 @@ class PluginFinder(MetaPathFinder):
module_origin = module_spec.origin
if not module_origin:
return
module_path = Path(module_origin).resolve()
for manager in reversed(_managers):
# use path instead of name in case of submodule name conflict
if (
fullname in manager.plugins
or module_path in manager._searched_plugin_names.values()
):
if fullname in manager.controlled_modules.values():
module_spec.loader = PluginLoader(manager, fullname, module_origin)
return module_spec
return
class PluginLoader(SourceFileLoader):
def __init__(self, manager: PluginManager, fullname: str, path) -> None:
def __init__(self, manager: PluginManager, fullname: str, path: str) -> None:
self.manager = manager
self.loaded = False
super().__init__(fullname, path)
@@ -226,17 +250,8 @@ class PluginLoader(SourceFileLoader):
plugin = _new_plugin(self.name, module, self.manager)
setattr(module, "__plugin__", plugin)
# detect parent plugin before entering current plugin context
parent_plugins = _current_plugin_chain.get()
for pre_plugin in reversed(parent_plugins):
# ensure parent plugin is declared before current plugin
if _managers.index(pre_plugin.manager) < _managers.index(self.manager):
plugin.parent_plugin = pre_plugin
pre_plugin.sub_plugins.add(plugin)
break
# enter plugin context
_plugin_token = _current_plugin_chain.set(parent_plugins + (plugin,))
_plugin_token = _current_plugin.set(plugin)
try:
super().exec_module(module)
@@ -245,7 +260,7 @@ class PluginLoader(SourceFileLoader):
raise
finally:
# leave plugin context
_current_plugin_chain.reset(_plugin_token)
_current_plugin.reset(_plugin_token)
# get plugin metadata
metadata: Optional[PluginMetadata] = getattr(module, "__plugin_meta__", None)

View File

@@ -1,6 +1,8 @@
"""本模块定义插件相关信息。
FrontMatter:
mdx:
format: md
sidebar_position: 3
description: nonebot.plugin.model 模块
"""
@@ -8,7 +10,7 @@ FrontMatter:
import contextlib
from types import ModuleType
from dataclasses import field, dataclass
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
from typing import TYPE_CHECKING, Any, Type, Optional # noqa: UP035
from pydantic import BaseModel
@@ -35,19 +37,19 @@ class PluginMetadata:
"""插件类型,用于商店分类"""
homepage: Optional[str] = None
"""插件主页"""
config: Optional[Type[BaseModel]] = None
config: Optional[Type[BaseModel]] = None # noqa: UP006
"""插件配置项"""
supported_adapters: Optional[Set[str]] = None
supported_adapters: Optional[set[str]] = None
"""插件支持的适配器模块路径
格式为 `<module>[:<Adapter>]``~` 为 `nonebot.adapters.` 的缩写。
`None` 表示支持**所有适配器**。
"""
extra: Dict[Any, Any] = field(default_factory=dict)
extra: dict[Any, Any] = field(default_factory=dict)
"""插件额外信息,可由插件编写者自由扩展定义"""
def get_supported_adapters(self) -> Optional[Set[Type["Adapter"]]]:
def get_supported_adapters(self) -> Optional[set[Type["Adapter"]]]: # noqa: UP006
"""获取当前已安装的插件支持适配器类列表"""
if self.supported_adapters is None:
return None
@@ -66,17 +68,24 @@ class Plugin:
"""存储插件信息"""
name: str
"""插件索引标识NoneBot 使用 文件/文件夹 名称作为标识符"""
"""插件名称NoneBot 使用 文件/文件夹 名称作为插件名称"""
module: ModuleType
"""插件模块对象"""
module_name: str
"""点分割模块路径"""
manager: "PluginManager"
"""导入该插件的插件管理器"""
matcher: Set[Type[Matcher]] = field(default_factory=set)
matcher: set[type[Matcher]] = field(default_factory=set)
"""插件加载时定义的 `Matcher`"""
parent_plugin: Optional["Plugin"] = None
"""父插件"""
sub_plugins: Set["Plugin"] = field(default_factory=set)
sub_plugins: set["Plugin"] = field(default_factory=set)
"""子插件集合"""
metadata: Optional[PluginMetadata] = None
@property
def id_(self) -> str:
"""插件索引标识"""
return (
f"{self.parent_plugin.id_}:{self.name}" if self.parent_plugin else self.name
)

View File

@@ -1,6 +1,8 @@
"""本模块定义事件响应器便携定义函数。
FrontMatter:
mdx:
format: md
sidebar_position: 2
description: nonebot.plugin.on 模块
"""
@@ -9,8 +11,8 @@ import re
import inspect
import warnings
from types import ModuleType
from typing import Any, Union, Optional
from datetime import datetime, timedelta
from typing import Any, Set, Dict, List, Type, Tuple, Union, Optional
from nonebot.adapters import Event
from nonebot.permission import Permission
@@ -31,19 +33,19 @@ from nonebot.rule import (
)
from .model import Plugin
from .manager import _current_plugin
from . import get_plugin_by_module_name
from .manager import _current_plugin_chain
def store_matcher(matcher: Type[Matcher]) -> None:
def store_matcher(matcher: type[Matcher]) -> None:
"""存储一个事件响应器到插件。
参数:
matcher: 事件响应器
"""
# only store the matcher defined when plugin loading
if plugin_chain := _current_plugin_chain.get():
plugin_chain[-1].matcher.add(matcher)
if plugin := _current_plugin.get():
plugin.matcher.add(matcher)
def get_matcher_plugin(depth: int = 1) -> Optional[Plugin]: # pragma: no cover
@@ -76,7 +78,7 @@ def get_matcher_module(depth: int = 1) -> Optional[ModuleType]: # pragma: no co
return (source := get_matcher_source(depth + 1)) and source.module
def get_matcher_source(depth: int = 1) -> Optional[MatcherSource]:
def get_matcher_source(depth: int = 0) -> Optional[MatcherSource]:
"""获取事件响应器定义所在源码信息。
参数:
@@ -85,20 +87,25 @@ def get_matcher_source(depth: int = 1) -> Optional[MatcherSource]:
current_frame = inspect.currentframe()
if current_frame is None:
return None
frame = inspect.getouterframes(current_frame)[depth + 1].frame
frame = current_frame
d = depth + 1
while d > 0:
frame = frame.f_back
if frame is None:
raise ValueError("Depth out of range")
d -= 1
module_name = (module := inspect.getmodule(frame)) and module.__name__
plugin: Optional["Plugin"] = None
# matcher defined when plugin loading
if plugin_chain := _current_plugin_chain.get():
plugin = plugin_chain[-1]
plugin: Optional["Plugin"] = _current_plugin.get()
# matcher defined when plugin running
elif module_name:
if plugin is None and module_name:
plugin = get_plugin_by_module_name(module_name)
return MatcherSource(
plugin_name=plugin and plugin.name,
plugin_id=plugin and plugin.id_,
module_name=module_name,
lineno=frame.f_lineno,
)
@@ -109,14 +116,14 @@ 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,
block: bool = False,
state: Optional[T_State] = None,
_depth: int = 0,
) -> Type[Matcher]:
) -> type[Matcher]:
"""注册一个基础事件响应器,可自定义类型。
参数:
@@ -146,7 +153,7 @@ def on(
return matcher
def on_metaevent(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
def on_metaevent(*args, _depth: int = 0, **kwargs) -> type[Matcher]:
"""注册一个元事件响应器。
参数:
@@ -162,7 +169,7 @@ def on_metaevent(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
return on("meta_event", *args, **kwargs, _depth=_depth + 1)
def on_message(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
def on_message(*args, _depth: int = 0, **kwargs) -> type[Matcher]:
"""注册一个消息事件响应器。
参数:
@@ -179,7 +186,7 @@ def on_message(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
return on("message", *args, **kwargs, _depth=_depth + 1)
def on_notice(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
def on_notice(*args, _depth: int = 0, **kwargs) -> type[Matcher]:
"""注册一个通知事件响应器。
参数:
@@ -195,7 +202,7 @@ def on_notice(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
return on("notice", *args, **kwargs, _depth=_depth + 1)
def on_request(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
def on_request(*args, _depth: int = 0, **kwargs) -> type[Matcher]:
"""注册一个请求事件响应器。
参数:
@@ -212,12 +219,12 @@ def on_request(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
def on_startswith(
msg: Union[str, Tuple[str, ...]],
msg: Union[str, tuple[str, ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = None,
ignorecase: bool = False,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
) -> type[Matcher]:
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
参数:
@@ -236,12 +243,12 @@ def on_startswith(
def on_endswith(
msg: Union[str, Tuple[str, ...]],
msg: Union[str, tuple[str, ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = None,
ignorecase: bool = False,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
) -> type[Matcher]:
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
参数:
@@ -260,12 +267,12 @@ def on_endswith(
def on_fullmatch(
msg: Union[str, Tuple[str, ...]],
msg: Union[str, tuple[str, ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = None,
ignorecase: bool = False,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
) -> type[Matcher]:
"""注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
参数:
@@ -284,11 +291,11 @@ def on_fullmatch(
def on_keyword(
keywords: Set[str],
keywords: set[str],
rule: Optional[Union[Rule, T_RuleChecker]] = None,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
) -> type[Matcher]:
"""注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
参数:
@@ -306,13 +313,13 @@ def on_keyword(
def on_command(
cmd: Union[str, Tuple[str, ...]],
cmd: Union[str, tuple[str, ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = None,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
aliases: Optional[set[Union[str, tuple[str, ...]]]] = None,
force_whitespace: Optional[Union[str, bool]] = None,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
) -> type[Matcher]:
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
@@ -341,13 +348,13 @@ def on_command(
def on_shell_command(
cmd: Union[str, Tuple[str, ...]],
cmd: Union[str, tuple[str, ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = None,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
aliases: Optional[set[Union[str, tuple[str, ...]]]] = None,
parser: Optional[ArgumentParser] = None,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
) -> type[Matcher]:
"""注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
@@ -383,7 +390,7 @@ def on_regex(
rule: Optional[Union[Rule, T_RuleChecker]] = None,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
) -> type[Matcher]:
"""注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
@@ -404,12 +411,12 @@ def on_regex(
def on_type(
types: Union[Type[Event], Tuple[Type[Event], ...]],
types: Union[type[Event], tuple[type[Event], ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = None,
*,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
) -> type[Matcher]:
"""注册一个事件响应器,并且当事件为指定类型时响应。
参数:
@@ -430,14 +437,14 @@ def on_type(
class _Group:
def __init__(self, **kwargs):
"""创建一个事件响应器组合,参数为默认值,与 `on` 一致"""
self.matchers: List[Type[Matcher]] = []
self.matchers: list[type[Matcher]] = []
"""组内事件响应器列表"""
self.base_kwargs: Dict[str, Any] = kwargs
self.base_kwargs: dict[str, Any] = kwargs
"""其他传递给 `on` 的参数默认值"""
def _get_final_kwargs(
self, update: Dict[str, Any], *, exclude: Optional[Set[str]] = None
) -> Dict[str, Any]:
self, update: dict[str, Any], *, exclude: Optional[set[str]] = None
) -> dict[str, Any]:
"""获取最终传递给 `on` 的参数
参数:
@@ -470,18 +477,18 @@ class CommandGroup(_Group):
"""
def __init__(
self, cmd: Union[str, Tuple[str, ...]], prefix_aliases: bool = False, **kwargs
self, cmd: Union[str, tuple[str, ...]], prefix_aliases: bool = False, **kwargs
):
"""命令前缀"""
super().__init__(**kwargs)
self.basecmd: Tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd
self.basecmd: tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd
self.base_kwargs.pop("aliases", None)
self.prefix_aliases = prefix_aliases
def __repr__(self) -> str:
return f"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})"
def command(self, cmd: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
def command(self, cmd: Union[str, tuple[str, ...]], **kwargs) -> type[Matcher]:
"""注册一个新的命令。新参数将会覆盖命令组默认值
参数:
@@ -509,8 +516,8 @@ class CommandGroup(_Group):
return matcher
def shell_command(
self, cmd: Union[str, Tuple[str, ...]], **kwargs
) -> Type[Matcher]:
self, cmd: Union[str, tuple[str, ...]], **kwargs
) -> type[Matcher]:
"""注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值
参数:
@@ -544,7 +551,7 @@ class MatcherGroup(_Group):
def __repr__(self) -> str:
return f"MatcherGroup(matchers={len(self.matchers)})"
def on(self, **kwargs) -> Type[Matcher]:
def on(self, **kwargs) -> type[Matcher]:
"""注册一个基础事件响应器,可自定义类型。
参数:
@@ -562,7 +569,7 @@ class MatcherGroup(_Group):
self.matchers.append(matcher)
return matcher
def on_metaevent(self, **kwargs) -> Type[Matcher]:
def on_metaevent(self, **kwargs) -> type[Matcher]:
"""注册一个元事件响应器。
参数:
@@ -580,7 +587,7 @@ class MatcherGroup(_Group):
self.matchers.append(matcher)
return matcher
def on_message(self, **kwargs) -> Type[Matcher]:
def on_message(self, **kwargs) -> type[Matcher]:
"""注册一个消息事件响应器。
参数:
@@ -598,7 +605,7 @@ class MatcherGroup(_Group):
self.matchers.append(matcher)
return matcher
def on_notice(self, **kwargs) -> Type[Matcher]:
def on_notice(self, **kwargs) -> type[Matcher]:
"""注册一个通知事件响应器。
参数:
@@ -616,7 +623,7 @@ class MatcherGroup(_Group):
self.matchers.append(matcher)
return matcher
def on_request(self, **kwargs) -> Type[Matcher]:
def on_request(self, **kwargs) -> type[Matcher]:
"""注册一个请求事件响应器。
参数:
@@ -635,8 +642,8 @@ class MatcherGroup(_Group):
return matcher
def on_startswith(
self, msg: Union[str, Tuple[str, ...]], **kwargs
) -> Type[Matcher]:
self, msg: Union[str, tuple[str, ...]], **kwargs
) -> type[Matcher]:
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
参数:
@@ -656,7 +663,7 @@ class MatcherGroup(_Group):
self.matchers.append(matcher)
return matcher
def on_endswith(self, msg: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
def on_endswith(self, msg: Union[str, tuple[str, ...]], **kwargs) -> type[Matcher]:
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
参数:
@@ -676,7 +683,7 @@ class MatcherGroup(_Group):
self.matchers.append(matcher)
return matcher
def on_fullmatch(self, msg: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
def on_fullmatch(self, msg: Union[str, tuple[str, ...]], **kwargs) -> type[Matcher]:
"""注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
参数:
@@ -696,7 +703,7 @@ class MatcherGroup(_Group):
self.matchers.append(matcher)
return matcher
def on_keyword(self, keywords: Set[str], **kwargs) -> Type[Matcher]:
def on_keyword(self, keywords: set[str], **kwargs) -> type[Matcher]:
"""注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
参数:
@@ -717,11 +724,11 @@ class MatcherGroup(_Group):
def on_command(
self,
cmd: Union[str, Tuple[str, ...]],
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
cmd: Union[str, tuple[str, ...]],
aliases: Optional[set[Union[str, tuple[str, ...]]]] = None,
force_whitespace: Optional[Union[str, bool]] = None,
**kwargs,
) -> Type[Matcher]:
) -> type[Matcher]:
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
@@ -748,11 +755,11 @@ class MatcherGroup(_Group):
def on_shell_command(
self,
cmd: Union[str, Tuple[str, ...]],
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
cmd: Union[str, tuple[str, ...]],
aliases: Optional[set[Union[str, tuple[str, ...]]]] = None,
parser: Optional[ArgumentParser] = None,
**kwargs,
) -> Type[Matcher]:
) -> type[Matcher]:
"""注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
@@ -780,7 +787,7 @@ class MatcherGroup(_Group):
def on_regex(
self, pattern: str, flags: Union[int, re.RegexFlag] = 0, **kwargs
) -> Type[Matcher]:
) -> type[Matcher]:
"""注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
@@ -803,8 +810,8 @@ class MatcherGroup(_Group):
return matcher
def on_type(
self, types: Union[Type[Event], Tuple[Type[Event]]], **kwargs
) -> Type[Matcher]:
self, types: Union[type[Event], tuple[type[Event]]], **kwargs
) -> type[Matcher]:
"""注册一个事件响应器,并且当事件为指定类型时响应。
参数:

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

@@ -19,4 +19,5 @@ echo = on_command("echo", to_me())
@echo.handle()
async def handle_echo(message: Message = CommandArg()):
await echo.send(message=message)
if any((not seg.is_text()) or str(seg) for seg in message):
await echo.send(message=message)

View File

@@ -1,4 +1,4 @@
from typing import Dict, AsyncGenerator
from collections.abc import AsyncGenerator
from nonebot.adapters import Event
from nonebot.params import Depends
@@ -15,7 +15,7 @@ __plugin_meta__ = PluginMetadata(
supported_adapters=None,
)
_running_matcher: Dict[str, int] = {}
_running_matcher: dict[str, int] = {}
async def matcher_mutex(event: Event) -> AsyncGenerator[bool, None]:

View File

@@ -5,6 +5,8 @@
只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。
FrontMatter:
mdx:
format: md
sidebar_position: 5
description: nonebot.rule 模块
"""
@@ -15,19 +17,16 @@ from argparse import Action
from gettext import gettext
from argparse import ArgumentError
from contextvars import ContextVar
from collections.abc import Sequence
from itertools import chain, product
from argparse import Namespace as Namespace
from argparse import ArgumentParser as ArgParser
from typing import (
IO,
TYPE_CHECKING,
List,
Type,
Tuple,
Union,
TypeVar,
Optional,
Sequence,
TypedDict,
NamedTuple,
cast,
@@ -63,7 +62,7 @@ T = TypeVar("T")
class CMD_RESULT(TypedDict):
command: Optional[Tuple[str, ...]]
command: Optional[tuple[str, ...]]
raw_command: Optional[str]
command_arg: Optional[Message]
command_start: Optional[str]
@@ -72,7 +71,7 @@ class CMD_RESULT(TypedDict):
class TRIE_VALUE(NamedTuple):
command_start: str
command: Tuple[str, ...]
command: tuple[str, ...]
parser_message: ContextVar[str] = ContextVar("parser_message")
@@ -117,6 +116,11 @@ class TrieRule:
# check whitespace
arg_str = segment_text[len(pf.key) :]
arg_str_stripped = arg_str.lstrip()
# check next segment until arg detected or no text remain
while not arg_str_stripped and msg and msg[0].is_text():
arg_str += str(msg.pop(0))
arg_str_stripped = arg_str.lstrip()
has_arg = arg_str_stripped or msg
if (
has_arg
@@ -144,7 +148,7 @@ class StartswithRule:
__slots__ = ("msg", "ignorecase")
def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False):
def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):
self.msg = msg
self.ignorecase = ignorecase
@@ -176,7 +180,7 @@ class StartswithRule:
return False
def startswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
def startswith(msg: Union[str, tuple[str, ...]], ignorecase: bool = False) -> Rule:
"""匹配消息纯文本开头。
参数:
@@ -199,7 +203,7 @@ class EndswithRule:
__slots__ = ("msg", "ignorecase")
def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False):
def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):
self.msg = msg
self.ignorecase = ignorecase
@@ -231,7 +235,7 @@ class EndswithRule:
return False
def endswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
def endswith(msg: Union[str, tuple[str, ...]], ignorecase: bool = False) -> Rule:
"""匹配消息纯文本结尾。
参数:
@@ -254,7 +258,7 @@ class FullmatchRule:
__slots__ = ("msg", "ignorecase")
def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False):
def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):
self.msg = tuple(map(str.casefold, msg) if ignorecase else msg)
self.ignorecase = ignorecase
@@ -285,7 +289,7 @@ class FullmatchRule:
return False
def fullmatch(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
def fullmatch(msg: Union[str, tuple[str, ...]], ignorecase: bool = False) -> Rule:
"""完全匹配消息。
参数:
@@ -356,7 +360,7 @@ class CommandRule:
def __init__(
self,
cmds: List[Tuple[str, ...]],
cmds: list[tuple[str, ...]],
force_whitespace: Optional[Union[str, bool]] = None,
):
self.cmds = tuple(cmds)
@@ -375,7 +379,7 @@ class CommandRule:
async def __call__(
self,
cmd: Optional[Tuple[str, ...]] = Command(),
cmd: Optional[tuple[str, ...]] = Command(),
cmd_arg: Optional[Message] = CommandArg(),
cmd_whitespace: Optional[str] = CommandWhitespace(),
) -> bool:
@@ -389,7 +393,7 @@ class CommandRule:
def command(
*cmds: Union[str, Tuple[str, ...]],
*cmds: Union[str, tuple[str, ...]],
force_whitespace: Optional[Union[str, bool]] = None,
) -> Rule:
"""匹配消息命令。
@@ -419,7 +423,7 @@ def command(
config = get_driver().config
command_start = config.command_start
command_sep = config.command_sep
commands: List[Tuple[str, ...]] = []
commands: list[tuple[str, ...]] = []
for command in cmds:
if isinstance(command, str):
command = (command,)
@@ -455,45 +459,38 @@ class ArgumentParser(ArgParser):
self,
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
namespace: None = None,
) -> Tuple[Namespace, List[Union[str, MessageSegment]]]:
...
) -> tuple[Namespace, list[Union[str, MessageSegment]]]: ...
@overload
def parse_known_args(
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
) -> Tuple[T, List[Union[str, MessageSegment]]]:
...
) -> tuple[T, list[Union[str, MessageSegment]]]: ...
@overload
def parse_known_args(
self, *, namespace: T
) -> Tuple[T, List[Union[str, MessageSegment]]]:
...
) -> tuple[T, list[Union[str, MessageSegment]]]: ...
def parse_known_args(
def parse_known_args( # pyright: ignore[reportIncompatibleMethodOverride]
self,
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
namespace: Optional[T] = None,
) -> Tuple[Union[Namespace, T], List[Union[str, MessageSegment]]]:
...
) -> tuple[Union[Namespace, T], list[Union[str, MessageSegment]]]: ...
@overload
def parse_args(
self,
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
namespace: None = None,
) -> Namespace:
...
) -> Namespace: ...
@overload
def parse_args(
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
) -> T:
...
) -> T: ...
@overload
def parse_args(self, *, namespace: T) -> T:
...
def parse_args(self, *, namespace: T) -> T: ...
def parse_args(
self,
@@ -508,7 +505,7 @@ class ArgumentParser(ArgParser):
def _parse_optional(
self, arg_string: Union[str, MessageSegment]
) -> Optional[Tuple[Optional[Action], str, Optional[str]]]:
) -> Optional[tuple[Optional[Action], str, Optional[str]]]:
return (
super()._parse_optional(arg_string) if isinstance(arg_string, str) else None
)
@@ -535,7 +532,7 @@ class ShellCommandRule:
__slots__ = ("cmds", "parser")
def __init__(self, cmds: List[Tuple[str, ...]], parser: Optional[ArgumentParser]):
def __init__(self, cmds: list[tuple[str, ...]], parser: Optional[ArgumentParser]):
self.cmds = tuple(cmds)
self.parser = parser
@@ -555,7 +552,7 @@ class ShellCommandRule:
async def __call__(
self,
state: T_State,
cmd: Optional[Tuple[str, ...]] = Command(),
cmd: Optional[tuple[str, ...]] = Command(),
msg: Optional[Message] = CommandArg(),
) -> bool:
if cmd not in self.cmds or msg is None:
@@ -573,7 +570,7 @@ class ShellCommandRule:
try:
args = self.parser.parse_args(state[SHELL_ARGV])
state[SHELL_ARGS] = args
except ArgumentError as e: # pragma: py-gte-39
except ArgumentError as e:
state[SHELL_ARGS] = ParserExit(status=2, message=str(e))
except ParserExit as e:
state[SHELL_ARGS] = e
@@ -583,7 +580,7 @@ class ShellCommandRule:
def shell_command(
*cmds: Union[str, Tuple[str, ...]], parser: Optional[ArgumentParser] = None
*cmds: Union[str, tuple[str, ...]], parser: Optional[ArgumentParser] = None
) -> Rule:
"""匹配 `shell_like` 形式的消息命令。
@@ -631,7 +628,7 @@ def shell_command(
config = get_driver().config
command_start = config.command_start
command_sep = config.command_sep
commands: List[Tuple[str, ...]] = []
commands: list[tuple[str, ...]] = []
for command in cmds:
if isinstance(command, str):
command = (command,)
@@ -742,7 +739,7 @@ class IsTypeRule:
__slots__ = ("types",)
def __init__(self, *types: Type[Event]):
def __init__(self, *types: type[Event]):
self.types = types
def __repr__(self) -> str:
@@ -758,7 +755,7 @@ class IsTypeRule:
return isinstance(event, self.types)
def is_type(*types: Type[Event]) -> Rule:
def is_type(*types: type[Event]) -> Rule:
"""匹配事件类型。
参数:

View File

@@ -6,33 +6,29 @@
[`typing`](https://docs.python.org/3/library/typing.html)。
FrontMatter:
mdx:
format: md
sidebar_position: 11
description: nonebot.typing 模块
"""
import sys
import types
import warnings
from typing_extensions import ParamSpec, TypeAlias, override
from typing import (
TYPE_CHECKING,
Any,
Dict,
Union,
TypeVar,
Callable,
Optional,
Awaitable,
)
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")
T_Wrapped: TypeAlias = Callable[P, T]
T_Wrapped: TypeAlias = t.Callable[P, T]
def overrides(InterfaceClass: object):
@@ -47,14 +43,90 @@ def overrides(InterfaceClass: object):
return override
if sys.version_info < (3, 10):
def type_has_args(type_: type[t.Any]) -> bool:
"""判断类型是否有参数"""
return isinstance(type_, (t._GenericAlias, types.GenericAlias)) # type: ignore
else:
def type_has_args(type_: type[t.Any]) -> bool:
return isinstance(type_, (t._GenericAlias, types.GenericAlias, types.UnionType)) # type: ignore
if sys.version_info < (3, 10):
def origin_is_union(origin: t.Optional[type[t.Any]]) -> bool:
"""判断是否是 Union 类型"""
return origin is t.Union
else:
def origin_is_union(origin: t.Optional[type[t.Any]]) -> bool:
return origin is t.Union or origin is types.UnionType
def origin_is_literal(origin: t.Optional[type[t.Any]]) -> bool:
"""判断是否是 Literal 类型"""
return origin is t.Literal or origin is t_ext.Literal
def _literal_values(type_: type[t.Any]) -> tuple[t.Any, ...]:
return get_args(type_)
def all_literal_values(type_: type[t.Any]) -> list[t.Any]:
"""获取 Literal 类型包含的所有值"""
if not origin_is_literal(get_origin(type_)):
return [type_]
return [x for value in _literal_values(type_) for x in all_literal_values(value)]
def origin_is_annotated(origin: t.Optional[type[t.Any]]) -> bool:
"""判断是否是 Annotated 类型"""
return origin is t_ext.Annotated
NONE_TYPES = {None, type(None), t.Literal[None], t_ext.Literal[None]}
if sys.version_info >= (3, 10):
NONE_TYPES.add(types.NoneType)
def is_none_type(type_: type[t.Any]) -> bool:
"""判断是否是 None 类型"""
return type_ in NONE_TYPES
def evaluate_forwardref(
ref: t.ForwardRef, globalns: dict[str, t.Any], localns: dict[str, t.Any]
) -> t.Any:
# Python 3.13/3.12.4+ made `recursive_guard` a kwarg,
# so name it explicitly to avoid:
# TypeError: ForwardRef._evaluate()
# missing 1 required keyword-only argument: 'recursive_guard'
return ref._evaluate(globalns, localns, recursive_guard=frozenset())
# state
T_State: TypeAlias = Dict[Any, Any]
# use annotated flag to avoid ForwardRef recreate generic type (py >= 3.11)
class StateFlag:
def __repr__(self) -> str:
return "StateFlag()"
_STATE_FLAG = StateFlag()
T_State: TypeAlias = t.Annotated[dict[t.Any, t.Any], _STATE_FLAG]
"""事件处理状态 State 类型"""
_DependentCallable: TypeAlias = Union[Callable[..., T], Callable[..., Awaitable[T]]]
_DependentCallable: TypeAlias = t.Union[
t.Callable[..., T], t.Callable[..., t.Awaitable[T]]
]
# driver hooks
T_BotConnectionHook: TypeAlias = _DependentCallable[Any]
T_BotConnectionHook: TypeAlias = _DependentCallable[t.Any]
"""Bot 连接建立时钩子函数
依赖参数:
@@ -63,7 +135,7 @@ T_BotConnectionHook: TypeAlias = _DependentCallable[Any]
- BotParam: Bot 对象
- DefaultParam: 带有默认值的参数
"""
T_BotDisconnectionHook: TypeAlias = _DependentCallable[Any]
T_BotDisconnectionHook: TypeAlias = _DependentCallable[t.Any]
"""Bot 连接断开时钩子函数
依赖参数:
@@ -74,15 +146,17 @@ T_BotDisconnectionHook: TypeAlias = _DependentCallable[Any]
"""
# api hooks
T_CallingAPIHook: TypeAlias = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]]
T_CallingAPIHook: TypeAlias = t.Callable[
["Bot", str, dict[str, t.Any]], t.Awaitable[t.Any]
]
"""`bot.call_api` 钩子函数"""
T_CalledAPIHook: TypeAlias = Callable[
["Bot", Optional[Exception], str, Dict[str, Any], Any], Awaitable[Any]
T_CalledAPIHook: TypeAlias = t.Callable[
["Bot", t.Optional[Exception], str, dict[str, t.Any], t.Any], t.Awaitable[t.Any]
]
"""`bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result"""
# event hooks
T_EventPreProcessor: TypeAlias = _DependentCallable[Any]
T_EventPreProcessor: TypeAlias = _DependentCallable[t.Any]
"""事件预处理函数 EventPreProcessor 类型
依赖参数:
@@ -93,8 +167,8 @@ T_EventPreProcessor: TypeAlias = _DependentCallable[Any]
- StateParam: State 对象
- DefaultParam: 带有默认值的参数
"""
T_EventPostProcessor: TypeAlias = _DependentCallable[Any]
"""事件处理函数 EventPostProcessor 类型
T_EventPostProcessor: TypeAlias = _DependentCallable[t.Any]
"""事件处理函数 EventPostProcessor 类型
依赖参数:
@@ -106,7 +180,7 @@ T_EventPostProcessor: TypeAlias = _DependentCallable[Any]
"""
# matcher run hooks
T_RunPreProcessor: TypeAlias = _DependentCallable[Any]
T_RunPreProcessor: TypeAlias = _DependentCallable[t.Any]
"""事件响应器运行前预处理函数 RunPreProcessor 类型
依赖参数:
@@ -118,7 +192,7 @@ T_RunPreProcessor: TypeAlias = _DependentCallable[Any]
- MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数
"""
T_RunPostProcessor: TypeAlias = _DependentCallable[Any]
T_RunPostProcessor: TypeAlias = _DependentCallable[t.Any]
"""事件响应器运行后后处理函数 RunPostProcessor 类型
依赖参数:
@@ -155,7 +229,7 @@ T_PermissionChecker: TypeAlias = _DependentCallable[bool]
- DefaultParam: 带有默认值的参数
"""
T_Handler: TypeAlias = _DependentCallable[Any]
T_Handler: TypeAlias = _DependentCallable[t.Any]
"""Handler 处理函数。"""
T_TypeUpdater: TypeAlias = _DependentCallable[str]
"""TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。
@@ -183,5 +257,5 @@ T_PermissionUpdater: TypeAlias = _DependentCallable["Permission"]
- MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数
"""
T_DependencyCache: TypeAlias = Dict[_DependentCallable[Any], "Task[Any]"]
T_DependencyCache: TypeAlias = dict[_DependentCallable[t.Any], "DependencyCache"]
"""依赖缓存, 用于存储依赖函数的返回值"""

View File

@@ -1,45 +1,46 @@
"""本模块包含了 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 contextvars import copy_context
from collections import deque
from functools import wraps, partial
from contextlib import asynccontextmanager
from contextlib import AbstractContextManager, asynccontextmanager
from typing_extensions import ParamSpec, get_args, override, get_origin
from typing import (
Any,
Type,
Tuple,
Union,
Generic,
TypeVar,
Callable,
Optional,
Coroutine,
AsyncGenerator,
ContextManager,
overload,
)
from typing import Any, Union, Generic, TypeVar, Callable, Optional, overload
from collections.abc import Mapping, Sequence, Coroutine, Generator, AsyncGenerator
from pydantic.typing import is_union, is_none_type
import anyio
import anyio.to_thread
from pydantic import BaseModel
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.log import logger
from nonebot.typing import (
is_none_type,
type_has_args,
origin_is_union,
origin_is_literal,
all_literal_values,
)
P = ParamSpec("P")
R = TypeVar("R")
T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")
E = TypeVar("E", bound=BaseException)
def escape_tag(s: str) -> str:
@@ -53,8 +54,36 @@ def escape_tag(s: str) -> str:
return re.sub(r"</?((?:[fb]g\s)?[^<>\s]*)>", r"\\\g<0>", s)
def deep_update(
mapping: dict[K, Any], *updating_mappings: dict[K, Any]
) -> dict[K, Any]:
"""深度更新合并字典"""
updated_mapping = mapping.copy()
for updating_mapping in updating_mappings:
for k, v in updating_mapping.items():
if (
k in updated_mapping
and isinstance(updated_mapping[k], dict)
and isinstance(v, dict)
):
updated_mapping[k] = deep_update(updated_mapping[k], v)
else:
updated_mapping[k] = v
return updated_mapping
def lenient_issubclass(
cls: Any, class_or_tuple: Union[type[Any], tuple[type[Any], ...]]
) -> bool:
"""检查 cls 是否是 class_or_tuple 中的一个类型子类并忽略类型错误。"""
try:
return isinstance(cls, type) and issubclass(cls, class_or_tuple)
except TypeError:
return False
def generic_check_issubclass(
cls: Any, class_or_tuple: Union[Type[Any], Tuple[Type[Any], ...]]
cls: Any, class_or_tuple: Union[type[Any], tuple[type[Any], ...]]
) -> bool:
"""检查 cls 是否是 class_or_tuple 中的一个类型子类。
@@ -62,33 +91,59 @@ def generic_check_issubclass(
- 如果 cls 是 `typing.Union` 或 `types.UnionType` 类型,
则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。
- 如果 cls 是 `typing.Literal` 类型,
则会检查其中的所有值是否是 class_or_tuple 中一个类型的实例。
- 如果 cls 是 `typing.TypeVar` 类型,
则会检查其 `__bound__` 或 `__constraints__`
是否是 class_or_tuple 中一个类型的子类或 None。
"""
try:
return issubclass(cls, class_or_tuple)
except TypeError:
origin = get_origin(cls)
if is_union(origin):
if not type_has_args(cls):
with contextlib.suppress(TypeError):
return issubclass(cls, class_or_tuple)
origin = get_origin(cls)
if origin_is_union(origin):
return all(
is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
for type_ in get_args(cls)
)
elif origin_is_literal(origin):
return all(
is_none_type(value) or isinstance(value, class_or_tuple)
for value in all_literal_values(cls)
)
# ensure generic List, Dict can be checked
elif origin:
# avoid class check error (typing.Final, typing.ClassVar, etc...)
try:
return issubclass(origin, class_or_tuple)
except TypeError:
return False
elif isinstance(cls, TypeVar):
if cls.__constraints__:
return all(
is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
for type_ in get_args(cls)
for type_ in cls.__constraints__
)
# ensure generic List, Dict can be checked
elif origin:
return issubclass(origin, class_or_tuple)
elif isinstance(cls, TypeVar):
if cls.__constraints__:
return all(
is_none_type(type_)
or generic_check_issubclass(type_, class_or_tuple)
for type_ in cls.__constraints__
)
elif cls.__bound__:
return generic_check_issubclass(cls.__bound__, class_or_tuple)
elif cls.__bound__:
return generic_check_issubclass(cls.__bound__, class_or_tuple)
return False
def type_is_complex(type_: type[Any]) -> bool:
"""检查 type_ 是否是复杂类型"""
origin = get_origin(type_)
return _type_is_complex_inner(type_) or _type_is_complex_inner(origin)
def _type_is_complex_inner(type_: Optional[type[Any]]) -> bool:
if lenient_issubclass(type_, (str, bytes)):
return False
return lenient_issubclass(
type_, (BaseModel, Mapping, Sequence, tuple, set, frozenset, deque)
) or dataclasses.is_dataclass(type_)
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
"""检查 call 是否是一个 callable 协程函数"""
@@ -125,18 +180,16 @@ 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
@asynccontextmanager
async def run_sync_ctx_manager(
cm: ContextManager[T],
cm: AbstractContextManager[T],
) -> AsyncGenerator[T, None]:
"""一个用于包装 sync context manager 为 async context manager 的执行函数"""
try:
@@ -152,24 +205,22 @@ async def run_sync_ctx_manager(
@overload
async def run_coro_with_catch(
coro: Coroutine[Any, Any, T],
exc: Tuple[Type[Exception], ...],
exc: tuple[type[Exception], ...],
return_on_err: None = None,
) -> Union[T, None]:
...
) -> Union[T, None]: ...
@overload
async def run_coro_with_catch(
coro: Coroutine[Any, Any, T],
exc: Tuple[Type[Exception], ...],
exc: tuple[type[Exception], ...],
return_on_err: R,
) -> Union[T, R]:
...
) -> Union[T, R]: ...
async def run_coro_with_catch(
coro: Coroutine[Any, Any, T],
exc: Tuple[Type[Exception], ...],
exc: tuple[type[Exception], ...],
return_on_err: Optional[R] = None,
) -> Optional[Union[T, R]]:
"""运行协程并当遇到指定异常时返回指定值。
@@ -183,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:
@@ -227,7 +302,7 @@ class classproperty(Generic[T]):
def __init__(self, func: Callable[[Any], T]) -> None:
self.func = func
def __get__(self, instance: Any, owner: Optional[Type[Any]] = None) -> T:
def __get__(self, instance: Any, owner: Optional[type[Any]] = None) -> T:
return self.func(type(instance) if owner is None else owner)

View File

@@ -17,7 +17,7 @@ _✨ NoneBot 本地文档插件 ✨_
<a href="https://pypi.python.org/pypi/nonebot-plugin-docs">
<img src="https://img.shields.io/pypi/v/nonebot-plugin-docs.svg" alt="pypi">
</a>
<img src="https://img.shields.io/badge/python-3.8+-blue.svg" alt="python">
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
</p>
## 使用方式

View File

@@ -12,7 +12,7 @@ include = ["nonebot_plugin_docs/dist/**/*"]
[tool.poetry.dependencies]
python = "^3.8"
python = "^3.9"
nonebot2 = "^2.0.0"
[tool.poetry.dev-dependencies]

2975
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.1.1"
version = "2.4.0"
description = "An asynchronous python bot framework."
authors = ["yanyongyu <yyy@nonebot.dev>"]
license = "MIT"
@@ -22,40 +22,38 @@ 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.8"
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"
pydantic = ">=1.10.0,<3.0.0,!=2.5.0,!=2.5.1"
tomli = { version = "^2.0.1", python = "<3.11" }
pydantic = { version = "^1.10.0", extras = ["dotenv"] }
websockets = { version = ">=10.0", optional = true }
Quart = { version = ">=0.18.0,<1.0.0", optional = true }
fastapi = { version = ">=0.93.0,<1.0.0", optional = true }
aiohttp = { version = "^3.7.4", extras = ["speedups"], optional = true }
aiohttp = { version = "^3.9.0b0", extras = ["speedups"], optional = true }
httpx = { version = ">=0.20.0,<1.0.0", extras = ["http2"], optional = true }
uvicorn = { version = ">=0.20.0,<1.0.0", extras = [
"standard",
], optional = true }
[tool.poetry.group.dev.dependencies]
ruff = "^0.7.0"
isort = "^5.10.1"
black = "^23.1.0"
black = "^24.0.0"
nonemoji = "^0.1.2"
pre-commit = "^3.0.0"
ruff = ">=0.0.272,<1.0.0"
[tool.poetry.group.test.dependencies]
nonebug = "^0.3.0"
werkzeug = "^2.3.6"
pytest-cov = "^4.0.0"
pytest-xdist = "^3.0.2"
pytest-asyncio = "^0.21.0"
coverage-conditional-plugin = "^0.9.0"
nonebot-test = { path = "./envs/test/", develop = false }
[tool.poetry.group.docs.dependencies]
nb-autodoc = "^1.0.0a5"
@@ -69,13 +67,12 @@ fastapi = ["fastapi", "uvicorn"]
all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
[tool.pytest.ini_options]
asyncio_mode = "strict"
addopts = "--cov=nonebot --cov-append --cov-report=term-missing"
addopts = "--cov=nonebot --cov-report=term-missing"
filterwarnings = ["error", "ignore::DeprecationWarning"]
[tool.black]
line-length = 88
target-version = ["py38", "py39", "py310", "py311"]
target-version = ["py39", "py310", "py311", "py312"]
include = '\.pyi?$'
extend-exclude = '''
'''
@@ -90,19 +87,40 @@ src_paths = ["nonebot", "tests"]
extra_standard_library = ["typing_extensions"]
[tool.ruff]
select = ["E", "W", "F", "UP", "C", "T", "PYI", "PT", "Q"]
ignore = ["E402", "C901", "UP037"]
line-length = 88
target-version = "py38"
target-version = "py39"
[tool.ruff.flake8-pytest-style]
[tool.ruff.lint]
select = [
"F", # Pyflakes
"W", # pycodestyle warnings
"E", # pycodestyle errors
"UP", # pyupgrade
"ASYNC", # flake8-async
"C4", # flake8-comprehensions
"T10", # flake8-debugger
"T20", # flake8-print
"PYI", # flake8-pyi
"PT", # flake8-pytest-style
"Q", # flake8-quotes
"RUF", # Ruff-specific rules
]
ignore = [
"E402", # module-import-not-at-top-of-file
"UP037", # quoted-annotation
"RUF001", # ambiguous-unicode-character-string
"RUF002", # ambiguous-unicode-character-docstring
"RUF003", # ambiguous-unicode-character-comment
]
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = false
[tool.pyright]
pythonVersion = "3.8"
pythonVersion = "3.9"
pythonPlatform = "All"
defineConstant = { PYDANTIC_V2 = true }
executionEnvironments = [
{ root = "./tests", extraPaths = [
"./",
@@ -110,7 +128,7 @@ executionEnvironments = [
{ root = "./" },
]
typeCheckingMode = "basic"
typeCheckingMode = "standard"
reportShadowedImports = false
disableBytesTypePromotions = true

11
scripts/build-api-docs.sh Executable file
View File

@@ -0,0 +1,11 @@
#! /usr/bin/env bash
# cd to the root of the project
cd "$(dirname "$0")/.."
poetry run nb-autodoc nonebot \
-s nonebot.plugins \
-u nonebot.internal \
-u nonebot.internal.*
cp -r ./build/nonebot/* ./website/docs/api/
yarn prettier

7
scripts/run-tests.sh Executable file
View File

@@ -0,0 +1,7 @@
#! /usr/bin/env bash
# cd to the root of the tests
cd "$(dirname "$0")/../tests"
# Run the tests
pytest -n auto --cov-append --cov-report xml $@

14
scripts/setup-envs.sh Executable file
View File

@@ -0,0 +1,14 @@
#! /usr/bin/env bash
# config poetry to install env in project
poetry config virtualenvs.in-project true
# setup dev environment
echo "Setting up dev environment"
poetry install --all-extras && poetry run pre-commit install && yarn install
# setup pydantic v2 test environment
for env in $(find ./envs/ -maxdepth 1 -mindepth 1 -type d -not -name test); do
echo "Setting up $env environment"
(cd $env && poetry install --no-root)
done

15
scripts/update-envs.sh Executable file
View File

@@ -0,0 +1,15 @@
#! /usr/bin/env bash
# update test env
echo "Updating test env..."
(cd ./envs/test/ && poetry update --lock)
# update dev env
echo "Updating dev env..."
poetry update
# update other envs
for env in $(find ./envs/ -maxdepth 1 -mindepth 1 -type d -not -name test); do
echo "Updating $env env..."
(cd $env && poetry update)
done

View File

@@ -12,7 +12,7 @@ exclude_lines =
@(abc\.)?abstractmethod
raise NotImplementedError
warnings\.warn
\.\.\.
^\.\.\.$
pass
if __name__ == .__main__.:
@@ -21,6 +21,7 @@ rules =
"sys_platform != 'win32'": py-win32
"sys_platform != 'linux'": py-linux
"sys_platform != 'darwin'": py-darwin
"sys_version_info < (3, 9)": py-gte-39
"sys_version_info < (3, 11)": py-gte-311
"sys_version_info >= (3, 11)": py-lt-311
"package_version('pydantic') < (2,)": pydantic-v2
"package_version('pydantic') >= (2,)": pydantic-v1

17
tests/.env.example Normal file
View File

@@ -0,0 +1,17 @@
SIMPLE=simple
COMPLEX='
[1, 2, 3]
'
COMPLEX_NONE
COMPLEX_UNION=[1, 2, 3]
NESTED={"a": 1}
NESTED__B=2
NESTED__C__C=3
NESTED__COMPLEX=[1, 2, 3]
NESTED_INNER__A=1
NESTED_INNER__B=2
OTHER_SIMPLE=simple
OTHER_NESTED={"a": 1}
OTHER_NESTED__B=2
OTHER_NESTED_INNER__A=1
OTHER_NESTED_INNER__B=2

View File

@@ -13,3 +13,4 @@ NESTED_MISSING_DICT__A=1
NESTED_MISSING_DICT__B__C=2
NOT_NESTED=some string
NOT_NESTED__A=1
PLUGIN_CONFIG=1

View File

@@ -1,7 +1,10 @@
import os
import threading
from pathlib import Path
from typing import TYPE_CHECKING, Set, Generator
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
@@ -19,6 +22,11 @@ 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/"]
def pytest_configure(config: pytest.Config) -> None:
config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"}
@@ -35,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,9 +1,15 @@
import json
import base64
from typing import Dict, List, Union, TypeVar
import socket
from typing import Union, TypeVar
from wsproto.events import Ping
from werkzeug import Request, Response
from werkzeug.datastructures import MultiDict
from wsproto.frame_protocol import CloseReason
from wsproto.events import Request as WSRequest
from wsproto import WSConnection, ConnectionType
from wsproto.events import TextMessage, BytesMessage, CloseConnection, AcceptConnection
K = TypeVar("K")
V = TypeVar("V")
@@ -25,12 +31,11 @@ def json_safe(string, content_type="application/octet-stream") -> str:
).decode("utf-8")
def flattern(d: "MultiDict[K, V]") -> Dict[K, Union[V, List[V]]]:
def flattern(d: "MultiDict[K, V]") -> dict[K, Union[V, list[V]]]:
return {k: v[0] if len(v) == 1 else v for k, v in d.to_dict(flat=False).items()}
@Request.application
def request_handler(request: Request) -> Response:
def http_echo(request: Request) -> Response:
try:
_json = json.loads(request.data.decode("utf-8"))
except (ValueError, TypeError):
@@ -67,3 +72,65 @@ def request_handler(request: Request) -> Response:
status=200,
content_type="application/json",
)
def websocket_echo(request: Request) -> Response:
stream = request.environ["werkzeug.socket"]
ws = WSConnection(ConnectionType.SERVER)
in_data = b"GET %s HTTP/1.1\r\n" % request.path.encode("utf-8")
for header, value in request.headers.items():
in_data += f"{header}: {value}\r\n".encode()
in_data += b"\r\n"
ws.receive_data(in_data)
running: bool = True
while True:
out_data = b""
for event in ws.events():
if isinstance(event, WSRequest):
out_data += ws.send(AcceptConnection())
elif isinstance(event, CloseConnection):
out_data += ws.send(event.response())
running = False
elif isinstance(event, Ping):
out_data += ws.send(event.response())
elif isinstance(event, TextMessage):
if event.data == "quit":
out_data += ws.send(
CloseConnection(CloseReason.NORMAL_CLOSURE, "bye")
)
running = False
else:
out_data += ws.send(TextMessage(data=event.data))
elif isinstance(event, BytesMessage):
if event.data == b"quit":
out_data += ws.send(
CloseConnection(CloseReason.NORMAL_CLOSURE, "bye")
)
running = False
else:
out_data += ws.send(BytesMessage(data=event.data))
if out_data:
stream.send(out_data)
if not running:
break
in_data = stream.recv(4096)
ws.receive_data(in_data)
stream.shutdown(socket.SHUT_RDWR)
return Response("", status=204)
@Request.application
def request_handler(request: Request) -> Response:
if request.headers.get("Connection") == "Upgrade":
return websocket_echo(request)
else:
return http_echo(request)

View File

@@ -78,8 +78,7 @@ async def reject_preset(a: str = ArgStr(), b: str = ArgStr()):
test_overload = on_message()
class FakeEvent(Event):
...
class FakeEvent(Event): ...
@test_overload.got("a")

View File

@@ -8,8 +8,7 @@ class Config(BaseModel):
custom: str = ""
class FakeAdapter(Adapter):
...
class FakeAdapter(Adapter): ...
__plugin_meta__ = PluginMetadata(

View File

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

View File

@@ -8,5 +8,5 @@ manager = PluginManager(
_managers.append(manager)
# test load nested plugin with require
manager.load_plugin("nested_subplugin")
manager.load_plugin("nested_subplugin2")
manager.load_plugin("plugins.nested.plugins.nested_subplugin")
manager.load_plugin("nested:nested_subplugin2")

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