mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-10-07 03:07:07 +00:00
Compare commits
558 Commits
v2.0.0-rc.
...
v2.0.0rc4
Author | SHA1 | Date | |
---|---|---|---|
|
890b2ee22f | ||
|
408292d679 | ||
|
ec4761c3a9 | ||
|
0091a03653 | ||
|
e1a63f980f | ||
|
d982e14793 | ||
|
43933920ed | ||
|
fc03c58c70 | ||
|
283560daa7 | ||
|
efc4f5a0d5 | ||
|
9f707469da | ||
|
6396a7558a | ||
|
a8a76393a5 | ||
|
0d0bc656c8 | ||
|
2a2f7b6dce | ||
|
17c86f7da2 | ||
|
1213e89bf5 | ||
|
ae08568daf | ||
|
8fbc85cf50 | ||
|
315dcb329e | ||
|
438e4f57e3 | ||
|
a346efd684 | ||
|
e3151c5f5e | ||
|
47536e6554 | ||
|
79ac7f024f | ||
|
3709e0ba4f | ||
|
c8ffafc1e8 | ||
|
fedb67d4ae | ||
|
076611166a | ||
|
d5234e44f5 | ||
|
64f78c279a | ||
|
744443ab18 | ||
|
9e5cde490e | ||
|
080c0db64b | ||
|
ec41b5f57f | ||
|
c441ec7080 | ||
|
8bb92309d5 | ||
|
9ab7666b6d | ||
|
add1f1473d | ||
|
cba38c399b | ||
|
18beb63d55 | ||
|
8977be2985 | ||
|
00686380b8 | ||
|
d4da953ad8 | ||
|
e5de8c8053 | ||
|
c2a2f8d420 | ||
|
af2ea7b83a | ||
|
909a335106 | ||
|
3e92fccd4e | ||
|
9afaf3d516 | ||
|
8ca56f24e3 | ||
|
8d09dd97f5 | ||
|
78bbf9e623 | ||
|
05a6af46b9 | ||
|
243ad3f896 | ||
|
709c36bf5f | ||
|
ba808c85d5 | ||
|
c5444799f5 | ||
|
36e99bc3ea | ||
|
f65127e655 | ||
|
600c4f3268 | ||
|
53898dfb51 | ||
|
95e3650c51 | ||
|
9f1b9ce2f3 | ||
|
551963c6c3 | ||
|
d59c999554 | ||
|
8f44df371a | ||
|
7822cabe32 | ||
|
ca0b17b46a | ||
|
d1404f6004 | ||
|
a294f0fbe0 | ||
|
3cd0066715 | ||
|
faaef1a387 | ||
|
ad4b244701 | ||
|
51e7bae8f2 | ||
|
f18b6f609e | ||
|
dfbb32937e | ||
|
cf9788ec99 | ||
|
6b83d03094 | ||
|
5508c1a4ee | ||
|
3462295562 | ||
|
fee16082e0 | ||
|
926b257065 | ||
|
fca2d074e0 | ||
|
1a473f171c | ||
|
97eee5a2f7 | ||
|
8f1fbd9b36 | ||
|
856f0b981f | ||
|
f629fc9309 | ||
|
e617bf2762 | ||
|
072c2a2a41 | ||
|
3a142033a1 | ||
|
2832514f49 | ||
|
e7887056b9 | ||
|
500b59905d | ||
|
4d4074ca24 | ||
|
8391de52d9 | ||
|
dd5f3bdea1 | ||
|
c4bfe3a823 | ||
|
3ec76454e3 | ||
|
0e481d96a6 | ||
|
c2b8bbee5f | ||
|
61ad0733de | ||
|
06fa0fb860 | ||
|
e9b1692124 | ||
|
653902c6a2 | ||
|
db0fcc0ceb | ||
|
f0021af6d4 | ||
|
7e614bb2b7 | ||
|
adba6c1890 | ||
|
6116d394e5 | ||
|
31ea5fa306 | ||
|
b209b77235 | ||
|
81870e0d64 | ||
|
40bccbc585 | ||
|
46c2817bba | ||
|
a5302a1872 | ||
|
6642500c1c | ||
|
f9464171fd | ||
|
34d307b881 | ||
|
8dc36aa630 | ||
|
f324b62eb2 | ||
|
a21b511568 | ||
|
df1c13accd | ||
|
c141b3eae7 | ||
|
a2a5af9b5e | ||
|
86a4f4043e | ||
|
f3aa8c6aa5 | ||
|
be81d094b4 | ||
|
d0f832c4cd | ||
|
317a2b8c9b | ||
|
433c672130 | ||
|
f8c67ebdf6 | ||
|
2a95588421 | ||
|
a5fc40f2dc | ||
|
e4ccb683cc | ||
|
34223d6b37 | ||
|
04a7c3bc13 | ||
|
dd04190ca2 | ||
|
5fd5b2f5b3 | ||
|
e8ad79aaf3 | ||
|
ec8bc0424e | ||
|
6063714093 | ||
|
1a0976e834 | ||
|
74743e6176 | ||
|
1befd9ffc6 | ||
|
c688450690 | ||
|
d9e7986f5c | ||
|
d9bdf38a4e | ||
|
1bd1f15c49 | ||
|
b4083ff9f9 | ||
|
2e766a86c2 | ||
|
93976a4162 | ||
|
4dc92ffc0b | ||
|
5747397790 | ||
|
17fb3f92eb | ||
|
8f79ba1ccd | ||
|
e11298d15b | ||
|
728902bfcf | ||
|
97723a0838 | ||
|
e42111f31f | ||
|
8d4eb7faf8 | ||
|
9b19bf63b2 | ||
|
6685d88b44 | ||
|
694db6c278 | ||
|
1dc02bfe8e | ||
|
045ab60699 | ||
|
dc5ebd7a90 | ||
|
676e729df8 | ||
|
dda6b97ef8 | ||
|
b3d22ea8c4 | ||
|
eac72d2d48 | ||
|
955ada47ed | ||
|
4c31683231 | ||
|
a8f3d83947 | ||
|
87e1866cf4 | ||
|
0d39b788bb | ||
|
635668e6d4 | ||
|
6358d07fbd | ||
|
c03e4161c7 | ||
|
4c0d4065c5 | ||
|
720c736343 | ||
|
6d7435bf36 | ||
|
423e055ecd | ||
|
b952f325d2 | ||
|
3c3331a1ef | ||
|
4679e7d9cb | ||
|
cbcd3987d2 | ||
|
d8cc1bd644 | ||
|
09b0b2084d | ||
|
6bc2870ea5 | ||
|
f14580e688 | ||
|
5ae24313f7 | ||
|
14088c6c51 | ||
|
73126535ef | ||
|
22e27cce5f | ||
|
8abab79cfc | ||
|
6b419dc929 | ||
|
8bf8b4760b | ||
|
678c1e1532 | ||
|
325d28fbd4 | ||
|
6bff6e9ad3 | ||
|
d58c1407b5 | ||
|
e8760b6e4a | ||
|
4c92890265 | ||
|
d2f000bb16 | ||
|
b534b3b03f | ||
|
485d6e94b4 | ||
|
2f5ba409ec | ||
|
e2e9bcc260 | ||
|
6e214efde8 | ||
|
da72d20d0e | ||
|
646ab1002f | ||
|
1b4e5d05ab | ||
|
f323feac1b | ||
|
cb715c132f | ||
|
9e1b439128 | ||
|
f07e9bf699 | ||
|
92e410ce0a | ||
|
e14709c7f0 | ||
|
0533058dc2 | ||
|
896e988f0e | ||
|
b39fe9f1a9 | ||
|
02301f6098 | ||
|
c5a0f0bd6d | ||
|
f59d5f0826 | ||
|
c369475603 | ||
|
18e698f2a0 | ||
|
33bab04a5a | ||
|
3cf31998b9 | ||
|
c6d85ac9b0 | ||
|
27286fdc57 | ||
|
735cf9db6b | ||
|
d457bece3d | ||
|
37b2a100d0 | ||
|
cd4037c4fe | ||
|
13a490de0d | ||
|
cec09397cc | ||
|
a11ac82a91 | ||
|
6410af19ba | ||
|
8af08c6417 | ||
|
801d29f66c | ||
|
c518f768d1 | ||
|
27149108d9 | ||
|
5d1582566a | ||
|
eceef1ebec | ||
|
50aa8c53e0 | ||
|
558f740c13 | ||
|
8bffba7efd | ||
|
ce93ea13e7 | ||
|
174182d62a | ||
|
36f047be7f | ||
|
9d73af0513 | ||
|
ecb0d78011 | ||
|
5920efb6c5 | ||
|
5893fbe57d | ||
|
27557af636 | ||
|
b37c7995cb | ||
|
f46addbb85 | ||
|
6f57a290d7 | ||
|
ae66e45287 | ||
|
03cf7f290a | ||
|
f203aaf4ca | ||
|
9a2edbbeb1 | ||
|
bd9ca99f63 | ||
|
8be262d305 | ||
|
b92d47b362 | ||
|
bdf8cb0d57 | ||
|
0cb65214c6 | ||
|
ccc2c5676a | ||
|
6daec67ebd | ||
|
051851faed | ||
|
8d2fca3e12 | ||
|
76f37c485c | ||
|
0c7af0873f | ||
|
31fa4ec5f4 | ||
|
fda490d252 | ||
|
40e443fd1a | ||
|
4a17e581d2 | ||
|
081d212487 | ||
|
3d6774136f | ||
|
fa934a156a | ||
|
bac5356a90 | ||
|
b289065f71 | ||
|
09cf0f29ba | ||
|
244837dd7c | ||
|
a0bc113912 | ||
|
6f6a296105 | ||
|
a0d316127f | ||
|
f0c0d7788f | ||
|
3f7e2604f1 | ||
|
f43c0087f7 | ||
|
e71d841045 | ||
|
a3af8da331 | ||
|
8bdfdaef91 | ||
|
afd13ed65d | ||
|
d83751d0ca | ||
|
63dd3b8fa7 | ||
|
ae689605a5 | ||
|
bbaba1c955 | ||
|
f1ee54e5c9 | ||
|
6f8e532afe | ||
|
6f68ff61e5 | ||
|
a930fc0997 | ||
|
65da0947fe | ||
|
1b64a54421 | ||
|
d4e1bb7bf3 | ||
|
d737679ccd | ||
|
4cef5512ee | ||
|
1d5d1602f0 | ||
|
87e767fa25 | ||
|
c38437a22f | ||
|
cafb7bedb4 | ||
|
ace053f387 | ||
|
d6e176d03b | ||
|
2fca5b9664 | ||
|
cd93ace0dd | ||
|
b118cb6f22 | ||
|
a69ccb4e6c | ||
|
d5ec31d0a0 | ||
|
62560635b2 | ||
|
c00430c53f | ||
|
1dcda4bd77 | ||
|
b60035f0e6 | ||
|
8551b13eab | ||
|
b448a6e083 | ||
|
956b202087 | ||
|
b95d49cd9c | ||
|
006b9dd816 | ||
|
a9125cd696 | ||
|
ee5dcf0d42 | ||
|
f13c1cc980 | ||
|
16c0a87929 | ||
|
39d1554905 | ||
|
37067229b0 | ||
|
5ca708d3f4 | ||
|
53dded52a7 | ||
|
f8cee790e7 | ||
|
10447ff3c4 | ||
|
f08aec7894 | ||
|
69edb98835 | ||
|
c73ca2b43f | ||
|
848c6c5061 | ||
|
58f82bf881 | ||
|
9b3e670cee | ||
|
f0bebb65b4 | ||
|
bc845c94e2 | ||
|
f4668bf0bc | ||
|
3493d69fcd | ||
|
125bcb943f | ||
|
e65eb3fb18 | ||
|
8045420f97 | ||
|
03a90006e5 | ||
|
adbe341076 | ||
|
2a623b1c81 | ||
|
c91926aea6 | ||
|
63329257de | ||
|
c9dc6e648e | ||
|
78a818547e | ||
|
41eed9d0e9 | ||
|
e32019f15d | ||
|
59c033e2dd | ||
|
49cf1ec5d3 | ||
|
e5ad15d6d6 | ||
|
4cf9790a95 | ||
|
7467e66dab | ||
|
516c1c220c | ||
|
136778ae5b | ||
|
17538f7a66 | ||
|
d1da0be0da | ||
|
e92bc24631 | ||
|
73d1e5dd88 | ||
|
ea83ba78ec | ||
|
712f80a307 | ||
|
5c4ef8fc00 | ||
|
2b0973c9f5 | ||
|
767a8ecb08 | ||
|
b342940b69 | ||
|
7880f07db4 | ||
|
ac702d7eb7 | ||
|
9f3b3b2c32 | ||
|
2d08465426 | ||
|
827d8fbc0e | ||
|
0320be1947 | ||
|
aca65954bd | ||
|
1da9376fc8 | ||
|
909b811f68 | ||
|
ceecf9c692 | ||
|
3de2922773 | ||
|
a5d26b7747 | ||
|
932f50d1fb | ||
|
c69619f142 | ||
|
5a49d1e0e2 | ||
|
4f9f3f449c | ||
|
60733c97be | ||
|
7a1aa0c204 | ||
|
6d1383a10c | ||
|
86006fafdc | ||
|
44ca4f729a | ||
|
cf29209a55 | ||
|
5e78e2bb5d | ||
|
440e15e204 | ||
|
6711a84cab | ||
|
5c2d2141e3 | ||
|
8ec1552fd6 | ||
|
c1dca723ae | ||
|
b6cd0424fa | ||
|
1beee94c1d | ||
|
f2a618f663 | ||
|
c4286f1f39 | ||
|
cd9e30bd52 | ||
|
24ae0dfa15 | ||
|
19a20a3407 | ||
|
36d7b44741 | ||
|
8176cd189c | ||
|
b25de93eb1 | ||
|
79e636ad89 | ||
|
9c9973d8d8 | ||
|
04d4954d50 | ||
|
2c81dc1975 | ||
|
850096ceaa | ||
|
6bb15f6533 | ||
|
92c6a8dd6e | ||
|
723eef10bb | ||
|
06c33ad6ea | ||
|
9826bc29ca | ||
|
f35b5b57b7 | ||
|
433b50c79c | ||
|
89f3496a2a | ||
|
df7e8f6d83 | ||
|
b57f17d447 | ||
|
665de1da3e | ||
|
091232e996 | ||
|
9ee2d94f3c | ||
|
78bdfe65ba | ||
|
5006cf7be6 | ||
|
a818e0056e | ||
|
3efae8bfbc | ||
|
024d97b997 | ||
|
c90ab949d2 | ||
|
e8ffa63b78 | ||
|
6c27ec7357 | ||
|
9bf08593d7 | ||
|
b016a59a38 | ||
|
794395e737 | ||
|
a758e6f06e | ||
|
11feb2c0d0 | ||
|
59a2ed7c2e | ||
|
d83866f03b | ||
|
cb83e76e16 | ||
|
1644615462 | ||
|
89d8abf863 | ||
|
f8cf7c94ae | ||
|
bef494615f | ||
|
6e110e725e | ||
|
85390a14b6 | ||
|
276041e314 | ||
|
2922da7b2f | ||
|
c783ab5e9b | ||
|
139190bff7 | ||
|
1524434444 | ||
|
b6857d59b8 | ||
|
a7b0eb10a0 | ||
|
0eadb44e20 | ||
|
6b43209d37 | ||
|
a50990bef2 | ||
|
f1525c1ecd | ||
|
7df9756205 | ||
|
376a720881 | ||
|
c7377647fa | ||
|
cfbd6f1e4d | ||
|
cecc853f25 | ||
|
fe5f85517e | ||
|
66040a7e44 | ||
|
81d2f017f6 | ||
|
4355025f87 | ||
|
0bc8a39578 | ||
|
7308f57776 | ||
|
7de8912edb | ||
|
42a4edc3ee | ||
|
4a12429a38 | ||
|
092f6d05f5 | ||
|
2c0c05dca1 | ||
|
31bafc832f | ||
|
e1720d8ea0 | ||
|
08be5724b9 | ||
|
828714a4e3 | ||
|
a79eeb73a6 | ||
|
c1aec637d5 | ||
|
12cc08efbd | ||
|
f410af72dc | ||
|
113021cdf4 | ||
|
fcc23f98f8 | ||
|
0b693c419e | ||
|
36e5b81510 | ||
|
71f17bebaa | ||
|
2f45f25d13 | ||
|
17d52446c3 | ||
|
2304aaf22b | ||
|
a70c37de69 | ||
|
1cabc18277 | ||
|
ed1235ed11 | ||
|
9952b4e838 | ||
|
e81390a104 | ||
|
7bc22289b4 | ||
|
bfa4a079bf | ||
|
403850262b | ||
|
ed25e7aa39 | ||
|
7ab480f044 | ||
|
d7d6152094 | ||
|
2954e58c77 | ||
|
f684b96433 | ||
|
4388d1c9e6 | ||
|
876acf3e88 | ||
|
963e73f517 | ||
|
bdc9e44142 | ||
|
4f2efb7304 | ||
|
6ae891124f | ||
|
145bd4d4e1 | ||
|
ab54049909 | ||
|
7aa554f5a2 | ||
|
5566777374 | ||
|
b2594f61de | ||
|
1ad1e0606c | ||
|
583d5060db | ||
|
eaa3dbdfa8 | ||
|
03f378690a | ||
|
512c66ccc0 | ||
|
9d20c7510a | ||
|
29ad8a6686 | ||
|
db534b8824 | ||
|
67b96528af | ||
|
a5929f80f7 | ||
|
9619477a27 | ||
|
8377680fd7 | ||
|
3e3d6f91a5 | ||
|
1d3d886004 | ||
|
ebc5a3cc9e | ||
|
1092767a51 | ||
|
ab227ee64b | ||
|
00b37fb3d2 | ||
|
e6494dc98e | ||
|
c80869f952 | ||
|
2be72eac5e | ||
|
830c4f8c6a | ||
|
138fb458b7 | ||
|
2c93f82ef3 | ||
|
77aa16c2fc | ||
|
945da7151e | ||
|
41ea0df0a5 | ||
|
cec45cf89c | ||
|
2de8c66c70 | ||
|
0bcc4277e5 | ||
|
d3dd93b36c | ||
|
e9bd81d9bb | ||
|
997d4f5042 | ||
|
3bb321c519 | ||
|
fe92d29322 |
@@ -1,15 +0,0 @@
|
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/codespaces-linux/.devcontainer/base.Dockerfile
|
||||
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/universal:2-focal
|
||||
|
||||
# ** [Optional] Uncomment this section to install additional packages. **
|
||||
# USER root
|
||||
#
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
USER codespace
|
||||
|
||||
# [Required] Poetry
|
||||
RUN curl -sSL https://install.python-poetry.org | python - -y
|
||||
RUN poetry config virtualenvs.in-project true
|
@@ -1,32 +1,13 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/codespaces-linux
|
||||
{
|
||||
"name": "GitHub Codespaces (Default)",
|
||||
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
"name": "Default Linux Universal",
|
||||
"image": "mcr.microsoft.com/devcontainers/universal:2-linux",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
|
||||
},
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"postCreateCommand": "poetry config virtualenvs.in-project true && poetry install -E all && poetry run pre-commit install && yarn install",
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"go.toolsManagement.checkForUpdates": "local",
|
||||
"go.useLanguageServer": true,
|
||||
"go.gopath": "/go",
|
||||
"python.defaultInterpreterPath": "/opt/python/latest/bin/python",
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
|
||||
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
|
||||
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
|
||||
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
|
||||
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
|
||||
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
|
||||
"python.analysis.diagnosticMode": "workspace",
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"[python]": {
|
||||
@@ -50,7 +31,6 @@
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"lldb.executable": "/usr/bin/lldb",
|
||||
"files.exclude": {
|
||||
"**/__pycache__": true
|
||||
},
|
||||
@@ -59,10 +39,7 @@
|
||||
"**/__pycache__": true
|
||||
}
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.isort",
|
||||
@@ -72,27 +49,5 @@
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"remoteUser": "codespace",
|
||||
|
||||
"overrideCommand": false,
|
||||
|
||||
"mounts": [
|
||||
"source=codespaces-linux-var-lib-docker,target=/var/lib/docker,type=volume"
|
||||
],
|
||||
|
||||
"runArgs": [
|
||||
"--cap-add=SYS_PTRACE",
|
||||
"--security-opt",
|
||||
"seccomp=unconfined",
|
||||
"--privileged",
|
||||
"--init"
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// "oryx build" will automatically install your dependencies and attempt to build your project
|
||||
"postCreateCommand": "poetry install && poetry run pre-commit install && yarn install"
|
||||
}
|
||||
}
|
||||
|
5
.github/actions/build-api-doc/action.yml
vendored
5
.github/actions/build-api-doc/action.yml
vendored
@@ -5,7 +5,10 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- run: |
|
||||
poetry run nb-autodoc nonebot
|
||||
poetry run nb-autodoc nonebot \
|
||||
-s nonebot.plugins \
|
||||
-u nonebot.internal \
|
||||
-u nonebot.internal.*
|
||||
cp -r ./build/nonebot/* ./website/docs/api/
|
||||
yarn prettier
|
||||
shell: bash
|
||||
|
4
.github/actions/setup-node/action.yml
vendored
4
.github/actions/setup-node/action.yml
vendored
@@ -9,10 +9,10 @@ runs:
|
||||
node-version: "16"
|
||||
|
||||
- id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
|
19
.github/actions/setup-python/action.yml
vendored
19
.github/actions/setup-python/action.yml
vendored
@@ -10,22 +10,15 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- id: python
|
||||
uses: actions/setup-python@v2
|
||||
- name: Install poetry
|
||||
run: pipx install poetry==1.3.2
|
||||
shell: bash
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
architecture: "x64"
|
||||
|
||||
- uses: Gr1N/setup-poetry@v7
|
||||
|
||||
- id: poetry-cache
|
||||
run: echo "::set-output name=dir::$(poetry config virtualenvs.path)"
|
||||
shell: bash
|
||||
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.poetry-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-poetry-${{ steps.python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
||||
cache: "poetry"
|
||||
|
||||
- run: poetry install -E all
|
||||
shell: bash
|
||||
|
6
.github/workflows/codecov.yml
vendored
6
.github/workflows/codecov.yml
vendored
@@ -5,6 +5,10 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
paths:
|
||||
- "nonebot/**"
|
||||
- "packages/**"
|
||||
- "tests/**"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -15,7 +19,7 @@ jobs:
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
fail-fast: false
|
||||
env:
|
||||
|
37
.github/workflows/publish-bot.yml
vendored
37
.github/workflows/publish-bot.yml
vendored
@@ -5,19 +5,47 @@ on:
|
||||
types: [opened, reopened, edited]
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
plugin_test:
|
||||
runs-on: ubuntu-latest
|
||||
name: nonebot2 plugin test
|
||||
if: github.event_name != 'issue_comment' || !github.event.issue.pull_request
|
||||
permissions:
|
||||
issues: read
|
||||
outputs:
|
||||
result: ${{ steps.plugin-test.outputs.RESULT }}
|
||||
output: ${{ steps.plugin-test.outputs.OUTPUT }}
|
||||
steps:
|
||||
- name: Install Poetry
|
||||
if: ${{ !startsWith(github.event_name, 'pull_request') }}
|
||||
run: pipx install poetry
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Test Plugin
|
||||
id: plugin-test
|
||||
run: |
|
||||
curl -sSL https://github.com/nonebot/nonebot2-publish-bot/releases/latest/download/plugin_test.py -o plugin_test.py
|
||||
python plugin_test.py
|
||||
publish_bot:
|
||||
runs-on: ubuntu-latest
|
||||
name: nonebot2 publish bot
|
||||
needs: plugin_test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: NoneBot2 Publish Bot
|
||||
uses: docker://ghcr.io/nonebot/nonebot2-publish-bot:main
|
||||
uses: docker://ghcr.io/nonebot/nonebot2-publish-bot:latest
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
config: >
|
||||
@@ -27,3 +55,6 @@ jobs:
|
||||
"bot_path": "website/static/bots.json",
|
||||
"adapter_path": "website/static/adapters.json"
|
||||
}
|
||||
env:
|
||||
PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }}
|
||||
PLUGIN_TEST_OUTPUT: ${{ needs.plugin_test.outputs.output }}
|
||||
|
2
.github/workflows/website-deploy.yml
vendored
2
.github/workflows/website-deploy.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy to Netlify
|
||||
uses: nwtgck/actions-netlify@v1
|
||||
uses: nwtgck/actions-netlify@v2
|
||||
with:
|
||||
publish-dir: "./website/build"
|
||||
production-deploy: true
|
||||
|
2
.github/workflows/website-preview.yml
vendored
2
.github/workflows/website-preview.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy to Netlify
|
||||
uses: nwtgck/actions-netlify@v1
|
||||
uses: nwtgck/actions-netlify@v2
|
||||
with:
|
||||
publish-dir: "./website/build"
|
||||
production-deploy: false
|
||||
|
@@ -6,23 +6,29 @@ ci:
|
||||
autoupdate_schedule: monthly
|
||||
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
||||
repos:
|
||||
- repo: https://github.com/hadialqattan/pycln
|
||||
rev: v2.1.3
|
||||
hooks:
|
||||
- id: pycln
|
||||
args: [--config, pyproject.toml]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.10.1
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.8.0
|
||||
rev: 23.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v3.0.0-alpha.0
|
||||
rev: v3.0.0-alpha.6
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [javascript, jsx, ts, tsx, markdown, yaml]
|
||||
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/nonebot/nonemoji
|
||||
|
@@ -33,7 +33,7 @@ pre-commit install
|
||||
|
||||
### 使用 GitHub Codespaces(Dev Container)
|
||||
|
||||
使用 GitHub Codespaces 选择 `NoneBot2` 项目,然后选择 `.devcontainer/devcontainer.json` 配置即可。
|
||||
[](https://github.com/codespaces/new?hide_repo_select=true&ref=master&repo=289605524)
|
||||
|
||||
### Commit 规范
|
||||
|
||||
@@ -66,16 +66,17 @@ yarn start
|
||||
|
||||
NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量写得简单易懂。
|
||||
|
||||
以下是比较重要的排版规范。目前 NoneBot2 文档中仍有部分文档不完全遵守此规范,如果在阅读时发现欢迎提交 PR。
|
||||
以下是比较重要的编写与排版规范。目前 NoneBot2 文档中仍有部分文档不完全遵守此规范,如果在阅读时发现欢迎提交 PR。
|
||||
|
||||
1. 中文与英文、数字、半角符号之间需要有空格。例:`NoneBot2 是一个可扩展的 Python 异步机器人框架。`
|
||||
2. 若非英文整句,使用全角标点符号。例:`现在你可以看到机器人回复你:“Hello, World !”。`
|
||||
3. 直引号`「」`和弯引号`“”`都可接受,但同一份文件里应使用同种引号。
|
||||
4. **不要使用斜体**,你不需要一种与粗体不同的强调。除此之外,你也可以考虑使用 docusaurus 提供的[告示](https://docusaurus.io/zh-CN/docs/markdown-features/admonitions)功能。
|
||||
5. 文档中应以“我们”指代机器人开发者,以“机器人用户”指代机器人的使用者。
|
||||
|
||||
这是社区创始人 richardchien 的中文排版规范,可供参考:<https://stdrc.cc/style-guides/chinese>。
|
||||
以上由[社区创始人 richardchien 的中文排版规范](https://stdrc.cc/style-guides/chinese)补充修改得到。
|
||||
|
||||
如果你需要编辑器提示 Markdown 规范,可以安装 VSCode 上的 markdownlint 插件。
|
||||
如果你需要编辑器检查 Markdown 规范,可以在 VSCode 中安装 markdownlint 扩展。
|
||||
|
||||
### 参与开发
|
||||
|
||||
@@ -83,7 +84,7 @@ NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/
|
||||
|
||||
## 为社区做贡献
|
||||
|
||||
你可以在 NoneBot 商店上架自己的适配器、插件、机器人,具体步骤可参考 [发布插件](https://v2.nonebot.dev/docs/advanced/publish-plugin) 一节。
|
||||
你可以在 NoneBot 商店上架自己的适配器、插件、机器人,具体步骤可参考 [发布插件](https://v2.nonebot.dev/docs/developer/plugin-publishing) 一节。
|
||||
|
||||
我们仅对插件的兼容性进行简单测试,并会在下一个版本发布前对与该版本不兼容的插件作出处理。
|
||||
|
||||
|
52
README.md
52
README.md
@@ -71,15 +71,13 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
||||
<p align="center">
|
||||
<a href="https://v2.nonebot.dev/">文档</a>
|
||||
·
|
||||
<a href="https://v2.nonebot.dev/docs/start/installation">安装</a>
|
||||
·
|
||||
<a href="https://v2.nonebot.dev/docs/tutorial/create-project">开始使用</a>
|
||||
<a href="https://v2.nonebot.dev/docs/quick-start">快速上手</a>
|
||||
·
|
||||
<a href="#插件">文档打不开?</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://asciinema.org/a/464654">
|
||||
<a href="https://asciinema.org/a/569440">
|
||||
<img src="https://v2.nonebot.dev/img/setup.svg">
|
||||
</a>
|
||||
</p>
|
||||
@@ -92,21 +90,24 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
|
||||
|
||||
- 异步优先:基于 Python 的异步特性,即使是~~非常~~大量的消息,也能吞吐自如
|
||||
- 易于开发:配合 NB-CLI 脚手架,代码编写上手简单,没有过多的冗余代码,可以让开发者专注于业务逻辑
|
||||
- 生而可靠:100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://v2.nonebot.dev/docs/start/editor-support))
|
||||
- 生而可靠:100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://v2.nonebot.dev/docs/editor-support))
|
||||
- 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源))
|
||||
- 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
|
||||
|
||||
| 协议名称 | 状态 | 注释 |
|
||||
| :---------------------------------------------------: | :--: | :----------------------------------------------------------------: |
|
||||
| [OneBot 协议](https://onebot.dev/) | ✅ | 支持 QQ、TG、微信公众号等[平台](https://onebot.dev/ecosystem.html) |
|
||||
| [Telegram](https://core.telegram.org/bots/api) | ✅ | |
|
||||
| [飞书](https://open.feishu.cn/document/home/index) | ✅ | |
|
||||
| [GitHub](https://docs.github.com/en/developers/apps) | ✅ | GitHub APP & OAuth APP |
|
||||
| [QQ 频道](https://bot.q.qq.com/wiki/) | ✅ | 官方接口调整较多 |
|
||||
| [钉钉](https://open.dingtalk.com/document/) | 🤗 | 寻找 Maintainer |
|
||||
| Console | ✅ | 控制台交互 |
|
||||
| [开黑啦](https://developer.kookapp.cn/) | ↗️ | 由社区贡献 |
|
||||
| [Mirai](https://docs.mirai.mamoe.net/mirai-api-http/) | ↗️ | 由社区贡献 |
|
||||
| 协议名称 | 状态 | 注释 |
|
||||
| :-----------------------------------------------------------------------: | :--: | :----------------------------------------------------------------: |
|
||||
| [OneBot 协议](https://onebot.dev/) | ✅ | 支持 QQ、TG、微信公众号等[平台](https://onebot.dev/ecosystem.html) |
|
||||
| [Telegram](https://core.telegram.org/bots/api) | ✅ | |
|
||||
| [飞书](https://open.feishu.cn/document/home/index) | ✅ | |
|
||||
| [GitHub](https://docs.github.com/en/developers/apps) | ✅ | GitHub APP & OAuth APP |
|
||||
| [QQ 频道](https://bot.q.qq.com/wiki/) | ✅ | 官方接口调整较多 |
|
||||
| [钉钉](https://open.dingtalk.com/document/) | 🤗 | 寻找 Maintainer |
|
||||
| Console | ✅ | 控制台交互 |
|
||||
| [开黑啦](https://developer.kookapp.cn/) | ↗️ | 由社区贡献 |
|
||||
| [Mirai](https://docs.mirai.mamoe.net/mirai-api-http/) | ↗️ | 由社区贡献 |
|
||||
| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat) | ↗️ | 由社区贡献 |
|
||||
| [MineCraft (Spigot)](https://github.com/17TheWord/nonebot-adapter-spigot) | ↗️ | 由社区贡献 |
|
||||
| [BiliBili Live](https://github.com/wwweww/adapter-bilibili) | ↗️ | 由社区贡献 |
|
||||
|
||||
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
|
||||
|
||||
@@ -134,12 +135,17 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
|
||||
|
||||
懒得看文档?下面是快速安装指南:
|
||||
|
||||
1. (**强烈建议**)使用你喜欢的 Python 环境管理工具创建新的虚拟环境。
|
||||
|
||||
2. 使用 `pip` (或其他) 安装 NoneBot 脚手架。
|
||||
1. 安装 [pipx](https://pypa.github.io/pipx/)
|
||||
|
||||
```bash
|
||||
pip install nb-cli
|
||||
python -m pip install --user pipx
|
||||
python -m pipx ensurepath
|
||||
```
|
||||
|
||||
2. 安装脚手架
|
||||
|
||||
```bash
|
||||
pipx install nb-cli
|
||||
```
|
||||
|
||||
3. 使用脚手架创建项目
|
||||
@@ -148,6 +154,12 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
|
||||
nb create
|
||||
```
|
||||
|
||||
4. 运行项目
|
||||
|
||||
```bash
|
||||
nb run
|
||||
```
|
||||
|
||||
## 社区资源
|
||||
|
||||
### 常见问题
|
||||
|
@@ -37,26 +37,25 @@ FrontMatter:
|
||||
description: nonebot 模块
|
||||
"""
|
||||
|
||||
import importlib
|
||||
from typing import Any, Dict, Type, Optional
|
||||
import os
|
||||
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.log import logger
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.utils import escape_tag
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.log import logger as logger
|
||||
from nonebot.adapters import Bot, Adapter
|
||||
from nonebot.utils import escape_tag, resolve_dot_notation
|
||||
from nonebot.drivers import Driver, ReverseDriver, combine_driver
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
|
||||
_dist: pkg_resources.Distribution = pkg_resources.get_distribution("nonebot2")
|
||||
__version__ = _dist.version
|
||||
VERSION = _dist.parsed_version
|
||||
__version__ = version("nonebot2")
|
||||
except Exception: # pragma: no cover
|
||||
__version__ = None
|
||||
VERSION = None
|
||||
|
||||
A = TypeVar("A", bound=Adapter)
|
||||
|
||||
_driver: Optional[Driver] = None
|
||||
|
||||
@@ -82,6 +81,56 @@ def get_driver() -> Driver:
|
||||
return _driver
|
||||
|
||||
|
||||
@overload
|
||||
def get_adapter(name: str) -> Adapter:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def get_adapter(name: Type[A]) -> A:
|
||||
...
|
||||
|
||||
|
||||
def get_adapter(name: Union[str, Type[Adapter]]) -> Adapter:
|
||||
"""获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
||||
|
||||
返回:
|
||||
指定名称或类型的 {ref}`nonebot.adapters.Adapter` 对象
|
||||
|
||||
异常:
|
||||
ValueError: 指定的 {ref}`nonebot.adapters.Adapter` 未注册
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
用法:
|
||||
```python
|
||||
from nonebot.adapters.console import Adapter
|
||||
adapter = nonebot.get_adapter(Adapter)
|
||||
```
|
||||
"""
|
||||
adapters = get_adapters()
|
||||
target = name if isinstance(name, str) else name.get_name()
|
||||
if target not in adapters:
|
||||
raise ValueError(f"Adapter {target} not registered.")
|
||||
return adapters[target]
|
||||
|
||||
|
||||
def get_adapters() -> Dict[str, Adapter]:
|
||||
"""获取所有已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
||||
|
||||
返回:
|
||||
所有 {ref}`nonebot.adapters.Adapter` 实例字典
|
||||
|
||||
异常:
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
用法:
|
||||
```python
|
||||
adapters = nonebot.get_adapters()
|
||||
```
|
||||
"""
|
||||
return get_driver()._adapters.copy()
|
||||
|
||||
|
||||
def get_app() -> Any:
|
||||
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应的 Server App 对象。
|
||||
|
||||
@@ -177,31 +226,16 @@ def get_bots() -> Dict[str, Bot]:
|
||||
return get_driver().bots
|
||||
|
||||
|
||||
def _resolve_dot_notation(
|
||||
obj_str: str, default_attr: str, default_prefix: Optional[str] = None
|
||||
) -> Any:
|
||||
modulename, _, cls = obj_str.partition(":")
|
||||
if default_prefix is not None and modulename.startswith("~"):
|
||||
modulename = default_prefix + modulename[1:]
|
||||
module = importlib.import_module(modulename)
|
||||
if not cls:
|
||||
return getattr(module, default_attr)
|
||||
instance = module
|
||||
for attr_str in cls.split("."):
|
||||
instance = getattr(instance, attr_str)
|
||||
return instance
|
||||
|
||||
|
||||
def _resolve_combine_expr(obj_str: str) -> Type[Driver]:
|
||||
drivers = obj_str.split("+")
|
||||
DriverClass = _resolve_dot_notation(
|
||||
DriverClass = resolve_dot_notation(
|
||||
drivers[0], "Driver", default_prefix="nonebot.drivers."
|
||||
)
|
||||
if len(drivers) == 1:
|
||||
logger.trace(f"Detected driver {DriverClass} with no mixins.")
|
||||
return DriverClass
|
||||
mixins = [
|
||||
_resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.")
|
||||
resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.")
|
||||
for mixin in drivers[1:]
|
||||
]
|
||||
logger.trace(f"Detected driver {DriverClass} with mixins {mixins}.")
|
||||
@@ -217,7 +251,7 @@ def _log_patcher(record: "loguru.Record"):
|
||||
)
|
||||
|
||||
|
||||
def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
|
||||
def init(*, _env_file: Optional[DotenvType] = None, **kwargs: Any) -> None:
|
||||
"""初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。
|
||||
|
||||
NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。
|
||||
@@ -237,10 +271,12 @@ def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
|
||||
if not _driver:
|
||||
logger.success("NoneBot is initializing...")
|
||||
env = Env()
|
||||
_env_file = _env_file or f".env.{env.environment}"
|
||||
config = Config(
|
||||
**kwargs,
|
||||
_common_config=env.dict(),
|
||||
_env_file=_env_file or f".env.{env.environment}",
|
||||
_env_file=(".env", _env_file)
|
||||
if isinstance(_env_file, (str, os.PathLike))
|
||||
else _env_file,
|
||||
)
|
||||
|
||||
logger.configure(
|
||||
|
@@ -7,21 +7,6 @@ FrontMatter:
|
||||
description: nonebot.adapters 模块
|
||||
"""
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
|
||||
pkg_resources.declare_namespace(__name__)
|
||||
del pkg_resources
|
||||
except ImportError:
|
||||
import pkgutil
|
||||
|
||||
__path__: Iterable[str] = pkgutil.extend_path(__path__, __name__) # type: ignore
|
||||
del pkgutil
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from nonebot.internal.adapter import Bot as Bot
|
||||
from nonebot.internal.adapter import Event as Event
|
||||
from nonebot.internal.adapter import Adapter as Adapter
|
||||
|
@@ -9,19 +9,18 @@ FrontMatter:
|
||||
description: nonebot.config 模块
|
||||
"""
|
||||
import os
|
||||
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 import BaseSettings, IPvAnyAddress
|
||||
from pydantic.utils import deep_update
|
||||
from pydantic import Extra, Field, BaseSettings, IPvAnyAddress
|
||||
from pydantic.env_settings import (
|
||||
DotenvType,
|
||||
SettingsError,
|
||||
EnvSettingsSource,
|
||||
InitSettingsSource,
|
||||
SettingsSourceCallable,
|
||||
read_env_file,
|
||||
env_file_sentinel,
|
||||
)
|
||||
|
||||
from nonebot.log import logger
|
||||
@@ -32,33 +31,15 @@ class CustomEnvSettings(EnvSettingsSource):
|
||||
"""
|
||||
Build environment variables suitable for passing to the Model.
|
||||
"""
|
||||
d: Dict[str, Optional[str]] = {}
|
||||
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: Dict[str, Optional[str]] = {}
|
||||
env_file = (
|
||||
self.env_file
|
||||
if self.env_file != env_file_sentinel
|
||||
else settings.__config__.env_file
|
||||
)
|
||||
env_file_encoding = (
|
||||
self.env_file_encoding
|
||||
if self.env_file_encoding is not None
|
||||
else settings.__config__.env_file_encoding
|
||||
)
|
||||
if env_file is not None:
|
||||
env_path = Path(env_file)
|
||||
if env_path.is_file():
|
||||
env_file_vars = read_env_file(
|
||||
env_path,
|
||||
encoding=env_file_encoding, # type: ignore
|
||||
case_sensitive=settings.__config__.case_sensitive,
|
||||
)
|
||||
env_vars = {**env_file_vars, **env_vars}
|
||||
env_file_vars = self._read_env_files(settings.__config__.case_sensitive)
|
||||
env_vars = {**env_file_vars, **env_vars}
|
||||
|
||||
for field in settings.__fields__.values():
|
||||
env_val: Optional[str] = None
|
||||
@@ -69,31 +50,56 @@ class CustomEnvSettings(EnvSettingsSource):
|
||||
if env_val is not None:
|
||||
break
|
||||
|
||||
if env_val is None:
|
||||
continue
|
||||
|
||||
if field.is_complex():
|
||||
try:
|
||||
env_val = settings.__config__.json_loads(env_val)
|
||||
except ValueError as e: # pragma: no cover
|
||||
raise SettingsError(
|
||||
f'error parsing JSON for "{env_name}"' # type: ignore
|
||||
) from e
|
||||
d[field.alias] = env_val
|
||||
|
||||
if env_file_vars:
|
||||
for env_name in env_file_vars.keys():
|
||||
env_val = env_vars[env_name]
|
||||
if env_val and (val_striped := env_val.strip()):
|
||||
is_complex, allow_parse_failure = self.field_is_complex(field)
|
||||
if is_complex:
|
||||
if env_val is None:
|
||||
if env_val_built := self.explode_env_vars(field, env_vars):
|
||||
d[field.alias] = env_val_built
|
||||
else:
|
||||
# field is complex and there's a value, decode that as JSON, then add explode_env_vars
|
||||
try:
|
||||
env_val = settings.__config__.json_loads(val_striped)
|
||||
env_val = settings.__config__.parse_env_var(field.name, env_val)
|
||||
except ValueError as e:
|
||||
logger.trace(
|
||||
"Error while parsing JSON for "
|
||||
f"{env_name!r}={val_striped!r}. "
|
||||
"Assumed as string."
|
||||
)
|
||||
if not allow_parse_failure:
|
||||
raise SettingsError(
|
||||
f'error parsing env var "{env_name}"' # type: ignore
|
||||
) from e
|
||||
|
||||
if isinstance(env_val, dict):
|
||||
d[field.alias] = deep_update(
|
||||
env_val, self.explode_env_vars(field, env_vars)
|
||||
)
|
||||
else:
|
||||
d[field.alias] = env_val
|
||||
elif env_val is not None:
|
||||
# simplest case, field is not complex, we only need to add the value if it was found
|
||||
d[field.alias] = env_val
|
||||
|
||||
# remain user custom config
|
||||
for env_name in env_file_vars:
|
||||
env_val = env_vars[env_name]
|
||||
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)
|
||||
except ValueError as e:
|
||||
logger.trace(
|
||||
"Error while parsing JSON for "
|
||||
f"{env_name!r}={val_striped!r}. "
|
||||
"Assumed as string."
|
||||
)
|
||||
|
||||
# explode value when it's a nested dict
|
||||
env_name, *nested_keys = env_name.split(self.env_nested_delimiter)
|
||||
if nested_keys and (env_name not in d or isinstance(d[env_name], dict)):
|
||||
result = {}
|
||||
*keys, last_key = nested_keys
|
||||
_tmp = result
|
||||
for key in keys:
|
||||
_tmp = _tmp.setdefault(key, {})
|
||||
_tmp[last_key] = env_val
|
||||
d[env_name] = deep_update(d.get(env_name, {}), result)
|
||||
elif not nested_keys:
|
||||
d[env_name] = env_val
|
||||
|
||||
return d
|
||||
@@ -106,6 +112,9 @@ class BaseConfig(BaseSettings):
|
||||
return self.__dict__.get(name)
|
||||
|
||||
class Config:
|
||||
extra = Extra.allow
|
||||
env_nested_delimiter = "__"
|
||||
|
||||
@classmethod
|
||||
def customise_sources(
|
||||
cls,
|
||||
@@ -117,7 +126,10 @@ class BaseConfig(BaseSettings):
|
||||
return (
|
||||
init_settings,
|
||||
CustomEnvSettings(
|
||||
env_settings.env_file, env_settings.env_file_encoding
|
||||
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,
|
||||
@@ -137,7 +149,6 @@ class Env(BaseConfig):
|
||||
"""
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
@@ -147,11 +158,10 @@ class Config(BaseConfig):
|
||||
除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。
|
||||
这些配置将会在 json 反序列化后一起带入 `Config` 类中。
|
||||
|
||||
配置方法参考: [配置](https://v2.nonebot.dev/docs/tutorial/configuration)
|
||||
配置方法参考: [配置](https://v2.nonebot.dev/docs/appendices/config)
|
||||
"""
|
||||
|
||||
_env_file: str = ".env"
|
||||
_common_config: Dict[str, Any] = {}
|
||||
_env_file: DotenvType = ".env", ".env.prod"
|
||||
|
||||
# nonebot configs
|
||||
driver: str = "~fastapi"
|
||||
@@ -163,7 +173,7 @@ class Config(BaseConfig):
|
||||
"""
|
||||
host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore
|
||||
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。"""
|
||||
port: int = 8080
|
||||
port: int = Field(default=8080, ge=1, le=65535)
|
||||
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。"""
|
||||
log_level: Union[int, str] = "INFO"
|
||||
"""NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称
|
||||
@@ -231,8 +241,7 @@ class Config(BaseConfig):
|
||||
# or from env file using json loads
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
env_file = ".env.prod"
|
||||
env_file = ".env", ".env.prod"
|
||||
|
||||
|
||||
__autodoc__ = {
|
||||
|
@@ -4,6 +4,8 @@ FrontMatter:
|
||||
sidebar_position: 9
|
||||
description: nonebot.consts 模块
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
# used by Matcher
|
||||
@@ -30,6 +32,8 @@ CMD_ARG_KEY: Literal["command_arg"] = "command_arg"
|
||||
"""命令参数存储 key"""
|
||||
CMD_START_KEY: Literal["command_start"] = "command_start"
|
||||
"""命令开头存储 key"""
|
||||
CMD_WHITESPACE_KEY: Literal["command_whitespace"] = "command_whitespace"
|
||||
"""命令与参数间空白符存储 key"""
|
||||
|
||||
SHELL_ARGS: Literal["_args"] = "_args"
|
||||
"""shell 命令 parse 后参数字典存储 key"""
|
||||
@@ -38,7 +42,19 @@ SHELL_ARGV: Literal["_argv"] = "_argv"
|
||||
|
||||
REGEX_MATCHED: Literal["_matched"] = "_matched"
|
||||
"""正则匹配结果存储 key"""
|
||||
REGEX_STR: Literal["_matched_str"] = "_matched_str"
|
||||
"""正则匹配文本存储 key"""
|
||||
REGEX_GROUP: Literal["_matched_groups"] = "_matched_groups"
|
||||
"""正则匹配 group 元组存储 key"""
|
||||
REGEX_DICT: Literal["_matched_dict"] = "_matched_dict"
|
||||
"""正则匹配 group 字典存储 key"""
|
||||
STARTSWITH_KEY: Literal["_startswith"] = "_startswith"
|
||||
"""响应触发前缀 key"""
|
||||
ENDSWITH_KEY: Literal["_endswith"] = "_endswith"
|
||||
"""响应触发后缀 key"""
|
||||
FULLMATCH_KEY: Literal["_fullmatch"] = "_fullmatch"
|
||||
"""响应触发完整消息 key"""
|
||||
KEYWORD_KEY: Literal["_keyword"] = "_keyword"
|
||||
"""响应触发关键字 key"""
|
||||
|
||||
WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt")
|
||||
|
@@ -4,11 +4,11 @@ FrontMatter:
|
||||
description: nonebot.dependencies.utils 模块
|
||||
"""
|
||||
import inspect
|
||||
from typing import Any, Dict, TypeVar, Callable
|
||||
from typing import Any, Dict, TypeVar, Callable, ForwardRef
|
||||
|
||||
from loguru import logger
|
||||
from pydantic.fields import ModelField
|
||||
from pydantic.typing import ForwardRef, evaluate_forwardref
|
||||
from pydantic.typing import evaluate_forwardref
|
||||
|
||||
from nonebot.exception import TypeMisMatch
|
||||
|
||||
|
@@ -1,152 +0,0 @@
|
||||
import signal
|
||||
import asyncio
|
||||
import threading
|
||||
from typing import Set, Union, Callable, Awaitable, cast
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.drivers import Driver
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
|
||||
HOOK_FUNC = Union[Callable[[], None], Callable[[], Awaitable[None]]]
|
||||
HANDLED_SIGNALS = (
|
||||
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
||||
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
|
||||
)
|
||||
|
||||
|
||||
class BlockDriver(Driver):
|
||||
def __init__(self, env: Env, config: Config):
|
||||
super().__init__(env, config)
|
||||
self.startup_funcs: Set[HOOK_FUNC] = set()
|
||||
self.shutdown_funcs: Set[HOOK_FUNC] = set()
|
||||
self.should_exit: asyncio.Event = asyncio.Event()
|
||||
self.force_exit: bool = False
|
||||
|
||||
@property
|
||||
@overrides(Driver)
|
||||
def type(self) -> str:
|
||||
"""驱动名称: `block_driver`"""
|
||||
return "block_driver"
|
||||
|
||||
@property
|
||||
@overrides(Driver)
|
||||
def logger(self):
|
||||
"""block driver 使用的 logger"""
|
||||
return logger
|
||||
|
||||
@overrides(Driver)
|
||||
def on_startup(self, func: HOOK_FUNC) -> HOOK_FUNC:
|
||||
"""
|
||||
注册一个启动时执行的函数
|
||||
"""
|
||||
self.startup_funcs.add(func)
|
||||
return func
|
||||
|
||||
@overrides(Driver)
|
||||
def on_shutdown(self, func: HOOK_FUNC) -> HOOK_FUNC:
|
||||
"""
|
||||
注册一个停止时执行的函数
|
||||
"""
|
||||
self.shutdown_funcs.add(func)
|
||||
return func
|
||||
|
||||
@overrides(Driver)
|
||||
def run(self, *args, **kwargs):
|
||||
"""启动 block driver"""
|
||||
super().run(*args, **kwargs)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self.serve())
|
||||
|
||||
async def serve(self):
|
||||
self.install_signal_handlers()
|
||||
await self.startup()
|
||||
if self.should_exit.is_set():
|
||||
return
|
||||
await self.main_loop()
|
||||
await self.shutdown()
|
||||
|
||||
async def startup(self):
|
||||
# run startup
|
||||
cors = [
|
||||
cast(Callable[..., Awaitable[None]], startup)()
|
||||
if is_coroutine_callable(startup)
|
||||
else run_sync(startup)()
|
||||
for startup in self.startup_funcs
|
||||
]
|
||||
if cors:
|
||||
try:
|
||||
await asyncio.gather(*cors)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running startup function. "
|
||||
"Ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
logger.info("Application startup completed.")
|
||||
|
||||
async def main_loop(self):
|
||||
await self.should_exit.wait()
|
||||
|
||||
async def shutdown(self):
|
||||
logger.info("Shutting down")
|
||||
|
||||
logger.info("Waiting for application shutdown.")
|
||||
# run shutdown
|
||||
cors = [
|
||||
cast(Callable[..., Awaitable[None]], shutdown)()
|
||||
if is_coroutine_callable(shutdown)
|
||||
else run_sync(shutdown)()
|
||||
for shutdown in self.shutdown_funcs
|
||||
]
|
||||
if cors:
|
||||
try:
|
||||
await asyncio.gather(*cors)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running shutdown function. "
|
||||
"Ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
for task in asyncio.all_tasks():
|
||||
if task is not asyncio.current_task() and not task.done():
|
||||
task.cancel()
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
if tasks and not self.force_exit:
|
||||
logger.info("Waiting for tasks to finish. (CTRL+C to force quit)")
|
||||
while tasks and not self.force_exit:
|
||||
await asyncio.sleep(0.1)
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
logger.info("Application shutdown complete.")
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.stop()
|
||||
|
||||
def install_signal_handlers(self) -> None:
|
||||
if threading.current_thread() is not threading.main_thread():
|
||||
# Signals can only be listened to from the main thread.
|
||||
return
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
try:
|
||||
for sig in HANDLED_SIGNALS:
|
||||
loop.add_signal_handler(sig, self.handle_exit, sig, None)
|
||||
except NotImplementedError:
|
||||
# Windows
|
||||
for sig in HANDLED_SIGNALS:
|
||||
signal.signal(sig, self.handle_exit)
|
||||
|
||||
def handle_exit(self, sig, frame):
|
||||
if self.should_exit.is_set():
|
||||
self.force_exit = True
|
||||
else:
|
||||
self.should_exit.set()
|
45
nonebot/drivers/_lifespan.py
Normal file
45
nonebot/drivers/_lifespan.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from typing import Any, List, Union, Callable, Awaitable, cast
|
||||
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
|
||||
SYNC_LIFESPAN_FUNC = Callable[[], Any]
|
||||
ASYNC_LIFESPAN_FUNC = Callable[[], Awaitable[Any]]
|
||||
LIFESPAN_FUNC = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
|
||||
|
||||
|
||||
class Lifespan:
|
||||
def __init__(self) -> None:
|
||||
self._startup_funcs: List[LIFESPAN_FUNC] = []
|
||||
self._shutdown_funcs: List[LIFESPAN_FUNC] = []
|
||||
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
self._startup_funcs.append(func)
|
||||
return func
|
||||
|
||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
self._shutdown_funcs.append(func)
|
||||
return func
|
||||
|
||||
@staticmethod
|
||||
async def _run_lifespan_func(
|
||||
funcs: List[LIFESPAN_FUNC],
|
||||
) -> None:
|
||||
for func in funcs:
|
||||
if is_coroutine_callable(func):
|
||||
await cast(ASYNC_LIFESPAN_FUNC, func)()
|
||||
else:
|
||||
await run_sync(cast(SYNC_LIFESPAN_FUNC, func))()
|
||||
|
||||
async def startup(self) -> None:
|
||||
if self._startup_funcs:
|
||||
await self._run_lifespan_func(self._startup_funcs)
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
if self._shutdown_funcs:
|
||||
await self._run_lifespan_func(self._shutdown_funcs)
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
await self.startup()
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
await self.shutdown()
|
@@ -21,16 +21,16 @@ from contextlib import asynccontextmanager
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.drivers import Request, Response
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers._block_driver import BlockDriver
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import HTTPVersion, ForwardMixin, ForwardDriver, combine_driver
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
except ImportError: # pragma: no cover
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install aiohttp first to use this driver. `pip install nonebot2[aiohttp]`"
|
||||
) from None
|
||||
) from e
|
||||
|
||||
|
||||
class Mixin(ForwardMixin):
|
||||
@@ -56,7 +56,13 @@ class Mixin(ForwardMixin):
|
||||
files = aiohttp.FormData()
|
||||
for name, file in setup.files:
|
||||
files.add_field(name, file[1], content_type=file[2], filename=file[0])
|
||||
async with aiohttp.ClientSession(version=version, trust_env=True) as session:
|
||||
|
||||
cookies = {
|
||||
cookie.name: cookie.value for cookie in setup.cookies if cookie.value
|
||||
}
|
||||
async with aiohttp.ClientSession(
|
||||
cookies=cookies, version=version, trust_env=True
|
||||
) as session:
|
||||
async with session.request(
|
||||
setup.method,
|
||||
setup.url,
|
||||
@@ -66,13 +72,12 @@ class Mixin(ForwardMixin):
|
||||
timeout=timeout,
|
||||
proxy=setup.proxy,
|
||||
) as response:
|
||||
res = Response(
|
||||
return Response(
|
||||
response.status,
|
||||
headers=response.headers.copy(),
|
||||
content=await response.read(),
|
||||
request=setup,
|
||||
)
|
||||
return res
|
||||
|
||||
@overrides(ForwardMixin)
|
||||
@asynccontextmanager
|
||||
@@ -92,8 +97,7 @@ class Mixin(ForwardMixin):
|
||||
headers=setup.headers,
|
||||
proxy=setup.proxy,
|
||||
) as ws:
|
||||
websocket = WebSocket(request=setup, session=session, websocket=ws)
|
||||
yield websocket
|
||||
yield WebSocket(request=setup, session=session, websocket=ws)
|
||||
|
||||
|
||||
class WebSocket(BaseWebSocket):
|
||||
@@ -166,5 +170,5 @@ class WebSocket(BaseWebSocket):
|
||||
await self.websocket.send_bytes(data)
|
||||
|
||||
|
||||
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore
|
||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
||||
"""AIOHTTP Driver"""
|
||||
|
@@ -1,5 +1,11 @@
|
||||
"""[FastAPI](https://fastapi.tiangolo.com/) 驱动适配
|
||||
|
||||
```bash
|
||||
nb driver install fastapi
|
||||
# 或者
|
||||
pip install nonebot2[fastapi]
|
||||
```
|
||||
|
||||
:::tip 提示
|
||||
本驱动仅支持服务端连接
|
||||
:::
|
||||
@@ -13,13 +19,9 @@ FrontMatter:
|
||||
import logging
|
||||
import contextlib
|
||||
from functools import wraps
|
||||
from typing import Any, List, Tuple, Union, Callable, Optional
|
||||
from typing import Any, Dict, List, Tuple, Union, Optional
|
||||
|
||||
import uvicorn
|
||||
from pydantic import BaseSettings
|
||||
from fastapi.responses import Response
|
||||
from fastapi import FastAPI, Request, UploadFile, status
|
||||
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
|
||||
|
||||
from nonebot.config import Env
|
||||
from nonebot.typing import overrides
|
||||
@@ -30,6 +32,18 @@ from nonebot.drivers import Request as BaseRequest
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
|
||||
|
||||
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
||||
|
||||
try:
|
||||
import uvicorn
|
||||
from fastapi.responses import Response
|
||||
from fastapi import FastAPI, Request, UploadFile, status
|
||||
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install FastAPI by using `pip install nonebot2[fastapi]`"
|
||||
) from e
|
||||
|
||||
|
||||
def catch_closed(func):
|
||||
@wraps(func)
|
||||
@@ -65,6 +79,8 @@ class Config(BaseSettings):
|
||||
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
fastapi_reload_excludes: Optional[List[str]] = None
|
||||
"""不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
fastapi_extra: Dict[str, Any] = {}
|
||||
"""传递给 `FastAPI` 的其他参数。"""
|
||||
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
@@ -78,10 +94,14 @@ class Driver(ReverseDriver):
|
||||
|
||||
self.fastapi_config: Config = Config(**config.dict())
|
||||
|
||||
self._lifespan = Lifespan()
|
||||
|
||||
self._server_app = FastAPI(
|
||||
lifespan=self._lifespan_manager,
|
||||
openapi_url=self.fastapi_config.fastapi_openapi_url,
|
||||
docs_url=self.fastapi_config.fastapi_docs_url,
|
||||
redoc_url=self.fastapi_config.fastapi_redoc_url,
|
||||
**self.fastapi_config.fastapi_extra,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -133,14 +153,20 @@ class Driver(ReverseDriver):
|
||||
)
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
def on_startup(self, func: Callable) -> Callable:
|
||||
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#startup-event>`_"""
|
||||
return self.server_app.on_event("startup")(func)
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
return self._lifespan.on_startup(func)
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
def on_shutdown(self, func: Callable) -> Callable:
|
||||
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#shutdown-event>`_"""
|
||||
return self.server_app.on_event("shutdown")(func)
|
||||
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()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await self._lifespan.shutdown()
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
def run(
|
||||
@@ -262,7 +288,7 @@ class FastAPIWebSocket(BaseWebSocket):
|
||||
async def close(
|
||||
self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = ""
|
||||
) -> None:
|
||||
await self.websocket.close(code)
|
||||
await self.websocket.close(code, reason)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
async def receive(self) -> Union[str, bytes]:
|
||||
|
@@ -18,7 +18,7 @@ from typing import Type, AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.drivers._block_driver import BlockDriver
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.drivers import (
|
||||
Request,
|
||||
Response,
|
||||
@@ -31,10 +31,10 @@ from nonebot.drivers import (
|
||||
|
||||
try:
|
||||
import httpx
|
||||
except ImportError: # pragma: no cover
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install httpx by using `pip install nonebot2[httpx]`"
|
||||
) from None
|
||||
) from e
|
||||
|
||||
|
||||
class Mixin(ForwardMixin):
|
||||
@@ -48,17 +48,18 @@ class Mixin(ForwardMixin):
|
||||
@overrides(ForwardMixin)
|
||||
async def request(self, setup: Request) -> Response:
|
||||
async with httpx.AsyncClient(
|
||||
cookies=setup.cookies.jar,
|
||||
http2=setup.version == HTTPVersion.H2,
|
||||
proxies=setup.proxy, # type: ignore
|
||||
proxies=setup.proxy,
|
||||
follow_redirects=True,
|
||||
) as client:
|
||||
response = await client.request(
|
||||
setup.method,
|
||||
str(setup.url),
|
||||
content=setup.content, # type: ignore
|
||||
data=setup.data, # type: ignore
|
||||
content=setup.content,
|
||||
data=setup.data,
|
||||
json=setup.json,
|
||||
files=setup.files, # type: ignore
|
||||
files=setup.files,
|
||||
headers=tuple(setup.headers.items()),
|
||||
timeout=setup.timeout,
|
||||
)
|
||||
@@ -76,5 +77,5 @@ class Mixin(ForwardMixin):
|
||||
yield ws
|
||||
|
||||
|
||||
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore
|
||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
||||
"""HTTPX Driver"""
|
||||
|
152
nonebot/drivers/none.py
Normal file
152
nonebot/drivers/none.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""None 驱动适配
|
||||
|
||||
:::tip 提示
|
||||
本驱动不支持任何服务器或客户端连接
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 6
|
||||
description: nonebot.drivers.none 模块
|
||||
"""
|
||||
|
||||
|
||||
import signal
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.consts import WINDOWS
|
||||
from nonebot.typing import overrides
|
||||
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>`.
|
||||
)
|
||||
if WINDOWS: # pragma: py-win32
|
||||
HANDLED_SIGNALS += (signal.SIGBREAK,) # Windows signal 21. Sent by Ctrl+Break.
|
||||
|
||||
|
||||
class Driver(BaseDriver):
|
||||
"""None 驱动框架"""
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
@overrides(BaseDriver)
|
||||
def type(self) -> str:
|
||||
"""驱动名称: `none`"""
|
||||
return "none"
|
||||
|
||||
@property
|
||||
@overrides(BaseDriver)
|
||||
def logger(self):
|
||||
"""none driver 使用的 logger"""
|
||||
return logger
|
||||
|
||||
@overrides(BaseDriver)
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""
|
||||
注册一个启动时执行的函数
|
||||
"""
|
||||
return self._lifespan.on_startup(func)
|
||||
|
||||
@overrides(BaseDriver)
|
||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""
|
||||
注册一个停止时执行的函数
|
||||
"""
|
||||
return self._lifespan.on_shutdown(func)
|
||||
|
||||
@overrides(BaseDriver)
|
||||
def run(self, *args, **kwargs):
|
||||
"""启动 none driver"""
|
||||
super().run(*args, **kwargs)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self._serve())
|
||||
|
||||
async def _serve(self):
|
||||
self._install_signal_handlers()
|
||||
await self._startup()
|
||||
if self.should_exit.is_set():
|
||||
return
|
||||
await self._main_loop()
|
||||
await self._shutdown()
|
||||
|
||||
async def _startup(self):
|
||||
try:
|
||||
await self._lifespan.startup()
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running startup function. "
|
||||
"Ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
logger.info("Application startup completed.")
|
||||
|
||||
async def _main_loop(self):
|
||||
await self.should_exit.wait()
|
||||
|
||||
async def _shutdown(self):
|
||||
logger.info("Shutting down")
|
||||
|
||||
logger.info("Waiting for application shutdown.")
|
||||
|
||||
try:
|
||||
await self._lifespan.shutdown()
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running shutdown function. "
|
||||
"Ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
for task in asyncio.all_tasks():
|
||||
if task is not asyncio.current_task() and not task.done():
|
||||
task.cancel()
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
if tasks and not self.force_exit:
|
||||
logger.info("Waiting for tasks to finish. (CTRL+C to force quit)")
|
||||
while tasks and not self.force_exit:
|
||||
await asyncio.sleep(0.1)
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
logger.info("Application shutdown complete.")
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.stop()
|
||||
|
||||
def _install_signal_handlers(self) -> None:
|
||||
if threading.current_thread() is not threading.main_thread():
|
||||
# Signals can only be listened to from the main thread.
|
||||
return
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
try:
|
||||
for sig in HANDLED_SIGNALS:
|
||||
loop.add_signal_handler(sig, self._handle_exit, sig, None)
|
||||
except NotImplementedError:
|
||||
# Windows
|
||||
for sig in HANDLED_SIGNALS:
|
||||
signal.signal(sig, self._handle_exit)
|
||||
|
||||
def _handle_exit(self, sig, frame):
|
||||
if self.should_exit.is_set():
|
||||
self.force_exit = True
|
||||
else:
|
||||
self.should_exit.set()
|
@@ -17,9 +17,8 @@ FrontMatter:
|
||||
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
from typing import List, Tuple, Union, TypeVar, Callable, Optional, Coroutine
|
||||
from typing import Any, Dict, List, Tuple, Union, TypeVar, Callable, Optional, Coroutine
|
||||
|
||||
import uvicorn
|
||||
from pydantic import BaseSettings
|
||||
|
||||
from nonebot.config import Env
|
||||
@@ -32,15 +31,16 @@ from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
|
||||
|
||||
try:
|
||||
import uvicorn
|
||||
from quart import request as _request
|
||||
from quart import websocket as _websocket
|
||||
from quart import Quart, Request, Response
|
||||
from quart.datastructures import FileStorage
|
||||
from quart import Websocket as QuartWebSocket
|
||||
except ImportError: # pragma: no cover
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install Quart by using `pip install nonebot2[quart]`"
|
||||
) from None
|
||||
) from e
|
||||
|
||||
_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine])
|
||||
|
||||
@@ -69,6 +69,8 @@ class Config(BaseSettings):
|
||||
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
quart_reload_excludes: Optional[List[str]] = None
|
||||
"""不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
quart_extra: Dict[str, Any] = {}
|
||||
"""传递给 `Quart` 的其他参数。"""
|
||||
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
@@ -82,7 +84,9 @@ class Driver(ReverseDriver):
|
||||
|
||||
self.quart_config = Config(**config.dict())
|
||||
|
||||
self._server_app = Quart(self.__class__.__qualname__)
|
||||
self._server_app = Quart(
|
||||
self.__class__.__qualname__, **self.quart_config.quart_extra
|
||||
)
|
||||
|
||||
@property
|
||||
@overrides(ReverseDriver)
|
||||
|
@@ -23,17 +23,17 @@ from nonebot.typing import overrides
|
||||
from nonebot.log import LoguruHandler
|
||||
from nonebot.drivers import Request, Response
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers._block_driver import BlockDriver
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ForwardMixin, ForwardDriver, combine_driver
|
||||
|
||||
try:
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
from websockets.legacy.client import Connect, WebSocketClientProtocol
|
||||
except ImportError: # pragma: no cover
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install websockets by using `pip install nonebot2[websockets]`"
|
||||
) from None
|
||||
) from e
|
||||
|
||||
logger = logging.Logger("websockets.client", "INFO")
|
||||
logger.addHandler(LoguruHandler())
|
||||
@@ -70,7 +70,7 @@ class Mixin(ForwardMixin):
|
||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||
connection = Connect(
|
||||
str(setup.url),
|
||||
extra_headers=setup.headers.items(),
|
||||
extra_headers={**setup.headers, **setup.cookies.as_header(setup)},
|
||||
open_timeout=setup.timeout,
|
||||
)
|
||||
async with connection as ws:
|
||||
@@ -101,8 +101,7 @@ class WebSocket(BaseWebSocket):
|
||||
@overrides(BaseWebSocket)
|
||||
@catch_closed
|
||||
async def receive(self) -> Union[str, bytes]:
|
||||
msg = await self.websocket.recv()
|
||||
return msg
|
||||
return await self.websocket.recv()
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@catch_closed
|
||||
@@ -129,5 +128,5 @@ class WebSocket(BaseWebSocket):
|
||||
await self.websocket.send(data)
|
||||
|
||||
|
||||
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore
|
||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
||||
"""Websockets Driver"""
|
||||
|
@@ -37,6 +37,9 @@ from pydantic.fields import ModelField
|
||||
class NoneBotException(Exception):
|
||||
"""所有 NoneBot 发生的异常基类。"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
# Rule Exception
|
||||
class ParserExit(NoneBotException):
|
||||
@@ -53,9 +56,6 @@ class ParserExit(NoneBotException):
|
||||
+ ")"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
# Processor Exception
|
||||
class ProcessException(NoneBotException):
|
||||
@@ -75,9 +75,6 @@ class IgnoredException(ProcessException):
|
||||
def __repr__(self) -> str:
|
||||
return f"IgnoredException(reason={self.reason!r})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
class SkippedException(ProcessException):
|
||||
"""指示 NoneBot 立即结束当前 `Dependent` 的运行。
|
||||
@@ -109,9 +106,6 @@ class TypeMisMatch(SkippedException):
|
||||
f"type={self.param._type_display()}, value={self.value!r}>"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
class MockApiException(ProcessException):
|
||||
"""指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。可由 api hook 抛出。
|
||||
@@ -126,9 +120,6 @@ class MockApiException(ProcessException):
|
||||
def __repr__(self) -> str:
|
||||
return f"MockApiException(result={self.result!r})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
class StopPropagation(ProcessException):
|
||||
"""指示 NoneBot 终止事件向下层传播。
|
||||
@@ -244,6 +235,3 @@ class WebSocketClosed(DriverException):
|
||||
+ (f", reason={self.reason!r}" if self.reason else "")
|
||||
+ ")"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
@@ -66,8 +66,9 @@ class Adapter(abc.ABC):
|
||||
参数:
|
||||
bot: {ref}`nonebot.adapters.Bot` 实例
|
||||
"""
|
||||
if self.bots.pop(bot.self_id, None) is None:
|
||||
raise RuntimeError(f"{bot} not found in adapter {self.get_name()}")
|
||||
self.driver._bot_disconnect(bot)
|
||||
self.bots.pop(bot.self_id, None)
|
||||
|
||||
def setup_http_server(self, setup: HTTPServerSetup):
|
||||
"""设置一个 HTTP 服务器路由配置"""
|
||||
|
@@ -43,6 +43,10 @@ class Bot(abc.ABC):
|
||||
return f"Bot(type={self.type!r}, self_id={self.self_id!r})"
|
||||
|
||||
def __getattr__(self, name: str) -> "_ApiCall":
|
||||
if name.startswith("__") and name.endswith("__"):
|
||||
raise AttributeError(
|
||||
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
||||
)
|
||||
return partial(self.call_api, name)
|
||||
|
||||
@property
|
||||
|
@@ -47,7 +47,7 @@ class Event(abc.ABC, BaseModel):
|
||||
通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,可以抛出 `NoLogException` 异常。
|
||||
|
||||
异常:
|
||||
NoLogException
|
||||
NoLogException:
|
||||
"""
|
||||
return f"[{self.get_event_name()}]: {self.get_event_description()}"
|
||||
|
||||
|
@@ -49,11 +49,11 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def __init__( # type:ignore
|
||||
self, template, factory=str
|
||||
) -> None: # TODO: fix type hint here
|
||||
self.template: TF = template
|
||||
self.factory: Type[TF] = factory
|
||||
def __init__(
|
||||
self, template: Union[str, TM], factory: Union[Type[str], Type[TM]] = str
|
||||
) -> None:
|
||||
self.template: TF = template # type: ignore
|
||||
self.factory: Type[TF] = factory # type: ignore
|
||||
self.format_specs: Dict[str, FormatSpecFunc] = {}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -98,7 +98,7 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
else:
|
||||
raise TypeError("template must be a string or instance of Message!")
|
||||
|
||||
self.check_unused_args(list(used_args), args, kwargs)
|
||||
self.check_unused_args(used_args, args, kwargs)
|
||||
return cast(TF, full_message)
|
||||
|
||||
def vformat(
|
||||
@@ -119,10 +119,9 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
) -> Tuple[TF, int]:
|
||||
results: List[Any] = [self.factory()]
|
||||
|
||||
for (literal_text, field_name, format_spec, conversion) in self.parse(
|
||||
for literal_text, field_name, format_spec, conversion in self.parse(
|
||||
format_string
|
||||
):
|
||||
|
||||
# output the literal text
|
||||
if literal_text:
|
||||
results.append(literal_text)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import abc
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
|
||||
|
||||
from nonebot.log import logger
|
||||
@@ -8,8 +8,12 @@ from nonebot.config import Env, Config
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.utils import escape_tag, run_coro_with_catch
|
||||
from nonebot.typing import T_BotConnectionHook, T_BotDisconnectionHook
|
||||
from nonebot.internal.params import BotParam, DependParam, DefaultParam
|
||||
from nonebot.typing import (
|
||||
T_DependencyCache,
|
||||
T_BotConnectionHook,
|
||||
T_BotDisconnectionHook,
|
||||
)
|
||||
|
||||
from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup
|
||||
|
||||
@@ -135,20 +139,22 @@ class Driver(abc.ABC):
|
||||
self._bots[bot.self_id] = bot
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)),
|
||||
self._bot_connection_hook,
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running WebSocketConnection hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
async with AsyncExitStack() as stack:
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
|
||||
(SkippedException,),
|
||||
)
|
||||
for hook in self._bot_connection_hook
|
||||
]:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running WebSocketConnection hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
asyncio.create_task(_run_hook(bot))
|
||||
|
||||
@@ -158,20 +164,22 @@ class Driver(abc.ABC):
|
||||
del self._bots[bot.self_id]
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)),
|
||||
self._bot_disconnection_hook,
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
async with AsyncExitStack() as stack:
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
|
||||
(SkippedException,),
|
||||
)
|
||||
for hook in self._bot_disconnection_hook
|
||||
]:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
asyncio.create_task(_run_hook(bot))
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import abc
|
||||
import urllib.request
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from http.cookiejar import Cookie, CookieJar
|
||||
@@ -105,12 +106,9 @@ class Request:
|
||||
self.url: URL = url
|
||||
|
||||
# headers
|
||||
self.headers: CIMultiDict[str]
|
||||
if headers is not None:
|
||||
self.headers = CIMultiDict(headers)
|
||||
else:
|
||||
self.headers = CIMultiDict()
|
||||
|
||||
self.headers: CIMultiDict[str] = (
|
||||
CIMultiDict(headers) if headers is not None else CIMultiDict()
|
||||
)
|
||||
# cookies
|
||||
self.cookies = Cookies(cookies)
|
||||
|
||||
@@ -147,12 +145,9 @@ class Response:
|
||||
self.status_code: int = status_code
|
||||
|
||||
# headers
|
||||
self.headers: CIMultiDict[str]
|
||||
if headers is not None:
|
||||
self.headers = CIMultiDict(headers)
|
||||
else:
|
||||
self.headers = CIMultiDict()
|
||||
|
||||
self.headers: CIMultiDict[str] = (
|
||||
CIMultiDict(headers) if headers is not None else CIMultiDict()
|
||||
)
|
||||
# body
|
||||
self.content: ContentTypes = content
|
||||
|
||||
@@ -308,6 +303,11 @@ class Cookies(MutableMapping):
|
||||
for cookie in cookies.jar:
|
||||
self.jar.set_cookie(cookie)
|
||||
|
||||
def as_header(self, request: Request) -> Dict[str, str]:
|
||||
urllib_request = self._CookieCompatRequest(request)
|
||||
self.jar.add_cookie_header(urllib_request)
|
||||
return urllib_request.added_headers
|
||||
|
||||
def __setitem__(self, name: str, value: str) -> None:
|
||||
return self.set(name, value)
|
||||
|
||||
@@ -333,6 +333,20 @@ class Cookies(MutableMapping):
|
||||
)
|
||||
return f"{self.__class__.__name__}({cookies_repr})"
|
||||
|
||||
class _CookieCompatRequest(urllib.request.Request):
|
||||
def __init__(self, request: Request) -> None:
|
||||
super().__init__(
|
||||
url=str(request.url),
|
||||
headers=dict(request.headers),
|
||||
method=request.method,
|
||||
)
|
||||
self.request = request
|
||||
self.added_headers: Dict[str, str] = {}
|
||||
|
||||
def add_unredirected_header(self, key: str, value: str) -> None:
|
||||
super().add_unredirected_header(key, value)
|
||||
self.added_headers[key] = value
|
||||
|
||||
|
||||
@dataclass
|
||||
class HTTPServerSetup:
|
||||
|
11
nonebot/internal/matcher/__init__.py
Normal file
11
nonebot/internal/matcher/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .manager import MatcherManager as MatcherManager
|
||||
from .provider import MatcherProvider as MatcherProvider
|
||||
from .provider import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
||||
|
||||
matchers = MatcherManager()
|
||||
|
||||
from .matcher import Matcher as Matcher
|
||||
from .matcher import current_bot as current_bot
|
||||
from .matcher import current_event as current_event
|
||||
from .matcher import current_handler as current_handler
|
||||
from .matcher import current_matcher as current_matcher
|
104
nonebot/internal/matcher/manager.py
Normal file
104
nonebot/internal/matcher/manager.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
List,
|
||||
Type,
|
||||
Tuple,
|
||||
Union,
|
||||
TypeVar,
|
||||
Iterator,
|
||||
KeysView,
|
||||
Optional,
|
||||
ItemsView,
|
||||
ValuesView,
|
||||
MutableMapping,
|
||||
overload,
|
||||
)
|
||||
|
||||
from .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .matcher import Matcher
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class MatcherManager(MutableMapping[int, List[Type["Matcher"]]]):
|
||||
"""事件响应器管理器
|
||||
|
||||
实现了常用字典操作,用于管理事件响应器。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.provider: MatcherProvider = DEFAULT_PROVIDER_CLASS({})
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"MatcherManager(provider={self.provider!r})"
|
||||
|
||||
def __contains__(self, o: object) -> bool:
|
||||
return o in self.provider
|
||||
|
||||
def __iter__(self) -> Iterator[int]:
|
||||
return iter(self.provider)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.provider)
|
||||
|
||||
def __getitem__(self, key: int) -> List[Type["Matcher"]]:
|
||||
return self.provider[key]
|
||||
|
||||
def __setitem__(self, key: int, value: List[Type["Matcher"]]) -> None:
|
||||
self.provider[key] = value
|
||||
|
||||
def __delitem__(self, key: int) -> None:
|
||||
del self.provider[key]
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, MatcherManager) and self.provider == other.provider
|
||||
|
||||
def keys(self) -> KeysView[int]:
|
||||
return self.provider.keys()
|
||||
|
||||
def values(self) -> ValuesView[List[Type["Matcher"]]]:
|
||||
return self.provider.values()
|
||||
|
||||
def items(self) -> ItemsView[int, List[Type["Matcher"]]]:
|
||||
return self.provider.items()
|
||||
|
||||
@overload
|
||||
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: Optional[T] = None
|
||||
) -> Optional[Union[List[Type["Matcher"]], T]]:
|
||||
return self.provider.get(key, default)
|
||||
|
||||
def pop(self, key: int) -> List[Type["Matcher"]]:
|
||||
return self.provider.pop(key)
|
||||
|
||||
def popitem(self) -> Tuple[int, List[Type["Matcher"]]]:
|
||||
return self.provider.popitem()
|
||||
|
||||
def clear(self) -> None:
|
||||
self.provider.clear()
|
||||
|
||||
def update(self, __m: MutableMapping[int, List[Type["Matcher"]]]) -> None:
|
||||
self.provider.update(__m)
|
||||
|
||||
def setdefault(
|
||||
self, key: int, default: List[Type["Matcher"]]
|
||||
) -> List[Type["Matcher"]]:
|
||||
return self.provider.setdefault(key, default)
|
||||
|
||||
def set_provider(self, provider_class: Type[MatcherProvider]) -> None:
|
||||
"""设置事件响应器存储器
|
||||
|
||||
参数:
|
||||
provider_class: 事件响应器存储器类
|
||||
"""
|
||||
self.provider = provider_class(self.provider)
|
@@ -1,17 +1,17 @@
|
||||
from types import ModuleType
|
||||
from contextvars import ContextVar
|
||||
from collections import defaultdict
|
||||
from contextlib import AsyncExitStack
|
||||
from typing_extensions import Self
|
||||
from datetime import datetime, timedelta
|
||||
from contextlib import AsyncExitStack, contextmanager
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Type,
|
||||
Union,
|
||||
TypeVar,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
NoReturn,
|
||||
Optional,
|
||||
@@ -19,7 +19,16 @@ from typing import (
|
||||
)
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.internal.rule import Rule
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.internal.permission import User, Permission
|
||||
from nonebot.internal.adapter import (
|
||||
Bot,
|
||||
Event,
|
||||
Message,
|
||||
MessageSegment,
|
||||
MessageTemplate,
|
||||
)
|
||||
from nonebot.consts import (
|
||||
ARG_KEY,
|
||||
RECEIVE_KEY,
|
||||
@@ -42,11 +51,7 @@ from nonebot.exception import (
|
||||
FinishedException,
|
||||
RejectedException,
|
||||
)
|
||||
|
||||
from .rule import Rule
|
||||
from .permission import USER, User, Permission
|
||||
from .adapter import Bot, Event, Message, MessageSegment, MessageTemplate
|
||||
from .params import (
|
||||
from nonebot.internal.params import (
|
||||
Depends,
|
||||
ArgParam,
|
||||
BotParam,
|
||||
@@ -57,13 +62,13 @@ from .params import (
|
||||
MatcherParam,
|
||||
)
|
||||
|
||||
from . import matchers
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
matchers: Dict[int, List[Type["Matcher"]]] = defaultdict(list)
|
||||
"""用于存储当前所有的事件响应器"""
|
||||
current_bot: ContextVar[Bot] = ContextVar("current_bot")
|
||||
current_event: ContextVar[Event] = ContextVar("current_event")
|
||||
current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
|
||||
@@ -77,7 +82,7 @@ class MatcherMeta(type):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"Matcher(type={self.type!r}"
|
||||
f"{self.__name__}(type={self.type!r}"
|
||||
+ (f", module={self.module_name}" if self.module_name else "")
|
||||
+ ")"
|
||||
)
|
||||
@@ -86,38 +91,38 @@ class MatcherMeta(type):
|
||||
class Matcher(metaclass=MatcherMeta):
|
||||
"""事件响应器类"""
|
||||
|
||||
plugin: Optional["Plugin"] = None
|
||||
plugin: ClassVar[Optional["Plugin"]] = None
|
||||
"""事件响应器所在插件"""
|
||||
module: Optional[ModuleType] = None
|
||||
module: ClassVar[Optional[ModuleType]] = None
|
||||
"""事件响应器所在插件模块"""
|
||||
plugin_name: Optional[str] = None
|
||||
plugin_name: ClassVar[Optional[str]] = None
|
||||
"""事件响应器所在插件名"""
|
||||
module_name: Optional[str] = None
|
||||
module_name: ClassVar[Optional[str]] = None
|
||||
"""事件响应器所在点分割插件模块路径"""
|
||||
|
||||
type: str = ""
|
||||
type: ClassVar[str] = ""
|
||||
"""事件响应器类型"""
|
||||
rule: Rule = Rule()
|
||||
rule: ClassVar[Rule] = Rule()
|
||||
"""事件响应器匹配规则"""
|
||||
permission: Permission = Permission()
|
||||
permission: ClassVar[Permission] = Permission()
|
||||
"""事件响应器触发权限"""
|
||||
handlers: List[Dependent[Any]] = []
|
||||
"""事件响应器拥有的事件处理函数列表"""
|
||||
priority: int = 1
|
||||
priority: ClassVar[int] = 1
|
||||
"""事件响应器优先级"""
|
||||
block: bool = False
|
||||
"""事件响应器是否阻止事件传播"""
|
||||
temp: bool = False
|
||||
temp: ClassVar[bool] = False
|
||||
"""事件响应器是否为临时"""
|
||||
expire_time: Optional[datetime] = None
|
||||
expire_time: ClassVar[Optional[datetime]] = None
|
||||
"""事件响应器过期时间点"""
|
||||
|
||||
_default_state: T_State = {}
|
||||
_default_state: ClassVar[T_State] = {}
|
||||
"""事件响应器默认状态"""
|
||||
|
||||
_default_type_updater: Optional[Dependent[str]] = None
|
||||
_default_type_updater: ClassVar[Optional[Dependent[str]]] = None
|
||||
"""事件响应器类型更新函数"""
|
||||
_default_permission_updater: Optional[Dependent[Permission]] = None
|
||||
_default_permission_updater: ClassVar[Optional[Dependent[Permission]]] = None
|
||||
"""事件响应器权限更新函数"""
|
||||
|
||||
HANDLER_PARAM_TYPES = (
|
||||
@@ -136,7 +141,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"Matcher(type={self.type!r}"
|
||||
f"{self.__class__.__name__}(type={self.type!r}"
|
||||
+ (f", module={self.module_name}" if self.module_name else "")
|
||||
+ ")"
|
||||
)
|
||||
@@ -160,7 +165,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
default_permission_updater: Optional[
|
||||
Union[T_PermissionUpdater, Dependent[Permission]]
|
||||
] = None,
|
||||
) -> Type["Matcher"]:
|
||||
) -> Type[Self]:
|
||||
"""
|
||||
创建一个新的事件响应器,并存储至 `matchers <#matchers>`_
|
||||
|
||||
@@ -181,8 +186,8 @@ class Matcher(metaclass=MatcherMeta):
|
||||
Type[Matcher]: 新的事件响应器类
|
||||
"""
|
||||
NewMatcher = type(
|
||||
"Matcher",
|
||||
(Matcher,),
|
||||
cls.__name__,
|
||||
(cls,),
|
||||
{
|
||||
"plugin": plugin,
|
||||
"module": module,
|
||||
@@ -244,6 +249,11 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
return NewMatcher
|
||||
|
||||
@classmethod
|
||||
def destroy(cls) -> None:
|
||||
"""销毁当前的事件响应器"""
|
||||
matchers[cls.priority].remove(cls)
|
||||
|
||||
@classmethod
|
||||
async def check_perm(
|
||||
cls,
|
||||
@@ -369,7 +379,6 @@ class Matcher(metaclass=MatcherMeta):
|
||||
_parameterless = (Depends(_receive), *(parameterless or tuple()))
|
||||
|
||||
def _decorator(func: T_Handler) -> T_Handler:
|
||||
|
||||
if cls.handlers and cls.handlers[-1].call is func:
|
||||
func_handler = cls.handlers[-1]
|
||||
new_handler = Dependent(
|
||||
@@ -417,7 +426,6 @@ class Matcher(metaclass=MatcherMeta):
|
||||
_parameterless = (Depends(_key_getter), *(parameterless or tuple()))
|
||||
|
||||
def _decorator(func: T_Handler) -> T_Handler:
|
||||
|
||||
if cls.handlers and cls.handlers[-1].call is func:
|
||||
func_handler = cls.handlers[-1]
|
||||
new_handler = Dependent(
|
||||
@@ -637,23 +645,44 @@ class Matcher(metaclass=MatcherMeta):
|
||||
"""阻止事件传播"""
|
||||
self.block = True
|
||||
|
||||
async def update_type(self, bot: Bot, event: Event) -> str:
|
||||
async def update_type(
|
||||
self,
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> str:
|
||||
updater = self.__class__._default_type_updater
|
||||
return (
|
||||
await updater(bot=bot, event=event, state=self.state, matcher=self)
|
||||
await updater(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
matcher=self,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
if updater
|
||||
else "message"
|
||||
)
|
||||
|
||||
async def update_permission(self, bot: Bot, event: Event) -> Permission:
|
||||
async def update_permission(
|
||||
self,
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> Permission:
|
||||
if updater := self.__class__._default_permission_updater:
|
||||
return await updater(bot=bot, event=event, state=self.state, matcher=self)
|
||||
permission = self.permission
|
||||
if len(permission.checkers) == 1 and isinstance(
|
||||
user_perm := tuple(permission.checkers)[0].call, User
|
||||
):
|
||||
permission = user_perm.perm
|
||||
return USER(event.get_session_id(), perm=permission)
|
||||
return await updater(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
matcher=self,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
return Permission(User.from_event(event, perm=self.permission))
|
||||
|
||||
async def resolve_reject(self):
|
||||
handler = current_handler.get()
|
||||
@@ -661,6 +690,18 @@ class Matcher(metaclass=MatcherMeta):
|
||||
if REJECT_CACHE_TARGET in self.state:
|
||||
self.state[REJECT_TARGET] = self.state[REJECT_CACHE_TARGET]
|
||||
|
||||
@contextmanager
|
||||
def ensure_context(self, bot: Bot, event: Event):
|
||||
b_t = current_bot.set(bot)
|
||||
e_t = current_event.set(event)
|
||||
m_t = current_matcher.set(self)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
current_bot.reset(b_t)
|
||||
current_event.reset(e_t)
|
||||
current_matcher.reset(m_t)
|
||||
|
||||
async def simple_run(
|
||||
self,
|
||||
bot: Bot,
|
||||
@@ -673,35 +714,31 @@ class Matcher(metaclass=MatcherMeta):
|
||||
f"{self} run with incoming args: "
|
||||
f"bot={bot}, event={event!r}, state={state!r}"
|
||||
)
|
||||
b_t = current_bot.set(bot)
|
||||
e_t = current_event.set(event)
|
||||
m_t = current_matcher.set(self)
|
||||
try:
|
||||
# Refresh preprocess state
|
||||
self.state.update(state)
|
||||
|
||||
while self.handlers:
|
||||
handler = self.handlers.pop(0)
|
||||
current_handler.set(handler)
|
||||
logger.debug(f"Running handler {handler}")
|
||||
try:
|
||||
await handler(
|
||||
matcher=self,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
except SkippedException:
|
||||
logger.debug(f"Handler {handler} skipped")
|
||||
except StopPropagation:
|
||||
self.block = True
|
||||
finally:
|
||||
logger.info(f"{self} running complete")
|
||||
current_bot.reset(b_t)
|
||||
current_event.reset(e_t)
|
||||
current_matcher.reset(m_t)
|
||||
with self.ensure_context(bot, event):
|
||||
try:
|
||||
# Refresh preprocess state
|
||||
self.state.update(state)
|
||||
|
||||
while self.handlers:
|
||||
handler = self.handlers.pop(0)
|
||||
current_handler.set(handler)
|
||||
logger.debug(f"Running handler {handler}")
|
||||
try:
|
||||
await handler(
|
||||
matcher=self,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
except SkippedException:
|
||||
logger.debug(f"Handler {handler} skipped")
|
||||
except StopPropagation:
|
||||
self.block = True
|
||||
finally:
|
||||
logger.info(f"{self} running complete")
|
||||
|
||||
# 运行handlers
|
||||
async def run(
|
||||
@@ -717,10 +754,12 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
except RejectedException:
|
||||
await self.resolve_reject()
|
||||
type_ = await self.update_type(bot, event)
|
||||
permission = await self.update_permission(bot, event)
|
||||
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||
permission = await self.update_permission(
|
||||
bot, event, stack, dependency_cache
|
||||
)
|
||||
|
||||
Matcher.new(
|
||||
self.new(
|
||||
type_,
|
||||
Rule(),
|
||||
permission,
|
||||
@@ -736,10 +775,12 @@ class Matcher(metaclass=MatcherMeta):
|
||||
default_permission_updater=self.__class__._default_permission_updater,
|
||||
)
|
||||
except PausedException:
|
||||
type_ = await self.update_type(bot, event)
|
||||
permission = await self.update_permission(bot, event)
|
||||
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||
permission = await self.update_permission(
|
||||
bot, event, stack, dependency_cache
|
||||
)
|
||||
|
||||
Matcher.new(
|
||||
self.new(
|
||||
type_,
|
||||
Rule(),
|
||||
permission,
|
||||
@@ -756,14 +797,3 @@ class Matcher(metaclass=MatcherMeta):
|
||||
)
|
||||
except FinishedException:
|
||||
pass
|
||||
|
||||
|
||||
__autodoc__ = {
|
||||
"MatcherMeta": False,
|
||||
"Matcher.get_target": False,
|
||||
"Matcher.set_target": False,
|
||||
"Matcher.update_type": False,
|
||||
"Matcher.update_permission": False,
|
||||
"Matcher.resolve_reject": False,
|
||||
"Matcher.simple_run": False,
|
||||
}
|
27
nonebot/internal/matcher/provider.py
Normal file
27
nonebot/internal/matcher/provider.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import abc
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, List, Type, Mapping, MutableMapping
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .matcher import Matcher
|
||||
|
||||
|
||||
class MatcherProvider(abc.ABC, MutableMapping[int, List[Type["Matcher"]]]):
|
||||
"""事件响应器存储器基类
|
||||
|
||||
参数:
|
||||
matchers: 当前存储器中已有的事件响应器
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __init__(self, matchers: Mapping[int, List[Type["Matcher"]]]):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _DictProvider(defaultdict, MatcherProvider):
|
||||
def __init__(self, matchers: Mapping[int, List[Type["Matcher"]]]):
|
||||
super().__init__(list, matchers)
|
||||
|
||||
|
||||
DEFAULT_PROVIDER_CLASS = _DictProvider
|
||||
"""默认存储器类型"""
|
@@ -1,8 +1,10 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
from typing_extensions import Annotated
|
||||
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Type, Tuple, Literal, Callable, Optional, cast
|
||||
|
||||
from pydantic.typing import get_args, get_origin
|
||||
from pydantic.fields import Required, Undefined, ModelField
|
||||
|
||||
from nonebot.dependencies.utils import check_field_type
|
||||
@@ -78,21 +80,33 @@ class DependParam(Param):
|
||||
def _check_param(
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["DependParam"]:
|
||||
if isinstance(param.default, DependsInner):
|
||||
dependency: T_Handler
|
||||
if param.default.dependency is None:
|
||||
assert param.annotation is not param.empty, "Dependency cannot be empty"
|
||||
dependency = param.annotation
|
||||
else:
|
||||
dependency = param.default.dependency
|
||||
sub_dependent = Dependent[Any].parse(
|
||||
call=dependency,
|
||||
allow_types=allow_types,
|
||||
)
|
||||
return cls(
|
||||
Required, use_cache=param.default.use_cache, dependent=sub_dependent
|
||||
type_annotation, depends_inner = param.annotation, None
|
||||
if get_origin(param.annotation) is Annotated:
|
||||
type_annotation, *extra_args = get_args(param.annotation)
|
||||
depends_inner = next(
|
||||
(x for x in extra_args if isinstance(x, DependsInner)), None
|
||||
)
|
||||
|
||||
depends_inner = (
|
||||
param.default if isinstance(param.default, DependsInner) else depends_inner
|
||||
)
|
||||
if depends_inner is None:
|
||||
return
|
||||
|
||||
dependency: T_Handler
|
||||
if depends_inner.dependency is None:
|
||||
assert (
|
||||
type_annotation is not inspect.Signature.empty
|
||||
), "Dependency cannot be empty"
|
||||
dependency = type_annotation
|
||||
else:
|
||||
dependency = depends_inner.dependency
|
||||
sub_dependent = Dependent[Any].parse(
|
||||
call=dependency,
|
||||
allow_types=allow_types,
|
||||
)
|
||||
return cls(Required, use_cache=depends_inner.use_cache, dependent=sub_dependent)
|
||||
|
||||
@classmethod
|
||||
def _check_parameterless(
|
||||
cls, value: Any, allow_types: Tuple[Type[Param], ...]
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
from typing_extensions import Self
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import Set, Tuple, Union, NoReturn, Optional
|
||||
|
||||
@@ -57,7 +58,7 @@ class Permission:
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> bool:
|
||||
"""检查是否满足某个权限
|
||||
"""检查是否满足某个权限。
|
||||
|
||||
参数:
|
||||
bot: Bot 对象
|
||||
@@ -109,7 +110,7 @@ class Permission:
|
||||
|
||||
|
||||
class User:
|
||||
"""检查当前事件是否属于指定会话
|
||||
"""检查当前事件是否属于指定会话。
|
||||
|
||||
参数:
|
||||
users: 会话 ID 元组
|
||||
@@ -140,13 +141,47 @@ class User:
|
||||
session in self.users and (self.perm is None or await self.perm(bot, event))
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _clean_permission(cls, perm: Permission) -> Optional[Permission]:
|
||||
if len(perm.checkers) == 1 and isinstance(
|
||||
user_perm := tuple(perm.checkers)[0].call, cls
|
||||
):
|
||||
return user_perm.perm
|
||||
return perm
|
||||
|
||||
@classmethod
|
||||
def from_event(cls, event: Event, perm: Optional[Permission] = None) -> Self:
|
||||
"""从事件中获取会话 ID。
|
||||
|
||||
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。
|
||||
|
||||
参数:
|
||||
event: Event 对象
|
||||
perm: 需同时满足的权限
|
||||
"""
|
||||
return cls((event.get_session_id(),), perm=perm and cls._clean_permission(perm))
|
||||
|
||||
@classmethod
|
||||
def from_permission(cls, *users: str, perm: Optional[Permission] = None) -> Self:
|
||||
"""指定会话与权限。
|
||||
|
||||
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。
|
||||
|
||||
参数:
|
||||
users: 会话白名单
|
||||
perm: 需同时满足的权限
|
||||
"""
|
||||
return cls(users, perm=perm and cls._clean_permission(perm))
|
||||
|
||||
|
||||
def USER(*users: str, perm: Optional[Permission] = None):
|
||||
"""匹配当前事件属于指定会话
|
||||
"""匹配当前事件属于指定会话。
|
||||
|
||||
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有检查函数的会话 ID 限制。
|
||||
|
||||
参数:
|
||||
user: 会话白名单
|
||||
perm: 需要同时满足的权限
|
||||
"""
|
||||
|
||||
return Permission(User(users, perm))
|
||||
return Permission(User.from_permission(*users, perm=perm))
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
NoneBot 使用 [`loguru`][loguru] 来记录日志信息。
|
||||
|
||||
自定义 logger 请参考 [自定义日志](https://v2.nonebot.dev/docs/tutorial/custom-logger)
|
||||
自定义 logger 请参考 [自定义日志](https://v2.nonebot.dev/docs/appendices/log)
|
||||
以及 [`loguru`][loguru] 文档。
|
||||
|
||||
[loguru]: https://github.com/Delgan/loguru
|
||||
|
@@ -9,10 +9,16 @@ from nonebot.internal.matcher import Matcher as Matcher
|
||||
from nonebot.internal.matcher import matchers as matchers
|
||||
from nonebot.internal.matcher import current_bot as current_bot
|
||||
from nonebot.internal.matcher import current_event as current_event
|
||||
from nonebot.internal.matcher import MatcherManager as MatcherManager
|
||||
from nonebot.internal.matcher import MatcherProvider as MatcherProvider
|
||||
from nonebot.internal.matcher import current_handler as current_handler
|
||||
from nonebot.internal.matcher import current_matcher as current_matcher
|
||||
from nonebot.internal.matcher import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
||||
|
||||
__autodoc__ = {
|
||||
"Matcher": True,
|
||||
"matchers": True,
|
||||
"MatcherManager": True,
|
||||
"MatcherProvider": True,
|
||||
"DEFAULT_PROVIDER_CLASS": True,
|
||||
}
|
||||
|
@@ -112,7 +112,6 @@ def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
|
||||
|
||||
|
||||
async def _check_matcher(
|
||||
priority: int,
|
||||
Matcher: Type[Matcher],
|
||||
bot: "Bot",
|
||||
event: "Event",
|
||||
@@ -122,7 +121,7 @@ async def _check_matcher(
|
||||
) -> None:
|
||||
if Matcher.expire_time and datetime.now() > Matcher.expire_time:
|
||||
with contextlib.suppress(Exception):
|
||||
matchers[priority].remove(Matcher)
|
||||
Matcher.destroy()
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -138,7 +137,7 @@ async def _check_matcher(
|
||||
|
||||
if Matcher.temp:
|
||||
with contextlib.suppress(Exception):
|
||||
matchers[priority].remove(Matcher)
|
||||
Matcher.destroy()
|
||||
await _run_matcher(Matcher, bot, event, state, stack, dependency_cache)
|
||||
|
||||
|
||||
@@ -167,17 +166,19 @@ async def _run_matcher(
|
||||
)
|
||||
for proc in _run_preprocessors
|
||||
]:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except IgnoredException:
|
||||
logger.opt(colors=True).info(f"{matcher} running is <b>cancelled</b>")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPreProcessors. Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
# ensure matcher function can be correctly called
|
||||
with matcher.ensure_context(bot, event):
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except IgnoredException:
|
||||
logger.opt(colors=True).info(f"{matcher} running is <b>cancelled</b>")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPreProcessors. Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
return
|
||||
return
|
||||
|
||||
exception = None
|
||||
|
||||
@@ -205,12 +206,14 @@ async def _run_matcher(
|
||||
)
|
||||
for proc in _run_postprocessors
|
||||
]:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPostProcessors</bg #f8bbd0></r>"
|
||||
)
|
||||
# ensure matcher function can be correctly called
|
||||
with matcher.ensure_context(bot, event):
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPostProcessors</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
if matcher.block:
|
||||
raise StopPropagation
|
||||
@@ -290,7 +293,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
|
||||
pending_tasks = [
|
||||
_check_matcher(
|
||||
priority, matcher, bot, event, state.copy(), stack, dependency_cache
|
||||
matcher, bot, event, state.copy(), stack, dependency_cache
|
||||
)
|
||||
for matcher in matchers[priority]
|
||||
]
|
||||
@@ -308,6 +311,9 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
if show_log:
|
||||
logger.debug("Checking for matchers completed")
|
||||
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
|
@@ -5,6 +5,7 @@ FrontMatter:
|
||||
description: nonebot.params 模块
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from typing import Any, Dict, List, Tuple, Union, Optional
|
||||
|
||||
from nonebot.typing import T_State
|
||||
@@ -24,15 +25,21 @@ from nonebot.internal.params import MatcherParam as MatcherParam
|
||||
from nonebot.internal.params import ExceptionParam as ExceptionParam
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
REGEX_STR,
|
||||
PREFIX_KEY,
|
||||
REGEX_DICT,
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
ENDSWITH_KEY,
|
||||
CMD_START_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
)
|
||||
|
||||
|
||||
@@ -108,6 +115,15 @@ def CommandStart() -> str:
|
||||
return Depends(_command_start)
|
||||
|
||||
|
||||
def _command_whitespace(state: T_State) -> str:
|
||||
return state[PREFIX_KEY][CMD_WHITESPACE_KEY]
|
||||
|
||||
|
||||
def CommandWhitespace() -> str:
|
||||
"""消息命令与参数之间的空白"""
|
||||
return Depends(_command_whitespace)
|
||||
|
||||
|
||||
def _shell_command_args(state: T_State) -> Any:
|
||||
return state[SHELL_ARGS] # Namespace or ParserExit
|
||||
|
||||
@@ -132,10 +148,25 @@ def _regex_matched(state: T_State) -> str:
|
||||
|
||||
def RegexMatched() -> str:
|
||||
"""正则匹配结果"""
|
||||
warnings.warn(
|
||||
'"RegexMatched()" will be changed to "re.Match" object, '
|
||||
'use "RegexStr()" instead. '
|
||||
"See https://github.com/nonebot/nonebot2/pull/1453 .",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return Depends(_regex_matched, use_cache=False)
|
||||
|
||||
|
||||
def _regex_group(state: T_State):
|
||||
def _regex_str(state: T_State) -> str:
|
||||
return state[REGEX_STR]
|
||||
|
||||
|
||||
def RegexStr() -> str:
|
||||
"""正则匹配结果文本"""
|
||||
return Depends(_regex_str, use_cache=False)
|
||||
|
||||
|
||||
def _regex_group(state: T_State) -> Tuple[Any, ...]:
|
||||
return state[REGEX_GROUP]
|
||||
|
||||
|
||||
@@ -144,7 +175,7 @@ def RegexGroup() -> Tuple[Any, ...]:
|
||||
return Depends(_regex_group, use_cache=False)
|
||||
|
||||
|
||||
def _regex_dict(state: T_State):
|
||||
def _regex_dict(state: T_State) -> Dict[str, Any]:
|
||||
return state[REGEX_DICT]
|
||||
|
||||
|
||||
@@ -153,10 +184,46 @@ def RegexDict() -> Dict[str, Any]:
|
||||
return Depends(_regex_dict, use_cache=False)
|
||||
|
||||
|
||||
def _startswith(state: T_State) -> str:
|
||||
return state[STARTSWITH_KEY]
|
||||
|
||||
|
||||
def Startswith() -> str:
|
||||
"""响应触发前缀"""
|
||||
return Depends(_startswith, use_cache=False)
|
||||
|
||||
|
||||
def _endswith(state: T_State) -> str:
|
||||
return state[ENDSWITH_KEY]
|
||||
|
||||
|
||||
def Endswith() -> str:
|
||||
"""响应触发后缀"""
|
||||
return Depends(_endswith, use_cache=False)
|
||||
|
||||
|
||||
def _fullmatch(state: T_State) -> str:
|
||||
return state[FULLMATCH_KEY]
|
||||
|
||||
|
||||
def Fullmatch() -> str:
|
||||
"""响应触发完整消息"""
|
||||
return Depends(_fullmatch, use_cache=False)
|
||||
|
||||
|
||||
def _keyword(state: T_State) -> str:
|
||||
return state[KEYWORD_KEY]
|
||||
|
||||
|
||||
def Keyword() -> str:
|
||||
"""响应触发关键字"""
|
||||
return Depends(_keyword, use_cache=False)
|
||||
|
||||
|
||||
def Received(id: Optional[str] = None, default: Any = None) -> Any:
|
||||
"""`receive` 事件参数"""
|
||||
|
||||
def _received(matcher: "Matcher"):
|
||||
def _received(matcher: "Matcher") -> Any:
|
||||
return matcher.get_receive(id or "", default)
|
||||
|
||||
return Depends(_received, use_cache=False)
|
||||
|
@@ -65,6 +65,8 @@ def _revert_plugin(plugin: "Plugin") -> None:
|
||||
if plugin.name not in _plugins:
|
||||
raise RuntimeError("Plugin not found!")
|
||||
del _plugins[plugin.name]
|
||||
if parent_plugin := plugin.parent_plugin:
|
||||
parent_plugin.sub_plugins.remove(plugin)
|
||||
|
||||
|
||||
def get_plugin(name: str) -> Optional["Plugin"]:
|
||||
|
@@ -9,14 +9,17 @@ from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Set, Union, Iterable, Optional
|
||||
|
||||
import tomlkit
|
||||
|
||||
from nonebot.utils import path_to_module_name
|
||||
|
||||
from .plugin import Plugin
|
||||
from .manager import PluginManager
|
||||
from . import _managers, get_plugin, _module_name_to_plugin_name
|
||||
|
||||
try: # pragma: py-gte-311
|
||||
import tomllib # pyright: reportMissingImports=false
|
||||
except ModuleNotFoundError: # pragma: py-lt-311
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
|
||||
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
|
||||
@@ -108,7 +111,7 @@ def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
||||
```
|
||||
"""
|
||||
with open(file_path, "r", encoding=encoding) as f:
|
||||
data = tomlkit.parse(f.read()) # type: ignore
|
||||
data = tomllib.loads(f.read())
|
||||
|
||||
nonebot_data = data.get("tool", {}).get("nonebot")
|
||||
if nonebot_data is None:
|
||||
|
@@ -14,7 +14,7 @@ from itertools import chain
|
||||
from types import ModuleType
|
||||
from importlib.abc import MetaPathFinder
|
||||
from importlib.machinery import PathFinder, SourceFileLoader
|
||||
from typing import Set, Dict, List, Union, Iterable, Optional, Sequence
|
||||
from typing import Set, Dict, List, Iterable, Optional, Sequence
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import escape_tag, path_to_module_name
|
||||
@@ -148,14 +148,21 @@ class PluginManager:
|
||||
else:
|
||||
raise RuntimeError(f"Plugin not found: {name}! Check your plugin name")
|
||||
|
||||
logger.opt(colors=True).success(
|
||||
f'Succeeded to import "<y>{escape_tag(name)}</y>"'
|
||||
)
|
||||
if (plugin := getattr(module, "__plugin__", None)) is None:
|
||||
if (
|
||||
plugin := getattr(module, "__plugin__", None)
|
||||
) is None or not isinstance(plugin, Plugin):
|
||||
raise RuntimeError(
|
||||
f"Module {module.__name__} is not loaded as a plugin! "
|
||||
"Make sure not to import it before loading."
|
||||
)
|
||||
logger.opt(colors=True).success(
|
||||
f'Succeeded to load plugin "<y>{escape_tag(plugin.name)}</y>"'
|
||||
+ (
|
||||
f' from "<m>{escape_tag(plugin.module_name)}</m>"'
|
||||
if plugin.module_name != plugin.name
|
||||
else ""
|
||||
)
|
||||
)
|
||||
return plugin
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
@@ -174,7 +181,7 @@ class PluginFinder(MetaPathFinder):
|
||||
def find_spec(
|
||||
self,
|
||||
fullname: str,
|
||||
path: Optional[Sequence[Union[bytes, str]]],
|
||||
path: Optional[Sequence[str]],
|
||||
target: Optional[ModuleType] = None,
|
||||
):
|
||||
if _managers:
|
||||
|
@@ -28,16 +28,44 @@ from nonebot.rule import (
|
||||
shell_command,
|
||||
)
|
||||
|
||||
from .plugin import Plugin
|
||||
from . import get_plugin_by_module_name
|
||||
from .manager import _current_plugin_chain
|
||||
|
||||
|
||||
def _store_matcher(matcher: Type[Matcher]) -> None:
|
||||
# only store the matcher defined in the plugin
|
||||
if plugins := _current_plugin_chain.get():
|
||||
plugins[-1].matcher.add(matcher)
|
||||
def store_matcher(matcher: Type[Matcher]) -> None:
|
||||
"""存储一个事件响应器到插件。
|
||||
|
||||
参数:
|
||||
matcher: 事件响应器
|
||||
"""
|
||||
# only store the matcher defined when plugin loading
|
||||
if plugin_chain := _current_plugin_chain.get():
|
||||
plugin_chain[-1].matcher.add(matcher)
|
||||
|
||||
|
||||
def _get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
|
||||
def get_matcher_plugin(depth: int = 1) -> Optional[Plugin]:
|
||||
"""获取事件响应器定义所在插件。
|
||||
|
||||
参数:
|
||||
depth: 调用栈深度
|
||||
"""
|
||||
# matcher defined when plugin loading
|
||||
if plugin_chain := _current_plugin_chain.get():
|
||||
return plugin_chain[-1]
|
||||
|
||||
# matcher defined when plugin running
|
||||
if module := get_matcher_module(depth + 1):
|
||||
if plugin := get_plugin_by_module_name(module.__name__):
|
||||
return plugin
|
||||
|
||||
|
||||
def get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
|
||||
"""获取事件响应器定义所在模块。
|
||||
|
||||
参数:
|
||||
depth: 调用栈深度
|
||||
"""
|
||||
current_frame = inspect.currentframe()
|
||||
if current_frame is None:
|
||||
return None
|
||||
@@ -71,7 +99,6 @@ def on(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
type,
|
||||
Rule() & rule,
|
||||
@@ -81,29 +108,20 @@ def on(
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
plugin=get_matcher_plugin(_depth + 1),
|
||||
module=get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
store_matcher(matcher)
|
||||
return matcher
|
||||
|
||||
|
||||
def on_metaevent(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
|
||||
temp: bool = False,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
priority: int = 1,
|
||||
block: bool = False,
|
||||
state: Optional[T_State] = None,
|
||||
_depth: int = 0,
|
||||
) -> Type[Matcher]:
|
||||
def on_metaevent(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个元事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -111,36 +129,10 @@ def on_metaevent(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
"meta_event",
|
||||
Rule() & rule,
|
||||
Permission(),
|
||||
temp=temp,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
return matcher
|
||||
return on("meta_event", *args, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_message(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = None,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
|
||||
temp: bool = False,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
priority: int = 1,
|
||||
block: bool = True,
|
||||
state: Optional[T_State] = None,
|
||||
_depth: int = 0,
|
||||
) -> Type[Matcher]:
|
||||
def on_message(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个消息事件响应器。
|
||||
|
||||
参数:
|
||||
@@ -153,39 +145,16 @@ def on_message(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
"message",
|
||||
Rule() & rule,
|
||||
Permission() | permission,
|
||||
temp=temp,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
return matcher
|
||||
kwargs.setdefault("block", True)
|
||||
return on("message", *args, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_notice(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
|
||||
temp: bool = False,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
priority: int = 1,
|
||||
block: bool = False,
|
||||
state: Optional[T_State] = None,
|
||||
_depth: int = 0,
|
||||
) -> Type[Matcher]:
|
||||
def on_notice(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个通知事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -193,39 +162,15 @@ def on_notice(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
"notice",
|
||||
Rule() & rule,
|
||||
Permission(),
|
||||
temp=temp,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
return matcher
|
||||
return on("notice", *args, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_request(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
|
||||
temp: bool = False,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
priority: int = 1,
|
||||
block: bool = False,
|
||||
state: Optional[T_State] = None,
|
||||
_depth: int = 0,
|
||||
) -> Type[Matcher]:
|
||||
def on_request(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个请求事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -233,22 +178,7 @@ def on_request(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
"request",
|
||||
Rule() & rule,
|
||||
Permission(),
|
||||
temp=temp,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
return matcher
|
||||
return on("request", *args, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_startswith(
|
||||
@@ -349,6 +279,7 @@ def on_command(
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
|
||||
force_whitespace: Optional[Union[str, bool]] = None,
|
||||
_depth: int = 0,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
@@ -360,6 +291,7 @@ def on_command(
|
||||
cmd: 指定命令内容
|
||||
rule: 事件响应规则
|
||||
aliases: 命令别名
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
@@ -370,9 +302,11 @@ def on_command(
|
||||
"""
|
||||
|
||||
commands = {cmd} | (aliases or set())
|
||||
block = kwargs.pop("block", False)
|
||||
kwargs.setdefault("block", False)
|
||||
return on_message(
|
||||
command(*commands) & rule, block=block, **kwargs, _depth=_depth + 1
|
||||
command(*commands, force_whitespace=force_whitespace) & rule,
|
||||
**kwargs,
|
||||
_depth=_depth + 1,
|
||||
)
|
||||
|
||||
|
||||
@@ -439,7 +373,7 @@ def on_regex(
|
||||
|
||||
|
||||
def on_type(
|
||||
types: Union[Type[Event], Tuple[Type[Event]]],
|
||||
types: Union[Type[Event], Tuple[Type[Event], ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
*,
|
||||
_depth: int = 0,
|
||||
@@ -518,6 +452,7 @@ class CommandGroup(_Group):
|
||||
参数:
|
||||
cmd: 指定命令内容
|
||||
aliases: 命令别名
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
@@ -587,6 +522,7 @@ class MatcherGroup(_Group):
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -622,6 +558,7 @@ class MatcherGroup(_Group):
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -639,6 +576,7 @@ class MatcherGroup(_Group):
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -736,6 +674,7 @@ class MatcherGroup(_Group):
|
||||
self,
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
|
||||
force_whitespace: Optional[Union[str, bool]] = None,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
|
||||
@@ -745,6 +684,7 @@ class MatcherGroup(_Group):
|
||||
参数:
|
||||
cmd: 指定命令内容
|
||||
aliases: 命令别名
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
@@ -755,7 +695,9 @@ class MatcherGroup(_Group):
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_command(cmd, aliases=aliases, **final_kwargs)
|
||||
matcher = on_command(
|
||||
cmd, aliases=aliases, force_whitespace=force_whitespace, **final_kwargs
|
||||
)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
from types import ModuleType
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Set, List, Type, Tuple, Union, Optional
|
||||
|
||||
@@ -9,6 +10,11 @@ from nonebot.dependencies import Dependent
|
||||
from nonebot.rule import Rule, ArgumentParser
|
||||
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
|
||||
|
||||
from .plugin import Plugin
|
||||
|
||||
def store_matcher(matcher: Type[Matcher]) -> None: ...
|
||||
def get_matcher_plugin(depth: int = ...) -> Optional[Plugin]: ...
|
||||
def get_matcher_module(depth: int = ...) -> Optional[ModuleType]: ...
|
||||
def on(
|
||||
type: str = "",
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
@@ -23,6 +29,7 @@ def on(
|
||||
) -> Type[Matcher]: ...
|
||||
def on_metaevent(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
@@ -44,6 +51,7 @@ def on_message(
|
||||
) -> Type[Matcher]: ...
|
||||
def on_notice(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
@@ -54,6 +62,7 @@ def on_notice(
|
||||
) -> Type[Matcher]: ...
|
||||
def on_request(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
@@ -117,6 +126,7 @@ def on_command(
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
force_whitespace: Optional[Union[str, bool]] = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
@@ -154,7 +164,7 @@ def on_regex(
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
def on_type(
|
||||
types: Union[Type[Event], Tuple[Type[Event]]],
|
||||
types: Union[Type[Event], Tuple[Type[Event], ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
@@ -186,6 +196,7 @@ class CommandGroup:
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
force_whitespace: Optional[Union[str, bool]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
@@ -241,6 +252,7 @@ class MatcherGroup:
|
||||
self,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
@@ -264,6 +276,7 @@ class MatcherGroup:
|
||||
self,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
@@ -275,6 +288,7 @@ class MatcherGroup:
|
||||
self,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
@@ -341,6 +355,7 @@ class MatcherGroup:
|
||||
self,
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
force_whitespace: Optional[Union[str, bool]] = ...,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
|
@@ -12,7 +12,8 @@ from pydantic import BaseModel
|
||||
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
from . import _plugins as plugins # FIXME: backport for nonebug
|
||||
# FIXME: backport for nonebug
|
||||
from . import _plugins as plugins # nopycln: import
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import PluginManager
|
||||
@@ -46,7 +47,7 @@ class Plugin:
|
||||
manager: "PluginManager"
|
||||
"""导入该插件的插件管理器"""
|
||||
matcher: Set[Type[Matcher]] = field(default_factory=set)
|
||||
"""插件内定义的 `Matcher`"""
|
||||
"""插件加载时定义的 `Matcher`"""
|
||||
parent_plugin: Optional["Plugin"] = None
|
||||
"""父插件"""
|
||||
sub_plugins: Set["Plugin"] = field(default_factory=set)
|
||||
|
150
nonebot/rule.py
150
nonebot/rule.py
@@ -12,13 +12,13 @@ import re
|
||||
import shlex
|
||||
from argparse import Action
|
||||
from argparse import ArgumentError
|
||||
from contextvars import ContextVar
|
||||
from itertools import chain, product
|
||||
from argparse import Namespace as Namespace
|
||||
from argparse import ArgumentParser as ArgParser
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
List,
|
||||
Type,
|
||||
Tuple,
|
||||
@@ -39,19 +39,25 @@ from nonebot.log import logger
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.exception import ParserExit
|
||||
from nonebot.internal.rule import Rule as Rule
|
||||
from nonebot.params import Command, EventToMe, CommandArg
|
||||
from nonebot.adapters import Bot, Event, Message, MessageSegment
|
||||
from nonebot.params import Command, EventToMe, CommandArg, CommandWhitespace
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
REGEX_STR,
|
||||
PREFIX_KEY,
|
||||
REGEX_DICT,
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
ENDSWITH_KEY,
|
||||
CMD_START_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
)
|
||||
|
||||
T = TypeVar("T")
|
||||
@@ -63,6 +69,7 @@ CMD_RESULT = TypedDict(
|
||||
"raw_command": Optional[str],
|
||||
"command_arg": Optional[Message[MessageSegment]],
|
||||
"command_start": Optional[str],
|
||||
"command_whitespace": Optional[str],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -70,6 +77,8 @@ TRIE_VALUE = NamedTuple(
|
||||
"TRIE_VALUE", [("command_start", str), ("command", Tuple[str, ...])]
|
||||
)
|
||||
|
||||
parser_message: ContextVar[str] = ContextVar("parser_message")
|
||||
|
||||
|
||||
class TrieRule:
|
||||
prefix: CharTrie = CharTrie()
|
||||
@@ -84,7 +93,11 @@ class TrieRule:
|
||||
@classmethod
|
||||
def get_value(cls, bot: Bot, event: Event, state: T_State) -> CMD_RESULT:
|
||||
prefix = CMD_RESULT(
|
||||
command=None, raw_command=None, command_arg=None, command_start=None
|
||||
command=None,
|
||||
raw_command=None,
|
||||
command_arg=None,
|
||||
command_start=None,
|
||||
command_whitespace=None,
|
||||
)
|
||||
state[PREFIX_KEY] = prefix
|
||||
if event.get_type() != "message":
|
||||
@@ -99,11 +112,25 @@ class TrieRule:
|
||||
prefix[RAW_CMD_KEY] = pf.key
|
||||
prefix[CMD_START_KEY] = value.command_start
|
||||
prefix[CMD_KEY] = value.command
|
||||
|
||||
msg = message.copy()
|
||||
msg.pop(0)
|
||||
new_message = msg.__class__(segment_text[len(pf.key) :].lstrip())
|
||||
for new_segment in reversed(new_message):
|
||||
msg.insert(0, new_segment)
|
||||
|
||||
# check whitespace
|
||||
arg_str = segment_text[len(pf.key) :]
|
||||
arg_str_stripped = arg_str.lstrip()
|
||||
has_arg = arg_str_stripped or msg
|
||||
if (
|
||||
has_arg
|
||||
and (stripped_len := len(arg_str) - len(arg_str_stripped)) > 0
|
||||
):
|
||||
prefix[CMD_WHITESPACE_KEY] = arg_str[:stripped_len]
|
||||
|
||||
# construct command arg
|
||||
if arg_str_stripped:
|
||||
new_message = msg.__class__(arg_str_stripped)
|
||||
for new_segment in reversed(new_message):
|
||||
msg.insert(0, new_segment)
|
||||
prefix[CMD_ARG_KEY] = msg
|
||||
|
||||
return prefix
|
||||
@@ -136,20 +163,19 @@ class StartswithRule:
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.msg), self.ignorecase))
|
||||
|
||||
async def __call__(self, event: Event) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
return False
|
||||
return bool(
|
||||
re.match(
|
||||
f"^(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})",
|
||||
text,
|
||||
re.IGNORECASE if self.ignorecase else 0,
|
||||
)
|
||||
)
|
||||
if match := re.match(
|
||||
f"^(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})",
|
||||
text,
|
||||
re.IGNORECASE if self.ignorecase else 0,
|
||||
):
|
||||
state[STARTSWITH_KEY] = match.group()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def startswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
||||
@@ -192,20 +218,19 @@ class EndswithRule:
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.msg), self.ignorecase))
|
||||
|
||||
async def __call__(self, event: Event) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
return False
|
||||
return bool(
|
||||
re.search(
|
||||
f"(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})$",
|
||||
text,
|
||||
re.IGNORECASE if self.ignorecase else 0,
|
||||
)
|
||||
)
|
||||
if match := re.search(
|
||||
f"(?:{'|'.join(re.escape(suffix) for suffix in self.msg)})$",
|
||||
text,
|
||||
re.IGNORECASE if self.ignorecase else 0,
|
||||
):
|
||||
state[ENDSWITH_KEY] = match.group()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def endswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
||||
@@ -248,14 +273,18 @@ class FullmatchRule:
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.msg), self.ignorecase))
|
||||
|
||||
async def __call__(self, event: Event) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
return False
|
||||
return (text.casefold() if self.ignorecase else text) in self.msg
|
||||
if not text:
|
||||
return False
|
||||
text = text.casefold() if self.ignorecase else text
|
||||
if text in self.msg:
|
||||
state[FULLMATCH_KEY] = text
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def fullmatch(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
||||
@@ -294,14 +323,17 @@ class KeywordsRule:
|
||||
def __hash__(self) -> int:
|
||||
return hash(frozenset(self.keywords))
|
||||
|
||||
async def __call__(self, event: Event) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
return False
|
||||
return bool(text and any(keyword in text for keyword in self.keywords))
|
||||
if not text:
|
||||
return False
|
||||
if key := next((k for k in self.keywords if k in text), None):
|
||||
state[KEYWORD_KEY] = key
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def keyword(*keywords: str) -> Rule:
|
||||
@@ -319,12 +351,18 @@ class CommandRule:
|
||||
|
||||
参数:
|
||||
cmds: 指定命令元组列表
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
"""
|
||||
|
||||
__slots__ = ("cmds",)
|
||||
__slots__ = ("cmds", "force_whitespace")
|
||||
|
||||
def __init__(self, cmds: List[Tuple[str, ...]]):
|
||||
def __init__(
|
||||
self,
|
||||
cmds: List[Tuple[str, ...]],
|
||||
force_whitespace: Optional[Union[str, bool]] = None,
|
||||
):
|
||||
self.cmds = tuple(cmds)
|
||||
self.force_whitespace = force_whitespace
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Command(cmds={self.cmds})"
|
||||
@@ -337,11 +375,24 @@ class CommandRule:
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.cmds),))
|
||||
|
||||
async def __call__(self, cmd: Optional[Tuple[str, ...]] = Command()) -> bool:
|
||||
return cmd in self.cmds
|
||||
async def __call__(
|
||||
self,
|
||||
cmd: Optional[Tuple[str, ...]] = Command(),
|
||||
cmd_whitespace: Optional[str] = CommandWhitespace(),
|
||||
) -> bool:
|
||||
if cmd not in self.cmds:
|
||||
return False
|
||||
if self.force_whitespace is None:
|
||||
return True
|
||||
if isinstance(self.force_whitespace, str):
|
||||
return self.force_whitespace == cmd_whitespace
|
||||
return self.force_whitespace == (cmd_whitespace is not None)
|
||||
|
||||
|
||||
def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
|
||||
def command(
|
||||
*cmds: Union[str, Tuple[str, ...]],
|
||||
force_whitespace: Optional[Union[str, bool]] = None,
|
||||
) -> Rule:
|
||||
"""匹配消息命令。
|
||||
|
||||
根据配置里提供的 {ref}``command_start` <nonebot.config.Config.command_start>`,
|
||||
@@ -353,6 +404,7 @@ def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
|
||||
|
||||
参数:
|
||||
cmds: 命令文本或命令元组
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
|
||||
用法:
|
||||
使用默认 `command_start`, `command_sep` 配置
|
||||
@@ -384,7 +436,7 @@ def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
|
||||
f"{start}{sep.join(command)}", TRIE_VALUE(start, command)
|
||||
)
|
||||
|
||||
return Rule(CommandRule(commands))
|
||||
return Rule(CommandRule(commands, force_whitespace))
|
||||
|
||||
|
||||
class ArgumentParser(ArgParser):
|
||||
@@ -430,13 +482,15 @@ class ArgumentParser(ArgParser):
|
||||
)
|
||||
|
||||
def _print_message(self, message: str, file: Optional[IO[str]] = None):
|
||||
if message:
|
||||
setattr(self, "_message", getattr(self, "_message", "") + message)
|
||||
if (msg := parser_message.get(None)) is not None:
|
||||
parser_message.set(msg + message)
|
||||
else:
|
||||
super()._print_message(message, file)
|
||||
|
||||
def exit(self, status: int = 0, message: Optional[str] = None):
|
||||
if message:
|
||||
self._print_message(message)
|
||||
raise ParserExit(status=status, message=getattr(self, "_message", None))
|
||||
raise ParserExit(status=status, message=parser_message.get(None))
|
||||
|
||||
|
||||
class ShellCommandRule:
|
||||
@@ -483,13 +537,16 @@ class ShellCommandRule:
|
||||
)
|
||||
|
||||
if self.parser:
|
||||
t = parser_message.set("")
|
||||
try:
|
||||
args = self.parser.parse_args(state[SHELL_ARGV])
|
||||
state[SHELL_ARGS] = args
|
||||
except ArgumentError as e:
|
||||
except ArgumentError as e: # pragma: py-gte-39
|
||||
state[SHELL_ARGS] = ParserExit(status=2, message=str(e))
|
||||
except ParserExit as e:
|
||||
state[SHELL_ARGS] = e
|
||||
finally:
|
||||
parser_message.reset(t)
|
||||
return True
|
||||
|
||||
|
||||
@@ -584,14 +641,13 @@ class RegexRule:
|
||||
return hash((self.regex, self.flags))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
try:
|
||||
msg = event.get_message()
|
||||
except Exception:
|
||||
return False
|
||||
if matched := re.search(self.regex, str(msg), self.flags):
|
||||
state[REGEX_MATCHED] = matched.group()
|
||||
state[REGEX_STR] = matched.group()
|
||||
state[REGEX_GROUP] = matched.groups()
|
||||
state[REGEX_DICT] = matched.groupdict()
|
||||
return True
|
||||
@@ -602,7 +658,7 @@ class RegexRule:
|
||||
def regex(regex: str, flags: Union[int, re.RegexFlag] = 0) -> Rule:
|
||||
"""匹配符合正则表达式的消息字符串。
|
||||
|
||||
可以通过 {ref}`nonebot.params.RegexMatched` 获取匹配成功的字符串,
|
||||
可以通过 {ref}`nonebot.params.RegexStr` 获取匹配成功的字符串,
|
||||
通过 {ref}`nonebot.params.RegexGroup` 获取匹配成功的 group 元组,
|
||||
通过 {ref}`nonebot.params.RegexDict` 获取匹配成功的 group 字典。
|
||||
|
||||
|
@@ -9,6 +9,7 @@ import re
|
||||
import json
|
||||
import asyncio
|
||||
import inspect
|
||||
import importlib
|
||||
import dataclasses
|
||||
from pathlib import Path
|
||||
from functools import wraps, partial
|
||||
@@ -167,13 +168,30 @@ def get_name(obj: Any) -> str:
|
||||
|
||||
|
||||
def path_to_module_name(path: Path) -> str:
|
||||
rel_path = path.resolve().relative_to(Path(".").resolve())
|
||||
"""转换路径为模块名"""
|
||||
rel_path = path.resolve().relative_to(Path.cwd().resolve())
|
||||
if rel_path.stem == "__init__":
|
||||
return ".".join(rel_path.parts[:-1])
|
||||
else:
|
||||
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
|
||||
|
||||
|
||||
def resolve_dot_notation(
|
||||
obj_str: str, default_attr: str, default_prefix: Optional[str] = None
|
||||
) -> Any:
|
||||
"""解析并导入点分表示法的对象"""
|
||||
modulename, _, cls = obj_str.partition(":")
|
||||
if default_prefix is not None and modulename.startswith("~"):
|
||||
modulename = default_prefix + modulename[1:]
|
||||
module = importlib.import_module(modulename)
|
||||
if not cls:
|
||||
return getattr(module, default_attr)
|
||||
instance = module
|
||||
for attr_str in cls.split("."):
|
||||
instance = getattr(instance, attr_str)
|
||||
return instance
|
||||
|
||||
|
||||
class DataclassEncoder(json.JSONEncoder):
|
||||
"""在JSON序列化 {re}`nonebot.adapters._message.Message` (List[Dataclass]) 时使用的 `JSONEncoder`"""
|
||||
|
||||
|
@@ -11,7 +11,7 @@
|
||||
"start": "yarn workspace nonebot start",
|
||||
"serve": "yarn workspace nonebot serve",
|
||||
"clear": "yarn workspace nonebot clear",
|
||||
"prettier": "prettier --config ./.prettierrc --write \"./website/**/*.md\""
|
||||
"prettier": "prettier --config ./.prettierrc --write \"./website/\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
|
3345
poetry.lock
generated
3345
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "nonebot2"
|
||||
version = "2.0.0-rc.1"
|
||||
version = "2.0.0rc4"
|
||||
description = "An asynchronous python bot framework."
|
||||
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||
license = "MIT"
|
||||
@@ -26,37 +26,45 @@ python = "^3.8"
|
||||
yarl = "^1.7.2"
|
||||
loguru = "^0.6.0"
|
||||
pygtrie = "^2.4.1"
|
||||
fastapi = "^0.79.0"
|
||||
tomlkit = ">=0.10.0,<1.0.0"
|
||||
typing-extensions = ">=3.10.0,<5.0.0"
|
||||
Quart = { version = "^0.17.0", optional = true }
|
||||
websockets = { version="^10.0", optional = true }
|
||||
pydantic = { version = "~1.9.0", extras = ["dotenv"] }
|
||||
uvicorn = { version = "^0.18.0", extras = ["standard"] }
|
||||
aiohttp = { version = "^3.7.4", extras = ["speedups"], optional = true }
|
||||
httpx = { version = ">=0.20.0, <1.0.0", extras = ["http2"], optional = true }
|
||||
typing-extensions = ">=4.0.0,<5.0.0"
|
||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||
pydantic = { version = "^1.10.0", extras = ["dotenv"] }
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
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 }
|
||||
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]
|
||||
pycln = "^2.1.2"
|
||||
isort = "^5.10.1"
|
||||
black = "^22.1.0"
|
||||
black = "^23.1.0"
|
||||
nonemoji = "^0.1.2"
|
||||
pytest-cov = "^3.0.0"
|
||||
pre-commit = "^2.16.0"
|
||||
pytest-xdist = "^2.5.0"
|
||||
pytest-asyncio = "^0.19.0"
|
||||
nonebug = { git = "https://github.com/nonebot/nonebug.git" }
|
||||
nb-autodoc = { git = "https://github.com/nonebot/nb-autodoc.git" }
|
||||
pre-commit = "^3.0.0"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
nonebug = "^0.3.0"
|
||||
pytest-cov = "^4.0.0"
|
||||
pytest-xdist = "^3.0.2"
|
||||
pytest-asyncio = "^0.21.0"
|
||||
coverage-conditional-plugin = "^0.8.0"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
nb-autodoc = "^1.0.0a5"
|
||||
|
||||
[tool.poetry.extras]
|
||||
quart = ["quart"]
|
||||
httpx = ["httpx"]
|
||||
aiohttp = ["aiohttp"]
|
||||
websockets = ["websockets"]
|
||||
all = ["quart", "aiohttp", "httpx", "websockets"]
|
||||
quart = ["quart", "uvicorn"]
|
||||
fastapi = ["fastapi", "uvicorn"]
|
||||
all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
addopts = "--cov=nonebot --cov-report=term-missing"
|
||||
addopts = "--cov=nonebot --cov-append --cov-report=term-missing"
|
||||
filterwarnings = [
|
||||
"error",
|
||||
"ignore::DeprecationWarning",
|
||||
@@ -64,7 +72,7 @@ filterwarnings = [
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ["py37", "py38", "py39", "py310"]
|
||||
target-version = ["py38", "py39", "py310", "py311"]
|
||||
include = '\.pyi?$'
|
||||
extend-exclude = '''
|
||||
'''
|
||||
@@ -78,6 +86,20 @@ force_sort_within_sections = true
|
||||
src_paths = ["nonebot", "tests"]
|
||||
extra_standard_library = ["typing_extensions"]
|
||||
|
||||
[tool.pycln]
|
||||
path = "."
|
||||
all = false
|
||||
|
||||
[tool.pyright]
|
||||
reportShadowedImports = false
|
||||
pythonVersion = "3.8"
|
||||
pythonPlatform = "All"
|
||||
executionEnvironments = [
|
||||
{ root = "./tests", extraPaths = ["./"] },
|
||||
{ root = "./" },
|
||||
]
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry_core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
@@ -1,9 +1,25 @@
|
||||
[run]
|
||||
plugins =
|
||||
coverage_conditional_plugin
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
def __repr__
|
||||
def __str__
|
||||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
@(typing\.)?overload
|
||||
if (typing\.)?TYPE_CHECKING( is True)?:
|
||||
@(abc\.)?abstractmethod
|
||||
raise NotImplementedError
|
||||
\.\.\.
|
||||
pass
|
||||
if __name__ == .__main__.:
|
||||
|
||||
[coverage_conditional_plugin]
|
||||
rules =
|
||||
"sys_platform != 'win32'": py-win32
|
||||
"sys_platform != 'linux'": py-linux
|
||||
"sys_platform != 'darwin'": py-darwin
|
||||
"sys_version_info < (3, 9)": py-gte-39
|
||||
"sys_version_info < (3, 11)": py-gte-311
|
||||
"sys_version_info >= (3, 11)": py-lt-311
|
||||
|
@@ -1,2 +1,3 @@
|
||||
ENVIRONMENT=test
|
||||
COMMON_CONFIG=common
|
||||
COMMON_OVERRIDE=old
|
||||
|
@@ -1,5 +1,13 @@
|
||||
LOG_LEVEL=TRACE
|
||||
NICKNAME=["test"]
|
||||
SUPERUSERS=["test", "fake:faketest"]
|
||||
COMMON_OVERRIDE=new
|
||||
CONFIG_FROM_ENV=
|
||||
CONFIG_OVERRIDE=old
|
||||
NESTED_DICT={"a": 1}
|
||||
NESTED_DICT__B=2
|
||||
NESTED_DICT__C__D=3
|
||||
NESTED_MISSING_DICT__A=1
|
||||
NESTED_MISSING_DICT__B__C=2
|
||||
NOT_NESTED=some string
|
||||
NOT_NESTED__A=1
|
||||
|
@@ -1,21 +1,30 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Set
|
||||
|
||||
import pytest
|
||||
from nonebug import NONEBOT_INIT_KWARGS
|
||||
|
||||
import nonebot
|
||||
|
||||
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
|
||||
os.environ["CONFIG_OVERRIDE"] = "new"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def load_plugin(nonebug_init: None) -> Set["Plugin"]:
|
||||
import nonebot
|
||||
def pytest_configure(config: pytest.Config) -> None:
|
||||
config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def load_plugin(nonebug_init: None) -> Set["Plugin"]:
|
||||
# preload global plugins
|
||||
return nonebot.load_plugins(str(Path(__file__).parent / "plugins"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def load_example(nonebug_init: None) -> Set["Plugin"]:
|
||||
import nonebot
|
||||
|
||||
# preload example plugins
|
||||
return nonebot.load_plugins(str(Path(__file__).parent / "examples"))
|
||||
|
0
tests/dynamic/path.py
Normal file
0
tests/dynamic/path.py
Normal file
0
tests/dynamic/require_not_declared.py
Normal file
0
tests/dynamic/require_not_declared.py
Normal file
0
tests/dynamic/require_not_loaded.py
Normal file
0
tests/dynamic/require_not_loaded.py
Normal file
0
tests/dynamic/simple.py
Normal file
0
tests/dynamic/simple.py
Normal file
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"plugins": [],
|
||||
"plugin_dirs": ["plugins"]
|
||||
"plugin_dirs": []
|
||||
}
|
||||
|
@@ -1,3 +1,3 @@
|
||||
[tool.nonebot]
|
||||
plugins = []
|
||||
plugin_dirs = ["plugins"]
|
||||
plugin_dirs = []
|
||||
|
@@ -90,3 +90,6 @@ async def overload(event: FakeEvent):
|
||||
@test_overload.handle()
|
||||
async def finish():
|
||||
await test_overload.finish()
|
||||
|
||||
|
||||
test_destroy = on_message()
|
||||
|
@@ -1 +1 @@
|
||||
from .nested_subplugin2 import a
|
||||
from .nested_subplugin2 import a # nopycln: import
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from nonebot import on_message
|
||||
from nonebot.params import Depends
|
||||
@@ -47,3 +48,17 @@ async def depends_cache(y: int = Depends(dependency, use_cache=True)):
|
||||
|
||||
async def class_depend(c: ClassDependency = Depends()):
|
||||
return c
|
||||
|
||||
|
||||
async def annotated_depend(x: Annotated[int, Depends(dependency)]):
|
||||
return x
|
||||
|
||||
|
||||
async def annotated_class_depend(c: Annotated[ClassDependency, Depends()]):
|
||||
return c
|
||||
|
||||
|
||||
async def annotated_prior_depend(
|
||||
x: Annotated[int, Depends(lambda: 2)] = Depends(dependency)
|
||||
):
|
||||
return x
|
||||
|
@@ -4,14 +4,20 @@ from nonebot.typing import T_State
|
||||
from nonebot.adapters import Message
|
||||
from nonebot.params import (
|
||||
Command,
|
||||
Keyword,
|
||||
Endswith,
|
||||
RegexStr,
|
||||
Fullmatch,
|
||||
RegexDict,
|
||||
CommandArg,
|
||||
RawCommand,
|
||||
RegexGroup,
|
||||
Startswith,
|
||||
CommandStart,
|
||||
RegexMatched,
|
||||
ShellCommandArgs,
|
||||
ShellCommandArgv,
|
||||
CommandWhitespace,
|
||||
)
|
||||
|
||||
|
||||
@@ -43,6 +49,10 @@ async def command_start(start: str = CommandStart()) -> str:
|
||||
return start
|
||||
|
||||
|
||||
async def command_whitespace(whitespace: str = CommandWhitespace()) -> str:
|
||||
return whitespace
|
||||
|
||||
|
||||
async def shell_command_args(
|
||||
shell_command_args: dict = ShellCommandArgs(),
|
||||
) -> dict:
|
||||
@@ -65,3 +75,23 @@ async def regex_group(regex_group: Tuple = RegexGroup()) -> Tuple:
|
||||
|
||||
async def regex_matched(regex_matched: str = RegexMatched()) -> str:
|
||||
return regex_matched
|
||||
|
||||
|
||||
async def regex_str(regex_matched: str = RegexStr()) -> str:
|
||||
return regex_matched
|
||||
|
||||
|
||||
async def startswith(startswith: str = Startswith()) -> str:
|
||||
return startswith
|
||||
|
||||
|
||||
async def endswith(endswith: str = Endswith()) -> str:
|
||||
return endswith
|
||||
|
||||
|
||||
async def fullmatch(fullmatch: str = Fullmatch()) -> str:
|
||||
return fullmatch
|
||||
|
||||
|
||||
async def keyword(keyword: str = Keyword()) -> str:
|
||||
return keyword
|
||||
|
@@ -1,6 +1,8 @@
|
||||
from typing import Type
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot import (
|
||||
CommandGroup,
|
||||
MatcherGroup,
|
||||
@@ -50,6 +52,20 @@ matcher_on = on(
|
||||
)
|
||||
|
||||
|
||||
def matcher_on_factory() -> Type[Matcher]:
|
||||
return on(
|
||||
"test",
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
matcher_on_metaevent = on_metaevent(
|
||||
rule=rule,
|
||||
handlers=[handler],
|
||||
|
@@ -87,7 +87,6 @@ def test_message_add():
|
||||
|
||||
|
||||
def test_message_getitem():
|
||||
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
@@ -102,7 +101,7 @@ def test_message_getitem():
|
||||
|
||||
assert message[0] == MessageSegment.text("test")
|
||||
|
||||
assert message[0:2] == Message(
|
||||
assert message[:2] == Message(
|
||||
[MessageSegment.text("test"), MessageSegment.image("test2")]
|
||||
)
|
||||
|
||||
|
@@ -1,9 +1,8 @@
|
||||
from nonebot.adapters import MessageTemplate
|
||||
from utils import escape_text, make_fake_message
|
||||
|
||||
|
||||
def test_template_basis():
|
||||
from nonebot.adapters import MessageTemplate
|
||||
|
||||
template = MessageTemplate("{key:.3%}")
|
||||
formatted = template.format(key=0.123456789)
|
||||
assert formatted == "12.346%"
|
||||
|
@@ -1,32 +1,86 @@
|
||||
from typing import cast
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Any, Set, cast
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
import nonebot
|
||||
from nonebot.config import Env
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.params import Depends
|
||||
from nonebot import _resolve_combine_expr
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers._lifespan import Lifespan
|
||||
from nonebot.drivers import (
|
||||
URL,
|
||||
Driver,
|
||||
Request,
|
||||
Response,
|
||||
WebSocket,
|
||||
ForwardDriver,
|
||||
ReverseDriver,
|
||||
HTTPServerSetup,
|
||||
WebSocketServerSetup,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="driver")
|
||||
def load_driver(request: pytest.FixtureRequest) -> Driver:
|
||||
driver_name = getattr(request, "param", None)
|
||||
global_driver = nonebot.get_driver()
|
||||
if driver_name is None:
|
||||
return global_driver
|
||||
|
||||
DriverClass = _resolve_combine_expr(driver_name)
|
||||
return DriverClass(Env(environment=global_driver.env), global_driver.config)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifespan():
|
||||
lifespan = Lifespan()
|
||||
|
||||
start_log = []
|
||||
shutdown_log = []
|
||||
|
||||
@lifespan.on_startup
|
||||
async def _startup1():
|
||||
assert start_log == []
|
||||
start_log.append(1)
|
||||
|
||||
@lifespan.on_startup
|
||||
async def _startup2():
|
||||
assert start_log == [1]
|
||||
start_log.append(2)
|
||||
|
||||
@lifespan.on_shutdown
|
||||
async def _shutdown1():
|
||||
assert shutdown_log == []
|
||||
shutdown_log.append(1)
|
||||
|
||||
@lifespan.on_shutdown
|
||||
async def _shutdown2():
|
||||
assert shutdown_log == [1]
|
||||
shutdown_log.append(2)
|
||||
|
||||
async with lifespan:
|
||||
assert start_log == [1, 2]
|
||||
|
||||
assert shutdown_log == [1, 2]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"nonebug_init",
|
||||
"driver",
|
||||
[
|
||||
pytest.param({"driver": "nonebot.drivers.fastapi:Driver"}, id="fastapi"),
|
||||
pytest.param({"driver": "nonebot.drivers.quart:Driver"}, id="quart"),
|
||||
pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"),
|
||||
pytest.param("nonebot.drivers.quart:Driver", id="quart"),
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_reverse_driver(app: App):
|
||||
import nonebot
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers import (
|
||||
URL,
|
||||
Request,
|
||||
Response,
|
||||
WebSocket,
|
||||
ReverseDriver,
|
||||
HTTPServerSetup,
|
||||
WebSocketServerSetup,
|
||||
)
|
||||
|
||||
driver = cast(ReverseDriver, nonebot.get_driver())
|
||||
async def test_reverse_driver(app: App, driver: Driver):
|
||||
driver = cast(ReverseDriver, driver)
|
||||
|
||||
async def _handle_http(request: Request) -> Response:
|
||||
assert request.content in (b"test", "test")
|
||||
@@ -59,7 +113,7 @@ async def test_reverse_driver(app: App):
|
||||
ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws)
|
||||
driver.setup_websocket_server(ws_setup)
|
||||
|
||||
async with app.test_server() as ctx:
|
||||
async with app.test_server(driver.asgi) as ctx:
|
||||
client = ctx.get_client()
|
||||
response = await client.post("/http_test", data="test")
|
||||
assert response.status_code == 200
|
||||
@@ -79,26 +133,142 @@ async def test_reverse_driver(app: App):
|
||||
|
||||
await ws.close()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"nonebug_init, driver_type",
|
||||
"driver",
|
||||
[
|
||||
pytest.param("nonebot.drivers.httpx:Driver", id="httpx"),
|
||||
pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"),
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_http_driver(driver: Driver):
|
||||
driver = cast(ForwardDriver, driver)
|
||||
|
||||
request = Request(
|
||||
"POST",
|
||||
"https://httpbin.org/post",
|
||||
params={"param": "test"},
|
||||
headers={"X-Test": "test"},
|
||||
cookies={"session": "test"},
|
||||
content="test",
|
||||
)
|
||||
response = await driver.request(request)
|
||||
assert response.status_code == 200 and response.content
|
||||
data = json.loads(response.content)
|
||||
assert data["args"] == {"param": "test"}
|
||||
assert data["headers"].get("X-Test") == "test"
|
||||
assert data["headers"].get("Cookie") == "session=test"
|
||||
assert data["data"] == "test"
|
||||
|
||||
request = Request("POST", "https://httpbin.org/post", data={"form": "test"})
|
||||
response = await driver.request(request)
|
||||
assert response.status_code == 200 and response.content
|
||||
data = json.loads(response.content)
|
||||
assert data["form"] == {"form": "test"}
|
||||
|
||||
request = Request("POST", "https://httpbin.org/post", json={"json": "test"})
|
||||
response = await driver.request(request)
|
||||
assert response.status_code == 200 and response.content
|
||||
data = json.loads(response.content)
|
||||
assert data["json"] == {"json": "test"}
|
||||
|
||||
request = Request(
|
||||
"POST", "https://httpbin.org/post", files={"test": ("test.txt", b"test")}
|
||||
)
|
||||
response = await driver.request(request)
|
||||
assert response.status_code == 200 and response.content
|
||||
data = json.loads(response.content)
|
||||
assert data["files"] == {"test": "test"}
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver, driver_type",
|
||||
[
|
||||
pytest.param(
|
||||
{"driver": "nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin"},
|
||||
"nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin",
|
||||
"fastapi+aiohttp",
|
||||
id="fastapi+aiohttp",
|
||||
),
|
||||
pytest.param(
|
||||
{"driver": "~httpx:Driver+~websockets"},
|
||||
"block_driver+httpx+websockets",
|
||||
"~httpx:Driver+~websockets",
|
||||
"none+httpx+websockets",
|
||||
id="httpx+websockets",
|
||||
),
|
||||
],
|
||||
indirect=["nonebug_init"],
|
||||
indirect=["driver"],
|
||||
)
|
||||
async def test_combine_driver(app: App, driver_type: str):
|
||||
import nonebot
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
async def test_combine_driver(driver: Driver, driver_type: str):
|
||||
assert driver.type == driver_type
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot_connect_hook(app: App, driver: Driver):
|
||||
with pytest.MonkeyPatch.context() as m:
|
||||
conn_hooks: Set[Dependent[Any]] = set()
|
||||
disconn_hooks: Set[Dependent[Any]] = set()
|
||||
m.setattr(Driver, "_bot_connection_hook", conn_hooks)
|
||||
m.setattr(Driver, "_bot_disconnection_hook", disconn_hooks)
|
||||
|
||||
conn_should_be_called = False
|
||||
disconn_should_be_called = False
|
||||
dependency_should_be_run = False
|
||||
dependency_should_be_cleaned = False
|
||||
|
||||
async def dependency():
|
||||
nonlocal dependency_should_be_run, dependency_should_be_cleaned
|
||||
dependency_should_be_run = True
|
||||
try:
|
||||
yield 1
|
||||
finally:
|
||||
dependency_should_be_cleaned = True
|
||||
|
||||
@driver.on_bot_connect
|
||||
async def conn_hook(foo: Bot, dep: int = Depends(dependency), default: int = 1):
|
||||
nonlocal conn_should_be_called
|
||||
conn_should_be_called = True
|
||||
|
||||
if foo is not bot:
|
||||
pytest.fail("on_bot_connect hook called with wrong bot")
|
||||
if dep != 1:
|
||||
pytest.fail("on_bot_connect hook called with wrong dependency")
|
||||
if default != 1:
|
||||
pytest.fail("on_bot_connect hook called with wrong default value")
|
||||
|
||||
@driver.on_bot_disconnect
|
||||
async def disconn_hook(
|
||||
foo: Bot, dep: int = Depends(dependency), default: int = 1
|
||||
):
|
||||
nonlocal disconn_should_be_called
|
||||
disconn_should_be_called = True
|
||||
|
||||
if foo is not bot:
|
||||
pytest.fail("on_bot_disconnect hook called with wrong bot")
|
||||
if dep != 1:
|
||||
pytest.fail("on_bot_connect hook called with wrong dependency")
|
||||
if default != 1:
|
||||
pytest.fail("on_bot_connect hook called with wrong default value")
|
||||
|
||||
if conn_hook not in {hook.call for hook in conn_hooks}:
|
||||
pytest.fail("on_bot_connect hook not registered")
|
||||
if disconn_hook not in {hook.call for hook in disconn_hooks}:
|
||||
pytest.fail("on_bot_disconnect hook not registered")
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
if not conn_should_be_called:
|
||||
pytest.fail("on_bot_connect hook not called")
|
||||
if not disconn_should_be_called:
|
||||
pytest.fail("on_bot_disconnect hook not called")
|
||||
if not dependency_should_be_run:
|
||||
pytest.fail("dependency not run")
|
||||
if not dependency_should_be_cleaned:
|
||||
pytest.fail("dependency not cleaned")
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from utils import make_fake_event, make_fake_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_weather(app: App, load_example):
|
||||
async def test_weather(app: App):
|
||||
from examples.weather import weather
|
||||
from utils import make_fake_event, make_fake_message
|
||||
|
||||
# 将此处的 make_fake_message() 替换为你要发送的平台消息 Message 类型
|
||||
# from nonebot.adapters.console import Message
|
||||
Message = make_fake_message()
|
||||
|
||||
async with app.test_matcher(weather) as ctx:
|
||||
@@ -15,6 +17,8 @@ async def test_weather(app: App, load_example):
|
||||
|
||||
msg = Message("/天气 上海")
|
||||
# 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型
|
||||
# from nonebot.adapters.console import MessageEvent
|
||||
# event = MessageEvent(message=msg, to_me=True, ...)
|
||||
event = make_fake_event(_message=msg, _to_me=True)()
|
||||
|
||||
ctx.receive_event(bot, event)
|
||||
|
@@ -1,52 +1,59 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
|
||||
os.environ["CONFIG_OVERRIDE"] = "new"
|
||||
import nonebot
|
||||
from nonebot.drivers import Driver, ReverseDriver
|
||||
from nonebot import (
|
||||
get_app,
|
||||
get_bot,
|
||||
get_asgi,
|
||||
get_bots,
|
||||
get_driver,
|
||||
get_adapter,
|
||||
get_adapters,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"nonebug_init",
|
||||
[
|
||||
{
|
||||
"config_from_init": "init",
|
||||
"driver": "~fastapi+~httpx+~websockets",
|
||||
},
|
||||
{"config_from_init": "init", "driver": "~fastapi+~aiohttp"},
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_init(nonebug_init):
|
||||
from nonebot import get_driver
|
||||
|
||||
env = get_driver().env
|
||||
async def test_init():
|
||||
env = nonebot.get_driver().env
|
||||
assert env == "test"
|
||||
|
||||
config = get_driver().config
|
||||
config = nonebot.get_driver().config
|
||||
assert config.config_from_env == {"test": "test"}
|
||||
assert config.config_override == "new"
|
||||
assert config.config_from_init == "init"
|
||||
assert config.common_config == "common"
|
||||
assert config.common_override == "new"
|
||||
assert config.nested_dict == {"a": 1, "b": 2, "c": {"d": 3}}
|
||||
assert config.nested_missing_dict == {"a": 1, "b": {"c": 2}}
|
||||
assert config.not_nested == "some string"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get(monkeypatch: pytest.MonkeyPatch, nonebug_clear):
|
||||
import nonebot
|
||||
from nonebot.drivers import ForwardDriver, ReverseDriver
|
||||
from nonebot import get_app, get_bot, get_asgi, get_bots, get_driver
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
get_driver()
|
||||
|
||||
nonebot.init(driver="nonebot.drivers.fastapi")
|
||||
async def test_get(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(nonebot, "_driver", None)
|
||||
with pytest.raises(ValueError):
|
||||
get_driver()
|
||||
|
||||
driver = get_driver()
|
||||
assert isinstance(driver, ReverseDriver)
|
||||
assert get_asgi() == driver.asgi
|
||||
assert get_app() == driver.server_app
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
adapter = ctx.create_adapter()
|
||||
adapter_name = adapter.get_name()
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(Driver, "_adapters", {adapter_name: adapter})
|
||||
assert get_adapters() == {adapter_name: adapter}
|
||||
assert get_adapter(adapter_name) is adapter
|
||||
assert get_adapter(adapter.__class__) is adapter
|
||||
with pytest.raises(ValueError):
|
||||
get_adapter("not exist")
|
||||
|
||||
runned = False
|
||||
|
||||
def mock_run(*args, **kwargs):
|
||||
|
@@ -1,11 +1,14 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot.permission import User
|
||||
from nonebot.message import _check_matcher
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
from utils import make_fake_event, make_fake_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher(app: App, load_plugin):
|
||||
async def test_matcher(app: App):
|
||||
from plugins.matcher.matcher_process import (
|
||||
test_got,
|
||||
test_handle,
|
||||
@@ -77,7 +80,21 @@ async def test_matcher(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_type_updater(app: App, load_plugin):
|
||||
async def test_matcher_destroy(app: App):
|
||||
from plugins.matcher.matcher_process import test_destroy
|
||||
|
||||
async with app.test_matcher(test_destroy) as ctx:
|
||||
assert len(matchers) == 1
|
||||
assert len(matchers[test_destroy.priority]) == 1
|
||||
assert matchers[test_destroy.priority][0] is test_destroy
|
||||
|
||||
test_destroy.destroy()
|
||||
|
||||
assert len(matchers[test_destroy.priority]) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_type_updater(app: App):
|
||||
from plugins.matcher.matcher_type import test_type_updater, test_custom_updater
|
||||
|
||||
event = make_fake_event()()
|
||||
@@ -98,8 +115,7 @@ async def test_type_updater(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permission_updater(app: App, load_plugin):
|
||||
from nonebot.permission import User
|
||||
async def test_permission_updater(app: App):
|
||||
from plugins.matcher.matcher_permission import (
|
||||
default_permission,
|
||||
test_custom_updater,
|
||||
@@ -143,40 +159,37 @@ async def test_permission_updater(app: App, load_plugin):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run(app: App):
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
with app.provider.context({}):
|
||||
assert not matchers
|
||||
event = make_fake_event()()
|
||||
|
||||
assert not matchers
|
||||
event = make_fake_event()()
|
||||
async def reject():
|
||||
await Matcher.reject()
|
||||
|
||||
async def reject():
|
||||
await Matcher.reject()
|
||||
test_reject = Matcher.new(handlers=[reject])
|
||||
|
||||
test_reject = Matcher.new(handlers=[reject])
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
await test_reject().run(bot, event, {})
|
||||
assert len(matchers[0]) == 1
|
||||
assert len(matchers[0][0].handlers) == 1
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
await test_reject().run(bot, event, {})
|
||||
assert len(matchers[0]) == 1
|
||||
assert len(matchers[0][0].handlers) == 1
|
||||
del matchers[0]
|
||||
|
||||
del matchers[0]
|
||||
async def pause():
|
||||
await Matcher.pause()
|
||||
|
||||
async def pause():
|
||||
await Matcher.pause()
|
||||
test_pause = Matcher.new(handlers=[pause])
|
||||
|
||||
test_pause = Matcher.new(handlers=[pause])
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
await test_pause().run(bot, event, {})
|
||||
assert len(matchers[0]) == 1
|
||||
assert len(matchers[0][0].handlers) == 0
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
await test_pause().run(bot, event, {})
|
||||
assert len(matchers[0]) == 1
|
||||
assert len(matchers[0][0].handlers) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expire(app: App, load_plugin):
|
||||
from nonebot.matcher import matchers
|
||||
from nonebot.message import _check_matcher
|
||||
async def test_expire(app: App):
|
||||
from plugins.matcher.matcher_expire import (
|
||||
test_temp_matcher,
|
||||
test_datetime_matcher,
|
||||
@@ -187,25 +200,19 @@ async def test_expire(app: App, load_plugin):
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert test_temp_matcher in matchers[test_temp_matcher.priority]
|
||||
await _check_matcher(
|
||||
test_temp_matcher.priority, test_temp_matcher, bot, event, {}
|
||||
)
|
||||
await _check_matcher(test_temp_matcher, bot, event, {})
|
||||
assert test_temp_matcher not in matchers[test_temp_matcher.priority]
|
||||
|
||||
event = make_fake_event()()
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert test_datetime_matcher in matchers[test_datetime_matcher.priority]
|
||||
await _check_matcher(
|
||||
test_datetime_matcher.priority, test_datetime_matcher, bot, event, {}
|
||||
)
|
||||
await _check_matcher(test_datetime_matcher, bot, event, {})
|
||||
assert test_datetime_matcher not in matchers[test_datetime_matcher.priority]
|
||||
|
||||
event = make_fake_event()()
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert test_timedelta_matcher in matchers[test_timedelta_matcher.priority]
|
||||
await _check_matcher(
|
||||
test_timedelta_matcher.priority, test_timedelta_matcher, bot, event, {}
|
||||
)
|
||||
await _check_matcher(test_timedelta_matcher, bot, event, {})
|
||||
assert test_timedelta_matcher not in matchers[test_timedelta_matcher.priority]
|
14
tests/test_matcher/test_provider.py
Normal file
14
tests/test_matcher/test_provider.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_manager(app: App):
|
||||
try:
|
||||
default_provider = matchers.provider
|
||||
matchers.set_provider(DEFAULT_PROVIDER_CLASS)
|
||||
assert default_provider == matchers.provider
|
||||
finally:
|
||||
matchers.provider = app.provider
|
@@ -1,18 +1,50 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.exception import TypeMisMatch
|
||||
from utils import make_fake_event, make_fake_message
|
||||
from nonebot.params import (
|
||||
ArgParam,
|
||||
BotParam,
|
||||
EventParam,
|
||||
StateParam,
|
||||
DependParam,
|
||||
DefaultParam,
|
||||
MatcherParam,
|
||||
ExceptionParam,
|
||||
)
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
REGEX_STR,
|
||||
PREFIX_KEY,
|
||||
REGEX_DICT,
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
ENDSWITH_KEY,
|
||||
CMD_START_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_depend(app: App, load_plugin):
|
||||
from nonebot.params import DependParam
|
||||
async def test_depend(app: App):
|
||||
from plugins.param.param_depend import (
|
||||
ClassDependency,
|
||||
runned,
|
||||
depends,
|
||||
class_depend,
|
||||
test_depends,
|
||||
annotated_depend,
|
||||
annotated_class_depend,
|
||||
annotated_prior_depend,
|
||||
)
|
||||
|
||||
async with app.test_dependent(depends, allow_types=[DependParam]) as ctx:
|
||||
@@ -29,14 +61,28 @@ async def test_depend(app: App, load_plugin):
|
||||
|
||||
assert len(runned) == 2 and runned[0] == runned[1] == 1
|
||||
|
||||
runned.clear()
|
||||
|
||||
async with app.test_dependent(class_depend, allow_types=[DependParam]) as ctx:
|
||||
ctx.should_return(ClassDependency(x=1, y=2))
|
||||
|
||||
async with app.test_dependent(annotated_depend, allow_types=[DependParam]) as ctx:
|
||||
ctx.should_return(1)
|
||||
|
||||
async with app.test_dependent(
|
||||
annotated_prior_depend, allow_types=[DependParam]
|
||||
) as ctx:
|
||||
ctx.should_return(1)
|
||||
assert runned == [1, 1]
|
||||
|
||||
async with app.test_dependent(
|
||||
annotated_class_depend, allow_types=[DependParam]
|
||||
) as ctx:
|
||||
ctx.should_return(ClassDependency(x=1, y=2))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot(app: App, load_plugin):
|
||||
from nonebot.params import BotParam
|
||||
from nonebot.exception import TypeMisMatch
|
||||
async def test_bot(app: App):
|
||||
from plugins.param.param_bot import (
|
||||
FooBot,
|
||||
get_bot,
|
||||
@@ -82,9 +128,7 @@ async def test_bot(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_event(app: App, load_plugin):
|
||||
from nonebot.exception import TypeMisMatch
|
||||
from nonebot.params import EventParam, DependParam
|
||||
async def test_event(app: App):
|
||||
from plugins.param.param_event import (
|
||||
FooEvent,
|
||||
event,
|
||||
@@ -159,24 +203,16 @@ async def test_event(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state(app: App, load_plugin):
|
||||
from nonebot.params import StateParam, DependParam
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
PREFIX_KEY,
|
||||
REGEX_DICT,
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
CMD_START_KEY,
|
||||
REGEX_MATCHED,
|
||||
)
|
||||
async def test_state(app: App):
|
||||
from plugins.param.param_state import (
|
||||
state,
|
||||
command,
|
||||
keyword,
|
||||
endswith,
|
||||
fullmatch,
|
||||
regex_str,
|
||||
regex_dict,
|
||||
startswith,
|
||||
command_arg,
|
||||
raw_command,
|
||||
regex_group,
|
||||
@@ -184,6 +220,7 @@ async def test_state(app: App, load_plugin):
|
||||
command_start,
|
||||
regex_matched,
|
||||
not_legacy_state,
|
||||
command_whitespace,
|
||||
shell_command_args,
|
||||
shell_command_argv,
|
||||
)
|
||||
@@ -195,12 +232,18 @@ async def test_state(app: App, load_plugin):
|
||||
RAW_CMD_KEY: "/cmd",
|
||||
CMD_START_KEY: "/",
|
||||
CMD_ARG_KEY: fake_message,
|
||||
CMD_WHITESPACE_KEY: " ",
|
||||
},
|
||||
SHELL_ARGV: ["-h"],
|
||||
SHELL_ARGS: {"help": True},
|
||||
REGEX_MATCHED: "[cq:test,arg=value]",
|
||||
REGEX_STR: "[cq:test,arg=value]",
|
||||
REGEX_GROUP: ("test", "arg=value"),
|
||||
REGEX_DICT: {"type": "test", "arg": "value"},
|
||||
STARTSWITH_KEY: "startswith",
|
||||
ENDSWITH_KEY: "endswith",
|
||||
FULLMATCH_KEY: "fullmatch",
|
||||
KEYWORD_KEY: "keyword",
|
||||
}
|
||||
|
||||
async with app.test_dependent(state, allow_types=[StateParam]) as ctx:
|
||||
@@ -241,6 +284,12 @@ async def test_state(app: App, load_plugin):
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[PREFIX_KEY][CMD_START_KEY])
|
||||
|
||||
async with app.test_dependent(
|
||||
command_whitespace, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[PREFIX_KEY][CMD_WHITESPACE_KEY])
|
||||
|
||||
async with app.test_dependent(
|
||||
shell_command_argv, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
@@ -259,6 +308,12 @@ async def test_state(app: App, load_plugin):
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[REGEX_MATCHED])
|
||||
|
||||
async with app.test_dependent(
|
||||
regex_str, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[REGEX_STR])
|
||||
|
||||
async with app.test_dependent(
|
||||
regex_group, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
@@ -271,11 +326,33 @@ async def test_state(app: App, load_plugin):
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[REGEX_DICT])
|
||||
|
||||
async with app.test_dependent(
|
||||
startswith, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[STARTSWITH_KEY])
|
||||
|
||||
async with app.test_dependent(
|
||||
endswith, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[ENDSWITH_KEY])
|
||||
|
||||
async with app.test_dependent(
|
||||
fullmatch, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[FULLMATCH_KEY])
|
||||
|
||||
async with app.test_dependent(
|
||||
keyword, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[KEYWORD_KEY])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher(app: App, load_plugin):
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.params import DependParam, MatcherParam
|
||||
async def test_matcher(app: App):
|
||||
from plugins.param.param_matcher import matcher, receive, last_receive
|
||||
|
||||
fake_matcher = Matcher()
|
||||
@@ -303,9 +380,7 @@ async def test_matcher(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_arg(app: App, load_plugin):
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.params import ArgParam
|
||||
async def test_arg(app: App):
|
||||
from plugins.param.param_arg import arg, arg_str, arg_plain_text
|
||||
|
||||
matcher = Matcher()
|
||||
@@ -326,8 +401,7 @@ async def test_arg(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exception(app: App, load_plugin):
|
||||
from nonebot.params import ExceptionParam
|
||||
async def test_exception(app: App):
|
||||
from plugins.param.param_exception import exc
|
||||
|
||||
exception = ValueError("test")
|
||||
@@ -337,8 +411,7 @@ async def test_exception(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default(app: App, load_plugin):
|
||||
from nonebot.params import DefaultParam
|
||||
async def test_default(app: App):
|
||||
from plugins.param.param_default import default
|
||||
|
||||
async with app.test_dependent(default, allow_types=[DefaultParam]) as ctx:
|
||||
|
@@ -4,13 +4,26 @@ import pytest
|
||||
from nonebug import App
|
||||
|
||||
from utils import make_fake_event
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.permission import (
|
||||
USER,
|
||||
NOTICE,
|
||||
MESSAGE,
|
||||
REQUEST,
|
||||
METAEVENT,
|
||||
SUPERUSER,
|
||||
User,
|
||||
Notice,
|
||||
Message,
|
||||
Request,
|
||||
MetaEvent,
|
||||
SuperUser,
|
||||
Permission,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permission(app: App):
|
||||
from nonebot.permission import Permission
|
||||
from nonebot.exception import SkippedException
|
||||
|
||||
async def falsy():
|
||||
return False
|
||||
|
||||
@@ -42,20 +55,8 @@ async def test_permission(app: App):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"type,expected",
|
||||
[
|
||||
("message", True),
|
||||
("notice", False),
|
||||
],
|
||||
)
|
||||
async def test_message(
|
||||
app: App,
|
||||
type: str,
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.permission import MESSAGE, Message
|
||||
|
||||
@pytest.mark.parametrize("type, expected", [("message", True), ("notice", False)])
|
||||
async def test_message(type: str, expected: bool):
|
||||
dependent = list(MESSAGE.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
@@ -66,20 +67,8 @@ async def test_message(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"type,expected",
|
||||
[
|
||||
("message", False),
|
||||
("notice", True),
|
||||
],
|
||||
)
|
||||
async def test_notice(
|
||||
app: App,
|
||||
type: str,
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.permission import NOTICE, Notice
|
||||
|
||||
@pytest.mark.parametrize("type, expected", [("message", False), ("notice", True)])
|
||||
async def test_notice(type: str, expected: bool):
|
||||
dependent = list(NOTICE.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
@@ -90,20 +79,8 @@ async def test_notice(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"type,expected",
|
||||
[
|
||||
("message", False),
|
||||
("request", True),
|
||||
],
|
||||
)
|
||||
async def test_request(
|
||||
app: App,
|
||||
type: str,
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.permission import REQUEST, Request
|
||||
|
||||
@pytest.mark.parametrize("type, expected", [("message", False), ("request", True)])
|
||||
async def test_request(type: str, expected: bool):
|
||||
dependent = list(REQUEST.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
@@ -114,20 +91,8 @@ async def test_request(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"type,expected",
|
||||
[
|
||||
("message", False),
|
||||
("meta_event", True),
|
||||
],
|
||||
)
|
||||
async def test_metaevent(
|
||||
app: App,
|
||||
type: str,
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.permission import METAEVENT, MetaEvent
|
||||
|
||||
@pytest.mark.parametrize("type, expected", [("message", False), ("meta_event", True)])
|
||||
async def test_metaevent(type: str, expected: bool):
|
||||
dependent = list(METAEVENT.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
@@ -139,7 +104,7 @@ async def test_metaevent(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"type,user_id,expected",
|
||||
"type, user_id, expected",
|
||||
[
|
||||
("message", "test", True),
|
||||
("message", "foo", False),
|
||||
@@ -148,14 +113,7 @@ async def test_metaevent(
|
||||
("notice", "test", True),
|
||||
],
|
||||
)
|
||||
async def test_superuser(
|
||||
app: App,
|
||||
type: str,
|
||||
user_id: str,
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.permission import SUPERUSER, SuperUser
|
||||
|
||||
async def test_superuser(app: App, type: str, user_id: str, expected: bool):
|
||||
dependent = list(SUPERUSER.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
@@ -170,7 +128,7 @@ async def test_superuser(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"session_ids,session_id,expected",
|
||||
"session_ids, session_id, expected",
|
||||
[
|
||||
(("user", "foo"), "user", True),
|
||||
(("user", "foo"), "bar", False),
|
||||
@@ -180,8 +138,6 @@ async def test_superuser(
|
||||
async def test_user(
|
||||
app: App, session_ids: Tuple[str, ...], session_id: Optional[str], expected: bool
|
||||
):
|
||||
from nonebot.permission import USER, User
|
||||
|
||||
dependent = list(USER(*session_ids).checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
|
@@ -1,16 +1,11 @@
|
||||
from typing import TYPE_CHECKING, Set
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager, _managers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
import nonebot
|
||||
|
||||
async def test_get_plugin():
|
||||
# check simple plugin
|
||||
plugin = nonebot.get_plugin("export")
|
||||
assert plugin
|
||||
@@ -28,12 +23,15 @@ async def test_get_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_available_plugin(app: App):
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager, _managers
|
||||
async def test_get_available_plugin():
|
||||
old_managers = _managers.copy()
|
||||
_managers.clear()
|
||||
try:
|
||||
_managers.append(PluginManager(["plugins.export", "plugin.require"]))
|
||||
|
||||
_managers.append(PluginManager(["plugins.export", "plugin.require"]))
|
||||
|
||||
# check get available plugins
|
||||
plugin_names = nonebot.get_available_plugin_names()
|
||||
assert plugin_names == {"export", "require"}
|
||||
# check get available plugins
|
||||
plugin_names = nonebot.get_available_plugin_names()
|
||||
assert plugin_names == {"export", "require"}
|
||||
finally:
|
||||
_managers.clear()
|
||||
_managers.extend(old_managers)
|
||||
|
@@ -1,38 +1,32 @@
|
||||
import sys
|
||||
from typing import Set
|
||||
from pathlib import Path
|
||||
from dataclasses import asdict
|
||||
from typing import TYPE_CHECKING, Set
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
import nonebot
|
||||
from nonebot.plugin import Plugin, PluginManager, _managers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_plugin(app: App):
|
||||
import nonebot
|
||||
|
||||
async def test_load_plugin():
|
||||
# check regular
|
||||
assert nonebot.load_plugin("plugins.metadata")
|
||||
assert nonebot.load_plugin("dynamic.simple")
|
||||
|
||||
# check path
|
||||
assert nonebot.load_plugin(Path("plugins/export"))
|
||||
assert nonebot.load_plugin(Path("dynamic/path.py"))
|
||||
|
||||
# check not found
|
||||
assert nonebot.load_plugin("some_plugin_not_exist") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_plugins(app: App, load_plugin: Set["Plugin"]):
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager
|
||||
|
||||
async def test_load_plugins(load_plugin: Set[Plugin], load_example: Set[Plugin]):
|
||||
loaded_plugins = {
|
||||
plugin for plugin in nonebot.get_loaded_plugins() if not plugin.parent_plugin
|
||||
}
|
||||
assert loaded_plugins == load_plugin
|
||||
assert loaded_plugins >= load_plugin | load_example
|
||||
|
||||
# check simple plugin
|
||||
assert "plugins.export" in sys.modules
|
||||
@@ -51,9 +45,7 @@ async def test_load_plugins(app: App, load_plugin: Set["Plugin"]):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_nested_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
import nonebot
|
||||
|
||||
async def test_load_nested_plugin():
|
||||
parent_plugin = nonebot.get_plugin("nested")
|
||||
sub_plugin = nonebot.get_plugin("nested_subplugin")
|
||||
sub_plugin2 = nonebot.get_plugin("nested_subplugin2")
|
||||
@@ -64,9 +56,7 @@ async def test_load_nested_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_json(app: App):
|
||||
import nonebot
|
||||
|
||||
async def test_load_json():
|
||||
nonebot.load_from_json("./plugins.json")
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
@@ -74,9 +64,7 @@ async def test_load_json(app: App):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_toml(app: App):
|
||||
import nonebot
|
||||
|
||||
async def test_load_toml():
|
||||
nonebot.load_from_toml("./plugins.toml")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
@@ -87,35 +75,27 @@ async def test_load_toml(app: App):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bad_plugin(app: App):
|
||||
import nonebot
|
||||
|
||||
async def test_bad_plugin():
|
||||
nonebot.load_plugins("bad_plugins")
|
||||
|
||||
assert nonebot.get_plugin("bad_plugins") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
import nonebot
|
||||
|
||||
async def test_require_loaded(monkeypatch: pytest.MonkeyPatch):
|
||||
def _patched_find(name: str):
|
||||
assert False
|
||||
pytest.fail("require existing plugin should not call find_manager_by_name")
|
||||
|
||||
monkeypatch.setattr("nonebot.plugin.load._find_manager_by_name", _patched_find)
|
||||
|
||||
nonebot.load_plugin("plugins.export")
|
||||
|
||||
nonebot.require("plugins.export")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_not_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager, _managers
|
||||
|
||||
m = PluginManager(["plugins.export"])
|
||||
async def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch):
|
||||
m = PluginManager(["dynamic.require_not_loaded"])
|
||||
_managers.append(m)
|
||||
num_managers = len(_managers)
|
||||
|
||||
origin_load = PluginManager.load_plugin
|
||||
|
||||
@@ -125,33 +105,29 @@ async def test_require_not_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
|
||||
monkeypatch.setattr(PluginManager, "load_plugin", _patched_load)
|
||||
|
||||
nonebot.require("plugins.export")
|
||||
nonebot.require("dynamic.require_not_loaded")
|
||||
|
||||
assert len(_managers) == 1
|
||||
assert len(_managers) == num_managers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_not_declared(app: App):
|
||||
import nonebot
|
||||
from nonebot.plugin import _managers
|
||||
async def test_require_not_declared():
|
||||
num_managers = len(_managers)
|
||||
|
||||
nonebot.require("plugins.export")
|
||||
nonebot.require("dynamic.require_not_declared")
|
||||
|
||||
assert len(_managers) == 1
|
||||
assert _managers[-1].plugins == {"plugins.export"}
|
||||
assert len(_managers) == num_managers + 1
|
||||
assert _managers[-1].plugins == {"dynamic.require_not_declared"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_not_found(app: App):
|
||||
import nonebot
|
||||
|
||||
async def test_require_not_found():
|
||||
with pytest.raises(RuntimeError):
|
||||
nonebot.require("some_plugin_not_exist")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plugin_metadata(app: App, load_plugin: Set["Plugin"]):
|
||||
import nonebot
|
||||
async def test_plugin_metadata():
|
||||
from plugins.metadata import Config
|
||||
|
||||
plugin = nonebot.get_plugin("metadata")
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot.plugin import PluginManager
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_plugin_name(app: App):
|
||||
from nonebot.plugin import PluginManager
|
||||
|
||||
m = PluginManager(plugins=["plugins.export"])
|
||||
module1 = m.load_plugin("export")
|
||||
module2 = m.load_plugin("plugins.export")
|
||||
async def test_load_plugin_name():
|
||||
m = PluginManager(plugins=["dynamic.manager"])
|
||||
module1 = m.load_plugin("manager")
|
||||
module2 = m.load_plugin("dynamic.manager")
|
||||
assert module1 is module2
|
||||
|
@@ -1,116 +1,149 @@
|
||||
from typing import Type, Optional
|
||||
from typing import Type, Callable, Optional
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
import nonebot
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.typing import T_RuleChecker
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
from nonebot.rule import (
|
||||
RegexRule,
|
||||
IsTypeRule,
|
||||
CommandRule,
|
||||
EndswithRule,
|
||||
KeywordsRule,
|
||||
FullmatchRule,
|
||||
StartswithRule,
|
||||
ShellCommandRule,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on(app: App, load_plugin):
|
||||
import nonebot
|
||||
@pytest.mark.parametrize(
|
||||
"matcher_name, pre_rule_factory, has_permission",
|
||||
[
|
||||
pytest.param("matcher_on", None, True),
|
||||
pytest.param("matcher_on_metaevent", None, False),
|
||||
pytest.param("matcher_on_message", None, True),
|
||||
pytest.param("matcher_on_notice", None, False),
|
||||
pytest.param("matcher_on_request", None, False),
|
||||
pytest.param(
|
||||
"matcher_on_startswith", lambda e: StartswithRule(("test",)), True
|
||||
),
|
||||
pytest.param("matcher_on_endswith", lambda e: EndswithRule(("test",)), True),
|
||||
pytest.param("matcher_on_fullmatch", lambda e: FullmatchRule(("test",)), True),
|
||||
pytest.param("matcher_on_keyword", lambda e: KeywordsRule("test"), True),
|
||||
pytest.param("matcher_on_command", lambda e: CommandRule([("test",)]), True),
|
||||
pytest.param(
|
||||
"matcher_on_shell_command",
|
||||
lambda e: ShellCommandRule([("test",)], None),
|
||||
True,
|
||||
),
|
||||
pytest.param("matcher_on_regex", lambda e: RegexRule("test"), True),
|
||||
pytest.param("matcher_on_type", lambda e: IsTypeRule(e), True),
|
||||
pytest.param("matcher_sub_cmd", lambda e: CommandRule([("test", "sub")]), True),
|
||||
pytest.param(
|
||||
"matcher_sub_shell_cmd",
|
||||
lambda e: ShellCommandRule([("test", "sub")], None),
|
||||
True,
|
||||
),
|
||||
pytest.param("matcher_group_on", None, True),
|
||||
pytest.param("matcher_group_on_metaevent", None, False),
|
||||
pytest.param("matcher_group_on_message", None, True),
|
||||
pytest.param("matcher_group_on_notice", None, False),
|
||||
pytest.param("matcher_group_on_request", None, False),
|
||||
pytest.param(
|
||||
"matcher_group_on_startswith",
|
||||
lambda e: StartswithRule(("test",)),
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"matcher_group_on_endswith",
|
||||
lambda e: EndswithRule(("test",)),
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"matcher_group_on_fullmatch",
|
||||
lambda e: FullmatchRule(("test",)),
|
||||
True,
|
||||
),
|
||||
pytest.param("matcher_group_on_keyword", lambda e: KeywordsRule("test"), True),
|
||||
pytest.param(
|
||||
"matcher_group_on_command",
|
||||
lambda e: CommandRule([("test",)]),
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"matcher_group_on_shell_command",
|
||||
lambda e: ShellCommandRule([("test",)], None),
|
||||
True,
|
||||
),
|
||||
pytest.param("matcher_group_on_regex", lambda e: RegexRule("test"), True),
|
||||
pytest.param("matcher_group_on_type", lambda e: IsTypeRule(e), True),
|
||||
],
|
||||
)
|
||||
async def test_on(
|
||||
matcher_name: str,
|
||||
pre_rule_factory: Optional[Callable[[Type[Event]], T_RuleChecker]],
|
||||
has_permission: bool,
|
||||
):
|
||||
import plugins.plugin.matchers as module
|
||||
from nonebot.typing import T_RuleChecker
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
from nonebot.rule import (
|
||||
RegexRule,
|
||||
IsTypeRule,
|
||||
CommandRule,
|
||||
EndswithRule,
|
||||
KeywordsRule,
|
||||
FullmatchRule,
|
||||
StartswithRule,
|
||||
ShellCommandRule,
|
||||
)
|
||||
from plugins.plugin.matchers import (
|
||||
TestEvent,
|
||||
rule,
|
||||
state,
|
||||
handler,
|
||||
priority,
|
||||
matcher_on,
|
||||
permission,
|
||||
expire_time,
|
||||
matcher_on_type,
|
||||
matcher_sub_cmd,
|
||||
matcher_group_on,
|
||||
matcher_on_regex,
|
||||
matcher_on_notice,
|
||||
matcher_on_command,
|
||||
matcher_on_keyword,
|
||||
matcher_on_message,
|
||||
matcher_on_request,
|
||||
matcher_on_endswith,
|
||||
matcher_on_fullmatch,
|
||||
matcher_on_metaevent,
|
||||
matcher_group_on_type,
|
||||
matcher_on_startswith,
|
||||
matcher_sub_shell_cmd,
|
||||
matcher_group_on_regex,
|
||||
matcher_group_on_notice,
|
||||
matcher_group_on_command,
|
||||
matcher_group_on_keyword,
|
||||
matcher_group_on_message,
|
||||
matcher_group_on_request,
|
||||
matcher_on_shell_command,
|
||||
matcher_group_on_endswith,
|
||||
matcher_group_on_fullmatch,
|
||||
matcher_group_on_metaevent,
|
||||
matcher_group_on_startswith,
|
||||
matcher_group_on_shell_command,
|
||||
)
|
||||
|
||||
matcher = getattr(module, matcher_name)
|
||||
assert issubclass(matcher, Matcher), f"{matcher_name} should be a Matcher"
|
||||
|
||||
pre_rule = pre_rule_factory(TestEvent) if pre_rule_factory else None
|
||||
|
||||
plugin = nonebot.get_plugin("plugin")
|
||||
assert plugin, "plugin should be loaded"
|
||||
|
||||
def _check(
|
||||
matcher: Type[Matcher],
|
||||
pre_rule: Optional[T_RuleChecker],
|
||||
has_permission: bool,
|
||||
):
|
||||
assert {dependent.call for dependent in matcher.rule.checkers} == (
|
||||
{pre_rule, rule} if pre_rule else {rule}
|
||||
)
|
||||
if has_permission:
|
||||
assert {dependent.call for dependent in matcher.permission.checkers} == {
|
||||
permission
|
||||
}
|
||||
else:
|
||||
assert not matcher.permission.checkers
|
||||
assert [dependent.call for dependent in matcher.handlers] == [handler]
|
||||
assert matcher.temp is True
|
||||
assert matcher.expire_time == expire_time
|
||||
assert matcher in matchers[priority]
|
||||
assert matcher.block is True
|
||||
assert matcher._default_state == state
|
||||
assert {dependent.call for dependent in matcher.rule.checkers} == (
|
||||
{pre_rule, rule} if pre_rule else {rule}
|
||||
)
|
||||
if has_permission:
|
||||
assert {dependent.call for dependent in matcher.permission.checkers} == {
|
||||
permission
|
||||
}
|
||||
else:
|
||||
assert not matcher.permission.checkers
|
||||
assert [dependent.call for dependent in matcher.handlers] == [handler]
|
||||
assert matcher.temp is True
|
||||
assert matcher.expire_time == expire_time
|
||||
assert matcher in matchers[priority]
|
||||
assert matcher.block is True
|
||||
assert matcher._default_state == state
|
||||
|
||||
assert matcher.plugin is plugin
|
||||
assert matcher in plugin.matcher
|
||||
assert matcher.module is module
|
||||
assert matcher.plugin_name == "plugin"
|
||||
assert matcher.module_name == "plugins.plugin.matchers"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_on():
|
||||
import plugins.plugin.matchers as module
|
||||
from plugins.plugin.matchers import matcher_on_factory
|
||||
|
||||
matcher = matcher_on_factory()
|
||||
|
||||
plugin = nonebot.get_plugin("plugin")
|
||||
assert plugin, "plugin should be loaded"
|
||||
|
||||
try:
|
||||
assert matcher.plugin is plugin
|
||||
assert matcher not in plugin.matcher
|
||||
assert matcher.module is module
|
||||
assert matcher.plugin_name == "plugin"
|
||||
assert matcher.module_name == "plugins.plugin.matchers"
|
||||
|
||||
_check(matcher_on, None, True)
|
||||
_check(matcher_on_metaevent, None, False)
|
||||
_check(matcher_on_message, None, True)
|
||||
_check(matcher_on_notice, None, False)
|
||||
_check(matcher_on_request, None, False)
|
||||
_check(matcher_on_startswith, StartswithRule(("test",)), True)
|
||||
_check(matcher_on_endswith, EndswithRule(("test",)), True)
|
||||
_check(matcher_on_fullmatch, FullmatchRule(("test",)), True)
|
||||
_check(matcher_on_keyword, KeywordsRule("test"), True)
|
||||
_check(matcher_on_command, CommandRule([("test",)]), True)
|
||||
_check(matcher_on_shell_command, ShellCommandRule([("test",)], None), True)
|
||||
_check(matcher_on_regex, RegexRule("test"), True)
|
||||
_check(matcher_on_type, IsTypeRule(TestEvent), True)
|
||||
_check(matcher_sub_cmd, CommandRule([("test", "sub")]), True)
|
||||
_check(matcher_sub_shell_cmd, ShellCommandRule([("test", "sub")], None), True)
|
||||
_check(matcher_group_on, None, True)
|
||||
_check(matcher_group_on_metaevent, None, False)
|
||||
_check(matcher_group_on_message, None, True)
|
||||
_check(matcher_group_on_notice, None, False)
|
||||
_check(matcher_group_on_request, None, False)
|
||||
_check(matcher_group_on_startswith, StartswithRule(("test",)), True)
|
||||
_check(matcher_group_on_endswith, EndswithRule(("test",)), True)
|
||||
_check(matcher_group_on_fullmatch, FullmatchRule(("test",)), True)
|
||||
_check(matcher_group_on_keyword, KeywordsRule("test"), True)
|
||||
_check(matcher_group_on_command, CommandRule([("test",)]), True)
|
||||
_check(matcher_group_on_shell_command, ShellCommandRule([("test",)], None), True)
|
||||
_check(matcher_group_on_regex, RegexRule("test"), True)
|
||||
_check(matcher_group_on_type, IsTypeRule(TestEvent), True)
|
||||
finally:
|
||||
matcher.destroy()
|
||||
|
@@ -4,14 +4,55 @@ from typing import Dict, Tuple, Union, Optional
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot.typing import T_State
|
||||
from utils import make_fake_event, make_fake_message
|
||||
from nonebot.exception import ParserExit, SkippedException
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
REGEX_STR,
|
||||
PREFIX_KEY,
|
||||
REGEX_DICT,
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
REGEX_GROUP,
|
||||
ENDSWITH_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
)
|
||||
from nonebot.rule import (
|
||||
CMD_RESULT,
|
||||
TRIE_VALUE,
|
||||
Rule,
|
||||
ToMeRule,
|
||||
TrieRule,
|
||||
Namespace,
|
||||
RegexRule,
|
||||
IsTypeRule,
|
||||
CommandRule,
|
||||
EndswithRule,
|
||||
KeywordsRule,
|
||||
FullmatchRule,
|
||||
ArgumentParser,
|
||||
StartswithRule,
|
||||
ShellCommandRule,
|
||||
regex,
|
||||
to_me,
|
||||
command,
|
||||
is_type,
|
||||
keyword,
|
||||
endswith,
|
||||
fullmatch,
|
||||
startswith,
|
||||
shell_command,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rule(app: App):
|
||||
from nonebot.rule import Rule
|
||||
from nonebot.exception import SkippedException
|
||||
|
||||
async def falsy():
|
||||
return False
|
||||
|
||||
@@ -42,9 +83,47 @@ async def test_rule(app: App):
|
||||
assert await Rule(truthy, skipped)(bot, event, {}) == False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trie(app: App):
|
||||
TrieRule.add_prefix("/fake-prefix", TRIE_VALUE("/", ("fake-prefix",)))
|
||||
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
message = Message("/fake-prefix 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=Message("some args"),
|
||||
command_start="/",
|
||||
command_whitespace=" ",
|
||||
)
|
||||
|
||||
message = MessageSegment.text("/fake-prefix ") + MessageSegment.image(
|
||||
"fake url"
|
||||
)
|
||||
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=Message(MessageSegment.image("fake url")),
|
||||
command_start="/",
|
||||
command_whitespace=" ",
|
||||
)
|
||||
|
||||
del TrieRule.prefix["/fake-prefix"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"msg,ignorecase,type,text,expected",
|
||||
"msg, ignorecase, type, text, expected",
|
||||
[
|
||||
("prefix", False, "message", "prefix_", True),
|
||||
("prefix", False, "message", "Prefix_", False),
|
||||
@@ -54,35 +133,37 @@ async def test_rule(app: App):
|
||||
("prefix", False, "message", "fooprefix", False),
|
||||
("prefix", False, "message", None, False),
|
||||
(("prefix", "foo"), False, "message", "fooprefix", True),
|
||||
("prefix", False, "notice", "prefix", True),
|
||||
("prefix", False, "notice", "foo", False),
|
||||
],
|
||||
)
|
||||
async def test_startswith(
|
||||
app: App,
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
ignorecase: bool,
|
||||
type: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.rule import StartswithRule, startswith
|
||||
|
||||
test_startswith = startswith(msg, ignorecase)
|
||||
dependent = list(test_startswith.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
msg = (msg,) if isinstance(msg, str) else msg
|
||||
|
||||
assert isinstance(checker, StartswithRule)
|
||||
assert checker.msg == (msg,) if isinstance(msg, str) else msg
|
||||
assert checker.msg == msg
|
||||
assert checker.ignorecase == ignorecase
|
||||
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
assert await dependent(event=event) == expected
|
||||
for prefix in msg:
|
||||
state = {STARTSWITH_KEY: prefix}
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"msg,ignorecase,type,text,expected",
|
||||
"msg, ignorecase, type, text, expected",
|
||||
[
|
||||
("suffix", False, "message", "_suffix", True),
|
||||
("suffix", False, "message", "_Suffix", False),
|
||||
@@ -92,35 +173,37 @@ async def test_startswith(
|
||||
("suffix", False, "message", "suffixfoo", False),
|
||||
("suffix", False, "message", None, False),
|
||||
(("suffix", "foo"), False, "message", "suffixfoo", True),
|
||||
("suffix", False, "notice", "suffix", True),
|
||||
("suffix", False, "notice", "foo", False),
|
||||
],
|
||||
)
|
||||
async def test_endswith(
|
||||
app: App,
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
ignorecase: bool,
|
||||
type: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.rule import EndswithRule, endswith
|
||||
|
||||
test_endswith = endswith(msg, ignorecase)
|
||||
dependent = list(test_endswith.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
msg = (msg,) if isinstance(msg, str) else msg
|
||||
|
||||
assert isinstance(checker, EndswithRule)
|
||||
assert checker.msg == (msg,) if isinstance(msg, str) else msg
|
||||
assert checker.msg == msg
|
||||
assert checker.ignorecase == ignorecase
|
||||
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
assert await dependent(event=event) == expected
|
||||
for suffix in msg:
|
||||
state = {ENDSWITH_KEY: suffix}
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"msg,ignorecase,type,text,expected",
|
||||
"msg, ignorecase, type, text, expected",
|
||||
[
|
||||
("fullmatch", False, "message", "fullmatch", True),
|
||||
("fullmatch", False, "message", "Fullmatch", False),
|
||||
@@ -130,51 +213,52 @@ async def test_endswith(
|
||||
("fullmatch", False, "message", "_fullmatch_", False),
|
||||
("fullmatch", False, "message", None, False),
|
||||
(("fullmatch", "foo"), False, "message", "fullmatchfoo", False),
|
||||
("fullmatch", False, "notice", "fullmatch", True),
|
||||
("fullmatch", False, "notice", "foo", False),
|
||||
],
|
||||
)
|
||||
async def test_fullmatch(
|
||||
app: App,
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
ignorecase: bool,
|
||||
type: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.rule import FullmatchRule, fullmatch
|
||||
|
||||
test_fullmatch = fullmatch(msg, ignorecase)
|
||||
dependent = list(test_fullmatch.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
msg = (msg,) if isinstance(msg, str) else msg
|
||||
|
||||
assert isinstance(checker, FullmatchRule)
|
||||
assert checker.msg == ((msg,) if isinstance(msg, str) else msg)
|
||||
assert checker.msg == msg
|
||||
assert checker.ignorecase == ignorecase
|
||||
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
assert await dependent(event=event) == expected
|
||||
for full in msg:
|
||||
state = {FULLMATCH_KEY: full}
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"kws,type,text,expected",
|
||||
"kws, type, text, expected",
|
||||
[
|
||||
(("key",), "message", "_key_", True),
|
||||
(("key", "foo"), "message", "_foo_", True),
|
||||
(("key",), "message", None, False),
|
||||
(("key",), "message", "foo", False),
|
||||
(("key",), "notice", "_key_", True),
|
||||
(("key",), "notice", "foo", False),
|
||||
],
|
||||
)
|
||||
async def test_keyword(
|
||||
app: App,
|
||||
kws: Tuple[str, ...],
|
||||
type: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.rule import KeywordsRule, keyword
|
||||
|
||||
test_keyword = keyword(*kws)
|
||||
dependent = list(test_keyword.checkers)[0]
|
||||
checker = dependent.call
|
||||
@@ -184,36 +268,44 @@ async def test_keyword(
|
||||
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
assert await dependent(event=event) == expected
|
||||
for kw in kws:
|
||||
state = {KEYWORD_KEY: kw}
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"cmds", [(("help",),), (("help", "foo"),), (("help",), ("foo",))]
|
||||
"cmds, cmd, force_whitespace, whitespace, expected",
|
||||
[
|
||||
[(("help",),), ("help",), None, None, True],
|
||||
[(("help",),), ("foo",), None, None, False],
|
||||
[(("help", "foo"),), ("help", "foo"), True, " ", True],
|
||||
[(("help",), ("foo",)), ("help",), " ", " ", True],
|
||||
[(("help",),), ("help",), False, " ", False],
|
||||
[(("help",),), ("help",), True, None, False],
|
||||
[(("help",),), ("help",), "\n", " ", False],
|
||||
],
|
||||
)
|
||||
async def test_command(app: App, cmds: Tuple[Tuple[str, ...]]):
|
||||
from nonebot.rule import CommandRule, command
|
||||
from nonebot.consts import CMD_KEY, PREFIX_KEY
|
||||
|
||||
test_command = command(*cmds)
|
||||
async def test_command(
|
||||
cmds: Tuple[Tuple[str, ...]],
|
||||
cmd: Tuple[str, ...],
|
||||
force_whitespace: Optional[Union[str, bool]],
|
||||
whitespace: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
test_command = command(*cmds, force_whitespace=force_whitespace)
|
||||
dependent = list(test_command.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
assert isinstance(checker, CommandRule)
|
||||
assert checker.cmds == cmds
|
||||
|
||||
for cmd in cmds:
|
||||
state = {PREFIX_KEY: {CMD_KEY: cmd}}
|
||||
assert await dependent(state=state)
|
||||
state = {PREFIX_KEY: {CMD_KEY: cmd, CMD_WHITESPACE_KEY: whitespace}}
|
||||
assert await dependent(state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shell_command(app: App):
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.exception import ParserExit
|
||||
from nonebot.consts import CMD_KEY, PREFIX_KEY, SHELL_ARGS, SHELL_ARGV, CMD_ARG_KEY
|
||||
from nonebot.rule import Namespace, ArgumentParser, ShellCommandRule, shell_command
|
||||
|
||||
async def test_shell_command():
|
||||
state: T_State
|
||||
CMD = ("test",)
|
||||
Message = make_fake_message()
|
||||
@@ -277,6 +369,7 @@ async def test_shell_command(app: App):
|
||||
assert state[SHELL_ARGV] == []
|
||||
assert isinstance(state[SHELL_ARGS], ParserExit)
|
||||
assert state[SHELL_ARGS].status != 0
|
||||
assert state[SHELL_ARGS].message.startswith(parser.format_usage() + "test: error:")
|
||||
|
||||
test_message_parser = shell_command(CMD, parser=parser)
|
||||
dependent = list(test_message_parser.checkers)[0]
|
||||
@@ -308,7 +401,7 @@ async def test_shell_command(app: App):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"pattern,type,text,expected,matched,group,dict",
|
||||
"pattern, type, text, expected, matched, string, group, dict",
|
||||
[
|
||||
(
|
||||
r"(?P<key>key\d)",
|
||||
@@ -316,27 +409,25 @@ async def test_shell_command(app: App):
|
||||
"_key1_",
|
||||
True,
|
||||
"key1",
|
||||
"key1",
|
||||
("key1",),
|
||||
{"key": "key1"},
|
||||
),
|
||||
(r"foo", "message", None, False, None, None, None),
|
||||
(r"foo", "notice", "foo", False, None, None, None),
|
||||
(r"foo", "message", None, False, None, None, None, None),
|
||||
(r"foo", "notice", "foo", True, "foo", "foo", tuple(), {}),
|
||||
(r"foo", "notice", "bar", False, None, None, None, None),
|
||||
],
|
||||
)
|
||||
async def test_regex(
|
||||
app: App,
|
||||
pattern: str,
|
||||
type: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
matched: Optional[str],
|
||||
string: Optional[str],
|
||||
group: Optional[Tuple[str, ...]],
|
||||
dict: Optional[Dict[str, str]],
|
||||
):
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.rule import RegexRule, regex
|
||||
from nonebot.consts import REGEX_DICT, REGEX_GROUP, REGEX_MATCHED
|
||||
|
||||
test_regex = regex(pattern)
|
||||
dependent = list(test_regex.checkers)[0]
|
||||
checker = dependent.call
|
||||
@@ -349,15 +440,14 @@ async def test_regex(
|
||||
state = {}
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
assert state.get(REGEX_MATCHED) == matched
|
||||
assert state.get(REGEX_STR) == string
|
||||
assert state.get(REGEX_GROUP) == group
|
||||
assert state.get(REGEX_DICT) == dict
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("expected", [True, False])
|
||||
async def test_to_me(app: App, expected: bool):
|
||||
from nonebot.rule import ToMeRule, to_me
|
||||
|
||||
async def test_to_me(expected: bool):
|
||||
test_to_me = to_me()
|
||||
dependent = list(test_to_me.checkers)[0]
|
||||
checker = dependent.call
|
||||
@@ -369,9 +459,7 @@ async def test_to_me(app: App, expected: bool):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_type(app: App):
|
||||
from nonebot.rule import IsTypeRule, is_type
|
||||
|
||||
async def test_is_type():
|
||||
Event1 = make_fake_event()
|
||||
Event2 = make_fake_event()
|
||||
Event3 = make_fake_event()
|
||||
|
@@ -1,11 +1,10 @@
|
||||
import json
|
||||
|
||||
from utils import make_fake_message
|
||||
from nonebot.utils import DataclassEncoder
|
||||
|
||||
|
||||
def test_dataclass_encoder():
|
||||
from nonebot.utils import DataclassEncoder
|
||||
|
||||
simple = json.dumps("123", cls=DataclassEncoder)
|
||||
assert simple == '"123"'
|
||||
|
||||
|
@@ -1,9 +1,8 @@
|
||||
from typing import TYPE_CHECKING, Type, Union, Mapping, Iterable, Optional
|
||||
from typing import Type, Union, Mapping, Iterable, Optional
|
||||
|
||||
from pydantic import create_model
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.adapters import Event, Message
|
||||
from nonebot.adapters import Event, Message, MessageSegment
|
||||
|
||||
|
||||
def escape_text(s: str, *, escape_comma: bool = True) -> str:
|
||||
@@ -14,8 +13,6 @@ def escape_text(s: str, *, escape_comma: bool = True) -> str:
|
||||
|
||||
|
||||
def make_fake_message():
|
||||
from nonebot.adapters import Message, MessageSegment
|
||||
|
||||
class FakeMessageSegment(MessageSegment):
|
||||
@classmethod
|
||||
def get_message_class(cls):
|
||||
@@ -61,18 +58,16 @@ def make_fake_message():
|
||||
|
||||
|
||||
def make_fake_event(
|
||||
_base: Optional[Type["Event"]] = None,
|
||||
_base: Optional[Type[Event]] = None,
|
||||
_type: str = "message",
|
||||
_name: str = "test",
|
||||
_description: str = "test",
|
||||
_user_id: Optional[str] = "test",
|
||||
_session_id: Optional[str] = "test",
|
||||
_message: Optional["Message"] = None,
|
||||
_message: Optional[Message] = None,
|
||||
_to_me: bool = True,
|
||||
**fields,
|
||||
) -> Type["Event"]:
|
||||
from nonebot.adapters import Event
|
||||
|
||||
) -> Type[Event]:
|
||||
_Fake = create_model("_Fake", __base__=_base or Event, **fields)
|
||||
|
||||
class FakeEvent(_Fake):
|
||||
|
@@ -6,44 +6,44 @@ slug: /
|
||||
|
||||
# 概览
|
||||
|
||||
NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架,它基于 Python 的类型注解和异步特性,能够为你的需求实现提供便捷灵活的支持。
|
||||
NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架(下称 NoneBot),它基于 Python 的类型注解和异步优先特性(兼容同步),能够为你的需求实现提供便捷灵活的支持。同时,NoneBot 拥有大量的开发者为其开发插件,用户无需编写任何代码,仅需完成环境配置及插件安装,就可以正常使用 NoneBot。
|
||||
|
||||
需要注意的是,NoneBot2 仅支持 **Python 3.8 以上版本**
|
||||
需要注意的是,NoneBot 仅支持 **Python 3.8 以上版本**
|
||||
|
||||
## 特色
|
||||
|
||||
### 异步优先
|
||||
|
||||
NoneBot2 基于 Python [asyncio](https://docs.python.org/3/library/asyncio.html) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。
|
||||
NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。
|
||||
|
||||
### 完整的类型注解
|
||||
|
||||
NoneBot2 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 `pyright`/`pylance` 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./start/editor-support))。
|
||||
NoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 Pyright(Pylance) 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./editor-support))。
|
||||
|
||||
### 开箱即用
|
||||
|
||||
NoneBot2 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得初次接触 NoneBot2 时更容易上手。详细使用方法请参考各文档章节以及[使用脚手架](./start/nb-cli)。
|
||||
NoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。
|
||||
|
||||
### 插件系统
|
||||
|
||||
插件系统是 NoneBot2 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。
|
||||
插件系统是 NoneBot 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。
|
||||
|
||||
### 依赖注入系统
|
||||
|
||||
NoneBot2 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。
|
||||
NoneBot 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。
|
||||
|
||||
#### 什么是依赖注入
|
||||
|
||||
[**“依赖注入”**](https://zh.m.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**“依赖”**。
|
||||
[**『依赖注入』**](https://zh.m.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**『依赖』**。
|
||||
|
||||
系统(在这里是指 NoneBot2)将负责做任何需要的事情,为你的代码提供这些必要依赖(即**“注入”**依赖性)
|
||||
系统(在这里是指 NoneBot)将负责做任何需要的事情,为你的代码提供这些必要依赖(即**『注入』**依赖性)
|
||||
|
||||
这在你有以下情形的需求时非常有用:
|
||||
|
||||
- 这部分代码拥有共享的逻辑(同样的代码逻辑多次重复)
|
||||
- 共享数据库以及网络请求连接会话
|
||||
- 比如 `httpx.AsyncClient`、`aiohttp.ClientSession` 和 `sqlalchemy.Session`
|
||||
- 用户权限检查以及认证
|
||||
- 机器人用户权限检查以及认证
|
||||
- 还有更多...
|
||||
|
||||
它在完成上述工作的同时,还能尽量减少代码的耦合和重复
|
||||
|
@@ -1,207 +0,0 @@
|
||||
---
|
||||
id: index
|
||||
sidebar_position: 0
|
||||
description: 深入了解 NoneBot2 运行机制
|
||||
slug: /advanced/
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 10
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 深入
|
||||
|
||||
:::danger 警告
|
||||
进阶部分尚未更新完成
|
||||
:::
|
||||
|
||||
## 它如何工作?
|
||||
|
||||
如同[概览](../README.md)所言:
|
||||
|
||||
> NoneBot2 是一个可扩展的 Python 异步机器人框架,它会对机器人收到的事件进行解析和处理,并以插件化的形式,按优先级分发给事件所对应的事件响应器,来完成具体的功能。
|
||||
|
||||
NoneBot2 是一个可以对机器人上报的事件进行处理并完成具体功能的机器人框架,在这里,我们将简要讲述它的工作内容。
|
||||
|
||||
**便捷起见,以下内容对 NoneBot2 会被称为 NoneBot,与 NoneBot2 交互的机器人实现会被称为协议端**。
|
||||
|
||||
在实际应用中,NoneBot 会充当一个高性能,轻量级的 Python 微服务框架。协议端可以通过 http、websocket 等方式与之通信,这个通信往往是双向的:一方面,协议端可以上报数据给 NoneBot,NoneBot 会处理数据并返回响应给协议端;另一方面,NoneBot 可以主动推送数据给协议端。而 NoneBot 便是围绕双向通信进行工作的。
|
||||
|
||||
在开始工作之前,NoneBot 需要进行准备工作:
|
||||
|
||||
1. **运行 `nonebot.init` 初始化函数**,它会读取配置文件,并初始化 NoneBot 和后端驱动 `Driver` 对象。
|
||||
2. **注册协议适配器 `Adapter`**。
|
||||
3. **加载插件**。
|
||||
|
||||
准备工作完成后,NoneBot 会利用 uvicorn 启动,并运行 `on_startup` 钩子函数。
|
||||
|
||||
随后,倘若一个协议端与 NoneBot 进行了连接,NoneBot 的后端驱动 `Driver` 就会将数据交给 `Adapter`,然后会实例化 `Bot`,NoneBot 便会利用 `Bot` 开始工作,它的工作内容分为两个方面:
|
||||
|
||||
1. **事件处理**,`Bot` 会将协议端上报的数据转化为 `Event`(事件),之后 NoneBot 会根据一套既定流程来处理事件。
|
||||
|
||||
2. **调用 `API`**,在**事件处理**的过程中,NoneBot 可以通过 `Bot` 调用协议端指定的 `API` 来获取更多数据,或者反馈响应给协议端;NoneBot 也可以通过调用 `API` 向协议端主动请求数据或者主动推送数据。
|
||||
|
||||
在**指南**模块,我们已经叙述了[如何配置 NoneBot](../tutorial/configuration.md)、[如何注册协议适配器](../tutorial/register-adapter.md)以及[如何加载插件](../tutorial/plugin/load-plugin.md),这里便不再赘述。
|
||||
|
||||
下面,我们将对**事件处理**,**调用 API** 进行说明。
|
||||
|
||||
## 事件处理
|
||||
|
||||
我们可以先看事件处理的流程图:
|
||||
|
||||

|
||||
|
||||
在流程图里,我们可以看到,NoneBot 会有三个阶段来处理事件:
|
||||
|
||||
1. **Driver 接收上报数据**
|
||||
2. **Adapter 处理原始数据**
|
||||
3. **NoneBot 处理 Event**
|
||||
|
||||
我们将顺序说明这三个阶段。其中,会将第三个阶段拆分成**概念解释**,**处理 Event**,**特殊异常处理**三个部分来说明。
|
||||
|
||||
### Driver 接收上报数据
|
||||
|
||||
1. 协议端会通过 websocket 或 http 等方式与 NoneBot 的后端驱动 `Driver` 连接,协议端上报数据后,`Driver` 会将原始数据交给 `Adapter` 处理。
|
||||
|
||||
:::warning
|
||||
连接之前必须要注册 `Adapter`
|
||||
:::
|
||||
|
||||
### Adapter 处理原始数据
|
||||
|
||||
1. `Adapter` 检查授权许可,并获取 `self-id` 作为唯一识别 id 。
|
||||
|
||||
:::tip
|
||||
如果协议端通过 websocket 上报数据,这个步骤只会在建立连接时进行,并在之后运行 `on_bot_connect` 钩子函数;通过 http 方式连接时,会在协议端每次上报数据时都进行这个步骤。
|
||||
:::
|
||||
|
||||
:::warning
|
||||
`self-id` 是帐号的唯一识别 ID ,这意味着不能出现相同的 `self-id`。
|
||||
:::
|
||||
|
||||
2. 根据 `self-id` 实例化 `Adapter` 相应的 `Bot` 。
|
||||
|
||||
3. 根据 `Event Model` 将原始数据转化为 NoneBot 可以处理的 `Event` 对象。
|
||||
|
||||
:::tip
|
||||
`Adapter` 在转换数据格式的同时可以进行一系列的特殊操作,例如 OneBot 适配器会对 reply 信息进行提取。
|
||||
:::
|
||||
|
||||
4. `Bot` 和 `Event` 交由 NoneBot 进一步处理。
|
||||
|
||||
### NoneBot 处理 Event
|
||||
|
||||
在讲述这个阶段之前,我们需要先对几个概念进行解释。
|
||||
|
||||
#### 概念解释
|
||||
|
||||
1. **hook** ,或者说**钩子函数**,它们可以在 NoneBot 处理 `Event` 的不同时刻进行拦截,修改或者扩展,在 NoneBot 中,事件钩子函数分为`事件预处理 hook`、`运行预处理 hook`、`运行后处理 hook` 和`事件后处理 hook`。
|
||||
|
||||
:::tip
|
||||
关于 `hook` 的更多信息,可以查阅[这里](./runtime-hook.md)。
|
||||
:::
|
||||
|
||||
2. **Matcher** 与 **matcher**,在**指南**中,我们讲述了[如何注册事件响应器](../tutorial/plugin/create-matcher.md),这里的事件响应器或者说 `Matcher` 并不是一个具体的实例 `instance`,而是一个具有特定属性的类 `class`。只有当 `Matcher` **响应事件**时,才会实例化为具体的 `instance`,也就是 `matcher` 。`matcher` 可以认为是 NoneBot 处理 `Event` 的基本单位,运行 `matcher` 是 NoneBot 工作的主要内容。
|
||||
|
||||
3. **handler**,或者说**事件处理函数**,它们可以认为是 NoneBot 处理 `Event` 的最小单位。在不考虑 `hook` 的情况下,**运行 matcher 就是顺序运行 matcher.handlers**,这句话换种表达方式就是,`handler` 只有添加到 `matcher.handlers` 时,才可以参与到 NoneBot 的工作中来。
|
||||
|
||||
:::tip
|
||||
如何让 `handler` 添加到 `matcher.handlers`?
|
||||
|
||||
一方面,我们可以参照[这里](../tutorial/plugin/create-handler.md)利用装饰器来添加;另一方面,我们在用 `on()` 或者 `on_*()` 注册事件响应器时,可以添加 `handlers=[handler1, handler2, ...]` 这样的关键词参数来添加。
|
||||
:::
|
||||
|
||||
#### 处理 Event
|
||||
|
||||
1. **执行事件预处理 hook**, NoneBot 接收到 `Event` 后,会传入到 `事件预处理 hook` 中进行处理。
|
||||
|
||||
:::warning
|
||||
需要注意的是,执行多个 `事件预处理 hook` 时并无顺序可言,它们是**并发运行**的。这个原则同样适用于其他的 `hook`。
|
||||
:::
|
||||
|
||||
2. **按优先级升序选出同一优先级的 Matcher**,NoneBot 提供了一个全局字典 `matchers`,这个字典的 `key` 是优先级 `priority`,`value` 是一个 `list`,里面存放着同一优先级的 `Matcher`。在注册 `Matcher` 时,它和优先级 `priority` 会添加到里面。
|
||||
|
||||
在执行 `事件预处理 hook` 后,NoneBot 会对 `matchers` 的 `key` 升序排序并选择出当前最小优先级的 `Matcher`。
|
||||
|
||||
3. **根据 Matcher 定义的 Rule、Permission 判断是否运行**,在选出 `Matcher` 后,NoneBot 会将 `bot`,`Event` 传入到 `Matcher.check_rule` 和 `Matcher.check_perm` 两个函数中,两个函数分别对 Matcher 定义的 `Rule`、`Permission` 进行 check,当 check 通过后,这个 `Matcher` 就会响应事件。当同一个优先级的所有 `Matcher` 均没有响应时,NoneBot 会返回到上一个步骤,选择出下一优先级的 `Matcher`。
|
||||
|
||||
4. **实例化 matcher 并执行运行预处理 hook**,当 `Matcher` 响应事件后,它便会实例化为 `matcher`,并执行 `运行预处理 hook`。
|
||||
|
||||
5. **顺序运行 matcher 的所有 handlers**,`运行预处理 hook` 执行完毕后,便会运行 `matcher`,也就是**顺序运行**它的 `handlers`。
|
||||
|
||||
:::tip
|
||||
`matcher` 运行 `handlers` 的顺序是:先运行该 `matcher` 的类 `Matcher` 注册时添加的 `handlers`(如果有的话),再按照装饰器装饰顺序运行装饰的 `handlers`。
|
||||
:::
|
||||
|
||||
6. **执行运行后处理 hook**,`matcher` 的 `handlers` 运行完毕后,会执行 `运行后处理 hook`。
|
||||
|
||||
7. **判断是否停止事件传播**,NoneBot 会根据当前优先级所有 `matcher` 的 `block` 参数或者 `StopPropagation` 异常判断是否停止传播 `Event`,如果事件没有停止传播,NoneBot 便会返回到第 2 步, 选择出下一优先级的 `Matcher`。
|
||||
|
||||
8. **执行事件后处理 hook**,在 `Event` 停止传播或执行完所有响应的 `Matcher` 后,NoneBot 会执行 `事件后处理 hook`。
|
||||
|
||||
当 `事件后处理 hook` 执行完毕后,当前 `Event` 的处理周期就顺利结束了。
|
||||
|
||||
#### 特殊异常处理
|
||||
|
||||
在这个阶段,NoneBot 规定了几个特殊的异常,当 NoneBot 捕获到它们时,会用特定的行为来处理它们。
|
||||
|
||||
1. **IgnoredException**
|
||||
|
||||
这个异常可以在 `事件预处理 hook` 和 `运行预处理 hook` 抛出。
|
||||
|
||||
当 `事件预处理 hook` 抛出它时,NoneBot 会忽略当前的 `Event`,不进行处理。
|
||||
|
||||
当 `运行预处理 hook` 抛出它时,NoneBot 会忽略当前的 `matcher`,结束当前 `matcher` 的运行。
|
||||
|
||||
:::warning
|
||||
当 `hook` 需要抛出这个异常时,要写明原因。
|
||||
:::
|
||||
|
||||
2. **PausedException**
|
||||
|
||||
这个异常可以在 `handler` 中由 `Matcher.pause` 抛出。
|
||||
|
||||
当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将后续的 `handler` 交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行后续的 `handler`。
|
||||
|
||||
3. **RejectedException**
|
||||
|
||||
这个异常可以在 `handler` 中由 `Matcher.reject` 抛出。
|
||||
|
||||
当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将当前 handler 和后续 `handler` 交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行当前 `handler` 和后续的 `handler` 。
|
||||
|
||||
4. **FinishedException**
|
||||
|
||||
这个异常可以在 `handler` 中由 `Matcher.finish` 抛出。
|
||||
|
||||
当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行。
|
||||
|
||||
5. **StopPropagation**
|
||||
|
||||
这个异常一般会在执行 `运行后处理 hook` 后抛出。
|
||||
|
||||
当 NoneBot 捕获到它时, 会停止传播当前 `Event` ,不再寻找下一优先级的 `Matcher` ,直接执行 `事件后处理 hook` 。
|
||||
|
||||
## 调用 API
|
||||
|
||||
NoneBot 可以通过 `bot` 来调用 `API`,`API` 可以向协议端发送数据,也可以向协议端请求更多的数据。
|
||||
|
||||
NoneBot 调用 `API` 会有如下过程:
|
||||
|
||||
1. 调用 `calling_api_hook` 预处理钩子。
|
||||
|
||||
2. `adapter` 将信息处理为原始数据,并转交 `driver`,`driver` 交给协议端处理。
|
||||
|
||||
3. `driver` 接收协议端的结果,交给`adapter` 处理之后将结果反馈给 NoneBot 。
|
||||
|
||||
4. 调用 `called_api_hook` 后处理钩子。
|
||||
|
||||
在调用 `API` 时同样规定了特殊的异常,叫做 `MockApiException` 。该异常会由预处理钩子和后处理钩子触发,当预处理钩子触发时,NoneBot 会跳过之后的调用过程,直接执行后处理钩子。
|
||||
|
||||
:::tip
|
||||
不同 `adapter` 规定了不同的 API,对应的 API 列表请参照协议规范。
|
||||
:::
|
||||
|
||||
一般来说,我们可以用 `bot.*` 来调用 `API`(\*是 `API` 的 `action` 或者 `endpoint`)。
|
||||
|
||||
对于发送消息而言,一方面可以调用既有的 `API` ;另一方面 NoneBot 实现了两个便捷方法,`bot.send(event, message, **kwargs)` 方法和可以在 `handler` 中使用的 `Matcher.send(message, **kwargs)` 方法,来向事件主体发送消息。
|
161
website/docs/advanced/adapter.md
Normal file
161
website/docs/advanced/adapter.md
Normal file
@@ -0,0 +1,161 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: 注册适配器与指定平台交互
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 20
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 使用适配器
|
||||
|
||||
适配器 (Adapter) 是机器人与平台交互的核心桥梁,它负责在驱动器和机器人插件之间转换与传递消息。
|
||||
|
||||
## 适配器功能与组成
|
||||
|
||||
适配器通常有两种功能,分别是**接收事件**和**调用平台接口**。其中,接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型,然后交由机器人插件处理;调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式,然后交由驱动器发送,并接收接口返回数据。
|
||||
|
||||
为了实现这两种功能,适配器通常由四个部分组成:
|
||||
|
||||
- **Adapter**:负责转换事件和调用接口,正确创建 Bot 对象并注册到 NoneBot 中。
|
||||
- **Bot**:负责存储平台机器人相关信息,并提供回复事件的方法。
|
||||
- **Event**:负责定义事件内容,以及事件主体对象。
|
||||
- **Message**:负责正确序列化消息,以便机器人插件处理。
|
||||
|
||||
## 注册适配器
|
||||
|
||||
在使用适配器之前,我们需要先将适配器注册到驱动器中,这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例,来看看如何注册适配器:
|
||||
|
||||
```python {2,5} title=bot.py
|
||||
import nonebot
|
||||
from nonebot.adapters.console import Adapter
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
driver.register_adapter(Adapter)
|
||||
```
|
||||
|
||||
我们首先需要从适配器模块中导入所需要的适配器类,然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。
|
||||
|
||||
## 获取已注册的适配器
|
||||
|
||||
NoneBot 提供了 `get_adapter` 方法来获取已注册的适配器,我们可以通过适配器的名称或类型来获取指定的适配器实例:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
from nonebot.adapters.console import Adapter
|
||||
|
||||
adapters = nonebot.get_adapters()
|
||||
console_adapter = nonebot.get_adapter(Adapter)
|
||||
console_adapter = nonebot.get_adapter(Adapter.get_name())
|
||||
```
|
||||
|
||||
## 获取 Bot 对象
|
||||
|
||||
当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取,这是一个以机器人 ID 为键的字典:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
bots = nonebot.get_bots()
|
||||
```
|
||||
|
||||
我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数,将会返回所有 Bot 中的第一个:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
bot = nonebot.get_bot("bot_id")
|
||||
```
|
||||
|
||||
如果需要获取指定适配器连接的 Bot 对象,我们可以通过适配器的 `bots` 属性获取,这也是一个以机器人 ID 为键的字典:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
from nonebot.adapters.console import Adapter
|
||||
|
||||
console_adapter = nonebot.get_adapter(Adapter)
|
||||
bots = console_adapter.bots
|
||||
```
|
||||
|
||||
Bot 对象都具有一个 `self_id` 属性,它是机器人的唯一 ID,由适配器填写,通常为机器人的帐号 ID 或者 APP ID。
|
||||
|
||||
## 获取事件通用信息
|
||||
|
||||
适配器的所有事件模型均继承自 `Event` 基类,在[事件类型与重载](../appendices/overload.md)一节中,我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息:
|
||||
|
||||
### 事件类型
|
||||
|
||||
事件类型通常为 `meta_event`、`message`、`notice`、`request`。
|
||||
|
||||
```python
|
||||
type: str = event.get_type()
|
||||
```
|
||||
|
||||
### 事件名称
|
||||
|
||||
事件名称由适配器定义,通常用于日志记录。
|
||||
|
||||
```python
|
||||
name: str = event.get_event_name()
|
||||
```
|
||||
|
||||
### 事件描述
|
||||
|
||||
事件描述由适配器定义,通常用于日志记录。
|
||||
|
||||
```python
|
||||
description: str = event.get_event_description()
|
||||
```
|
||||
|
||||
### 事件日志字符串
|
||||
|
||||
事件日志字符串由事件名称和事件描述组成,用于日志记录。
|
||||
|
||||
```python
|
||||
log: str = event.get_log_string()
|
||||
```
|
||||
|
||||
### 事件主体 ID
|
||||
|
||||
事件主体 ID 通常为机器人用户 ID。
|
||||
|
||||
```python
|
||||
user_id: str = event.get_user_id()
|
||||
```
|
||||
|
||||
### 事件会话 ID
|
||||
|
||||
事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。
|
||||
|
||||
```python
|
||||
session_id: str = event.get_session_id()
|
||||
```
|
||||
|
||||
### 事件消息
|
||||
|
||||
如果事件包含消息,则可以通过该方法获取,否则会产生异常。
|
||||
|
||||
```python
|
||||
message: Message = event.get_message()
|
||||
```
|
||||
|
||||
### 事件纯文本消息
|
||||
|
||||
通常为事件消息的纯文本内容,如果事件不包含消息,则会产生异常。
|
||||
|
||||
```python
|
||||
text: str = event.get_plaintext()
|
||||
```
|
||||
|
||||
### 事件是否与机器人有关
|
||||
|
||||
由适配器实现的判断,通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。
|
||||
|
||||
```python
|
||||
is_tome: bool = event.is_tome()
|
||||
```
|
||||
|
||||
## 更多
|
||||
|
||||
官方支持的适配器和社区贡献的适配器均可在[商店](/store)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。
|
1189
website/docs/advanced/dependency.mdx
Normal file
1189
website/docs/advanced/dependency.mdx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"label": "依赖注入",
|
||||
"position": 5
|
||||
}
|
@@ -1,243 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: 依赖注入简介
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 60
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 简介
|
||||
|
||||
受 [FastAPI](https://fastapi.tiangolo.com/tutorial/dependencies/) 启发,NoneBot 同样编写了一个简易的依赖注入模块,使得开发者可以通过事件处理函数参数的类型标注来自动注入依赖。
|
||||
|
||||
## 什么是依赖注入?
|
||||
|
||||
[依赖注入](https://zh.wikipedia.org/wiki/%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5)
|
||||
|
||||
> 在软件工程中,**依赖注入**(dependency injection)的意思为,给予调用方它所需要的事物。 “依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接使用“依赖”,取而代之是“注入” 。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖。 传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。
|
||||
|
||||
依赖注入往往起到了分离依赖和调用方的作用,这样一方面能让代码更为整洁可读,一方面可以提升代码的复用性。
|
||||
|
||||
## 使用依赖注入
|
||||
|
||||
以下通过一个简单的例子来说明依赖注入的使用方法:
|
||||
|
||||
```python {2,7-8,11}
|
||||
from nonebot import on_command
|
||||
from nonebot.params import Depends # 1.引用 Depends
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent
|
||||
|
||||
test = on_command("123")
|
||||
|
||||
async def depend(event: MessageEvent): # 2.编写依赖函数
|
||||
return {"uid": event.get_user_id(), "nickname": event.sender.nickname}
|
||||
|
||||
@test.handle()
|
||||
async def _(x: dict = Depends(depend)): # 3.在事件处理函数里声明依赖项
|
||||
print(x["uid"], x["nickname"])
|
||||
```
|
||||
|
||||
如注释所言,可以用三步来说明依赖注入的使用过程:
|
||||
|
||||
1. 引用 `Depends` 。
|
||||
|
||||
2. 编写依赖函数。依赖函数和普通的事件处理函数并无区别,同样可以接收 `bot`, `event`, `state` 等参数,你可以把它当作一个普通的事件处理函数,但是去除了装饰器(没有使用 `matcher.handle()` 等来装饰),并且可以返回任何类型的值。
|
||||
|
||||
在这里我们接受了 `event`,并以 `onebot` 的 `MessageEvent` 作为类型标注,返回一个新的字典,包括 `uid` 和 `nickname` 两个键值。
|
||||
|
||||
3. 在事件处理函数中声明依赖项。依赖项必须要 `Depends` 包裹依赖函数作为默认值。
|
||||
|
||||
:::tip
|
||||
请注意,参数 `x` 的类型标注将会影响到事件处理函数的运行,与类型标注不符的值将会导致事件处理函数被跳过。
|
||||
:::
|
||||
|
||||
:::tip
|
||||
事实上,bot、event、state 它们本身只是依赖注入的一个特例,它们无需声明这是依赖即可注入。
|
||||
:::
|
||||
|
||||
虽然声明依赖项的方式和其他参数如 `bot`, `event` 并无二样,但他的参数有一些限制,必须是**可调用对象**,函数自然是可调用对象,类和生成器也是,我们会在接下来的小节说明。
|
||||
|
||||
一般来说,当接收到事件时,`NoneBot2` 会进行以下处理:
|
||||
|
||||
1. 准备依赖函数所需要的参数。
|
||||
2. 调用依赖函数并获得返回值。
|
||||
3. 将返回值作为事件处理函数中的参数值传入。
|
||||
|
||||
## 依赖缓存
|
||||
|
||||
在使用 `Depends` 包裹依赖函数时,有一个参数 `use_cache` ,它默认为 `True` ,这个参数会决定 `Nonebot2` 在依赖注入的处理中是否使用缓存。
|
||||
|
||||
```python {11}
|
||||
import random
|
||||
from nonebot import on_command
|
||||
from nonebot.params import Depends
|
||||
|
||||
test = on_command("123")
|
||||
|
||||
async def always_run():
|
||||
return random.randint(1, 100)
|
||||
|
||||
@test.handle()
|
||||
async def _(x: int = Depends(always_run, use_cache=False)):
|
||||
print(x)
|
||||
```
|
||||
|
||||
:::tip
|
||||
缓存是针对单次事件处理来说的,在事件处理中 `Depends` 第一次被调用时,结果存入缓存,在之后都会直接返回缓存中的值,在事件处理结束后缓存就会被清除。
|
||||
:::
|
||||
|
||||
当使用缓存时,依赖注入会这样处理:
|
||||
|
||||
1. 查询缓存,如果缓存中有相应的值,则直接返回。
|
||||
2. 准备依赖函数所需要的参数。
|
||||
3. 调用依赖函数并获得返回值。
|
||||
4. 将返回值存入缓存。
|
||||
5. 将返回值作为事件处理函数中的参数值传入。
|
||||
|
||||
## 同步支持
|
||||
|
||||
我们在编写依赖函数时,可以简单地用同步函数,`NoneBot2` 的内部流程会进行处理:
|
||||
|
||||
```python {2,8-9,12}
|
||||
from nonebot.log import logger
|
||||
from nonebot.params import Depends # 1.引用 Depends
|
||||
from nonebot import on_command, on_message
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent
|
||||
|
||||
test = on_command("123")
|
||||
|
||||
def depend(event: MessageEvent): # 2.编写同步依赖函数
|
||||
return {"uid": event.get_user_id(), "nickname": event.sender.nickname}
|
||||
|
||||
@test.handle()
|
||||
async def _(x: dict = Depends(depend)): # 3.在事件处理函数里声明依赖项
|
||||
print(x["uid"], x["nickname"])
|
||||
```
|
||||
|
||||
## Class 作为依赖
|
||||
|
||||
我们可以看下面的代码段:
|
||||
|
||||
```python
|
||||
class A:
|
||||
def __init__(self):
|
||||
pass
|
||||
a = A()
|
||||
```
|
||||
|
||||
在我们实例化类 `A` 的时候,其实我们就在**调用**它,类本身也是一个**可调用对象**,所以类可以被 `Depends` 包裹成为依赖项。
|
||||
|
||||
因此我们对第一节的代码段做一下改造:
|
||||
|
||||
```python {2,7-10,13}
|
||||
from nonebot import on_command
|
||||
from nonebot.params import Depends # 1.引用 Depends
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent
|
||||
|
||||
test = on_command("123")
|
||||
|
||||
class DependClass: # 2.编写依赖类
|
||||
def __init__(self, event: MessageEvent):
|
||||
self.uid = event.get_user_id()
|
||||
self.nickname = event.sender.nickname
|
||||
|
||||
@test.handle()
|
||||
async def _(x: DependClass = Depends(DependClass)): # 3.在事件处理函数里声明依赖项
|
||||
print(x.uid, x.nickname)
|
||||
```
|
||||
|
||||
依然可以用三步说明如何用类作为依赖项:
|
||||
|
||||
1. 引用 `Depends` 。
|
||||
2. 编写依赖类。类的 `__init__` 函数可以接收 `bot`, `event`, `state` 等参数,在这里我们接受了 `event`,并以 `onebot` 的 `MessageEvent` 作为类型标注。
|
||||
3. 在事件处理函数中声明依赖项。当用类作为依赖项时,它会是一个对应的实例,在这里 `x` 就是 `DependClass` 实例。
|
||||
|
||||
### 另一种依赖项声明方式
|
||||
|
||||
当使用类作为依赖项时,`Depends` 的参数可以为空,`NoneBot2` 会根据参数的类型标注进行推断并进行依赖注入。
|
||||
|
||||
```python
|
||||
@test.handle()
|
||||
async def _(x: DependClass = Depends()): # 在事件处理函数里声明依赖项
|
||||
print(x.uid, x.nickname)
|
||||
```
|
||||
|
||||
## 生成器作为依赖
|
||||
|
||||
:::warning
|
||||
`yield` 语句只能写一次,否则会引发异常。
|
||||
如果对此有疑问并想探究原因,可以看 [contextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.contextmanager) 和 [asynccontextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager) 文档,实际上,`Nonebot2` 的内部就使用了这两个装饰器。
|
||||
:::
|
||||
|
||||
:::tips
|
||||
生成器是 `Python` 高级特性,如果你对此处文档感到疑惑那说明暂时你还用不上这个功能。
|
||||
:::
|
||||
|
||||
与 `FastAPI` 一样,`NoneBot2` 的依赖注入支持依赖项在事件处理结束后进行一些额外的工作,比如数据库 session 或者网络 IO 的关闭,互斥锁的解锁等等。
|
||||
|
||||
要实现上述功能,我们可以用生成器函数作为依赖项,我们用 `yield` 关键字取代 `return` 关键字,并在 `yield` 之后进行额外的工作。
|
||||
|
||||
我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO:
|
||||
|
||||
```python {3,7-10,13}
|
||||
import httpx
|
||||
from nonebot import on_command
|
||||
from nonebot.params import Depends # 1.引用 Depends
|
||||
|
||||
test = on_command("123")
|
||||
|
||||
async def get_client(): # 2.编写异步生成器函数
|
||||
async with httpx.AsyncClient() as client:
|
||||
yield client
|
||||
print("调用结束")
|
||||
|
||||
@test.handle()
|
||||
async def _(x: httpx.AsyncClient = Depends(get_client)): # 3.在事件处理函数里声明依赖项
|
||||
resp = await x.get("https://v2.nonebot.dev")
|
||||
# do something
|
||||
```
|
||||
|
||||
我们用 `yield` 代码段作为生成器函数的“返回”,在事件处理函数里用返回出来的 `client` 做自己需要的工作。在 `NoneBot2` 结束事件处理时,会执行 `yield` 之后的代码。
|
||||
|
||||
## 创造可调用对象作为依赖
|
||||
|
||||
:::tips
|
||||
魔法方法 `__call__` 是 `Python` 高级特性,如果你对此处文档感到疑惑那说明暂时你还用不上这个功能。
|
||||
:::
|
||||
|
||||
在 `Python` 的里,类的 `__call__` 方法会让类的实例变成**可调用对象**,我们可以利用这个魔法方法做一个简单的尝试:
|
||||
|
||||
```python{3,9-14,16,19}
|
||||
from typing import Type
|
||||
from nonebot.log import logger
|
||||
from nonebot.params import Depends # 1.引用 Depends
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent
|
||||
|
||||
test = on_command("123")
|
||||
|
||||
class EventChecker: # 2.编写需要的类
|
||||
def __init__(self, EventClass: Type[MessageEvent]):
|
||||
self.event_class = EventClass
|
||||
|
||||
def __call__(self, event: MessageEvent) -> bool:
|
||||
return isinstance(event, self.event_class)
|
||||
|
||||
checker = EventChecker(GroupMessageEvent) # 3.将类实例化
|
||||
|
||||
@test.handle()
|
||||
async def _(x: bool = Depends(checker)): # 4.在事件处理函数里声明依赖项
|
||||
if x:
|
||||
print("这是群聊消息")
|
||||
else:
|
||||
print("这不是群聊消息")
|
||||
```
|
||||
|
||||
这是判断 `onebot` 的消息事件是不是群聊消息事件的一个例子,我们可以用四步来说明这个例子:
|
||||
|
||||
1. 引用 `Depends` 。
|
||||
2. 编写需要的类。类的 `__init__` 函数接收参数 `EventClass`,它将接收事件类本身。类的 `__call__` 函数将接受消息事件对象,并返回一个 `bool` 类型的判定结果。
|
||||
3. 将类实例化。我们传入群聊消息事件作为参数实例化 `checker` 。
|
||||
4. 在事件处理函数里声明依赖项。`NoneBot2` 将会调用 `checker` 的 `__call__` 方法,返回给参数 `x` 相应的判断结果。
|
@@ -1,76 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
description: 重载事件处理函数
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 61
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 事件处理函数重载
|
||||
|
||||
当我们在编写 NoneBot2 应用时,常常会遇到这样一个问题:该怎么让同一类型的不同事件执行不同的响应逻辑?又或者如何让不同的 `bot` 针对同一类型的事件作出不同响应?
|
||||
|
||||
针对这个问题, NoneBot2 提供一个便捷而高效的解决方案:事件处理函数重载机制。简单地说,`handler`(事件处理函数)会根据其参数的 `type hints`([PEP484 类型标注](https://www.python.org/dev/peps/pep-0484/))来对相对应的 `bot` 和 `event` 进行响应,并且会忽略不符合其参数类型标注的情况。
|
||||
|
||||
<!-- 必须要注意的是,该机制利用了 `inspect` 标准库获取到了事件处理函数的 `signature`(签名),进一步获取到参数名称和类型标注。故而,我们在编写 `handler` 时,参数的名称和类型标注必须要符合 `T_Handler` 规定,详情可以参看**指南**中的[事件处理](../../guide/creating-a-handler)。 -->
|
||||
|
||||
:::tip 提示
|
||||
如果想了解更多关于 `inspect` 标准库的信息,可以查看[官方文档](https://docs.python.org/zh-cn/3.9/library/inspect.html)。
|
||||
:::
|
||||
|
||||
下面,我们会以 OneBot 适配器中的群聊消息事件和私聊消息事件为例,对该机制的应用进行简单的介绍。
|
||||
|
||||
## 一个例子
|
||||
|
||||
首先,我们需要导入需要的方法、类型。
|
||||
|
||||
```python
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, PrivateMessageEvent
|
||||
```
|
||||
|
||||
之后,我们可以注册一个 `Matcher` 来响应消息事件。
|
||||
|
||||
```python
|
||||
matcher = on_command("test_overload")
|
||||
```
|
||||
|
||||
最后,我们编写不同的 `handler` 并编写不同的类型标注来实现事件处理函数重载:
|
||||
|
||||
```python
|
||||
@matcher.handle()
|
||||
async def _(bot: Bot, event: GroupMessageEvent):
|
||||
await matcher.send("群聊消息事件响应成功!")
|
||||
|
||||
|
||||
@matcher.handle()
|
||||
async def _(bot: Bot, event: PrivateMessageEvent):
|
||||
await matcher.send("私聊消息事件响应成功!")
|
||||
```
|
||||
|
||||
此时,我们可以在群聊或私聊中对我们的机器人发送 `test_overload`,它会在不同的场景做出不同的应答。
|
||||
|
||||
这样一个简单的事件处理函数重载就完成了。
|
||||
|
||||
## 进阶
|
||||
|
||||
事件处理函数重载机制同样支持被 `matcher.got` 等装饰器装饰的函数。例如:
|
||||
|
||||
```python
|
||||
@matcher.got("key1", prompt="群事件提问")
|
||||
async def _(bot: Bot, event: GroupMessageEvent):
|
||||
await matcher.send("群聊消息事件响应成功!")
|
||||
|
||||
|
||||
@matcher.got("key2", prompt="私聊事件提问")
|
||||
async def _(bot: Bot, event: PrivateMessageEvent):
|
||||
await matcher.send("私聊消息事件响应成功!")
|
||||
```
|
||||
|
||||
只有触发事件符合的函数才会触发装饰器。
|
||||
|
||||
:::warning 注意
|
||||
bot 和 event 参数具有最高的检查优先级,因此,如果参数类型不符合,所有的依赖项 `Depends` 等都不会被执行。
|
||||
:::
|
286
website/docs/advanced/driver.md
Normal file
286
website/docs/advanced/driver.md
Normal file
@@ -0,0 +1,286 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
description: 选择合适的驱动器运行机器人
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 10
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 选择驱动器
|
||||
|
||||
驱动器 (Driver) 是机器人运行的基石,它是机器人初始化的第一步,主要负责数据收发。
|
||||
|
||||
:::important 提示
|
||||
驱动器的选择通常与机器人所使用的协议适配器相关,如果不知道该选择哪个驱动器,可以先阅读相关协议适配器文档说明。
|
||||
:::
|
||||
|
||||
:::tip 提示
|
||||
如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。
|
||||
:::
|
||||
|
||||
## 驱动器类型
|
||||
|
||||
驱动器的类型有两种:
|
||||
|
||||
- `ForwardDriver`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。
|
||||
- `ReverseDriver`:即服务端型驱动器,多用于使用 WebHook,接收 WebSocket 客户端连接等情形。
|
||||
|
||||
客户端型驱动器具有以下两种功能:
|
||||
|
||||
1. 异步发送 HTTP 请求,自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。
|
||||
2. 异步建立 WebSocket 连接上下文,自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。
|
||||
|
||||
服务端型驱动器通常为 ASGI 应用框架,具有以下功能:
|
||||
|
||||
1. 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。
|
||||
2. 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。
|
||||
3. 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。
|
||||
|
||||
## 配置驱动器
|
||||
|
||||
驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍,这里将详细介绍驱动器配置的格式。
|
||||
|
||||
NoneBot 中的客户端和服务端型驱动器可以相互配合使用,但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类,即驱动器类,他可以作为驱动器单独运行。同时,客户端驱动器模块中还会提供一个 `Mixin` 子类,用于在与其他驱动器配合使用时加载。因此,驱动器配置格式采用特殊语法:`<module>[:<Driver>][+<module>[:<Mixin>]]*`。
|
||||
|
||||
其中,`<module>` 代表**驱动器模块路径**;`<Driver>` 代表**驱动器类名**,默认为 `Driver`;`<Mixin>` 代表**驱动器混入类名**,默认为 `Mixin`。即,我们需要选择一个主要驱动器,然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型,但混入类驱动器只能为客户端类型。
|
||||
|
||||
特别的,为了简化内置驱动器模块路径,我们可以使用 `~` 符号作为内置驱动器模块路径的前缀,如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配,但需要安装额外依赖才能使用,具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下:
|
||||
|
||||
```dotenv
|
||||
DRIVER=~fastapi
|
||||
DRIVER=~aiohttp
|
||||
DRIVER=~httpx+~websockets
|
||||
DRIVER=~fastapi+~httpx+~websockets
|
||||
```
|
||||
|
||||
## 获取驱动器
|
||||
|
||||
在 NoneBot 框架初始化完成后,我们就可以通过 `get_driver()` 方法获取全局驱动器实例:
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
```
|
||||
|
||||
## 内置驱动器
|
||||
|
||||
### None
|
||||
|
||||
**类型:**服务端驱动器
|
||||
|
||||
NoneBot 内置的空驱动器,不提供任何收发数据功能,可以在不需要外部网络连接时使用。
|
||||
|
||||
```env
|
||||
DRIVER=~none
|
||||
```
|
||||
|
||||
### FastAPI(默认)
|
||||
|
||||
**类型:**服务端驱动器
|
||||
|
||||
> FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
|
||||
|
||||
[FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架,具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能,也可以挂载其他 ASGI、WSGI 应用。
|
||||
|
||||
```env
|
||||
DRIVER=~fastapi
|
||||
```
|
||||
|
||||
#### FastAPI 配置项
|
||||
|
||||
##### `fastapi_openapi_url`
|
||||
|
||||
类型:`str | None`
|
||||
默认值:`None`
|
||||
说明:`FastAPI` 提供的 `OpenAPI` JSON 定义地址,如果为 `None`,则不提供 `OpenAPI` JSON 定义。
|
||||
|
||||
##### `fastapi_docs_url`
|
||||
|
||||
类型:`str | None`
|
||||
默认值:`None`
|
||||
说明:`FastAPI` 提供的 `Swagger` 文档地址,如果为 `None`,则不提供 `Swagger` 文档。
|
||||
|
||||
##### `fastapi_redoc_url`
|
||||
|
||||
类型:`str | None`
|
||||
默认值:`None`
|
||||
说明:`FastAPI` 提供的 `ReDoc` 文档地址,如果为 `None`,则不提供 `ReDoc` 文档。
|
||||
|
||||
##### `fastapi_include_adapter_schema`
|
||||
|
||||
类型:`bool`
|
||||
默认值:`True`
|
||||
说明:`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`。
|
||||
|
||||
##### `fastapi_reload`
|
||||
|
||||
:::warning 警告
|
||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||
|
||||
```bash
|
||||
nb run --reload
|
||||
```
|
||||
|
||||
开启该功能后,在 uvicorn 运行时(FastAPI 提供的 ASGI 底层,即 reload 功能的实际来源),asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`。
|
||||
|
||||
> 相关信息参考 [uvicorn#529](https://github.com/encode/uvicorn/issues/529),[uvicorn#1070](https://github.com/encode/uvicorn/pull/1070),[uvicorn#1257](https://github.com/encode/uvicorn/pull/1257)
|
||||
|
||||
后者(`SelectorEventLoop`)在 Windows 平台的可使用性不如前者(`ProactorEventLoop`),包括但不限于
|
||||
|
||||
1. 不支持创建子进程
|
||||
2. 最多只支持 512 个套接字
|
||||
3. ...
|
||||
|
||||
> 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows)
|
||||
|
||||
所以,一些使用了 asyncio 的库因此可能无法正常工作,如:
|
||||
|
||||
1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows)
|
||||
|
||||
如果在开启该功能后,原本**正常运行**的代码报错,且打印的异常堆栈信息和 asyncio 有关(异常一般为 `NotImplementedError`),
|
||||
你可能就需要考虑相关库对事件循环的支持,以及是否启用该功能。
|
||||
:::
|
||||
|
||||
类型:`bool`
|
||||
默认值:`False`
|
||||
说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。
|
||||
|
||||
```python title=bot.py
|
||||
app = nonebot.get_asgi()
|
||||
nonebot.run(app="bot:app")
|
||||
```
|
||||
|
||||
##### `fastapi_reload_dirs`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:重载监控文件夹列表,默认为 uvicorn 默认值
|
||||
|
||||
##### `fastapi_reload_delay`
|
||||
|
||||
类型:`float | None`
|
||||
默认值:`None`
|
||||
说明:重载延迟,默认为 uvicorn 默认值
|
||||
|
||||
##### `fastapi_reload_includes`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
||||
|
||||
##### `fastapi_reload_excludes`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
||||
|
||||
##### `fastapi_extra`
|
||||
|
||||
类型:`Dist[str, Any]`
|
||||
默认值:`{}`
|
||||
说明:传递给 `FastAPI` 的其他参数
|
||||
|
||||
### Quart
|
||||
|
||||
**类型:**`ReverseDriver`
|
||||
|
||||
> Quart is an asyncio reimplementation of the popular Flask microframework API.
|
||||
|
||||
[Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本,拥有与 Flask 非常相似的接口和使用方法。
|
||||
|
||||
```env
|
||||
DRIVER=~quart
|
||||
```
|
||||
|
||||
#### Quart 配置项
|
||||
|
||||
##### `quart_reload`
|
||||
|
||||
:::warning 警告
|
||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||
|
||||
```bash
|
||||
nb run --reload
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
类型:`bool`
|
||||
默认值:`False`
|
||||
说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。
|
||||
|
||||
```python title=bot.py
|
||||
app = nonebot.get_asgi()
|
||||
nonebot.run(app="bot:app")
|
||||
```
|
||||
|
||||
##### `quart_reload_dirs`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:重载监控文件夹列表,默认为 uvicorn 默认值
|
||||
|
||||
##### `quart_reload_delay`
|
||||
|
||||
类型:`float | None`
|
||||
默认值:`None`
|
||||
说明:重载延迟,默认为 uvicorn 默认值
|
||||
|
||||
##### `quart_reload_includes`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
||||
|
||||
##### `quart_reload_excludes`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
||||
|
||||
##### `quart_extra`
|
||||
|
||||
类型:`Dist[str, Any]`
|
||||
默认值:`{}`
|
||||
说明:传递给 `Quart` 的其他参数
|
||||
|
||||
### HTTPX
|
||||
|
||||
**类型:**`ForwardDriver`
|
||||
|
||||
:::warning 注意
|
||||
本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。
|
||||
:::
|
||||
|
||||
> [HTTPX](https://www.python-httpx.org/) is a fully featured HTTP client for Python 3, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2.
|
||||
|
||||
```env
|
||||
DRIVER=~httpx
|
||||
```
|
||||
|
||||
### websockets
|
||||
|
||||
**类型:**`ForwardDriver`
|
||||
|
||||
:::warning 注意
|
||||
本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。
|
||||
:::
|
||||
|
||||
> [websockets](https://websockets.readthedocs.io/) is a library for building WebSocket servers and clients in Python with a focus on correctness, simplicity, robustness, and performance.
|
||||
|
||||
```env
|
||||
DRIVER=~websockets
|
||||
```
|
||||
|
||||
### AIOHTTP
|
||||
|
||||
**类型:**`ForwardDriver`
|
||||
|
||||
> [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python.
|
||||
|
||||
```env
|
||||
DRIVER=~aiohttp
|
||||
```
|
Binary file not shown.
Before Width: | Height: | Size: 376 KiB |
Binary file not shown.
Before Width: | Height: | Size: 103 KiB |
Binary file not shown.
Before Width: | Height: | Size: 128 KiB |
40
website/docs/advanced/matcher-provider.md
Normal file
40
website/docs/advanced/matcher-provider.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
sidebar_position: 10
|
||||
description: 自定义事件响应器存储
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 110
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 事件响应器存储
|
||||
|
||||
事件响应器是 NoneBot 处理事件的核心,它们默认存储在一个字典中。在进入会话状态后,事件响应器将会转为临时响应器,作为最高优先级同样存储于该字典中。因此,事件响应器的存储类似于会话存储,它决定了整个 NoneBot 对事件的处理行为。
|
||||
|
||||
NoneBot 默认使用 Python 的字典将事件响应器存储于内存中,但是我们也可以自定义事件响应器存储,将事件响应器存储于其他地方,例如 Redis 等。这样我们就可以实现持久化、在多实例间共享会话状态等功能。
|
||||
|
||||
## 编写存储提供者
|
||||
|
||||
事件响应器的存储提供者 `MatcherProvider` 抽象类继承自 `MutableMapping[int, list[type[Matcher]]]`,即以优先级为键,以事件响应器列表为值的映射。我们可以方便地进行逐优先级事件传播。
|
||||
|
||||
编写一个自定义的存储提供者,只需要继承并实现 `MatcherProvider` 抽象类:
|
||||
|
||||
```python
|
||||
from nonebot.matcher import MatcherProvider
|
||||
|
||||
class CustomProvider(MatcherProvider):
|
||||
...
|
||||
```
|
||||
|
||||
## 设置存储提供者
|
||||
|
||||
我们可以通过 `matchers.set_provider` 方法设置存储提供者:
|
||||
|
||||
```python {3}
|
||||
from nonebot.matcher import matchers
|
||||
|
||||
matchers.set_provider(CustomProvider)
|
||||
|
||||
assert isinstance(matchers.provider, CustomProvider)
|
||||
```
|
304
website/docs/advanced/matcher.md
Normal file
304
website/docs/advanced/matcher.md
Normal file
@@ -0,0 +1,304 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
description: 事件响应器组成与内置响应规则
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 60
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 事件响应器进阶
|
||||
|
||||
在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,以及内置的响应规则。
|
||||
|
||||
## 事件响应器组成
|
||||
|
||||
### 事件响应器类型
|
||||
|
||||
事件响应器类型 `type` 即是该响应器所要响应的事件类型,只有在接收到的事件类型与该响应器的类型相同时,才会触发该响应器。如果类型为空字符串 `""`,则响应器将会响应所有类型的事件。事件响应器类型的检查在所有其他检查(权限控制、响应规则)之前进行。
|
||||
|
||||
NoneBot 内置了四种常用事件类型:`meta_event`、`message`、`notice`、`request`,分别对应元事件、消息、通知、请求。通常情况下,协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应,可以自行定义新的类型。
|
||||
|
||||
### 事件触发权限
|
||||
|
||||
事件触发权限 `permission` 是一个 `Permission` 对象,这在[权限控制](../appendices/permission.mdx)一节中已经介绍过。事件触发权限会在事件响应器的类型检查通过后进行检查,如果权限检查通过,则执行响应规则检查。
|
||||
|
||||
### 事件响应规则
|
||||
|
||||
事件响应规则 `rule` 是一个 `Rule` 对象,这在[响应规则](../appendices/rule.md)一节中已经介绍过。事件响应器的响应规则会在事件响应器的权限检查通过后进行匹配,如果响应规则检查通过,则触发该响应器。
|
||||
|
||||
### 响应优先级
|
||||
|
||||
响应优先级 `priority` 是一个正整数,用于指定响应器的优先级。响应器的优先级越小,越先被触发。如果响应器的优先级相同,则按照响应器的注册顺序进行触发。
|
||||
|
||||
### 阻断
|
||||
|
||||
阻断 `block` 是一个布尔值,用于指定响应器是否阻断事件的传播。如果阻断为 `True`,则在该响应器被触发后,事件将不会再传播给其他下一优先级的响应器。
|
||||
|
||||
NoneBot 内置的事件响应器中,所有非 `command` 规则的 `message` 类型的事件响应器都会阻断事件传递,其他则不会。
|
||||
|
||||
在部分情况中,可以使用 [`stop_propagation`](../appendices/session-control.mdx#stop_propagation) 方法动态阻止事件传播,该方法需要 handler 在参数中获取 matcher 实例后调用方法。
|
||||
|
||||
### 有效期
|
||||
|
||||
事件响应器的有效期分为 `temp` 和 `expire_time` 。`temp` 是一个布尔值,用于指定响应器是否为临时响应器。如果为 `True`,则该响应器在被触发后会被自动销毁。`expire_time` 是一个 `datetime` 对象,用于指定响应器的过期时间。如果 `expire_time` 不为 `None`,则在该时间点后,该响应器会被自动销毁。
|
||||
|
||||
### 默认状态
|
||||
|
||||
事件响应器的默认状态 `default_state` 是一个 `dict` 对象,用于指定响应器的默认状态。在响应器被触发时,响应器将会初始化默认状态然后开始执行事件处理流程。
|
||||
|
||||
## 基本辅助函数
|
||||
|
||||
NoneBot 为四种类型的事件响应器提供了五个基本的辅助函数:
|
||||
|
||||
- `on`:创建任何类型的事件响应器。
|
||||
- `on_metaevent`:创建元事件响应器。
|
||||
- `on_message`:创建消息事件响应器。
|
||||
- `on_request`:创建请求事件响应器。
|
||||
- `on_notice`:创建通知事件响应器。
|
||||
|
||||
除了 `on` 函数具有一个 `type` 参数外,其余参数均相同:
|
||||
|
||||
- `rule`:响应规则,可以是 `Rule` 对象或者 `RuleChecker` 函数。
|
||||
- `permission`:事件触发权限,可以是 `Permission` 对象或者 `PermissionChecker` 函数。
|
||||
- `handlers`:事件处理函数列表。
|
||||
- `temp`:是否为临时响应器。
|
||||
- `expire_time`:响应器的过期时间。
|
||||
- `priority`:响应器的优先级。
|
||||
- `block`:是否阻断事件传播。
|
||||
- `state`:响应器的默认状态。
|
||||
|
||||
在消息类型的事件响应器的基础上,NoneBot 还内置了一些常用的响应规则,并结合为辅助函数来方便我们快速创建指定功能的响应器。下面我们逐个介绍。
|
||||
|
||||
## 内置响应规则
|
||||
|
||||
### `startswith`
|
||||
|
||||
`startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。
|
||||
|
||||
例如,我们可以创建一个匹配消息开头为 `!` 或者 `/` 的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import startswith
|
||||
|
||||
rule = startswith(("!", "/"), ignorecase=False)
|
||||
```
|
||||
|
||||
也可以直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_startswith
|
||||
|
||||
matcher = on_startswith(("!", "/"), ignorecase=False)
|
||||
```
|
||||
|
||||
### `endswith`
|
||||
|
||||
`endswith` 响应规则用于匹配消息纯文本部分的结尾是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。
|
||||
|
||||
例如,我们可以创建一个匹配消息结尾为 `.` 或者 `。` 的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import endswith
|
||||
|
||||
rule = endswith((".", "。"), ignorecase=False)
|
||||
```
|
||||
|
||||
也可以直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_endswith
|
||||
|
||||
matcher = on_endswith((".", "。"), ignorecase=False)
|
||||
```
|
||||
|
||||
### `fullmatch`
|
||||
|
||||
`fullmatch` 响应规则用于匹配消息纯文本部分是否与指定字符串(或一系列字符串)完全相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。
|
||||
|
||||
例如,我们可以创建一个匹配消息为 `ping` 或者 `pong` 的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import fullmatch
|
||||
|
||||
rule = fullmatch(("ping", "pong"), ignorecase=False)
|
||||
```
|
||||
|
||||
也可以直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_fullmatch
|
||||
|
||||
matcher = on_fullmatch(("ping", "pong"), ignorecase=False)
|
||||
```
|
||||
|
||||
### `keyword`
|
||||
|
||||
`keyword` 响应规则用于匹配消息纯文本部分是否包含指定字符串(或一系列字符串)。
|
||||
|
||||
例如,我们可以创建一个匹配消息中包含 `hello` 或者 `hi` 的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import keyword
|
||||
|
||||
rule = keyword("hello", "hi")
|
||||
```
|
||||
|
||||
也可以直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_keyword
|
||||
|
||||
matcher = on_keyword("hello", "hi")
|
||||
```
|
||||
|
||||
### `command`
|
||||
|
||||
`command` 是最常用的响应规则,它用于匹配消息是否为命令。它会根据配置中的 [Command Start 和 Command Separator](../appendices/config.mdx#command-start-和-command-separator) 来判断消息是否为命令。
|
||||
|
||||
例如,当我们配置了 `Command Start` 为 `/`,`Command Separator` 为 `.` 时:
|
||||
|
||||
```python
|
||||
from nonebot.rule import command
|
||||
|
||||
# 匹配 "/help" 或者 "/帮助" 开头的消息
|
||||
rule = command("help", "帮助")
|
||||
# 匹配 "/help.cmd" 开头的消息
|
||||
rule = command(("help", "cmd"))
|
||||
```
|
||||
|
||||
也可以直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_command
|
||||
|
||||
matcher = on_command("help", aliases={"帮助"})
|
||||
```
|
||||
|
||||
此外,`command` 响应规则默认允许消息命令与参数间不加空格,如果需要严格匹配命令与参数间的空白符,可以使用 `command` 函数的 `force_whitespace` 参数。`force_whitespace` 参数可以是 bool 类型或者具体的字符串,默认为 `False`。如果为 `True`,则命令与参数间必须有任意个数的空白符;如果为字符串,则命令与参数间必须有且与给定字符串一致的空白符。
|
||||
|
||||
```python
|
||||
rule = command("help", force_whitespace=True)
|
||||
rule = command("help", force_whitespace=" ")
|
||||
```
|
||||
|
||||
命令解析后的结果可以通过 [`Command`](./dependency.mdx#command)、[`RawCommand`](./dependency.mdx#rawcommand)、[`CommandArg`](./dependency.mdx#commandarg)、[`CommandStart`](./dependency.mdx#commandstart)、[`CommandWhitespace`](./dependency.mdx#commandwhitespace) 依赖注入获取。
|
||||
|
||||
### `shell_command`
|
||||
|
||||
`shell_command` 响应规则用于匹配类 shell 命令形式的消息。它首先与 [`command`](#command) 响应规则一样进行命令匹配,如果匹配成功,则会进行进一步的参数解析。参数解析采用 `argparse` 标准库进行,在此基础上添加了消息序列 `Message` 支持。
|
||||
|
||||
例如,我们可以创建一个匹配 `/cmd` 命令并且带有 `-v` 选项与默认 `-h` 帮助选项的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import shell_command, ArgumentParser
|
||||
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("-v", "--verbose", action="store_true")
|
||||
|
||||
rule = shell_command("cmd", parser=parser)
|
||||
```
|
||||
|
||||
更多关于 `argparse` 的使用方法请参考 [argparse 文档](https://docs.python.org/zh-cn/3/library/argparse.html)。我们也可以选择不提供 `parser` 参数,这样 `shell_command` 将不会解析参数,但会提供参数列表 `argv`。
|
||||
|
||||
直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_shell_command
|
||||
from nonebot.rule import ArgumentParser
|
||||
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("-v", "--verbose", action="store_true")
|
||||
|
||||
matcher = on_shell_command("cmd", parser=parser)
|
||||
```
|
||||
|
||||
参数解析后的结果可以通过 [`ShellCommandArgv`](./dependency.mdx#shellcommandargv)、[`ShellCommandArgs`](./dependency.mdx#shellcommandargs) 依赖注入获取。
|
||||
|
||||
### `regex`
|
||||
|
||||
`regex` 响应规则用于匹配消息是否与指定正则表达式匹配。
|
||||
|
||||
:::tip 提示
|
||||
正则表达式匹配使用 search 而非 match,如需从头匹配请使用 `r"^xxx"` 模式来确保匹配开头。
|
||||
:::
|
||||
|
||||
例如,我们可以创建一个匹配消息中包含字母并且忽略大小写的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import regex
|
||||
|
||||
rule = regex(r"[a-z]+", flags=re.IGNORECASE)
|
||||
```
|
||||
|
||||
也可以直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_regex
|
||||
|
||||
matcher = on_regex(r"[a-z]+", flags=re.IGNORECASE)
|
||||
```
|
||||
|
||||
正则匹配后的结果可以通过 [`RegexStr`](./dependency.mdx#regexstr)、[`RegexGroup`](./dependency.mdx#regexgroup)、[`RegexDict`](./dependency.mdx#regexdict) 依赖注入获取。
|
||||
|
||||
### `to_me`
|
||||
|
||||
`to_me` 响应规则用于匹配事件是否与机器人相关。
|
||||
|
||||
例如:
|
||||
|
||||
```python
|
||||
from nonebot.rule import to_me
|
||||
|
||||
rule = to_me()
|
||||
```
|
||||
|
||||
### `is_type`
|
||||
|
||||
`is_type` 响应规则用于匹配事件类型是否为指定类型(或者一系列类型)。
|
||||
|
||||
例如,我们可以创建一个匹配 OneBot v11 私聊和群聊消息事件的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import is_type
|
||||
from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent
|
||||
|
||||
rule = is_type(PrivateMessageEvent, GroupMessageEvent)
|
||||
```
|
||||
|
||||
## 响应器组
|
||||
|
||||
为了更方便的管理一系列功能相近的响应器,NoneBot 提供了两种响应器组,它们可以帮助我们进行响应器的统一管理。
|
||||
|
||||
### `CommandGroup`
|
||||
|
||||
`CommandGroup` 可以用于管理一系列具有相同前置命令的子命令响应器。
|
||||
|
||||
例如,我们创建 `/cmd`、`/cmd.sub`、`/cmd.help` 三个命令,他们具有相同的优先级:
|
||||
|
||||
```python
|
||||
from nonebot import CommandGroup
|
||||
|
||||
group = CommandGroup("cmd", priority=10)
|
||||
|
||||
cmd = group.command(tuple())
|
||||
sub_cmd = group.command("sub")
|
||||
help_cmd = group.command("help")
|
||||
```
|
||||
|
||||
### `MatcherGroup`
|
||||
|
||||
`MatcherGroup` 可以用于管理一系列具有相同属性的响应器。
|
||||
|
||||
例如,我们创建一个具有相同响应规则的响应器组:
|
||||
|
||||
```python
|
||||
from nonebot.rule import to_me
|
||||
from nonebot import MatcherGroup
|
||||
|
||||
group = MatcherGroup(rule=to_me())
|
||||
|
||||
matcher1 = group.on_message()
|
||||
matcher2 = group.on_message()
|
||||
```
|
@@ -1,95 +0,0 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
description: 自定义事件响应器的响应权限
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 40
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 权限控制
|
||||
|
||||
**权限控制**是机器人在实际应用中需要解决的重点问题之一,NoneBot2 提供了灵活的权限控制机制——`Permission`,接下来我们将简单说明。
|
||||
|
||||
## 应用
|
||||
|
||||
如同 `Rule` 一样,`Permission` 可以在[定义事件响应器](../tutorial/plugin/create-matcher.md)时添加 `permission` 参数来加以应用,这样 NoneBot2 会在事件响应时检测事件主体的权限。下面我们以 `SUPERUSER` 为例,对该机制的应用做一下介绍。
|
||||
|
||||
```python
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot import on_command
|
||||
|
||||
matcher = on_command("测试超管", permission=SUPERUSER)
|
||||
|
||||
|
||||
@matcher.handle()
|
||||
async def _():
|
||||
await matcher.send("超管命令测试成功")
|
||||
|
||||
|
||||
@matcher.got("key1", "超管提问")
|
||||
async def _():
|
||||
await matcher.send("超管命令 got 成功")
|
||||
```
|
||||
|
||||
在这段代码中,我们事件响应器指定了 `SUPERUSER` 这样一个权限,那么机器人只会响应超级管理员的 `测试超管` 命令,并且会响应该超级管理员的连续对话。
|
||||
|
||||
:::tip 提示
|
||||
在这里需要强调的是,`Permission` 与 `Rule` 的表现并不相同, `Rule` 只会在初次响应时生效,在余下的对话中并没有限制事件;但是 `Permission` 会持续生效,在连续对话中一直对事件主体加以限制。
|
||||
:::
|
||||
|
||||
## 进阶
|
||||
|
||||
`Permission` 除了可以在注册事件响应器时加以应用,还可以在编写事件处理函数 `handler` 时主动调用,我们可以利用这个特性在一个 `handler` 里对不同权限的事件主体进行区别响应,下面我们以 OneBot 适配器中的 `GROUP_ADMIN`(普通管理员非群主)和 `GROUP_OWNER` 为例,说明下怎么进行主动调用。
|
||||
|
||||
```python
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
|
||||
from nonebot.adapters.onebot.v11 import GROUP_ADMIN, GROUP_OWNER
|
||||
|
||||
matcher = on_command("测试权限")
|
||||
|
||||
@matcher.handle()
|
||||
async def _(bot: Bot, event: GroupMessageEvent):
|
||||
if await GROUP_ADMIN(bot, event):
|
||||
await matcher.send("管理员测试成功")
|
||||
elif await GROUP_OWNER(bot, event):
|
||||
await matcher.send("群主测试成功")
|
||||
else:
|
||||
await matcher.send("群员测试成功")
|
||||
```
|
||||
|
||||
在这段代码里,我们并没有对命令的权限指定,这个命令会响应所有在群聊中的 `测试权限` 命令,但是在 `handler` 里,我们对两个 `Permission` 进行主动调用,从而可以对不同的角色进行不同的响应。
|
||||
|
||||
## 自定义
|
||||
|
||||
如同 `Rule` 一样,`Permission` 也是由非负数个 `PermissionChecker` 组成的,但只需其中一个返回 `True` 时就会匹配成功。下面是自定义 `PermissionChecker` 和 `Permission` 的示例:
|
||||
|
||||
```python
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.permission import Permission
|
||||
|
||||
async def async_checker(bot: Bot, event: Event) -> bool:
|
||||
return True
|
||||
|
||||
def sync_checker(bot: Bot, event: Event) -> bool:
|
||||
return True
|
||||
|
||||
def check(arg1, arg2):
|
||||
|
||||
async def _checker(bot: Bot, event: Event) -> bool:
|
||||
return bool(arg1 + arg2)
|
||||
|
||||
return Permission(_checker)
|
||||
```
|
||||
|
||||
`Permission` 和 `PermissionChecker` 之间可以使用 `|`(或符号)互相组合:
|
||||
|
||||
```python
|
||||
from nonebot.permission import Permission
|
||||
|
||||
Permission(async_checker1) | sync_checker | async_checker2
|
||||
```
|
||||
|
||||
同样地,如果想用 `Permission(*checkers)` 包裹构造 `Permission`,函数必须是异步的;但是在利用 `|`(或符号)连接构造时,NoneBot2 会自动包裹同步函数为异步函数。
|
97
website/docs/advanced/plugin-info.md
Normal file
97
website/docs/advanced/plugin-info.md
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
description: 填写与获取插件相关的信息
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 30
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 插件信息
|
||||
|
||||
NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。同时,我们也可以通过 NoneBot 的插件系统来获取相关信息,例如插件的名称、使用方法,用于收集帮助信息等。下面我们将介绍如何为插件添加元数据,以及如何获取插件信息。
|
||||
|
||||
## 插件元数据
|
||||
|
||||
在 NoneBot 中,插件 [`Plugin`](../api/plugin/plugin.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。
|
||||
|
||||
现在,假设我们有一个插件 `example`, 它的模块结构如下:
|
||||
|
||||
```tree {4-6} title=Project
|
||||
📦 awesome-bot
|
||||
├── 📂 awesome_bot
|
||||
│ └── 📂 plugins
|
||||
| └── 📂 example
|
||||
| ├── 📜 __init__.py
|
||||
| └── 📜 config.py
|
||||
├── 📜 pyproject.toml
|
||||
└── 📜 README.md
|
||||
```
|
||||
|
||||
我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据,如下所示:
|
||||
|
||||
```python {1,5-11} title=example/__init__.py
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from .config import Config
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="示例插件",
|
||||
description="这是一个示例插件",
|
||||
usage="没什么用",
|
||||
config=Config,
|
||||
extra={},
|
||||
)
|
||||
```
|
||||
|
||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有两个可选的属性。`config` 属性用于指定插件的[配置类](../appendices/config.mdx#插件配置),`extra` 属性,它是一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。
|
||||
|
||||
请注意,这里的**插件名称**是供使用者或机器人用户查看的,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。
|
||||
|
||||
## 获取插件信息
|
||||
|
||||
NoneBot 提供了多种获取插件对象的方法,例如获取当前所有已导入的插件:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
plugins: set[Plugin] = nonebot.get_loaded_plugins()
|
||||
```
|
||||
|
||||
也可以通过插件索引名称获取插件对象:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
plugin: Plugin | None = nonebot.get_plugin("example")
|
||||
```
|
||||
|
||||
或者通过模块路径获取插件对象:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
plugin: Plugin | None = nonebot.get_plugin_by_module_name("awesome_bot.plugins.example")
|
||||
```
|
||||
|
||||
如果需要获取所有当前声明的插件名称(可能还未加载),可以使用 `get_available_plugin_names` 函数:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
plugin_names: set[str] = nonebot.get_available_plugin_names()
|
||||
```
|
||||
|
||||
插件对象 `Plugin` 中包含了多个属性:
|
||||
|
||||
- `name`:插件索引名称
|
||||
- `module`:插件模块
|
||||
- `module_name`:插件模块路径
|
||||
- `manager`:插件管理器
|
||||
- `matcher`:插件中定义的事件响应器
|
||||
- `parent_plugin`:插件的父插件
|
||||
- `sub_plugins`:插件的子插件集合
|
||||
- `metadata`:插件元数据
|
||||
|
||||
通过这些属性以及插件元数据,我们就可以收集所需要的插件信息了。
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user