Compare commits

..

238 Commits

Author SHA1 Message Date
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
396 changed files with 22889 additions and 12413 deletions

View File

@@ -4,7 +4,7 @@
"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": {

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,20 @@ 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

@@ -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"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
os: [ubuntu-latest, windows-latest, macos-latest]
env: [pydantic-v1, pydantic-v2]
fail-fast: false
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

@@ -47,7 +47,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 +73,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 +84,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 }}
@@ -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

@@ -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

@@ -7,26 +7,26 @@ 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.2.0
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
stages: [commit]
- repo: https://github.com/pycqa/isort
rev: 5.12.0
rev: 5.13.2
hooks:
- id: isort
stages: [commit]
- repo: https://github.com/psf/black
rev: 23.9.1
rev: 24.1.1
hooks:
- id: black
stages: [commit]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.3
rev: v4.0.0-alpha.8
hooks:
- id: prettier
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]

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"
}
}
]
}

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

@@ -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>
@@ -116,11 +116,13 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
| 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/) | ✅ | 官方接口调整较多 |
| QQ[仓库](https://github.com/nonebot/adapter-qq)[协议](https://bot.q.qq.com/wiki/) | ✅ | QQ 官方接口调整较多 |
| 钉钉([仓库](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 协议 |
| 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 协议 |
| 开黑啦([仓库](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) | ↗️ | 微信协议,由社区贡献 |
@@ -230,10 +232,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 +285,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
},
@@ -188,5 +188,30 @@
"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
}
]

View File

@@ -574,5 +574,38 @@
}
],
"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
}
]

5556
assets/plugins.json Normal file

File diff suppressed because it is too large Load Diff

2166
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.8"
[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"

2238
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.8"
[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

1029
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.8"
nonebug = "^0.3.0"
wsproto = "^1.2.0"
pytest-cov = "^4.0.0"
pytest-xdist = "^3.0.2"
pytest-asyncio = "^0.23.2"
werkzeug = ">=2.3.6,<4.0.0"
coverage-conditional-plugin = "^0.9.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -35,6 +35,7 @@
{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:
@@ -47,11 +48,11 @@ from importlib.metadata import version
from typing import Any, Dict, Type, 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
@@ -273,7 +274,7 @@ def _log_patcher(record: "loguru.Record"):
)
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 +297,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)
_env_file=(
(".env", _env_file)
if isinstance(_env_file, (str, os.PathLike))
else _env_file,
else _env_file
),
)
logger.configure(
@@ -308,7 +311,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 +356,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}

383
nonebot/compat.py Normal file
View File

@@ -0,0 +1,383 @@
"""本模块为 Pydantic 版本兼容层模块
为兼容 Pydantic V1 与 V2 版本,定义了一系列兼容函数与类供使用。
FrontMatter:
sidebar_position: 16
description: nonebot.compat 模块
"""
from dataclasses import dataclass, is_dataclass
from typing_extensions import Self, Annotated, get_args, get_origin, is_typeddict
from typing import (
TYPE_CHECKING,
Any,
Set,
Dict,
List,
Type,
Union,
TypeVar,
Callable,
Optional,
Protocol,
Generator,
)
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",
"extract_field_info",
"model_field_validate",
"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_core import CoreSchema, core_schema
from pydantic._internal._repr import display_as_type
from pydantic import TypeAdapter, GetCoreSchemaHandler
from pydantic.fields import FieldInfo as BaseFieldInfo
Required = Ellipsis
"""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 _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 __hash__(self) -> int:
# Each ModelField is unique for our purposes,
# to allow store them in a set.
return id(self)
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_field_validate(
model_field: ModelField, value: Any, config: Optional[ConfigDict] = None
) -> Any:
"""Validate the value pass to the field."""
type: Any = Annotated[model_field.annotation, model_field.field_info]
return TypeAdapter(
type, config=None if model_field._annotation_has_config() else config
).validate_python(value)
def model_fields(model: Type[BaseModel]) -> List[ModelField]:
"""Get field list of a model."""
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 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_field_validate(
model_field: ModelField, value: Any, config: Optional[Type[ConfigDict]] = None
) -> Any:
"""Validate the value pass to the field.
Set config before validate to ensure validate correctly.
"""
if model_field.model_config is not config:
model_field.set_config(config or ConfigDict)
v, errs_ = model_field.validate(value, {}, loc=())
if errs_:
raise ValueError(value, model_field)
return v
def model_fields(model: Type[BaseModel]) -> List[ModelField]:
"""Get field list of a model."""
# 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

@@ -12,76 +12,256 @@ FrontMatter:
"""
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 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 typing_extensions import TypeAlias, get_args, get_origin
from typing import (
TYPE_CHECKING,
Any,
Set,
Dict,
List,
Type,
Tuple,
Union,
Mapping,
Optional,
)
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] = {}
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)
for field in model_fields(self.settings_cls):
field_name = field.name
env_name = self._apply_case_sensitive(field_name)
# 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]
if env_val is not Undefined:
break
is_complex, allow_parse_failure = self.field_is_complex(field)
is_complex, allow_parse_failure = self._field_is_complex(field)
if is_complex:
if 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 +269,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 +293,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
if PYDANTIC_V2: # pragma: pydantic-v2
model_config: SettingsConfig = SettingsConfig(
extra="allow",
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
env_nested_delimiter="__",
)
else: # pragma: pydantic-v1
class Config(SettingsConfig):
extra = "allow" # type: ignore
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = False
env_nested_delimiter = "__"
@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,
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 +378,8 @@ class Env(BaseConfig):
NoneBot 将从 `.env.{environment}` 文件中加载配置。
"""
class Config:
env_file = ".env"
class Config(BaseConfig):
class Config(BaseSettings):
"""NoneBot 主要配置。大小写不敏感。
除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。
@@ -169,7 +388,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"
@@ -241,9 +461,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 +473,19 @@ class Config(BaseConfig):
# custom configs can be assigned during nonebot.init
# or from env file using json loads
class Config:
if PYDANTIC_V2: # pragma: pydantic-v2
model_config = SettingsConfig(env_file=(".env", ".env.prod"))
else: # pragma: pydantic-v1
class Config(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

@@ -24,14 +24,11 @@ from typing import (
cast,
)
from pydantic import BaseConfig
from pydantic.schema import get_annotation_from_field_info
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
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 .utils import check_field_type, get_typed_signature
@@ -69,10 +66,6 @@ class Param(abc.ABC, FieldInfo):
return
class CustomConfig(BaseConfig):
arbitrary_types_allowed = True
@dataclass(frozen=True)
class Dependent(Generic[R]):
"""依赖注入容器
@@ -125,12 +118,8 @@ class Dependent(Generic[R]):
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 +130,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
)
)
@@ -207,7 +184,7 @@ class Dependent(Generic[R]):
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

View File

@@ -8,10 +8,10 @@ import inspect
from typing import Any, Dict, Callable, ForwardRef
from loguru import logger
from pydantic.fields import ModelField
from pydantic.typing import evaluate_forwardref
from nonebot.exception import TypeMisMatch
from nonebot.typing import evaluate_forwardref
from nonebot.compat import DEFAULT_CONFIG, ModelField, model_field_validate
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
@@ -50,7 +50,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 model_field_validate(field, value, DEFAULT_CONFIG)
except ValueError:
raise TypeMisMatch(field, value)
return v

View File

@@ -179,8 +179,7 @@ class WebSocket(BaseWebSocket):
if TYPE_CHECKING:
class Driver(Mixin, NoneDriver):
...
class Driver(Mixin, NoneDriver): ...
else:
Driver = combine_driver(NoneDriver, Mixin)

View File

@@ -15,14 +15,13 @@ FrontMatter:
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 pydantic import BaseSettings
from pydantic import BaseModel
from nonebot.config import Env
from nonebot.drivers import ASGIMixin
@@ -32,10 +31,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 +59,7 @@ def catch_closed(func):
return decorator
class Config(BaseSettings):
class Config(BaseModel):
"""FastAPI 驱动框架设置,详情参考 FastAPI 文档"""
fastapi_openapi_url: Optional[str] = None
@@ -85,9 +83,6 @@ class Config(BaseSettings):
fastapi_extra: Dict[str, Any] = {}
"""传递给 `FastAPI` 的其他参数。"""
class Config:
extra = "ignore"
class Driver(BaseDriver, ASGIMixin):
"""FastAPI 驱动框架。"""
@@ -95,9 +90,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 +148,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()

View File

@@ -72,8 +72,7 @@ class Mixin(HTTPClientMixin):
if TYPE_CHECKING:
class Driver(Mixin, NoneDriver):
...
class Driver(Mixin, NoneDriver): ...
else:
Driver = combine_driver(NoneDriver, Mixin)

View File

@@ -19,8 +19,6 @@ from nonebot.consts import WINDOWS
from nonebot.config import Env, Config
from nonebot.drivers import Driver as BaseDriver
from ._lifespan import LIFESPAN_FUNC, Lifespan
HANDLED_SIGNALS = (
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
@@ -35,8 +33,6 @@ 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
@@ -52,16 +48,6 @@ 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"""

View File

@@ -18,20 +18,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, Dict, List, Tuple, Union, Optional, cast
from pydantic import BaseSettings
from pydantic import BaseModel
from nonebot.config import Env
from nonebot.drivers import ASGIMixin
@@ -41,6 +30,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 +47,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,7 +59,7 @@ def catch_closed(func):
return decorator
class Config(BaseSettings):
class Config(BaseModel):
"""Quart 驱动框架设置"""
quart_reload: bool = False
@@ -87,9 +75,6 @@ class Config(BaseSettings):
quart_extra: Dict[str, Any] = {}
"""传递给 `Quart` 的其他参数。"""
class Config:
extra = "ignore"
class Driver(BaseDriver, ASGIMixin):
"""Quart 驱动框架"""
@@ -97,11 +82,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,16 +137,6 @@ 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,

View File

@@ -50,10 +50,7 @@ def catch_closed(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[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
@@ -131,8 +128,7 @@ class WebSocket(BaseWebSocket):
if TYPE_CHECKING:
class Driver(Mixin, NoneDriver):
...
class Driver(Mixin, NoneDriver): ...
else:
Driver = combine_driver(NoneDriver, Mixin)

View File

@@ -31,7 +31,7 @@ FrontMatter:
from typing import Any, Optional
from pydantic.fields import ModelField
from nonebot.compat import ModelField
class NoneBotException(Exception):

View File

@@ -3,6 +3,7 @@ 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,
@@ -97,6 +98,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

@@ -14,8 +14,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):

View File

@@ -4,6 +4,7 @@ from typing import Any, Type, TypeVar
from pydantic import BaseModel
from nonebot.utils import DataclassEncoder
from nonebot.compat import PYDANTIC_V2, ConfigDict
from .message import Message
@@ -13,10 +14,16 @@ E = TypeVar("E", bound="Event")
class Event(abc.ABC, BaseModel):
"""Event 基类。提供获取关键信息的方法,其余信息可直接获取。"""
class Config:
extra = "allow"
if PYDANTIC_V2: # pragma: pydantic-v2
model_config = ConfigDict(extra="allow")
else: # pragma: pydantic-v1
class Config(ConfigDict):
extra = "allow" # type: ignore
json_encoders = {Message: DataclassEncoder}
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):

View File

@@ -17,7 +17,7 @@ from typing import (
overload,
)
from pydantic import parse_obj_as
from nonebot.compat import custom_validation, type_validate_python
from .template import MessageTemplate
@@ -25,6 +25,7 @@ TMS = TypeVar("TMS", bound="MessageSegment")
TM = TypeVar("TM", bound="Message")
@custom_validation
@dataclass
class MessageSegment(abc.ABC, Generic[TM]):
"""消息段基类"""
@@ -65,6 +66,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,6 +100,7 @@ class MessageSegment(abc.ABC, Generic[TM]):
raise NotImplementedError
@custom_validation
class Message(List[TMS], abc.ABC):
"""消息序列
@@ -158,9 +162,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)}"
@@ -281,7 +285,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:

View File

@@ -20,9 +20,16 @@ from typing import (
overload,
)
from _string import formatter_field_name_split # type: ignore
if TYPE_CHECKING:
from .message import Message, MessageSegment
def formatter_field_name_split( # noqa: F811
field_name: str,
) -> Tuple[str, List[Tuple[bool, str]]]: ...
TM = TypeVar("TM", bound="Message")
TF = TypeVar("TF", str, "Message")
@@ -36,26 +43,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.private_getattr = private_getattr
def __repr__(self) -> str:
return f"MessageTemplate({self.template!r}, factory={self.factory!r})"
@@ -167,6 +183,19 @@ 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):

View File

@@ -11,6 +11,7 @@ LIFESPAN_FUNC: TypeAlias = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
class Lifespan:
def __init__(self) -> None:
self._startup_funcs: List[LIFESPAN_FUNC] = []
self._ready_funcs: List[LIFESPAN_FUNC] = []
self._shutdown_funcs: List[LIFESPAN_FUNC] = []
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
@@ -21,6 +22,10 @@ class Lifespan:
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: List[LIFESPAN_FUNC],
@@ -35,6 +40,9 @@ class Lifespan:
if self._startup_funcs:
await self._run_lifespan_func(self._startup_funcs)
if self._ready_funcs:
await self._run_lifespan_func(self._ready_funcs)
async def shutdown(self) -> None:
if self._shutdown_funcs:
await self._run_lifespan_func(self._shutdown_funcs)

View File

@@ -2,7 +2,7 @@ import abc
import asyncio
from typing_extensions import TypeAlias
from contextlib import AsyncExitStack, asynccontextmanager
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
from typing import TYPE_CHECKING, Any, Set, Dict, Type, AsyncGenerator
from nonebot.log import logger
from nonebot.config import Env, Config
@@ -16,6 +16,7 @@ from nonebot.typing import (
T_BotDisconnectionHook,
)
from ._lifespan import LIFESPAN_FUNC, Lifespan
from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup
if TYPE_CHECKING:
@@ -49,6 +50,7 @@ class Driver(abc.ABC):
"""全局配置对象"""
self._bots: Dict[str, "Bot"] = {}
self._bot_tasks: Set[asyncio.Task] = set()
self._lifespan = Lifespan()
def __repr__(self) -> str:
return (
@@ -100,15 +102,13 @@ class Driver(abc.ABC):
self.on_shutdown(self._cleanup)
@abc.abstractmethod
def on_startup(self, func: Callable) -> Callable:
"""注册一个在驱动器启动时执行的函数"""
raise NotImplementedError
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""注册一个启动时执行的函数"""
return self._lifespan.on_startup(func)
@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:

View File

@@ -6,18 +6,15 @@ 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], *mixins: Type[Mixin]) -> Type["CombinedDriver"]: ...
def combine_driver(
@@ -35,9 +32,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

@@ -65,12 +65,10 @@ class MatcherManager(MutableMapping[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

View File

@@ -262,16 +262,20 @@ class Matcher(metaclass=MatcherMeta):
"type": type_,
"rule": rule or Rule(),
"permission": permission or Permission(),
"handlers": [
"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 [],
else []
),
"temp": temp,
"expire_time": (
expire_time
@@ -313,7 +317,7 @@ class Matcher(metaclass=MatcherMeta):
matchers[priority].append(NewMatcher)
return NewMatcher
return NewMatcher # type: ignore
@classmethod
def destroy(cls) -> None:
@@ -658,12 +662,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 +682,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 +697,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 +722,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)

View File

@@ -1,7 +1,7 @@
import asyncio
import inspect
from typing_extensions import Self, Annotated, override
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
from typing_extensions import Self, Annotated, get_args, override, get_origin
from typing import (
TYPE_CHECKING,
Any,
@@ -14,12 +14,12 @@ from typing import (
cast,
)
from pydantic.typing import get_args, get_origin
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
from pydantic.fields import FieldInfo as PydanticFieldInfo
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.utils import (
get_name,
run_sync,
@@ -34,23 +34,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 +41,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 +58,7 @@ def Depends(
dependency: Optional[T_Handler] = None,
*,
use_cache: bool = True,
validate: Union[bool, FieldInfo] = False,
validate: Union[bool, PydanticFieldInfo] = False,
) -> Any:
"""子依赖装饰器
@@ -113,24 +96,32 @@ class DependParam(Param):
本注入应该具有最高优先级,因此应该在其他参数之前检查。
"""
def __init__(
self, *args, dependent: Dependent, 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,
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
@@ -191,10 +182,10 @@ 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: Dependent = self.dependent
call = cast(Callable[..., Any], sub_dependent.call)
# solve sub dependency with current cache
@@ -231,8 +222,7 @@ class DependParam(Param):
@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,14 +233,16 @@ 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 "")
+ ")"
)
@@ -265,18 +257,13 @@ 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:
@@ -284,8 +271,8 @@ class BotParam(Param):
@override
async def _check(self, bot: "Bot", **kwargs: Any) -> None:
if checker := self.extra.get("checker"):
check_field_type(checker, bot)
if self.checker is not None:
check_field_type(self.checker, bot)
class EventParam(Param):
@@ -296,14 +283,16 @@ 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 "")
+ ")"
)
@@ -318,18 +307,13 @@ 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:
@@ -337,8 +321,8 @@ class EventParam(Param):
@override
async def _check(self, event: "Event", **kwargs: Any) -> Any:
if checker := self.extra.get("checker", None):
check_field_type(checker, event)
if self.checker is not None:
check_field_type(self.checker, event)
class StateParam(Param):
@@ -359,10 +343,10 @@ class StateParam(Param):
) -> Optional[Self]:
# param type is T_State
if param.annotation is T_State:
return cls(Required)
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:
@@ -377,8 +361,18 @@ 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
@@ -391,18 +385,13 @@ 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:
@@ -410,16 +399,16 @@ class MatcherParam(Param):
@override
async def _check(self, matcher: "Matcher", **kwargs: Any) -> Any:
if checker := self.extra.get("checker", None):
check_field_type(checker, matcher)
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,8 +438,19 @@ 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
@@ -458,22 +458,19 @@ class ArgParam(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)
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()
@@ -497,10 +494,10 @@ class ExceptionParam(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:
@@ -524,11 +521,11 @@ class DefaultParam(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

@@ -39,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
)
)
for checker in checkers
}
"""存储 `PermissionChecker`"""

