mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-10-07 19:26:44 +00:00
Compare commits
316 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
b9392371c7 | ||
|
d3c26a1548 | ||
|
31c2a61cce | ||
|
f84ba9768b | ||
|
1faa935527 | ||
|
5f940ff309 | ||
|
4c4c0ea0ba | ||
|
787b40a99e | ||
|
fd6a0ae747 | ||
|
298a32c096 | ||
|
aecff5ffd6 | ||
|
c1a6b7b787 | ||
|
0903f19f9c | ||
|
51aa23817a | ||
|
8f3f385cb6 | ||
|
915274081d | ||
|
a388c52b3f | ||
|
b4d3cd4d4d | ||
|
50c03b0675 | ||
|
fa3bb96417 | ||
|
09bde57835 | ||
|
76ac2a8843 | ||
|
f6ec6962ab | ||
|
28ad6829cd | ||
|
7f4b002a87 | ||
|
7e073b6ff4 | ||
|
fa3781efe5 | ||
|
bec74d85cd | ||
|
abc3829c64 | ||
|
18f5d6eab9 | ||
|
00f3e30930 | ||
|
97cd21d004 | ||
|
09b4d44f23 | ||
|
3536bf56bd | ||
|
f8eaf5def0 | ||
|
6077f85e52 | ||
|
e2976a3859 | ||
|
1e25fde22e | ||
|
55d88b7dae | ||
|
de30f8917f | ||
|
52653fa005 | ||
|
4628358add | ||
|
117b08a73e | ||
|
700888a8e0 | ||
|
ef882927f3 | ||
|
af9327de14 | ||
|
2881d42bf5 | ||
|
dc3a49fe57 | ||
|
addabd6396 | ||
|
3341c641cc | ||
|
363413e1e6 | ||
|
b675d27a30 | ||
|
796023408a | ||
|
983a8512b2 | ||
|
6593102632 | ||
|
65fff13150 | ||
|
edd1a140d7 | ||
|
18070baad4 | ||
|
acf729f6e7 | ||
|
6dbc8eac03 | ||
|
35944bcbdc | ||
|
3f919f91c1 | ||
|
443a20d83d | ||
|
2fca26eaae | ||
|
ebc8141971 | ||
|
5d6bcc9b9b | ||
|
55fca332ba | ||
|
6b65c5fe69 | ||
|
3e4dbe1015 | ||
|
20197e64b2 | ||
|
94eecaf448 | ||
|
fa91e0e79b | ||
|
891adc38fc | ||
|
af6cc63db2 | ||
|
af73e14b64 | ||
|
9305fe7875 | ||
|
613fde4639 | ||
|
61db2c898b | ||
|
acf313c420 | ||
|
15fca08641 | ||
|
e2cbe3c1f8 | ||
|
d3883ea3ae | ||
|
8b2c4b3e60 | ||
|
65d0d00591 | ||
|
97a57c2f6e | ||
|
6559b2ff27 | ||
|
4c1deeb899 | ||
|
a65ea6805d | ||
|
effe65b034 | ||
|
37296cf048 | ||
|
1b597c1301 | ||
|
c2454d0689 | ||
|
9b60b44554 | ||
|
75516bdafb | ||
|
12f5a487c1 | ||
|
8d128d5035 | ||
|
cfa7117e64 | ||
|
7880bf0dc1 | ||
|
0054041829 | ||
|
99931f785a | ||
|
5e121269f0 | ||
|
38ced0243f | ||
|
869db878e1 | ||
|
e6c6e355e1 | ||
|
6221b9a5fd | ||
|
5f2c9c935b | ||
|
76559b253c | ||
|
3c54655c39 | ||
|
7a851ac199 | ||
|
b2ba5dfcd1 | ||
|
4a4fae8f8c | ||
|
de894ce7b2 | ||
|
09c4a955c9 | ||
|
db1581a0a2 | ||
|
db9d7b3060 | ||
|
7e0c29472e | ||
|
d13492070d | ||
|
695ede51ea | ||
|
168f382aa6 | ||
|
5bd433318d | ||
|
d1cd2a793e | ||
|
5a4464f338 | ||
|
561d25320b | ||
|
b225c2dd3b | ||
|
2a2e357513 | ||
|
28bfe1ecb8 | ||
|
cc12f0af7e | ||
|
da831a1b08 | ||
|
eb97be17dd | ||
|
2dd1c9b2ad | ||
|
41191db863 | ||
|
ee20204b22 | ||
|
f1032804bb | ||
|
ba1540d75b | ||
|
f5c87f80e1 | ||
|
d2d7603ff5 | ||
|
56013dca48 | ||
|
d33ed4a69f | ||
|
ed753b5564 | ||
|
7e65552d01 | ||
|
f77dc523e6 | ||
|
0d84bf3592 | ||
|
94dff49e60 | ||
|
5d4cf7e421 | ||
|
0e3e16e809 | ||
|
183fc8defb | ||
|
8712e89322 | ||
|
e2b49f9b65 | ||
|
7e11f3a3d6 | ||
|
71bebb6ec7 | ||
|
842c6ff4c6 | ||
|
7754f6da1d | ||
|
60e0752f1a | ||
|
ede1a20c53 | ||
|
04289fd50f | ||
|
ba3efa9e7c | ||
|
c5a66a6ed0 | ||
|
8a23b1554a | ||
|
d73f226cbd | ||
|
fd9ba678ec | ||
|
d29ba62ff9 | ||
|
00c97fd18f | ||
|
9531c3fa74 | ||
|
94293122e8 | ||
|
7aaa66c8ba | ||
|
0030bf725e | ||
|
22b6062900 | ||
|
005968ab70 | ||
|
dc6c194701 | ||
|
9b8772b590 | ||
|
ae8ba9f55d | ||
|
f4a7ce2c09 | ||
|
c84723668f | ||
|
bd3ed4207a | ||
|
1e8c2cfc9f | ||
|
5ce0238ace | ||
|
4e6b52b85c | ||
|
05fe7bb715 | ||
|
c555e2fac6 | ||
|
fd126ae154 | ||
|
6c7b6a9575 | ||
|
c4716e3e17 | ||
|
3601a33f20 | ||
|
451023518b | ||
|
2bd377a221 | ||
|
66384adad4 | ||
|
ec1f7ba5bc | ||
|
e7fc5b7b7e | ||
|
11477ea9d7 | ||
|
6adf40f45d | ||
|
1bdf169980 | ||
|
81cb356503 | ||
|
805778794c | ||
|
28cd8dd08a | ||
|
139b39984e | ||
|
f9b5fece80 | ||
|
8076c6bc0a | ||
|
44b89d13f8 | ||
|
fbc4225110 | ||
|
f07f35ccc1 | ||
|
111dfbf164 | ||
|
c713c7723b | ||
|
4fa2af41b0 | ||
|
39c09d22d1 | ||
|
4819b21f52 | ||
|
6ef6721527 | ||
|
14cb447874 | ||
|
1b2b89074d | ||
|
75c5678782 | ||
|
45ec5cdfb4 | ||
|
f6dd98825b | ||
|
f59271bd47 | ||
|
79f833b946 | ||
|
9ad562bbfd | ||
|
267b49247d | ||
|
dbda4150fb | ||
|
a4e17f0c49 | ||
|
8d8d1169d1 | ||
|
7bc9e61985 | ||
|
35cc6011b5 | ||
|
086af8fd22 | ||
|
a60d1520e6 | ||
|
30c22ba25a | ||
|
41fbaec42c | ||
|
562ec79e3b | ||
|
f620bd8eb2 | ||
|
13e40458d7 | ||
|
dc4ac6d8d7 | ||
|
41498bdf21 | ||
|
b8eae2eb82 | ||
|
039c2b5509 | ||
|
2e635370bb | ||
|
807a86371d | ||
|
c66953779c | ||
|
117ef18f1c | ||
|
520dd03d77 | ||
|
63f3ca2f6f | ||
|
2e8230e9f4 | ||
|
4bfea99e54 | ||
|
f58eba7975 | ||
|
53d1de4aec | ||
|
00f18c1bd8 | ||
|
ba4fbb2ec3 | ||
|
b3722bd637 | ||
|
012bd6d4fb | ||
|
9c4ca28d61 | ||
|
53bcae04ff | ||
|
754c54e268 | ||
|
f97fbc814e | ||
|
b8856a0577 | ||
|
1c0e88907b | ||
|
31b6df5b39 | ||
|
bca9e4fd08 | ||
|
026ceb5028 | ||
|
47d5a647b7 | ||
|
37d7230949 | ||
|
be458b1d5e | ||
|
f375a4a723 | ||
|
3edce9a630 | ||
|
c525bda1e0 | ||
|
417f586e0d | ||
|
80d7e68835 | ||
|
a284e6df5c | ||
|
7176a69f81 | ||
|
e3a1c02e8a | ||
|
5e789ae4e0 | ||
|
bb684e20cb | ||
|
e11293e46b | ||
|
e0d74a1657 | ||
|
fdd36565b1 | ||
|
28c53fe0d7 | ||
|
26539bf2b1 | ||
|
347889c822 | ||
|
91849b762c | ||
|
5d1319ddb9 | ||
|
d98228926e | ||
|
493997d998 | ||
|
3098b7c153 | ||
|
2b0a050226 | ||
|
1f3abc2bb9 | ||
|
dd5541e658 | ||
|
a76bf27f60 | ||
|
d70ce366cc | ||
|
f94b802c9b | ||
|
17d7bd4e31 | ||
|
76a40b60ff | ||
|
469efedab2 | ||
|
383699a8b4 | ||
|
1b9a07b923 | ||
|
15b76c266c | ||
|
dfdecaddb1 | ||
|
5de9de903d | ||
|
327f3fa441 | ||
|
08fde7580c | ||
|
4ca91ecc7e | ||
|
885db90bc0 | ||
|
c43d631eb5 | ||
|
cfda433d14 | ||
|
ea4a27bf89 | ||
|
23944833f2 | ||
|
4a40782be0 | ||
|
babafcaa87 | ||
|
9b164a6f5a | ||
|
4a07981972 | ||
|
6bb2c46f8a | ||
|
2054655912 | ||
|
062af45367 | ||
|
83c3ed5966 | ||
|
a2f2b818a7 | ||
|
e7941efd9a | ||
|
aa6faba9ae | ||
|
8ca72f3c64 | ||
|
45e10e7139 | ||
|
73d1b19669 | ||
|
ad4cf86a96 | ||
|
48b3e3aaf3 |
6
.eslintignore
Normal file
6
.eslintignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.yarn
|
||||||
|
.history
|
||||||
|
build
|
||||||
|
lib
|
85
.eslintrc.js
Normal file
85
.eslintrc.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
commonjs: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ["./tsconfig.json", "./website/tsconfig.json"],
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
JSX: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"plugin:regexp/recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
"import/resolver": {
|
||||||
|
node: {
|
||||||
|
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
||||||
|
},
|
||||||
|
typescript: true,
|
||||||
|
},
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["*.ts", "*.tsx"],
|
||||||
|
rules: {
|
||||||
|
"import/no-unresolved": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["*.js", "*.cjs"],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: ["@typescript-eslint"],
|
||||||
|
rules: {
|
||||||
|
"linebreak-style": ["error", "unix"],
|
||||||
|
quotes: ["error", "double", { avoidEscape: true }],
|
||||||
|
semi: ["error", "always"],
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
|
"import/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
groups: [
|
||||||
|
"builtin",
|
||||||
|
"external",
|
||||||
|
"internal",
|
||||||
|
"parent",
|
||||||
|
"sibling",
|
||||||
|
"index",
|
||||||
|
],
|
||||||
|
pathGroups: [
|
||||||
|
{ pattern: "react", group: "builtin", position: "before" },
|
||||||
|
{ pattern: "fs-extra", group: "builtin" },
|
||||||
|
{ pattern: "lodash", group: "external", position: "before" },
|
||||||
|
{ pattern: "clsx", group: "external", position: "before" },
|
||||||
|
{ pattern: "@theme/**", group: "internal" },
|
||||||
|
{ pattern: "@site/**", group: "internal" },
|
||||||
|
{ pattern: "@theme-init/**", group: "internal" },
|
||||||
|
{ pattern: "@theme-original/**", group: "internal" },
|
||||||
|
],
|
||||||
|
pathGroupsExcludedImportTypes: [],
|
||||||
|
"newlines-between": "always",
|
||||||
|
alphabetize: {
|
||||||
|
order: "asc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
website/versioned_*/** linguist-documentation
|
16
.github/actions/setup-node/action.yml
vendored
16
.github/actions/setup-node/action.yml
vendored
@@ -4,18 +4,10 @@ description: Setup Node
|
|||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
node-version: "18"
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
- id: yarn-cache-dir-path
|
- run: yarn install --frozen-lockfile
|
||||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
|
|
||||||
- run: yarn install
|
|
||||||
shell: bash
|
shell: bash
|
||||||
|
2
.github/actions/setup-python/action.yml
vendored
2
.github/actions/setup-python/action.yml
vendored
@@ -14,7 +14,7 @@ runs:
|
|||||||
run: pipx install poetry
|
run: pipx install poetry
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
architecture: "x64"
|
architecture: "x64"
|
||||||
|
31
.github/dependabot.yml
vendored
31
.github/dependabot.yml
vendored
@@ -4,3 +4,34 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/.github/actions/build-api-doc"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/.github/actions/setup-node"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/.github/actions/setup-python"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
8
.github/workflows/codecov.yml
vendored
8
.github/workflows/codecov.yml
vendored
@@ -9,6 +9,10 @@ on:
|
|||||||
- "nonebot/**"
|
- "nonebot/**"
|
||||||
- "packages/**"
|
- "packages/**"
|
||||||
- "tests/**"
|
- "tests/**"
|
||||||
|
- ".github/actions/setup-python/**"
|
||||||
|
- ".github/workflows/codecov.yml"
|
||||||
|
- "pyproject.toml"
|
||||||
|
- "poetry.lock"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -19,7 +23,7 @@ jobs:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
env:
|
env:
|
||||||
@@ -27,7 +31,7 @@ jobs:
|
|||||||
PYTHON_VERSION: ${{ matrix.python-version }}
|
PYTHON_VERSION: ${{ matrix.python-version }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Python environment
|
- name: Setup Python environment
|
||||||
uses: ./.github/actions/setup-python
|
uses: ./.github/actions/setup-python
|
||||||
|
14
.github/workflows/noneflow.yml
vendored
14
.github/workflows/noneflow.yml
vendored
@@ -47,9 +47,9 @@ jobs:
|
|||||||
run: pipx install poetry
|
run: pipx install poetry
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.x"
|
||||||
|
|
||||||
- name: Test Plugin
|
- name: Test Plugin
|
||||||
id: plugin-test
|
id: plugin-test
|
||||||
@@ -62,13 +62,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate token
|
- name: Generate token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.APP_ID }}
|
app_id: ${{ secrets.APP_ID }}
|
||||||
private_key: ${{ secrets.APP_KEY }}
|
private_key: ${{ secrets.APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
@@ -84,9 +84,9 @@ jobs:
|
|||||||
config: >
|
config: >
|
||||||
{
|
{
|
||||||
"base": "master",
|
"base": "master",
|
||||||
"plugin_path": "website/static/plugins.json",
|
"plugin_path": "assets/plugins.json",
|
||||||
"bot_path": "website/static/bots.json",
|
"bot_path": "assets/bots.json",
|
||||||
"adapter_path": "website/static/adapters.json"
|
"adapter_path": "assets/adapters.json"
|
||||||
}
|
}
|
||||||
env:
|
env:
|
||||||
PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }}
|
PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }}
|
||||||
|
2
.github/workflows/pyright.yml
vendored
2
.github/workflows/pyright.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
name: Pyright Lint
|
name: Pyright Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Python environment
|
- name: Setup Python environment
|
||||||
uses: ./.github/actions/setup-python
|
uses: ./.github/actions/setup-python
|
||||||
|
66
.github/workflows/release-drafter.yml
vendored
66
.github/workflows/release-drafter.yml
vendored
@@ -20,12 +20,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate token
|
- name: Generate token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.APP_ID }}
|
app_id: ${{ secrets.APP_ID }}
|
||||||
private_key: ${{ secrets.APP_KEY }}
|
private_key: ${{ secrets.APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- uses: release-drafter/release-drafter@v5
|
- uses: release-drafter/release-drafter@v5
|
||||||
id: release-drafter
|
id: release-drafter
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
- name: Update Changelog
|
- name: Update Changelog
|
||||||
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
||||||
@@ -59,8 +59,18 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Generate token
|
||||||
|
id: generate-token
|
||||||
|
uses: tibdex/github-app-token@v2
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.APP_ID }}
|
||||||
|
private_key: ${{ secrets.APP_KEY }}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Python Environment
|
- name: Setup Python Environment
|
||||||
uses: ./.github/actions/setup-python
|
uses: ./.github/actions/setup-python
|
||||||
@@ -71,33 +81,53 @@ jobs:
|
|||||||
- name: Build API Doc
|
- name: Build API Doc
|
||||||
uses: ./.github/actions/build-api-doc
|
uses: ./.github/actions/build-api-doc
|
||||||
|
|
||||||
- run: |
|
- name: Get Version
|
||||||
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
id: version
|
||||||
|
run: |
|
||||||
|
echo "VERSION=$(poetry version -s)" >> $GITHUB_OUTPUT
|
||||||
|
echo "TAG_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||||
|
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Check Version
|
||||||
|
if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION
|
||||||
|
run: exit 1
|
||||||
|
|
||||||
- uses: release-drafter/release-drafter@v5
|
- uses: release-drafter/release-drafter@v5
|
||||||
with:
|
with:
|
||||||
name: Release ${{ env.TAG_NAME }} 🌈
|
name: Release ${{ steps.version.outputs.TAG_NAME }} 🌈
|
||||||
tag: ${{ env.TAG_NAME }}
|
tag: ${{ steps.version.outputs.TAG_NAME }}
|
||||||
publish: true
|
publish: true
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
- name: Build and Publish Package
|
- name: Build Package
|
||||||
run: |
|
run: |
|
||||||
poetry build
|
poetry build
|
||||||
poetry publish -u ${{secrets.PYPI_USERNAME}} -p ${{secrets.PYPI_PASSWORD}}
|
|
||||||
gh release upload --clobber ${{ env.TAG_NAME }} dist/*.tar.gz dist/*.whl
|
- name: Publish package to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
|
||||||
|
- name: Publish package to GitHub
|
||||||
|
run: |
|
||||||
|
gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
- name: Build and Publish Doc Package
|
- name: Build and Publish Doc Package
|
||||||
run: |
|
run: |
|
||||||
yarn build:plugin --out-dir ../packages/nonebot-plugin-docs/nonebot_plugin_docs/dist
|
yarn build:plugin --out-dir ../packages/nonebot-plugin-docs/nonebot_plugin_docs/dist
|
||||||
export NONEBOT_VERSION=`poetry version -s`
|
|
||||||
cd packages/nonebot-plugin-docs/
|
cd packages/nonebot-plugin-docs/
|
||||||
poetry version $NONEBOT_VERSION
|
poetry version ${{ steps.version.outputs.VERSION }}
|
||||||
poetry build
|
poetry build
|
||||||
poetry publish -u ${{secrets.PYPI_USERNAME}} -p ${{secrets.PYPI_PASSWORD}}
|
|
||||||
gh release upload --clobber ${{ env.TAG_NAME }} dist/*.tar.gz dist/*.whl
|
- name: Publish Doc Package to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
with:
|
||||||
|
packages-dir: packages/nonebot-plugin-docs/dist/
|
||||||
|
|
||||||
|
- name: Publish Doc Package to GitHub
|
||||||
|
run: |
|
||||||
|
cd packages/nonebot-plugin-docs/
|
||||||
|
gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -9,12 +9,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate token
|
- name: Generate token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.APP_ID }}
|
app_id: ${{ secrets.APP_ID }}
|
||||||
private_key: ${{ secrets.APP_KEY }}
|
private_key: ${{ secrets.APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
|
2
.github/workflows/ruff.yml
vendored
2
.github/workflows/ruff.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
name: Ruff Lint
|
name: Ruff Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Run Ruff Lint
|
- name: Run Ruff Lint
|
||||||
uses: chartboost/ruff-action@v1
|
uses: chartboost/ruff-action@v1
|
||||||
|
4
.github/workflows/website-deploy.yml
vendored
4
.github/workflows/website-deploy.yml
vendored
@@ -13,7 +13,9 @@ jobs:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Python Environment
|
- name: Setup Python Environment
|
||||||
uses: ./.github/actions/setup-python
|
uses: ./.github/actions/setup-python
|
||||||
|
3
.github/workflows/website-preview.yml
vendored
3
.github/workflows/website-preview.yml
vendored
@@ -11,9 +11,10 @@ jobs:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Python Environment
|
- name: Setup Python Environment
|
||||||
uses: ./.github/actions/setup-python
|
uses: ./.github/actions/setup-python
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -139,7 +139,7 @@ fabric.properties
|
|||||||
.LSOverride
|
.LSOverride
|
||||||
|
|
||||||
# Icon must end with two \r
|
# Icon must end with two \r
|
||||||
Icon
|
# Icon
|
||||||
|
|
||||||
# Thumbnails
|
# Thumbnails
|
||||||
._*
|
._*
|
||||||
|
@@ -7,7 +7,7 @@ ci:
|
|||||||
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.0.276
|
rev: v0.1.6
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [--fix, --exit-non-zero-on-fix]
|
args: [--fix, --exit-non-zero-on-fix]
|
||||||
@@ -20,13 +20,13 @@ repos:
|
|||||||
stages: [commit]
|
stages: [commit]
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.3.0
|
rev: 23.11.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
stages: [commit]
|
stages: [commit]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
rev: v3.0.0-alpha.9-for-vscode
|
rev: v3.0.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
|
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
|
||||||
|
31
.stylelintrc.js
Normal file
31
.stylelintrc.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ["stylelint-config-standard", "stylelint-prettier/recommended"],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["*.css"],
|
||||||
|
rules: {
|
||||||
|
"function-no-unknown": [true, { ignoreFunctions: ["theme"] }],
|
||||||
|
"selector-class-pattern": [
|
||||||
|
"^([a-z][a-z0-9]*)(-[a-z0-9]+)*$",
|
||||||
|
{
|
||||||
|
resolveNestedSelectors: true,
|
||||||
|
message: (selector) =>
|
||||||
|
`Expected class selector "${selector}" to be kebab-case`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["*.module.css"],
|
||||||
|
rules: {
|
||||||
|
"selector-class-pattern": [
|
||||||
|
"^[a-z][a-zA-Z0-9]+$",
|
||||||
|
{
|
||||||
|
message: (selector) =>
|
||||||
|
`Expected class selector "${selector}" to be lowerCamelCase`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@@ -10,12 +10,10 @@
|
|||||||
|
|
||||||
### 报告问题、故障与漏洞
|
### 报告问题、故障与漏洞
|
||||||
|
|
||||||
NoneBot2 仍然是一个不够稳定的开发中项目,如果你在使用过程中发现问题并确信是由 NoneBot2 引起的,欢迎提交 Issue。
|
如果你在使用过程中发现问题并确信是由 NoneBot2 引起的,欢迎提交 Issue。
|
||||||
|
|
||||||
### 建议功能
|
### 建议功能
|
||||||
|
|
||||||
NoneBot2 还未进入正式版,欢迎在 Issue 中提议要加入哪些新功能。
|
|
||||||
|
|
||||||
为了让开发者更好地理解你的意图,请认真描述你所需要的特性,可能的话可以提出你认为可行的解决方案。
|
为了让开发者更好地理解你的意图,请认真描述你所需要的特性,可能的话可以提出你认为可行的解决方案。
|
||||||
|
|
||||||
## Pull Request
|
## Pull Request
|
||||||
|
57
README.md
57
README.md
@@ -54,6 +54,9 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
|||||||
<a href="https://onebot.dev/">
|
<a href="https://onebot.dev/">
|
||||||
<img src="https://img.shields.io/badge/OneBot-v12-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
|
<img src="https://img.shields.io/badge/OneBot-v12-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://bot.q.qq.com/wiki/">
|
||||||
|
<img src="https://img.shields.io/badge/QQ-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ">
|
||||||
|
</a>
|
||||||
<a href="https://core.telegram.org/bots/api">
|
<a href="https://core.telegram.org/bots/api">
|
||||||
<img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram" alt="telegram">
|
<img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram" alt="telegram">
|
||||||
</a>
|
</a>
|
||||||
@@ -63,9 +66,6 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
|||||||
<a href="https://docs.github.com/en/developers/apps">
|
<a href="https://docs.github.com/en/developers/apps">
|
||||||
<img src="https://img.shields.io/badge/GitHub-Bot-181717?style=social&logo=github" alt="github"/>
|
<img src="https://img.shields.io/badge/GitHub-Bot-181717?style=social&logo=github" alt="github"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://bot.q.qq.com/wiki/">
|
|
||||||
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ频道">
|
|
||||||
</a>
|
|
||||||
<!-- <a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p">
|
<!-- <a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p">
|
||||||
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="dingtalk"> -->
|
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="dingtalk"> -->
|
||||||
</a>
|
</a>
|
||||||
@@ -110,22 +110,26 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
|
|||||||
- 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源))
|
- 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源))
|
||||||
- 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
|
- 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
|
||||||
|
|
||||||
| 协议名称 | 状态 | 注释 |
|
| 协议名称 | 状态 | 注释 |
|
||||||
| :-------------------------------------------------------------------------------------------------------------------: | :--: | :-----------------------------------------------------------------------: |
|
| :--------------------------------------------------------------------------------------------------------------------------: | :--: | :-----------------------------------------------------------------------: |
|
||||||
| OneBot([仓库](https://github.com/nonebot/adapter-onebot),[协议](https://onebot.dev/)) | ✅ | 支持 QQ、TG、微信公众号、KOOK 等[平台](https://onebot.dev/ecosystem.html) |
|
| OneBot([仓库](https://github.com/nonebot/adapter-onebot),[协议](https://onebot.dev/)) | ✅ | 支持 QQ、TG、微信公众号、KOOK 等[平台](https://onebot.dev/ecosystem.html) |
|
||||||
| Telegram([仓库](https://github.com/nonebot/adapter-telegram),[协议](https://core.telegram.org/bots/api)) | ✅ | |
|
| Telegram([仓库](https://github.com/nonebot/adapter-telegram),[协议](https://core.telegram.org/bots/api)) | ✅ | |
|
||||||
| 飞书([仓库](https://github.com/nonebot/adapter-feishu),[协议](https://open.feishu.cn/document/home/index)) | ✅ | |
|
| 飞书([仓库](https://github.com/nonebot/adapter-feishu),[协议](https://open.feishu.cn/document/home/index)) | ✅ | |
|
||||||
| GitHub([仓库](https://github.com/nonebot/adapter-github),[协议](https://docs.github.com/en/apps)) | ✅ | GitHub APP & OAuth APP |
|
| GitHub([仓库](https://github.com/nonebot/adapter-github),[协议](https://docs.github.com/en/apps)) | ✅ | GitHub APP & OAuth APP |
|
||||||
| QQ 频道([仓库](https://github.com/nonebot/adapter-qqguild),[协议](https://bot.q.qq.com/wiki/)) | ✅ | 官方接口调整较多 |
|
| QQ([仓库](https://github.com/nonebot/adapter-qq),[协议](https://bot.q.qq.com/wiki/)) | ✅ | QQ 官方接口调整较多 |
|
||||||
| 钉钉([仓库](https://github.com/nonebot/adapter-ding),[协议](https://open.dingtalk.com/document/)) | 🤗 | 寻找 Maintainer(暂不可用) |
|
| 钉钉([仓库](https://github.com/nonebot/adapter-ding),[协议](https://open.dingtalk.com/document/)) | 🤗 | 寻找 Maintainer(暂不可用) |
|
||||||
| Console([仓库](https://github.com/nonebot/adapter-console)) | ✅ | 控制台交互 |
|
| Console([仓库](https://github.com/nonebot/adapter-console)) | ✅ | 控制台交互 |
|
||||||
| 开黑啦([仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila),[协议](https://developer.kookapp.cn/)) | ↗️ | 由社区贡献 |
|
| Red ([仓库](https://github.com/nonebot/adapter-red),[协议](https://chrononeko.github.io/QQNTRedProtocol/)) | ✅ | QQ 协议 |
|
||||||
| Mirai([仓库](https://github.com/ieew/nonebot_adapter_mirai2),[协议](https://docs.mirai.mamoe.net/mirai-api-http/)) | ↗️ | QQ 协议,由社区贡献 |
|
| Satori([仓库](https://github.com/nonebot/adapter-satori),[协议](https://satori.js.org/zh-CN)) | ✅ | 支持 Onebot、TG、飞书、微信公众号、Koishi 等 |
|
||||||
| Ntchat([仓库](https://github.com/JustUndertaker/adapter-ntchat)) | ↗️ | 微信协议,由社区贡献 |
|
| Discord ([仓库](https://github.com/nonebot/adapter-discord),[协议](https://discord.com/developers/docs/intro)) | ✅ | Discord Bot 协议 |
|
||||||
| MineCraft([仓库](https://github.com/17TheWord/nonebot-adapter-minecraft)) | ↗️ | 由社区贡献 |
|
| DoDo ([仓库](https://github.com/nonebot/adapter-dodo),[协议](https://open.imdodo.com/)) | ✅ | DoDo Bot 协议 |
|
||||||
| BiliBili Live([仓库](https://github.com/wwweww/adapter-bilibili)) | ↗️ | 由社区贡献 |
|
| 开黑啦([仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila),[协议](https://developer.kookapp.cn/)) | ↗️ | 由社区贡献 |
|
||||||
| Walle-Q([仓库](https://github.com/onebot-walle/nonebot_adapter_walleq)) | ↗️ | QQ 协议,由社区贡献 |
|
| Mirai([仓库](https://github.com/ieew/nonebot_adapter_mirai2),[协议](https://docs.mirai.mamoe.net/mirai-api-http/)) | ↗️ | QQ 协议,由社区贡献 |
|
||||||
| Villa([仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa)) | ↗️ | 米游社大别野 Bot 协议,由社区贡献 |
|
| Ntchat([仓库](https://github.com/JustUndertaker/adapter-ntchat)) | ↗️ | 微信协议,由社区贡献 |
|
||||||
|
| MineCraft([仓库](https://github.com/17TheWord/nonebot-adapter-minecraft)) | ↗️ | 由社区贡献 |
|
||||||
|
| BiliBili Live([仓库](https://github.com/wwweww/adapter-bilibili)) | ↗️ | 由社区贡献 |
|
||||||
|
| Walle-Q([仓库](https://github.com/onebot-walle/nonebot_adapter_walleq)) | ↗️ | QQ 协议,由社区贡献 |
|
||||||
|
| Villa([仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa),[协议](https://webstatic.mihoyo.com/vila/bot/doc/)) | ↗️ | 米游社大别野 Bot 协议,由社区贡献 |
|
||||||
|
|
||||||
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
|
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
|
||||||
|
|
||||||
@@ -204,9 +208,8 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
|
|||||||
或者尝试以下镜像:
|
或者尝试以下镜像:
|
||||||
|
|
||||||
- [文档镜像(中国境内)](https://nb2.baka.icu)
|
- [文档镜像(中国境内)](https://nb2.baka.icu)
|
||||||
- [文档镜像(Vercel)](https://nonebot2-vercel-mirror.vercel.app)
|
|
||||||
|
|
||||||
- 其他插件请查看 [商店](https://nonebot.dev/store)
|
- 其他插件请查看 [商店](https://nonebot.dev/store/plugins)
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
@@ -225,7 +228,17 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|||||||
|
|
||||||
请参考 [贡献指南](./CONTRIBUTING.md)
|
请参考 [贡献指南](./CONTRIBUTING.md)
|
||||||
|
|
||||||
### 鸣谢
|
## 鸣谢
|
||||||
|
|
||||||
|
### 赞助者
|
||||||
|
|
||||||
|
感谢以下赞助者对 NoneBot 项目提供的资金支持:
|
||||||
|
|
||||||
|
<a href="https://assets.nonebot.dev/sponsors.svg">
|
||||||
|
<img src='https://assets.nonebot.dev/sponsors.svg'/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### 开发者
|
||||||
|
|
||||||
感谢以下开发者对 NoneBot2 作出的贡献:
|
感谢以下开发者对 NoneBot2 作出的贡献:
|
||||||
|
|
||||||
|
@@ -40,12 +40,12 @@
|
|||||||
"is_official": true
|
"is_official": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"module_name": "nonebot.adapters.qqguild",
|
"module_name": "nonebot.adapters.qq",
|
||||||
"project_link": "nonebot-adapter-qqguild",
|
"project_link": "nonebot-adapter-qq",
|
||||||
"name": "QQ 频道",
|
"name": "QQ",
|
||||||
"desc": "QQ 频道官方机器人",
|
"desc": "QQ 官方机器人",
|
||||||
"author": "yanyongyu",
|
"author": "yanyongyu",
|
||||||
"homepage": "https://github.com/nonebot/adapter-qqguild",
|
"homepage": "https://github.com/nonebot/adapter-qq",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
},
|
},
|
||||||
@@ -168,5 +168,50 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot.adapters.red",
|
||||||
|
"project_link": "nonebot-adapter-red",
|
||||||
|
"name": "RedProtocol",
|
||||||
|
"desc": "QQNT RedProtocol 适配",
|
||||||
|
"author": "zhaomaoniu",
|
||||||
|
"homepage": "https://github.com/nonebot/adapter-red",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot.adapters.discord",
|
||||||
|
"project_link": "nonebot-adapter-discord",
|
||||||
|
"name": "Discord",
|
||||||
|
"desc": "Discord 官方 Bot 协议适配",
|
||||||
|
"author": "CMHopeSunshine",
|
||||||
|
"homepage": "https://github.com/nonebot/adapter-discord",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot.adapters.satori",
|
||||||
|
"project_link": "nonebot-adapter-satori",
|
||||||
|
"name": "Satori",
|
||||||
|
"desc": "Satori 协议适配器",
|
||||||
|
"author": "RF-Tar-Railt",
|
||||||
|
"homepage": "https://github.com/nonebot/adapter-satori",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "跨平台",
|
||||||
|
"color": "#bf40bf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot.adapters.dodo",
|
||||||
|
"project_link": "nonebot-adapter-dodo",
|
||||||
|
"name": "DoDo",
|
||||||
|
"desc": "DoDo Bot 协议适配器",
|
||||||
|
"author": "CMHopeSunshine",
|
||||||
|
"homepage": "https://github.com/nonebot/adapter-dodo",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": true
|
||||||
}
|
}
|
||||||
]
|
]
|
@@ -541,5 +541,71 @@
|
|||||||
"homepage": "https://github.com/LambdaYH/MigangBot",
|
"homepage": "https://github.com/LambdaYH/MigangBot",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "不正经的妹妹",
|
||||||
|
"desc": "一款功能丰富、简单易用、自定义性强、扩展性强的可爱的QQ娱乐机器人",
|
||||||
|
"author": "itsevin",
|
||||||
|
"homepage": "https://github.com/itsevin/sister_bot",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "星见Kirami",
|
||||||
|
"desc": "🌟 读作 Kirami,写作星见,简明轻快的聊天机器人应用。",
|
||||||
|
"author": "A-kirami",
|
||||||
|
"homepage": "https://kiramibot.dev/",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OCNbot",
|
||||||
|
"desc": "OI Contest Notifier bot,一个可以推送洛谷、cf、atcoder、牛客比赛通知的bot",
|
||||||
|
"author": "ACnoway",
|
||||||
|
"homepage": "https://github.com/ACnoway/OCNbot",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "OI",
|
||||||
|
"color": "#2fccff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "ACM",
|
||||||
|
"color": "#ff0004"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "妃爱",
|
||||||
|
"desc": "超可爱的妃爱QQ群聊机器人",
|
||||||
|
"author": "jiangyuxiaoxiao",
|
||||||
|
"homepage": "https://github.com/jiangyuxiaoxiao/Hiyori",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "芙芙",
|
||||||
|
"desc": "供 Mooncell Wiki 协作使用的跨平台机器人",
|
||||||
|
"author": "StarHeartHunt",
|
||||||
|
"homepage": "https://github.com/MooncellWiki/BotFooChan",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sakiko",
|
||||||
|
"desc": "基于 LiteLoaderBDS 的 Minecraft 基岩版 Bot",
|
||||||
|
"author": "zhaomaoniu",
|
||||||
|
"homepage": "https://github.com/zhaomaoniu/Sakiko",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "Minecraft",
|
||||||
|
"color": "#6cc349"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "BanGDream",
|
||||||
|
"color": "#e70050"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
}
|
}
|
||||||
]
|
]
|
5319
assets/plugins.json
Normal file
5319
assets/plugins.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -53,7 +53,7 @@ from nonebot.config import Env, Config
|
|||||||
from nonebot.log import logger as logger
|
from nonebot.log import logger as logger
|
||||||
from nonebot.adapters import Bot, Adapter
|
from nonebot.adapters import Bot, Adapter
|
||||||
from nonebot.utils import escape_tag, resolve_dot_notation
|
from nonebot.utils import escape_tag, resolve_dot_notation
|
||||||
from nonebot.drivers import Driver, ReverseDriver, combine_driver
|
from nonebot.drivers import Driver, ASGIMixin, combine_driver
|
||||||
|
|
||||||
try:
|
try:
|
||||||
__version__ = version("nonebot2")
|
__version__ = version("nonebot2")
|
||||||
@@ -149,13 +149,13 @@ def get_adapters() -> Dict[str, Adapter]:
|
|||||||
|
|
||||||
|
|
||||||
def get_app() -> Any:
|
def get_app() -> Any:
|
||||||
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应的 Server App 对象。
|
"""获取全局 {ref}`nonebot.drivers.ASGIMixin` 对应的 Server App 对象。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
Server App 对象
|
Server App 对象
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ReverseDriver` 类型
|
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ASGIMixin` 类型
|
||||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||||
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||||
|
|
||||||
@@ -165,21 +165,19 @@ def get_app() -> Any:
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
driver = get_driver()
|
driver = get_driver()
|
||||||
assert isinstance(
|
assert isinstance(driver, ASGIMixin), "app object is only available for asgi driver"
|
||||||
driver, ReverseDriver
|
|
||||||
), "app object is only available for reverse driver"
|
|
||||||
return driver.server_app
|
return driver.server_app
|
||||||
|
|
||||||
|
|
||||||
def get_asgi() -> Any:
|
def get_asgi() -> Any:
|
||||||
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应
|
"""获取全局 {ref}`nonebot.drivers.ASGIMixin` 对应的
|
||||||
[ASGI](https://asgi.readthedocs.io/) 对象。
|
[ASGI](https://asgi.readthedocs.io/) 对象。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
ASGI 对象
|
ASGI 对象
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ReverseDriver` 类型
|
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ASGIMixin` 类型
|
||||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||||
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||||
|
|
||||||
@@ -190,8 +188,8 @@ def get_asgi() -> Any:
|
|||||||
"""
|
"""
|
||||||
driver = get_driver()
|
driver = get_driver()
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
driver, ReverseDriver
|
driver, ASGIMixin
|
||||||
), "asgi object is only available for reverse driver"
|
), "asgi object is only available for asgi driver"
|
||||||
return driver.asgi
|
return driver.asgi
|
||||||
|
|
||||||
|
|
||||||
|
@@ -45,6 +45,10 @@ class Param(abc.ABC, FieldInfo):
|
|||||||
继承自 `pydantic.fields.FieldInfo`,用于描述参数信息(不包括参数名)。
|
继承自 `pydantic.fields.FieldInfo`,用于描述参数信息(不包括参数名)。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, validate: bool = False, **kwargs: Any) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.validate = validate
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type["Param"], ...]
|
cls, param: inspect.Parameter, allow_types: Tuple[Type["Param"], ...]
|
||||||
@@ -97,22 +101,26 @@ class Dependent(Generic[R]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def __call__(self, **kwargs: Any) -> R:
|
async def __call__(self, **kwargs: Any) -> R:
|
||||||
# do pre-check
|
try:
|
||||||
await self.check(**kwargs)
|
# do pre-check
|
||||||
|
await self.check(**kwargs)
|
||||||
|
|
||||||
# solve param values
|
# solve param values
|
||||||
values = await self.solve(**kwargs)
|
values = await self.solve(**kwargs)
|
||||||
|
|
||||||
# call function
|
# call function
|
||||||
if is_coroutine_callable(self.call):
|
if is_coroutine_callable(self.call):
|
||||||
return await cast(Callable[..., Awaitable[R]], self.call)(**values)
|
return await cast(Callable[..., Awaitable[R]], self.call)(**values)
|
||||||
else:
|
else:
|
||||||
return await run_sync(cast(Callable[..., R], self.call))(**values)
|
return await run_sync(cast(Callable[..., R], self.call))(**values)
|
||||||
|
except SkippedException as e:
|
||||||
|
logger.trace(f"{self} skipped due to {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_params(
|
def parse_params(
|
||||||
call: _DependentCallable[R], allow_types: Tuple[Type[Param], ...]
|
call: _DependentCallable[R], allow_types: Tuple[Type[Param], ...]
|
||||||
) -> Tuple[ModelField]:
|
) -> Tuple[ModelField, ...]:
|
||||||
fields: List[ModelField] = []
|
fields: List[ModelField] = []
|
||||||
params = get_typed_signature(call).parameters.values()
|
params = get_typed_signature(call).parameters.values()
|
||||||
|
|
||||||
@@ -191,25 +199,18 @@ class Dependent(Generic[R]):
|
|||||||
return cls(call, params, parameterless_params)
|
return cls(call, params, parameterless_params)
|
||||||
|
|
||||||
async def check(self, **params: Any) -> None:
|
async def check(self, **params: Any) -> None:
|
||||||
try:
|
await asyncio.gather(*(param._check(**params) for param in self.parameterless))
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*(param._check(**params) for param in self.parameterless)
|
*(cast(Param, param.field_info)._check(**params) for param in self.params)
|
||||||
)
|
)
|
||||||
await asyncio.gather(
|
|
||||||
*(
|
|
||||||
cast(Param, param.field_info)._check(**params)
|
|
||||||
for param in self.params
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except SkippedException as e:
|
|
||||||
logger.trace(f"{self} skipped due to {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def _solve_field(self, field: ModelField, params: Dict[str, Any]) -> Any:
|
async def _solve_field(self, field: ModelField, params: Dict[str, Any]) -> Any:
|
||||||
value = await cast(Param, field.field_info)._solve(**params)
|
param = cast(Param, field.field_info)
|
||||||
|
value = await param._solve(**params)
|
||||||
if value is Undefined:
|
if value is Undefined:
|
||||||
value = field.get_default()
|
value = field.get_default()
|
||||||
return check_field_type(field, value)
|
v = check_field_type(field, value)
|
||||||
|
return v if param.validate else value
|
||||||
|
|
||||||
async def solve(self, **params: Any) -> Dict[str, Any]:
|
async def solve(self, **params: Any) -> Dict[str, Any]:
|
||||||
# solve parameterless
|
# solve parameterless
|
||||||
|
@@ -5,7 +5,7 @@ FrontMatter:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
from typing import Any, Dict, TypeVar, Callable, ForwardRef
|
from typing import Any, Dict, Callable, ForwardRef
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic.fields import ModelField
|
from pydantic.fields import ModelField
|
||||||
@@ -13,8 +13,6 @@ from pydantic.typing import evaluate_forwardref
|
|||||||
|
|
||||||
from nonebot.exception import TypeMisMatch
|
from nonebot.exception import TypeMisMatch
|
||||||
|
|
||||||
V = TypeVar("V")
|
|
||||||
|
|
||||||
|
|
||||||
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
||||||
"""获取可调用对象签名"""
|
"""获取可调用对象签名"""
|
||||||
@@ -49,10 +47,10 @@ def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) ->
|
|||||||
return annotation
|
return annotation
|
||||||
|
|
||||||
|
|
||||||
def check_field_type(field: ModelField, value: V) -> V:
|
def check_field_type(field: ModelField, value: Any) -> Any:
|
||||||
"""检查字段类型是否匹配"""
|
"""检查字段类型是否匹配"""
|
||||||
|
|
||||||
_, errs_ = field.validate(value, {}, loc=())
|
v, errs_ = field.validate(value, {}, loc=())
|
||||||
if errs_:
|
if errs_:
|
||||||
raise TypeMisMatch(field, value)
|
raise TypeMisMatch(field, value)
|
||||||
return value
|
return v
|
||||||
|
@@ -8,30 +8,40 @@ FrontMatter:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from nonebot.internal.driver import URL as URL
|
from nonebot.internal.driver import URL as URL
|
||||||
|
from nonebot.internal.driver import Mixin as Mixin
|
||||||
from nonebot.internal.driver import Driver as Driver
|
from nonebot.internal.driver import Driver as Driver
|
||||||
from nonebot.internal.driver import Cookies as Cookies
|
from nonebot.internal.driver import Cookies as Cookies
|
||||||
from nonebot.internal.driver import Request as Request
|
from nonebot.internal.driver import Request as Request
|
||||||
from nonebot.internal.driver import Response as Response
|
from nonebot.internal.driver import Response as Response
|
||||||
|
from nonebot.internal.driver import ASGIMixin as ASGIMixin
|
||||||
from nonebot.internal.driver import WebSocket as WebSocket
|
from nonebot.internal.driver import WebSocket as WebSocket
|
||||||
from nonebot.internal.driver import HTTPVersion as HTTPVersion
|
from nonebot.internal.driver import HTTPVersion as HTTPVersion
|
||||||
from nonebot.internal.driver import ForwardMixin as ForwardMixin
|
from nonebot.internal.driver import ForwardMixin as ForwardMixin
|
||||||
|
from nonebot.internal.driver import ReverseMixin as ReverseMixin
|
||||||
from nonebot.internal.driver import ForwardDriver as ForwardDriver
|
from nonebot.internal.driver import ForwardDriver as ForwardDriver
|
||||||
from nonebot.internal.driver import ReverseDriver as ReverseDriver
|
from nonebot.internal.driver import ReverseDriver as ReverseDriver
|
||||||
from nonebot.internal.driver import combine_driver as combine_driver
|
from nonebot.internal.driver import combine_driver as combine_driver
|
||||||
|
from nonebot.internal.driver import HTTPClientMixin as HTTPClientMixin
|
||||||
from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup
|
from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup
|
||||||
|
from nonebot.internal.driver import WebSocketClientMixin as WebSocketClientMixin
|
||||||
from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup
|
from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup
|
||||||
|
|
||||||
__autodoc__ = {
|
__autodoc__ = {
|
||||||
"URL": True,
|
"URL": True,
|
||||||
"Driver": True,
|
|
||||||
"Cookies": True,
|
"Cookies": True,
|
||||||
"Request": True,
|
"Request": True,
|
||||||
"Response": True,
|
"Response": True,
|
||||||
"WebSocket": True,
|
"WebSocket": True,
|
||||||
"HTTPVersion": True,
|
"HTTPVersion": True,
|
||||||
|
"Driver": True,
|
||||||
|
"Mixin": True,
|
||||||
"ForwardMixin": True,
|
"ForwardMixin": True,
|
||||||
"ForwardDriver": True,
|
"ForwardDriver": True,
|
||||||
|
"HTTPClientMixin": True,
|
||||||
|
"WebSocketClientMixin": True,
|
||||||
|
"ReverseMixin": True,
|
||||||
"ReverseDriver": True,
|
"ReverseDriver": True,
|
||||||
|
"ASGIMixin": True,
|
||||||
"combine_driver": True,
|
"combine_driver": True,
|
||||||
"HTTPServerSetup": True,
|
"HTTPServerSetup": True,
|
||||||
"WebSocketServerSetup": True,
|
"WebSocketServerSetup": True,
|
||||||
|
@@ -16,14 +16,19 @@ FrontMatter:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
from typing import Type, AsyncGenerator
|
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import TYPE_CHECKING, AsyncGenerator
|
||||||
|
|
||||||
from nonebot.drivers import Request, Response
|
from nonebot.drivers import Request, Response
|
||||||
from nonebot.exception import WebSocketClosed
|
from nonebot.exception import WebSocketClosed
|
||||||
from nonebot.drivers.none import Driver as NoneDriver
|
from nonebot.drivers.none import Driver as NoneDriver
|
||||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||||
from nonebot.drivers import HTTPVersion, ForwardMixin, ForwardDriver, combine_driver
|
from nonebot.drivers import (
|
||||||
|
HTTPVersion,
|
||||||
|
HTTPClientMixin,
|
||||||
|
WebSocketClientMixin,
|
||||||
|
combine_driver,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -34,7 +39,7 @@ except ModuleNotFoundError as e: # pragma: no cover
|
|||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
class Mixin(ForwardMixin):
|
class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
||||||
"""AIOHTTP Mixin"""
|
"""AIOHTTP Mixin"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -172,5 +177,11 @@ class WebSocket(BaseWebSocket):
|
|||||||
await self.websocket.send_bytes(data)
|
await self.websocket.send_bytes(data)
|
||||||
|
|
||||||
|
|
||||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
if TYPE_CHECKING:
|
||||||
"""AIOHTTP Driver"""
|
|
||||||
|
class Driver(Mixin, NoneDriver):
|
||||||
|
...
|
||||||
|
|
||||||
|
else:
|
||||||
|
Driver = combine_driver(NoneDriver, Mixin)
|
||||||
|
"""AIOHTTP Driver"""
|
||||||
|
@@ -25,14 +25,14 @@ from typing import Any, Dict, List, Tuple, Union, Optional
|
|||||||
from pydantic import BaseSettings
|
from pydantic import BaseSettings
|
||||||
|
|
||||||
from nonebot.config import Env
|
from nonebot.config import Env
|
||||||
|
from nonebot.drivers import ASGIMixin
|
||||||
from nonebot.exception import WebSocketClosed
|
from nonebot.exception import WebSocketClosed
|
||||||
from nonebot.internal.driver import FileTypes
|
from nonebot.internal.driver import FileTypes
|
||||||
|
from nonebot.drivers import Driver as BaseDriver
|
||||||
from nonebot.config import Config as NoneBotConfig
|
from nonebot.config import Config as NoneBotConfig
|
||||||
from nonebot.drivers import Request as BaseRequest
|
from nonebot.drivers import Request as BaseRequest
|
||||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||||
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
|
from nonebot.drivers import HTTPServerSetup, WebSocketServerSetup
|
||||||
|
|
||||||
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@@ -87,7 +87,7 @@ class Config(BaseSettings):
|
|||||||
extra = "ignore"
|
extra = "ignore"
|
||||||
|
|
||||||
|
|
||||||
class Driver(ReverseDriver):
|
class Driver(BaseDriver, ASGIMixin):
|
||||||
"""FastAPI 驱动框架。"""
|
"""FastAPI 驱动框架。"""
|
||||||
|
|
||||||
def __init__(self, env: Env, config: NoneBotConfig):
|
def __init__(self, env: Env, config: NoneBotConfig):
|
||||||
@@ -95,8 +95,6 @@ class Driver(ReverseDriver):
|
|||||||
|
|
||||||
self.fastapi_config: Config = Config(**config.dict())
|
self.fastapi_config: Config = Config(**config.dict())
|
||||||
|
|
||||||
self._lifespan = Lifespan()
|
|
||||||
|
|
||||||
self._server_app = FastAPI(
|
self._server_app = FastAPI(
|
||||||
lifespan=self._lifespan_manager,
|
lifespan=self._lifespan_manager,
|
||||||
openapi_url=self.fastapi_config.fastapi_openapi_url,
|
openapi_url=self.fastapi_config.fastapi_openapi_url,
|
||||||
@@ -153,14 +151,6 @@ class Driver(ReverseDriver):
|
|||||||
name=setup.name,
|
name=setup.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
@override
|
|
||||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
|
||||||
return self._lifespan.on_startup(func)
|
|
||||||
|
|
||||||
@override
|
|
||||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
|
||||||
return self._lifespan.on_shutdown(func)
|
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def _lifespan_manager(self, app: FastAPI):
|
async def _lifespan_manager(self, app: FastAPI):
|
||||||
await self._lifespan.startup()
|
await self._lifespan.startup()
|
||||||
@@ -179,7 +169,7 @@ class Driver(ReverseDriver):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""使用 `uvicorn` 启动 FastAPI"""
|
"""使用 `uvicorn` 启动 FastAPI"""
|
||||||
super().run(host, port, app, **kwargs)
|
super().run(host, port, app=app, **kwargs)
|
||||||
LOGGING_CONFIG = {
|
LOGGING_CONFIG = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"disable_existing_loggers": False,
|
"disable_existing_loggers": False,
|
||||||
|
@@ -15,18 +15,15 @@ FrontMatter:
|
|||||||
description: nonebot.drivers.httpx 模块
|
description: nonebot.drivers.httpx 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
from typing import Type, AsyncGenerator
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
|
|
||||||
from nonebot.drivers.none import Driver as NoneDriver
|
from nonebot.drivers.none import Driver as NoneDriver
|
||||||
from nonebot.drivers import (
|
from nonebot.drivers import (
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
WebSocket,
|
|
||||||
HTTPVersion,
|
HTTPVersion,
|
||||||
ForwardMixin,
|
HTTPClientMixin,
|
||||||
ForwardDriver,
|
|
||||||
combine_driver,
|
combine_driver,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,7 +36,7 @@ except ModuleNotFoundError as e: # pragma: no cover
|
|||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
class Mixin(ForwardMixin):
|
class Mixin(HTTPClientMixin):
|
||||||
"""HTTPX Mixin"""
|
"""HTTPX Mixin"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -72,12 +69,12 @@ class Mixin(ForwardMixin):
|
|||||||
request=setup,
|
request=setup,
|
||||||
)
|
)
|
||||||
|
|
||||||
@override
|
|
||||||
@asynccontextmanager
|
|
||||||
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
|
|
||||||
async with super(Mixin, self).websocket(setup) as ws:
|
|
||||||
yield ws
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
class Driver(Mixin, NoneDriver):
|
||||||
"""HTTPX Driver"""
|
...
|
||||||
|
|
||||||
|
else:
|
||||||
|
Driver = combine_driver(NoneDriver, Mixin)
|
||||||
|
"""HTTPX Driver"""
|
||||||
|
@@ -19,8 +19,6 @@ from nonebot.consts import WINDOWS
|
|||||||
from nonebot.config import Env, Config
|
from nonebot.config import Env, Config
|
||||||
from nonebot.drivers import Driver as BaseDriver
|
from nonebot.drivers import Driver as BaseDriver
|
||||||
|
|
||||||
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
|
||||||
|
|
||||||
HANDLED_SIGNALS = (
|
HANDLED_SIGNALS = (
|
||||||
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
||||||
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
|
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
|
||||||
@@ -35,8 +33,6 @@ class Driver(BaseDriver):
|
|||||||
def __init__(self, env: Env, config: Config):
|
def __init__(self, env: Env, config: Config):
|
||||||
super().__init__(env, config)
|
super().__init__(env, config)
|
||||||
|
|
||||||
self._lifespan = Lifespan()
|
|
||||||
|
|
||||||
self.should_exit: asyncio.Event = asyncio.Event()
|
self.should_exit: asyncio.Event = asyncio.Event()
|
||||||
self.force_exit: bool = False
|
self.force_exit: bool = False
|
||||||
|
|
||||||
@@ -52,16 +48,6 @@ class Driver(BaseDriver):
|
|||||||
"""none driver 使用的 logger"""
|
"""none driver 使用的 logger"""
|
||||||
return logger
|
return logger
|
||||||
|
|
||||||
@override
|
|
||||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
|
||||||
"""注册一个启动时执行的函数"""
|
|
||||||
return self._lifespan.on_startup(func)
|
|
||||||
|
|
||||||
@override
|
|
||||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
|
||||||
"""注册一个停止时执行的函数"""
|
|
||||||
return self._lifespan.on_shutdown(func)
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
"""启动 none driver"""
|
"""启动 none driver"""
|
||||||
|
@@ -18,28 +18,19 @@ FrontMatter:
|
|||||||
import asyncio
|
import asyncio
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
from typing import (
|
from typing import Any, Dict, List, Tuple, Union, Optional, cast
|
||||||
Any,
|
|
||||||
Dict,
|
|
||||||
List,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
TypeVar,
|
|
||||||
Callable,
|
|
||||||
Optional,
|
|
||||||
Coroutine,
|
|
||||||
cast,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pydantic import BaseSettings
|
from pydantic import BaseSettings
|
||||||
|
|
||||||
from nonebot.config import Env
|
from nonebot.config import Env
|
||||||
|
from nonebot.drivers import ASGIMixin
|
||||||
from nonebot.exception import WebSocketClosed
|
from nonebot.exception import WebSocketClosed
|
||||||
from nonebot.internal.driver import FileTypes
|
from nonebot.internal.driver import FileTypes
|
||||||
|
from nonebot.drivers import Driver as BaseDriver
|
||||||
from nonebot.config import Config as NoneBotConfig
|
from nonebot.config import Config as NoneBotConfig
|
||||||
from nonebot.drivers import Request as BaseRequest
|
from nonebot.drivers import Request as BaseRequest
|
||||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||||
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
|
from nonebot.drivers import HTTPServerSetup, WebSocketServerSetup
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@@ -55,8 +46,6 @@ except ModuleNotFoundError as e: # pragma: no cover
|
|||||||
"Install with pip: `pip install nonebot2[quart]`"
|
"Install with pip: `pip install nonebot2[quart]`"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine])
|
|
||||||
|
|
||||||
|
|
||||||
def catch_closed(func):
|
def catch_closed(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
@@ -89,7 +78,7 @@ class Config(BaseSettings):
|
|||||||
extra = "ignore"
|
extra = "ignore"
|
||||||
|
|
||||||
|
|
||||||
class Driver(ReverseDriver):
|
class Driver(BaseDriver, ASGIMixin):
|
||||||
"""Quart 驱动框架"""
|
"""Quart 驱动框架"""
|
||||||
|
|
||||||
def __init__(self, env: Env, config: NoneBotConfig):
|
def __init__(self, env: Env, config: NoneBotConfig):
|
||||||
@@ -100,6 +89,8 @@ class Driver(ReverseDriver):
|
|||||||
self._server_app = Quart(
|
self._server_app = Quart(
|
||||||
self.__class__.__qualname__, **self.quart_config.quart_extra
|
self.__class__.__qualname__, **self.quart_config.quart_extra
|
||||||
)
|
)
|
||||||
|
self._server_app.before_serving(self._lifespan.startup)
|
||||||
|
self._server_app.after_serving(self._lifespan.shutdown)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
@@ -148,16 +139,6 @@ class Driver(ReverseDriver):
|
|||||||
view_func=_handle,
|
view_func=_handle,
|
||||||
)
|
)
|
||||||
|
|
||||||
@override
|
|
||||||
def on_startup(self, func: _AsyncCallable) -> _AsyncCallable:
|
|
||||||
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
|
|
||||||
return self.server_app.before_serving(func) # type: ignore
|
|
||||||
|
|
||||||
@override
|
|
||||||
def on_shutdown(self, func: _AsyncCallable) -> _AsyncCallable:
|
|
||||||
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
|
|
||||||
return self.server_app.after_serving(func) # type: ignore
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
|
@@ -19,14 +19,14 @@ import logging
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing_extensions import ParamSpec, override
|
from typing_extensions import ParamSpec, override
|
||||||
from typing import Type, Union, TypeVar, Callable, Awaitable, AsyncGenerator
|
from typing import TYPE_CHECKING, Union, TypeVar, Callable, Awaitable, AsyncGenerator
|
||||||
|
|
||||||
|
from nonebot.drivers import Request
|
||||||
from nonebot.log import LoguruHandler
|
from nonebot.log import LoguruHandler
|
||||||
from nonebot.drivers import Request, Response
|
|
||||||
from nonebot.exception import WebSocketClosed
|
from nonebot.exception import WebSocketClosed
|
||||||
from nonebot.drivers.none import Driver as NoneDriver
|
from nonebot.drivers.none import Driver as NoneDriver
|
||||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||||
from nonebot.drivers import ForwardMixin, ForwardDriver, combine_driver
|
from nonebot.drivers import WebSocketClientMixin, combine_driver
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from websockets.exceptions import ConnectionClosed
|
from websockets.exceptions import ConnectionClosed
|
||||||
@@ -58,7 +58,7 @@ def catch_closed(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class Mixin(ForwardMixin):
|
class Mixin(WebSocketClientMixin):
|
||||||
"""Websockets Mixin"""
|
"""Websockets Mixin"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -66,10 +66,6 @@ class Mixin(ForwardMixin):
|
|||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
return "websockets"
|
return "websockets"
|
||||||
|
|
||||||
@override
|
|
||||||
async def request(self, setup: Request) -> Response:
|
|
||||||
return await super(Mixin, self).request(setup)
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||||
@@ -133,5 +129,11 @@ class WebSocket(BaseWebSocket):
|
|||||||
await self.websocket.send(data)
|
await self.websocket.send(data)
|
||||||
|
|
||||||
|
|
||||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
if TYPE_CHECKING:
|
||||||
"""Websockets Driver"""
|
|
||||||
|
class Driver(Mixin, NoneDriver):
|
||||||
|
...
|
||||||
|
|
||||||
|
else:
|
||||||
|
Driver = combine_driver(NoneDriver, Mixin)
|
||||||
|
"""Websockets Driver"""
|
||||||
|
@@ -3,14 +3,16 @@ from contextlib import asynccontextmanager
|
|||||||
from typing import Any, Dict, AsyncGenerator
|
from typing import Any, Dict, AsyncGenerator
|
||||||
|
|
||||||
from nonebot.config import Config
|
from nonebot.config import Config
|
||||||
|
from nonebot.internal.driver._lifespan import LIFESPAN_FUNC
|
||||||
from nonebot.internal.driver import (
|
from nonebot.internal.driver import (
|
||||||
Driver,
|
Driver,
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
|
ASGIMixin,
|
||||||
WebSocket,
|
WebSocket,
|
||||||
ForwardDriver,
|
HTTPClientMixin,
|
||||||
ReverseDriver,
|
|
||||||
HTTPServerSetup,
|
HTTPServerSetup,
|
||||||
|
WebSocketClientMixin,
|
||||||
WebSocketServerSetup,
|
WebSocketServerSetup,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,30 +74,33 @@ class Adapter(abc.ABC):
|
|||||||
|
|
||||||
def setup_http_server(self, setup: HTTPServerSetup):
|
def setup_http_server(self, setup: HTTPServerSetup):
|
||||||
"""设置一个 HTTP 服务器路由配置"""
|
"""设置一个 HTTP 服务器路由配置"""
|
||||||
if not isinstance(self.driver, ReverseDriver):
|
if not isinstance(self.driver, ASGIMixin):
|
||||||
raise TypeError("Current driver does not support http server")
|
raise TypeError("Current driver does not support http server")
|
||||||
self.driver.setup_http_server(setup)
|
self.driver.setup_http_server(setup)
|
||||||
|
|
||||||
def setup_websocket_server(self, setup: WebSocketServerSetup):
|
def setup_websocket_server(self, setup: WebSocketServerSetup):
|
||||||
"""设置一个 WebSocket 服务器路由配置"""
|
"""设置一个 WebSocket 服务器路由配置"""
|
||||||
if not isinstance(self.driver, ReverseDriver):
|
if not isinstance(self.driver, ASGIMixin):
|
||||||
raise TypeError("Current driver does not support websocket server")
|
raise TypeError("Current driver does not support websocket server")
|
||||||
self.driver.setup_websocket_server(setup)
|
self.driver.setup_websocket_server(setup)
|
||||||
|
|
||||||
async def request(self, setup: Request) -> Response:
|
async def request(self, setup: Request) -> Response:
|
||||||
"""进行一个 HTTP 客户端请求"""
|
"""进行一个 HTTP 客户端请求"""
|
||||||
if not isinstance(self.driver, ForwardDriver):
|
if not isinstance(self.driver, HTTPClientMixin):
|
||||||
raise TypeError("Current driver does not support http client")
|
raise TypeError("Current driver does not support http client")
|
||||||
return await self.driver.request(setup)
|
return await self.driver.request(setup)
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
|
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
|
||||||
"""建立一个 WebSocket 客户端连接请求"""
|
"""建立一个 WebSocket 客户端连接请求"""
|
||||||
if not isinstance(self.driver, ForwardDriver):
|
if not isinstance(self.driver, WebSocketClientMixin):
|
||||||
raise TypeError("Current driver does not support websocket client")
|
raise TypeError("Current driver does not support websocket client")
|
||||||
async with self.driver.websocket(setup) as ws:
|
async with self.driver.websocket(setup) as ws:
|
||||||
yield ws
|
yield ws
|
||||||
|
|
||||||
|
def on_ready(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
return self.driver._lifespan.on_ready(func)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:
|
async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:
|
||||||
"""`Adapter` 实际调用 api 的逻辑实现函数,实现该方法以调用 api。
|
"""`Adapter` 实际调用 api 的逻辑实现函数,实现该方法以调用 api。
|
||||||
|
@@ -106,7 +106,10 @@ class Bot(abc.ABC):
|
|||||||
logger.debug("Running CalledAPI hooks...")
|
logger.debug("Running CalledAPI hooks...")
|
||||||
await asyncio.gather(*coros)
|
await asyncio.gather(*coros)
|
||||||
except MockApiException as e:
|
except MockApiException as e:
|
||||||
|
# mock api result
|
||||||
result = e.result
|
result = e.result
|
||||||
|
# ignore exception
|
||||||
|
exception = None
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Calling API {api} result is mocked. Return {result} instead."
|
f"Calling API {api} result is mocked. Return {result} instead."
|
||||||
)
|
)
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
from .model import URL as URL
|
from .model import URL as URL
|
||||||
from .model import RawURL as RawURL
|
from .model import RawURL as RawURL
|
||||||
from .driver import Driver as Driver
|
from .abstract import Mixin as Mixin
|
||||||
from .model import Cookies as Cookies
|
from .model import Cookies as Cookies
|
||||||
from .model import Request as Request
|
from .model import Request as Request
|
||||||
|
from .abstract import Driver as Driver
|
||||||
from .model import FileType as FileType
|
from .model import FileType as FileType
|
||||||
from .model import Response as Response
|
from .model import Response as Response
|
||||||
from .model import DataTypes as DataTypes
|
from .model import DataTypes as DataTypes
|
||||||
@@ -10,16 +11,20 @@ from .model import FileTypes as FileTypes
|
|||||||
from .model import WebSocket as WebSocket
|
from .model import WebSocket as WebSocket
|
||||||
from .model import FilesTypes as FilesTypes
|
from .model import FilesTypes as FilesTypes
|
||||||
from .model import QueryTypes as QueryTypes
|
from .model import QueryTypes as QueryTypes
|
||||||
|
from .abstract import ASGIMixin as ASGIMixin
|
||||||
from .model import CookieTypes as CookieTypes
|
from .model import CookieTypes as CookieTypes
|
||||||
from .model import FileContent as FileContent
|
from .model import FileContent as FileContent
|
||||||
from .model import HTTPVersion as HTTPVersion
|
from .model import HTTPVersion as HTTPVersion
|
||||||
from .model import HeaderTypes as HeaderTypes
|
from .model import HeaderTypes as HeaderTypes
|
||||||
from .model import SimpleQuery as SimpleQuery
|
from .model import SimpleQuery as SimpleQuery
|
||||||
from .model import ContentTypes as ContentTypes
|
from .model import ContentTypes as ContentTypes
|
||||||
from .driver import ForwardMixin as ForwardMixin
|
|
||||||
from .model import QueryVariable as QueryVariable
|
from .model import QueryVariable as QueryVariable
|
||||||
from .driver import ForwardDriver as ForwardDriver
|
from .abstract import ForwardMixin as ForwardMixin
|
||||||
from .driver import ReverseDriver as ReverseDriver
|
from .abstract import ReverseMixin as ReverseMixin
|
||||||
from .driver import combine_driver as combine_driver
|
from .abstract import ForwardDriver as ForwardDriver
|
||||||
|
from .abstract import ReverseDriver as ReverseDriver
|
||||||
|
from .combine import combine_driver as combine_driver
|
||||||
from .model import HTTPServerSetup as HTTPServerSetup
|
from .model import HTTPServerSetup as HTTPServerSetup
|
||||||
|
from .abstract import HTTPClientMixin as HTTPClientMixin
|
||||||
from .model import WebSocketServerSetup as WebSocketServerSetup
|
from .model import WebSocketServerSetup as WebSocketServerSetup
|
||||||
|
from .abstract import WebSocketClientMixin as WebSocketClientMixin
|
||||||
|
@@ -11,6 +11,7 @@ LIFESPAN_FUNC: TypeAlias = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
|
|||||||
class Lifespan:
|
class Lifespan:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._startup_funcs: List[LIFESPAN_FUNC] = []
|
self._startup_funcs: List[LIFESPAN_FUNC] = []
|
||||||
|
self._ready_funcs: List[LIFESPAN_FUNC] = []
|
||||||
self._shutdown_funcs: List[LIFESPAN_FUNC] = []
|
self._shutdown_funcs: List[LIFESPAN_FUNC] = []
|
||||||
|
|
||||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
@@ -21,6 +22,10 @@ class Lifespan:
|
|||||||
self._shutdown_funcs.append(func)
|
self._shutdown_funcs.append(func)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
def on_ready(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
self._ready_funcs.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _run_lifespan_func(
|
async def _run_lifespan_func(
|
||||||
funcs: List[LIFESPAN_FUNC],
|
funcs: List[LIFESPAN_FUNC],
|
||||||
@@ -35,6 +40,9 @@ class Lifespan:
|
|||||||
if self._startup_funcs:
|
if self._startup_funcs:
|
||||||
await self._run_lifespan_func(self._startup_funcs)
|
await self._run_lifespan_func(self._startup_funcs)
|
||||||
|
|
||||||
|
if self._ready_funcs:
|
||||||
|
await self._run_lifespan_func(self._ready_funcs)
|
||||||
|
|
||||||
async def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
if self._shutdown_funcs:
|
if self._shutdown_funcs:
|
||||||
await self._run_lifespan_func(self._shutdown_funcs)
|
await self._run_lifespan_func(self._shutdown_funcs)
|
@@ -1,7 +1,8 @@
|
|||||||
import abc
|
import abc
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from typing_extensions import TypeAlias
|
||||||
from contextlib import AsyncExitStack, asynccontextmanager
|
from contextlib import AsyncExitStack, asynccontextmanager
|
||||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
|
from typing import TYPE_CHECKING, Any, Set, Dict, Type, AsyncGenerator
|
||||||
|
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from nonebot.config import Env, Config
|
from nonebot.config import Env, Config
|
||||||
@@ -15,6 +16,7 @@ from nonebot.typing import (
|
|||||||
T_BotDisconnectionHook,
|
T_BotDisconnectionHook,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
||||||
from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup
|
from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -25,7 +27,9 @@ BOT_HOOK_PARAMS = [DependParam, BotParam, DefaultParam]
|
|||||||
|
|
||||||
|
|
||||||
class Driver(abc.ABC):
|
class Driver(abc.ABC):
|
||||||
"""Driver 基类。
|
"""驱动器基类。
|
||||||
|
|
||||||
|
驱动器控制框架的启动和停止,适配器的注册,以及机器人生命周期管理。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
env: 包含环境信息的 Env 对象
|
env: 包含环境信息的 Env 对象
|
||||||
@@ -45,6 +49,8 @@ class Driver(abc.ABC):
|
|||||||
self.config: Config = config
|
self.config: Config = config
|
||||||
"""全局配置对象"""
|
"""全局配置对象"""
|
||||||
self._bots: Dict[str, "Bot"] = {}
|
self._bots: Dict[str, "Bot"] = {}
|
||||||
|
self._bot_tasks: Set[asyncio.Task] = set()
|
||||||
|
self._lifespan = Lifespan()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
@@ -94,15 +100,15 @@ class Driver(abc.ABC):
|
|||||||
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
|
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
|
||||||
)
|
)
|
||||||
|
|
||||||
@abc.abstractmethod
|
self.on_shutdown(self._cleanup)
|
||||||
def on_startup(self, func: Callable) -> Callable:
|
|
||||||
"""注册一个在驱动器启动时执行的函数"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
def on_shutdown(self, func: Callable) -> Callable:
|
"""注册一个启动时执行的函数"""
|
||||||
"""注册一个在驱动器停止时执行的函数"""
|
return self._lifespan.on_startup(func)
|
||||||
raise NotImplementedError
|
|
||||||
|
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
"""注册一个停止时执行的函数"""
|
||||||
|
return self._lifespan.on_shutdown(func)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def on_bot_connect(cls, func: T_BotConnectionHook) -> T_BotConnectionHook:
|
def on_bot_connect(cls, func: T_BotConnectionHook) -> T_BotConnectionHook:
|
||||||
@@ -156,7 +162,9 @@ class Driver(abc.ABC):
|
|||||||
"</bg #f8bbd0></r>"
|
"</bg #f8bbd0></r>"
|
||||||
)
|
)
|
||||||
|
|
||||||
asyncio.create_task(_run_hook(bot))
|
task = asyncio.create_task(_run_hook(bot))
|
||||||
|
task.add_done_callback(self._bot_tasks.discard)
|
||||||
|
self._bot_tasks.add(task)
|
||||||
|
|
||||||
def _bot_disconnect(self, bot: "Bot") -> None:
|
def _bot_disconnect(self, bot: "Bot") -> None:
|
||||||
"""在连接断开后,调用该函数来注销 bot 对象"""
|
"""在连接断开后,调用该函数来注销 bot 对象"""
|
||||||
@@ -183,23 +191,49 @@ class Driver(abc.ABC):
|
|||||||
"</bg #f8bbd0></r>"
|
"</bg #f8bbd0></r>"
|
||||||
)
|
)
|
||||||
|
|
||||||
asyncio.create_task(_run_hook(bot))
|
task = asyncio.create_task(_run_hook(bot))
|
||||||
|
task.add_done_callback(self._bot_tasks.discard)
|
||||||
|
self._bot_tasks.add(task)
|
||||||
|
|
||||||
|
async def _cleanup(self) -> None:
|
||||||
|
"""清理驱动器资源"""
|
||||||
|
if self._bot_tasks:
|
||||||
|
logger.opt(colors=True).debug(
|
||||||
|
"<y>Waiting for running bot connection hooks...</y>"
|
||||||
|
)
|
||||||
|
await asyncio.gather(*self._bot_tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
|
||||||
class ForwardMixin(abc.ABC):
|
class Mixin(abc.ABC):
|
||||||
"""客户端混入基类。"""
|
"""可与其他驱动器共用的混入基类。"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
"""客户端驱动类型名称"""
|
"""混入驱动类型名称"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class ForwardMixin(Mixin):
|
||||||
|
"""客户端混入基类。"""
|
||||||
|
|
||||||
|
|
||||||
|
class ReverseMixin(Mixin):
|
||||||
|
"""服务端混入基类。"""
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPClientMixin(ForwardMixin):
|
||||||
|
"""HTTP 客户端混入基类。"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def request(self, setup: Request) -> Response:
|
async def request(self, setup: Request) -> Response:
|
||||||
"""发送一个 HTTP 请求"""
|
"""发送一个 HTTP 请求"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketClientMixin(ForwardMixin):
|
||||||
|
"""WebSocket 客户端混入基类。"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
|
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
|
||||||
@@ -208,12 +242,11 @@ class ForwardMixin(abc.ABC):
|
|||||||
yield # used for static type checking's generator detection
|
yield # used for static type checking's generator detection
|
||||||
|
|
||||||
|
|
||||||
class ForwardDriver(Driver, ForwardMixin):
|
class ASGIMixin(ReverseMixin):
|
||||||
"""客户端基类。将客户端框架封装,以满足适配器使用。"""
|
"""ASGI 服务端基类。
|
||||||
|
|
||||||
|
将后端框架封装,以满足适配器使用。
|
||||||
class ReverseDriver(Driver):
|
"""
|
||||||
"""服务端基类。将后端框架封装,以满足适配器使用。"""
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -238,24 +271,14 @@ class ReverseDriver(Driver):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Driver]:
|
ForwardDriver: TypeAlias = ForwardMixin
|
||||||
"""将一个驱动器和多个混入类合并。"""
|
"""支持客户端请求的驱动器。
|
||||||
# check first
|
|
||||||
assert issubclass(driver, Driver), "`driver` must be subclass of Driver"
|
|
||||||
assert all(
|
|
||||||
issubclass(m, ForwardMixin) for m in mixins
|
|
||||||
), "`mixins` must be subclass of ForwardMixin"
|
|
||||||
|
|
||||||
if not mixins:
|
**Deprecated**,请使用 {ref}`nonebot.drivers.ForwardMixin` 或其子类代替。
|
||||||
return driver
|
"""
|
||||||
|
|
||||||
def type_(self: ForwardDriver) -> str:
|
ReverseDriver: TypeAlias = ReverseMixin
|
||||||
return (
|
"""支持服务端请求的驱动器。
|
||||||
driver.type.__get__(self)
|
|
||||||
+ "+"
|
|
||||||
+ "+".join(x.type.__get__(self) for x in mixins)
|
|
||||||
)
|
|
||||||
|
|
||||||
return type(
|
**Deprecated**,请使用 {ref}`nonebot.drivers.ReverseMixin` 或其子类代替。
|
||||||
"CombinedDriver", (*mixins, driver, ForwardDriver), {"type": property(type_)}
|
"""
|
||||||
) # type: ignore
|
|
45
nonebot/internal/driver/combine.py
Normal file
45
nonebot/internal/driver/combine.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from typing import TYPE_CHECKING, Type, Union, TypeVar, overload
|
||||||
|
|
||||||
|
from .abstract import Mixin, Driver
|
||||||
|
|
||||||
|
D = TypeVar("D", bound="Driver")
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
class CombinedDriver(Driver, Mixin):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def combine_driver(driver: Type[D]) -> Type[D]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def combine_driver(driver: Type[D], *mixins: Type[Mixin]) -> Type["CombinedDriver"]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def combine_driver(
|
||||||
|
driver: Type[D], *mixins: Type[Mixin]
|
||||||
|
) -> Union[Type[D], Type["CombinedDriver"]]:
|
||||||
|
"""将一个驱动器和多个混入类合并。"""
|
||||||
|
# check first
|
||||||
|
if not issubclass(driver, Driver):
|
||||||
|
raise TypeError("`driver` must be subclass of Driver")
|
||||||
|
if not all(issubclass(m, Mixin) for m in mixins):
|
||||||
|
raise TypeError("`mixins` must be subclass of Mixin")
|
||||||
|
|
||||||
|
if not mixins:
|
||||||
|
return driver
|
||||||
|
|
||||||
|
def type_(self: "CombinedDriver") -> str:
|
||||||
|
return (
|
||||||
|
driver.type.__get__(self) # type: ignore
|
||||||
|
+ "+"
|
||||||
|
+ "+".join(x.type.__get__(self) for x in mixins) # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
return type(
|
||||||
|
"CombinedDriver", (*mixins, driver), {"type": property(type_)}
|
||||||
|
) # type: ignore
|
@@ -125,7 +125,7 @@ class Request:
|
|||||||
files_ = files.items() if isinstance(files, dict) else files
|
files_ = files.items() if isinstance(files, dict) else files
|
||||||
for name, file_info in files_:
|
for name, file_info in files_:
|
||||||
if not isinstance(file_info, tuple):
|
if not isinstance(file_info, tuple):
|
||||||
self.files.append((name, (None, file_info, None)))
|
self.files.append((name, (name, file_info, None)))
|
||||||
elif len(file_info) == 2:
|
elif len(file_info) == 2:
|
||||||
self.files.append((name, (file_info[0], file_info[1], None)))
|
self.files.append((name, (file_info[0], file_info[1], None)))
|
||||||
else:
|
else:
|
||||||
|
@@ -6,6 +6,7 @@ matchers = MatcherManager()
|
|||||||
|
|
||||||
from .matcher import Matcher as Matcher
|
from .matcher import Matcher as Matcher
|
||||||
from .matcher import current_bot as current_bot
|
from .matcher import current_bot as current_bot
|
||||||
|
from .matcher import MatcherSource as MatcherSource
|
||||||
from .matcher import current_event as current_event
|
from .matcher import current_event as current_event
|
||||||
from .matcher import current_handler as current_handler
|
from .matcher import current_handler as current_handler
|
||||||
from .matcher import current_matcher as current_matcher
|
from .matcher import current_matcher as current_matcher
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
|
||||||
List,
|
List,
|
||||||
Type,
|
Type,
|
||||||
Tuple,
|
Tuple,
|
||||||
@@ -53,7 +52,7 @@ class MatcherManager(MutableMapping[int, List[Type["Matcher"]]]):
|
|||||||
def __delitem__(self, key: int) -> None:
|
def __delitem__(self, key: int) -> None:
|
||||||
del self.provider[key]
|
del self.provider[key]
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
def __eq__(self, other: object) -> bool:
|
||||||
return isinstance(other, MatcherManager) and self.provider == other.provider
|
return isinstance(other, MatcherManager) and self.provider == other.provider
|
||||||
|
|
||||||
def keys(self) -> KeysView[int]:
|
def keys(self) -> KeysView[int]:
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
|
import sys
|
||||||
|
import inspect
|
||||||
|
import warnings
|
||||||
|
from pathlib import Path
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
from dataclasses import dataclass
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@@ -8,6 +13,7 @@ from typing import (
|
|||||||
Any,
|
Any,
|
||||||
List,
|
List,
|
||||||
Type,
|
Type,
|
||||||
|
Tuple,
|
||||||
Union,
|
Union,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Callable,
|
Callable,
|
||||||
@@ -20,7 +26,8 @@ from typing import (
|
|||||||
|
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from nonebot.internal.rule import Rule
|
from nonebot.internal.rule import Rule
|
||||||
from nonebot.dependencies import Dependent
|
from nonebot.utils import classproperty
|
||||||
|
from nonebot.dependencies import Param, Dependent
|
||||||
from nonebot.internal.permission import User, Permission
|
from nonebot.internal.permission import User, Permission
|
||||||
from nonebot.internal.adapter import (
|
from nonebot.internal.adapter import (
|
||||||
Bot,
|
Bot,
|
||||||
@@ -74,15 +81,51 @@ current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
|
|||||||
current_handler: ContextVar[Dependent] = ContextVar("current_handler")
|
current_handler: ContextVar[Dependent] = ContextVar("current_handler")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MatcherSource:
|
||||||
|
"""Matcher 源代码上下文信息"""
|
||||||
|
|
||||||
|
plugin_name: Optional[str] = None
|
||||||
|
"""事件响应器所在插件名称"""
|
||||||
|
module_name: Optional[str] = None
|
||||||
|
"""事件响应器所在插件模块的路径名"""
|
||||||
|
lineno: Optional[int] = None
|
||||||
|
"""事件响应器所在行号"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin(self) -> Optional["Plugin"]:
|
||||||
|
"""事件响应器所在插件"""
|
||||||
|
from nonebot.plugin import get_plugin
|
||||||
|
|
||||||
|
if self.plugin_name is not None:
|
||||||
|
return get_plugin(self.plugin_name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def module(self) -> Optional[ModuleType]:
|
||||||
|
if self.module_name is not None:
|
||||||
|
return sys.modules.get(self.module_name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file(self) -> Optional[Path]:
|
||||||
|
if self.module is not None and (file := inspect.getsourcefile(self.module)):
|
||||||
|
return Path(file).absolute()
|
||||||
|
|
||||||
|
|
||||||
class MatcherMeta(type):
|
class MatcherMeta(type):
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
module_name: Optional[str]
|
|
||||||
type: str
|
type: str
|
||||||
|
_source: Optional[MatcherSource]
|
||||||
|
module_name: Optional[str]
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
f"{self.__name__}(type={self.type!r}"
|
f"{self.__name__}(type={self.type!r}"
|
||||||
+ (f", module={self.module_name}" if self.module_name else "")
|
+ (f", module={self.module_name}" if self.module_name else "")
|
||||||
|
+ (
|
||||||
|
f", lineno={self._source.lineno}"
|
||||||
|
if self._source and self._source.lineno is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
+ ")"
|
+ ")"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -90,14 +133,7 @@ class MatcherMeta(type):
|
|||||||
class Matcher(metaclass=MatcherMeta):
|
class Matcher(metaclass=MatcherMeta):
|
||||||
"""事件响应器类"""
|
"""事件响应器类"""
|
||||||
|
|
||||||
plugin: ClassVar[Optional["Plugin"]] = None
|
_source: ClassVar[Optional[MatcherSource]] = None
|
||||||
"""事件响应器所在插件"""
|
|
||||||
module: ClassVar[Optional[ModuleType]] = None
|
|
||||||
"""事件响应器所在插件模块"""
|
|
||||||
plugin_name: ClassVar[Optional[str]] = None
|
|
||||||
"""事件响应器所在插件名"""
|
|
||||||
module_name: ClassVar[Optional[str]] = None
|
|
||||||
"""事件响应器所在点分割插件模块路径"""
|
|
||||||
|
|
||||||
type: ClassVar[str] = ""
|
type: ClassVar[str] = ""
|
||||||
"""事件响应器类型"""
|
"""事件响应器类型"""
|
||||||
@@ -124,7 +160,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
_default_permission_updater: ClassVar[Optional[Dependent[Permission]]] = None
|
_default_permission_updater: ClassVar[Optional[Dependent[Permission]]] = None
|
||||||
"""事件响应器权限更新函数"""
|
"""事件响应器权限更新函数"""
|
||||||
|
|
||||||
HANDLER_PARAM_TYPES = (
|
HANDLER_PARAM_TYPES: ClassVar[Tuple[Type[Param], ...]] = (
|
||||||
DependParam,
|
DependParam,
|
||||||
BotParam,
|
BotParam,
|
||||||
EventParam,
|
EventParam,
|
||||||
@@ -142,6 +178,11 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
return (
|
return (
|
||||||
f"{self.__class__.__name__}(type={self.type!r}"
|
f"{self.__class__.__name__}(type={self.type!r}"
|
||||||
+ (f", module={self.module_name}" if self.module_name else "")
|
+ (f", module={self.module_name}" if self.module_name else "")
|
||||||
|
+ (
|
||||||
|
f", lineno={self._source.lineno}"
|
||||||
|
if self._source and self._source.lineno is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
+ ")"
|
+ ")"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -158,6 +199,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
*,
|
*,
|
||||||
plugin: Optional["Plugin"] = None,
|
plugin: Optional["Plugin"] = None,
|
||||||
module: Optional[ModuleType] = None,
|
module: Optional[ModuleType] = None,
|
||||||
|
source: Optional[MatcherSource] = None,
|
||||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||||
default_state: Optional[T_State] = None,
|
default_state: Optional[T_State] = None,
|
||||||
default_type_updater: Optional[Union[T_TypeUpdater, Dependent[str]]] = None,
|
default_type_updater: Optional[Union[T_TypeUpdater, Dependent[str]]] = None,
|
||||||
@@ -176,22 +218,47 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
temp: 是否为临时事件响应器,即触发一次后删除
|
temp: 是否为临时事件响应器,即触发一次后删除
|
||||||
priority: 响应优先级
|
priority: 响应优先级
|
||||||
block: 是否阻止事件向更低优先级的响应器传播
|
block: 是否阻止事件向更低优先级的响应器传播
|
||||||
plugin: 事件响应器所在插件
|
plugin: **Deprecated.** 事件响应器所在插件
|
||||||
module: 事件响应器所在模块
|
module: **Deprecated.** 事件响应器所在模块
|
||||||
default_state: 默认状态 `state`
|
source: 事件响应器源代码上下文信息
|
||||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||||
|
default_state: 默认状态 `state`
|
||||||
|
default_type_updater: 默认事件类型更新函数
|
||||||
|
default_permission_updater: 默认会话权限更新函数
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
Type[Matcher]: 新的事件响应器类
|
Type[Matcher]: 新的事件响应器类
|
||||||
"""
|
"""
|
||||||
|
if plugin is not None:
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
"Pass `plugin` context info to create Matcher is deprecated. "
|
||||||
|
"Use `source` instead."
|
||||||
|
),
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
if module is not None:
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
"Pass `module` context info to create Matcher is deprecated. "
|
||||||
|
"Use `source` instead."
|
||||||
|
),
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
source = source or (
|
||||||
|
MatcherSource(
|
||||||
|
plugin_name=plugin and plugin.name,
|
||||||
|
module_name=module and module.__name__,
|
||||||
|
)
|
||||||
|
if plugin is not None or module is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
NewMatcher = type(
|
NewMatcher = type(
|
||||||
cls.__name__,
|
cls.__name__,
|
||||||
(cls,),
|
(cls,),
|
||||||
{
|
{
|
||||||
"plugin": plugin,
|
"_source": source,
|
||||||
"module": module,
|
|
||||||
"plugin_name": plugin and plugin.name,
|
|
||||||
"module_name": module and module.__name__,
|
|
||||||
"type": type_,
|
"type": type_,
|
||||||
"rule": rule or Rule(),
|
"rule": rule or Rule(),
|
||||||
"permission": permission or Permission(),
|
"permission": permission or Permission(),
|
||||||
@@ -246,13 +313,33 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
|
|
||||||
matchers[priority].append(NewMatcher)
|
matchers[priority].append(NewMatcher)
|
||||||
|
|
||||||
return NewMatcher
|
return NewMatcher # type: ignore
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def destroy(cls) -> None:
|
def destroy(cls) -> None:
|
||||||
"""销毁当前的事件响应器"""
|
"""销毁当前的事件响应器"""
|
||||||
matchers[cls.priority].remove(cls)
|
matchers[cls.priority].remove(cls)
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def plugin(cls) -> Optional["Plugin"]:
|
||||||
|
"""事件响应器所在插件"""
|
||||||
|
return cls._source and cls._source.plugin
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def module(cls) -> Optional[ModuleType]:
|
||||||
|
"""事件响应器所在插件模块"""
|
||||||
|
return cls._source and cls._source.module
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def plugin_name(cls) -> Optional[str]:
|
||||||
|
"""事件响应器所在插件名"""
|
||||||
|
return cls._source and cls._source.plugin_name
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def module_name(cls) -> Optional[str]:
|
||||||
|
"""事件响应器所在插件模块路径"""
|
||||||
|
return cls._source and cls._source.module_name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def check_perm(
|
async def check_perm(
|
||||||
cls,
|
cls,
|
||||||
@@ -773,8 +860,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
temp=True,
|
temp=True,
|
||||||
priority=0,
|
priority=0,
|
||||||
block=True,
|
block=True,
|
||||||
plugin=self.plugin,
|
source=self.__class__._source,
|
||||||
module=self.module,
|
|
||||||
expire_time=bot.config.session_expire_timeout,
|
expire_time=bot.config.session_expire_timeout,
|
||||||
default_state=self.state,
|
default_state=self.state,
|
||||||
default_type_updater=self.__class__._default_type_updater,
|
default_type_updater=self.__class__._default_type_updater,
|
||||||
@@ -794,8 +880,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
temp=True,
|
temp=True,
|
||||||
priority=0,
|
priority=0,
|
||||||
block=True,
|
block=True,
|
||||||
plugin=self.plugin,
|
source=self.__class__._source,
|
||||||
module=self.module,
|
|
||||||
expire_time=bot.config.session_expire_timeout,
|
expire_time=bot.config.session_expire_timeout,
|
||||||
default_state=self.state,
|
default_state=self.state,
|
||||||
default_type_updater=self.__class__._default_type_updater,
|
default_type_updater=self.__class__._default_type_updater,
|
||||||
|
@@ -1,11 +1,21 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Self, Annotated, override
|
||||||
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
|
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
|
||||||
from typing import TYPE_CHECKING, Any, Type, Tuple, Literal, Callable, Optional, cast
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Type,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
Literal,
|
||||||
|
Callable,
|
||||||
|
Optional,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
from pydantic.typing import get_args, get_origin
|
from pydantic.typing import get_args, get_origin
|
||||||
from pydantic.fields import Required, Undefined, ModelField
|
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
|
||||||
|
|
||||||
from nonebot.dependencies.utils import check_field_type
|
from nonebot.dependencies.utils import check_field_type
|
||||||
from nonebot.dependencies import Param, Dependent, CustomConfig
|
from nonebot.dependencies import Param, Dependent, CustomConfig
|
||||||
@@ -24,6 +34,23 @@ if TYPE_CHECKING:
|
|||||||
from nonebot.matcher import Matcher
|
from nonebot.matcher import Matcher
|
||||||
from nonebot.adapters import Bot, Event
|
from nonebot.adapters import Bot, Event
|
||||||
|
|
||||||
|
EXTRA_FIELD_INFO = (
|
||||||
|
"gt",
|
||||||
|
"lt",
|
||||||
|
"ge",
|
||||||
|
"le",
|
||||||
|
"multiple_of",
|
||||||
|
"allow_inf_nan",
|
||||||
|
"max_digits",
|
||||||
|
"decimal_places",
|
||||||
|
"min_items",
|
||||||
|
"max_items",
|
||||||
|
"unique_items",
|
||||||
|
"min_length",
|
||||||
|
"max_length",
|
||||||
|
"regex",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DependsInner:
|
class DependsInner:
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -31,26 +58,31 @@ class DependsInner:
|
|||||||
dependency: Optional[T_Handler] = None,
|
dependency: Optional[T_Handler] = None,
|
||||||
*,
|
*,
|
||||||
use_cache: bool = True,
|
use_cache: bool = True,
|
||||||
|
validate: Union[bool, FieldInfo] = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.dependency = dependency
|
self.dependency = dependency
|
||||||
self.use_cache = use_cache
|
self.use_cache = use_cache
|
||||||
|
self.validate = validate
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
dep = get_name(self.dependency)
|
dep = get_name(self.dependency)
|
||||||
cache = "" if self.use_cache else ", use_cache=False"
|
cache = "" if self.use_cache else ", use_cache=False"
|
||||||
return f"DependsInner({dep}{cache})"
|
validate = f", validate={self.validate}" if self.validate else ""
|
||||||
|
return f"DependsInner({dep}{cache}{validate})"
|
||||||
|
|
||||||
|
|
||||||
def Depends(
|
def Depends(
|
||||||
dependency: Optional[T_Handler] = None,
|
dependency: Optional[T_Handler] = None,
|
||||||
*,
|
*,
|
||||||
use_cache: bool = True,
|
use_cache: bool = True,
|
||||||
|
validate: Union[bool, FieldInfo] = False,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""子依赖装饰器
|
"""子依赖装饰器
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
dependency: 依赖函数。默认为参数的类型注释。
|
dependency: 依赖函数。默认为参数的类型注释。
|
||||||
use_cache: 是否使用缓存。默认为 `True`。
|
use_cache: 是否使用缓存。默认为 `True`。
|
||||||
|
validate: 是否使用 Pydantic 类型校验。默认为 `False`。
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
```python
|
```python
|
||||||
@@ -70,7 +102,7 @@ def Depends(
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
return DependsInner(dependency, use_cache=use_cache)
|
return DependsInner(dependency, use_cache=use_cache, validate=validate)
|
||||||
|
|
||||||
|
|
||||||
class DependParam(Param):
|
class DependParam(Param):
|
||||||
@@ -85,23 +117,44 @@ class DependParam(Param):
|
|||||||
return f"Depends({self.extra['dependent']})"
|
return f"Depends({self.extra['dependent']})"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
def _from_field(
|
||||||
|
cls, sub_dependent: Dependent, use_cache: bool, validate: Union[bool, FieldInfo]
|
||||||
|
) -> Self:
|
||||||
|
kwargs = {}
|
||||||
|
if isinstance(validate, FieldInfo):
|
||||||
|
kwargs.update((k, getattr(validate, k)) for k in EXTRA_FIELD_INFO)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
Required,
|
||||||
|
validate=bool(validate),
|
||||||
|
**kwargs,
|
||||||
|
dependent=sub_dependent,
|
||||||
|
use_cache=use_cache,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
) -> Optional["DependParam"]:
|
) -> Optional[Self]:
|
||||||
type_annotation, depends_inner = param.annotation, None
|
type_annotation, depends_inner = param.annotation, None
|
||||||
|
# extract type annotation and dependency from Annotated
|
||||||
if get_origin(param.annotation) is Annotated:
|
if get_origin(param.annotation) is Annotated:
|
||||||
type_annotation, *extra_args = get_args(param.annotation)
|
type_annotation, *extra_args = get_args(param.annotation)
|
||||||
depends_inner = next(
|
depends_inner = next(
|
||||||
(x for x in extra_args if isinstance(x, DependsInner)), None
|
(x for x in reversed(extra_args) if isinstance(x, DependsInner)), None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# param default value takes higher priority
|
||||||
depends_inner = (
|
depends_inner = (
|
||||||
param.default if isinstance(param.default, DependsInner) else depends_inner
|
param.default if isinstance(param.default, DependsInner) else depends_inner
|
||||||
)
|
)
|
||||||
|
# not a dependent
|
||||||
if depends_inner is None:
|
if depends_inner is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
dependency: T_Handler
|
dependency: T_Handler
|
||||||
|
# sub dependency is not specified, use type annotation
|
||||||
if depends_inner.dependency is None:
|
if depends_inner.dependency is None:
|
||||||
assert (
|
assert (
|
||||||
type_annotation is not inspect.Signature.empty
|
type_annotation is not inspect.Signature.empty
|
||||||
@@ -109,13 +162,18 @@ class DependParam(Param):
|
|||||||
dependency = type_annotation
|
dependency = type_annotation
|
||||||
else:
|
else:
|
||||||
dependency = depends_inner.dependency
|
dependency = depends_inner.dependency
|
||||||
|
# parse sub dependency
|
||||||
sub_dependent = Dependent[Any].parse(
|
sub_dependent = Dependent[Any].parse(
|
||||||
call=dependency,
|
call=dependency,
|
||||||
allow_types=allow_types,
|
allow_types=allow_types,
|
||||||
)
|
)
|
||||||
return cls(Required, use_cache=depends_inner.use_cache, dependent=sub_dependent)
|
|
||||||
|
return cls._from_field(
|
||||||
|
sub_dependent, depends_inner.use_cache, depends_inner.validate
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@override
|
||||||
def _check_parameterless(
|
def _check_parameterless(
|
||||||
cls, value: Any, allow_types: Tuple[Type[Param], ...]
|
cls, value: Any, allow_types: Tuple[Type[Param], ...]
|
||||||
) -> Optional["Param"]:
|
) -> Optional["Param"]:
|
||||||
@@ -124,8 +182,9 @@ class DependParam(Param):
|
|||||||
dependent = Dependent[Any].parse(
|
dependent = Dependent[Any].parse(
|
||||||
call=value.dependency, allow_types=allow_types
|
call=value.dependency, allow_types=allow_types
|
||||||
)
|
)
|
||||||
return cls(Required, use_cache=value.use_cache, dependent=dependent)
|
return cls._from_field(dependent, value.use_cache, value.validate)
|
||||||
|
|
||||||
|
@override
|
||||||
async def _solve(
|
async def _solve(
|
||||||
self,
|
self,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: Optional[AsyncExitStack] = None,
|
||||||
@@ -169,6 +228,7 @@ class DependParam(Param):
|
|||||||
dependency_cache[call] = task
|
dependency_cache[call] = task
|
||||||
return await task
|
return await task
|
||||||
|
|
||||||
|
@override
|
||||||
async def _check(self, **kwargs: Any) -> None:
|
async def _check(self, **kwargs: Any) -> None:
|
||||||
# run sub dependent pre-checkers
|
# run sub dependent pre-checkers
|
||||||
sub_dependent: Dependent = self.extra["dependent"]
|
sub_dependent: Dependent = self.extra["dependent"]
|
||||||
@@ -195,9 +255,10 @@ class BotParam(Param):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
) -> Optional["BotParam"]:
|
) -> Optional[Self]:
|
||||||
from nonebot.adapters import Bot
|
from nonebot.adapters import Bot
|
||||||
|
|
||||||
# param type is Bot(s) or subclass(es) of Bot or None
|
# param type is Bot(s) or subclass(es) of Bot or None
|
||||||
@@ -217,9 +278,11 @@ class BotParam(Param):
|
|||||||
elif param.annotation == param.empty and param.name == "bot":
|
elif param.annotation == param.empty and param.name == "bot":
|
||||||
return cls(Required)
|
return cls(Required)
|
||||||
|
|
||||||
|
@override
|
||||||
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
|
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
|
||||||
return bot
|
return bot
|
||||||
|
|
||||||
|
@override
|
||||||
async def _check(self, bot: "Bot", **kwargs: Any) -> None:
|
async def _check(self, bot: "Bot", **kwargs: Any) -> None:
|
||||||
if checker := self.extra.get("checker"):
|
if checker := self.extra.get("checker"):
|
||||||
check_field_type(checker, bot)
|
check_field_type(checker, bot)
|
||||||
@@ -245,9 +308,10 @@ class EventParam(Param):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
) -> Optional["EventParam"]:
|
) -> Optional[Self]:
|
||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
|
|
||||||
# param type is Event(s) or subclass(es) of Event or None
|
# param type is Event(s) or subclass(es) of Event or None
|
||||||
@@ -267,9 +331,11 @@ class EventParam(Param):
|
|||||||
elif param.annotation == param.empty and param.name == "event":
|
elif param.annotation == param.empty and param.name == "event":
|
||||||
return cls(Required)
|
return cls(Required)
|
||||||
|
|
||||||
|
@override
|
||||||
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
|
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
@override
|
||||||
async def _check(self, event: "Event", **kwargs: Any) -> Any:
|
async def _check(self, event: "Event", **kwargs: Any) -> Any:
|
||||||
if checker := self.extra.get("checker", None):
|
if checker := self.extra.get("checker", None):
|
||||||
check_field_type(checker, event)
|
check_field_type(checker, event)
|
||||||
@@ -287,9 +353,10 @@ class StateParam(Param):
|
|||||||
return "StateParam()"
|
return "StateParam()"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
) -> Optional["StateParam"]:
|
) -> Optional[Self]:
|
||||||
# param type is T_State
|
# param type is T_State
|
||||||
if param.annotation is T_State:
|
if param.annotation is T_State:
|
||||||
return cls(Required)
|
return cls(Required)
|
||||||
@@ -297,6 +364,7 @@ class StateParam(Param):
|
|||||||
elif param.annotation == param.empty and param.name == "state":
|
elif param.annotation == param.empty and param.name == "state":
|
||||||
return cls(Required)
|
return cls(Required)
|
||||||
|
|
||||||
|
@override
|
||||||
async def _solve(self, state: T_State, **kwargs: Any) -> Any:
|
async def _solve(self, state: T_State, **kwargs: Any) -> Any:
|
||||||
return state
|
return state
|
||||||
|
|
||||||
@@ -313,9 +381,10 @@ class MatcherParam(Param):
|
|||||||
return "MatcherParam()"
|
return "MatcherParam()"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
) -> Optional["MatcherParam"]:
|
) -> Optional[Self]:
|
||||||
from nonebot.matcher import Matcher
|
from nonebot.matcher import Matcher
|
||||||
|
|
||||||
# param type is Matcher(s) or subclass(es) of Matcher or None
|
# param type is Matcher(s) or subclass(es) of Matcher or None
|
||||||
@@ -335,9 +404,11 @@ class MatcherParam(Param):
|
|||||||
elif param.annotation == param.empty and param.name == "matcher":
|
elif param.annotation == param.empty and param.name == "matcher":
|
||||||
return cls(Required)
|
return cls(Required)
|
||||||
|
|
||||||
|
@override
|
||||||
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||||
return matcher
|
return matcher
|
||||||
|
|
||||||
|
@override
|
||||||
async def _check(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
async def _check(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||||
if checker := self.extra.get("checker", None):
|
if checker := self.extra.get("checker", None):
|
||||||
check_field_type(checker, matcher)
|
check_field_type(checker, matcher)
|
||||||
@@ -382,15 +453,16 @@ class ArgParam(Param):
|
|||||||
return f"ArgParam(key={self.extra['key']!r}, type={self.extra['type']!r})"
|
return f"ArgParam(key={self.extra['key']!r}, type={self.extra['type']!r})"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
) -> Optional["ArgParam"]:
|
) -> Optional[Self]:
|
||||||
if isinstance(param.default, ArgInner):
|
if isinstance(param.default, ArgInner):
|
||||||
return cls(
|
return cls(
|
||||||
Required, key=param.default.key or param.name, type=param.default.type
|
Required, key=param.default.key or param.name, type=param.default.type
|
||||||
)
|
)
|
||||||
elif get_origin(param.annotation) is Annotated:
|
elif get_origin(param.annotation) is Annotated:
|
||||||
for arg in get_args(param.annotation):
|
for arg in get_args(param.annotation)[:0:-1]:
|
||||||
if isinstance(arg, ArgInner):
|
if isinstance(arg, ArgInner):
|
||||||
return cls(Required, key=arg.key or param.name, type=arg.type)
|
return cls(Required, key=arg.key or param.name, type=arg.type)
|
||||||
|
|
||||||
@@ -419,9 +491,10 @@ class ExceptionParam(Param):
|
|||||||
return "ExceptionParam()"
|
return "ExceptionParam()"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
) -> Optional["ExceptionParam"]:
|
) -> Optional[Self]:
|
||||||
# param type is Exception(s) or subclass(es) of Exception or None
|
# param type is Exception(s) or subclass(es) of Exception or None
|
||||||
if generic_check_issubclass(param.annotation, Exception):
|
if generic_check_issubclass(param.annotation, Exception):
|
||||||
return cls(Required)
|
return cls(Required)
|
||||||
@@ -429,6 +502,7 @@ class ExceptionParam(Param):
|
|||||||
elif param.annotation == param.empty and param.name == "exception":
|
elif param.annotation == param.empty and param.name == "exception":
|
||||||
return cls(Required)
|
return cls(Required)
|
||||||
|
|
||||||
|
@override
|
||||||
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
|
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
|
||||||
return exception
|
return exception
|
||||||
|
|
||||||
@@ -445,12 +519,14 @@ class DefaultParam(Param):
|
|||||||
return f"DefaultParam(default={self.default!r})"
|
return f"DefaultParam(default={self.default!r})"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
) -> Optional["DefaultParam"]:
|
) -> Optional[Self]:
|
||||||
if param.default != param.empty:
|
if param.default != param.empty:
|
||||||
return cls(param.default)
|
return cls(param.default)
|
||||||
|
|
||||||
|
@override
|
||||||
async def _solve(self, **kwargs: Any) -> Any:
|
async def _solve(self, **kwargs: Any) -> Any:
|
||||||
return Undefined
|
return Undefined
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ FrontMatter:
|
|||||||
from nonebot.internal.matcher import Matcher as Matcher
|
from nonebot.internal.matcher import Matcher as Matcher
|
||||||
from nonebot.internal.matcher import matchers as matchers
|
from nonebot.internal.matcher import matchers as matchers
|
||||||
from nonebot.internal.matcher import current_bot as current_bot
|
from nonebot.internal.matcher import current_bot as current_bot
|
||||||
|
from nonebot.internal.matcher import MatcherSource as MatcherSource
|
||||||
from nonebot.internal.matcher import current_event as current_event
|
from nonebot.internal.matcher import current_event as current_event
|
||||||
from nonebot.internal.matcher import MatcherManager as MatcherManager
|
from nonebot.internal.matcher import MatcherManager as MatcherManager
|
||||||
from nonebot.internal.matcher import MatcherProvider as MatcherProvider
|
from nonebot.internal.matcher import MatcherProvider as MatcherProvider
|
||||||
|
@@ -358,9 +358,18 @@ async def _check_matcher(
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not await Matcher.check_perm(
|
if not await Matcher.check_perm(bot, event, stack, dependency_cache):
|
||||||
bot, event, stack, dependency_cache
|
logger.trace(f"Permission conditions not met for {Matcher}")
|
||||||
) or not await Matcher.check_rule(bot, event, state, stack, dependency_cache):
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.opt(colors=True, exception=e).error(
|
||||||
|
f"<r><bg #f8bbd0>Permission check failed for {Matcher}.</bg #f8bbd0></r>"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not await Matcher.check_rule(bot, event, state, stack, dependency_cache):
|
||||||
|
logger.trace(f"Rule conditions not met for {Matcher}")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.opt(colors=True, exception=e).error(
|
logger.opt(colors=True, exception=e).error(
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
- `load_builtin_plugins` =>
|
- `load_builtin_plugins` =>
|
||||||
{ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
|
{ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
|
||||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||||
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.plugin.PluginMetadata>`
|
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.model.PluginMetadata>`
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
sidebar_position: 0
|
sidebar_position: 0
|
||||||
@@ -77,7 +77,7 @@ def get_plugin(name: str) -> Optional["Plugin"]:
|
|||||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
name: 插件名,即 {ref}`nonebot.plugin.plugin.Plugin.name`。
|
name: 插件名,即 {ref}`nonebot.plugin.model.Plugin.name`。
|
||||||
"""
|
"""
|
||||||
return _plugins.get(name)
|
return _plugins.get(name)
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ def get_plugin_by_module_name(module_name: str) -> Optional["Plugin"]:
|
|||||||
如果提供的模块名为某个插件的子模块,同样会返回该插件。
|
如果提供的模块名为某个插件的子模块,同样会返回该插件。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
module_name: 模块名,即 {ref}`nonebot.plugin.plugin.Plugin.module_name`。
|
module_name: 模块名,即 {ref}`nonebot.plugin.model.Plugin.module_name`。
|
||||||
"""
|
"""
|
||||||
loaded = {plugin.module_name: plugin for plugin in _plugins.values()}
|
loaded = {plugin.module_name: plugin for plugin in _plugins.values()}
|
||||||
has_parent = True
|
has_parent = True
|
||||||
@@ -111,9 +111,9 @@ def get_available_plugin_names() -> Set[str]:
|
|||||||
from .on import on as on
|
from .on import on as on
|
||||||
from .manager import PluginManager
|
from .manager import PluginManager
|
||||||
from .on import on_type as on_type
|
from .on import on_type as on_type
|
||||||
|
from .model import Plugin as Plugin
|
||||||
from .load import require as require
|
from .load import require as require
|
||||||
from .on import on_regex as on_regex
|
from .on import on_regex as on_regex
|
||||||
from .plugin import Plugin as Plugin
|
|
||||||
from .on import on_notice as on_notice
|
from .on import on_notice as on_notice
|
||||||
from .on import on_command as on_command
|
from .on import on_command as on_command
|
||||||
from .on import on_keyword as on_keyword
|
from .on import on_keyword as on_keyword
|
||||||
@@ -129,8 +129,8 @@ from .load import load_plugins as load_plugins
|
|||||||
from .on import on_startswith as on_startswith
|
from .on import on_startswith as on_startswith
|
||||||
from .load import load_from_json as load_from_json
|
from .load import load_from_json as load_from_json
|
||||||
from .load import load_from_toml as load_from_toml
|
from .load import load_from_toml as load_from_toml
|
||||||
|
from .model import PluginMetadata as PluginMetadata
|
||||||
from .on import on_shell_command as on_shell_command
|
from .on import on_shell_command as on_shell_command
|
||||||
from .plugin import PluginMetadata as PluginMetadata
|
|
||||||
from .load import load_all_plugins as load_all_plugins
|
from .load import load_all_plugins as load_all_plugins
|
||||||
from .load import load_builtin_plugin as load_builtin_plugin
|
from .load import load_builtin_plugin as load_builtin_plugin
|
||||||
from .load import load_builtin_plugins as load_builtin_plugins
|
from .load import load_builtin_plugins as load_builtin_plugins
|
||||||
|
@@ -12,7 +12,7 @@ from typing import Set, Union, Iterable, Optional
|
|||||||
|
|
||||||
from nonebot.utils import path_to_module_name
|
from nonebot.utils import path_to_module_name
|
||||||
|
|
||||||
from .plugin import Plugin
|
from .model import Plugin
|
||||||
from .manager import PluginManager
|
from .manager import PluginManager
|
||||||
from . import _managers, get_plugin, _current_plugin_chain, _module_name_to_plugin_name
|
from . import _managers, get_plugin, _current_plugin_chain, _module_name_to_plugin_name
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ def require(name: str) -> ModuleType:
|
|||||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
name: 插件名,即 {ref}`nonebot.plugin.plugin.Plugin.name`。
|
name: 插件名,即 {ref}`nonebot.plugin.model.Plugin.name`。
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
RuntimeError: 插件无法加载
|
RuntimeError: 插件无法加载
|
||||||
|
@@ -20,7 +20,7 @@ from typing import Set, Dict, List, Iterable, Optional, Sequence
|
|||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from nonebot.utils import escape_tag, path_to_module_name
|
from nonebot.utils import escape_tag, path_to_module_name
|
||||||
|
|
||||||
from .plugin import Plugin, PluginMetadata
|
from .model import Plugin, PluginMetadata
|
||||||
from . import (
|
from . import (
|
||||||
_managers,
|
_managers,
|
||||||
_new_plugin,
|
_new_plugin,
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
sidebar_position: 3
|
sidebar_position: 3
|
||||||
description: nonebot.plugin.plugin 模块
|
description: nonebot.plugin.model 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
@@ -7,14 +7,15 @@ FrontMatter:
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import inspect
|
import inspect
|
||||||
|
import warnings
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Set, Dict, List, Type, Tuple, Union, Optional
|
from typing import Any, Set, Dict, List, Type, Tuple, Union, Optional
|
||||||
|
|
||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
from nonebot.matcher import Matcher
|
|
||||||
from nonebot.permission import Permission
|
from nonebot.permission import Permission
|
||||||
from nonebot.dependencies import Dependent
|
from nonebot.dependencies import Dependent
|
||||||
|
from nonebot.matcher import Matcher, MatcherSource
|
||||||
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
|
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
|
||||||
from nonebot.rule import (
|
from nonebot.rule import (
|
||||||
Rule,
|
Rule,
|
||||||
@@ -29,7 +30,7 @@ from nonebot.rule import (
|
|||||||
shell_command,
|
shell_command,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .plugin import Plugin
|
from .model import Plugin
|
||||||
from . import get_plugin_by_module_name
|
from . import get_plugin_by_module_name
|
||||||
from .manager import _current_plugin_chain
|
from .manager import _current_plugin_chain
|
||||||
|
|
||||||
@@ -45,25 +46,39 @@ def store_matcher(matcher: Type[Matcher]) -> None:
|
|||||||
plugin_chain[-1].matcher.add(matcher)
|
plugin_chain[-1].matcher.add(matcher)
|
||||||
|
|
||||||
|
|
||||||
def get_matcher_plugin(depth: int = 1) -> Optional[Plugin]:
|
def get_matcher_plugin(depth: int = 1) -> Optional[Plugin]: # pragma: no cover
|
||||||
"""获取事件响应器定义所在插件。
|
"""获取事件响应器定义所在插件。
|
||||||
|
|
||||||
|
**Deprecated**, 请使用 {ref}`nonebot.plugin.on.get_matcher_source` 获取信息。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
depth: 调用栈深度
|
depth: 调用栈深度
|
||||||
"""
|
"""
|
||||||
# matcher defined when plugin loading
|
warnings.warn(
|
||||||
if plugin_chain := _current_plugin_chain.get():
|
"`get_matcher_plugin` is deprecated, please use `get_matcher_source` instead",
|
||||||
return plugin_chain[-1]
|
DeprecationWarning,
|
||||||
|
)
|
||||||
# matcher defined when plugin running
|
return (source := get_matcher_source(depth + 1)) and source.plugin
|
||||||
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]:
|
def get_matcher_module(depth: int = 1) -> Optional[ModuleType]: # pragma: no cover
|
||||||
"""获取事件响应器定义所在模块。
|
"""获取事件响应器定义所在模块。
|
||||||
|
|
||||||
|
**Deprecated**, 请使用 {ref}`nonebot.plugin.on.get_matcher_source` 获取信息。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
depth: 调用栈深度
|
||||||
|
"""
|
||||||
|
warnings.warn(
|
||||||
|
"`get_matcher_module` is deprecated, please use `get_matcher_source` instead",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
return (source := get_matcher_source(depth + 1)) and source.module
|
||||||
|
|
||||||
|
|
||||||
|
def get_matcher_source(depth: int = 1) -> Optional[MatcherSource]:
|
||||||
|
"""获取事件响应器定义所在源码信息。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
depth: 调用栈深度
|
depth: 调用栈深度
|
||||||
"""
|
"""
|
||||||
@@ -71,7 +86,22 @@ def get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
|
|||||||
if current_frame is None:
|
if current_frame is None:
|
||||||
return None
|
return None
|
||||||
frame = inspect.getouterframes(current_frame)[depth + 1].frame
|
frame = inspect.getouterframes(current_frame)[depth + 1].frame
|
||||||
return inspect.getmodule(frame)
|
|
||||||
|
module_name = (module := inspect.getmodule(frame)) and module.__name__
|
||||||
|
|
||||||
|
plugin: Optional["Plugin"] = None
|
||||||
|
# matcher defined when plugin loading
|
||||||
|
if plugin_chain := _current_plugin_chain.get():
|
||||||
|
plugin = plugin_chain[-1]
|
||||||
|
# matcher defined when plugin running
|
||||||
|
elif module_name:
|
||||||
|
plugin = get_plugin_by_module_name(module_name)
|
||||||
|
|
||||||
|
return MatcherSource(
|
||||||
|
plugin_name=plugin and plugin.name,
|
||||||
|
module_name=module_name,
|
||||||
|
lineno=frame.f_lineno,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def on(
|
def on(
|
||||||
@@ -109,8 +139,7 @@ def on(
|
|||||||
priority=priority,
|
priority=priority,
|
||||||
block=block,
|
block=block,
|
||||||
handlers=handlers,
|
handlers=handlers,
|
||||||
plugin=get_matcher_plugin(_depth + 1),
|
source=get_matcher_source(_depth + 1),
|
||||||
module=get_matcher_module(_depth + 1),
|
|
||||||
default_state=state,
|
default_state=state,
|
||||||
)
|
)
|
||||||
store_matcher(matcher)
|
store_matcher(matcher)
|
||||||
|
@@ -4,17 +4,18 @@ from types import ModuleType
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
from nonebot.matcher import Matcher
|
|
||||||
from nonebot.permission import Permission
|
from nonebot.permission import Permission
|
||||||
from nonebot.dependencies import Dependent
|
from nonebot.dependencies import Dependent
|
||||||
from nonebot.rule import Rule, ArgumentParser
|
from nonebot.rule import Rule, ArgumentParser
|
||||||
|
from nonebot.matcher import Matcher, MatcherSource
|
||||||
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
|
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
|
||||||
|
|
||||||
from .plugin import Plugin
|
from .model import Plugin
|
||||||
|
|
||||||
def store_matcher(matcher: type[Matcher]) -> None: ...
|
def store_matcher(matcher: type[Matcher]) -> None: ...
|
||||||
def get_matcher_plugin(depth: int = ...) -> Plugin | None: ...
|
def get_matcher_plugin(depth: int = ...) -> Plugin | None: ...
|
||||||
def get_matcher_module(depth: int = ...) -> ModuleType | None: ...
|
def get_matcher_module(depth: int = ...) -> ModuleType | None: ...
|
||||||
|
def get_matcher_source(depth: int = ...) -> MatcherSource | None: ...
|
||||||
def on(
|
def on(
|
||||||
type: str = "",
|
type: str = "",
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
|
@@ -117,6 +117,11 @@ class TrieRule:
|
|||||||
# check whitespace
|
# check whitespace
|
||||||
arg_str = segment_text[len(pf.key) :]
|
arg_str = segment_text[len(pf.key) :]
|
||||||
arg_str_stripped = arg_str.lstrip()
|
arg_str_stripped = arg_str.lstrip()
|
||||||
|
# check next segment until arg detected or no text remain
|
||||||
|
while not arg_str_stripped and msg and msg[0].is_text():
|
||||||
|
arg_str += str(msg.pop(0))
|
||||||
|
arg_str_stripped = arg_str.lstrip()
|
||||||
|
|
||||||
has_arg = arg_str_stripped or msg
|
has_arg = arg_str_stripped or msg
|
||||||
if (
|
if (
|
||||||
has_arg
|
has_arg
|
||||||
@@ -599,7 +604,7 @@ def shell_command(
|
|||||||
通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典
|
通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典
|
||||||
(例: `{"arg": "arg", "h": True}`)。
|
(例: `{"arg": "arg", "h": True}`)。
|
||||||
|
|
||||||
:::warning 警告
|
:::caution 警告
|
||||||
如果参数解析失败,则通过 {ref}`nonebot.params.ShellCommandArgs`
|
如果参数解析失败,则通过 {ref}`nonebot.params.ShellCommandArgs`
|
||||||
获取的将是 {ref}`nonebot.exception.ParserExit` 异常。
|
获取的将是 {ref}`nonebot.exception.ParserExit` 异常。
|
||||||
:::
|
:::
|
||||||
|
@@ -94,7 +94,7 @@ T_EventPreProcessor: TypeAlias = _DependentCallable[Any]
|
|||||||
- DefaultParam: 带有默认值的参数
|
- DefaultParam: 带有默认值的参数
|
||||||
"""
|
"""
|
||||||
T_EventPostProcessor: TypeAlias = _DependentCallable[Any]
|
T_EventPostProcessor: TypeAlias = _DependentCallable[Any]
|
||||||
"""事件预处理函数 EventPostProcessor 类型
|
"""事件后处理函数 EventPostProcessor 类型
|
||||||
|
|
||||||
依赖参数:
|
依赖参数:
|
||||||
|
|
||||||
|
@@ -21,6 +21,7 @@ from typing import (
|
|||||||
Type,
|
Type,
|
||||||
Tuple,
|
Tuple,
|
||||||
Union,
|
Union,
|
||||||
|
Generic,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Callable,
|
Callable,
|
||||||
Optional,
|
Optional,
|
||||||
@@ -30,7 +31,7 @@ from typing import (
|
|||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pydantic.typing import is_union, is_none_type
|
from pydantic.typing import is_union, is_none_type, is_literal_type, all_literal_values
|
||||||
|
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
|
|
||||||
@@ -74,9 +75,18 @@ def generic_check_issubclass(
|
|||||||
is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
|
is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
|
||||||
for type_ in get_args(cls)
|
for type_ in get_args(cls)
|
||||||
)
|
)
|
||||||
|
elif is_literal_type(cls):
|
||||||
|
return all(
|
||||||
|
is_none_type(value) or isinstance(value, class_or_tuple)
|
||||||
|
for value in all_literal_values(cls)
|
||||||
|
)
|
||||||
# ensure generic List, Dict can be checked
|
# ensure generic List, Dict can be checked
|
||||||
elif origin:
|
elif origin:
|
||||||
return issubclass(origin, class_or_tuple)
|
# avoid class check error (typing.Final, typing.ClassVar, etc...)
|
||||||
|
try:
|
||||||
|
return issubclass(origin, class_or_tuple)
|
||||||
|
except TypeError:
|
||||||
|
return False
|
||||||
elif isinstance(cls, TypeVar):
|
elif isinstance(cls, TypeVar):
|
||||||
if cls.__constraints__:
|
if cls.__constraints__:
|
||||||
return all(
|
return all(
|
||||||
@@ -220,6 +230,16 @@ def resolve_dot_notation(
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class classproperty(Generic[T]):
|
||||||
|
"""类属性装饰器"""
|
||||||
|
|
||||||
|
def __init__(self, func: Callable[[Any], T]) -> None:
|
||||||
|
self.func = func
|
||||||
|
|
||||||
|
def __get__(self, instance: Any, owner: Optional[Type[Any]] = None) -> T:
|
||||||
|
return self.func(type(instance) if owner is None else owner)
|
||||||
|
|
||||||
|
|
||||||
class DataclassEncoder(json.JSONEncoder):
|
class DataclassEncoder(json.JSONEncoder):
|
||||||
"""可以序列化 {ref}`nonebot.adapters.Message`(List[Dataclass]) 的 `JSONEncoder`"""
|
"""可以序列化 {ref}`nonebot.adapters.Message`(List[Dataclass]) 的 `JSONEncoder`"""
|
||||||
|
|
||||||
|
23
package.json
23
package.json
@@ -12,11 +12,30 @@
|
|||||||
"serve": "yarn workspace nonebot serve",
|
"serve": "yarn workspace nonebot serve",
|
||||||
"clear": "yarn workspace nonebot clear",
|
"clear": "yarn workspace nonebot clear",
|
||||||
"prettier": "prettier --config ./.prettierrc --write \"./website/\"",
|
"prettier": "prettier --config ./.prettierrc --write \"./website/\"",
|
||||||
|
"lint": "yarn lint:js && yarn lint:style",
|
||||||
|
"lint:js": "eslint --cache --report-unused-disable-directives \"**/*.{js,jsx,ts,tsx,mjs}\"",
|
||||||
|
"lint:js:fix": "eslint --cache --report-unused-disable-directives --fix \"**/*.{js,jsx,ts,tsx,mjs}\"",
|
||||||
|
"lint:style": "stylelint \"**/*.css\"",
|
||||||
|
"lint:style:fix": "stylelint --fix \"**/*.css\"",
|
||||||
"pyright": "pyright"
|
"pyright": "pyright"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||||
|
"@typescript-eslint/parser": "^6.6.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"prettier": "^2.5.0",
|
"eslint": "^8.48.0",
|
||||||
"pyright": "^1.1.317"
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.6.0",
|
||||||
|
"eslint-plugin-import": "^2.28.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-regexp": "^1.15.0",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"pyright": "^1.1.317",
|
||||||
|
"stylelint": "^15.10.3",
|
||||||
|
"stylelint-config-standard": "^34.0.0",
|
||||||
|
"stylelint-prettier": "^4.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2163
poetry.lock
generated
2163
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "nonebot2"
|
name = "nonebot2"
|
||||||
version = "2.0.1"
|
version = "2.1.3"
|
||||||
description = "An asynchronous python bot framework."
|
description = "An asynchronous python bot framework."
|
||||||
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -14,11 +14,9 @@ classifiers = [
|
|||||||
"Framework :: Robot Framework",
|
"Framework :: Robot Framework",
|
||||||
"Framework :: Robot Framework :: Library",
|
"Framework :: Robot Framework :: Library",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python :: 3"
|
"Programming Language :: Python :: 3",
|
||||||
]
|
|
||||||
packages = [
|
|
||||||
{ include = "nonebot" },
|
|
||||||
]
|
]
|
||||||
|
packages = [{ include = "nonebot" }]
|
||||||
include = ["nonebot/py.typed"]
|
include = ["nonebot/py.typed"]
|
||||||
|
|
||||||
[tool.poetry.urls]
|
[tool.poetry.urls]
|
||||||
@@ -31,16 +29,18 @@ python = "^3.8"
|
|||||||
yarl = "^1.7.2"
|
yarl = "^1.7.2"
|
||||||
pygtrie = "^2.4.1"
|
pygtrie = "^2.4.1"
|
||||||
loguru = ">=0.6.0,<1.0.0"
|
loguru = ">=0.6.0,<1.0.0"
|
||||||
typing-extensions = ">=4.0.0,<5.0.0"
|
typing-extensions = ">=4.4.0,<5.0.0"
|
||||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||||
pydantic = { version = "^1.10.0", extras = ["dotenv"] }
|
pydantic = { version = "^1.10.0", extras = ["dotenv"] }
|
||||||
|
|
||||||
websockets = { version = ">=10.0", optional = true }
|
websockets = { version = ">=10.0", optional = true }
|
||||||
Quart = { version = ">=0.18.0,<1.0.0", optional = true }
|
Quart = { version = ">=0.18.0,<1.0.0", optional = true }
|
||||||
fastapi = { version = ">=0.93.0,<1.0.0", optional = true }
|
fastapi = { version = ">=0.93.0,<1.0.0", optional = true }
|
||||||
aiohttp = { version = "^3.7.4", extras = ["speedups"], optional = true }
|
aiohttp = { version = "^3.9.0b0", extras = ["speedups"], optional = true }
|
||||||
httpx = { version = ">=0.20.0,<1.0.0", extras = ["http2"], optional = true }
|
httpx = { version = ">=0.20.0,<1.0.0", extras = ["http2"], optional = true }
|
||||||
uvicorn = { version = ">=0.20.0,<1.0.0", extras = ["standard"], optional = true }
|
uvicorn = { version = ">=0.20.0,<1.0.0", extras = [
|
||||||
|
"standard",
|
||||||
|
], optional = true }
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
isort = "^5.10.1"
|
isort = "^5.10.1"
|
||||||
@@ -51,10 +51,10 @@ ruff = ">=0.0.272,<1.0.0"
|
|||||||
|
|
||||||
[tool.poetry.group.test.dependencies]
|
[tool.poetry.group.test.dependencies]
|
||||||
nonebug = "^0.3.0"
|
nonebug = "^0.3.0"
|
||||||
werkzeug = "^2.3.6"
|
|
||||||
pytest-cov = "^4.0.0"
|
pytest-cov = "^4.0.0"
|
||||||
pytest-xdist = "^3.0.2"
|
pytest-xdist = "^3.0.2"
|
||||||
pytest-asyncio = "^0.21.0"
|
pytest-asyncio = "^0.23.2"
|
||||||
|
werkzeug = ">=2.3.6,<4.0.0"
|
||||||
coverage-conditional-plugin = "^0.9.0"
|
coverage-conditional-plugin = "^0.9.0"
|
||||||
|
|
||||||
[tool.poetry.group.docs.dependencies]
|
[tool.poetry.group.docs.dependencies]
|
||||||
@@ -71,10 +71,7 @@ all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
|
|||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
asyncio_mode = "strict"
|
asyncio_mode = "strict"
|
||||||
addopts = "--cov=nonebot --cov-append --cov-report=term-missing"
|
addopts = "--cov=nonebot --cov-append --cov-report=term-missing"
|
||||||
filterwarnings = [
|
filterwarnings = ["error", "ignore::DeprecationWarning"]
|
||||||
"error",
|
|
||||||
"ignore::DeprecationWarning",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
@@ -94,7 +91,7 @@ extra_standard_library = ["typing_extensions"]
|
|||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
select = ["E", "W", "F", "UP", "C", "T", "PYI", "PT", "Q"]
|
select = ["E", "W", "F", "UP", "C", "T", "PYI", "PT", "Q"]
|
||||||
ignore = ["E402", "C901"]
|
ignore = ["E402", "C901", "UP037"]
|
||||||
|
|
||||||
line-length = 88
|
line-length = 88
|
||||||
target-version = "py38"
|
target-version = "py38"
|
||||||
@@ -107,13 +104,15 @@ mark-parentheses = false
|
|||||||
pythonVersion = "3.8"
|
pythonVersion = "3.8"
|
||||||
pythonPlatform = "All"
|
pythonPlatform = "All"
|
||||||
executionEnvironments = [
|
executionEnvironments = [
|
||||||
{ root = "./tests", extraPaths = ["./"] },
|
{ root = "./tests", extraPaths = [
|
||||||
|
"./",
|
||||||
|
] },
|
||||||
{ root = "./" },
|
{ root = "./" },
|
||||||
]
|
]
|
||||||
|
|
||||||
typeCheckingMode = "basic"
|
typeCheckingMode = "basic"
|
||||||
reportShadowedImports = false
|
reportShadowedImports = false
|
||||||
|
disableBytesTypePromotions = true
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry_core>=1.0.0"]
|
requires = ["poetry_core>=1.0.0"]
|
||||||
|
@@ -11,7 +11,8 @@ exclude_lines =
|
|||||||
if (typing\.)?TYPE_CHECKING( is True)?:
|
if (typing\.)?TYPE_CHECKING( is True)?:
|
||||||
@(abc\.)?abstractmethod
|
@(abc\.)?abstractmethod
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
\.\.\.
|
warnings\.warn
|
||||||
|
^\.\.\.$
|
||||||
pass
|
pass
|
||||||
if __name__ == .__main__.:
|
if __name__ == .__main__.:
|
||||||
|
|
||||||
|
@@ -8,8 +8,10 @@ from nonebug import NONEBOT_INIT_KWARGS
|
|||||||
from werkzeug.serving import BaseWSGIServer, make_server
|
from werkzeug.serving import BaseWSGIServer, make_server
|
||||||
|
|
||||||
import nonebot
|
import nonebot
|
||||||
from nonebot.drivers import URL
|
from nonebot.config import Env
|
||||||
from fake_server import request_handler
|
from fake_server import request_handler
|
||||||
|
from nonebot.drivers import URL, Driver
|
||||||
|
from nonebot import _resolve_combine_expr
|
||||||
|
|
||||||
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
|
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
|
||||||
os.environ["CONFIG_OVERRIDE"] = "new"
|
os.environ["CONFIG_OVERRIDE"] = "new"
|
||||||
@@ -17,11 +19,24 @@ os.environ["CONFIG_OVERRIDE"] = "new"
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from nonebot.plugin import Plugin
|
from nonebot.plugin import Plugin
|
||||||
|
|
||||||
|
collect_ignore = ["plugins/", "dynamic/", "bad_plugins/"]
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config: pytest.Config) -> None:
|
def pytest_configure(config: pytest.Config) -> None:
|
||||||
config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"}
|
config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"}
|
||||||
|
|
||||||
|
|
||||||
|
@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.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
def load_plugin(nonebug_init: None) -> Set["Plugin"]:
|
def load_plugin(nonebug_init: None) -> Set["Plugin"]:
|
||||||
# preload global plugins
|
# preload global plugins
|
||||||
|
3
tests/plugins/matcher/matcher_info.py
Normal file
3
tests/plugins/matcher/matcher_info.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from nonebot import on
|
||||||
|
|
||||||
|
matcher = on("message", temp=False, expire_time=None, priority=1, block=True)
|
@@ -26,3 +26,14 @@ async def annotated_arg_str(key: Annotated[str, ArgStr()]) -> str:
|
|||||||
|
|
||||||
async def annotated_arg_plain_text(key: Annotated[str, ArgPlainText()]) -> str:
|
async def annotated_arg_plain_text(key: Annotated[str, ArgPlainText()]) -> str:
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
# test dependency priority
|
||||||
|
async def annotated_prior_arg(key: Annotated[str, ArgStr("foo")] = ArgPlainText()):
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
async def annotated_multi_arg(
|
||||||
|
key: Annotated[Annotated[str, ArgStr("foo")], ArgPlainText()]
|
||||||
|
):
|
||||||
|
return key
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
from nonebot import on_message
|
from nonebot import on_message
|
||||||
|
from nonebot.adapters import Bot
|
||||||
from nonebot.params import Depends
|
from nonebot.params import Depends
|
||||||
|
|
||||||
test_depends = on_message()
|
test_depends = on_message()
|
||||||
@@ -33,6 +36,14 @@ class ClassDependency:
|
|||||||
y: int = Depends(gen_async)
|
y: int = Depends(gen_async)
|
||||||
|
|
||||||
|
|
||||||
|
class FooBot(Bot):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
async def sub_bot(b: FooBot) -> FooBot:
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
# test parameterless
|
# test parameterless
|
||||||
@test_depends.handle(parameterless=[Depends(parameterless)])
|
@test_depends.handle(parameterless=[Depends(parameterless)])
|
||||||
async def depends(x: int = Depends(dependency)):
|
async def depends(x: int = Depends(dependency)):
|
||||||
@@ -46,19 +57,52 @@ async def depends_cache(y: int = Depends(dependency, use_cache=True)):
|
|||||||
return y
|
return y
|
||||||
|
|
||||||
|
|
||||||
|
# test class dependency
|
||||||
async def class_depend(c: ClassDependency = Depends()):
|
async def class_depend(c: ClassDependency = Depends()):
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
# test annotated dependency
|
||||||
async def annotated_depend(x: Annotated[int, Depends(dependency)]):
|
async def annotated_depend(x: Annotated[int, Depends(dependency)]):
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
# test annotated class dependency
|
||||||
async def annotated_class_depend(c: Annotated[ClassDependency, Depends()]):
|
async def annotated_class_depend(c: Annotated[ClassDependency, Depends()]):
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
# test dependency priority
|
||||||
async def annotated_prior_depend(
|
async def annotated_prior_depend(
|
||||||
x: Annotated[int, Depends(lambda: 2)] = Depends(dependency)
|
x: Annotated[int, Depends(lambda: 2)] = Depends(dependency)
|
||||||
):
|
):
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
async def annotated_multi_depend(
|
||||||
|
x: Annotated[Annotated[int, Depends(lambda: 2)], Depends(dependency)]
|
||||||
|
):
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
# test sub dependency type mismatch
|
||||||
|
async def sub_type_mismatch(b: FooBot = Depends(sub_bot)):
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
# test type validate
|
||||||
|
async def validate(x: int = Depends(lambda: "1", validate=True)):
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_fail(x: int = Depends(lambda: "not_number", validate=True)):
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
# test FieldInfo validate
|
||||||
|
async def validate_field(x: int = Depends(lambda: "1", validate=Field(gt=0))):
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_field_fail(x: int = Depends(lambda: "0", validate=Field(gt=0))):
|
||||||
|
return x
|
||||||
|
211
tests/test_adapters/test_adapter.py
Normal file
211
tests/test_adapters/test_adapter.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from nonebug import App
|
||||||
|
|
||||||
|
from utils import FakeAdapter
|
||||||
|
from nonebot.adapters import Bot
|
||||||
|
from nonebot.drivers import (
|
||||||
|
URL,
|
||||||
|
Driver,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
WebSocket,
|
||||||
|
HTTPServerSetup,
|
||||||
|
WebSocketServerSetup,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_adapter_connect(app: App, driver: Driver):
|
||||||
|
last_connect_bot: Optional[Bot] = None
|
||||||
|
last_disconnect_bot: Optional[Bot] = None
|
||||||
|
|
||||||
|
def _fake_bot_connect(bot: Bot):
|
||||||
|
nonlocal last_connect_bot
|
||||||
|
last_connect_bot = bot
|
||||||
|
|
||||||
|
def _fake_bot_disconnect(bot: Bot):
|
||||||
|
nonlocal last_disconnect_bot
|
||||||
|
last_disconnect_bot = bot
|
||||||
|
|
||||||
|
with pytest.MonkeyPatch.context() as m:
|
||||||
|
m.setattr(driver, "_bot_connect", _fake_bot_connect)
|
||||||
|
m.setattr(driver, "_bot_disconnect", _fake_bot_disconnect)
|
||||||
|
|
||||||
|
adapter = FakeAdapter(driver)
|
||||||
|
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot(adapter=adapter)
|
||||||
|
assert last_connect_bot is bot
|
||||||
|
assert adapter.bots[bot.self_id] is bot
|
||||||
|
|
||||||
|
assert last_disconnect_bot is bot
|
||||||
|
assert bot.self_id not in adapter.bots
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"driver",
|
||||||
|
[
|
||||||
|
pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"),
|
||||||
|
pytest.param("nonebot.drivers.quart:Driver", id="quart"),
|
||||||
|
pytest.param(
|
||||||
|
"nonebot.drivers.httpx:Driver",
|
||||||
|
id="httpx",
|
||||||
|
marks=pytest.mark.xfail(
|
||||||
|
reason="not a server", raises=TypeError, strict=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
"nonebot.drivers.websockets:Driver",
|
||||||
|
id="websockets",
|
||||||
|
marks=pytest.mark.xfail(
|
||||||
|
reason="not a server", raises=TypeError, strict=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
"nonebot.drivers.aiohttp:Driver",
|
||||||
|
id="aiohttp",
|
||||||
|
marks=pytest.mark.xfail(
|
||||||
|
reason="not a server", raises=TypeError, strict=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
indirect=True,
|
||||||
|
)
|
||||||
|
async def test_adapter_server(driver: Driver):
|
||||||
|
last_http_setup: Optional[HTTPServerSetup] = None
|
||||||
|
last_ws_setup: Optional[WebSocketServerSetup] = None
|
||||||
|
|
||||||
|
def _fake_setup_http_server(setup: HTTPServerSetup):
|
||||||
|
nonlocal last_http_setup
|
||||||
|
last_http_setup = setup
|
||||||
|
|
||||||
|
def _fake_setup_websocket_server(setup: WebSocketServerSetup):
|
||||||
|
nonlocal last_ws_setup
|
||||||
|
last_ws_setup = setup
|
||||||
|
|
||||||
|
with pytest.MonkeyPatch.context() as m:
|
||||||
|
m.setattr(driver, "setup_http_server", _fake_setup_http_server, raising=False)
|
||||||
|
m.setattr(
|
||||||
|
driver,
|
||||||
|
"setup_websocket_server",
|
||||||
|
_fake_setup_websocket_server,
|
||||||
|
raising=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle_http(request: Request):
|
||||||
|
return Response(200, content="test")
|
||||||
|
|
||||||
|
async def handle_ws(ws: WebSocket):
|
||||||
|
...
|
||||||
|
|
||||||
|
adapter = FakeAdapter(driver)
|
||||||
|
|
||||||
|
setup = HTTPServerSetup(URL("/test"), "GET", "test", handle_http)
|
||||||
|
adapter.setup_http_server(setup)
|
||||||
|
assert last_http_setup is setup
|
||||||
|
|
||||||
|
setup = WebSocketServerSetup(URL("/test"), "test", handle_ws)
|
||||||
|
adapter.setup_websocket_server(setup)
|
||||||
|
assert last_ws_setup is setup
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"driver",
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
"nonebot.drivers.fastapi:Driver",
|
||||||
|
id="fastapi",
|
||||||
|
marks=pytest.mark.xfail(
|
||||||
|
reason="not a http client", raises=TypeError, strict=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
"nonebot.drivers.quart:Driver",
|
||||||
|
id="quart",
|
||||||
|
marks=pytest.mark.xfail(
|
||||||
|
reason="not a http client", raises=TypeError, strict=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pytest.param("nonebot.drivers.httpx:Driver", id="httpx"),
|
||||||
|
pytest.param(
|
||||||
|
"nonebot.drivers.websockets:Driver",
|
||||||
|
id="websockets",
|
||||||
|
marks=pytest.mark.xfail(
|
||||||
|
reason="not a http client", raises=TypeError, strict=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"),
|
||||||
|
],
|
||||||
|
indirect=True,
|
||||||
|
)
|
||||||
|
async def test_adapter_http_client(driver: Driver):
|
||||||
|
last_request: Optional[Request] = None
|
||||||
|
|
||||||
|
async def _fake_request(request: Request):
|
||||||
|
nonlocal last_request
|
||||||
|
last_request = request
|
||||||
|
|
||||||
|
with pytest.MonkeyPatch.context() as m:
|
||||||
|
m.setattr(driver, "request", _fake_request, raising=False)
|
||||||
|
|
||||||
|
adapter = FakeAdapter(driver)
|
||||||
|
|
||||||
|
request = Request("GET", URL("/test"))
|
||||||
|
await adapter.request(request)
|
||||||
|
assert last_request is request
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"driver",
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
"nonebot.drivers.fastapi:Driver",
|
||||||
|
id="fastapi",
|
||||||
|
marks=pytest.mark.xfail(
|
||||||
|
reason="not a websocket client", raises=TypeError, strict=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
"nonebot.drivers.quart:Driver",
|
||||||
|
id="quart",
|
||||||
|
marks=pytest.mark.xfail(
|
||||||
|
reason="not a websocket client", raises=TypeError, strict=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
"nonebot.drivers.httpx:Driver",
|
||||||
|
id="httpx",
|
||||||
|
marks=pytest.mark.xfail(
|
||||||
|
reason="not a websocket client", raises=TypeError, strict=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pytest.param("nonebot.drivers.websockets:Driver", id="websockets"),
|
||||||
|
pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"),
|
||||||
|
],
|
||||||
|
indirect=True,
|
||||||
|
)
|
||||||
|
async def test_adapter_websocket_client(driver: Driver):
|
||||||
|
_fake_ws = object()
|
||||||
|
_last_request: Optional[Request] = None
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def _fake_websocket(setup: Request):
|
||||||
|
nonlocal _last_request
|
||||||
|
_last_request = setup
|
||||||
|
yield _fake_ws
|
||||||
|
|
||||||
|
with pytest.MonkeyPatch.context() as m:
|
||||||
|
m.setattr(driver, "websocket", _fake_websocket, raising=False)
|
||||||
|
|
||||||
|
adapter = FakeAdapter(driver)
|
||||||
|
|
||||||
|
request = Request("GET", URL("/test"))
|
||||||
|
async with adapter.websocket(request) as ws:
|
||||||
|
assert _last_request is request
|
||||||
|
assert ws is _fake_ws
|
152
tests/test_adapters/test_bot.py
Normal file
152
tests/test_adapters/test_bot.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from nonebug import App
|
||||||
|
|
||||||
|
from nonebot.adapters import Bot
|
||||||
|
from nonebot.exception import MockApiException
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bot_call_api(app: App):
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
ctx.should_call_api("test", {}, True)
|
||||||
|
result = await bot.call_api("test")
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
ctx.should_call_api("test", {}, exception=RuntimeError("test"))
|
||||||
|
with pytest.raises(RuntimeError, match="test"):
|
||||||
|
await bot.call_api("test")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bot_calling_api_hook_simple(app: App):
|
||||||
|
runned: bool = False
|
||||||
|
|
||||||
|
async def calling_api_hook(bot: Bot, api: str, data: Dict[str, Any]):
|
||||||
|
nonlocal runned
|
||||||
|
runned = True
|
||||||
|
|
||||||
|
hooks = set()
|
||||||
|
|
||||||
|
with pytest.MonkeyPatch.context() as m:
|
||||||
|
m.setattr(Bot, "_calling_api_hook", hooks)
|
||||||
|
|
||||||
|
Bot.on_calling_api(calling_api_hook)
|
||||||
|
|
||||||
|
assert hooks == {calling_api_hook}
|
||||||
|
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
ctx.should_call_api("test", {}, True)
|
||||||
|
result = await bot.call_api("test")
|
||||||
|
|
||||||
|
assert runned is True
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bot_calling_api_hook_mock(app: App):
|
||||||
|
runned: bool = False
|
||||||
|
|
||||||
|
async def calling_api_hook(bot: Bot, api: str, data: Dict[str, Any]):
|
||||||
|
nonlocal runned
|
||||||
|
runned = True
|
||||||
|
|
||||||
|
raise MockApiException(False)
|
||||||
|
|
||||||
|
hooks = set()
|
||||||
|
|
||||||
|
with pytest.MonkeyPatch.context() as m:
|
||||||
|
m.setattr(Bot, "_calling_api_hook", hooks)
|
||||||
|
|
||||||
|
Bot.on_calling_api(calling_api_hook)
|
||||||
|
|
||||||
|
assert hooks == {calling_api_hook}
|
||||||
|
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
result = await bot.call_api("test")
|
||||||
|
|
||||||
|
assert runned is True
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bot_called_api_hook_simple(app: App):
|
||||||
|
runned: bool = False
|
||||||
|
|
||||||
|
async def called_api_hook(
|
||||||
|
bot: Bot,
|
||||||
|
exception: Optional[Exception],
|
||||||
|
api: str,
|
||||||
|
data: Dict[str, Any],
|
||||||
|
result: Any,
|
||||||
|
):
|
||||||
|
nonlocal runned
|
||||||
|
runned = True
|
||||||
|
|
||||||
|
hooks = set()
|
||||||
|
|
||||||
|
with pytest.MonkeyPatch.context() as m:
|
||||||
|
m.setattr(Bot, "_called_api_hook", hooks)
|
||||||
|
|
||||||
|
Bot.on_called_api(called_api_hook)
|
||||||
|
|
||||||
|
assert hooks == {called_api_hook}
|
||||||
|
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
ctx.should_call_api("test", {}, True)
|
||||||
|
result = await bot.call_api("test")
|
||||||
|
|
||||||
|
assert runned is True
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bot_called_api_hook_mock(app: App):
|
||||||
|
runned: bool = False
|
||||||
|
|
||||||
|
async def called_api_hook(
|
||||||
|
bot: Bot,
|
||||||
|
exception: Optional[Exception],
|
||||||
|
api: str,
|
||||||
|
data: Dict[str, Any],
|
||||||
|
result: Any,
|
||||||
|
):
|
||||||
|
nonlocal runned
|
||||||
|
runned = True
|
||||||
|
|
||||||
|
raise MockApiException(False)
|
||||||
|
|
||||||
|
hooks = set()
|
||||||
|
|
||||||
|
with pytest.MonkeyPatch.context() as m:
|
||||||
|
m.setattr(Bot, "_called_api_hook", hooks)
|
||||||
|
|
||||||
|
Bot.on_called_api(called_api_hook)
|
||||||
|
|
||||||
|
assert hooks == {called_api_hook}
|
||||||
|
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
ctx.should_call_api("test", {}, True)
|
||||||
|
result = await bot.call_api("test")
|
||||||
|
|
||||||
|
assert runned is True
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
runned = False
|
||||||
|
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
ctx.should_call_api("test", {}, exception=RuntimeError("test"))
|
||||||
|
result = await bot.call_api("test")
|
||||||
|
|
||||||
|
assert runned is True
|
||||||
|
assert result is False
|
@@ -1,71 +1,74 @@
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Any, Set, Optional, cast
|
from typing import Any, Set, Optional
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from nonebug import App
|
from nonebug import App
|
||||||
|
|
||||||
import nonebot
|
from utils import FakeAdapter
|
||||||
from nonebot.config import Env
|
|
||||||
from nonebot.adapters import Bot
|
from nonebot.adapters import Bot
|
||||||
from nonebot.params import Depends
|
from nonebot.params import Depends
|
||||||
from nonebot import _resolve_combine_expr
|
|
||||||
from nonebot.dependencies import Dependent
|
from nonebot.dependencies import Dependent
|
||||||
from nonebot.exception import WebSocketClosed
|
from nonebot.exception import WebSocketClosed
|
||||||
from nonebot.drivers._lifespan import Lifespan
|
|
||||||
from nonebot.drivers import (
|
from nonebot.drivers import (
|
||||||
URL,
|
URL,
|
||||||
Driver,
|
Driver,
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
|
ASGIMixin,
|
||||||
WebSocket,
|
WebSocket,
|
||||||
ForwardDriver,
|
HTTPClientMixin,
|
||||||
ReverseDriver,
|
|
||||||
HTTPServerSetup,
|
HTTPServerSetup,
|
||||||
|
WebSocketClientMixin,
|
||||||
WebSocketServerSetup,
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_lifespan():
|
@pytest.mark.parametrize(
|
||||||
lifespan = Lifespan()
|
"driver", [pytest.param("nonebot.drivers.none:Driver", id="none")], indirect=True
|
||||||
|
)
|
||||||
|
async def test_lifespan(driver: Driver):
|
||||||
|
adapter = FakeAdapter(driver)
|
||||||
|
|
||||||
start_log = []
|
start_log = []
|
||||||
|
ready_log = []
|
||||||
shutdown_log = []
|
shutdown_log = []
|
||||||
|
|
||||||
@lifespan.on_startup
|
@driver.on_startup
|
||||||
async def _startup1():
|
async def _startup1():
|
||||||
assert start_log == []
|
assert start_log == []
|
||||||
start_log.append(1)
|
start_log.append(1)
|
||||||
|
|
||||||
@lifespan.on_startup
|
@driver.on_startup
|
||||||
async def _startup2():
|
async def _startup2():
|
||||||
assert start_log == [1]
|
assert start_log == [1]
|
||||||
start_log.append(2)
|
start_log.append(2)
|
||||||
|
|
||||||
@lifespan.on_shutdown
|
@adapter.on_ready
|
||||||
|
def _ready1():
|
||||||
|
assert start_log == [1, 2]
|
||||||
|
assert ready_log == []
|
||||||
|
ready_log.append(1)
|
||||||
|
|
||||||
|
@adapter.on_ready
|
||||||
|
def _ready2():
|
||||||
|
assert ready_log == [1]
|
||||||
|
ready_log.append(2)
|
||||||
|
|
||||||
|
@driver.on_shutdown
|
||||||
async def _shutdown1():
|
async def _shutdown1():
|
||||||
assert shutdown_log == []
|
assert shutdown_log == []
|
||||||
shutdown_log.append(1)
|
shutdown_log.append(1)
|
||||||
|
|
||||||
@lifespan.on_shutdown
|
@driver.on_shutdown
|
||||||
async def _shutdown2():
|
async def _shutdown2():
|
||||||
assert shutdown_log == [1]
|
assert shutdown_log == [1]
|
||||||
shutdown_log.append(2)
|
shutdown_log.append(2)
|
||||||
|
|
||||||
async with lifespan:
|
async with driver._lifespan:
|
||||||
assert start_log == [1, 2]
|
assert start_log == [1, 2]
|
||||||
|
assert ready_log == [1, 2]
|
||||||
|
|
||||||
assert shutdown_log == [1, 2]
|
assert shutdown_log == [1, 2]
|
||||||
|
|
||||||
@@ -80,7 +83,7 @@ async def test_lifespan():
|
|||||||
indirect=True,
|
indirect=True,
|
||||||
)
|
)
|
||||||
async def test_http_server(app: App, driver: Driver):
|
async def test_http_server(app: App, driver: Driver):
|
||||||
driver = cast(ReverseDriver, driver)
|
assert isinstance(driver, ASGIMixin)
|
||||||
|
|
||||||
async def _handle_http(request: Request) -> Response:
|
async def _handle_http(request: Request) -> Response:
|
||||||
assert request.content in (b"test", "test")
|
assert request.content in (b"test", "test")
|
||||||
@@ -108,7 +111,7 @@ async def test_http_server(app: App, driver: Driver):
|
|||||||
indirect=True,
|
indirect=True,
|
||||||
)
|
)
|
||||||
async def test_websocket_server(app: App, driver: Driver):
|
async def test_websocket_server(app: App, driver: Driver):
|
||||||
driver = cast(ReverseDriver, driver)
|
assert isinstance(driver, ASGIMixin)
|
||||||
|
|
||||||
async def _handle_ws(ws: WebSocket) -> None:
|
async def _handle_ws(ws: WebSocket) -> None:
|
||||||
await ws.accept()
|
await ws.accept()
|
||||||
@@ -164,7 +167,7 @@ async def test_websocket_server(app: App, driver: Driver):
|
|||||||
indirect=True,
|
indirect=True,
|
||||||
)
|
)
|
||||||
async def test_cross_context(app: App, driver: Driver):
|
async def test_cross_context(app: App, driver: Driver):
|
||||||
driver = cast(ReverseDriver, driver)
|
assert isinstance(driver, ASGIMixin)
|
||||||
|
|
||||||
ws: Optional[WebSocket] = None
|
ws: Optional[WebSocket] = None
|
||||||
ws_ready = asyncio.Event()
|
ws_ready = asyncio.Event()
|
||||||
@@ -221,7 +224,7 @@ async def test_cross_context(app: App, driver: Driver):
|
|||||||
indirect=True,
|
indirect=True,
|
||||||
)
|
)
|
||||||
async def test_http_client(driver: Driver, server_url: URL):
|
async def test_http_client(driver: Driver, server_url: URL):
|
||||||
driver = cast(ForwardDriver, driver)
|
assert isinstance(driver, HTTPClientMixin)
|
||||||
|
|
||||||
# simple post with query, headers, cookies and content
|
# simple post with query, headers, cookies and content
|
||||||
request = Request(
|
request = Request(
|
||||||
@@ -233,6 +236,23 @@ async def test_http_client(driver: Driver, server_url: URL):
|
|||||||
content="test",
|
content="test",
|
||||||
)
|
)
|
||||||
response = await driver.request(request)
|
response = await driver.request(request)
|
||||||
|
assert server_url.host is not None
|
||||||
|
request_raw_url = Request(
|
||||||
|
"POST",
|
||||||
|
(
|
||||||
|
server_url.scheme.encode("ascii"),
|
||||||
|
server_url.host.encode("ascii"),
|
||||||
|
server_url.port,
|
||||||
|
server_url.path.encode("ascii"),
|
||||||
|
),
|
||||||
|
params={"param": "test"},
|
||||||
|
headers={"X-Test": "test"},
|
||||||
|
cookies={"session": "test"},
|
||||||
|
content="test",
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
request.url == request_raw_url.url
|
||||||
|
), "request.url should be equal to request_raw_url.url"
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.content
|
assert response.content
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
@@ -265,7 +285,11 @@ async def test_http_client(driver: Driver, server_url: URL):
|
|||||||
"POST",
|
"POST",
|
||||||
server_url,
|
server_url,
|
||||||
data={"form": "test"},
|
data={"form": "test"},
|
||||||
files={"test": ("test.txt", b"test")},
|
files=[
|
||||||
|
("test1", b"test"),
|
||||||
|
("test2", ("test.txt", b"test")),
|
||||||
|
("test3", ("test.txt", b"test", "text/plain")),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
response = await driver.request(request)
|
response = await driver.request(request)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -273,11 +297,28 @@ async def test_http_client(driver: Driver, server_url: URL):
|
|||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
assert data["method"] == "POST"
|
assert data["method"] == "POST"
|
||||||
assert data["form"] == {"form": "test"}
|
assert data["form"] == {"form": "test"}
|
||||||
assert data["files"] == {"test": "test"}
|
assert data["files"] == {
|
||||||
|
"test1": "test",
|
||||||
|
"test2": "test",
|
||||||
|
"test3": "test",
|
||||||
|
}, "file parsing error"
|
||||||
|
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"driver",
|
||||||
|
[
|
||||||
|
pytest.param("nonebot.drivers.websockets:Driver", id="websockets"),
|
||||||
|
pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"),
|
||||||
|
],
|
||||||
|
indirect=True,
|
||||||
|
)
|
||||||
|
async def test_websocket_client(driver: Driver):
|
||||||
|
assert isinstance(driver, WebSocketClientMixin)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("driver", "driver_type"),
|
("driver", "driver_type"),
|
||||||
|
@@ -2,7 +2,7 @@ import pytest
|
|||||||
from nonebug import App
|
from nonebug import App
|
||||||
|
|
||||||
import nonebot
|
import nonebot
|
||||||
from nonebot.drivers import Driver, ReverseDriver
|
from nonebot.drivers import Driver, ASGIMixin, ReverseDriver
|
||||||
from nonebot import (
|
from nonebot import (
|
||||||
get_app,
|
get_app,
|
||||||
get_bot,
|
get_bot,
|
||||||
@@ -47,6 +47,7 @@ async def test_get_driver(app: App, monkeypatch: pytest.MonkeyPatch):
|
|||||||
async def test_get_asgi(app: App, monkeypatch: pytest.MonkeyPatch):
|
async def test_get_asgi(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||||
driver = get_driver()
|
driver = get_driver()
|
||||||
assert isinstance(driver, ReverseDriver)
|
assert isinstance(driver, ReverseDriver)
|
||||||
|
assert isinstance(driver, ASGIMixin)
|
||||||
assert get_asgi() == driver.asgi
|
assert get_asgi() == driver.asgi
|
||||||
|
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ async def test_get_asgi(app: App, monkeypatch: pytest.MonkeyPatch):
|
|||||||
async def test_get_app(app: App, monkeypatch: pytest.MonkeyPatch):
|
async def test_get_app(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||||
driver = get_driver()
|
driver = get_driver()
|
||||||
assert isinstance(driver, ReverseDriver)
|
assert isinstance(driver, ReverseDriver)
|
||||||
|
assert isinstance(driver, ASGIMixin)
|
||||||
assert get_app() == driver.server_app
|
assert get_app() == driver.server_app
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,10 +1,88 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from nonebug import App
|
from nonebug import App
|
||||||
|
|
||||||
from nonebot.permission import User
|
from nonebot.rule import Rule
|
||||||
|
from nonebot import get_plugin
|
||||||
from nonebot.matcher import Matcher, matchers
|
from nonebot.matcher import Matcher, matchers
|
||||||
from utils import FakeMessage, make_fake_event
|
from utils import FakeMessage, make_fake_event
|
||||||
from nonebot.message import check_and_run_matcher
|
from nonebot.permission import User, Permission
|
||||||
|
from nonebot.message import _check_matcher, check_and_run_matcher
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_matcher_info(app: App):
|
||||||
|
from plugins.matcher.matcher_info import matcher
|
||||||
|
|
||||||
|
assert issubclass(matcher, Matcher)
|
||||||
|
assert matcher.type == "message"
|
||||||
|
assert matcher.priority == 1
|
||||||
|
assert matcher.temp is False
|
||||||
|
assert matcher.expire_time is None
|
||||||
|
assert matcher.block is True
|
||||||
|
|
||||||
|
assert matcher._source
|
||||||
|
|
||||||
|
assert matcher._source.module_name == "plugins.matcher.matcher_info"
|
||||||
|
assert matcher.module is sys.modules["plugins.matcher.matcher_info"]
|
||||||
|
assert matcher.module_name == "plugins.matcher.matcher_info"
|
||||||
|
|
||||||
|
assert matcher._source.plugin_name == "matcher_info"
|
||||||
|
assert matcher.plugin is get_plugin("matcher_info")
|
||||||
|
assert matcher.plugin_name == "matcher_info"
|
||||||
|
|
||||||
|
assert (
|
||||||
|
matcher._source.file
|
||||||
|
== (Path(__file__).parent.parent / "plugins/matcher/matcher_info.py").absolute()
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matcher._source.lineno == 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_matcher_check(app: App):
|
||||||
|
async def falsy():
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def truthy():
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def error():
|
||||||
|
raise RuntimeError
|
||||||
|
|
||||||
|
event = make_fake_event(_type="test")()
|
||||||
|
with app.provider.context({}):
|
||||||
|
test_perm_falsy = Matcher.new(permission=Permission(falsy))
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
assert await _check_matcher(test_perm_falsy, bot, event, {}) is False
|
||||||
|
|
||||||
|
test_perm_truthy = Matcher.new(permission=Permission(truthy))
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
assert await _check_matcher(test_perm_truthy, bot, event, {}) is True
|
||||||
|
|
||||||
|
test_perm_error = Matcher.new(permission=Permission(error))
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
assert await _check_matcher(test_perm_error, bot, event, {}) is False
|
||||||
|
|
||||||
|
test_rule_falsy = Matcher.new(rule=Rule(falsy))
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
assert await _check_matcher(test_rule_falsy, bot, event, {}) is False
|
||||||
|
|
||||||
|
test_rule_truthy = Matcher.new(rule=Rule(truthy))
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
assert await _check_matcher(test_rule_truthy, bot, event, {}) is True
|
||||||
|
|
||||||
|
test_rule_error = Matcher.new(rule=Rule(error))
|
||||||
|
async with app.test_api() as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
assert await _check_matcher(test_rule_error, bot, event, {}) is False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -62,7 +140,7 @@ async def test_matcher_receive(app: App):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_matcher_(app: App):
|
async def test_matcher_combine(app: App):
|
||||||
from plugins.matcher.matcher_process import test_combine
|
from plugins.matcher.matcher_process import test_combine
|
||||||
|
|
||||||
message = FakeMessage("text")
|
message = FakeMessage("text")
|
||||||
|
@@ -42,10 +42,16 @@ async def test_depend(app: App):
|
|||||||
ClassDependency,
|
ClassDependency,
|
||||||
runned,
|
runned,
|
||||||
depends,
|
depends,
|
||||||
|
validate,
|
||||||
class_depend,
|
class_depend,
|
||||||
test_depends,
|
test_depends,
|
||||||
|
validate_fail,
|
||||||
|
validate_field,
|
||||||
annotated_depend,
|
annotated_depend,
|
||||||
|
sub_type_mismatch,
|
||||||
|
validate_field_fail,
|
||||||
annotated_class_depend,
|
annotated_class_depend,
|
||||||
|
annotated_multi_depend,
|
||||||
annotated_prior_depend,
|
annotated_prior_depend,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,8 +68,7 @@ async def test_depend(app: App):
|
|||||||
event_next = make_fake_event()()
|
event_next = make_fake_event()()
|
||||||
ctx.receive_event(bot, event_next)
|
ctx.receive_event(bot, event_next)
|
||||||
|
|
||||||
assert len(runned) == 2
|
assert runned == [1, 1]
|
||||||
assert runned[0] == runned[1] == 1
|
|
||||||
|
|
||||||
runned.clear()
|
runned.clear()
|
||||||
|
|
||||||
@@ -77,13 +82,42 @@ async def test_depend(app: App):
|
|||||||
annotated_prior_depend, allow_types=[DependParam]
|
annotated_prior_depend, allow_types=[DependParam]
|
||||||
) as ctx:
|
) as ctx:
|
||||||
ctx.should_return(1)
|
ctx.should_return(1)
|
||||||
assert runned == [1, 1]
|
|
||||||
|
async with app.test_dependent(
|
||||||
|
annotated_multi_depend, allow_types=[DependParam]
|
||||||
|
) as ctx:
|
||||||
|
ctx.should_return(1)
|
||||||
|
|
||||||
|
assert runned == [1, 1, 1]
|
||||||
|
|
||||||
async with app.test_dependent(
|
async with app.test_dependent(
|
||||||
annotated_class_depend, allow_types=[DependParam]
|
annotated_class_depend, allow_types=[DependParam]
|
||||||
) as ctx:
|
) as ctx:
|
||||||
ctx.should_return(ClassDependency(x=1, y=2))
|
ctx.should_return(ClassDependency(x=1, y=2))
|
||||||
|
|
||||||
|
with pytest.raises(TypeMisMatch): # noqa: PT012
|
||||||
|
async with app.test_dependent(
|
||||||
|
sub_type_mismatch, allow_types=[DependParam, BotParam]
|
||||||
|
) as ctx:
|
||||||
|
bot = ctx.create_bot()
|
||||||
|
ctx.pass_params(bot=bot)
|
||||||
|
|
||||||
|
async with app.test_dependent(validate, allow_types=[DependParam]) as ctx:
|
||||||
|
ctx.should_return(1)
|
||||||
|
|
||||||
|
with pytest.raises(TypeMisMatch):
|
||||||
|
async with app.test_dependent(validate_fail, allow_types=[DependParam]) as ctx:
|
||||||
|
...
|
||||||
|
|
||||||
|
async with app.test_dependent(validate_field, allow_types=[DependParam]) as ctx:
|
||||||
|
ctx.should_return(1)
|
||||||
|
|
||||||
|
with pytest.raises(TypeMisMatch):
|
||||||
|
async with app.test_dependent(
|
||||||
|
validate_field_fail, allow_types=[DependParam]
|
||||||
|
) as ctx:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_bot(app: App):
|
async def test_bot(app: App):
|
||||||
@@ -447,6 +481,8 @@ async def test_arg(app: App):
|
|||||||
annotated_arg,
|
annotated_arg,
|
||||||
arg_plain_text,
|
arg_plain_text,
|
||||||
annotated_arg_str,
|
annotated_arg_str,
|
||||||
|
annotated_multi_arg,
|
||||||
|
annotated_prior_arg,
|
||||||
annotated_arg_plain_text,
|
annotated_arg_plain_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -480,6 +516,14 @@ async def test_arg(app: App):
|
|||||||
ctx.pass_params(matcher=matcher)
|
ctx.pass_params(matcher=matcher)
|
||||||
ctx.should_return(message.extract_plain_text())
|
ctx.should_return(message.extract_plain_text())
|
||||||
|
|
||||||
|
async with app.test_dependent(annotated_multi_arg, allow_types=[ArgParam]) as ctx:
|
||||||
|
ctx.pass_params(matcher=matcher)
|
||||||
|
ctx.should_return(message.extract_plain_text())
|
||||||
|
|
||||||
|
async with app.test_dependent(annotated_prior_arg, allow_types=[ArgParam]) as ctx:
|
||||||
|
ctx.pass_params(matcher=matcher)
|
||||||
|
ctx.should_return(message.extract_plain_text())
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_exception(app: App):
|
async def test_exception(app: App):
|
||||||
|
@@ -80,7 +80,7 @@ async def test_load_toml():
|
|||||||
async def test_bad_plugin():
|
async def test_bad_plugin():
|
||||||
nonebot.load_plugins("bad_plugins")
|
nonebot.load_plugins("bad_plugins")
|
||||||
|
|
||||||
assert nonebot.get_plugin("bad_plugins") is None
|
assert nonebot.get_plugin("bad_plugin") is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
@@ -113,6 +113,36 @@ async def test_trie(app: App):
|
|||||||
command_whitespace=" ",
|
command_whitespace=" ",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
message = FakeMessageSegment.text("/fake-prefix ") + FakeMessageSegment.text(
|
||||||
|
" some args"
|
||||||
|
)
|
||||||
|
event = make_fake_event(_message=message)()
|
||||||
|
state = {}
|
||||||
|
TrieRule.get_value(bot, event, state)
|
||||||
|
assert state[PREFIX_KEY] == CMD_RESULT(
|
||||||
|
command=("fake-prefix",),
|
||||||
|
raw_command="/fake-prefix",
|
||||||
|
command_arg=FakeMessage("some args"),
|
||||||
|
command_start="/",
|
||||||
|
command_whitespace=" ",
|
||||||
|
)
|
||||||
|
|
||||||
|
message = (
|
||||||
|
FakeMessageSegment.text("/fake-prefix ")
|
||||||
|
+ FakeMessageSegment.text(" ")
|
||||||
|
+ FakeMessageSegment.text(" some args")
|
||||||
|
)
|
||||||
|
event = make_fake_event(_message=message)()
|
||||||
|
state = {}
|
||||||
|
TrieRule.get_value(bot, event, state)
|
||||||
|
assert state[PREFIX_KEY] == CMD_RESULT(
|
||||||
|
command=("fake-prefix",),
|
||||||
|
raw_command="/fake-prefix",
|
||||||
|
command_arg=FakeMessage("some args"),
|
||||||
|
command_start="/",
|
||||||
|
command_whitespace=" ",
|
||||||
|
)
|
||||||
|
|
||||||
del TrieRule.prefix["/fake-prefix"]
|
del TrieRule.prefix["/fake-prefix"]
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
from typing import Dict, List, Union, TypeVar
|
from typing import Dict, List, Union, Literal, TypeVar, ClassVar
|
||||||
|
|
||||||
from utils import FakeMessage, FakeMessageSegment
|
from utils import FakeMessage, FakeMessageSegment
|
||||||
from nonebot.utils import (
|
from nonebot.utils import (
|
||||||
@@ -24,8 +24,11 @@ def test_generic_check_issubclass():
|
|||||||
assert generic_check_issubclass(int, (int, float))
|
assert generic_check_issubclass(int, (int, float))
|
||||||
assert not generic_check_issubclass(str, (int, float))
|
assert not generic_check_issubclass(str, (int, float))
|
||||||
assert generic_check_issubclass(Union[int, float, None], (int, float))
|
assert generic_check_issubclass(Union[int, float, None], (int, float))
|
||||||
|
assert generic_check_issubclass(Literal[1, 2, 3], int)
|
||||||
|
assert not generic_check_issubclass(Literal[1, 2, "3"], int)
|
||||||
assert generic_check_issubclass(List[int], list)
|
assert generic_check_issubclass(List[int], list)
|
||||||
assert generic_check_issubclass(Dict[str, int], dict)
|
assert generic_check_issubclass(Dict[str, int], dict)
|
||||||
|
assert not generic_check_issubclass(ClassVar[int], int)
|
||||||
assert generic_check_issubclass(TypeVar("T", int, float), (int, float))
|
assert generic_check_issubclass(TypeVar("T", int, float), (int, float))
|
||||||
assert generic_check_issubclass(TypeVar("T", bound=int), (int, float))
|
assert generic_check_issubclass(TypeVar("T", bound=int), (int, float))
|
||||||
|
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
|
from typing_extensions import override
|
||||||
from typing import Type, Union, Mapping, Iterable, Optional
|
from typing import Type, Union, Mapping, Iterable, Optional
|
||||||
|
|
||||||
from pydantic import Extra, create_model
|
from pydantic import Extra, create_model
|
||||||
|
|
||||||
from nonebot.adapters import Event, Message, MessageSegment
|
from nonebot.adapters import Bot, Event, Adapter, Message, MessageSegment
|
||||||
|
|
||||||
|
|
||||||
def escape_text(s: str, *, escape_comma: bool = True) -> str:
|
def escape_text(s: str, *, escape_comma: bool = True) -> str:
|
||||||
@@ -12,11 +13,24 @@ def escape_text(s: str, *, escape_comma: bool = True) -> str:
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
class FakeAdapter(Adapter):
|
||||||
|
@classmethod
|
||||||
|
@override
|
||||||
|
def get_name(cls) -> str:
|
||||||
|
return "fake"
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _call_api(self, bot: Bot, api: str, **data):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class FakeMessageSegment(MessageSegment["FakeMessage"]):
|
class FakeMessageSegment(MessageSegment["FakeMessage"]):
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@override
|
||||||
def get_message_class(cls):
|
def get_message_class(cls):
|
||||||
return FakeMessage
|
return FakeMessage
|
||||||
|
|
||||||
|
@override
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.data["text"] if self.type == "text" else f"[fake:{self.type}]"
|
return self.data["text"] if self.type == "text" else f"[fake:{self.type}]"
|
||||||
|
|
||||||
@@ -32,16 +46,19 @@ class FakeMessageSegment(MessageSegment["FakeMessage"]):
|
|||||||
def nested(content: "FakeMessage"):
|
def nested(content: "FakeMessage"):
|
||||||
return FakeMessageSegment("node", {"content": content})
|
return FakeMessageSegment("node", {"content": content})
|
||||||
|
|
||||||
|
@override
|
||||||
def is_text(self) -> bool:
|
def is_text(self) -> bool:
|
||||||
return self.type == "text"
|
return self.type == "text"
|
||||||
|
|
||||||
|
|
||||||
class FakeMessage(Message[FakeMessageSegment]):
|
class FakeMessage(Message[FakeMessageSegment]):
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@override
|
||||||
def get_segment_class(cls):
|
def get_segment_class(cls):
|
||||||
return FakeMessageSegment
|
return FakeMessageSegment
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@override
|
||||||
def _construct(msg: Union[str, Iterable[Mapping]]):
|
def _construct(msg: Union[str, Iterable[Mapping]]):
|
||||||
if isinstance(msg, str):
|
if isinstance(msg, str):
|
||||||
yield FakeMessageSegment.text(msg)
|
yield FakeMessageSegment.text(msg)
|
||||||
@@ -50,6 +67,7 @@ class FakeMessage(Message[FakeMessageSegment]):
|
|||||||
yield FakeMessageSegment(**seg)
|
yield FakeMessageSegment(**seg)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@override
|
||||||
def __add__(
|
def __add__(
|
||||||
self, other: Union[str, FakeMessageSegment, Iterable[FakeMessageSegment]]
|
self, other: Union[str, FakeMessageSegment, Iterable[FakeMessageSegment]]
|
||||||
):
|
):
|
||||||
@@ -71,30 +89,37 @@ def make_fake_event(
|
|||||||
Base = _base or Event
|
Base = _base or Event
|
||||||
|
|
||||||
class FakeEvent(Base, extra=Extra.forbid):
|
class FakeEvent(Base, extra=Extra.forbid):
|
||||||
|
@override
|
||||||
def get_type(self) -> str:
|
def get_type(self) -> str:
|
||||||
return _type
|
return _type
|
||||||
|
|
||||||
|
@override
|
||||||
def get_event_name(self) -> str:
|
def get_event_name(self) -> str:
|
||||||
return _name
|
return _name
|
||||||
|
|
||||||
|
@override
|
||||||
def get_event_description(self) -> str:
|
def get_event_description(self) -> str:
|
||||||
return _description
|
return _description
|
||||||
|
|
||||||
|
@override
|
||||||
def get_user_id(self) -> str:
|
def get_user_id(self) -> str:
|
||||||
if _user_id is not None:
|
if _user_id is not None:
|
||||||
return _user_id
|
return _user_id
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@override
|
||||||
def get_session_id(self) -> str:
|
def get_session_id(self) -> str:
|
||||||
if _session_id is not None:
|
if _session_id is not None:
|
||||||
return _session_id
|
return _session_id
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@override
|
||||||
def get_message(self) -> "Message":
|
def get_message(self) -> "Message":
|
||||||
if _message is not None:
|
if _message is not None:
|
||||||
return _message
|
return _message
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@override
|
||||||
def is_tome(self) -> bool:
|
def is_tome(self) -> bool:
|
||||||
return _to_me
|
return _to_me
|
||||||
|
|
||||||
|
45
tsconfig.json
Normal file
45
tsconfig.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"module": "NodeNext",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": false,
|
||||||
|
"sourceMap": false,
|
||||||
|
"jsx": "react-native",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
// "noUnusedLocals": false, // ensured by eslint, should not block compilation
|
||||||
|
// "noImplicitReturns": true,
|
||||||
|
// "noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
/* Disabled on purpose (handled by ESLint, should not block compilation) */
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
|
||||||
|
/* Advanced Options */
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true, // @types/webpack and webpack/types.d.ts are not the same thing
|
||||||
|
|
||||||
|
/* Use tslib */
|
||||||
|
"importHelpers": true,
|
||||||
|
"noEmitHelpers": true
|
||||||
|
},
|
||||||
|
"include": ["./**/.eslintrc.js", "./**/.stylelintrc.js"],
|
||||||
|
"exclude": ["node_modules", "**/lib/**/*"]
|
||||||
|
}
|
@@ -1,33 +0,0 @@
|
|||||||
# Website
|
|
||||||
|
|
||||||
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```
|
|
||||||
$ yarn
|
|
||||||
```
|
|
||||||
|
|
||||||
### Local Development
|
|
||||||
|
|
||||||
```
|
|
||||||
$ yarn start
|
|
||||||
```
|
|
||||||
|
|
||||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```
|
|
||||||
$ yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
|
||||||
|
|
||||||
### Deployment
|
|
||||||
|
|
||||||
```
|
|
||||||
$ GIT_USER=<Your GitHub username> USE_SSH=true yarn deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
|
@@ -4,8 +4,8 @@ description: 注册适配器与指定平台交互
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 20
|
- category: advanced
|
||||||
category: advanced
|
weight: 20
|
||||||
---
|
---
|
||||||
|
|
||||||
# 使用适配器
|
# 使用适配器
|
||||||
@@ -158,4 +158,4 @@ is_tome: bool = event.is_tome()
|
|||||||
|
|
||||||
## 更多
|
## 更多
|
||||||
|
|
||||||
官方支持的适配器和社区贡献的适配器均可在[商店](/store)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。
|
官方支持的适配器和社区贡献的适配器均可在[商店](/store/adapters)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。
|
||||||
|
@@ -4,8 +4,8 @@ description: 通过依赖注入获取上下文信息
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 70
|
- category: advanced
|
||||||
category: advanced
|
weight: 70
|
||||||
---
|
---
|
||||||
|
|
||||||
# 依赖注入
|
# 依赖注入
|
||||||
@@ -219,7 +219,7 @@ async def _(e: Union[ActionFailed, NetworkError]): ...
|
|||||||
<Tabs groupId="python">
|
<Tabs groupId="python">
|
||||||
<TabItem value="3.9" label="Python 3.9+" default>
|
<TabItem value="3.9" label="Python 3.9+" default>
|
||||||
|
|
||||||
```python {4,16}
|
```python {5,15}
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from nonebot import on_command
|
from nonebot import on_command
|
||||||
@@ -241,7 +241,7 @@ async def _(event: Annotated[Event, Depends(check)]):
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="3.8" label="Python 3.8+">
|
<TabItem value="3.8" label="Python 3.8+">
|
||||||
|
|
||||||
```python {2,14}
|
```python {3,13}
|
||||||
from nonebot import on_command
|
from nonebot import on_command
|
||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
from nonebot.params import Depends
|
from nonebot.params import Depends
|
||||||
@@ -267,7 +267,7 @@ async def _(event: Event = Depends(check)):
|
|||||||
|
|
||||||
特别的,我们可以为 `Dependent` 对象定义一系列前置子依赖,它们会在参数执行前被顺序执行,且返回值将会被忽略,例如:
|
特别的,我们可以为 `Dependent` 对象定义一系列前置子依赖,它们会在参数执行前被顺序执行,且返回值将会被忽略,例如:
|
||||||
|
|
||||||
```python {2,14}
|
```python {11}
|
||||||
from nonebot import on_command
|
from nonebot import on_command
|
||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
from nonebot.params import Depends
|
from nonebot.params import Depends
|
||||||
@@ -353,6 +353,80 @@ async def _(x: int = Depends(random_result, use_cache=False)):
|
|||||||
缓存的生命周期与当前接收到的事件相同。接收到事件后,子依赖在首次执行时缓存,在该事件处理完成后,缓存就会被清除。
|
缓存的生命周期与当前接收到的事件相同。接收到事件后,子依赖在首次执行时缓存,在该事件处理完成后,缓存就会被清除。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
### 类型转换与校验
|
||||||
|
|
||||||
|
在依赖注入系统中,我们可以对子依赖的返回值进行自动类型转换与校验。这个功能由 Pydantic 支持,因此我们通过参数类型注解自动使用 Pydantic 支持的类型转换。例如:
|
||||||
|
|
||||||
|
<Tabs groupId="python">
|
||||||
|
<TabItem value="3.9" label="Python 3.9+" default>
|
||||||
|
|
||||||
|
```python {6,9}
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from nonebot.params import Depends
|
||||||
|
from nonebot.adapters import Event
|
||||||
|
|
||||||
|
def get_user_id(event: Event) -> str:
|
||||||
|
return event.get_user_id()
|
||||||
|
|
||||||
|
async def _(user_id: Annotated[int, Depends(get_user_id, validate=True)]):
|
||||||
|
print(user_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="3.8" label="Python 3.8+">
|
||||||
|
|
||||||
|
```python {4,7}
|
||||||
|
from nonebot.params import Depends
|
||||||
|
from nonebot.adapters import Event
|
||||||
|
|
||||||
|
def get_user_id(event: Event) -> str:
|
||||||
|
return event.get_user_id()
|
||||||
|
|
||||||
|
async def _(user_id: int = Depends(get_user_id, validate=True)):
|
||||||
|
print(user_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
在进行类型自动转换的同时,Pydantic 还支持对数据进行更多的限制,如:大于、小于、长度等。使用方法如下:
|
||||||
|
|
||||||
|
<Tabs groupId="python">
|
||||||
|
<TabItem value="3.9" label="Python 3.9+" default>
|
||||||
|
|
||||||
|
```python {7,10}
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
from nonebot.params import Depends
|
||||||
|
from nonebot.adapters import Event
|
||||||
|
|
||||||
|
def get_user_id(event: Event) -> str:
|
||||||
|
return event.get_user_id()
|
||||||
|
|
||||||
|
async def _(user_id: Annotated[int, Depends(get_user_id, validate=Field(gt=100))]):
|
||||||
|
print(user_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="3.8" label="Python 3.8+">
|
||||||
|
|
||||||
|
```python {5,8}
|
||||||
|
from pydantic import Field
|
||||||
|
from nonebot.params import Depends
|
||||||
|
from nonebot.adapters import Event
|
||||||
|
|
||||||
|
def get_user_id(event: Event) -> str:
|
||||||
|
return event.get_user_id()
|
||||||
|
|
||||||
|
async def _(user_id: int = Depends(get_user_id, validate=Field(gt=100))):
|
||||||
|
print(user_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
### 类作为依赖
|
### 类作为依赖
|
||||||
|
|
||||||
在前面的事例中,我们使用了函数作为子依赖。实际上,我们还可以使用类作为依赖。当我们在实例化一个类的时候,其实我们就在调用它,类本身也是一个可调用对象。例如:
|
在前面的事例中,我们使用了函数作为子依赖。实际上,我们还可以使用类作为依赖。当我们在实例化一个类的时候,其实我们就在调用它,类本身也是一个可调用对象。例如:
|
||||||
@@ -483,7 +557,7 @@ async def _(x: httpx.AsyncClient = Depends(get_client)):
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
:::warning 注意
|
:::caution 注意
|
||||||
生成器作为依赖时,其中只能进行一次 `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) 文档。事实上,NoneBot 内部就使用了这两个装饰器。
|
生成器作为依赖时,其中只能进行一次 `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) 文档。事实上,NoneBot 内部就使用了这两个装饰器。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
@@ -4,8 +4,8 @@ description: 选择合适的驱动器运行机器人
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 10
|
- category: advanced
|
||||||
category: advanced
|
weight: 10
|
||||||
---
|
---
|
||||||
|
|
||||||
# 选择驱动器
|
# 选择驱动器
|
||||||
@@ -22,21 +22,22 @@ options:
|
|||||||
|
|
||||||
## 驱动器类型
|
## 驱动器类型
|
||||||
|
|
||||||
驱动器的类型有两种:
|
驱动器类型大体上可以分为两种:
|
||||||
|
|
||||||
- `ForwardDriver`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。
|
- `Forward`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。
|
||||||
- `ReverseDriver`:即服务端型驱动器,多用于使用 WebHook,接收 WebSocket 客户端连接等情形。
|
- `Reverse`:即服务端型驱动器,多用于使用 WebHook,接收 WebSocket 客户端连接等情形。
|
||||||
|
|
||||||
客户端型驱动器具有以下两种功能:
|
客户端型驱动器可以分为以下两种:
|
||||||
|
|
||||||
1. 异步发送 HTTP 请求,自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。
|
1. 异步发送 HTTP 请求,自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。
|
||||||
2. 异步建立 WebSocket 连接上下文,自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。
|
2. 异步建立 WebSocket 连接上下文,自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。
|
||||||
|
|
||||||
服务端型驱动器通常为 ASGI 应用框架,具有以下功能:
|
服务端型驱动器目前有:
|
||||||
|
|
||||||
1. 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。
|
1. ASGI 应用框架,具有以下功能:
|
||||||
2. 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。
|
- 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。
|
||||||
3. 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。
|
- 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。
|
||||||
|
- 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。
|
||||||
|
|
||||||
## 配置驱动器
|
## 配置驱动器
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ DRIVER=~none
|
|||||||
|
|
||||||
### FastAPI(默认)
|
### FastAPI(默认)
|
||||||
|
|
||||||
**类型:**服务端驱动器
|
**类型:**ASGI 服务端驱动器
|
||||||
|
|
||||||
> FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
|
> FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
|
||||||
|
|
||||||
@@ -117,7 +118,7 @@ DRIVER=~fastapi
|
|||||||
|
|
||||||
##### `fastapi_reload`
|
##### `fastapi_reload`
|
||||||
|
|
||||||
:::warning 警告
|
:::caution 警告
|
||||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -185,7 +186,7 @@ nonebot.run(app="bot:app")
|
|||||||
|
|
||||||
### Quart
|
### Quart
|
||||||
|
|
||||||
**类型:**`ReverseDriver`
|
**类型:**ASGI 服务端驱动器
|
||||||
|
|
||||||
> Quart is an asyncio reimplementation of the popular Flask microframework API.
|
> Quart is an asyncio reimplementation of the popular Flask microframework API.
|
||||||
|
|
||||||
@@ -199,7 +200,7 @@ DRIVER=~quart
|
|||||||
|
|
||||||
##### `quart_reload`
|
##### `quart_reload`
|
||||||
|
|
||||||
:::warning 警告
|
:::caution 警告
|
||||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -249,9 +250,9 @@ nonebot.run(app="bot:app")
|
|||||||
|
|
||||||
### HTTPX
|
### HTTPX
|
||||||
|
|
||||||
**类型:**`ForwardDriver`
|
**类型:**HTTP 客户端驱动器
|
||||||
|
|
||||||
:::warning 注意
|
:::caution 注意
|
||||||
本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。
|
本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -263,9 +264,9 @@ DRIVER=~httpx
|
|||||||
|
|
||||||
### websockets
|
### websockets
|
||||||
|
|
||||||
**类型:**`ForwardDriver`
|
**类型:**WebSocket 客户端驱动器
|
||||||
|
|
||||||
:::warning 注意
|
:::caution 注意
|
||||||
本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。
|
本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -277,7 +278,7 @@ DRIVER=~websockets
|
|||||||
|
|
||||||
### AIOHTTP
|
### AIOHTTP
|
||||||
|
|
||||||
**类型:**`ForwardDriver`
|
**类型:**HTTP/WebSocket 客户端驱动器
|
||||||
|
|
||||||
> [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python.
|
> [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python.
|
||||||
|
|
||||||
|
@@ -4,8 +4,8 @@ description: 自定义事件响应器存储
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 110
|
- category: advanced
|
||||||
category: advanced
|
weight: 110
|
||||||
---
|
---
|
||||||
|
|
||||||
# 事件响应器存储
|
# 事件响应器存储
|
||||||
|
@@ -4,8 +4,8 @@ description: 事件响应器组成与内置响应规则
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 60
|
- category: advanced
|
||||||
category: advanced
|
weight: 60
|
||||||
---
|
---
|
||||||
|
|
||||||
# 事件响应器进阶
|
# 事件响应器进阶
|
||||||
@@ -333,4 +333,6 @@ matcher2 = group.on_message()
|
|||||||
基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。
|
基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。
|
||||||
标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。
|
标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。
|
||||||
|
|
||||||
详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna.md) 章节。
|
该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。
|
||||||
|
|
||||||
|
详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna/README.mdx) 章节。
|
||||||
|
@@ -4,8 +4,8 @@ description: 填写与获取插件相关的信息
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 30
|
- category: advanced
|
||||||
category: advanced
|
weight: 30
|
||||||
---
|
---
|
||||||
|
|
||||||
# 插件信息
|
# 插件信息
|
||||||
@@ -14,7 +14,7 @@ NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。
|
|||||||
|
|
||||||
## 插件元数据
|
## 插件元数据
|
||||||
|
|
||||||
在 NoneBot 中,插件 [`Plugin`](../api/plugin/plugin.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。
|
在 NoneBot 中,插件 [`Plugin`](../api/plugin/model.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。
|
||||||
|
|
||||||
现在,假设我们有一个插件 `example`, 它的模块结构如下:
|
现在,假设我们有一个插件 `example`, 它的模块结构如下:
|
||||||
|
|
||||||
|
@@ -4,8 +4,8 @@ description: 编写与加载嵌套插件
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 40
|
- category: advanced
|
||||||
category: advanced
|
weight: 40
|
||||||
---
|
---
|
||||||
|
|
||||||
# 嵌套插件
|
# 嵌套插件
|
||||||
|
@@ -4,8 +4,8 @@ description: 使用其他插件提供的功能
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 50
|
- category: advanced
|
||||||
category: advanced
|
weight: 50
|
||||||
---
|
---
|
||||||
|
|
||||||
# 跨插件访问
|
# 跨插件访问
|
||||||
|
@@ -4,15 +4,15 @@ description: 添加服务端路由规则
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 100
|
- category: advanced
|
||||||
category: advanced
|
weight: 100
|
||||||
---
|
---
|
||||||
|
|
||||||
# 添加路由
|
# 添加路由
|
||||||
|
|
||||||
在[驱动器](./driver.md)一节中,我们了解了驱动器的两种类型。既然驱动器可以作为服务端运行,那么我们就可以向驱动器添加路由规则,从而实现自定义的 API 接口等功能。在添加路由规则时,我们需要注意驱动器的类型,详情可以参考[选择驱动器](./driver.md#配置驱动器)。
|
在[驱动器](./driver.md)一节中,我们了解了驱动器的两种类型。既然驱动器可以作为服务端运行,那么我们就可以向驱动器添加路由规则,从而实现自定义的 API 接口等功能。在添加路由规则时,我们需要注意驱动器的类型,详情可以参考[选择驱动器](./driver.md#配置驱动器)。
|
||||||
|
|
||||||
NoneBot 中,我们可以通过两种途径向驱动器添加路由规则:
|
NoneBot 中,我们可以通过两种途径向 ASGI 驱动器添加路由规则:
|
||||||
|
|
||||||
1. 通过 NoneBot 的兼容层建立路由规则。
|
1. 通过 NoneBot 的兼容层建立路由规则。
|
||||||
2. 直接向 ASGI 应用添加路由规则。
|
2. 直接向 ASGI 应用添加路由规则。
|
||||||
@@ -21,11 +21,12 @@ NoneBot 中,我们可以通过两种途径向驱动器添加路由规则:
|
|||||||
|
|
||||||
在向驱动器添加路由规则时,我们需要注意驱动器是否为服务端类型,我们可以通过以下方式判断:
|
在向驱动器添加路由规则时,我们需要注意驱动器是否为服务端类型,我们可以通过以下方式判断:
|
||||||
|
|
||||||
```python {3}
|
```python
|
||||||
from nonebot import get_driver
|
from nonebot import get_driver
|
||||||
from nonebot.drivers import ReverseDriver
|
from nonebot.drivers import ASGIMixin
|
||||||
|
|
||||||
can_use = isinstance(get_driver(), ReverseDriver)
|
# highlight-next-line
|
||||||
|
can_use = isinstance(get_driver(), ASGIMixin)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 通过兼容层添加路由
|
## 通过兼容层添加路由
|
||||||
@@ -45,12 +46,12 @@ NoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServer
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
from nonebot import get_driver
|
from nonebot import get_driver
|
||||||
from nonebot.drivers import URL, Request, Response, HTTPServerSetup
|
from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup
|
||||||
|
|
||||||
async def hello(request: Request) -> Response:
|
async def hello(request: Request) -> Response:
|
||||||
return Response(200, content="Hello, world!")
|
return Response(200, content="Hello, world!")
|
||||||
|
|
||||||
if isinstance((driver := get_driver()), ReverseDriver):
|
if isinstance((driver := get_driver()), ASGIMixin):
|
||||||
driver.setup_http_server(
|
driver.setup_http_server(
|
||||||
HTTPServerSetup(
|
HTTPServerSetup(
|
||||||
path=URL("/hello"),
|
path=URL("/hello"),
|
||||||
@@ -75,7 +76,7 @@ if isinstance((driver := get_driver()), ReverseDriver):
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
from nonebot import get_driver
|
from nonebot import get_driver
|
||||||
from nonebot.drivers import URL, WebSocket, WebSocketServerSetup
|
from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup
|
||||||
|
|
||||||
async def ws_handler(ws: WebSocket):
|
async def ws_handler(ws: WebSocket):
|
||||||
await ws.accept()
|
await ws.accept()
|
||||||
@@ -91,7 +92,7 @@ async def ws_handler(ws: WebSocket):
|
|||||||
await websocket.close()
|
await websocket.close()
|
||||||
# do some cleanup
|
# do some cleanup
|
||||||
|
|
||||||
if isinstance((driver := get_driver()), ReverseDriver):
|
if isinstance((driver := get_driver()), ASGIMixin):
|
||||||
driver.setup_websocket_server(
|
driver.setup_websocket_server(
|
||||||
WebSocketServerSetup(
|
WebSocketServerSetup(
|
||||||
path=URL("/ws"),
|
path=URL("/ws"),
|
||||||
|
@@ -4,8 +4,8 @@ description: 在特定的生命周期中执行代码
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 90
|
- category: advanced
|
||||||
category: advanced
|
weight: 90
|
||||||
---
|
---
|
||||||
|
|
||||||
# 钩子函数
|
# 钩子函数
|
||||||
|
@@ -4,8 +4,8 @@ description: 控制会话响应对象
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 80
|
- category: advanced
|
||||||
category: advanced
|
weight: 80
|
||||||
---
|
---
|
||||||
|
|
||||||
# 会话更新
|
# 会话更新
|
||||||
@@ -56,4 +56,4 @@ async def _(matcher: Matcher) -> Permission:
|
|||||||
|
|
||||||
请注意,此处为全大写字母的 `USER` 权限,它可以匹配多个会话 ID。通过这种方式,我们可以实现多用户同时参与的会话。
|
请注意,此处为全大写字母的 `USER` 权限,它可以匹配多个会话 ID。通过这种方式,我们可以实现多用户同时参与的会话。
|
||||||
|
|
||||||
我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store)。
|
我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store/plugins)。
|
||||||
|
@@ -4,14 +4,13 @@ description: 使用平台接口,完成更多功能
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 50
|
- category: appendices
|
||||||
category: appendices
|
weight: 50
|
||||||
---
|
---
|
||||||
|
|
||||||
# 使用平台接口
|
# 使用平台接口
|
||||||
|
|
||||||
import Messenger from "@site/src/components/Messenger";
|
import Messenger from "@/components/Messenger";
|
||||||
import MarkdownText from "!!raw-loader!./assets/console-markdown.txt";
|
|
||||||
|
|
||||||
在 NoneBot 中,除了使用事件响应器操作发送文本消息外,我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口,完成发送特殊消息、获取信息等其他平台提供的功能。同时,在部分无法使用事件响应器的情况中,例如[定时任务](../best-practice/scheduler.md),我们也可以使用平台接口来完成需要的功能。
|
在 NoneBot 中,除了使用事件响应器操作发送文本消息外,我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口,完成发送特殊消息、获取信息等其他平台提供的功能。同时,在部分无法使用事件响应器的情况中,例如[定时任务](../best-practice/scheduler.md),我们也可以使用平台接口来完成需要的功能。
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ import MarkdownText from "!!raw-loader!./assets/console-markdown.txt";
|
|||||||
|
|
||||||
在之前的章节中,我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md),现在我们来向用户发送平台特殊消息。
|
在之前的章节中,我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md),现在我们来向用户发送平台特殊消息。
|
||||||
|
|
||||||
:::warning 注意
|
:::caution 注意
|
||||||
在以下的示例中,我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。
|
在以下的示例中,我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -49,7 +48,11 @@ async def got_location(location: str = ArgPlainText()):
|
|||||||
{ position: "right", msg: "/天气" },
|
{ position: "right", msg: "/天气" },
|
||||||
{ position: "left", msg: "❓请输入地名" },
|
{ position: "left", msg: "❓请输入地名" },
|
||||||
{ position: "right", msg: "北京" },
|
{ position: "right", msg: "北京" },
|
||||||
{ position: "left", msg: MarkdownText },
|
{
|
||||||
|
position: "left",
|
||||||
|
monospace: true,
|
||||||
|
msg: "┏━━━━━━━━━━━━━━━━┓\n┃ 北京 ┃\n┗━━━━━━━━━━━━━━━━┛\n• 今天\n⛅ 多云 20℃~24℃",
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -100,7 +103,7 @@ result = await bot.get_user_info(user_id=12345678)
|
|||||||
result = await bot.call_api("get_user_info", user_id=12345678)
|
result = await bot.call_api("get_user_info", user_id=12345678)
|
||||||
```
|
```
|
||||||
|
|
||||||
:::warning 注意
|
:::caution 注意
|
||||||
实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现,请参考协议适配器以及平台文档。
|
实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现,请参考协议适配器以及平台文档。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
┏━━━━━━━━━━━━━━━━┓
|
|
||||||
┃ 北京 ┃
|
|
||||||
┗━━━━━━━━━━━━━━━━┛
|
|
||||||
|
|
||||||
• 今天
|
|
||||||
⛅ 多云 20℃~24℃
|
|
@@ -4,8 +4,8 @@ description: 读取用户配置来控制插件行为
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 10
|
- category: appendices
|
||||||
category: appendices
|
weight: 10
|
||||||
---
|
---
|
||||||
|
|
||||||
# 配置
|
# 配置
|
||||||
@@ -62,7 +62,7 @@ export CUSTOM_CONFIG="config in environment variables"
|
|||||||
|
|
||||||
那最终 NoneBot 所读取的内容为环境变量中的内容,即 `config in environment variables`。
|
那最终 NoneBot 所读取的内容为环境变量中的内容,即 `config in environment variables`。
|
||||||
|
|
||||||
:::warning 注意
|
:::caution 注意
|
||||||
NoneBot 不会自发读取未被定义的配置项的环境变量,如果需要读取某一环境变量需要在 dotenv 配置文件中进行声明。
|
NoneBot 不会自发读取未被定义的配置项的环境变量,如果需要读取某一环境变量需要在 dotenv 配置文件中进行声明。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -482,7 +482,7 @@ nonebot.init(superusers={"123123123"})
|
|||||||
- **类型**: `set[str]`
|
- **类型**: `set[str]`
|
||||||
- **默认值**: `set()`
|
- **默认值**: `set()`
|
||||||
|
|
||||||
机器人昵称,通常协议适配器会根据用户是否 @user 或者是否以机器人昵称开头来判断是否是向机器人发送的消息。
|
机器人昵称,通常协议适配器会根据用户是否 @bot 或者是否以机器人昵称开头来判断是否是向机器人发送的消息。
|
||||||
|
|
||||||
<Tabs groupId="configMethod">
|
<Tabs groupId="configMethod">
|
||||||
<TabItem value="dotenv" label="dotenv" default>
|
<TabItem value="dotenv" label="dotenv" default>
|
||||||
|
@@ -4,8 +4,8 @@ description: 记录与控制日志
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 70
|
- category: appendices
|
||||||
category: appendices
|
weight: 70
|
||||||
---
|
---
|
||||||
|
|
||||||
# 日志
|
# 日志
|
||||||
|
@@ -4,8 +4,8 @@ description: 根据事件类型进行不同的处理
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 80
|
- category: appendices
|
||||||
category: appendices
|
weight: 80
|
||||||
---
|
---
|
||||||
|
|
||||||
# 事件类型与重载
|
# 事件类型与重载
|
||||||
@@ -28,7 +28,7 @@ async def got_location(event: MessageEvent, location: str = ArgPlainText()):
|
|||||||
|
|
||||||
在上面的代码中,我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。
|
在上面的代码中,我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。
|
||||||
|
|
||||||
:::warning 注意
|
:::caution 注意
|
||||||
如果**基类**就能满足你的需求,那么就**不要修改**事件参数类型注解,这样可以使你的代码更加**通用**,可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理,我们将在[重载](#重载)一节中介绍。
|
如果**基类**就能满足你的需求,那么就**不要修改**事件参数类型注解,这样可以使你的代码更加**通用**,可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理,我们将在[重载](#重载)一节中介绍。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ async def handle_onebot(bot: OneBot):
|
|||||||
await bot.send_group_message(group_id=123123, message="OneBot")
|
await bot.send_group_message(group_id=123123, message="OneBot")
|
||||||
```
|
```
|
||||||
|
|
||||||
:::warning 注意
|
:::caution 注意
|
||||||
重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。
|
重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。
|
||||||
|
|
||||||
但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。
|
但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。
|
||||||
|
@@ -4,8 +4,8 @@ description: 控制事件响应器的权限
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 60
|
- category: appendices
|
||||||
category: appendices
|
weight: 60
|
||||||
---
|
---
|
||||||
|
|
||||||
# 权限控制
|
# 权限控制
|
||||||
|
@@ -4,8 +4,8 @@ description: 自定义响应规则
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 20
|
- category: appendices
|
||||||
category: appendices
|
weight: 20
|
||||||
---
|
---
|
||||||
|
|
||||||
# 响应规则
|
# 响应规则
|
||||||
|
@@ -4,8 +4,8 @@ description: 更灵活的会话控制
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 30
|
- category: appendices
|
||||||
category: appendices
|
weight: 30
|
||||||
---
|
---
|
||||||
|
|
||||||
# 会话控制
|
# 会话控制
|
||||||
@@ -322,7 +322,7 @@ async def _(matcher: Matcher):
|
|||||||
matcher.stop_propagation()
|
matcher.stop_propagation()
|
||||||
```
|
```
|
||||||
|
|
||||||
:::warning 注意
|
:::caution 注意
|
||||||
`stop_propagation` 操作是实例方法,需要先通过依赖注入获取事件响应器实例再进行调用。
|
`stop_propagation` 操作是实例方法,需要先通过依赖注入获取事件响应器实例再进行调用。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -374,6 +374,14 @@ async def _(matcher: Matcher):
|
|||||||
|
|
||||||
`get_last_receive` 操作接受一个可选的 default 参数。当事件不存在时,将返回 default 或 `None`。
|
`get_last_receive` 操作接受一个可选的 default 参数。当事件不存在时,将返回 default 或 `None`。
|
||||||
|
|
||||||
|
```python
|
||||||
|
from nonebot.matcher import Matcher
|
||||||
|
|
||||||
|
@matcher.handle()
|
||||||
|
async def _(matcher: Matcher):
|
||||||
|
event = matcher.get_last_receive(default=None)
|
||||||
|
```
|
||||||
|
|
||||||
### set_receive
|
### set_receive
|
||||||
|
|
||||||
设置 / 覆盖一个 `receive` 接收的事件。
|
设置 / 覆盖一个 `receive` 接收的事件。
|
||||||
|
@@ -4,8 +4,8 @@ description: 会话状态信息
|
|||||||
|
|
||||||
options:
|
options:
|
||||||
menu:
|
menu:
|
||||||
weight: 40
|
- category: appendices
|
||||||
category: appendices
|
weight: 40
|
||||||
---
|
---
|
||||||
|
|
||||||
# 会话状态
|
# 会话状态
|
||||||
|
@@ -1,288 +0,0 @@
|
|||||||
---
|
|
||||||
sidebar_position: 6
|
|
||||||
description: Alconna 命令解析拓展
|
|
||||||
---
|
|
||||||
|
|
||||||
# Alconna 命令解析
|
|
||||||
|
|
||||||
[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。
|
|
||||||
该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器,
|
|
||||||
是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。
|
|
||||||
|
|
||||||
特点包括:
|
|
||||||
|
|
||||||
- 高效
|
|
||||||
- 直观的命令组件创建方式
|
|
||||||
- 强大的类型解析与类型转换功能
|
|
||||||
- 自定义的帮助信息格式
|
|
||||||
- 多语言支持
|
|
||||||
- 易用的快捷命令创建与使用
|
|
||||||
- 可创建命令补全会话, 以实现多轮连续的补全提示
|
|
||||||
- 可嵌套的多级子命令
|
|
||||||
- 正则匹配支持
|
|
||||||
|
|
||||||
该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。
|
|
||||||
|
|
||||||
同时,基于 [Annotated 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解 `AlcMatches` 与 `AlcResult`
|
|
||||||
|
|
||||||
该插件还可以通过 `handle(parameterless)` 来控制一个具体的响应函数是否在不满足条件时跳过响应:
|
|
||||||
|
|
||||||
- `pip.handle([Check(assign("add.name", "nb"))])` 表示仅在命令为 `role-group add` 并且 name 为 `nb` 时响应
|
|
||||||
- `pip.handle([Check(assign("list"))])` 表示仅在命令为 `role-group list` 时响应
|
|
||||||
- `pip.handle([Check(assign("add"))])` 表示仅在命令为 `role-group add` 时响应
|
|
||||||
|
|
||||||
基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。
|
|
||||||
标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。
|
|
||||||
|
|
||||||
## 安装插件
|
|
||||||
|
|
||||||
在使用前请先安装 `nonebot-plugin-alconna` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如:
|
|
||||||
|
|
||||||
在**项目目录**下执行以下命令:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
nb plugin install nonebot-plugin-alconna
|
|
||||||
```
|
|
||||||
|
|
||||||
或
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pip install nonebot-plugin-alconna
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用插件
|
|
||||||
|
|
||||||
以下为一个简单的使用示例:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna.adapters import At
|
|
||||||
from nonebot.adapters.onebot.v12 import Message
|
|
||||||
from nonebot_plugin_alconna.adapters.onebot12 import Image
|
|
||||||
from nonebot_plugin_alconna import AlconnaMatches, on_alconna
|
|
||||||
from nonebot.adapters.onebot.v12 import MessageSegment as Ob12MS
|
|
||||||
from arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand
|
|
||||||
|
|
||||||
alc = Alconna(
|
|
||||||
["/", "!"],
|
|
||||||
"role-group",
|
|
||||||
Subcommand(
|
|
||||||
"add",
|
|
||||||
Args["name", str],
|
|
||||||
Option("member", Args["target", MultiVar(At)]),
|
|
||||||
),
|
|
||||||
Option("list"),
|
|
||||||
)
|
|
||||||
rg = on_alconna(alc, auto_send_output=True)
|
|
||||||
|
|
||||||
|
|
||||||
@rg.handle()
|
|
||||||
async def _(result: Arparma = AlconnaMatches()):
|
|
||||||
if result.find("list"):
|
|
||||||
img = await gen_role_group_list_image()
|
|
||||||
await rg.finish(Message([Image(img)]))
|
|
||||||
if result.find("add"):
|
|
||||||
group = await create_role_group(result["add.name"])
|
|
||||||
if result.find("add.member"):
|
|
||||||
ats: tuple[Ob12MS, ...] = result["add.member.target"]
|
|
||||||
group.extend(member.data["user_id"] for member in ats)
|
|
||||||
await rg.finish("添加成功")
|
|
||||||
```
|
|
||||||
|
|
||||||
### 导入插件
|
|
||||||
|
|
||||||
由于 `nonebot-plugin-alconna` 作为插件,因此需要在使用前对其进行**加载**并**导入**其中的 `on_alconna` 来使用命令拓展。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解。
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot import require
|
|
||||||
|
|
||||||
require("nonebot_plugin_alconna")
|
|
||||||
|
|
||||||
from nonebot_plugin_alconna import on_alconna
|
|
||||||
```
|
|
||||||
|
|
||||||
### 命令编写
|
|
||||||
|
|
||||||
我们可以看到主要的两大组件:`Option` 与 `Subcommand`。
|
|
||||||
|
|
||||||
`Option` 可以传入一组别名,如 `Option("--foo|-F|--FOO|-f")` 或 `Option("--foo", alias=["-F"]`
|
|
||||||
|
|
||||||
`Subcommand` 则可以传入自己的 `Option` 与 `Subcommand`:
|
|
||||||
|
|
||||||
他们拥有如下共同参数:
|
|
||||||
|
|
||||||
- `help_text`: 传入该组件的帮助信息
|
|
||||||
- `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name)
|
|
||||||
- `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换
|
|
||||||
- `default`: 默认值,在该组件未被解析时使用使用该值替换。
|
|
||||||
|
|
||||||
然后是 `Args` 与 `MultiVar`,他们是用于解析参数的组件。
|
|
||||||
|
|
||||||
`Args` 是参数解析的基础组件,构造方法形如 `Args["foo", str]["bar", int]["baz", bool, False]`,
|
|
||||||
与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。
|
|
||||||
|
|
||||||
`MultiVar` 则是一个特殊的标注,用于告知解析器该参数可以接受多个值,其构造方法形如 `MultiVar(str)`。
|
|
||||||
同样的还有 `KeyWordVar`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。
|
|
||||||
|
|
||||||
:::tip
|
|
||||||
`MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,其构造方法形如 `MultiVar(KeyWordVar(str))`
|
|
||||||
|
|
||||||
`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值。
|
|
||||||
|
|
||||||
`MultiVar` 不能在 `KeyWordVar` 之后传入。
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 参数标注
|
|
||||||
|
|
||||||
`Args` 的参数类型表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例。
|
|
||||||
|
|
||||||
```python
|
|
||||||
from arclet.alconna import Args
|
|
||||||
from nepattern import BasePattern
|
|
||||||
|
|
||||||
# 表示 foo 参数需要匹配一个 @number 样式的字符串
|
|
||||||
args = Args["foo", BasePattern("@\d+")]
|
|
||||||
```
|
|
||||||
|
|
||||||
示例中传入的 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]`。
|
|
||||||
|
|
||||||
默认支持的类型有:
|
|
||||||
|
|
||||||
- `str`: 匹配任意字符串
|
|
||||||
- `int`: 匹配整数
|
|
||||||
- `float`: 匹配浮点数
|
|
||||||
- `bool`: 匹配 `True` 与 `False` 以及他们小写形式
|
|
||||||
- `hex`: 匹配 `0x` 开头的十六进制字符串
|
|
||||||
- `url`: 匹配网址
|
|
||||||
- `email`: 匹配 `xxxx@xxx` 的字符串
|
|
||||||
- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串
|
|
||||||
- `list`: 匹配类似 `["foo","bar","baz"]` 的字符串
|
|
||||||
- `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串
|
|
||||||
- `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳
|
|
||||||
- `Any`: 匹配任意类型
|
|
||||||
- `AnyString`: 匹配任意类型,转为 `str`
|
|
||||||
- `Number`: 匹配 `int` 与 `float`,转为 `int`
|
|
||||||
|
|
||||||
同时可以使用 typing 中的类型:
|
|
||||||
|
|
||||||
- `Literal[X]`: 匹配其中的任意一个值
|
|
||||||
- `Union[X, Y]`: 匹配其中的任意一个类型
|
|
||||||
- `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值
|
|
||||||
- `List[X]`: 匹配一个列表,其中的元素为 `X` 类型
|
|
||||||
- `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型,value 为 `Y` 类型
|
|
||||||
- ...
|
|
||||||
|
|
||||||
:::tip
|
|
||||||
几类特殊的传入标记:
|
|
||||||
|
|
||||||
- `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联)
|
|
||||||
- `RawStr("foo")`: 匹配字符串 "foo" (不会被 `BasePattern` 替换)
|
|
||||||
- `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz"
|
|
||||||
- `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型
|
|
||||||
- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值
|
|
||||||
- `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0]
|
|
||||||
- `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象
|
|
||||||
- `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值)
|
|
||||||
- ...
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 消息段标注
|
|
||||||
|
|
||||||
示例中使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。
|
|
||||||
|
|
||||||
消息段标注会匹配特定的 `MessageSegment`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
...
|
|
||||||
ats: tuple[Ob12MS, ...] = result["add.member.target"]
|
|
||||||
group.extend(member.data["user_id"] for member in ats)
|
|
||||||
```
|
|
||||||
|
|
||||||
:::tip
|
|
||||||
通用标注与适配器标注的区别在于,通用标注会匹配多个适配器中相似类型的消息段。
|
|
||||||
|
|
||||||
通用标注返回的是 `nonebot_plugin_alconna.adapters` 中定义的 `Segment` 模型:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Segment:
|
|
||||||
"""基类标注"""
|
|
||||||
origin: MessageSegment
|
|
||||||
|
|
||||||
class At(Segment):
|
|
||||||
"""At对象, 表示一类提醒某用户的元素"""
|
|
||||||
target: str
|
|
||||||
|
|
||||||
class Emoji(Segment):
|
|
||||||
"""Emoji对象, 表示一类表情元素"""
|
|
||||||
id: str
|
|
||||||
name: Optional[str]
|
|
||||||
|
|
||||||
class Media(Segment):
|
|
||||||
url: Optional[str]
|
|
||||||
id: Optional[str]
|
|
||||||
|
|
||||||
class Image(Media):
|
|
||||||
"""Image对象, 表示一类图片元素"""
|
|
||||||
|
|
||||||
class Audio(Media):
|
|
||||||
"""Audio对象, 表示一类音频元素"""
|
|
||||||
|
|
||||||
class Voice(Media):
|
|
||||||
"""Voice对象, 表示一类语音元素"""
|
|
||||||
|
|
||||||
class Video(Media):
|
|
||||||
"""Video对象, 表示一类视频元素"""
|
|
||||||
|
|
||||||
class File(Segment):
|
|
||||||
"""File对象, 表示一类文件元素"""
|
|
||||||
id: str
|
|
||||||
name: Optional[str] = field(default=None)
|
|
||||||
```
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 响应器使用
|
|
||||||
|
|
||||||
`on_alconna` 的所有参数如下:
|
|
||||||
|
|
||||||
- `command: Alconna | str`: Alconna 命令
|
|
||||||
- `skip_for_unmatch: bool = True`: 是否在命令不匹配时跳过该响应
|
|
||||||
- `auto_send_output: bool = False`: 是否自动发送输出信息并跳过响应
|
|
||||||
- `output_converter: TConvert | None = None`: 输出信息字符串转换为消息序列方法
|
|
||||||
- `aliases: set[str | tuple[str, ...]] | None = None`: 命令别名, 作用类似于 `on_command` 中的 aliases
|
|
||||||
- `comp_config: CompConfig | None = None`: 补全会话配置, 不传入则不启用补全会话
|
|
||||||
|
|
||||||
`AlconnaMatches` 是一个依赖注入函数,可注入 `Alconna` 命令解析结果。
|
|
||||||
|
|
||||||
### 配置项
|
|
||||||
|
|
||||||
#### alconna_auto_send_output
|
|
||||||
|
|
||||||
- **类型**: `bool`
|
|
||||||
- **默认值**: `False`
|
|
||||||
|
|
||||||
"是否全局启用输出信息自动发送,不启用则会在触特殊内置选项后仍然将解析结果传递至响应器。
|
|
||||||
|
|
||||||
#### alconna_use_command_start
|
|
||||||
|
|
||||||
- **类型**: `bool`
|
|
||||||
- **默认值**: `False`
|
|
||||||
|
|
||||||
是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀
|
|
||||||
|
|
||||||
#### alconna_auto_completion
|
|
||||||
|
|
||||||
- **类型**: `bool`
|
|
||||||
- **默认值**: `False`
|
|
||||||
|
|
||||||
是否全局启用命令自动补全,启用后会在参数缺失或触发 `--comp` 选项时自自动启用交互式补全。
|
|
||||||
|
|
||||||
## 文档参考
|
|
||||||
|
|
||||||
插件文档: [📦 这里](https://github.com/nonebot/plugin-alconna/blob/master/docs.md)
|
|
||||||
|
|
||||||
官方文档: [👉 指路](https://arclet.top/)
|
|
||||||
|
|
||||||
QQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH)
|
|
||||||
|
|
||||||
友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html)
|
|
142
website/docs/best-practice/alconna/README.mdx
Normal file
142
website/docs/best-practice/alconna/README.mdx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
description: Alconna 命令解析拓展
|
||||||
|
|
||||||
|
slug: /best-practice/alconna/
|
||||||
|
---
|
||||||
|
|
||||||
|
import Tabs from "@theme/Tabs";
|
||||||
|
import TabItem from "@theme/TabItem";
|
||||||
|
|
||||||
|
# Alconna 插件
|
||||||
|
|
||||||
|
[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。
|
||||||
|
该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器,
|
||||||
|
是一个简单、灵活、高效的命令参数解析器,并且不局限于解析命令式字符串。
|
||||||
|
|
||||||
|
该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。
|
||||||
|
|
||||||
|
该插件声明了一个 `Matcher` 的子类 `AlconnaMatcher`,并在 `AlconnaMatcher` 中添加了一些新的方法,例如:
|
||||||
|
|
||||||
|
- `assign`:基于 `Alconna` 解析结果,执行满足目标路径的处理函数
|
||||||
|
- `dispatch`:类似 `CommandGroup`,对目标路径创建一个新的 `AlconnaMatcher`,并将解析结果分配给该 `AlconnaMatcher`
|
||||||
|
- `got_path`:类似 `got`,但是可以指定目标路径,并且能够验证解析结果是否可用
|
||||||
|
- ...
|
||||||
|
|
||||||
|
基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。
|
||||||
|
标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。
|
||||||
|
|
||||||
|
该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。
|
||||||
|
|
||||||
|
## 安装插件
|
||||||
|
|
||||||
|
在使用前请先安装 `nonebot-plugin-alconna` 插件至项目环境中,可参考[获取商店插件](../../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如:
|
||||||
|
|
||||||
|
在**项目目录**下执行以下命令:
|
||||||
|
|
||||||
|
<Tabs groupId="install">
|
||||||
|
<TabItem value="cli" label="使用 nb-cli">
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nb plugin install nonebot-plugin-alconna
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="pip" label="使用 pip">
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install nonebot-plugin-alconna
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="pdm" label="使用 pdm">
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pdm add nonebot-plugin-alconna
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## 导入插件
|
||||||
|
|
||||||
|
由于 `nonebot-plugin-alconna` 作为插件,因此需要在使用前对其进行**加载**并**导入**其中的 `on_alconna` 来使用命令拓展。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../../advanced/requiring.md) 一节进行了解。
|
||||||
|
|
||||||
|
```python
|
||||||
|
from nonebot import require
|
||||||
|
|
||||||
|
require("nonebot_plugin_alconna")
|
||||||
|
|
||||||
|
from nonebot_plugin_alconna import on_alconna
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用插件
|
||||||
|
|
||||||
|
在前面的[深入指南](../../appendices/session-control.mdx)中,我们已经得到了一个天气插件。
|
||||||
|
现在我们将使用 `Alconna` 来改写这个插件。
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>插件示例</summary>
|
||||||
|
|
||||||
|
```python title=weather/__init__.py
|
||||||
|
from nonebot import on_command
|
||||||
|
from nonebot.rule import to_me
|
||||||
|
from nonebot.matcher import Matcher
|
||||||
|
from nonebot.adapters import Message
|
||||||
|
from nonebot.params import CommandArg, ArgPlainText
|
||||||
|
|
||||||
|
weather = on_command("天气", rule=to_me(), aliases={"weather", "天气预报"})
|
||||||
|
|
||||||
|
@weather.handle()
|
||||||
|
async def handle_function(matcher: Matcher, args: Message = CommandArg()):
|
||||||
|
if args.extract_plain_text():
|
||||||
|
matcher.set_arg("location", args)
|
||||||
|
|
||||||
|
@weather.got("location", prompt="请输入地名")
|
||||||
|
async def got_location(location: str = ArgPlainText()):
|
||||||
|
if location not in ["北京", "上海", "广州", "深圳"]:
|
||||||
|
await weather.reject(f"你想查询的城市 {location} 暂不支持,请重新输入!")
|
||||||
|
await weather.finish(f"今天{location}的天气是...")
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```python {5-10,14-16,18-19}
|
||||||
|
from nonebot.rule import to_me
|
||||||
|
from arclet.alconna import Alconna, Args
|
||||||
|
from nonebot_plugin_alconna import Match, on_alconna
|
||||||
|
|
||||||
|
weather = on_alconna(
|
||||||
|
Alconna("天气", Args["location?", str]),
|
||||||
|
rule=to_me(),
|
||||||
|
)
|
||||||
|
weather.shortcut("weather", {"command": "天气"})
|
||||||
|
weather.shortcut("天气预报", {"command": "天气"})
|
||||||
|
|
||||||
|
|
||||||
|
@weather.handle()
|
||||||
|
async def handle_function(location: Match[str]):
|
||||||
|
if location.available:
|
||||||
|
weather.set_path_arg("location", location.result)
|
||||||
|
|
||||||
|
@weather.got_path("location", prompt="请输入地名")
|
||||||
|
async def got_location(location: str):
|
||||||
|
if location not in ["北京", "上海", "广州", "深圳"]:
|
||||||
|
await weather.reject(f"你想查询的城市 {location} 暂不支持,请重新输入!")
|
||||||
|
await weather.finish(f"今天{location}的天气是...")
|
||||||
|
```
|
||||||
|
|
||||||
|
在上面的代码中,我们使用 `Alconna` 来解析命令,`on_alconna` 用来创建响应器,使用 `Match` 来获取解析结果。
|
||||||
|
|
||||||
|
关于更多 `Alconna` 的使用方法,可参考 [Alconna 文档](https://arclet.top/docs/tutorial/alconna),
|
||||||
|
或阅读 [Alconna 基本介绍](./command.md) 一节。
|
||||||
|
|
||||||
|
关于更多 `on_alconna` 的使用方法,可参考 [插件文档](https://github.com/nonebot/plugin-alconna/blob/master/docs.md),
|
||||||
|
或阅读 [响应规则的使用](./matcher.md) 一节。
|
||||||
|
|
||||||
|
## 交流与反馈
|
||||||
|
|
||||||
|
QQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH)
|
||||||
|
|
||||||
|
友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html)
|
4
website/docs/best-practice/alconna/_category_.json
Normal file
4
website/docs/best-practice/alconna/_category_.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"label": "Alconna 命令解析拓展",
|
||||||
|
"position": 6
|
||||||
|
}
|
578
website/docs/best-practice/alconna/command.md
Normal file
578
website/docs/best-practice/alconna/command.md
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 2
|
||||||
|
description: Alconna 基本介绍
|
||||||
|
---
|
||||||
|
|
||||||
|
# Alconna 命令解析
|
||||||
|
|
||||||
|
[Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器,
|
||||||
|
是一个简单、灵活、高效的命令参数解析器,并且不局限于解析命令式字符串。
|
||||||
|
|
||||||
|
特点包括:
|
||||||
|
|
||||||
|
- 高效
|
||||||
|
- 直观的命令组件创建方式
|
||||||
|
- 强大的类型解析与类型转换功能
|
||||||
|
- 自定义的帮助信息格式
|
||||||
|
- 多语言支持
|
||||||
|
- 易用的快捷命令创建与使用
|
||||||
|
- 可创建命令补全会话,以实现多轮连续的补全提示
|
||||||
|
- 可嵌套的多级子命令
|
||||||
|
- 正则匹配支持
|
||||||
|
|
||||||
|
## 命令示范
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from arclet.alconna import Alconna, Args, Field, Option, CommandMeta, MultiVar, Arparma
|
||||||
|
from nepattern import AnyString
|
||||||
|
|
||||||
|
alc = Alconna(
|
||||||
|
"exec",
|
||||||
|
Args["code", MultiVar(AnyString), Field(completion=lambda: "print(1+1)")] / "\n",
|
||||||
|
Option("纯文本"),
|
||||||
|
Option("无输出"),
|
||||||
|
Option("目标", Args["name", str, "res"]),
|
||||||
|
meta=CommandMeta("exec python code", example="exec\\nprint(1+1)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
alc.shortcut(
|
||||||
|
"echo",
|
||||||
|
{"command": "exec 纯文本\nprint(\\'{*}\\')"},
|
||||||
|
)
|
||||||
|
|
||||||
|
alc.shortcut(
|
||||||
|
"sin(\d+)",
|
||||||
|
{"command": "exec 纯文本\nimport math\nprint(math.sin({0}*math.pi/180))"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def exec_code(result: Arparma):
|
||||||
|
if result.find("纯文本"):
|
||||||
|
codes = list(result.code)
|
||||||
|
else:
|
||||||
|
codes = str(result.origin).split("\n")[1:]
|
||||||
|
output = result.query[str]("目标.name", "res")
|
||||||
|
if not codes:
|
||||||
|
return ""
|
||||||
|
lcs = {}
|
||||||
|
_stdout = StringIO()
|
||||||
|
_to = sys.stdout
|
||||||
|
sys.stdout = _stdout
|
||||||
|
try:
|
||||||
|
exec(
|
||||||
|
"def rc(__out: str):\n "
|
||||||
|
+ " ".join(_code + "\n" for _code in codes)
|
||||||
|
+ " return locals().get(__out)",
|
||||||
|
{**globals(), **locals()},
|
||||||
|
lcs,
|
||||||
|
)
|
||||||
|
code_res = lcs["rc"](output)
|
||||||
|
sys.stdout = _to
|
||||||
|
if result.find("无输出"):
|
||||||
|
return ""
|
||||||
|
if code_res is not None:
|
||||||
|
return f"{output}: {code_res}"
|
||||||
|
_out = _stdout.getvalue()
|
||||||
|
return f"输出: {_out}"
|
||||||
|
except Exception as e:
|
||||||
|
sys.stdout = _to
|
||||||
|
return str(e)
|
||||||
|
finally:
|
||||||
|
sys.stdout = _to
|
||||||
|
|
||||||
|
print(exec_code(alc.parse("echo 1234")))
|
||||||
|
print(exec_code(alc.parse("sin30")))
|
||||||
|
print(
|
||||||
|
exec_code(
|
||||||
|
alc.parse(
|
||||||
|
"""\
|
||||||
|
exec
|
||||||
|
print(
|
||||||
|
exec_code(
|
||||||
|
alc.parse(
|
||||||
|
"exec\\n"
|
||||||
|
"import sys;print(sys.version)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 命令编写
|
||||||
|
|
||||||
|
### 命令头
|
||||||
|
|
||||||
|
命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合,例如 `!help` 中的 `!` 与 `help`。
|
||||||
|
|
||||||
|
在 Alconna 中,你可以传入多种类型的命令头,例如:
|
||||||
|
|
||||||
|
| 前缀 | 命令名 | 匹配内容 | 说明 |
|
||||||
|
| :--------------------------: | :--------: | :---------------------------------------------------------: | :--------------: |
|
||||||
|
| - | "foo" | `"foo"` | 无前缀的纯文字头 |
|
||||||
|
| - | 123 | `123` | 无前缀的元素头 |
|
||||||
|
| - | "re:\d{2}" | `"32"` | 无前缀的正则头 |
|
||||||
|
| - | int | `123` 或 `"456"` | 无前缀的类型头 |
|
||||||
|
| [int, bool] | - | `True` 或 `123` | 无名的元素类头 |
|
||||||
|
| ["foo", "bar"] | - | `"foo"` 或 `"bar"` | 无名的纯文字头 |
|
||||||
|
| ["foo", "bar"] | "baz" | `"foobaz"` 或 `"barbaz"` | 纯文字头 |
|
||||||
|
| [int, bool] | "foo" | `[123, "foo"]` 或 `[False, "foo"]` | 类型头 |
|
||||||
|
| [123, 4567] | "foo" | `[123, "foo"]` 或 `[4567, "foo"]` | 元素头 |
|
||||||
|
| [nepattern.NUMBER] | "bar" | `[123, "bar"]` 或 `[123.456, "bar"]` | 表达式头 |
|
||||||
|
| [123, "foo"] | "bar" | `[123, "bar"]` 或 `"foobar"` 或 `["foo", "bar"]` | 混合头 |
|
||||||
|
| [(int, "foo"), (456, "bar")] | "baz" | `[123, "foobaz"]` 或 `[456, "foobaz"]` 或 `[456, "barbaz"]` | 对头 |
|
||||||
|
|
||||||
|
其中
|
||||||
|
|
||||||
|
- 元素头:只会匹配对应的值,例如 `[123, 456]` 只会匹配 `123` 或 `456`,不会匹配 `789`。
|
||||||
|
- 纯文字头:只会匹配对应的字符串,例如 `["foo", "bar"]` 只会匹配 `"foo"` 或 `"bar"`,不会匹配 `"baz"`。
|
||||||
|
- 正则头:`re:xxx` 会将 `xxx` 转为正则表达式,然后匹配对应的字符串,例如 `re:\d{2}` 只会匹配 `"12"` 或 `"34"`,不会匹配 `"foo"`。
|
||||||
|
**正则只在命令名上生效,命令前缀中的正则会被转义**。
|
||||||
|
- 类型头:只会匹配对应的类型,例如 `[int, bool]` 只会匹配 `123` 或 `True`,不会匹配 `"foo"`。
|
||||||
|
- 无前缀的类型头:此时会将传入的值尝试转为 BasePattern,例如 `int` 会转为 `nepattern.INTEGER`。此时命令头会匹配对应的类型,
|
||||||
|
例如 `int` 会匹配 `123` 或 `"456"`,但不会匹配 `"foo"`。同时,Alconna 会将命令头匹配到的值转为对应的类型,例如 `int` 会将 `"123"` 转为 `123`。
|
||||||
|
- 表达式头:只会匹配对应的表达式,例如 `[nepattern.NUMBER]` 只会匹配 `123` 或 `123.456`,不会匹配 `"foo"`。
|
||||||
|
- 混合头:
|
||||||
|
|
||||||
|
除了通过传入 `re:xxx` 来使用正则表达式外,Alconna 还提供了一种更加简洁的方式来使用正则表达式,那就是 Bracket Header。
|
||||||
|
|
||||||
|
```python
|
||||||
|
from alconna import Alconna
|
||||||
|
|
||||||
|
alc = Alconna(".rd{roll:int}")
|
||||||
|
assert alc.parse(".rd123").header["roll"] == 123
|
||||||
|
```
|
||||||
|
|
||||||
|
Bracket Header 类似 python 里的 f-string 写法,通过 "{}" 声明匹配类型
|
||||||
|
|
||||||
|
"{}" 中的内容为 "name:type or pat":
|
||||||
|
|
||||||
|
- "{}", "{:}": 占位符,等价于 "(.+)"
|
||||||
|
- "{foo}": 等价于 "(?P<foo>.+)"
|
||||||
|
- "{:\d+}": 等价于 "(\d+)"
|
||||||
|
- "{foo:int}": 等价于 "(?P<foo>\d+)",其中 "int" 部分若能转为 `BasePattern` 则读取里面的表达式
|
||||||
|
|
||||||
|
### 组件
|
||||||
|
|
||||||
|
我们可以看到主要的两大组件:`Option` 与 `Subcommand`。
|
||||||
|
|
||||||
|
`Option` 可以传入一组 `alias`,如 `Option("--foo|-F|FOO|f")` 或 `Option("--foo", alias=["-F"])`
|
||||||
|
|
||||||
|
传入别名后,Option 会选择其中长度最长的作为选项名称。若传入为 "--foo|-f",则命令名称为 "--foo"。
|
||||||
|
|
||||||
|
:::tip 特别提醒!!!
|
||||||
|
|
||||||
|
在 Alconna 中 Option 的名字或别名**没有要求**必须在前面写上 `-`
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
`Subcommand` 则可以传入自己的 **Option** 与 **Subcommand**。
|
||||||
|
|
||||||
|
```python
|
||||||
|
from arclet.alconna import Alconna, Option, Subcommand
|
||||||
|
|
||||||
|
alc = Alconna(
|
||||||
|
"command_name",
|
||||||
|
Option("opt1"),
|
||||||
|
Option("--opt2"),
|
||||||
|
Subcommand(
|
||||||
|
"sub1",
|
||||||
|
Option("sub1_opt1"),
|
||||||
|
Option("SO2"),
|
||||||
|
Subcommand(
|
||||||
|
"sub1_sub1"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Subcommand(
|
||||||
|
"sub2"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
他们拥有如下共同参数:
|
||||||
|
|
||||||
|
- `help_text`: 传入该组件的帮助信息
|
||||||
|
- `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name)
|
||||||
|
- `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换
|
||||||
|
对于命令 `test foo bar baz qux <a:int>` 来讲,因为`foo bar baz` 仅需要判断是否相等,所以可以这么编写:
|
||||||
|
|
||||||
|
```python
|
||||||
|
Alconna("test", Option("qux", Args["a", int], requires=["foo", "bar", "baz"]))
|
||||||
|
```
|
||||||
|
|
||||||
|
- `default`: 默认值,在该组件未被解析时使用使用该值替换。
|
||||||
|
|
||||||
|
特别的,使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from arclet.alconna import Option, OptionResult
|
||||||
|
|
||||||
|
opt1 = Option("--foo", default=False)
|
||||||
|
opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 选项操作
|
||||||
|
|
||||||
|
`Option` 可以特别设置传入一类 `Action`,作为解析操作
|
||||||
|
|
||||||
|
`Action` 分为三类:
|
||||||
|
|
||||||
|
- `store`: 无 Args 时, 仅存储一个值, 默认为 Ellipsis; 有 Args 时, 后续的解析结果会覆盖之前的值
|
||||||
|
- `append`: 无 Args 时, 将多个值存为列表, 默认为 Ellipsis; 有 Args 时, 每个解析结果会追加到列表中
|
||||||
|
|
||||||
|
当存在默认值并且不为列表时, 会自动将默认值变成列表, 以保证追加的正确性
|
||||||
|
|
||||||
|
- `count`: 无 Args 时, 计数器加一; 有 Args 时, 表现与 STORE 相同
|
||||||
|
|
||||||
|
当存在默认值并且不为数字时, 会自动将默认值变成 1, 以保证计数器的正确性。
|
||||||
|
|
||||||
|
`Alconna` 提供了预制的几类 `action`:
|
||||||
|
|
||||||
|
- `store`,`store_value`,`store_true`,`store_false`
|
||||||
|
- `append`,`append_value`
|
||||||
|
- `count`
|
||||||
|
|
||||||
|
### 参数声明
|
||||||
|
|
||||||
|
`Args` 是用于声明命令参数的组件。
|
||||||
|
|
||||||
|
`Args` 是参数解析的基础组件,构造方法形如 `Args["foo", str]["bar", int]["baz", bool, False]`,
|
||||||
|
与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。
|
||||||
|
|
||||||
|
`Args` 中的 `name` 是用以标记解析出来的参数并存放于 **Arparma** 中,以方便用户调用。
|
||||||
|
|
||||||
|
其有三种为 Args 注解的标识符: `?`、`/` 与 `!`。标识符与 key 之间建议以 `;` 分隔:
|
||||||
|
|
||||||
|
- `!` 标识符表示该处传入的参数应不是规定的类型,或不在指定的值中。
|
||||||
|
- `?` 标识符表示该参数为可选参数,会在无参数匹配时跳过。
|
||||||
|
- `/` 标识符表示该参数的类型注解需要隐藏。
|
||||||
|
|
||||||
|
另外,对于参数的注释也可以标记在 `name` 中,其与 name 或者标识符 以 `#` 分割:
|
||||||
|
|
||||||
|
`foo#这是注释;?` 或 `foo?#这是注释`
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
`Args` 中的 `name` 在实际命令中并不需要传入(keyword 参数除外):
|
||||||
|
|
||||||
|
```python
|
||||||
|
from arclet.alconna import Alconna, Args
|
||||||
|
|
||||||
|
alc = Alconna("test", Args["foo", str])
|
||||||
|
alc.parse("test --foo abc") # 错误
|
||||||
|
alc.parse("test abc") # 正确
|
||||||
|
```
|
||||||
|
|
||||||
|
若需要 `test --foo abc`,你应该使用 `Option`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from arclet.alconna import Alconna, Args, Option
|
||||||
|
|
||||||
|
alc = Alconna("test", Option("--foo", Args["foo", str]))
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
`Args` 的参数类型表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例。
|
||||||
|
|
||||||
|
```python
|
||||||
|
from arclet.alconna import Args
|
||||||
|
from nepattern import BasePattern
|
||||||
|
|
||||||
|
# 表示 foo 参数需要匹配一个 @number 样式的字符串
|
||||||
|
args = Args["foo", BasePattern("@\d+")]
|
||||||
|
```
|
||||||
|
|
||||||
|
示例中传入的 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]`。
|
||||||
|
|
||||||
|
默认支持的类型有:
|
||||||
|
|
||||||
|
- `str`: 匹配任意字符串
|
||||||
|
- `int`: 匹配整数
|
||||||
|
- `float`: 匹配浮点数
|
||||||
|
- `bool`: 匹配 `True` 与 `False` 以及他们小写形式
|
||||||
|
- `hex`: 匹配 `0x` 开头的十六进制字符串
|
||||||
|
- `url`: 匹配网址
|
||||||
|
- `email`: 匹配 `xxxx@xxx` 的字符串
|
||||||
|
- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串
|
||||||
|
- `list`: 匹配类似 `["foo","bar","baz"]` 的字符串
|
||||||
|
- `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串
|
||||||
|
- `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳
|
||||||
|
- `Any`: 匹配任意类型
|
||||||
|
- `AnyString`: 匹配任意类型,转为 `str`
|
||||||
|
- `Number`: 匹配 `int` 与 `float`,转为 `int`
|
||||||
|
|
||||||
|
同时可以使用 typing 中的类型:
|
||||||
|
|
||||||
|
- `Literal[X]`: 匹配其中的任意一个值
|
||||||
|
- `Union[X, Y]`: 匹配其中的任意一个类型
|
||||||
|
- `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值
|
||||||
|
- `List[X]`: 匹配一个列表,其中的元素为 `X` 类型
|
||||||
|
- `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型,value 为 `Y` 类型
|
||||||
|
- ...
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
几类特殊的传入标记:
|
||||||
|
|
||||||
|
- `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联)
|
||||||
|
- `RawStr("foo")`: 匹配字符串 "foo" (不会被 `BasePattern` 替换)
|
||||||
|
- `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz"
|
||||||
|
- `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型
|
||||||
|
- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值
|
||||||
|
- `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0]
|
||||||
|
- `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象
|
||||||
|
- `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值)
|
||||||
|
- ...
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
`MultiVar` 则是一个特殊的标注,用于告知解析器该参数可以接受多个值,其构造方法形如 `MultiVar(str)`。
|
||||||
|
同样的还有 `KeyWordVar`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
`MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,其构造方法形如 `MultiVar(KeyWordVar(str))`
|
||||||
|
|
||||||
|
`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值。
|
||||||
|
|
||||||
|
`MultiVar` 不能在 `KeyWordVar` 之后传入。
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 紧凑命令
|
||||||
|
|
||||||
|
`Alconna`,`Option` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from arclet.alconna import Alconna, Option, CommandMeta, Args
|
||||||
|
|
||||||
|
alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True))
|
||||||
|
|
||||||
|
assert alc.parse("test123 BARabc").matched
|
||||||
|
```
|
||||||
|
|
||||||
|
这使得我们可以实现如下命令:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> from arclet.alconna import Alconna, Option, Args, append
|
||||||
|
>>> alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True))
|
||||||
|
>>> alc.parse("gcc -Fabc -Fdef -Fxyz").query[list[str]]("flag.content")
|
||||||
|
['abc', 'def', 'xyz']
|
||||||
|
```
|
||||||
|
|
||||||
|
当 `Option` 的 `action` 为 `count` 时,其自动支持 `compact` 特性:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> from arclet.alconna import Alconna, Option, Args, count
|
||||||
|
>>> alc = Alconna("pp", Option("--verbose|-v", action=count, default=0))
|
||||||
|
>>> alc.parse("pp -vvv").query[int]("verbose.value")
|
||||||
|
3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 命令特性
|
||||||
|
|
||||||
|
### 配置
|
||||||
|
|
||||||
|
`arclet.alconna.Namespace` 表示某一命名空间下的默认配置:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from arclet.alconna import config, namespace, Namespace
|
||||||
|
from arclet.alconna.tools import ShellTextFormatter
|
||||||
|
|
||||||
|
|
||||||
|
np = Namespace("foo", prefixes=["/"]) # 创建 Namespace 对象,并进行初始配置
|
||||||
|
|
||||||
|
with namespace("bar") as np1:
|
||||||
|
np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令
|
||||||
|
np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter
|
||||||
|
np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称
|
||||||
|
|
||||||
|
config.namespaces["foo"] = np # 将命名空间挂载到 config 上
|
||||||
|
```
|
||||||
|
|
||||||
|
同时也提供了默认命名空间配置与修改方法:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from arclet.alconna import config, namespace, Namespace
|
||||||
|
|
||||||
|
|
||||||
|
config.default_namespace.prefixes = [...] # 直接修改默认配置
|
||||||
|
|
||||||
|
np = Namespace("xxx", prefixes=[...])
|
||||||
|
config.default_namespace = np # 更换默认的命名空间
|
||||||
|
|
||||||
|
with namespace(config.default_namespace.name) as np:
|
||||||
|
np.prefixes = [...]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 半自动补全
|
||||||
|
|
||||||
|
半自动补全为用户提供了推荐后续输入的功能。
|
||||||
|
|
||||||
|
补全默认通过 `--comp` 或 `-cp` 或 `?` 触发:(命名空间配置可修改名称)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from arclet.alconna import Alconna, Args, Option
|
||||||
|
|
||||||
|
alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar")
|
||||||
|
alc.parse("test ?")
|
||||||
|
|
||||||
|
'''
|
||||||
|
output
|
||||||
|
|
||||||
|
以下是建议的输入:
|
||||||
|
* <abc: int>
|
||||||
|
* --help
|
||||||
|
* -h
|
||||||
|
* -sct
|
||||||
|
* --shortcut
|
||||||
|
* foo
|
||||||
|
* bar
|
||||||
|
'''
|
||||||
|
```
|
||||||
|
|
||||||
|
### 快捷指令
|
||||||
|
|
||||||
|
快捷指令顾名思义,可以为基础指令创建便捷的触发方式
|
||||||
|
|
||||||
|
一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建,删除);
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> from arclet.alconna import Alconna, Args
|
||||||
|
>>> alc = Alconna("setu", Args["count", int])
|
||||||
|
>>> alc.shortcut("涩图(\d+)张", {"args": ["{0}"]})
|
||||||
|
'Alconna::setu 的快捷指令: "涩图(\\d+)张" 添加成功'
|
||||||
|
>>> alc.parse("涩图3张").query("count")
|
||||||
|
3
|
||||||
|
```
|
||||||
|
|
||||||
|
`shortcut` 的第一个参数为快捷指令名称,第二个参数为 `ShortcutArgs`,作为快捷指令的配置
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ShortcutArgs(TypedDict):
|
||||||
|
"""快捷指令参数"""
|
||||||
|
|
||||||
|
command: NotRequired[DataCollection[Any]]
|
||||||
|
"""快捷指令的命令"""
|
||||||
|
args: NotRequired[list[Any]]
|
||||||
|
"""快捷指令的附带参数"""
|
||||||
|
fuzzy: NotRequired[bool]
|
||||||
|
"""是否允许命令后随参数"""
|
||||||
|
prefix: NotRequired[bool]
|
||||||
|
"""是否调用时保留指令前缀"""
|
||||||
|
```
|
||||||
|
|
||||||
|
当 `fuzzy` 为 False 时,传入 `"涩图1张 abc"` 之类的快捷指令将视为解析失败
|
||||||
|
|
||||||
|
快捷指令允许三类特殊的 placeholder:
|
||||||
|
|
||||||
|
- `{%X}`: 如 `setu {%0}`,表示此处必须填入快捷指令后随的第 X 个参数。
|
||||||
|
|
||||||
|
例如,若快捷指令为 `涩图`,配置为 `{"command": "setu {%0}"}`,则指令 `涩图 1` 相当于 `setu 1`
|
||||||
|
|
||||||
|
- `{*}`: 表示此处填入所有后随参数,并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。
|
||||||
|
- `{X}`: 表示此处填入可能的正则匹配的组:
|
||||||
|
- 若 `command` 中存在匹配组 `(xxx)`,则 `{X}` 表示第 X 个匹配组的内容
|
||||||
|
- 若 `command` 中存储匹配组 `(?P<xxx>...)`,则 `{X}` 表示名字为 X 的匹配结果
|
||||||
|
|
||||||
|
除此之外,通过内置选项 `--shortcut` 可以动态操作快捷指令。
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- `cmd --shortcut <key> <cmd>` 来增加一个快捷指令
|
||||||
|
- `cmd --shortcut list` 来列出当前指令的所有快捷指令
|
||||||
|
- `cmd --shortcut delete key` 来删除一个快捷指令
|
||||||
|
|
||||||
|
### 使用模糊匹配
|
||||||
|
|
||||||
|
模糊匹配通过在 Alconna 中设置其 CommandMeta 开启。
|
||||||
|
|
||||||
|
模糊匹配会应用在任意需要进行名称判断的地方,如**命令名称**,**选项名称**和**参数名称**(如指定需要传入参数名称)。
|
||||||
|
|
||||||
|
```python
|
||||||
|
from arclet.alconna import Alconna, CommandMeta
|
||||||
|
|
||||||
|
alc = Alconna("test_fuzzy", meta=CommandMeta(fuzzy_match=True))
|
||||||
|
alc.parse("test_fuzy")
|
||||||
|
# output: test_fuzy is not matched. Do you mean "test_fuzzy"?
|
||||||
|
```
|
||||||
|
|
||||||
|
## 解析结果
|
||||||
|
|
||||||
|
`Alconna.parse` 会返回由 **Arparma** 承载的解析结果。
|
||||||
|
|
||||||
|
`Arpamar` 会有如下参数:
|
||||||
|
|
||||||
|
- 调试类
|
||||||
|
|
||||||
|
- matched: 是否匹配成功
|
||||||
|
- error_data: 解析失败时剩余的数据
|
||||||
|
- error_info: 解析失败时的异常内容
|
||||||
|
- origin: 原始命令,可以类型标注
|
||||||
|
|
||||||
|
- 分析类
|
||||||
|
- header_match: 命令头部的解析结果,包括原始头部、解析后头部、解析结果与可能的正则匹配组
|
||||||
|
- main_args: 命令的主参数的解析结果
|
||||||
|
- options: 命令所有选项的解析结果
|
||||||
|
- subcommands: 命令所有子命令的解析结果
|
||||||
|
- other_args: 除主参数外的其他解析结果
|
||||||
|
- all_matched_args: 所有 Args 的解析结果
|
||||||
|
|
||||||
|
`Arparma` 同时提供了便捷的查询方法 `query[type]()`,会根据传入的 `path` 查找参数并返回
|
||||||
|
|
||||||
|
`path` 支持如下:
|
||||||
|
|
||||||
|
- `main_args`,`options`,...: 返回对应的属性
|
||||||
|
- `args`: 返回 all_matched_args
|
||||||
|
- `main_args.xxx`,`options.xxx`,...: 返回字典中 `xxx`键对应的值
|
||||||
|
- `args.xxx`: 返回 all_matched_args 中 `xxx`键对应的值
|
||||||
|
- `options.foo`,`foo`: 返回选项 `foo` 的解析结果 (OptionResult)
|
||||||
|
- `options.foo.value`,`foo.value`: 返回选项 `foo` 的解析值
|
||||||
|
- `options.foo.args`,`foo.args`: 返回选项 `foo` 的解析参数字典
|
||||||
|
- `options.foo.args.bar`,`foo.bar`: 返回选项 `foo` 的参数字典中 `bar` 键对应的值
|
||||||
|
...
|
||||||
|
|
||||||
|
同样,`Arparma["foo.bar"]` 的表现与 `query()` 一致
|
||||||
|
|
||||||
|
## Duplication
|
||||||
|
|
||||||
|
**Duplication** 用来提供更好的自动补全,类似于 **ArgParse** 的 **Namespace**,经测试表现良好(好耶)。
|
||||||
|
|
||||||
|
普通情况下使用,需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分,
|
||||||
|
|
||||||
|
以 pip 为例,其对应的 Duplication 应如下构造:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from arclet.alconna import OptionResult, Duplication, SubcommandStub
|
||||||
|
|
||||||
|
class MyDup(Duplication):
|
||||||
|
verbose: OptionResult
|
||||||
|
install: SubcommandStub # 选项与子命令对应的stub的变量名必须与其名字相同
|
||||||
|
```
|
||||||
|
|
||||||
|
并在解析时传入 Duplication:
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = alc.parse("pip -v install ...", duplication=MyDup)
|
||||||
|
>>> type(result)
|
||||||
|
<class MyDup>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Optional
|
||||||
|
from arclet.alconna import Duplication
|
||||||
|
|
||||||
|
|
||||||
|
class MyDup(Duplication):
|
||||||
|
package: str
|
||||||
|
file: Optional[str] = None
|
||||||
|
url: Optional[str] = None
|
||||||
|
```
|
48
website/docs/best-practice/alconna/config.md
Normal file
48
website/docs/best-practice/alconna/config.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 4
|
||||||
|
description: 配置项
|
||||||
|
---
|
||||||
|
|
||||||
|
# 配置项
|
||||||
|
|
||||||
|
## alconna_auto_send_output
|
||||||
|
|
||||||
|
- **类型**: `bool`
|
||||||
|
- **默认值**: `False`
|
||||||
|
|
||||||
|
是否全局启用输出信息自动发送,不启用则会在触特殊内置选项后仍然将解析结果传递至响应器。
|
||||||
|
|
||||||
|
## alconna_use_command_start
|
||||||
|
|
||||||
|
- **类型**: `bool`
|
||||||
|
- **默认值**: `False`
|
||||||
|
|
||||||
|
是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀
|
||||||
|
|
||||||
|
## alconna_auto_completion
|
||||||
|
|
||||||
|
- **类型**: `bool`
|
||||||
|
- **默认值**: `False`
|
||||||
|
|
||||||
|
是否全局启用命令自动补全,启用后会在参数缺失或触发 `--comp` 选项时自自动启用交互式补全。
|
||||||
|
|
||||||
|
## alconna_use_origin
|
||||||
|
|
||||||
|
- **类型**: `bool`
|
||||||
|
- **默认值**: `False`
|
||||||
|
|
||||||
|
是否全局使用原始消息 (即未经过 to_me 等处理的),该选项会影响到 Alconna 的匹配行为。
|
||||||
|
|
||||||
|
## alconna_use_command_sep
|
||||||
|
|
||||||
|
- **类型**: `bool`
|
||||||
|
- **默认值**: `False`
|
||||||
|
|
||||||
|
是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符
|
||||||
|
|
||||||
|
## alconna_global_extensions
|
||||||
|
|
||||||
|
- **类型**: `List[str]`
|
||||||
|
- **默认值**: `[]`
|
||||||
|
|
||||||
|
全局加载的扩展,路径以 . 分隔,如 `foo.bar.baz:DemoExtension`
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user