View File

@@ -38,11 +38,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
)
)
for checker in checkers
}
"""存储 `RuleChecker`"""

View File

@@ -5,7 +5,18 @@ FrontMatter:
description: nonebot.params 模块
"""
from typing import Any, Dict, List, Match, Tuple, Union, Optional
from typing import (
Any,
Dict,
List,
Match,
Tuple,
Union,
Literal,
Callable,
Optional,
overload,
)
from nonebot.typing import T_State
from nonebot.matcher import Matcher
@@ -147,13 +158,34 @@ 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, ...]:

View File

@@ -39,7 +39,14 @@ FrontMatter:
from itertools import chain
from types import ModuleType
from contextvars import ContextVar
from typing import Set, Dict, List, Tuple, Optional
from typing import Set, Dict, List, Type, Tuple, TypeVar, Optional
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"] = []
@@ -108,6 +115,11 @@ 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

@@ -213,8 +213,10 @@ def inherit_supported_adapters(*names: str) -> Optional[Set[str]]:
)
return final_supported and {
(
f"nonebot.adapters.{adapter_name[1:]}"
if adapter_name.startswith("~")
else adapter_name
)
for adapter_name in final_supported
}

View File

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

View File

@@ -117,6 +117,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
@@ -455,45 +460,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(
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,

View File

@@ -10,18 +10,14 @@ FrontMatter:
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 contextlib
import typing as t
import typing_extensions as t_ext
from typing import TYPE_CHECKING, TypeVar
from typing_extensions import ParamSpec, TypeAlias, get_args, override, get_origin
if TYPE_CHECKING:
from asyncio import Task
@@ -32,7 +28,7 @@ if TYPE_CHECKING:
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,77 @@ def overrides(InterfaceClass: object):
return override
if sys.version_info < (3, 10):
def origin_is_union(origin: t.Optional[t.Type[t.Any]]) -> bool:
"""判断是否是 Union 类型"""
return origin is t.Union
else:
def origin_is_union(origin: t.Optional[t.Type[t.Any]]) -> bool:
return origin is t.Union or origin is types.UnionType
def origin_is_literal(origin: t.Optional[t.Type[t.Any]]) -> bool:
"""判断是否是 Literal 类型"""
return origin is t.Literal or origin is t_ext.Literal
def _literal_values(type_: t.Type[t.Any]) -> t.Tuple[t.Any, ...]:
return get_args(type_)
def all_literal_values(type_: t.Type[t.Any]) -> t.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[t.Type[t.Any]]) -> bool:
"""判断是否是 Annotated 类型"""
with contextlib.suppress(TypeError):
return origin is not None and issubclass(origin, t_ext.Annotated)
return False
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_: t.Type[t.Any]) -> bool:
"""判断是否是 None 类型"""
return type_ in NONE_TYPES
if sys.version_info < (3, 9): # pragma: py-lt-39
def evaluate_forwardref(
ref: t.ForwardRef, globalns: t.Dict[str, t.Any], localns: t.Dict[str, t.Any]
) -> t.Any:
return ref._evaluate(globalns, localns)
else: # pragma: py-gte-39
def evaluate_forwardref(
ref: t.ForwardRef, globalns: t.Dict[str, t.Any], localns: t.Dict[str, t.Any]
) -> t.Any:
return ref._evaluate(globalns, localns, frozenset())
# state
T_State: TypeAlias = Dict[Any, Any]
T_State: TypeAlias = t.Dict[t.Any, t.Any]
"""事件处理状态 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 +122,7 @@ T_BotConnectionHook: TypeAlias = _DependentCallable[Any]
- BotParam: Bot 对象
- DefaultParam: 带有默认值的参数
"""
T_BotDisconnectionHook: TypeAlias = _DependentCallable[Any]
T_BotDisconnectionHook: TypeAlias = _DependentCallable[t.Any]
"""Bot 连接断开时钩子函数
依赖参数:
@@ -74,15 +133,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, t.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, t.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 +154,8 @@ T_EventPreProcessor: TypeAlias = _DependentCallable[Any]
- StateParam: State 对象
- DefaultParam: 带有默认值的参数
"""
T_EventPostProcessor: TypeAlias = _DependentCallable[Any]
"""事件处理函数 EventPostProcessor 类型
T_EventPostProcessor: TypeAlias = _DependentCallable[t.Any]
"""事件处理函数 EventPostProcessor 类型
依赖参数:
@@ -106,7 +167,7 @@ T_EventPostProcessor: TypeAlias = _DependentCallable[Any]
"""
# matcher run hooks
T_RunPreProcessor: TypeAlias = _DependentCallable[Any]
T_RunPreProcessor: TypeAlias = _DependentCallable[t.Any]
"""事件响应器运行前预处理函数 RunPreProcessor 类型
依赖参数:
@@ -118,7 +179,7 @@ T_RunPreProcessor: TypeAlias = _DependentCallable[Any]
- MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数
"""
T_RunPostProcessor: TypeAlias = _DependentCallable[Any]
T_RunPostProcessor: TypeAlias = _DependentCallable[t.Any]
"""事件响应器运行后后处理函数 RunPostProcessor 类型
依赖参数:
@@ -155,7 +216,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 +244,5 @@ T_PermissionUpdater: TypeAlias = _DependentCallable["Permission"]
- MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数
"""
T_DependencyCache: TypeAlias = Dict[_DependentCallable[Any], "Task[Any]"]
T_DependencyCache: TypeAlias = t.Dict[_DependentCallable[t.Any], "Task[t.Any]"]
"""依赖缓存, 用于存储依赖函数的返回值"""

View File

@@ -12,28 +12,38 @@ import inspect
import importlib
import dataclasses
from pathlib import Path
from collections import deque
from contextvars import copy_context
from functools import wraps, partial
from contextlib import asynccontextmanager
from typing_extensions import ParamSpec, get_args, override, get_origin
from typing import (
Any,
Dict,
Type,
Tuple,
Union,
Generic,
Mapping,
TypeVar,
Callable,
Optional,
Sequence,
Coroutine,
AsyncGenerator,
ContextManager,
overload,
)
from pydantic.typing import is_union, is_none_type
from pydantic import BaseModel
from nonebot.log import logger
from nonebot.typing import (
is_none_type,
origin_is_union,
origin_is_literal,
all_literal_values,
)
P = ParamSpec("P")
R = TypeVar("R")
@@ -53,6 +63,34 @@ 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], ...]]
) -> bool:
@@ -62,6 +100,8 @@ 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。
@@ -70,14 +110,23 @@ def generic_check_issubclass(
return issubclass(cls, class_or_tuple)
except TypeError:
origin = get_origin(cls)
if is_union(origin):
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(
@@ -90,6 +139,21 @@ def generic_check_issubclass(
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 协程函数"""
if inspect.isroutine(call):
@@ -154,8 +218,7 @@ async def run_coro_with_catch(
coro: Coroutine[Any, Any, T],
exc: Tuple[Type[Exception], ...],
return_on_err: None = None,
) -> Union[T, None]:
...
) -> Union[T, None]: ...
@overload
@@ -163,8 +226,7 @@ async def run_coro_with_catch(
coro: Coroutine[Any, Any, T],
exc: Tuple[Type[Exception], ...],
return_on_err: R,
) -> Union[T, R]:
...
) -> Union[T, R]: ...
async def run_coro_with_catch(

2247
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.2.1"
description = "An asynchronous python bot framework."
authors = ["yanyongyu <yyy@nonebot.dev>"]
license = "MIT"
@@ -29,33 +29,29 @@ python = "^3.8"
yarl = "^1.7.2"
pygtrie = "^2.4.1"
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.2.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"
@@ -90,19 +86,21 @@ 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"
[tool.ruff.flake8-pytest-style]
[tool.ruff.lint]
select = ["E", "W", "F", "UP", "C", "T", "PYI", "PT", "Q"]
ignore = ["E402", "C901", "UP037"]
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = false
[tool.pyright]
pythonVersion = "3.8"
pythonPlatform = "All"
defineConstant = { PYDANTIC_V2 = true }
executionEnvironments = [
{ root = "./tests", extraPaths = [
"./",

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-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__.:
@@ -22,5 +22,8 @@ rules =
"sys_platform != 'linux'": py-linux
"sys_platform != 'darwin'": py-darwin
"sys_version_info < (3, 9)": py-gte-39
"sys_version_info >= (3, 9)": py-lt-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

@@ -19,6 +19,8 @@ os.environ["CONFIG_OVERRIDE"] = "new"
if TYPE_CHECKING:
from nonebot.plugin import Plugin
collect_ignore = ["plugins/", "dynamic/", "bad_plugins/"]
def pytest_configure(config: pytest.Config) -> None:
config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"}

View File

@@ -1,9 +1,15 @@
import json
import base64
import socket
from typing import Dict, List, 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")
@@ -29,8 +35,7 @@ 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

@@ -11,20 +11,17 @@ async def legacy_bot(bot):
return bot
async def not_legacy_bot(bot: int):
...
async def not_legacy_bot(bot: int): ...
class FooBot(Bot):
...
class FooBot(Bot): ...
async def sub_bot(b: FooBot) -> FooBot:
return b
class BarBot(Bot):
...
class BarBot(Bot): ...
async def union_bot(b: Union[FooBot, BarBot]) -> Union[FooBot, BarBot]:
@@ -45,5 +42,4 @@ async def generic_bot_none(b: CB) -> CB:
return b
async def not_bot(b: Union[int, Bot]):
...
async def not_bot(b: Union[int, Bot]): ...

View File

@@ -36,8 +36,7 @@ class ClassDependency:
y: int = Depends(gen_async)
class FooBot(Bot):
...
class FooBot(Bot): ...
async def sub_bot(b: FooBot) -> FooBot:

View File

@@ -12,20 +12,17 @@ async def legacy_event(event):
return event
async def not_legacy_event(event: int):
...
async def not_legacy_event(event: int): ...
class FooEvent(Event):
...
class FooEvent(Event): ...
async def sub_event(e: FooEvent) -> FooEvent:
return e
class BarEvent(Event):
...
class BarEvent(Event): ...
async def union_event(e: Union[FooEvent, BarEvent]) -> Union[FooEvent, BarEvent]:
@@ -46,8 +43,7 @@ async def generic_event_none(e: CE) -> CE:
return e
async def not_event(e: Union[int, Event]):
...
async def not_event(e: Union[int, Event]): ...
async def event_type(t: str = EventType()) -> str:

View File

@@ -4,3 +4,7 @@ from typing import Union
async def exc(e: Exception, x: Union[ValueError, TypeError]) -> Exception:
assert e == x
return e
async def legacy_exc(exception) -> Exception:
return exception

View File

@@ -13,20 +13,17 @@ async def legacy_matcher(matcher):
return matcher
async def not_legacy_matcher(matcher: int):
...
async def not_legacy_matcher(matcher: int): ...
class FooMatcher(Matcher):
...
class FooMatcher(Matcher): ...
async def sub_matcher(m: FooMatcher) -> FooMatcher:
return m
class BarMatcher(Matcher):
...
class BarMatcher(Matcher): ...
async def union_matcher(
@@ -49,8 +46,7 @@ async def generic_matcher_none(m: CM) -> CM:
return m
async def not_matcher(m: Union[int, Matcher]):
...
async def not_matcher(m: Union[int, Matcher]): ...
async def receive(e: Event = Received("test")) -> Event:

View File

@@ -29,8 +29,7 @@ async def legacy_state(state):
return state
async def not_legacy_state(state: int):
...
async def not_legacy_state(state: int): ...
async def command(cmd: Tuple[str, ...] = Command()) -> Tuple[str, ...]:
@@ -77,8 +76,13 @@ async def regex_matched(regex_matched: Match[str] = RegexMatched()) -> Match[str
return regex_matched
async def regex_str(regex_str: str = RegexStr()) -> str:
return regex_str
async def regex_str(
entire: str = RegexStr(),
type_: str = RegexStr("type"),
second: str = RegexStr(2),
groups: Tuple[str, ...] = RegexStr(1, "arg"),
) -> Tuple[str, str, str, Tuple[str, ...]]:
return entire, type_, second, groups
async def startswith(startswith: str = Startswith()) -> str:

View File

@@ -19,5 +19,4 @@ async def complex_priority(
arg: Message = Arg(),
exception: Optional[Exception] = None,
default: int = 1,
):
...
): ...

View File

@@ -202,8 +202,7 @@ matcher_on_regex = on_regex(
)
class TestEvent(Event):
...
class TestEvent(Event): ...
matcher_on_type = on_type(

View File

@@ -99,8 +99,7 @@ async def test_adapter_server(driver: Driver):
async def handle_http(request: Request):
return Response(200, content="test")
async def handle_ws(ws: WebSocket):
...
async def handle_ws(ws: WebSocket): ...
adapter = FakeAdapter(driver)

View File

@@ -1,8 +1,9 @@
import pytest
from pydantic import ValidationError, parse_obj_as
from pydantic import ValidationError
from nonebot.adapters import Message
from nonebot.compat import type_validate_python
from utils import FakeMessage, FakeMessageSegment
from nonebot.adapters import Message, MessageSegment
def test_segment_data():
@@ -47,16 +48,21 @@ def test_segment_add():
def test_segment_validate():
assert parse_obj_as(
assert type_validate_python(
FakeMessageSegment,
{"type": "text", "data": {"text": "text"}, "extra": "should be ignored"},
) == FakeMessageSegment.text("text")
with pytest.raises(ValidationError):
type_validate_python(
type("FakeMessageSegment2", (MessageSegment,), {}),
FakeMessageSegment.text("text"),
)
with pytest.raises(ValidationError):
parse_obj_as(FakeMessageSegment, "some str")
type_validate_python(FakeMessageSegment, "some str")
with pytest.raises(ValidationError):
parse_obj_as(FakeMessageSegment, {"data": {}})
type_validate_python(FakeMessageSegment, {"data": {}})
def test_segment_join():
@@ -144,26 +150,26 @@ def test_message_getitem():
def test_message_validate():
assert parse_obj_as(FakeMessage, FakeMessage([])) == FakeMessage([])
assert type_validate_python(FakeMessage, FakeMessage([])) == FakeMessage([])
with pytest.raises(ValidationError):
parse_obj_as(type("FakeMessage2", (Message,), {}), FakeMessage([]))
type_validate_python(type("FakeMessage2", (Message,), {}), FakeMessage([]))
assert parse_obj_as(FakeMessage, "text") == FakeMessage(
assert type_validate_python(FakeMessage, "text") == FakeMessage(
[FakeMessageSegment.text("text")]
)
assert parse_obj_as(
assert type_validate_python(
FakeMessage, {"type": "text", "data": {"text": "text"}}
) == FakeMessage([FakeMessageSegment.text("text")])
assert parse_obj_as(
assert type_validate_python(
FakeMessage,
[FakeMessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}],
) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")])
with pytest.raises(ValidationError):
parse_obj_as(FakeMessage, object())
type_validate_python(FakeMessage, object())
def test_message_contains():
@@ -186,6 +192,11 @@ def test_message_contains():
assert message.has("foo") is False
assert "foo" not in message
assert not bool(FakeMessageSegment.text(""))
msg_with_empty_seg = FakeMessage([FakeMessageSegment.text("")])
assert msg_with_empty_seg.has("text") is True
assert "text" in msg_with_empty_seg
def test_message_only():
message = FakeMessage(

View File

@@ -1,3 +1,5 @@
import pytest
from nonebot.adapters import MessageTemplate
from utils import FakeMessage, FakeMessageSegment, escape_text
@@ -15,12 +17,8 @@ def test_template_message():
def custom(input: str) -> str:
return f"{input}-custom!"
try:
with pytest.raises(ValueError, match="already exists"):
template.add_format_spec(custom)
except ValueError:
pass
else:
raise AssertionError("Should raise ValueError")
format_args = {
"a": "custom",
@@ -57,3 +55,22 @@ def test_message_injection():
message = template.format(name="[fake:image]")
assert message.extract_plain_text() == escape_text("[fake:image]Is Bad")
def test_malformed_template():
positive_template = FakeMessage.template("{a}{b}")
message = positive_template.format(a="a", b="b")
assert message.extract_plain_text() == "ab"
malformed_template = FakeMessage.template("{a.__init__}")
with pytest.raises(ValueError, match="private attribute"):
message = malformed_template.format(a="a")
malformed_template = FakeMessage.template("{a[__builtins__]}")
with pytest.raises(ValueError, match="private attribute"):
message = malformed_template.format(a=globals())
malformed_template = MessageTemplate(
"{a[__builtins__][__import__]}{b.__init__}", private_getattr=True
)
message = malformed_template.format(a=globals(), b="b")

92
tests/test_compat.py Normal file
View File

@@ -0,0 +1,92 @@
from typing import Any, Optional
from dataclasses import dataclass
import pytest
from pydantic import BaseModel
from nonebot.compat import (
DEFAULT_CONFIG,
Required,
FieldInfo,
PydanticUndefined,
model_dump,
custom_validation,
type_validate_json,
type_validate_python,
)
@pytest.mark.asyncio
async def test_default_config():
assert DEFAULT_CONFIG.get("extra") == "allow"
assert DEFAULT_CONFIG.get("arbitrary_types_allowed") is True
@pytest.mark.asyncio
async def test_field_info():
# required should be convert to PydanticUndefined
assert FieldInfo(Required).default is PydanticUndefined
# field info should allow extra attributes
assert FieldInfo(test="test").extra["test"] == "test"
@pytest.mark.asyncio
async def test_model_dump():
class TestModel(BaseModel):
test1: int
test2: int
assert model_dump(TestModel(test1=1, test2=2), include={"test1"}) == {"test1": 1}
assert model_dump(TestModel(test1=1, test2=2), exclude={"test1"}) == {"test2": 2}
@pytest.mark.asyncio
async def test_custom_validation():
called = []
@custom_validation
@dataclass
class TestModel:
test: int
@classmethod
def __get_validators__(cls):
yield cls._validate_1
yield cls._validate_2
@classmethod
def _validate_1(cls, v: Any) -> Any:
called.append(1)
return v
@classmethod
def _validate_2(cls, v: Any) -> Any:
called.append(2)
return cls(test=v["test"])
assert type_validate_python(TestModel, {"test": 1}) == TestModel(test=1)
assert called == [1, 2]
@pytest.mark.asyncio
async def test_validate_json():
class TestModel(BaseModel):
test1: int
test2: str
test3: bool
test4: dict
test5: list
test6: Optional[int]
assert type_validate_json(
TestModel,
"{"
' "test1": 1,'
' "test2": "2",'
' "test3": true,'
' "test4": {},'
' "test5": [],'
' "test6": null'
"}",
) == TestModel(test1=1, test2="2", test3=True, test4={}, test5=[], test6=None)

118
tests/test_config.py Normal file
View File

@@ -0,0 +1,118 @@
from typing import List, Union, Optional
import pytest
from pydantic import BaseModel
from nonebot.config import DOTENV_TYPE, BaseSettings, SettingsError
class Simple(BaseModel):
a: int = 0
b: int = 0
c: dict = {}
complex: list = []
class Example(BaseSettings):
_env_file: Optional[DOTENV_TYPE] = ".env", ".env.example"
_env_nested_delimiter: Optional[str] = "__"
simple: str = ""
complex: List[int] = [1]
complex_none: Optional[List[int]] = None
complex_union: Union[int, List[int]] = 1
nested: Simple = Simple()
nested_inner: Simple = Simple()
class Config:
env_file = ".env", ".env.example"
env_nested_delimiter = "__"
class ExampleWithoutDelimiter(Example):
class Config:
env_nested_delimiter = None
@pytest.mark.asyncio
async def test_config_no_env():
config = Example(_env_file=None)
assert config.simple == ""
with pytest.raises(AttributeError):
config.common_config
@pytest.mark.asyncio
async def test_config_with_env():
config = Example(_env_file=(".env", ".env.example"))
assert config.simple == "simple"
assert config.complex == [1, 2, 3]
assert config.complex_none is None
assert config.complex_union == [1, 2, 3]
assert config.nested.a == 1
assert config.nested.b == 2
assert config.nested.c == {"c": "3"}
assert config.nested.complex == [1, 2, 3]
with pytest.raises(AttributeError):
config.nested__b
with pytest.raises(AttributeError):
config.nested__c__c
with pytest.raises(AttributeError):
config.nested__complex
assert config.nested_inner.a == 1
assert config.nested_inner.b == 2
with pytest.raises(AttributeError):
config.nested_inner__a
with pytest.raises(AttributeError):
config.nested_inner__b
assert config.common_config == "common"
assert config.other_simple == "simple"
assert config.other_nested == {"a": 1, "b": 2}
with pytest.raises(AttributeError):
config.other_nested__b
assert config.other_nested_inner == {"a": 1, "b": 2}
with pytest.raises(AttributeError):
config.other_nested_inner__a
with pytest.raises(AttributeError):
config.other_nested_inner__b
@pytest.mark.asyncio
async def test_config_error_env():
with pytest.MonkeyPatch().context() as m:
m.setenv("COMPLEX", "not json")
with pytest.raises(SettingsError):
Example(_env_file=(".env", ".env.example"))
@pytest.mark.asyncio
async def test_config_without_delimiter():
config = ExampleWithoutDelimiter()
assert config.nested.a == 1
assert config.nested.b == 0
assert config.nested__b == 2
assert config.nested.c == {}
assert config.nested__c__c == 3
assert config.nested.complex == []
assert config.nested__complex == [1, 2, 3]
assert config.nested_inner.a == 0
assert config.nested_inner.b == 0
assert config.other_nested == {"a": 1}
assert config.other_nested__b == 2
with pytest.raises(AttributeError):
config.other_nested_inner
assert config.other_nested_inner__a == 1
assert config.other_nested_inner__b == 2

View File

@@ -5,11 +5,11 @@ from typing import Any, Set, Optional
import pytest
from nonebug import App
from utils import FakeAdapter
from nonebot.adapters import Bot
from nonebot.params import Depends
from nonebot.dependencies import Dependent
from nonebot.exception import WebSocketClosed
from nonebot.drivers._lifespan import Lifespan
from nonebot.drivers import (
URL,
Driver,
@@ -25,34 +25,50 @@ from nonebot.drivers import (
@pytest.mark.asyncio
async def test_lifespan():
lifespan = Lifespan()
@pytest.mark.parametrize(
"driver", [pytest.param("nonebot.drivers.none:Driver", id="none")], indirect=True
)
async def test_lifespan(driver: Driver):
adapter = FakeAdapter(driver)
start_log = []
ready_log = []
shutdown_log = []
@lifespan.on_startup
@driver.on_startup
async def _startup1():
assert start_log == []
start_log.append(1)
@lifespan.on_startup
@driver.on_startup
async def _startup2():
assert start_log == [1]
start_log.append(2)
@lifespan.on_shutdown
@adapter.on_ready
def _ready1():
assert start_log == [1, 2]
assert ready_log == []
ready_log.append(1)
@adapter.on_ready
def _ready2():
assert ready_log == [1]
ready_log.append(2)
@driver.on_shutdown
async def _shutdown1():
assert shutdown_log == []
shutdown_log.append(1)
@lifespan.on_shutdown
@driver.on_shutdown
async def _shutdown2():
assert shutdown_log == [1]
shutdown_log.append(2)
async with lifespan:
async with driver._lifespan:
assert start_log == [1, 2]
assert ready_log == [1, 2]
assert shutdown_log == [1, 2]
@@ -115,7 +131,7 @@ async def test_websocket_server(app: App, driver: Driver):
assert data == b"ping"
await ws.send(b"pong")
with pytest.raises(WebSocketClosed):
with pytest.raises(WebSocketClosed, match=r"code=1000"):
await ws.receive()
ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws)
@@ -136,7 +152,7 @@ async def test_websocket_server(app: App, driver: Driver):
await ws.send_bytes(b"ping")
assert await ws.receive_bytes() == b"pong"
await ws.close()
await ws.close(code=1000)
await asyncio.sleep(1)
@@ -299,9 +315,29 @@ async def test_http_client(driver: Driver, server_url: URL):
],
indirect=True,
)
async def test_websocket_client(driver: Driver):
async def test_websocket_client(driver: Driver, server_url: URL):
assert isinstance(driver, WebSocketClientMixin)
request = Request("GET", server_url.with_scheme("ws"))
async with driver.websocket(request) as ws:
await ws.send("test")
assert await ws.receive() == "test"
await ws.send(b"test")
assert await ws.receive() == b"test"
await ws.send_text("test")
assert await ws.receive_text() == "test"
await ws.send_bytes(b"test")
assert await ws.receive_bytes() == b"test"
await ws.send("quit")
with pytest.raises(WebSocketClosed, match=r"code=1000"):
await ws.receive()
await asyncio.sleep(1)
@pytest.mark.asyncio
@pytest.mark.parametrize(

33
tests/test_echo.py Normal file
View File

@@ -0,0 +1,33 @@
import pytest
from nonebug import App
from utils import FakeMessage, FakeMessageSegment, make_fake_event
@pytest.mark.asyncio
async def test_echo(app: App):
from nonebot.plugins.echo import echo
async with app.test_matcher(echo) as ctx:
bot = ctx.create_bot()
message = FakeMessage("/echo 123")
event = make_fake_event(_message=message)()
ctx.receive_event(bot, event)
ctx.should_call_send(event, FakeMessage("123"), True, bot=bot)
message = FakeMessageSegment.text("/echo 123") + FakeMessageSegment.image(
"test"
)
event = make_fake_event(_message=message)()
ctx.receive_event(bot, event)
ctx.should_call_send(
event,
FakeMessageSegment.text("123") + FakeMessageSegment.image("test"),
True,
bot=bot,
)
message = FakeMessage("/echo")
event = make_fake_event(_message=message)()
ctx.receive_event(bot, event)

View File

@@ -218,7 +218,7 @@ async def test_event(app: App):
async with app.test_dependent(union_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_fooevent)
ctx.should_return(fake_event)
ctx.should_return(fake_fooevent)
async with app.test_dependent(generic_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_event)
@@ -361,7 +361,9 @@ async def test_state(app: App):
regex_str, allow_types=[StateParam, DependParam]
) as ctx:
ctx.pass_params(state=fake_state)
ctx.should_return("[cq:test,arg=value]")
ctx.should_return(
("[cq:test,arg=value]", "test", "arg=value", ("test", "arg=value"))
)
async with app.test_dependent(
regex_group, allow_types=[StateParam, DependParam]
@@ -527,13 +529,17 @@ async def test_arg(app: App):
@pytest.mark.asyncio
async def test_exception(app: App):
from plugins.param.param_exception import exc
from plugins.param.param_exception import exc, legacy_exc
exception = ValueError("test")
async with app.test_dependent(exc, allow_types=[ExceptionParam]) as ctx:
ctx.pass_params(exception=exception)
ctx.should_return(exception)
async with app.test_dependent(legacy_exc, allow_types=[ExceptionParam]) as ctx:
ctx.pass_params(exception=exception)
ctx.should_return(exception)
@pytest.mark.asyncio
async def test_default(app: App):

View File

@@ -1,4 +1,5 @@
import pytest
from pydantic import BaseModel
import nonebot
from nonebot.plugin import PluginManager, _managers
@@ -35,3 +36,14 @@ async def test_get_available_plugin():
finally:
_managers.clear()
_managers.extend(old_managers)
@pytest.mark.asyncio
async def test_get_plugin_config():
class Config(BaseModel):
plugin_config: int
# check get plugin config
config = nonebot.get_plugin_config(Config)
assert isinstance(config, Config)
assert config.plugin_config == 1

View File

@@ -80,7 +80,7 @@ async def test_load_toml():
async def test_bad_plugin():
nonebot.load_plugins("bad_plugins")
assert nonebot.get_plugin("bad_plugins") is None
assert nonebot.get_plugin("bad_plugin") is None
@pytest.mark.asyncio

View File

@@ -113,6 +113,36 @@ async def test_trie(app: App):
command_whitespace=" ",
)
message = FakeMessageSegment.text("/fake-prefix ") + FakeMessageSegment.text(
" some args"
)
event = make_fake_event(_message=message)()
state = {}
TrieRule.get_value(bot, event, state)
assert state[PREFIX_KEY] == CMD_RESULT(
command=("fake-prefix",),
raw_command="/fake-prefix",
command_arg=FakeMessage("some args"),
command_start="/",
command_whitespace=" ",
)
message = (
FakeMessageSegment.text("/fake-prefix ")
+ FakeMessageSegment.text(" ")
+ FakeMessageSegment.text(" some args")
)
event = make_fake_event(_message=message)()
state = {}
TrieRule.get_value(bot, event, state)
assert state[PREFIX_KEY] == CMD_RESULT(
command=("fake-prefix",),
raw_command="/fake-prefix",
command_arg=FakeMessage("some args"),
command_start="/",
command_whitespace=" ",
)
del TrieRule.prefix["/fake-prefix"]

View File

@@ -1,5 +1,5 @@
import json
from typing import Dict, List, Union, TypeVar
from typing import Dict, List, Union, Literal, TypeVar, ClassVar
from utils import FakeMessage, FakeMessageSegment
from nonebot.utils import (
@@ -24,26 +24,25 @@ def test_generic_check_issubclass():
assert generic_check_issubclass(int, (int, float))
assert not generic_check_issubclass(str, (int, float))
assert generic_check_issubclass(Union[int, float, None], (int, float))
assert generic_check_issubclass(Literal[1, 2, 3], int)
assert not generic_check_issubclass(Literal[1, 2, "3"], int)
assert generic_check_issubclass(List[int], list)
assert generic_check_issubclass(Dict[str, int], dict)
assert not generic_check_issubclass(ClassVar[int], int)
assert generic_check_issubclass(TypeVar("T", int, float), (int, float))
assert generic_check_issubclass(TypeVar("T", bound=int), (int, float))
def test_is_coroutine_callable():
async def test1():
...
async def test1(): ...
def test2():
...
def test2(): ...
class TestClass1:
async def __call__(self):
...
async def __call__(self): ...
class TestClass2:
def __call__(self):
...
def __call__(self): ...
assert is_coroutine_callable(test1)
assert not is_coroutine_callable(test2)
@@ -59,8 +58,7 @@ def test_is_gen_callable():
async def test2():
yield
def test3():
...
def test3(): ...
class TestClass1:
def __call__(self):
@@ -71,8 +69,7 @@ def test_is_gen_callable():
yield
class TestClass3:
def __call__(self):
...
def __call__(self): ...
assert is_gen_callable(test1)
assert not is_gen_callable(test2)
@@ -89,8 +86,7 @@ def test_is_async_gen_callable():
def test2():
yield
async def test3():
...
async def test3(): ...
class TestClass1:
async def __call__(self):
@@ -101,8 +97,7 @@ def test_is_async_gen_callable():
yield
class TestClass3:
async def __call__(self):
...
async def __call__(self): ...
assert is_async_gen_callable(test1)
assert not is_async_gen_callable(test2)

View File

@@ -19,6 +19,25 @@ NoneBot 使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`]
NoneBot 内置的配置项列表及含义可以在[内置配置项](#内置配置项)中查看。
:::caution 注意
NoneBot 自 2.2.0 起兼容了 Pydantic v1 与 v2 版本,以下文档中 Pydantic 相关示例均采用 v2 版本用法。
如果在使用商店或其他第三方插件的过程中遇到 Pydantic 相关警告或报错,例如:
```python
pydantic_core._pydantic_core.ValidationError: 1 validation error for Config
Input should be a valid dictionary or instance of Config [type=model_type, input_value=Config(...), input_type=Config]
```
请考虑降级 Pydantic 至 v1 版本:
```bash
pip install --force-reinstall 'pydantic~=1.10'
```
:::
## 配置项的加载
在 NoneBot 中,我们可以把配置途径分为 **直接传入**、**系统环境变量**、**dotenv 配置文件** 三种,其加载优先级依次由高到低。
@@ -182,18 +201,19 @@ superusers = config.superusers
在 NoneBot 中,我们使用强大高效的 `pydantic` 来定义配置模型,这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型:
```python title=weather/config.py
from pydantic import BaseModel, validator
from pydantic import BaseModel, field_validator
class Config(BaseModel):
weather_api_key: str
weather_command_priority: int = 10
weather_plugin_enabled: bool = True
@validator("weather_command_priority")
def check_priority(cls, v):
if isinstance(v, int) and v >= 1:
@field_validator("weather_command_priority")
@classmethod
def check_priority(cls, v: int) -> int:
if v >= 1:
return v
raise ValueError("weather command priority must be an integer and greater than 1")
raise ValueError("weather command priority must greater than 1")
```
在 `config.py` 中,我们定义了一个 `Config` 类,它继承自 `pydantic.BaseModel`,并定义了一些配置项。在 `Config` 类中,我们还定义了一个 `check_priority` 方法,它用于检查 `weather_command_priority` 配置项的合法性。更多关于 `pydantic` 的编写方式,可以参考 [pydantic 官方文档](https://docs.pydantic.dev/)。
@@ -201,11 +221,11 @@ class Config(BaseModel):
在定义好配置模型后,我们可以在插件加载时获取全局配置,导入插件自身的配置模型并使用:
```python {5,11} title=weather/__init__.py
from nonebot import get_driver
from nonebot import get_plugin_config
from .config import Config
plugin_config = Config.parse_obj(get_driver().config)
plugin_config = get_plugin_config(Config)
weather = on_command(
"天气",
@@ -239,11 +259,11 @@ class Config(BaseModel):
```
```python title=weather/__init__.py
from nonebot import get_driver
from nonebot import get_plugin_config
from .config import Config
plugin_config = Config.parse_obj(get_driver().config).weather
plugin_config = get_plugin_config(Config).weather
```
这样我们就可以省略插件配置项名称中的前缀 `weather_` 了。但需要注意的是,如果我们使用了 scope 配置,那么在配置文件中也需要使用 [`env_nested_delimiter` 格式](#配置项解析),例如:
@@ -559,13 +579,13 @@ nonebot.init(command_start={"/", ""}, command_sep={".", " "})
- **类型**: `timedelta`
- **默认值**: `timedelta(minutes=2)`
用户会话超时时间,配置格式参考 [Datetime Types](https://docs.pydantic.dev/usage/types/#datetime-types),可以为单位为秒的 `int | float` 等
用户会话超时时间,配置格式参考 [Datetime Types](https://docs.pydantic.dev/latest/api/standard_library_types/#datetimetimedelta)
<Tabs groupId="configMethod">
<TabItem value="dotenv" label="dotenv" default>
```dotenv
SESSION_EXPIRE_TIMEOUT=120
SESSION_EXPIRE_TIMEOUT=00:02:00
```
</TabItem>
@@ -573,9 +593,9 @@ SESSION_EXPIRE_TIMEOUT=120
```bash
# windows
set SESSION_EXPIRE_TIMEOUT '120'
set SESSION_EXPIRE_TIMEOUT '00:02:00'
# linux/macOS
export SESSION_EXPIRE_TIMEOUT='120'
export SESSION_EXPIRE_TIMEOUT='00:02:00'
```
</TabItem>

View File

@@ -21,7 +21,7 @@ options:
`RuleChecker` 是一个返回值为 `bool` 类型的依赖函数,即 `RuleChecker` 支持依赖注入。我们可以根据上一节中添加的[配置项](./config.mdx#插件配置),在 `weather` 插件目录中编写一个响应规则:
```python {3,4} title=weather/__init__.py
plugin_config = Config.parse_obj(get_driver().config)
plugin_config = get_plugin_config(Config)
async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled
@@ -57,7 +57,7 @@ weather = on_command("天气", rule=rule)
```python {10} title=weather/__init__.py
from nonebot.rule import to_me
plugin_config = Config.parse_obj(get_driver().config)
plugin_config = get_plugin_config(Config)
async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled

View File

@@ -5,16 +5,17 @@ description: Alconna 命令解析拓展
slug: /best-practice/alconna/
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
# Alconna 插件
[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。
该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器,
是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。
是一个简单、灵活、高效的命令参数解析器并且不局限于解析命令式字符串。
该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。
同时,基于 [Annotated 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解 `AlcMatches` 与 `AlcResult`
该插件声明了一个 `Matcher` 的子类 `AlconnaMatcher`,并在 `AlconnaMatcher` 中添加了一些新的方法,例如:
- `assign`:基于 `Alconna` 解析结果,执行满足目标路径的处理函数
@@ -33,16 +34,31 @@ slug: /best-practice/alconna/
在**项目目录**下执行以下命令:
<Tabs groupId="install">
<TabItem value="cli" label="使用 nb-cli">
```shell
nb plugin install nonebot-plugin-alconna
```
</TabItem>
<TabItem value="pip" label="使用 pip">
```shell
pip install nonebot-plugin-alconna
```
</TabItem>
<TabItem value="pdm" label="使用 pdm">
```shell
pdm add nonebot-plugin-alconna
```
</TabItem>
</Tabs>
## 导入插件
由于 `nonebot-plugin-alconna` 作为插件,因此需要在使用前对其进行**加载**并**导入**其中的 `on_alconna` 来使用命令拓展。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../../advanced/requiring.md) 一节进行了解。
@@ -89,7 +105,7 @@ async def got_location(location: str = ArgPlainText()):
```python {5-10,14-16,18-19}
from nonebot.rule import to_me
from arclet.alconna import Alconna, Args
from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna
from nonebot_plugin_alconna import Match, on_alconna
weather = on_alconna(
Alconna("天气", Args["location?", str]),
@@ -100,9 +116,9 @@ weather.shortcut("天气预报", {"command": "天气"})
@weather.handle()
async def handle_function(matcher: AlconnaMatcher, location: Match[str]):
async def handle_function(location: Match[str]):
if location.available:
matcher.set_path_arg("location", location.result)
weather.set_path_arg("location", location.result)
@weather.got_path("location", prompt="请输入地名")
async def got_location(location: str):
@@ -112,7 +128,7 @@ async def got_location(location: str):
```
在上面的代码中,我们使用 `Alconna` 来解析命令,`on_alconna` 用来创建响应器,使用 `Match` 来获取解析结果。
command
关于更多 `Alconna` 的使用方法,可参考 [Alconna 文档](https://arclet.top/docs/tutorial/alconna)
或阅读 [Alconna 基本介绍](./command.md) 一节。

View File

@@ -3,30 +3,42 @@ sidebar_position: 2
description: Alconna 基本介绍
---
# Alconna 命令解析
# Alconna 本体
[Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器,
是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。
[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`,是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。
特点包括:
我们通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`
- 高效
- 直观的命令组件创建方式
- 强大的类型解析与类型转换功能
- 自定义的帮助信息格式
- 多语言支持
- 易用的快捷命令创建与使用
- 可创建命令补全会话, 以实现多轮连续的补全提示
- 可嵌套的多级子命令
- 正则匹配支持
```python
from arclet.alconna import Alconna, Args, Subcommand, Option
## 命令编写
alc = Alconna(
"pip",
Subcommand(
"install",
Args["package", str],
Option("-r|--requirement", Args["file", str]),
Option("-i|--index-url", Args["url", str]),
)
)
res = alc.parse("pip install nonebot2 -i URL")
print(res)
# matched=True, header_match=(origin='pip' result='pip' matched=True groups={}), subcommands={'install': (value=Ellipsis args={'package': 'nonebot2'} options={'index-url': (value=None args={'url': 'URL'})} subcommands={})}, other_args={'package': 'nonebot2', 'url': 'URL'}
print(res.all_matched_args)
# {'package': 'nonebot2', 'url': 'URL'}
```
这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r``-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例。
## 组成
### 命令头
命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合,例如 `!help` 中的 `!``help`
在 Alconna 中,你可以传入多种类型的命令头,例如:
命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合,例如 !help 中的 ! 与 help。
| 前缀 | 命令名 | 匹配内容 | 说明 |
| :--------------------------: | :--------: | :---------------------------------------------------------: | :--------------: |
@@ -43,23 +55,20 @@ description: Alconna 基本介绍
| [123, "foo"] | "bar" | `[123, "bar"]``"foobar"``["foo", "bar"]` | 混合头 |
| [(int, "foo"), (456, "bar")] | "baz" | `[123, "foobaz"]``[456, "foobaz"]``[456, "barbaz"]` | 对头 |
其中
无前缀的类型头:此时会将传入的值尝试转为 BasePattern例如 `int` 会转为 `nepattern.INTEGER`。此时命令头会匹配对应的类型, 例如 `int` 会匹配 `123` 或 `"456"`,但不会匹配 `"foo"`。同时Alconna 会将命令头匹配到的值转为对应的类型,例如 `int` 会将 `"123"` 转为 `123`
- 元素头:只会匹配对应的值,例如 `[123, 456]` 只会匹配 `123``456`,不会匹配 `789`
- 纯文字头:只会匹配对应的字符串,例如 `["foo", "bar"]` 只会匹配 `"foo"``"bar"`,不会匹配 `"baz"`
- 正则头:`re:xxx` 会将 `xxx` 转为正则表达式,然后匹配对应的字符串,例如 `re:\d{2}` 只会匹配 `"12"``"34"`,不会匹配 `"foo"`
**正则只在命令名上生效,命令前缀中的正则会被转义**
- 类型头:只会匹配对应的类型,例如 `[int, bool]` 只会匹配 `123``True`,不会匹配 `"foo"`
- 无前缀的类型头:此时会将传入的值尝试转为 BasePattern例如 `int` 会转为 `nepattern.INTEGER`。此时命令头会匹配对应的类型,
例如 `int` 会匹配 `123``"456"`,但不会匹配 `"foo"`。同时Alconna 会将命令头匹配到的值转为对应的类型,例如 `int` 会将 `"123"` 转为 `123`
- 表达式头:只会匹配对应的表达式,例如 `[nepattern.NUMBER]` 只会匹配 `123``123.456`,不会匹配 `"foo"`
- 混合头:
:::tip
除了通过传入 `re:xxx` 来使用正则表达式外Alconna 还提供了一种更加简洁的方式来使用正则表达式,那就是 Bracket Header。
**正则只在命令名上生效,命令前缀中的正则会被转义**
:::
除了通过传入 `re:xxx` 来使用正则表达式外Alconna 还提供了一种更加简洁的方式来使用正则表达式,那就是 Bracket Header
```python
from alconna import Alconna
alc = Alconna(".rd{roll:int}")
assert alc.parse(".rd123").header["roll"] == 123
```
@@ -68,56 +77,158 @@ Bracket Header 类似 python 里的 f-string 写法,通过 "{}" 声明匹配
"{}" 中的内容为 "name:type or pat"
- "{}", "{:}": 占位符,等价于 "(.+)"
- "{foo}": 等价于 "(?P&lt;foo&gt;.+)"
- "{:\d+}": 等价于 "(\d+)"
- "{foo:int}": 等价于 "(?P&lt;foo&gt;\d+)",其中 "int" 部分若能转为 `BasePattern` 则读取里面的表达式
- "{}", "{:}" "(.+)", 占位符
- "{foo}" "(?P&lt;foo&gt;.+)"
- "{:\d+}" "(\d+)"
- "{foo:int}" "(?P&lt;foo&gt;\d+)",其中 "int" 部分若能转为 `BasePattern` 则读取里面的表达式
### 组件
### 参数声明(Args)
我们可以看到主要的两大组件:`Option``Subcommand`
`Args` 是用于声明命令参数的组件, 可以通过以下几种方式构造 **Args**
`Option` 可以传入一组 `alias`,如 `Option("--foo|-F|--FOO|-f")``Option("--foo", alias=["-F"])`
- `Args[key, var, default][key1, var1, default1][...]`
- `Args[(key, var, default)]`
- `Args.key[var, default]`
传入别名后Option 会选择其中长度最长的作为选项名称。若传入为 "--foo|-f",则命令名称为 "--foo"。
其中key **一定**是字符串,而 var 一般为参数的类型default 为具体的值或者 **arclet.alconna.args.Field**
`Subcommand` 则可以传入自己的 **Option****Subcommand**
其与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)
#### key
`key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中,以方便用户调用。
其有三种为 Args 注解的标识符:  `?``/`、 `!`, 标识符与 key 之间建议以 `;` 分隔:
- `!` 标识符表示该处传入的参数应**不是**规定的类型,或**不在**指定的值中。
- `?` 标识符表示该参数为**可选**参数,会在无参数匹配时跳过。
- `/` 标识符表示该参数的类型注解需要隐藏。
另外,对于参数的注释也可以标记在 `key` 中,其与 key 或者标识符 以 `#` 分割:
`foo#这是注释;?` 或 `foo?#这是注释`
:::tip
`Args` 中的 `key` 在实际命令中并不需要传入keyword 参数除外):
```python
from arclet.alconna import Alconna, Option, Subcommand
from arclet.alconna import Alconna, Args
alc = Alconna(
"command_name",
Option("opt1"),
Option("--opt2"),
Subcommand(
"sub1",
Option("sub1_opt1"),
Option("-SO2"),
Subcommand(
"sub1_sub1"
)
),
Subcommand(
"sub2"
)
)
alc = Alconna("test", Args["foo", str])
alc.parse("test --foo abc") # 错误
alc.parse("test abc") # 正确
```
若需要 `test --foo abc`,你应该使用 `Option`
```python
from arclet.alconna import Alconna, Args, Option
alc = Alconna("test", Option("--foo", Args["foo", str]))
```
:::
#### var
var 负责命令参数的**类型检查**与**类型转化**
`Args` 的`var`表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例:
```python
from arclet.alconna import Args
from nepattern import BasePattern
# 表示 foo 参数需要匹配一个 @number 样式的字符串
args = Args["foo", BasePattern("@\d+")]
```
示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]`
`nepattern.global_patterns`默认支持的类型有:
- `str`: 匹配任意字符串
- `int`: 匹配整数
- `float`: 匹配浮点数
- `bool`: 匹配 `True` 与 `False` 以及他们小写形式
- `hex`: 匹配 `0x` 开头的十六进制字符串
- `url`: 匹配网址
- `email`: 匹配 `xxxx@xxx` 的字符串
- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串
- `list`: 匹配类似 `["foo","bar","baz"]` 的字符串
- `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串
- `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳
- `Any`: 匹配任意类型
- `AnyString`: 匹配任意类型,转为 `str`
- `Number`: 匹配 `int` 与 `float`,转为 `int`
同时可以使用 typing 中的类型:
- `Literal[X]`: 匹配其中的任意一个值
- `Union[X, Y]`: 匹配其中的任意一个类型
- `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值
- `List[X]`: 匹配一个列表,其中的元素为 `X` 类型
- `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型value 为 `Y` 类型
- ...
:::tip
几类特殊的传入标记:
- `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联)
- `RawStr("foo")`: 匹配字符串 "foo" (不会被 `BasePattern` 替换)
- `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz"
- `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型
- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值
- `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0]
- `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象
- `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值)
- ...
:::
`MultiVar` 则是一个特殊的标注,用于告知解析器该参数可以接受多个值,其构造方法形如 `MultiVar(str)`。 同样的还有 `KeyWordVar`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。
:::tip
`MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,其构造方法形如 `MultiVar(KeyWordVar(str))`
`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值
`MultiVar` 不能在 `KeyWordVar` 之后传入
:::
### Option 和 Subcommand
`Option` 可以传入一组 `alias`,如 `Option("--foo|-F|--FOO|-f")` 或 `Option("--foo", alias=["-F"]`
传入别名后,`option` 会选择其中长度最长的作为选项名称。若传入为 "--foo|-f",则命令名称为 "--foo"
:::tip 特别提醒!!!
在 Alconna 中 Option 的名字或别名**没有要求**必须在前面写上 `-`
:::
`Subcommand` 可以传入自己的 **Option** 与 **Subcommand**
他们拥有如下共同参数:
- `help_text`: 传入该组件的帮助信息
- `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name)
- `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换
对于命令 `test foo bar baz qux <a:int>` 来讲,因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写:
对于命令 `test foo bar baz qux <a:int>` 来讲,因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写:
```python
Alconna("test", Option("qux", Args.a[int], requires=["foo", "bar", "baz"]))
```
- `default`: 默认值,在该组件未被解析时使用使用该值替换。
特别的,使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值:
特别的,使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值:
```python
from arclet.alconna import Option, OptionResult
@@ -126,294 +237,25 @@ alc = Alconna(
opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1}))
```
### 选项操作
`Option` 可以特别设置传入一类 `Action`,作为解析操作
`Option` 可以特别设置传入一类 `Action`,作为解析操作
`Action` 分为三类:
`Action` 分为三类:
- `store`: 无 Args 时, 仅存储一个值, 默认为 Ellipsis 有 Args 时, 后续的解析结果会覆盖之前的值
- `append`: 无 Args 时, 将多个值存为列表, 默认为 Ellipsis 有 Args 时, 每个解析结果会追加到列表中
- `append`: 无 Args 时, 将多个值存为列表, 默认为 Ellipsis 有 Args 时, 每个解析结果会追加到列表中, 当存在默认值并且不为列表时, 会自动将默认值变成列表, 以保证追加的正确性
- `count`: 无 Args 时, 计数器加一; 有 Args 时, 表现与 STORE 相同, 当存在默认值并且不为数字时, 会自动将默认值变成 1 以保证计数器的正确性。
当存在默认值并且不为列表时, 会自动将默认值变成列表, 以保证追加的正确性
`Alconna` 提供了预制的几类 `Action`
- `count`: 无 Args 时, 计数器加一; 有 Args 时, 表现与 STORE 相同
当存在默认值并且不为数字时, 会自动将默认值变成 1 以保证计数器的正确性。
`Alconna` 提供了预制的几类 `action`
- `store``store_value``store_true``store_false`
- `store`(默认)`store_value``store_true``store_false`
- `append``append_value`
- `count`
### 参数声明
### Arparma
`Args` 是用于声明命令参数的组件。
`Alconna.parse` 会返回由 **Arparma** 承载的解析结果
`Args` 是参数解析的基础组件,构造方法形如 `Args["foo", str]["bar", int]["baz", bool, False]`
与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。
`Args` 中的 `name` 是用以标记解析出来的参数并存放于 **Arparma** 中,以方便用户调用。
其有三种为 Args 注解的标识符: `?`、`/` 与 `!`。标识符与 key 之间建议以 `;` 分隔:
- `!` 标识符表示该处传入的参数应不是规定的类型,或不在指定的值中。
- `?` 标识符表示该参数为可选参数,会在无参数匹配时跳过。
- `/` 标识符表示该参数的类型注解需要隐藏。
另外,对于参数的注释也可以标记在 `name` 中,其与 name 或者标识符 以 `#` 分割:
`foo#这是注释;?` 或 `foo?#这是注释`
:::tip
`Args` 中的 `name` 在实际命令中并不需要传入keyword 参数除外):
```python
from arclet.alconna import Alconna, Args
alc = Alconna("test", Args["foo", str])
alc.parse("test --foo abc") # 错误
alc.parse("test abc") # 之前
```
若需要 `test --foo abc`,你应该使用 `Option`
```python
from arclet.alconna import Alconna, Args, Option
alc = Alconna("test", Option("--foo", Args["foo", str]))
```
:::
`Args` 的参数类型表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例。
```python
from arclet.alconna import Args
from nepattern import BasePattern
# 表示 foo 参数需要匹配一个 @number 样式的字符串
args = Args["foo", BasePattern("@\d+")]
```
示例中传入的 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]`。
默认支持的类型有:
- `str`: 匹配任意字符串
- `int`: 匹配整数
- `float`: 匹配浮点数
- `bool`: 匹配 `True` 与 `False` 以及他们小写形式
- `hex`: 匹配 `0x` 开头的十六进制字符串
- `url`: 匹配网址
- `email`: 匹配 `xxxx@xxx` 的字符串
- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串
- `list`: 匹配类似 `["foo","bar","baz"]` 的字符串
- `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串
- `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳
- `Any`: 匹配任意类型
- `AnyString`: 匹配任意类型,转为 `str`
- `Number`: 匹配 `int` 与 `float`,转为 `int`
同时可以使用 typing 中的类型:
- `Literal[X]`: 匹配其中的任意一个值
- `Union[X, Y]`: 匹配其中的任意一个类型
- `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值
- `List[X]`: 匹配一个列表,其中的元素为 `X` 类型
- `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型value 为 `Y` 类型
- ...
:::tip
几类特殊的传入标记:
- `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联)
- `RawStr("foo")`: 匹配字符串 "foo" (不会被 `BasePattern` 替换)
- `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz"
- `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型
- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值
- `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0]
- `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象
- `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值)
- ...
:::
`MultiVar` 则是一个特殊的标注,用于告知解析器该参数可以接受多个值,其构造方法形如 `MultiVar(str)`。
同样的还有 `KeyWordVar`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。
:::tip
`MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,其构造方法形如 `MultiVar(KeyWordVar(str))`
`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值。
`MultiVar` 不能在 `KeyWordVar` 之后传入。
:::
### 紧凑命令
`Alconna`, `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔:
```python
from arclet.alconna import Alconna, Option, CommandMeta, Args
alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True))
assert alc.parse("test123 BARabc").matched
```
这使得我们可以实现如下命令:
```python
>>> from arclet.alconna import Alconna, Option, Args, append
>>> alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True))
>>> alc.parse("gcc -Fabc -Fdef -Fxyz").query[list[str]]("flag.content")
['abc', 'def', 'xyz']
```
当 `Option` 的 `action` 为 `count` 时,其自动支持 `compact` 特性:
```python
>>> from arclet.alconna import Alconna, Option, Args, count
>>> alc = Alconna("pp", Option("--verbose|-v", action=count, default=0))
>>> alc.parse("pp -vvv").query[int]("verbose.value")
3
```
## 命令特性
### 配置
`arclet.alconna.Namespace` 表示某一命名空间下的默认配置:
```python
from arclet.alconna import config, namespace, Namespace
from arclet.alconna.tools import ShellTextFormatter
np = Namespace("foo", prefixes=["/"]) # 创建 Namespace 对象,并进行初始配置
with namespace("bar") as np1:
np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令
np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter
np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称
config.namespaces["foo"] = np # 将命名空间挂载到 config 上
```
同时也提供了默认命名空间配置与修改方法:
```python
from arclet.alconna import config, namespace, Namespace
config.default_namespace.prefixes = [...] # 直接修改默认配置
np = Namespace("xxx", prefixes=[...])
config.default_namespace = np # 更换默认的命名空间
with namespace(config.default_namespace.name) as np:
np.prefixes = [...]
```
### 半自动补全
半自动补全为用户提供了推荐后续输入的功能。
补全默认通过 `--comp` 或 `-cp` 或 `?` 触发:(命名空间配置可修改名称)
```python
from arclet.alconna import Alconna, Args, Option
alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar")
alc.parse("test ?")
'''
output
以下是建议的输入:
* <abc: int>
* --help
* -h
* -sct
* --shortcut
* foo
* bar
'''
```
### 快捷指令
快捷指令顾名思义,可以为基础指令创建便捷的触发方式
一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建,删除)
```python
>>> from arclet.alconna import Alconna, Args
>>> alc = Alconna("setu", Args["count", int])
>>> alc.shortcut("涩图(\d+)张", {"args": ["{0}"]})
'Alconna::setu 的快捷指令: "涩图(\\d+)张" 添加成功'
>>> alc.parse("涩图3张").query("count")
3
```
`shortcut` 的第一个参数为快捷指令名称,第二个参数为 `ShortcutArgs`,作为快捷指令的配置
```python
class ShortcutArgs(TypedDict):
"""快捷指令参数"""
command: NotRequired[DataCollection[Any]]
"""快捷指令的命令"""
args: NotRequired[list[Any]]
"""快捷指令的附带参数"""
fuzzy: NotRequired[bool]
"""是否允许命令后随参数"""
prefix: NotRequired[bool]
"""是否调用时保留指令前缀"""
```
当 `fuzzy` 为 False 时,传入 `"涩图1张 abc"` 之类的快捷指令将视为解析失败
快捷指令允许三类特殊的 placeholder:
- `{%X}`: 如 `setu {%0}`,表示此处填入快捷指令后随的第 X 个参数。
例如,若快捷指令为 `涩图`, 配置为 `{"command": "setu {%0}"}`, 则指令 `涩图 1` 相当于 `setu 1`
- `{*}`: 表示此处填入所有后随参数,并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。
- `{X}`: 表示此处填入可能的正则匹配的组:
- 若 `command` 中存在匹配组 `(xxx)`,则 `{X}` 表示第 X 个匹配组的内容
- 若 `command` 中存储匹配组 `(?P<xxx>...)`, 则 `{X}` 表示名字为 X 的匹配结果
除此之外, 通过内置选项 `--shortcut` 可以动态操作快捷指令。
例如:
- `cmd --shortcut <key> <cmd>` 来增加一个快捷指令
- `cmd --shortcut list` 来列出当前指令的所有快捷指令
- `cmd --shortcut delete key` 来删除一个快捷指令
### 使用模糊匹配
模糊匹配通过在 Alconna 中设置其 CommandMeta 开启。
模糊匹配会应用在任意需要进行名称判断的地方,如**命令名称****选项名称**和**参数名称**(如指定需要传入参数名称)。
```python
from arclet.alconna import Alconna, CommandMeta
alc = Alconna("test_fuzzy", meta=CommandMeta(fuzzy_match=True))
alc.parse("test_fuzy")
# output: test_fuzy is not matched. Do you mean "test_fuzzy"?
```
## 解析结果
`Alconna.parse` 会返回由 **Arparma** 承载的解析结果。
`Arpamar` 会有如下参数:
`Arparma` 会有如下参数:
- 调试类
@@ -430,47 +272,286 @@ alc.parse("test_fuzy")
- other_args: 除主参数外的其他解析结果
- all_matched_args: 所有 Args 的解析结果
`Arparma` 同时提供了便捷的查询方法 `query[type]()`,会根据传入的 `path` 查找参数并返回
`Arparma` 同时提供了便捷的查询方法 `query[type]()`,会根据传入的 `path` 查找参数并返回
`path` 支持如下
`path` 支持如下:
- `main_args`, `options`, ...: 返回对应的属性
- `main_args`, `options`, ...: 返回对应的属性
- `args`: 返回 all_matched_args
- `main_args.xxx`, `options.xxx`, ...: 返回字典中 `xxx`键对应的值
- `args.xxx`: 返回 all_matched_args 中 `xxx`键对应的值
- `options.foo`, `foo`: 返回选项 `foo` 的解析结果 (OptionResult)
- `options.foo.value`, `foo.value`: 返回选项 `foo` 的解析值
- `options.foo.args`, `foo.args`: 返回选项 `foo` 的解析参数字典
- `options.foo.args.bar`, `foo.bar`: 返回选项 `foo` 的参数字典中 `bar` 键对应的值
...
- `main_args.xxx`, `options.xxx`, ...: 返回字典中 `xxx`键对应的值
- `args.xxx`: 返回 all_matched_args 中 `xxx`键对应的值
- `options.foo`, `foo`: 返回选项 `foo` 的解析结果 (OptionResult)
- `options.foo.value`, `foo.value`: 返回选项 `foo` 的解析值
- `options.foo.args`, `foo.args`: 返回选项 `foo` 的解析参数字典
- `options.foo.args.bar`, `foo.bar`: 返回选项 `foo` 的参数字典中 `bar` 键对应的值 ...
同样, `Arparma["foo.bar"]` 的表现与 `query()` 一致
## 命名空间配置
命名空间配置 (以下简称命名空间) 相当于`Alconna`的设置,`Alconna`默认使用“Alconna”命名空间命名空间有以下几个属性
- name: 命名空间名称
- prefixes: 默认前缀配置
- separators: 默认分隔符配置
- formatter_type: 默认格式化器类型
- fuzzy_match: 默认是否开启模糊匹配
- raise_exception: 默认是否抛出异常
- builtin_option_name: 默认的内置选项名称(--help, --shortcut, --comp)
- enable_message_cache: 默认是否启用消息缓存
- compact: 默认是否开启紧凑模式
- strict: 命令是否严格匹配
- ...
### 新建命名空间并替换
```python
from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config
ns = Namespace("foo", prefixes=["/"])  # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/
alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间
# 可以通过with方式创建命名空间
with namespace("bar") as np1:
np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令
np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter
np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称
# 你还可以使用config来管理所有命名空间并切换至任意命名空间
config.namespaces["foo"] = ns # 将命名空间挂载到 config 上
alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=config.namespaces["foo"]) # 也是同样可以切换到"foo"命名空间
```
### 修改默认的命名空间
```python
from arclet.alconna import config, namespace, Namespace
config.default_namespace.prefixes = [...] # 直接修改默认配置
np = Namespace("xxx", prefixes=[...])
config.default_namespace = np # 更换默认的命名空间
with namespace(config.default_namespace.name) as np:
np.prefixes = [...]
```
## 快捷指令
快捷命令可以做到标识一段命令, 并且传递参数给原命令
一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建,删除)
`shortcut` 的第一个参数为快捷指令名称,第二个参数为 `ShortcutArgs`,作为快捷指令的配置:
```python
class ShortcutArgs(TypedDict):
"""快捷指令参数"""
command: NotRequired[DataCollection[Any]]
"""快捷指令的命令"""
args: NotRequired[list[Any]]
"""快捷指令的附带参数"""
fuzzy: NotRequired[bool]
"""是否允许命令后随参数"""
prefix: NotRequired[bool]
"""是否调用时保留指令前缀"""
```
### args的使用
```python
from arclet.alconna import Alconna, Args
alc = Alconna("setu", Args["count", int])
alc.shortcut("涩图(\d+)张", {"args": ["{0}"]})
# 'Alconna::setu 的快捷指令: "涩图(\\d+)张" 添加成功'
alc.parse("涩图3张").query("count")
# 3
```
### command的使用
```python
from arclet.alconna import Alconna, Args
alc = Alconna("eval", Args["content", str])
alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"})
# 'Alconna::eval 的快捷指令: "echo" 添加成功'
alc.shortcut("echo", delete=True) # 删除快捷指令
# 'Alconna::eval 的快捷指令: "echo" 删除成功'
@alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器
def cb(content: str):
eval(content, {}, {})
alc.parse('eval print(\\"hello world\\")')
# hello world
alc.parse("echo hello world!")
# hello world!
```
当 `fuzzy` 为 False 时,第一个例子中传入 `"涩图1张 abc"` 之类的快捷指令将视为解析失败
快捷指令允许三类特殊的 placeholder
- `{%X}`: 如 `setu {%0}`,表示此处填入快捷指令后随的第 X 个参数。
例如,若快捷指令为 `涩图`, 配置为 `{"command": "setu {%0}"}`, 则指令 `涩图 1` 相当于 `setu 1`
- `{*}`: 表示此处填入所有后随参数,并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。
- `{X}`: 表示此处填入可能的正则匹配的组:
- 若 `command` 中存在匹配组 `(xxx)`,则 `{X}` 表示第 X 个匹配组的内容
- 若 `command` 中存储匹配组 `(?P<xxx>...)`, 则 `{X}` 表示 **名字** 为 X 的匹配结果
除此之外, 通过 **Alconna** 内置选项 `--shortcut` 可以动态操作快捷指令
例如:
- `cmd --shortcut <key> <cmd>` 来增加一个快捷指令
- `cmd --shortcut list` 来列出当前指令的所有快捷指令
- `cmd --shortcut delete key` 来删除一个快捷指令
```python
from arclet.alconna import Alconna, Args
alc = Alconna("eval", Args["content", str])
alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"})
alc.parse("eval --shortcut list")
# 'echo'
```
## 紧凑命令
`Alconna`,  `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔:
```python
from arclet.alconna import Alconna, Option, CommandMeta, Args
alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True))
assert alc.parse("test123 BARabc").matched
```
这使得我们可以实现如下命令:
```python
from arclet.alconna import Alconna, Option, Args, append
alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True))
print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content"))
# ['abc', 'def', 'xyz']
```
当 `Option` 的 `action` 为 `count` 时,其自动支持 `compact` 特性:
```python
from arclet.alconna import Alconna, Option, count
alc = Alconna("pp", Option("--verbose|-v", action=count, default=0))
print(alc.parse("pp -vvv").query[int]("verbose.value"))
# 3
```
## 模糊匹配
模糊匹配通过在 Alconna 中设置其 CommandMeta 开启
模糊匹配会应用在任意需要进行名称判断的地方,如 **命令名称****选项名称** 和 **参数名称** (如指定需要传入参数名称)。
```python
from arclet.alconna import Alconna, CommandMeta
alc = Alconna("test_fuzzy", meta=CommandMeta(fuzzy_match=True))
alc.parse("test_fuzy")
# test_fuzy is not matched. Do you mean "test_fuzzy"?
```
## 半自动补全
半自动补全为用户提供了推荐后续输入的功能
补全默认通过 `--comp` 或 `-cp` 或 `?` 触发:(命名空间配置可修改名称)
```python
from arclet.alconna import Alconna, Args, Option
alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar")
alc.parse("test --comp")
'''
output
以下是建议的输入:
* <abc: int>
* --help
* -h
* -sct
* --shortcut
* foo
* bar
'''
```
## Duplication
**Duplication** 用来提供更好的自动补全,类似于 **ArgParse****Namespace**,经测试表现良好(好耶)。
**Duplication** 用来提供更好的自动补全,类似于 **ArgParse** 的 **Namespace**
普通情况下使用,需要利用到 **ArgsStub**、**OptionStub****SubcommandStub** 三个部分
普通情况下使用,需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分
pip 为例,其对应的 Duplication 应如下构造
pip为例其对应的 Duplication 应如下构造:
```python
from arclet.alconna import OptionResult, Duplication, SubcommandStub
from arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count
class MyDup(Duplication):
verbose: OptionResult
install: SubcommandStub # 选项与子命令对应的stub的变量名必须与其名字相同
```
    verbose: OptionResult
    install: SubcommandStub
并在解析时传入 Duplication
```python
alc = Alconna(
    "pip",
    Subcommand(
        "install",
        Args["package", str],
        Option("-r|--requirement", Args["file", str]),
        Option("-i|--index-url", Args["url", str]),
    ),
    Option("-v|--version"),
    Option("-v|--verbose", action=count),
)
res = alc.parse("pip -v install ...") # 不使用duplication获得的提示较少
print(res.query("install"))
# (value=Ellipsis args={'package': '...'} options={} subcommands={})
result = alc.parse("pip -v install ...", duplication=MyDup)
>>> type(result)
<class MyDup>
print(result.install)
# SubcommandStub(_origin=Subcommand('install', args=Args('package': str)), _value=Ellipsis, available=True, args=ArgsStub(_origin=Args('package': str), _value={'package': '...'}, available=True), dest='install', options=[OptionStub(_origin=Option('requirement', args=Args('file': str)), _value=None, available=False, args=ArgsStub(_origin=Args('file': str), _value={}, available=False), dest='requirement', aliases=['r', 'requirement'], name='requirement'), OptionStub(_origin=Option('index-url', args=Args('url': str)), _value=None, available=False, args=ArgsStub(_origin=Args('url': str), _value={}, available=False), dest='index-url', aliases=['index-url', 'i'], name='index-url')], subcommands=[], name='install')
```
**Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型:
**Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型:
```python
from typing import Optional

View File

@@ -10,7 +10,7 @@ description: 配置项
- **类型**: `bool`
- **默认值**: `False`
是否全局启用输出信息自动发送,不启用则会在触特殊内置选项后仍然将解析结果传递至响应器。
是否全局启用输出信息自动发送,不启用则会在触特殊内置选项后仍然将解析结果传递至响应器。
## alconna_use_command_start
@@ -31,11 +31,18 @@ description: 配置项
- **类型**: `bool`
- **默认值**: `False`
是否全局使用原始消息 (即未经过 to_me 等处理的), 该选项会影响到 Alconna 的匹配行为。
是否全局使用原始消息 (即未经过 to_me 等处理的)该选项会影响到 Alconna 的匹配行为。
## alconna_use_param
## alconna_use_command_sep
- **类型**: `bool`
- **默认值**: `True`
- **默认值**: `False`
是否使用特制的 Param 提供更好的依赖注入,该选项不会对使用依赖注入函数形式造成影响
是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符。
## alconna_global_extensions
- **类型**: `List[str]`
- **默认值**: `[]`
全局加载的扩展,路径以 . 分隔,如 `foo.bar.baz:DemoExtension`

View File

@@ -3,15 +3,16 @@ sidebar_position: 3
description: 响应规则的使用
---
# Alconna 响应规则
# Alconna 插件
以下为一个简单的使用示例
展示
```python
from nonebot_plugin_alconna.adapters.onebot12 import Image
from nonebot_plugin_alconna import At, AlconnaMatches, on_alconna
from nonebot_plugin_alconna import At, on_alconna
from arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand
alc = Alconna(
["/", "!"],
"role-group",
@@ -27,9 +28,9 @@ rg = on_alconna(alc, auto_send_output=True)
@rg.handle()
async def _(result: Arparma = AlconnaMatches()):
async def _(result: Arparma):
if result.find("list"):
img = await gen_role_group_list_image()
img = await ob12_gen_role_group_list_image()
await rg.finish(Image(img))
if result.find("add"):
group = await create_role_group(result.query[str]("add.name"))
@@ -41,74 +42,97 @@ async def _(result: Arparma = AlconnaMatches()):
## 响应器使用
`on_alconna` 的所有参数如下
- `command: Alconna | str`: Alconna 命令
- `skip_for_unmatch: bool = True`: 是否在命令不匹配时跳过该响应
- `auto_send_output: bool = False`: 是否自动发送输出信息并跳过响应
- `output_converter: TConvert | None = None`: 输出信息字符串转换为消息序列方法
- `aliases: set[str | tuple[str, ...]] | None = None`: 命令别名, 作用类似于 `on_command` 中的 aliases
- `comp_config: CompConfig | None = None`: 补全会话配置, 不传入则不启用补全会话
- `use_origin: bool = False`: 是否使用未经 to_me 等处理过的消息
- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀
`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher`,其拓展了如下方法:
- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理
- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上,会以 path 对应的参数为准,读取传入 message 的最后一个消息段并验证转换
- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg``got_arg`,为 `got_path` 的特化版本
- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`,对应 `got_path`
- `.dispatch`: 同样的分派处理,但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`
- `.got`, `send`, `reject`, ...: 拓展了 prompt 类型,即支持使用 `UniMessage` 作为 prompt
用例:
本插件基于 **Alconna**,为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`
```python
def on_alconna(
command: Alconna | str,
skip_for_unmatch: bool = True,
auto_send_output: bool = False,
aliases: set[str | tuple[str, ...]] | None = None,
comp_config: CompConfig | None = None,
extensions: list[type[Extension] | Extension] | None = None,
exclude_ext: list[type[Extension] | str] | None = None,
use_origin: bool = False,
use_cmd_start: bool = False,
use_cmd_sep: bool = False,
**kwargs,
...,
):
```
- `command`: Alconna 命令或字符串,字符串将通过 `AlconnaFormat` 转换为 Alconna 命令
- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应
- `auto_send_output`: 是否自动发送输出信息并跳过响应
- `aliases`: 命令别名, 作用类似于 `on_command` 中的 aliases
- `comp_config`: 补全会话配置, 不传入则不启用补全会话
- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例
- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id
- `use_origin`: 是否使用未经 to_me 等处理过的消息
- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀
- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符
`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher` ,其拓展了如下方法:
- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理(具体请看[条件控制](./matcher.md#条件控制)
- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上,会以 path 对应的参数为准,读取传入 message 的最后一个消息段并验证转换
- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`,为 `got_path` 的特化版本
- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`,对应 `got_path`
- `.dispatch`: 同样的分派处理,但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`
- `.got`, `send`, `reject`, ... : 拓展了 prompt 类型,即支持使用 `UniMessage` 作为 prompt
实例:
```python
from nonebot import require
require("nonebot_plugin_alconna")
from arclet.alconna import Alconna, Option, Args
from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match, AlconnaMatcher, AlconnaArg
from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match, UniMessage
login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall")))
login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall"))) # 这里["/"]指命令前缀必须是/
# /login -r 触发
@login.assign("recall")
async def login_exit():
await login.finish("已退出")
    await login.finish("已退出")
# /login xxx 触发
@login.assign("password")
async def login_handle(matcher: AlconnaMatcher, pw: Match[str] = AlconnaMatch("password")):
matcher.set_path_arg("password", pw.result)
async def login_handle(pw: Match[str] = AlconnaMatch("password")):
    if pw.available:
        login.set_path_arg("password", pw.result)
@login.got_path("password", prompt="请输入密码")
async def login_got(password: str = AlconnaArg("password")):
# /login 触发
@login.got_path("password", prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请输入密码"))
async def login_got(password: str):
assert password
await login.send("登录成功")
```
## 依赖注入
`Alconna` 的解析结果会放入 `Arparma` 类中,或用户指定的 `Duplication` 类。
本插件提供了一系列依赖注入函数,便于在响应函数中获取解析结果:
`nonebot_plugin_alconna` 提供了一系列的依赖注入函数,他们包括:
- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
- `AlconnaMatch`: `Match` 类型的依赖注入函数
- `AlconnaQuery`: `Query` 类型的依赖注入函数
- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
- `AlconnaMatch`: `Match` 类型的依赖注入函数,其能够额外传入一个 middleware 函数来处理得到的参数
- `AlconnaQuery`: `Query` 类型的依赖注入函数,其能够额外传入一个 middleware 函数来处理得到的参数
- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数
同时,基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832),添加了两类注解:
- `AlcMatches`:同 `AlconnaMatches`
- `AlcResult`:同 `AlconnaResult`
可以看到,本插件提供了几类额外的模型:
- `CommandResult`: 解析结果,包括了源命令 `source: Alconna` ,解析结果 `result: Arparma`,以及可能的输出信息 `output: str | None` 字段
- `Match`: 匹配项,表示参数是否存在于 `all_matched_args` 内,可用 `Match.available` 判断是否匹配,`Match.result` 获取匹配的值
- `Query`: 查询项,表示参数是否可由 `Arparma.query` 查询并获得结果,可用 `Query.available` 判断是否查询成功,`Query.result` 获取查询结果
- `CommandResult`: 解析结果,包括了源命令 `source: Alconna` ,解析结果 `result: Arparma`,以及可能的输出信息 `output: str | None` 字段
- `Match`: 匹配项,表示参数是否存在于 `all_matched_args` 内,可用 `Match.available` 判断是否匹配,`Match.result` 获取匹配的值
- `Query`: 查询项,表示参数是否可由 `Arparma.query` 查询并获得结果,可用 `Query.available` 判断是否查询成功,`Query.result` 获取查询结果
同时,基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了三类注解:
- `AlcMatches`:同 `AlconnaMatches`
- `AlcResult`:同 `AlconnaResult`
- `AlcExecResult`: 同 `AlconnaExecResult`
而若设置配置项 **ALCONNA_USE_PARAM** (默认为 True) 为 True则上述依赖注入的目标参数皆不需要使用依赖注入函数
**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数, 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效:
```python
async def handle(
@@ -123,25 +147,36 @@ async def handle(
...
```
该效果对于 `got_path` 下的 Arg 同样有效
:::note
如果你更喜欢 Depends 式的依赖注入,`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数,他们包括:
- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
- `AlconnaMatch`: `Match` 类型的依赖注入函数,其能够额外传入一个 middleware 函数来处理得到的参数
- `AlconnaQuery`: `Query` 类型的依赖注入函数,其能够额外传入一个 middleware 函数来处理得到的参数
- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数
- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数
:::
实例:
```python
...
from nonebot import require
require("nonebot_plugin_alconna")
...
from nonebot_plugin_alconna import (
on_alconna,
Match,
Query,
AlconnaQuery,
AlconnaMatch,
AlcResult
)
from arclet.alconna import Alconna, Args, Option, Arparma
test = on_alconna(
Alconna(
"test",
@@ -151,7 +186,6 @@ test = on_alconna(
auto_send_output=True
)
@test.handle()
async def handle_test1(result: AlcResult):
await test.send(f"matched: {result.matched}")
@@ -163,34 +197,23 @@ async def handle_test2(result: Arparma):
await test.send(f"args: {result.all_matched_args}")
@test.handle()
async def handle_test3(bar: Match[int]):
async def handle_test3(bar: Match[int] = AlconnaMatch("bar")):
if bar.available:
await test.send(f"foo={bar.result}")
@test.handle()
async def handle_test4(qux: Query[bool] = AlconnaQuery("baz.qux", False)):
async def handle_test4(qux: Query[bool] = Query("baz.qux", False)):
if qux.available:
await test.send(f"baz.qux={qux.result}")
```
## 消息段标注
## 多平台适配
示例中使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。
本插件提供了通用消息段标注, 通用消息段序列, 使插件使用者可以忽略平台之间字段的差异
适配器下的消息段标注会匹配特定的 `MessageSegment`
响应器使用示例中使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。
而通用标注与适配器标注的区别在于,通用标注会匹配多个适配器中相似类型的消息段,并返回
`nonebot_plugin_alconna.uniseg` 中定义的 [`Segment` 模型](./utils.md#通用消息段)
例如:
```python
...
ats = result.query[tuple[At, ...]]("add.member.target")
group.extend(member.target for member in ats)
```
这样插件使用者就不用考虑平台之间字段的差异
具体介绍和使用请查看 [通用信息组件](./uniseg.mdx#通用消息段)
本插件为以下适配器提供了专门的适配器标注:
@@ -200,7 +223,8 @@ group.extend(member.target for member in ats)
| [Telegram](https://github.com/nonebot/adapter-telegram) | adapters.telegram |
| [飞书](https://github.com/nonebot/adapter-feishu) | adapters.feishu |
| [GitHub](https://github.com/nonebot/adapter-github) | adapters.github |
| [QQ 频道](https://github.com/nonebot/adapter-qqguild) | adapters.qqguild |
| [QQ bot](https://github.com/nonebot/adapter-qq) | adapters.qq |
| [QQ 频道 bot](https://github.com/nonebot/adapter-qq) | adapters.qqguild |
| [钉钉](https://github.com/nonebot/adapter-ding) | adapters.ding |
| [Console](https://github.com/nonebot/adapter-console) | adapters.console |
| [开黑啦](https://github.com/Tian-que/nonebot-adapter-kaiheila) | adapters.kook |
@@ -212,10 +236,11 @@ group.extend(member.target for member in ats)
| [Villa](https://github.com/CMHopeSunshine/nonebot-adapter-villa) | adapters.villa |
| [Discord](https://github.com/nonebot/adapter-discord) | adapters.discord |
| [Red 协议](https://github.com/nonebot/adapter-red) | adapters.red |
| [Satori 协议](https://github.com/nonebot/adapter-satori) | adapters.satori |
## 条件控制
本插件可以通过 `handle(parameterless)` 来控制一个具体的响应函数是否在不满足条件时跳过响应。
本插件可以通过 `assign` 来控制一个具体的响应函数是否在不满足条件时跳过响应。
```python
...
@@ -224,7 +249,8 @@ require("nonebot_plugin_alconna")
...
from arclet.alconna import Alconna, Subcommand, Option, Args
from nonebot_plugin_alconna import assign, on_alconna, CommandResult, Check
from nonebot_plugin_alconna import on_alconna, CommandResult
pip = Alconna(
"pip",
@@ -238,48 +264,252 @@ pip = Alconna(
pip_cmd = on_alconna(pip)
# 仅在命令为 `pip install` 并且 pak 为 `pip` 时响应
@pip_cmd.handle([Check(assign("install.pak", "pip"))])
async def update(arp: CommandResult):
...
# 仅在命令为 `pip list` 时响应
@pip_cmd.handle([Check(assign("list"))])
async def list_(arp: CommandResult):
...
# 仅在命令为 `pip install` 时响应
@pip_cmd.handle([Check(assign("install"))])
async def install(arp: CommandResult):
...
```
或者使用 `AlconnaMatcher.assign`
```python
# 仅在命令为 `pip install pip` 时响应
@pip_cmd.assign("install.pak", "pip")
async def update(arp: CommandResult):
async def update(res: CommandResult):
...
# 仅在命令为 `pip list` 时响应
@pip_cmd.assign("list")
async def list_(arp: CommandResult):
async def list_(res: CommandResult):
...
# 在命令为 `pip install` 时响应
# 在命令为 `pip install xxx` 时响应
@pip_cmd.assign("install")
async def install(arp: CommandResult):
async def install(res: CommandResult):
...
```
或者使用 `AlconnaMatcher.dispatch`
此外,还能像 `CommandGroup` 一样为每个分发设置独立的 matcher
此外,使用 `AlconnaMatcher.dispatch` 还能像 `CommandGroup` 一样为每个分发设置独立的 matcher
```python
update_cmd = pip_cmd.dispatch("install.pak", "pip")
@update_cmd.handle()
async def update(arp: CommandResult = AlconnaResult()):
async def update(arp: CommandResult):
...
```
另外,`AlconnaMatcher` 有类似于 `got``got_path`
```python
from nonebot_plugin_alconna import At, Match, UniMessage, on_alconna
test_cmd = on_alconna(Alconna("test", Args["target?", Union[str, At]]))
@test_cmd.handle()
async def tt_h(target: Match[Union[str, At]]):
if target.available:
test_cmd.set_path_arg("target", target.result)
@test_cmd.got_path("target", prompt="请输入目标")
async def tt(target: Union[str, At]):
await test_cmd.send(UniMessage(["ok\n", target]))
```
`got_path``assign``Match``Query` 等地方一样,都需要指明 `path` 参数 (即对应 Arg 验证的路径)
`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型,例如示例中 `target` 对应的 Arg 里要求 str 或 At则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数。
:::tip
`path` 支持 ~XXX 语法,其会把 ~ 替换为可能的父级路径:
```python
pip = Alconna(
"pip",
Subcommand(
"install",
Args["pak", str],
Option("--upgrade|-U"),
Option("--force-reinstall"),
),
Subcommand("list", Option("--out-dated")),
)
pipcmd = on_alconna(pip)
pip_install_cmd = pipcmd.dispatch("install")
@pip_install_cmd.assign("~upgrade")
async def pip1_u(pak: Query[str] = Query("~pak")):
await pip_install_cmd.finish(f"pip upgrading {pak.result}...")
```
:::
## 响应器创建装饰
本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数, 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器:
```python
from nonebot_plugin_alconna import funcommand
@funcommand()
async def echo(msg: str):
return msg
```
其等同于:
```python
from arclet.alconna import Alconna, Args
from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match
echo = on_alconna(Alconna("echo", Args["msg", str]))
@echo.handle()
async def echo_exit(msg: Match[str] = AlconnaMatch("msg")):
await echo.finish(msg.result)
```
## 类Koishi构造器
本插件提供了一个 `Command` 构造器,其基于 `arclet.alconna.tools` 中的 `AlconnaString` 以类似 `Koishi` 中注册命令的方式来构建一个 **AlconnaMatcher**
```python
from nonebot_plugin_alconna import Command, Arparma
book = (
Command("book", "测试")
.option("writer", "-w <id:int>")
.option("writer", "--anonymous", {"id": 0})
.usage("book [-w <id:int> | --anonymous]")
.shortcut("测试", {"args": ["--anonymous"]})
.build()
)
@book.handle()
async def _(arp: Arparma):
await book.send(str(arp.options))
```
甚至,你可以设置 `action` 来设定响应行为:
```python
book = (
Command("book", "测试")
.option("writer", "-w <id:int>")
.option("writer", "--anonymous", {"id": 0})
.usage("book [-w <id:int> | --anonymous]")
.shortcut("测试", {"args": ["--anonymous"]})
.action(lambda options: str(options)) # 会自动通过 bot.send 发送
.build()
)
```
## 返回值回调
在 `AlconnaMatch``AlconnaQuery` 或 `got_path` 中,你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数:
```python
from nonebot_plugin_alconna import image_fetch
mask_cmd = on_alconna(
Alconna("search", Args["img?", Image]),
)
@mask_cmd.handle()
async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img", image_fetch)):
result = await search_img(img.result)
await matcher.send(result.content)
```
其中,`image_fetch` 是一个中间件,其接受一个 `Image` 对象,并提取图片的二进制数据返回。
## 匹配拓展
本插件提供了一个 `Extension` 类,其用于自定义 AlconnaMatcher 的部分行为
例如 `LLMExtension` (仅举例)
```python
from nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface
class LLMExtension(Extension):
@property
def priority(self) -> int:
return 10
@property
def id(self) -> str:
return "LLMExtension"
def __init__(self, llm):
self.llm = llm
def post_init(self, alc: Alconna) -> None:
self.llm.add_context(alc.command, alc.meta.description)
async def receive_wrapper(self, bot, event, receive):
resp = await self.llm.input(str(receive))
return receive.__class__(resp.content)
def before_catch(self, name, annotation, default):
return name == "llm"
def catch(self, interface: Interface):
if interface.name == "llm":
return self.llm
matcher = on_alconna(
Alconna(...),
extensions=[LLMExtension(LLM)]
)
...
```
那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息,同时可以在响应器中为所有 `llm` 参数注入模型变量。
目前 `Extension` 的功能有:
- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应
- `output_converter`: 输出信息的自定义转换方法
- `message_provider`: 从传入事件中自定义提取消息的方法
- `receive_provider`: 对传入的消息 (Message 或 UniMessage) 的额外处理
- `permission_check`: 命令对消息解析并确认头部匹配(即确认选择响应)时对发送者的权限判断
- `parse_wrapper`: 对命令解析结果的额外处理
- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理
- `before_catch`: 自定义依赖注入的绑定确认函数
- `catch`: 自定义依赖注入处理函数
- `post_init`: 响应器创建后对命令对象的额外处理
例如内置的 `DiscordSlashExtension`,其可自动将 Alconna 对象翻译成 slash 指令并注册,且将收到的指令交互事件转为指令供命令解析:
```python
from nonebot_plugin_alconna import Match, on_alconna
from nonebot_plugin_alconna.adapters.discord import DiscordSlashExtension
alc = Alconna(
["/"],
"permission",
Subcommand("add", Args["plugin", str]["priority?", int]),
Option("remove", Args["plugin", str]["time?", int]),
meta=CommandMeta(description="权限管理"),
)
matcher = on_alconna(alc, extensions=[DiscordSlashExtension()])
@matcher.assign("add")
async def add(plugin: Match[str], priority: Match[int]):
await matcher.finish(f"added {plugin.result} with {priority.result if priority.available else 0}")
@matcher.assign("remove")
async def remove(plugin: Match[str], time: Match[int]):
await matcher.finish(f"removed {plugin.result} with {time.result if time.available else -1}")
```
:::tip
全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后,这部分响应器会被追加拓展)
:::

View File

@@ -0,0 +1,445 @@
---
sidebar_position: 5
description: 通用消息组件
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
# 通用消息组件
`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件,其提供了一套通用的消息组件,用于在 `nonebot-plugin-alconna` 下构建通用消息。
## 通用消息段
适配器下的消息段标注会匹配适配器特定的 `MessageSegment` 而通用消息段与适配器消息段的区别在于:
通用消息段会匹配多个适配器中相似类型的消息段,并返回 `uniseg` 模块中定义的 [`Segment` 模型](https://nonebot.dev/docs/next/best-practice/alconna/utils#%E9%80%9A%E7%94%A8%E6%B6%88%E6%81%AF%E6%AE%B5), 以达到**跨平台接收消息**的作用。
`nonebot-plugin-alconna.uniseg` 提供了类似 `MessageSegment` 的通用消息段,并可在 `Alconna` 下直接标注使用:
```python
class Segment:
"""基类标注"""
class Text(Segment):
"""Text对象, 表示一类文本元素"""
text: str
style: Optional[str]
class At(Segment):
"""At对象, 表示一类提醒某用户的元素"""
type: Literal["user", "role", "channel"]
target: str
class AtAll(Segment):
"""AtAll对象, 表示一类提醒所有人的元素"""
class Emoji(Segment):
"""Emoji对象, 表示一类表情元素"""
id: str
name: Optional[str]
class Media(Segment):
url: Optional[str]
id: Optional[str]
path: Optional[str]
raw: Optional[bytes]
class Image(Media):
"""Image对象, 表示一类图片元素"""
class Audio(Media):
"""Audio对象, 表示一类音频元素"""
class Voice(Media):
"""Voice对象, 表示一类语音元素"""
class Video(Media):
"""Video对象, 表示一类视频元素"""
class File(Segment):
"""File对象, 表示一类文件元素"""
id: str
name: Optional[str]
class Reply(Segment):
"""Reply对象表示一类回复消息"""
id: str
"""此处不一定是消息ID可能是其他ID如消息序号等"""
msg: Optional[Union[Message, str]]
origin: Optional[Any]
class Reference(Segment):
"""Reference对象表示一类引用消息。转发消息 (Forward) 也属于此类"""
id: Optional[str]
"""此处不一定是消息ID可能是其他ID如消息序号等"""
content: Optional[Union[Message, str, List[Union[RefNode, CustomNode]]]]
class Card(Segment):
type: Literal["xml", "json"]
raw: str
class Other(Segment):
"""其他 Segment"""
```
此类消息段通过 `UniMessage.export` 可以转为特定的 `MessageSegment`
## 通用消息序列
`nonebot-plugin-alconna.uniseg` 同时提供了一个类似于 `Message` 的 `UniMessage` 类型,其元素为经过通用标注转换后的通用消息段。
你可以用如下方式获取 `UniMessage`
<Tabs groupId="get_unimsg">
<TabItem value="depend" label="使用依赖注入">
通过提供的 `UniversalMessage` 或 `UniMsg` 依赖注入器来获取 `UniMessage`。
```python
from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply
matcher = on_xxx(...)
@matcher.handle()
async def _(msg: UniMsg):
reply = msg[Reply, 0]
print(reply.origin)
if msg.has(At):
ats = msg.get(At)
print(ats)
...
```
</TabItem>
<TabItem value="method" label="使用 UniMessage.generate">
注意,`generate` 方法在响应器以外的地方如果不传入 `event` 与 `bot` 则无法处理 reply。
```python
from nonebot import Message, EventMessage
from nonebot_plugin_alconna.uniseg import UniMessage
matcher = on_xxx(...)
@matcher.handle()
async def _(message: Message = EventMessage()):
msg = await UniMessage.generate(message=message)
```
</TabItem>
</Tabs>
不仅如此,你还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**。
`UniMessage.export` 会通过传入的 `bot: Bot` 参数,或上下文中的 `Bot` 对象读取适配器信息,并使用对应的生成方法把通用消息转为适配器对应的消息序列:
```python
from nonebot import Bot, on_command
from nonebot_plugin_alconna.uniseg import Image, UniMessage
test = on_command("test")
@test.handle()
async def handle_test():
await test.send(await UniMessage(Image(path="path/to/img")).export())
```
而在 `AlconnaMatcher` 下,`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`,不需要手动调用 export 方法:
```python
from arclet.alconna import Alconna, Args
from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna
from nonebot_plugin_alconna.uniseg import At, UniMessage
test_cmd = on_alconna(Alconna("test", Args["target?", At]))
@test_cmd.handle()
async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
if target.available:
matcher.set_path_arg("target", target.result)
@test_cmd.got_path("target", prompt="请输入目标")
async def tt(target: At):
await test_cmd.send(UniMessage([target, "\ndone."]))
```
除此之外 `UniMessage.send` 方法基于 `UniMessage.export` 并调用各适配器下的发送消息方法,返回一个 `Receipt` 对象,用于修改/撤回消息:
```python
from nonebot import Bot, on_command
from nonebot_plugin_alconna.uniseg import UniMessage
test = on_command("test")
@test.handle()
async def handle():
receipt = await UniMessage.text("hello!").send(at_sender=True, reply_to=True)
await receipt.recall(delay=1)
```
:::caution
在响应器以外的地方,`bot` 参数必须手动传入。
:::
### 构造
如同 `Message`, `UniMessage` 可以传入单个字符串/消息段,或可迭代的字符串/消息段:
```python
from nonebot_plugin_alconna.uniseg import UniMessage, At
msg = UniMessage("Hello")
msg1 = UniMessage(At("user", "124"))
msg2 = UniMessage(["Hello", At("user", "124")])
```
`UniMessage` 上同时存在便捷方法,令其可以链式地添加消息段:
```python
from nonebot_plugin_alconna.uniseg import UniMessage, At, Image
msg = UniMessage.text("Hello").at("124").image(path="/path/to/img")
assert msg == UniMessage(
["Hello", At("user", "124"), Image(path="/path/to/img")]
)
```
### 拼接消息
`str`、`UniMessage`、`Segment` 对象之间可以直接相加,相加均会返回一个新的 `UniMessage` 对象:
```python
# 消息序列与消息段相加
UniMessage("text") + Text("text")
# 消息序列与字符串相加
UniMessage([Text("text")]) + "text"
# 消息序列与消息序列相加
UniMessage("text") + UniMessage([Text("text")])
# 字符串与消息序列相加
"text" + UniMessage([Text("text")])
# 消息段与消息段相加
Text("text") + Text("text")
# 消息段与字符串相加
Text("text") + "text"
# 消息段与消息序列相加
Text("text") + UniMessage([Text("text")])
# 字符串与消息段相加
"text" + Text("text")
```
如果需要在当前消息序列后直接拼接新的消息段,可以使用 `Message.append`、`Message.extend` 方法,或者使用自加:
```python
msg = UniMessage([Text("text")])
# 自加
msg += "text"
msg += Text("text")
msg += UniMessage([Text("text")])
# 附加
msg.append(Text("text"))
# 扩展
msg.extend([Text("text")])
```
### 使用消息模板
`UniMessage.template` 同样类似于 `Message.template`,可以用于格式化消息,大体用法参考 [消息模板](../../tutorial/message#使用消息模板)。
这里额外说明 `UniMessage.template` 的拓展控制符
相比 `Message`UniMessage 对于 {:XXX} 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行
以 At(...) 为例:
```python title=使用通用消息段的拓展控制符
>>> from nonebot_plugin_alconna.uniseg import UniMessage
>>> UniMessage.template("{:At(user, target)}").format(target="123")
UniMessage(At("user", "123"))
>>> UniMessage.template("{:At(type=user, target=id)}").format(id="123")
UniMessage(At("user", "123"))
>>> UniMessage.template("{:At(type=user, target=123)}").format()
UniMessage(At("user", "123"))
```
而在 `AlconnaMatcher` 中,{:XXX} 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能:
```python title=在AlconnaMatcher中使用通用消息段的拓展控制符
from arclet.alconna import Alconna, Args
from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna
test_cmd = on_alconna(Alconna("test", Args["target?", At]))
@test_cmd.handle()
async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
if target.available:
matcher.set_path_arg("target", target.result)
@test_cmd.got_path(
"target",
prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请确认目标")
)
async def tt():
await test_cmd.send(
UniMessage.template("{:At(user, $event.get_user_id())} 已确认目标为 {target}")
)
```
另外也有 `$message_id` 与 `$target` 两个特殊值。
### 检查消息段
我们可以通过 `in` 运算符或消息序列的 `has` 方法来:
```python
# 是否存在消息段
At("user", "1234") in message
# 是否存在指定类型的消息段
At in message
```
我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段:
```python
# 是否都为 "test"
message.only("test")
# 是否仅包含指定类型的消息段
message.only(Text)
```
### 获取消息纯文本
类似于 `Message.extract_plain_text()`,用于获取通用消息的纯文本:
```python
from nonebot_plugin_alconna.uniseg import UniMessage, At
# 提取消息纯文本字符串
assert UniMessage(
[At("user", "1234"), "text"]
).extract_plain_text() == "text"
```
### 遍历
通用消息序列继承自 `List[Segment]` ,因此可以使用 `for` 循环遍历消息段:
```python
for segment in message: # type: Segment
...
```
### 过滤、索引与切片
消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片:
```python
from nonebot_plugin_alconna.uniseg import UniMessage, At, Text, Reply
message = UniMessage(
[
Reply(...),
"text1",
At("user", "1234"),
"text2"
]
)
# 索引
message[0] == Reply(...)
# 切片
message[0:2] == UniMessage([Reply(...), Text("text1")])
# 类型过滤
message[At] == Message([At("user", "1234")])
# 类型索引
message[At, 0] == At("user", "1234")
# 类型切片
message[Text, 0:2] == UniMessage([Text("text1"), Text("text2")])
```
我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤:
```python
message.include(Text, At)
message.exclude(Reply)
```
同样的,消息序列对列表的 `index`、`count` 方法也进行了增强,可以用于索引指定类型的消息段:
```python
# 指定类型首个消息段索引
message.index(Text) == 1
# 指定类型消息段数量
message.count(Text) == 2
```
此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段:
```python
# 获取指定类型指定个数的消息段
message.get(Text, 1) == UniMessage([Text("test1")])
```
### 消息发送
前面提到,通用消息可用 `UniMessage.send` 发送自身:
```python
async def send(
self,
target: Union[Event, Target, None] = None,
bot: Optional[Bot] = None,
fallback: bool = True,
at_sender: Union[str, bool] = False,
reply_to: Union[str, bool] = False,
) -> Receipt:
```
实际上,`UniMessage` 同时提供了获取消息事件 id 与消息发送对象的方法:
```python
from nonebot import Event, Bot
from nonebot_plugin_alconna.uniseg import UniMessage, Target
matcher = on_xxx(...)
@matcher.handle()
asycn def _(bot: Bot, event: Event):
target: Target = UniMessage.get_target(event, bot)
msg_id: str = UniMessage.get_message_id(event, bot)
```
`send`, `get_target`, `get_message_id` 中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象。
其中,`Target`:
```python
class Target:
id: str
"""目标id若为群聊则为group_id或者channel_id若为私聊则为user_id"""
parent_id: str = ""
"""父级id若为频道则为guild_id其他情况为空字符串"""
channel: bool = False
"""是否为频道,仅当目标平台同时支持群聊和频道时有效"""
private: bool = False
"""是否为私聊"""
source: str = ""
"""可能的事件id"""
```
是用来描述响应消息时的发送对象。
同样的,你可以通过依赖注入的方式在响应器中直接获取它们。

View File

@@ -1,317 +0,0 @@
---
sidebar_position: 5
description: 杂项
---
# 杂项
## 通用消息段
`nonebot-plugin-alconna` 提供了类似 `MessageSegment` 的通用消息段,并可在 `Alconna` 下直接标注使用:
```python
class Segment:
"""基类标注"""
class Text(Segment):
"""Text对象, 表示一类文本元素"""
text: str
style: Optional[str]
class At(Segment):
"""At对象, 表示一类提醒某用户的元素"""
type: Literal["user", "role", "channel"]
target: str
class AtAll(Segment):
"""AtAll对象, 表示一类提醒所有人的元素"""
class Emoji(Segment):
"""Emoji对象, 表示一类表情元素"""
id: str
name: Optional[str]
class Media(Segment):
url: Optional[str]
id: Optional[str]
path: Optional[str]
raw: Optional[bytes]
class Image(Media):
"""Image对象, 表示一类图片元素"""
class Audio(Media):
"""Audio对象, 表示一类音频元素"""
class Voice(Media):
"""Voice对象, 表示一类语音元素"""
class Video(Media):
"""Video对象, 表示一类视频元素"""
class File(Segment):
"""File对象, 表示一类文件元素"""
id: str
name: Optional[str]
class Reply(Segment):
"""Reply对象表示一类回复消息"""
origin: Any
id: str
msg: Optional[Union[Message, str]]
class Card(Segment):
type: Literal["xml", "json"]
raw: str
class Other(Segment):
"""其他 Segment"""
```
来自各自适配器的消息序列都会经过这些通用消息段对应的标注转换,以达到跨平台接收消息的作用
## 通用消息序列
除了通用消息段外,`nonebot-plugin-alconna` 还提供了一个类似于 `Message``UniMessage` 类型,其元素为经过通用标注转换后的通用消息段。
你可以通过提供的 `UniversalMessage``UniMsg` 依赖注入器来获取 `UniMessage`
```python
from nonebot_plugin_alconna import UniMsg, At, Reply
matcher = on_xxx(...)
@matcher.handle()
async def _(msg: UniMsg):
reply = msg[Reply, 0]
print(reply.origin)
if msg.has(At):
ats = msg.get(At)
print(ats)
...
```
### 获取消息纯文本
类似于 `Message.extract_plain_text()`,用于获取通用消息的纯文本。
```python
from nonebot_plugin_alconna import UniMessage, At
# 提取消息纯文本字符串
assert UniMessage(
[At("user", "1234"), "text"]
).extract_plain_text() == "text"
```
### 遍历
通用消息序列继承自 `List[Segment]` ,因此可以使用 `for` 循环遍历消息段。
```python
for segment in message: # type: Segment
...
```
### 检查消息段
我们可以通过 `in` 运算符或消息序列的 `has` 方法来:
```python
# 是否存在消息段
At("user", "1234") in message
# 是否存在指定类型的消息段
At in message
```
我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段。
```python
# 是否都为 "test"
message.only("test")
# 是否仅包含指定类型的消息段
message.only(Text)
```
### 过滤、索引与切片
消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。
```python
from nonebot_plugin_alconna import UniMessage, At, Text, Reply
message = UniMessage(
[
Reply(...),
"text1",
At("user", "1234"),
"text2"
]
)
# 索引
message[0] == Reply(...)
# 切片
message[0:2] == UniMessage([Reply(...), Text("text1")])
# 类型过滤
message[At] == Message([At("user", "1234")])
# 类型索引
message[At, 0] == At("user", "1234")
# 类型切片
message[Text, 0:2] == UniMessage([Text("text1"), Text("text2")])
```
我们也可以通过消息序列的 `include``exclude` 方法进行类型过滤。
```python
message.include(Text, At)
message.exclude(Reply)
```
同样的,消息序列对列表的 `index``count` 方法也进行了增强,可以用于索引指定类型的消息段。
```python
# 指定类型首个消息段索引
message.index(Text) == 1
# 指定类型消息段数量
message.count(Text) == 2
```
此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段。
```python
# 获取指定类型指定个数的消息段
message.get(Text, 1) == UniMessage([Text("test1")])
```
### 拼接消息
`str``UniMessage``Segment` 对象之间可以直接相加,相加均会返回一个新的 `UniMessage` 对象。
```python
# 消息序列与消息段相加
UniMessage("text") + Text("text")
# 消息序列与字符串相加
UniMessage([Text("text")]) + "text"
# 消息序列与消息序列相加
UniMessage("text") + UniMessage([Text("text")])
# 字符串与消息序列相加
"text" + UniMessage([Text("text")])
# 消息段与消息段相加
Text("text") + Text("text")
# 消息段与字符串相加
Text("text") + "text"
# 消息段与消息序列相加
Text("text") + UniMessage([Text("text")])
# 字符串与消息段相加
"text" + Text("text")
```
如果需要在当前消息序列后直接拼接新的消息段,可以使用 `Message.append``Message.extend` 方法,或者使用自加。
```python
msg = UniMessage([Text("text")])
# 自加
msg += "text"
msg += Text("text")
msg += UniMessage([Text("text")])
# 附加
msg.append(Text("text"))
# 扩展
msg.extend([Text("text")])
```
## 跨平台发送
`nonebot-plugin-alconna` 不仅支持跨平台接收消息,通过 `UniMessage.export` 方法其同样支持了跨平台发送消息。
`UniMessage.export` 会通过传入的 `bot: Bot` 参数读取适配器信息,并使用对应的生成方法把通用消息转为适配器对应的消息序列:
```python
from nonebot import Bot, on_command
from nonebot_plugin_alconna import Image, UniMessage
test = on_command("test")
@test.handle()
async def handle_test(bot: Bot):
await test.send(await UniMessage(Image(path="path/to/img")).export(bot))
```
而在 `AlconnaMatcher` 下,`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`,不需要手动调用 export 方法:
```python
from arclet.alconna import Alconna, Args
from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna
test_cmd = on_alconna(Alconna("test", Args["target?", At]))
@test_cmd.handle()
async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
if target.available:
matcher.set_path_arg("target", target.result)
@test_cmd.got_path("target", prompt="请输入目标")
async def tt(target: At):
await test_cmd.send(UniMessage([target, "\ndone."]))
```
## 特殊装饰器
`nonebot_plugin_alconna` 提供 了一个 `funcommand` 装饰器, 其用于将一个接受任意参数,
返回 `str``Message``MessageSegment` 的函数转换为命令响应器。
```python
from nonebot_plugin_alconna import funcommand
@funcommand()
async def echo(msg: str):
return msg
```
其等同于
```python
from arclet.alconna import Alconna, Args
from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match
echo = on_alconna(Alconna("echo", Args["msg", str]))
@echo.handle()
async def echo_exit(msg: Match[str] = AlconnaMatch("msg")):
await echo.finish(msg.result)
```
## 特殊构造器
`nonebot_plugin_alconna` 提供了一个 `Command` 构造器,其基于 `arclet.alconna.tools` 中的 `AlconnaString`
以类似 `Koishi` 中注册命令的方式来构建一个 AlconnaMatcher
```python
from nonebot_plugin_alconna import Command, Arparma
book = (
Command("book", "测试")
.option("writer", "-w <id:int>")
.option("writer", "--anonymous", {"id": 0})
.usage("book [-w <id:int> | --anonymous]")
.shortcut("测试", {"args": ["--anonymous"]})
.build()
)
@book.handle()
async def _(arp: Arparma):
await book.send(str(arp.options))
```
甚至,你可以设置 `action` 来设定响应行为:
```python
book = (
Command("book", "测试")
.option("writer", "-w <id:int>")
.option("writer", "--anonymous", {"id": 0})
.usage("book [-w <id:int> | --anonymous]")
.shortcut("测试", {"args": ["--anonymous"]})
.action(lambda options: str(options)) # 会自动通过 bot.send 发送
.build()
)
```

View File

@@ -79,9 +79,9 @@ except Exception as e:
通常适配器需要一些配置项,例如平台连接密钥等。适配器的配置方法与[插件配置](../appendices/config#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE)类似,例如:
```python title=config.py
from pydantic import BaseModel, Extra
from pydantic import BaseModel
class Config(BaseModel, extra=Extra.ignore):
class Config(BaseModel):
xxx_id: str
xxx_token: str
```
@@ -97,6 +97,7 @@ from typing import Any
from typing_extensions import override
from nonebot.drivers import Driver
from nonebot import get_plugin_config
from nonebot.adapters import Adapter as BaseAdapter
from .config import Config
@@ -106,7 +107,7 @@ class Adapter(BaseAdapter):
def __init__(self, driver: Driver, **kwargs: Any):
super().__init__(driver, **kwargs)
# 读取适配器所需的配置项
self.adapter_config: Config = Config(**self.config.dict())
self.adapter_config: Config = get_plugin_config(Config)
@classmethod
@override
@@ -125,6 +126,7 @@ NoneBot 提供了多种 [Driver](../advanced/driver) 来帮助适配器进行网
import asyncio
from typing_extensions import override
from nonebot import get_plugin_config
from nonebot.exception import WebSocketClosed
from nonebot.drivers import Request, WebSocketClientMixin
@@ -132,7 +134,7 @@ class Adapter(BaseAdapter):
@override
def __init__(self, driver: Driver, **kwargs: Any):
super().__init__(driver, **kwargs)
self.adapter_config: Config = Config(**self.config.dict())
self.adapter_config: Config = get_plugin_config(Config)
self.task: Optional[asyncio.Task] = None # 存储 ws 任务
self.setup()
@@ -200,6 +202,7 @@ class Adapter(BaseAdapter):
##### 服务端通信方式
```python {30,38} title=adapter.py
from nonebot import get_plugin_config
from nonebot.drivers import (
Request,
ASGIMixin,
@@ -212,7 +215,7 @@ class Adapter(BaseAdapter):
@override
def __init__(self, driver: Driver, **kwargs: Any):
super().__init__(driver, **kwargs)
self.adapter_config: Config = Config(**self.config.dict())
self.adapter_config: Config = get_plugin_config(Config)
self.setup()
def setup(self) -> None:
@@ -286,6 +289,8 @@ class Adapter(BaseAdapter):
import asyncio
from typing import Any, Dict
from nonebot.compat import type_validate_python
from .bot import Bot
from .event import Event
from .log import log
@@ -301,7 +306,7 @@ class Adapter(BaseAdapter):
# 做一层异常处理,以应对平台事件数据的变更
try:
return your_event_class.parse_obj(payload)
return type_validate_python(your_event_class, payload)
except Exception as e:
# 无法正常解析为具体 Event 时,给出日志提示
log(
@@ -309,7 +314,7 @@ class Adapter(BaseAdapter):
f"Parse event error: {str(payload)}",
)
# 也可以尝试转为基础 Event 进行处理
return Event.parse_obj(payload)
return type_validate_python(Event, payload)
async def _forward(self, bot: Bot):
@@ -440,6 +445,7 @@ Event 是 NoneBot 中的事件主体对象,所有平台消息在进入处理
```python {5,8,13,18,23,28,33} title=event.py
from typing_extensions import override
from nonebot.compat import model_dump
from nonebot.adapters import Event as BaseEvent
class Event(BaseEvent):
@@ -452,7 +458,7 @@ class Event(BaseEvent):
@override
def get_event_description(self) -> str:
# 返回事件的描述,用于日志打印,请注意转义 loguru tag
return escape_tag(repr(self.dict()))
return escape_tag(repr(model_dump(self)))
@override
def get_message(self):
@@ -574,6 +580,27 @@ class Message(BaseMessage[MessageSegment]):
- [QQGuild](https://github.com/nonebot/adapter-qqguild/blob/master/nonebot/adapters/qqguild/message.py#L22-L150)
- [Telegram](https://github.com/nonebot/adapter-telegram/blob/beta/nonebot/adapters/telegram/message.py#L43-L250)
## 适配器测试
关于适配器测试相关内容在这里不再展开,开发者可以根据需要进行合适的测试。这里为开发者提供几个常见问题的解决方法:
1. 在测试中无法导入 editable 模式安装的适配器代码。在 pytest 的 `conftest.py` 内添加如下代码:
```python title=tests/conftest.py
from pathlib import Path
import nonebot.adapters
nonebot.adapters.__path__.append( # type: ignore
str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve())
)
```
2. 需要计算适配器测试覆盖率,请在 `pyproject.toml` 中添加 pytest 配置:
```toml title=pyproject.toml
[tool.pytest.ini_options]
addopts = "--cov nonebot/adapters/{adapter-name} --cov-report term-missing"
```
## 后续工作
在完成适配器代码的编写后,如果想要将适配器发布到 NoneBot 商店,我们需要将适配器发布到 PyPI 中,然后前往[商店](/store/adapters)页面,切换到适配器页签,点击**发布适配器**按钮,填写适配器相关信息并提交。

View File

@@ -56,7 +56,7 @@ options:
## 创建配置文件
配置文件用于存放 NoneBot 运行所需要的配置项,使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。配置项需符合 dotenv 格式,复杂类型数据需使用 JSON 格式填写。具体可选配置方式以及配置项详情参考[配置](../appendices/config.mdx)。
配置文件用于存放 NoneBot 运行所需要的配置项,使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。配置项需符合 dotenv 格式,复杂类型数据需使用 JSON 格式填写。具体可选配置方式以及配置项详情参考[配置](../appendices/config.mdx)。
在**项目文件夹**中创建一个 `.env` 文本文件,并写入以下内容:

View File

@@ -82,20 +82,19 @@ Message([MessageSegment.text("Hello, world!")])
#### 从字典数组构造
`Message` 对象支持 Pydantic 自定义类型构造,可以使用 Pydantic 的 `parse_obj_as` 方法进行构造。
`Message` 对象支持 Pydantic 自定义类型构造,可以使用 Pydantic 的 `TypeAdapter` 方法进行构造。
```python
from pydantic import parse_obj_as
from pydantic import TypeAdapter
from nonebot.adapters.console import Message, MessageSegment
# 由字典构造消息段
parse_obj_as(
MessageSegment, {"type": "text", "data": {"text": "text"}}
TypeAdapter(MessageSegment).validate_python(
{"type": "text", "data": {"text": "text"}}
) == MessageSegment.text("text")
# 由字典数组构造消息序列
parse_obj_as(
Message,
TypeAdapter(Message).validate_python(
[MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}],
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
```

View File

@@ -224,6 +224,12 @@ const siteConfig = {
},
scripts: [
{
type: "text/javascript",
charset: "UTF-8",
src: "https://hm.baidu.com/hm.js?875efa50097818701ee681edd63eaac6",
async: true,
},
{
type: "text/javascript",
charset: "UTF-8",

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