mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-10-07 11:16:43 +00:00
Compare commits
246 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "16"
|
||||
node-version: "18"
|
||||
cache: "yarn"
|
||||
|
||||
- id: yarn-cache-dir-path
|
||||
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
|
||||
- run: yarn install --frozen-lockfile
|
||||
shell: bash
|
||||
|
31
.github/dependabot.yml
vendored
31
.github/dependabot.yml
vendored
@@ -4,3 +4,34 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
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:
|
||||
- "*"
|
||||
|
2
.github/workflows/codecov.yml
vendored
2
.github/workflows/codecov.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
PYTHON_VERSION: ${{ matrix.python-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python environment
|
||||
uses: ./.github/actions/setup-python
|
||||
|
12
.github/workflows/noneflow.yml
vendored
12
.github/workflows/noneflow.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Test Plugin
|
||||
id: plugin-test
|
||||
@@ -62,13 +62,13 @@ jobs:
|
||||
steps:
|
||||
- name: Generate token
|
||||
id: generate-token
|
||||
uses: tibdex/github-app-token@v1
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_KEY }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
@@ -84,9 +84,9 @@ jobs:
|
||||
config: >
|
||||
{
|
||||
"base": "master",
|
||||
"plugin_path": "website/static/plugins.json",
|
||||
"bot_path": "website/static/bots.json",
|
||||
"adapter_path": "website/static/adapters.json"
|
||||
"plugin_path": "assets/plugins.json",
|
||||
"bot_path": "assets/bots.json",
|
||||
"adapter_path": "assets/adapters.json"
|
||||
}
|
||||
env:
|
||||
PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }}
|
||||
|
2
.github/workflows/pyright.yml
vendored
2
.github/workflows/pyright.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
name: Pyright Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python environment
|
||||
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:
|
||||
- name: Generate token
|
||||
id: generate-token
|
||||
uses: tibdex/github-app-token@v1
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
id: release-drafter
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Update Changelog
|
||||
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
||||
@@ -59,8 +59,18 @@ jobs:
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
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
|
||||
uses: ./.github/actions/setup-python
|
||||
@@ -71,33 +81,53 @@ jobs:
|
||||
- name: Build API Doc
|
||||
uses: ./.github/actions/build-api-doc
|
||||
|
||||
- run: |
|
||||
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
- name: Get Version
|
||||
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
|
||||
with:
|
||||
name: Release ${{ env.TAG_NAME }} 🌈
|
||||
tag: ${{ env.TAG_NAME }}
|
||||
name: Release ${{ steps.version.outputs.TAG_NAME }} 🌈
|
||||
tag: ${{ steps.version.outputs.TAG_NAME }}
|
||||
publish: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Build and Publish Package
|
||||
- name: Build Package
|
||||
run: |
|
||||
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:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Build and Publish Doc Package
|
||||
run: |
|
||||
yarn build:plugin --out-dir ../packages/nonebot-plugin-docs/nonebot_plugin_docs/dist
|
||||
export NONEBOT_VERSION=`poetry version -s`
|
||||
cd packages/nonebot-plugin-docs/
|
||||
poetry version $NONEBOT_VERSION
|
||||
poetry version ${{ steps.version.outputs.VERSION }}
|
||||
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:
|
||||
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:
|
||||
- name: Generate token
|
||||
id: generate-token
|
||||
uses: tibdex/github-app-token@v1
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run Ruff Lint
|
||||
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
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python Environment
|
||||
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
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python Environment
|
||||
uses: ./.github/actions/setup-python
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -139,7 +139,7 @@ fabric.properties
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
# Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
@@ -7,7 +7,7 @@ ci:
|
||||
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.276
|
||||
rev: v0.0.292
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
@@ -20,13 +20,13 @@ repos:
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
rev: 23.9.1
|
||||
hooks:
|
||||
- id: black
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v3.0.0-alpha.9-for-vscode
|
||||
rev: v3.0.3
|
||||
hooks:
|
||||
- id: prettier
|
||||
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
|
||||
|
30
README.md
30
README.md
@@ -54,6 +54,9 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
||||
<a href="https://onebot.dev/">
|
||||
<img src="https://img.shields.io/badge/OneBot-v12-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
|
||||
</a>
|
||||
<a href="https://bot.q.qq.com/wiki/">
|
||||
<img src="https://img.shields.io/badge/QQ-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ">
|
||||
</a>
|
||||
<a href="https://core.telegram.org/bots/api">
|
||||
<img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram" alt="telegram">
|
||||
</a>
|
||||
@@ -63,9 +66,6 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
||||
<a href="https://docs.github.com/en/developers/apps">
|
||||
<img src="https://img.shields.io/badge/GitHub-Bot-181717?style=social&logo=github" alt="github"/>
|
||||
</a>
|
||||
<a href="https://bot.q.qq.com/wiki/">
|
||||
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ频道">
|
||||
</a>
|
||||
<!-- <a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p">
|
||||
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="dingtalk"> -->
|
||||
</a>
|
||||
@@ -111,21 +111,24 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
|
||||
- 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
|
||||
|
||||
| 协议名称 | 状态 | 注释 |
|
||||
| :-------------------------------------------------------------------------------------------------------------------: | :--: | :-----------------------------------------------------------------------: |
|
||||
| :--------------------------------------------------------------------------------------------------------------------------: | :--: | :-----------------------------------------------------------------------: |
|
||||
| OneBot([仓库](https://github.com/nonebot/adapter-onebot),[协议](https://onebot.dev/)) | ✅ | 支持 QQ、TG、微信公众号、KOOK 等[平台](https://onebot.dev/ecosystem.html) |
|
||||
| Telegram([仓库](https://github.com/nonebot/adapter-telegram),[协议](https://core.telegram.org/bots/api)) | ✅ | |
|
||||
| 飞书([仓库](https://github.com/nonebot/adapter-feishu),[协议](https://open.feishu.cn/document/home/index)) | ✅ | |
|
||||
| GitHub([仓库](https://github.com/nonebot/adapter-github),[协议](https://docs.github.com/en/apps)) | ✅ | GitHub APP & OAuth APP |
|
||||
| QQ 频道([仓库](https://github.com/nonebot/adapter-qqguild),[协议](https://bot.q.qq.com/wiki/)) | ✅ | 官方接口调整较多 |
|
||||
| QQ([仓库](https://github.com/nonebot/adapter-qq),[协议](https://bot.q.qq.com/wiki/)) | ✅ | QQ 官方接口调整较多 |
|
||||
| 钉钉([仓库](https://github.com/nonebot/adapter-ding),[协议](https://open.dingtalk.com/document/)) | 🤗 | 寻找 Maintainer(暂不可用) |
|
||||
| Console([仓库](https://github.com/nonebot/adapter-console)) | ✅ | 控制台交互 |
|
||||
| Red ([仓库](https://github.com/nonebot/adapter-red),[协议](https://chrononeko.github.io/QQNTRedProtocol/)) | ✅ | QQ 协议 |
|
||||
| Satori([仓库](https://github.com/nonebot/adapter-satori),[协议](https://satori.js.org/zh-CN)) | ✅ | 支持 Onebot、TG、飞书、微信公众号、Koishi 等 |
|
||||
| Discord ([仓库](https://github.com/nonebot/adapter-discord),[协议](https://discord.com/developers/docs/intro)) | ✅ | Discord Bot 协议 |
|
||||
| 开黑啦([仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila),[协议](https://developer.kookapp.cn/)) | ↗️ | 由社区贡献 |
|
||||
| Mirai([仓库](https://github.com/ieew/nonebot_adapter_mirai2),[协议](https://docs.mirai.mamoe.net/mirai-api-http/)) | ↗️ | QQ 协议,由社区贡献 |
|
||||
| Ntchat([仓库](https://github.com/JustUndertaker/adapter-ntchat)) | ↗️ | 微信协议,由社区贡献 |
|
||||
| MineCraft([仓库](https://github.com/17TheWord/nonebot-adapter-minecraft)) | ↗️ | 由社区贡献 |
|
||||
| BiliBili Live([仓库](https://github.com/wwweww/adapter-bilibili)) | ↗️ | 由社区贡献 |
|
||||
| Walle-Q([仓库](https://github.com/onebot-walle/nonebot_adapter_walleq)) | ↗️ | QQ 协议,由社区贡献 |
|
||||
| Villa([仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa)) | ↗️ | 米游社大别野 Bot 协议,由社区贡献 |
|
||||
| Villa([仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa),[协议](https://webstatic.mihoyo.com/vila/bot/doc/)) | ↗️ | 米游社大别野 Bot 协议,由社区贡献 |
|
||||
|
||||
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
|
||||
|
||||
@@ -204,9 +207,8 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
|
||||
或者尝试以下镜像:
|
||||
|
||||
- [文档镜像(中国境内)](https://nb2.baka.icu)
|
||||
- [文档镜像(Vercel)](https://nonebot2-vercel-mirror.vercel.app)
|
||||
|
||||
- 其他插件请查看 [商店](https://nonebot.dev/store)
|
||||
- 其他插件请查看 [商店](https://nonebot.dev/store/plugins)
|
||||
|
||||
## 许可证
|
||||
|
||||
@@ -225,7 +227,17 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
请参考 [贡献指南](./CONTRIBUTING.md)
|
||||
|
||||
### 鸣谢
|
||||
## 鸣谢
|
||||
|
||||
### 赞助者
|
||||
|
||||
感谢以下赞助者对 NoneBot 项目提供的资金支持:
|
||||
|
||||
<a href="https://assets.nonebot.dev/sponsors.svg">
|
||||
<img src='https://assets.nonebot.dev/sponsors.svg'/>
|
||||
</a>
|
||||
|
||||
### 开发者
|
||||
|
||||
感谢以下开发者对 NoneBot2 作出的贡献:
|
||||
|
||||
|
@@ -40,12 +40,12 @@
|
||||
"is_official": true
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot.adapters.qqguild",
|
||||
"project_link": "nonebot-adapter-qqguild",
|
||||
"name": "QQ 频道",
|
||||
"desc": "QQ 频道官方机器人",
|
||||
"module_name": "nonebot.adapters.qq",
|
||||
"project_link": "nonebot-adapter-qq",
|
||||
"name": "QQ",
|
||||
"desc": "QQ 官方机器人",
|
||||
"author": "yanyongyu",
|
||||
"homepage": "https://github.com/nonebot/adapter-qqguild",
|
||||
"homepage": "https://github.com/nonebot/adapter-qq",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
},
|
||||
@@ -168,5 +168,40 @@
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
]
|
@@ -541,5 +541,54 @@
|
||||
"homepage": "https://github.com/LambdaYH/MigangBot",
|
||||
"tags": [],
|
||||
"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
|
||||
}
|
||||
]
|
5259
assets/plugins.json
Normal file
5259
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.adapters import Bot, Adapter
|
||||
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:
|
||||
__version__ = version("nonebot2")
|
||||
@@ -149,13 +149,13 @@ def get_adapters() -> Dict[str, Adapter]:
|
||||
|
||||
|
||||
def get_app() -> Any:
|
||||
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应的 Server App 对象。
|
||||
"""获取全局 {ref}`nonebot.drivers.ASGIMixin` 对应的 Server App 对象。
|
||||
|
||||
返回:
|
||||
Server App 对象
|
||||
|
||||
异常:
|
||||
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ReverseDriver` 类型
|
||||
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ASGIMixin` 类型
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
@@ -165,21 +165,19 @@ def get_app() -> Any:
|
||||
```
|
||||
"""
|
||||
driver = get_driver()
|
||||
assert isinstance(
|
||||
driver, ReverseDriver
|
||||
), "app object is only available for reverse driver"
|
||||
assert isinstance(driver, ASGIMixin), "app object is only available for asgi driver"
|
||||
return driver.server_app
|
||||
|
||||
|
||||
def get_asgi() -> Any:
|
||||
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应
|
||||
"""获取全局 {ref}`nonebot.drivers.ASGIMixin` 对应的
|
||||
[ASGI](https://asgi.readthedocs.io/) 对象。
|
||||
|
||||
返回:
|
||||
ASGI 对象
|
||||
|
||||
异常:
|
||||
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ReverseDriver` 类型
|
||||
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ASGIMixin` 类型
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
@@ -190,8 +188,8 @@ def get_asgi() -> Any:
|
||||
"""
|
||||
driver = get_driver()
|
||||
assert isinstance(
|
||||
driver, ReverseDriver
|
||||
), "asgi object is only available for reverse driver"
|
||||
driver, ASGIMixin
|
||||
), "asgi object is only available for asgi driver"
|
||||
return driver.asgi
|
||||
|
||||
|
||||
|
@@ -45,6 +45,10 @@ class Param(abc.ABC, FieldInfo):
|
||||
继承自 `pydantic.fields.FieldInfo`,用于描述参数信息(不包括参数名)。
|
||||
"""
|
||||
|
||||
def __init__(self, *args, validate: bool = False, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.validate = validate
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type["Param"], ...]
|
||||
@@ -97,6 +101,7 @@ class Dependent(Generic[R]):
|
||||
)
|
||||
|
||||
async def __call__(self, **kwargs: Any) -> R:
|
||||
try:
|
||||
# do pre-check
|
||||
await self.check(**kwargs)
|
||||
|
||||
@@ -108,11 +113,14 @@ class Dependent(Generic[R]):
|
||||
return await cast(Callable[..., Awaitable[R]], self.call)(**values)
|
||||
else:
|
||||
return await run_sync(cast(Callable[..., R], self.call))(**values)
|
||||
except SkippedException as e:
|
||||
logger.trace(f"{self} skipped due to {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def parse_params(
|
||||
call: _DependentCallable[R], allow_types: Tuple[Type[Param], ...]
|
||||
) -> Tuple[ModelField]:
|
||||
) -> Tuple[ModelField, ...]:
|
||||
fields: List[ModelField] = []
|
||||
params = get_typed_signature(call).parameters.values()
|
||||
|
||||
@@ -191,25 +199,18 @@ class Dependent(Generic[R]):
|
||||
return cls(call, params, parameterless_params)
|
||||
|
||||
async def check(self, **params: Any) -> None:
|
||||
try:
|
||||
await asyncio.gather(*(param._check(**params) for param in self.parameterless))
|
||||
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:
|
||||
value = await cast(Param, field.field_info)._solve(**params)
|
||||
param = cast(Param, field.field_info)
|
||||
value = await param._solve(**params)
|
||||
if value is Undefined:
|
||||
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]:
|
||||
# solve parameterless
|
||||
|
@@ -5,7 +5,7 @@ FrontMatter:
|
||||
"""
|
||||
|
||||
import inspect
|
||||
from typing import Any, Dict, TypeVar, Callable, ForwardRef
|
||||
from typing import Any, Dict, Callable, ForwardRef
|
||||
|
||||
from loguru import logger
|
||||
from pydantic.fields import ModelField
|
||||
@@ -13,8 +13,6 @@ from pydantic.typing import evaluate_forwardref
|
||||
|
||||
from nonebot.exception import TypeMisMatch
|
||||
|
||||
V = TypeVar("V")
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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_:
|
||||
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 Mixin as Mixin
|
||||
from nonebot.internal.driver import Driver as Driver
|
||||
from nonebot.internal.driver import Cookies as Cookies
|
||||
from nonebot.internal.driver import Request as Request
|
||||
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 HTTPVersion as HTTPVersion
|
||||
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 ReverseDriver as ReverseDriver
|
||||
from nonebot.internal.driver import combine_driver as combine_driver
|
||||
from nonebot.internal.driver import HTTPClientMixin as HTTPClientMixin
|
||||
from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup
|
||||
from nonebot.internal.driver import WebSocketClientMixin as WebSocketClientMixin
|
||||
from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup
|
||||
|
||||
__autodoc__ = {
|
||||
"URL": True,
|
||||
"Driver": True,
|
||||
"Cookies": True,
|
||||
"Request": True,
|
||||
"Response": True,
|
||||
"WebSocket": True,
|
||||
"HTTPVersion": True,
|
||||
"Driver": True,
|
||||
"Mixin": True,
|
||||
"ForwardMixin": True,
|
||||
"ForwardDriver": True,
|
||||
"HTTPClientMixin": True,
|
||||
"WebSocketClientMixin": True,
|
||||
"ReverseMixin": True,
|
||||
"ReverseDriver": True,
|
||||
"ASGIMixin": True,
|
||||
"combine_driver": True,
|
||||
"HTTPServerSetup": True,
|
||||
"WebSocketServerSetup": True,
|
||||
|
@@ -16,14 +16,19 @@ FrontMatter:
|
||||
"""
|
||||
|
||||
from typing_extensions import override
|
||||
from typing import Type, AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import TYPE_CHECKING, AsyncGenerator
|
||||
|
||||
from nonebot.drivers import Request, Response
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
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:
|
||||
import aiohttp
|
||||
@@ -34,7 +39,7 @@ except ModuleNotFoundError as e: # pragma: no cover
|
||||
) from e
|
||||
|
||||
|
||||
class Mixin(ForwardMixin):
|
||||
class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
||||
"""AIOHTTP Mixin"""
|
||||
|
||||
@property
|
||||
@@ -172,5 +177,11 @@ class WebSocket(BaseWebSocket):
|
||||
await self.websocket.send_bytes(data)
|
||||
|
||||
|
||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class Driver(Mixin, NoneDriver):
|
||||
...
|
||||
|
||||
else:
|
||||
Driver = combine_driver(NoneDriver, Mixin)
|
||||
"""AIOHTTP Driver"""
|
||||
|
@@ -25,12 +25,14 @@ from typing import Any, Dict, List, Tuple, Union, Optional
|
||||
from pydantic import BaseSettings
|
||||
|
||||
from nonebot.config import Env
|
||||
from nonebot.drivers import ASGIMixin
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.internal.driver import FileTypes
|
||||
from nonebot.drivers import Driver as BaseDriver
|
||||
from nonebot.config import Config as NoneBotConfig
|
||||
from nonebot.drivers import Request as BaseRequest
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
|
||||
from nonebot.drivers import HTTPServerSetup, WebSocketServerSetup
|
||||
|
||||
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
||||
|
||||
@@ -87,7 +89,7 @@ class Config(BaseSettings):
|
||||
extra = "ignore"
|
||||
|
||||
|
||||
class Driver(ReverseDriver):
|
||||
class Driver(BaseDriver, ASGIMixin):
|
||||
"""FastAPI 驱动框架。"""
|
||||
|
||||
def __init__(self, env: Env, config: NoneBotConfig):
|
||||
@@ -179,7 +181,7 @@ class Driver(ReverseDriver):
|
||||
**kwargs,
|
||||
):
|
||||
"""使用 `uvicorn` 启动 FastAPI"""
|
||||
super().run(host, port, app, **kwargs)
|
||||
super().run(host, port, app=app, **kwargs)
|
||||
LOGGING_CONFIG = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
|
@@ -15,18 +15,15 @@ FrontMatter:
|
||||
description: nonebot.drivers.httpx 模块
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
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 import (
|
||||
Request,
|
||||
Response,
|
||||
WebSocket,
|
||||
HTTPVersion,
|
||||
ForwardMixin,
|
||||
ForwardDriver,
|
||||
HTTPClientMixin,
|
||||
combine_driver,
|
||||
)
|
||||
|
||||
@@ -39,7 +36,7 @@ except ModuleNotFoundError as e: # pragma: no cover
|
||||
) from e
|
||||
|
||||
|
||||
class Mixin(ForwardMixin):
|
||||
class Mixin(HTTPClientMixin):
|
||||
"""HTTPX Mixin"""
|
||||
|
||||
@property
|
||||
@@ -72,12 +69,12 @@ class Mixin(ForwardMixin):
|
||||
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):
|
||||
...
|
||||
|
||||
else:
|
||||
Driver = combine_driver(NoneDriver, Mixin)
|
||||
"""HTTPX Driver"""
|
||||
|
@@ -34,12 +34,14 @@ from typing import (
|
||||
from pydantic import BaseSettings
|
||||
|
||||
from nonebot.config import Env
|
||||
from nonebot.drivers import ASGIMixin
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.internal.driver import FileTypes
|
||||
from nonebot.drivers import Driver as BaseDriver
|
||||
from nonebot.config import Config as NoneBotConfig
|
||||
from nonebot.drivers import Request as BaseRequest
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
|
||||
from nonebot.drivers import HTTPServerSetup, WebSocketServerSetup
|
||||
|
||||
try:
|
||||
import uvicorn
|
||||
@@ -89,7 +91,7 @@ class Config(BaseSettings):
|
||||
extra = "ignore"
|
||||
|
||||
|
||||
class Driver(ReverseDriver):
|
||||
class Driver(BaseDriver, ASGIMixin):
|
||||
"""Quart 驱动框架"""
|
||||
|
||||
def __init__(self, env: Env, config: NoneBotConfig):
|
||||
|
@@ -19,14 +19,14 @@ import logging
|
||||
from functools import wraps
|
||||
from contextlib import asynccontextmanager
|
||||
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.drivers import Request, Response
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ForwardMixin, ForwardDriver, combine_driver
|
||||
from nonebot.drivers import WebSocketClientMixin, combine_driver
|
||||
|
||||
try:
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
@@ -58,7 +58,7 @@ def catch_closed(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
||||
return decorator
|
||||
|
||||
|
||||
class Mixin(ForwardMixin):
|
||||
class Mixin(WebSocketClientMixin):
|
||||
"""Websockets Mixin"""
|
||||
|
||||
@property
|
||||
@@ -66,10 +66,6 @@ class Mixin(ForwardMixin):
|
||||
def type(self) -> str:
|
||||
return "websockets"
|
||||
|
||||
@override
|
||||
async def request(self, setup: Request) -> Response:
|
||||
return await super(Mixin, self).request(setup)
|
||||
|
||||
@override
|
||||
@asynccontextmanager
|
||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||
@@ -133,5 +129,11 @@ class WebSocket(BaseWebSocket):
|
||||
await self.websocket.send(data)
|
||||
|
||||
|
||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class Driver(Mixin, NoneDriver):
|
||||
...
|
||||
|
||||
else:
|
||||
Driver = combine_driver(NoneDriver, Mixin)
|
||||
"""Websockets Driver"""
|
||||
|
@@ -7,10 +7,11 @@ from nonebot.internal.driver import (
|
||||
Driver,
|
||||
Request,
|
||||
Response,
|
||||
ASGIMixin,
|
||||
WebSocket,
|
||||
ForwardDriver,
|
||||
ReverseDriver,
|
||||
HTTPClientMixin,
|
||||
HTTPServerSetup,
|
||||
WebSocketClientMixin,
|
||||
WebSocketServerSetup,
|
||||
)
|
||||
|
||||
@@ -72,26 +73,26 @@ class Adapter(abc.ABC):
|
||||
|
||||
def setup_http_server(self, setup: HTTPServerSetup):
|
||||
"""设置一个 HTTP 服务器路由配置"""
|
||||
if not isinstance(self.driver, ReverseDriver):
|
||||
if not isinstance(self.driver, ASGIMixin):
|
||||
raise TypeError("Current driver does not support http server")
|
||||
self.driver.setup_http_server(setup)
|
||||
|
||||
def setup_websocket_server(self, setup: WebSocketServerSetup):
|
||||
"""设置一个 WebSocket 服务器路由配置"""
|
||||
if not isinstance(self.driver, ReverseDriver):
|
||||
if not isinstance(self.driver, ASGIMixin):
|
||||
raise TypeError("Current driver does not support websocket server")
|
||||
self.driver.setup_websocket_server(setup)
|
||||
|
||||
async def request(self, setup: Request) -> Response:
|
||||
"""进行一个 HTTP 客户端请求"""
|
||||
if not isinstance(self.driver, ForwardDriver):
|
||||
if not isinstance(self.driver, HTTPClientMixin):
|
||||
raise TypeError("Current driver does not support http client")
|
||||
return await self.driver.request(setup)
|
||||
|
||||
@asynccontextmanager
|
||||
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
|
||||
"""建立一个 WebSocket 客户端连接请求"""
|
||||
if not isinstance(self.driver, ForwardDriver):
|
||||
if not isinstance(self.driver, WebSocketClientMixin):
|
||||
raise TypeError("Current driver does not support websocket client")
|
||||
async with self.driver.websocket(setup) as ws:
|
||||
yield ws
|
||||
|
@@ -106,7 +106,10 @@ class Bot(abc.ABC):
|
||||
logger.debug("Running CalledAPI hooks...")
|
||||
await asyncio.gather(*coros)
|
||||
except MockApiException as e:
|
||||
# mock api result
|
||||
result = e.result
|
||||
# ignore exception
|
||||
exception = None
|
||||
logger.debug(
|
||||
f"Calling API {api} result is mocked. Return {result} instead."
|
||||
)
|
||||
|
@@ -1,8 +1,9 @@
|
||||
from .model import URL as URL
|
||||
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 Request as Request
|
||||
from .abstract import Driver as Driver
|
||||
from .model import FileType as FileType
|
||||
from .model import Response as Response
|
||||
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 FilesTypes as FilesTypes
|
||||
from .model import QueryTypes as QueryTypes
|
||||
from .abstract import ASGIMixin as ASGIMixin
|
||||
from .model import CookieTypes as CookieTypes
|
||||
from .model import FileContent as FileContent
|
||||
from .model import HTTPVersion as HTTPVersion
|
||||
from .model import HeaderTypes as HeaderTypes
|
||||
from .model import SimpleQuery as SimpleQuery
|
||||
from .model import ContentTypes as ContentTypes
|
||||
from .driver import ForwardMixin as ForwardMixin
|
||||
from .model import QueryVariable as QueryVariable
|
||||
from .driver import ForwardDriver as ForwardDriver
|
||||
from .driver import ReverseDriver as ReverseDriver
|
||||
from .driver import combine_driver as combine_driver
|
||||
from .abstract import ForwardMixin as ForwardMixin
|
||||
from .abstract import ReverseMixin as ReverseMixin
|
||||
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 .abstract import HTTPClientMixin as HTTPClientMixin
|
||||
from .model import WebSocketServerSetup as WebSocketServerSetup
|
||||
from .abstract import WebSocketClientMixin as WebSocketClientMixin
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import abc
|
||||
import asyncio
|
||||
from typing_extensions import TypeAlias
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
|
||||
|
||||
@@ -25,7 +26,9 @@ BOT_HOOK_PARAMS = [DependParam, BotParam, DefaultParam]
|
||||
|
||||
|
||||
class Driver(abc.ABC):
|
||||
"""Driver 基类。
|
||||
"""驱动器基类。
|
||||
|
||||
驱动器控制框架的启动和停止,适配器的注册,以及机器人生命周期管理。
|
||||
|
||||
参数:
|
||||
env: 包含环境信息的 Env 对象
|
||||
@@ -45,6 +48,7 @@ class Driver(abc.ABC):
|
||||
self.config: Config = config
|
||||
"""全局配置对象"""
|
||||
self._bots: Dict[str, "Bot"] = {}
|
||||
self._bot_tasks: Set[asyncio.Task] = set()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
@@ -94,6 +98,8 @@ class Driver(abc.ABC):
|
||||
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
|
||||
)
|
||||
|
||||
self.on_shutdown(self._cleanup)
|
||||
|
||||
@abc.abstractmethod
|
||||
def on_startup(self, func: Callable) -> Callable:
|
||||
"""注册一个在驱动器启动时执行的函数"""
|
||||
@@ -156,7 +162,9 @@ class Driver(abc.ABC):
|
||||
"</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:
|
||||
"""在连接断开后,调用该函数来注销 bot 对象"""
|
||||
@@ -183,23 +191,49 @@ class Driver(abc.ABC):
|
||||
"</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
|
||||
@abc.abstractmethod
|
||||
def type(self) -> str:
|
||||
"""客户端驱动类型名称"""
|
||||
"""混入驱动类型名称"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ForwardMixin(Mixin):
|
||||
"""客户端混入基类。"""
|
||||
|
||||
|
||||
class ReverseMixin(Mixin):
|
||||
"""服务端混入基类。"""
|
||||
|
||||
|
||||
class HTTPClientMixin(ForwardMixin):
|
||||
"""HTTP 客户端混入基类。"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def request(self, setup: Request) -> Response:
|
||||
"""发送一个 HTTP 请求"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class WebSocketClientMixin(ForwardMixin):
|
||||
"""WebSocket 客户端混入基类。"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@asynccontextmanager
|
||||
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
|
||||
|
||||
|
||||
class ForwardDriver(Driver, ForwardMixin):
|
||||
"""客户端基类。将客户端框架封装,以满足适配器使用。"""
|
||||
class ASGIMixin(ReverseMixin):
|
||||
"""ASGI 服务端基类。
|
||||
|
||||
|
||||
class ReverseDriver(Driver):
|
||||
"""服务端基类。将后端框架封装,以满足适配器使用。"""
|
||||
将后端框架封装,以满足适配器使用。
|
||||
"""
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
@@ -238,24 +271,14 @@ class ReverseDriver(Driver):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Driver]:
|
||||
"""将一个驱动器和多个混入类合并。"""
|
||||
# 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"
|
||||
ForwardDriver: TypeAlias = ForwardMixin
|
||||
"""支持客户端请求的驱动器。
|
||||
|
||||
if not mixins:
|
||||
return driver
|
||||
**Deprecated**,请使用 {ref}`nonebot.drivers.ForwardMixin` 或其子类代替。
|
||||
"""
|
||||
|
||||
def type_(self: ForwardDriver) -> str:
|
||||
return (
|
||||
driver.type.__get__(self)
|
||||
+ "+"
|
||||
+ "+".join(x.type.__get__(self) for x in mixins)
|
||||
)
|
||||
ReverseDriver: TypeAlias = ReverseMixin
|
||||
"""支持服务端请求的驱动器。
|
||||
|
||||
return type(
|
||||
"CombinedDriver", (*mixins, driver, ForwardDriver), {"type": property(type_)}
|
||||
) # type: ignore
|
||||
**Deprecated**,请使用 {ref}`nonebot.drivers.ReverseMixin` 或其子类代替。
|
||||
"""
|
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)
|
||||
+ "+"
|
||||
+ "+".join(x.type.__get__(self) for x in mixins)
|
||||
)
|
||||
|
||||
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
|
||||
for name, file_info in files_:
|
||||
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:
|
||||
self.files.append((name, (file_info[0], file_info[1], None)))
|
||||
else:
|
||||
|
@@ -6,6 +6,7 @@ matchers = MatcherManager()
|
||||
|
||||
from .matcher import Matcher as Matcher
|
||||
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_handler as current_handler
|
||||
from .matcher import current_matcher as current_matcher
|
||||
|
@@ -1,6 +1,5 @@
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
List,
|
||||
Type,
|
||||
Tuple,
|
||||
@@ -53,7 +52,7 @@ class MatcherManager(MutableMapping[int, List[Type["Matcher"]]]):
|
||||
def __delitem__(self, key: int) -> None:
|
||||
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
|
||||
|
||||
def keys(self) -> KeysView[int]:
|
||||
|
@@ -1,4 +1,9 @@
|
||||
import sys
|
||||
import inspect
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from dataclasses import dataclass
|
||||
from contextvars import ContextVar
|
||||
from typing_extensions import Self
|
||||
from datetime import datetime, timedelta
|
||||
@@ -8,6 +13,7 @@ from typing import (
|
||||
Any,
|
||||
List,
|
||||
Type,
|
||||
Tuple,
|
||||
Union,
|
||||
TypeVar,
|
||||
Callable,
|
||||
@@ -20,7 +26,8 @@ from typing import (
|
||||
|
||||
from nonebot.log import logger
|
||||
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.adapter import (
|
||||
Bot,
|
||||
@@ -74,15 +81,51 @@ current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
|
||||
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):
|
||||
if TYPE_CHECKING:
|
||||
module_name: Optional[str]
|
||||
type: str
|
||||
_source: Optional[MatcherSource]
|
||||
module_name: Optional[str]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"{self.__name__}(type={self.type!r}"
|
||||
+ (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):
|
||||
"""事件响应器类"""
|
||||
|
||||
plugin: ClassVar[Optional["Plugin"]] = None
|
||||
"""事件响应器所在插件"""
|
||||
module: ClassVar[Optional[ModuleType]] = None
|
||||
"""事件响应器所在插件模块"""
|
||||
plugin_name: ClassVar[Optional[str]] = None
|
||||
"""事件响应器所在插件名"""
|
||||
module_name: ClassVar[Optional[str]] = None
|
||||
"""事件响应器所在点分割插件模块路径"""
|
||||
_source: ClassVar[Optional[MatcherSource]] = None
|
||||
|
||||
type: ClassVar[str] = ""
|
||||
"""事件响应器类型"""
|
||||
@@ -124,7 +160,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
_default_permission_updater: ClassVar[Optional[Dependent[Permission]]] = None
|
||||
"""事件响应器权限更新函数"""
|
||||
|
||||
HANDLER_PARAM_TYPES = (
|
||||
HANDLER_PARAM_TYPES: ClassVar[Tuple[Type[Param], ...]] = (
|
||||
DependParam,
|
||||
BotParam,
|
||||
EventParam,
|
||||
@@ -142,6 +178,11 @@ class Matcher(metaclass=MatcherMeta):
|
||||
return (
|
||||
f"{self.__class__.__name__}(type={self.type!r}"
|
||||
+ (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,
|
||||
module: Optional[ModuleType] = None,
|
||||
source: Optional[MatcherSource] = None,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
default_state: Optional[T_State] = None,
|
||||
default_type_updater: Optional[Union[T_TypeUpdater, Dependent[str]]] = None,
|
||||
@@ -176,22 +218,47 @@ class Matcher(metaclass=MatcherMeta):
|
||||
temp: 是否为临时事件响应器,即触发一次后删除
|
||||
priority: 响应优先级
|
||||
block: 是否阻止事件向更低优先级的响应器传播
|
||||
plugin: 事件响应器所在插件
|
||||
module: 事件响应器所在模块
|
||||
default_state: 默认状态 `state`
|
||||
plugin: **Deprecated.** 事件响应器所在插件
|
||||
module: **Deprecated.** 事件响应器所在模块
|
||||
source: 事件响应器源代码上下文信息
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
default_state: 默认状态 `state`
|
||||
default_type_updater: 默认事件类型更新函数
|
||||
default_permission_updater: 默认会话权限更新函数
|
||||
|
||||
返回:
|
||||
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(
|
||||
cls.__name__,
|
||||
(cls,),
|
||||
{
|
||||
"plugin": plugin,
|
||||
"module": module,
|
||||
"plugin_name": plugin and plugin.name,
|
||||
"module_name": module and module.__name__,
|
||||
"_source": source,
|
||||
"type": type_,
|
||||
"rule": rule or Rule(),
|
||||
"permission": permission or Permission(),
|
||||
@@ -253,6 +320,26 @@ class Matcher(metaclass=MatcherMeta):
|
||||
"""销毁当前的事件响应器"""
|
||||
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
|
||||
async def check_perm(
|
||||
cls,
|
||||
@@ -773,8 +860,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
temp=True,
|
||||
priority=0,
|
||||
block=True,
|
||||
plugin=self.plugin,
|
||||
module=self.module,
|
||||
source=self.__class__._source,
|
||||
expire_time=bot.config.session_expire_timeout,
|
||||
default_state=self.state,
|
||||
default_type_updater=self.__class__._default_type_updater,
|
||||
@@ -794,8 +880,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
temp=True,
|
||||
priority=0,
|
||||
block=True,
|
||||
plugin=self.plugin,
|
||||
module=self.module,
|
||||
source=self.__class__._source,
|
||||
expire_time=bot.config.session_expire_timeout,
|
||||
default_state=self.state,
|
||||
default_type_updater=self.__class__._default_type_updater,
|
||||
|
@@ -1,11 +1,21 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
from typing_extensions import Annotated
|
||||
from typing_extensions import Self, Annotated, override
|
||||
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.fields import Required, Undefined, ModelField
|
||||
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
|
||||
|
||||
from nonebot.dependencies.utils import check_field_type
|
||||
from nonebot.dependencies import Param, Dependent, CustomConfig
|
||||
@@ -24,6 +34,23 @@ if TYPE_CHECKING:
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.adapters import Bot, Event
|
||||
|
||||
EXTRA_FIELD_INFO = (
|
||||
"gt",
|
||||
"lt",
|
||||
"ge",
|
||||
"le",
|
||||
"multiple_of",
|
||||
"allow_inf_nan",
|
||||
"max_digits",
|
||||
"decimal_places",
|
||||
"min_items",
|
||||
"max_items",
|
||||
"unique_items",
|
||||
"min_length",
|
||||
"max_length",
|
||||
"regex",
|
||||
)
|
||||
|
||||
|
||||
class DependsInner:
|
||||
def __init__(
|
||||
@@ -31,26 +58,31 @@ class DependsInner:
|
||||
dependency: Optional[T_Handler] = None,
|
||||
*,
|
||||
use_cache: bool = True,
|
||||
validate: Union[bool, FieldInfo] = False,
|
||||
) -> None:
|
||||
self.dependency = dependency
|
||||
self.use_cache = use_cache
|
||||
self.validate = validate
|
||||
|
||||
def __repr__(self) -> str:
|
||||
dep = get_name(self.dependency)
|
||||
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(
|
||||
dependency: Optional[T_Handler] = None,
|
||||
*,
|
||||
use_cache: bool = True,
|
||||
validate: Union[bool, FieldInfo] = False,
|
||||
) -> Any:
|
||||
"""子依赖装饰器
|
||||
|
||||
参数:
|
||||
dependency: 依赖函数。默认为参数的类型注释。
|
||||
use_cache: 是否使用缓存。默认为 `True`。
|
||||
validate: 是否使用 Pydantic 类型校验。默认为 `False`。
|
||||
|
||||
用法:
|
||||
```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):
|
||||
@@ -85,23 +117,44 @@ class DependParam(Param):
|
||||
return f"Depends({self.extra['dependent']})"
|
||||
|
||||
@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(
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["DependParam"]:
|
||||
) -> Optional[Self]:
|
||||
type_annotation, depends_inner = param.annotation, None
|
||||
# extract type annotation and dependency from Annotated
|
||||
if get_origin(param.annotation) is Annotated:
|
||||
type_annotation, *extra_args = get_args(param.annotation)
|
||||
depends_inner = next(
|
||||
(x for x in extra_args if isinstance(x, DependsInner)), None
|
||||
(x for x in reversed(extra_args) if isinstance(x, DependsInner)), None
|
||||
)
|
||||
|
||||
# param default value takes higher priority
|
||||
depends_inner = (
|
||||
param.default if isinstance(param.default, DependsInner) else depends_inner
|
||||
)
|
||||
# not a dependent
|
||||
if depends_inner is None:
|
||||
return
|
||||
|
||||
dependency: T_Handler
|
||||
# sub dependency is not specified, use type annotation
|
||||
if depends_inner.dependency is None:
|
||||
assert (
|
||||
type_annotation is not inspect.Signature.empty
|
||||
@@ -109,13 +162,18 @@ class DependParam(Param):
|
||||
dependency = type_annotation
|
||||
else:
|
||||
dependency = depends_inner.dependency
|
||||
# parse sub dependency
|
||||
sub_dependent = Dependent[Any].parse(
|
||||
call=dependency,
|
||||
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
|
||||
@override
|
||||
def _check_parameterless(
|
||||
cls, value: Any, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["Param"]:
|
||||
@@ -124,8 +182,9 @@ class DependParam(Param):
|
||||
dependent = Dependent[Any].parse(
|
||||
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(
|
||||
self,
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
@@ -169,6 +228,7 @@ class DependParam(Param):
|
||||
dependency_cache[call] = task
|
||||
return await task
|
||||
|
||||
@override
|
||||
async def _check(self, **kwargs: Any) -> None:
|
||||
# run sub dependent pre-checkers
|
||||
sub_dependent: Dependent = self.extra["dependent"]
|
||||
@@ -195,9 +255,10 @@ class BotParam(Param):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def _check_param(
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["BotParam"]:
|
||||
) -> Optional[Self]:
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
# 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":
|
||||
return cls(Required)
|
||||
|
||||
@override
|
||||
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
|
||||
return bot
|
||||
|
||||
@override
|
||||
async def _check(self, bot: "Bot", **kwargs: Any) -> None:
|
||||
if checker := self.extra.get("checker"):
|
||||
check_field_type(checker, bot)
|
||||
@@ -245,9 +308,10 @@ class EventParam(Param):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def _check_param(
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["EventParam"]:
|
||||
) -> Optional[Self]:
|
||||
from nonebot.adapters import Event
|
||||
|
||||
# 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":
|
||||
return cls(Required)
|
||||
|
||||
@override
|
||||
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
|
||||
return event
|
||||
|
||||
@override
|
||||
async def _check(self, event: "Event", **kwargs: Any) -> Any:
|
||||
if checker := self.extra.get("checker", None):
|
||||
check_field_type(checker, event)
|
||||
@@ -287,9 +353,10 @@ class StateParam(Param):
|
||||
return "StateParam()"
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def _check_param(
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["StateParam"]:
|
||||
) -> Optional[Self]:
|
||||
# param type is T_State
|
||||
if param.annotation is T_State:
|
||||
return cls(Required)
|
||||
@@ -297,6 +364,7 @@ class StateParam(Param):
|
||||
elif param.annotation == param.empty and param.name == "state":
|
||||
return cls(Required)
|
||||
|
||||
@override
|
||||
async def _solve(self, state: T_State, **kwargs: Any) -> Any:
|
||||
return state
|
||||
|
||||
@@ -313,9 +381,10 @@ class MatcherParam(Param):
|
||||
return "MatcherParam()"
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def _check_param(
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["MatcherParam"]:
|
||||
) -> Optional[Self]:
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
# 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":
|
||||
return cls(Required)
|
||||
|
||||
@override
|
||||
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||
return matcher
|
||||
|
||||
@override
|
||||
async def _check(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||
if checker := self.extra.get("checker", None):
|
||||
check_field_type(checker, matcher)
|
||||
@@ -382,15 +453,16 @@ class ArgParam(Param):
|
||||
return f"ArgParam(key={self.extra['key']!r}, type={self.extra['type']!r})"
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def _check_param(
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["ArgParam"]:
|
||||
) -> Optional[Self]:
|
||||
if isinstance(param.default, ArgInner):
|
||||
return cls(
|
||||
Required, key=param.default.key or param.name, type=param.default.type
|
||||
)
|
||||
elif get_origin(param.annotation) is Annotated:
|
||||
for arg in get_args(param.annotation):
|
||||
for arg in get_args(param.annotation)[:0:-1]:
|
||||
if isinstance(arg, ArgInner):
|
||||
return cls(Required, key=arg.key or param.name, type=arg.type)
|
||||
|
||||
@@ -419,9 +491,10 @@ class ExceptionParam(Param):
|
||||
return "ExceptionParam()"
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def _check_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
|
||||
if generic_check_issubclass(param.annotation, Exception):
|
||||
return cls(Required)
|
||||
@@ -429,6 +502,7 @@ class ExceptionParam(Param):
|
||||
elif param.annotation == param.empty and param.name == "exception":
|
||||
return cls(Required)
|
||||
|
||||
@override
|
||||
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
|
||||
return exception
|
||||
|
||||
@@ -445,12 +519,14 @@ class DefaultParam(Param):
|
||||
return f"DefaultParam(default={self.default!r})"
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def _check_param(
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["DefaultParam"]:
|
||||
) -> Optional[Self]:
|
||||
if param.default != param.empty:
|
||||
return cls(param.default)
|
||||
|
||||
@override
|
||||
async def _solve(self, **kwargs: Any) -> Any:
|
||||
return Undefined
|
||||
|
||||
|
@@ -8,6 +8,7 @@ FrontMatter:
|
||||
from nonebot.internal.matcher import Matcher as Matcher
|
||||
from nonebot.internal.matcher import matchers as matchers
|
||||
from nonebot.internal.matcher import current_bot as current_bot
|
||||
from nonebot.internal.matcher import MatcherSource as MatcherSource
|
||||
from nonebot.internal.matcher import current_event as current_event
|
||||
from nonebot.internal.matcher import MatcherManager as MatcherManager
|
||||
from nonebot.internal.matcher import MatcherProvider as MatcherProvider
|
||||
|
@@ -358,9 +358,18 @@ async def _check_matcher(
|
||||
return False
|
||||
|
||||
try:
|
||||
if not await Matcher.check_perm(
|
||||
bot, event, stack, dependency_cache
|
||||
) or not await Matcher.check_rule(bot, event, state, stack, dependency_cache):
|
||||
if not await Matcher.check_perm(bot, event, stack, dependency_cache):
|
||||
logger.trace(f"Permission conditions not met for {Matcher}")
|
||||
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
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
|
@@ -29,7 +29,7 @@
|
||||
- `load_builtin_plugins` =>
|
||||
{ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
|
||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.plugin.PluginMetadata>`
|
||||
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.model.PluginMetadata>`
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 0
|
||||
@@ -77,7 +77,7 @@ def get_plugin(name: str) -> Optional["Plugin"]:
|
||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
||||
|
||||
参数:
|
||||
name: 插件名,即 {ref}`nonebot.plugin.plugin.Plugin.name`。
|
||||
name: 插件名,即 {ref}`nonebot.plugin.model.Plugin.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()}
|
||||
has_parent = True
|
||||
@@ -111,9 +111,9 @@ def get_available_plugin_names() -> Set[str]:
|
||||
from .on import on as on
|
||||
from .manager import PluginManager
|
||||
from .on import on_type as on_type
|
||||
from .model import Plugin as Plugin
|
||||
from .load import require as require
|
||||
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_command as on_command
|
||||
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 .load import load_from_json as load_from_json
|
||||
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 .plugin import PluginMetadata as PluginMetadata
|
||||
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_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 .plugin import Plugin
|
||||
from .model import Plugin
|
||||
from .manager import PluginManager
|
||||
from . import _managers, get_plugin, _current_plugin_chain, _module_name_to_plugin_name
|
||||
|
||||
@@ -160,7 +160,7 @@ def require(name: str) -> ModuleType:
|
||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
||||
|
||||
参数:
|
||||
name: 插件名,即 {ref}`nonebot.plugin.plugin.Plugin.name`。
|
||||
name: 插件名,即 {ref}`nonebot.plugin.model.Plugin.name`。
|
||||
|
||||
异常:
|
||||
RuntimeError: 插件无法加载
|
||||
|
@@ -20,7 +20,7 @@ from typing import Set, Dict, List, Iterable, Optional, Sequence
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import escape_tag, path_to_module_name
|
||||
|
||||
from .plugin import Plugin, PluginMetadata
|
||||
from .model import Plugin, PluginMetadata
|
||||
from . import (
|
||||
_managers,
|
||||
_new_plugin,
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 3
|
||||
description: nonebot.plugin.plugin 模块
|
||||
description: nonebot.plugin.model 模块
|
||||
"""
|
||||
|
||||
import contextlib
|
@@ -7,14 +7,15 @@ FrontMatter:
|
||||
|
||||
import re
|
||||
import inspect
|
||||
import warnings
|
||||
from types import ModuleType
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Set, Dict, List, Type, Tuple, Union, Optional
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.permission import Permission
|
||||
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.rule import (
|
||||
Rule,
|
||||
@@ -29,7 +30,7 @@ from nonebot.rule import (
|
||||
shell_command,
|
||||
)
|
||||
|
||||
from .plugin import Plugin
|
||||
from .model import Plugin
|
||||
from . import get_plugin_by_module_name
|
||||
from .manager import _current_plugin_chain
|
||||
|
||||
@@ -45,25 +46,39 @@ def store_matcher(matcher: Type[Matcher]) -> None:
|
||||
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: 调用栈深度
|
||||
"""
|
||||
# matcher defined when plugin loading
|
||||
if plugin_chain := _current_plugin_chain.get():
|
||||
return plugin_chain[-1]
|
||||
|
||||
# matcher defined when plugin running
|
||||
if module := get_matcher_module(depth + 1):
|
||||
if plugin := get_plugin_by_module_name(module.__name__):
|
||||
return plugin
|
||||
warnings.warn(
|
||||
"`get_matcher_plugin` is deprecated, please use `get_matcher_source` instead",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return (source := get_matcher_source(depth + 1)) and source.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: 调用栈深度
|
||||
"""
|
||||
@@ -71,7 +86,22 @@ def get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
|
||||
if current_frame is None:
|
||||
return None
|
||||
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(
|
||||
@@ -109,8 +139,7 @@ def on(
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=get_matcher_plugin(_depth + 1),
|
||||
module=get_matcher_module(_depth + 1),
|
||||
source=get_matcher_source(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
store_matcher(matcher)
|
||||
|
@@ -4,17 +4,18 @@ from types import ModuleType
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.permission import Permission
|
||||
from nonebot.dependencies import Dependent
|
||||
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 .plugin import Plugin
|
||||
from .model import Plugin
|
||||
|
||||
def store_matcher(matcher: type[Matcher]) -> None: ...
|
||||
def get_matcher_plugin(depth: int = ...) -> Plugin | None: ...
|
||||
def get_matcher_module(depth: int = ...) -> ModuleType | None: ...
|
||||
def get_matcher_source(depth: int = ...) -> MatcherSource | None: ...
|
||||
def on(
|
||||
type: str = "",
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
|
@@ -117,6 +117,11 @@ class TrieRule:
|
||||
# check whitespace
|
||||
arg_str = segment_text[len(pf.key) :]
|
||||
arg_str_stripped = arg_str.lstrip()
|
||||
# check next segment until arg detected or no text remain
|
||||
while not arg_str_stripped and msg and msg[0].is_text():
|
||||
arg_str += str(msg.pop(0))
|
||||
arg_str_stripped = arg_str.lstrip()
|
||||
|
||||
has_arg = arg_str_stripped or msg
|
||||
if (
|
||||
has_arg
|
||||
@@ -599,7 +604,7 @@ def shell_command(
|
||||
通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典
|
||||
(例: `{"arg": "arg", "h": True}`)。
|
||||
|
||||
:::warning 警告
|
||||
:::caution 警告
|
||||
如果参数解析失败,则通过 {ref}`nonebot.params.ShellCommandArgs`
|
||||
获取的将是 {ref}`nonebot.exception.ParserExit` 异常。
|
||||
:::
|
||||
|
@@ -21,6 +21,7 @@ from typing import (
|
||||
Type,
|
||||
Tuple,
|
||||
Union,
|
||||
Generic,
|
||||
TypeVar,
|
||||
Callable,
|
||||
Optional,
|
||||
@@ -30,7 +31,7 @@ from typing import (
|
||||
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
|
||||
|
||||
@@ -74,9 +75,18 @@ def generic_check_issubclass(
|
||||
is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
|
||||
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
|
||||
elif origin:
|
||||
# avoid class check error (typing.Final, typing.ClassVar, etc...)
|
||||
try:
|
||||
return issubclass(origin, class_or_tuple)
|
||||
except TypeError:
|
||||
return False
|
||||
elif isinstance(cls, TypeVar):
|
||||
if cls.__constraints__:
|
||||
return all(
|
||||
@@ -220,6 +230,16 @@ def resolve_dot_notation(
|
||||
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):
|
||||
"""可以序列化 {ref}`nonebot.adapters.Message`(List[Dataclass]) 的 `JSONEncoder`"""
|
||||
|
||||
|
23
package.json
23
package.json
@@ -12,11 +12,30 @@
|
||||
"serve": "yarn workspace nonebot serve",
|
||||
"clear": "yarn workspace nonebot clear",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||
"@typescript-eslint/parser": "^6.6.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"prettier": "^2.5.0",
|
||||
"pyright": "^1.1.317"
|
||||
"eslint": "^8.48.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
1697
poetry.lock
generated
1697
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "nonebot2"
|
||||
version = "2.0.1"
|
||||
version = "2.1.2"
|
||||
description = "An asynchronous python bot framework."
|
||||
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||
license = "MIT"
|
||||
@@ -14,11 +14,9 @@ classifiers = [
|
||||
"Framework :: Robot Framework",
|
||||
"Framework :: Robot Framework :: Library",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3"
|
||||
]
|
||||
packages = [
|
||||
{ include = "nonebot" },
|
||||
"Programming Language :: Python :: 3",
|
||||
]
|
||||
packages = [{ include = "nonebot" }]
|
||||
include = ["nonebot/py.typed"]
|
||||
|
||||
[tool.poetry.urls]
|
||||
@@ -31,7 +29,7 @@ python = "^3.8"
|
||||
yarl = "^1.7.2"
|
||||
pygtrie = "^2.4.1"
|
||||
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" }
|
||||
pydantic = { version = "^1.10.0", extras = ["dotenv"] }
|
||||
|
||||
@@ -40,7 +38,9 @@ Quart = { version = ">=0.18.0,<1.0.0", optional = true }
|
||||
fastapi = { version = ">=0.93.0,<1.0.0", optional = true }
|
||||
aiohttp = { version = "^3.7.4", extras = ["speedups"], optional = true }
|
||||
httpx = { version = ">=0.20.0,<1.0.0", extras = ["http2"], optional = true }
|
||||
uvicorn = { version = ">=0.20.0,<1.0.0", extras = ["standard"], optional = true }
|
||||
uvicorn = { version = ">=0.20.0,<1.0.0", extras = [
|
||||
"standard",
|
||||
], optional = true }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
isort = "^5.10.1"
|
||||
@@ -51,10 +51,10 @@ ruff = ">=0.0.272,<1.0.0"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
nonebug = "^0.3.0"
|
||||
werkzeug = "^2.3.6"
|
||||
pytest-cov = "^4.0.0"
|
||||
pytest-xdist = "^3.0.2"
|
||||
pytest-asyncio = "^0.21.0"
|
||||
pytest-asyncio = "^0.22.0"
|
||||
werkzeug = ">=2.3.6,<4.0.0"
|
||||
coverage-conditional-plugin = "^0.9.0"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
@@ -71,10 +71,7 @@ all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "strict"
|
||||
addopts = "--cov=nonebot --cov-append --cov-report=term-missing"
|
||||
filterwarnings = [
|
||||
"error",
|
||||
"ignore::DeprecationWarning",
|
||||
]
|
||||
filterwarnings = ["error", "ignore::DeprecationWarning"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
@@ -94,7 +91,7 @@ extra_standard_library = ["typing_extensions"]
|
||||
|
||||
[tool.ruff]
|
||||
select = ["E", "W", "F", "UP", "C", "T", "PYI", "PT", "Q"]
|
||||
ignore = ["E402", "C901"]
|
||||
ignore = ["E402", "C901", "UP037"]
|
||||
|
||||
line-length = 88
|
||||
target-version = "py38"
|
||||
@@ -107,13 +104,15 @@ mark-parentheses = false
|
||||
pythonVersion = "3.8"
|
||||
pythonPlatform = "All"
|
||||
executionEnvironments = [
|
||||
{ root = "./tests", extraPaths = ["./"] },
|
||||
{ root = "./tests", extraPaths = [
|
||||
"./",
|
||||
] },
|
||||
{ root = "./" },
|
||||
]
|
||||
|
||||
typeCheckingMode = "basic"
|
||||
reportShadowedImports = false
|
||||
|
||||
disableBytesTypePromotions = true
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry_core>=1.0.0"]
|
||||
|
@@ -11,6 +11,7 @@ exclude_lines =
|
||||
if (typing\.)?TYPE_CHECKING( is True)?:
|
||||
@(abc\.)?abstractmethod
|
||||
raise NotImplementedError
|
||||
warnings\.warn
|
||||
\.\.\.
|
||||
pass
|
||||
if __name__ == .__main__.:
|
||||
|
@@ -8,8 +8,10 @@ from nonebug import NONEBOT_INIT_KWARGS
|
||||
from werkzeug.serving import BaseWSGIServer, make_server
|
||||
|
||||
import nonebot
|
||||
from nonebot.drivers import URL
|
||||
from nonebot.config import Env
|
||||
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_OVERRIDE"] = "new"
|
||||
@@ -17,11 +19,24 @@ os.environ["CONFIG_OVERRIDE"] = "new"
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
|
||||
collect_ignore = ["plugins/", "dynamic/", "bad_plugins/"]
|
||||
|
||||
|
||||
def pytest_configure(config: pytest.Config) -> None:
|
||||
config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"}
|
||||
|
||||
|
||||
@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)
|
||||
def load_plugin(nonebug_init: None) -> Set["Plugin"]:
|
||||
# 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:
|
||||
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 typing_extensions import Annotated
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from nonebot import on_message
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.params import Depends
|
||||
|
||||
test_depends = on_message()
|
||||
@@ -33,6 +36,14 @@ class ClassDependency:
|
||||
y: int = Depends(gen_async)
|
||||
|
||||
|
||||
class FooBot(Bot):
|
||||
...
|
||||
|
||||
|
||||
async def sub_bot(b: FooBot) -> FooBot:
|
||||
return b
|
||||
|
||||
|
||||
# test parameterless
|
||||
@test_depends.handle(parameterless=[Depends(parameterless)])
|
||||
async def depends(x: int = Depends(dependency)):
|
||||
@@ -46,19 +57,52 @@ async def depends_cache(y: int = Depends(dependency, use_cache=True)):
|
||||
return y
|
||||
|
||||
|
||||
# test class dependency
|
||||
async def class_depend(c: ClassDependency = Depends()):
|
||||
return c
|
||||
|
||||
|
||||
# test annotated dependency
|
||||
async def annotated_depend(x: Annotated[int, Depends(dependency)]):
|
||||
return x
|
||||
|
||||
|
||||
# test annotated class dependency
|
||||
async def annotated_class_depend(c: Annotated[ClassDependency, Depends()]):
|
||||
return c
|
||||
|
||||
|
||||
# test dependency priority
|
||||
async def annotated_prior_depend(
|
||||
x: Annotated[int, Depends(lambda: 2)] = Depends(dependency)
|
||||
):
|
||||
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,15 +1,12 @@
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Any, Set, Optional, cast
|
||||
from typing import Any, Set, Optional
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
import nonebot
|
||||
from nonebot.config import Env
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.params import Depends
|
||||
from nonebot import _resolve_combine_expr
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers._lifespan import Lifespan
|
||||
@@ -18,25 +15,15 @@ from nonebot.drivers import (
|
||||
Driver,
|
||||
Request,
|
||||
Response,
|
||||
ASGIMixin,
|
||||
WebSocket,
|
||||
ForwardDriver,
|
||||
ReverseDriver,
|
||||
HTTPClientMixin,
|
||||
HTTPServerSetup,
|
||||
WebSocketClientMixin,
|
||||
WebSocketServerSetup,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="driver")
|
||||
def load_driver(request: pytest.FixtureRequest) -> Driver:
|
||||
driver_name = getattr(request, "param", None)
|
||||
global_driver = nonebot.get_driver()
|
||||
if driver_name is None:
|
||||
return global_driver
|
||||
|
||||
DriverClass = _resolve_combine_expr(driver_name)
|
||||
return DriverClass(Env(environment=global_driver.env), global_driver.config)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifespan():
|
||||
lifespan = Lifespan()
|
||||
@@ -80,7 +67,7 @@ async def test_lifespan():
|
||||
indirect=True,
|
||||
)
|
||||
async def test_http_server(app: App, driver: Driver):
|
||||
driver = cast(ReverseDriver, driver)
|
||||
assert isinstance(driver, ASGIMixin)
|
||||
|
||||
async def _handle_http(request: Request) -> Response:
|
||||
assert request.content in (b"test", "test")
|
||||
@@ -108,7 +95,7 @@ async def test_http_server(app: App, driver: Driver):
|
||||
indirect=True,
|
||||
)
|
||||
async def test_websocket_server(app: App, driver: Driver):
|
||||
driver = cast(ReverseDriver, driver)
|
||||
assert isinstance(driver, ASGIMixin)
|
||||
|
||||
async def _handle_ws(ws: WebSocket) -> None:
|
||||
await ws.accept()
|
||||
@@ -164,7 +151,7 @@ async def test_websocket_server(app: App, driver: Driver):
|
||||
indirect=True,
|
||||
)
|
||||
async def test_cross_context(app: App, driver: Driver):
|
||||
driver = cast(ReverseDriver, driver)
|
||||
assert isinstance(driver, ASGIMixin)
|
||||
|
||||
ws: Optional[WebSocket] = None
|
||||
ws_ready = asyncio.Event()
|
||||
@@ -221,7 +208,7 @@ async def test_cross_context(app: App, driver: Driver):
|
||||
indirect=True,
|
||||
)
|
||||
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
|
||||
request = Request(
|
||||
@@ -233,6 +220,23 @@ async def test_http_client(driver: Driver, server_url: URL):
|
||||
content="test",
|
||||
)
|
||||
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.content
|
||||
data = json.loads(response.content)
|
||||
@@ -265,7 +269,11 @@ async def test_http_client(driver: Driver, server_url: URL):
|
||||
"POST",
|
||||
server_url,
|
||||
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)
|
||||
assert response.status_code == 200
|
||||
@@ -273,11 +281,28 @@ async def test_http_client(driver: Driver, server_url: URL):
|
||||
data = json.loads(response.content)
|
||||
assert data["method"] == "POST"
|
||||
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)
|
||||
|
||||
|
||||
@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.parametrize(
|
||||
("driver", "driver_type"),
|
||||
|
@@ -2,7 +2,7 @@ import pytest
|
||||
from nonebug import App
|
||||
|
||||
import nonebot
|
||||
from nonebot.drivers import Driver, ReverseDriver
|
||||
from nonebot.drivers import Driver, ASGIMixin, ReverseDriver
|
||||
from nonebot import (
|
||||
get_app,
|
||||
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):
|
||||
driver = get_driver()
|
||||
assert isinstance(driver, ReverseDriver)
|
||||
assert isinstance(driver, ASGIMixin)
|
||||
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):
|
||||
driver = get_driver()
|
||||
assert isinstance(driver, ReverseDriver)
|
||||
assert isinstance(driver, ASGIMixin)
|
||||
assert get_app() == driver.server_app
|
||||
|
||||
|
||||
|
@@ -1,10 +1,88 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
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 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
|
||||
@@ -62,7 +140,7 @@ async def test_matcher_receive(app: App):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher_(app: App):
|
||||
async def test_matcher_combine(app: App):
|
||||
from plugins.matcher.matcher_process import test_combine
|
||||
|
||||
message = FakeMessage("text")
|
||||
|
@@ -42,10 +42,16 @@ async def test_depend(app: App):
|
||||
ClassDependency,
|
||||
runned,
|
||||
depends,
|
||||
validate,
|
||||
class_depend,
|
||||
test_depends,
|
||||
validate_fail,
|
||||
validate_field,
|
||||
annotated_depend,
|
||||
sub_type_mismatch,
|
||||
validate_field_fail,
|
||||
annotated_class_depend,
|
||||
annotated_multi_depend,
|
||||
annotated_prior_depend,
|
||||
)
|
||||
|
||||
@@ -62,8 +68,7 @@ async def test_depend(app: App):
|
||||
event_next = make_fake_event()()
|
||||
ctx.receive_event(bot, event_next)
|
||||
|
||||
assert len(runned) == 2
|
||||
assert runned[0] == runned[1] == 1
|
||||
assert runned == [1, 1]
|
||||
|
||||
runned.clear()
|
||||
|
||||
@@ -77,13 +82,42 @@ async def test_depend(app: App):
|
||||
annotated_prior_depend, allow_types=[DependParam]
|
||||
) as ctx:
|
||||
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(
|
||||
annotated_class_depend, allow_types=[DependParam]
|
||||
) as ctx:
|
||||
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
|
||||
async def test_bot(app: App):
|
||||
@@ -447,6 +481,8 @@ async def test_arg(app: App):
|
||||
annotated_arg,
|
||||
arg_plain_text,
|
||||
annotated_arg_str,
|
||||
annotated_multi_arg,
|
||||
annotated_prior_arg,
|
||||
annotated_arg_plain_text,
|
||||
)
|
||||
|
||||
@@ -480,6 +516,14 @@ async def test_arg(app: App):
|
||||
ctx.pass_params(matcher=matcher)
|
||||
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
|
||||
async def test_exception(app: App):
|
||||
|
@@ -80,7 +80,7 @@ async def test_load_toml():
|
||||
async def test_bad_plugin():
|
||||
nonebot.load_plugins("bad_plugins")
|
||||
|
||||
assert nonebot.get_plugin("bad_plugins") is None
|
||||
assert nonebot.get_plugin("bad_plugin") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
@@ -113,6 +113,36 @@ async def test_trie(app: App):
|
||||
command_whitespace=" ",
|
||||
)
|
||||
|
||||
message = FakeMessageSegment.text("/fake-prefix ") + FakeMessageSegment.text(
|
||||
" some args"
|
||||
)
|
||||
event = make_fake_event(_message=message)()
|
||||
state = {}
|
||||
TrieRule.get_value(bot, event, state)
|
||||
assert state[PREFIX_KEY] == CMD_RESULT(
|
||||
command=("fake-prefix",),
|
||||
raw_command="/fake-prefix",
|
||||
command_arg=FakeMessage("some args"),
|
||||
command_start="/",
|
||||
command_whitespace=" ",
|
||||
)
|
||||
|
||||
message = (
|
||||
FakeMessageSegment.text("/fake-prefix ")
|
||||
+ FakeMessageSegment.text(" ")
|
||||
+ FakeMessageSegment.text(" some args")
|
||||
)
|
||||
event = make_fake_event(_message=message)()
|
||||
state = {}
|
||||
TrieRule.get_value(bot, event, state)
|
||||
assert state[PREFIX_KEY] == CMD_RESULT(
|
||||
command=("fake-prefix",),
|
||||
raw_command="/fake-prefix",
|
||||
command_arg=FakeMessage("some args"),
|
||||
command_start="/",
|
||||
command_whitespace=" ",
|
||||
)
|
||||
|
||||
del TrieRule.prefix["/fake-prefix"]
|
||||
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from typing import Dict, List, Union, TypeVar
|
||||
from typing import Dict, List, Union, Literal, TypeVar, ClassVar
|
||||
|
||||
from utils import FakeMessage, FakeMessageSegment
|
||||
from nonebot.utils import (
|
||||
@@ -24,8 +24,11 @@ def test_generic_check_issubclass():
|
||||
assert generic_check_issubclass(int, (int, float))
|
||||
assert not generic_check_issubclass(str, (int, float))
|
||||
assert generic_check_issubclass(Union[int, float, None], (int, float))
|
||||
assert generic_check_issubclass(Literal[1, 2, 3], int)
|
||||
assert not generic_check_issubclass(Literal[1, 2, "3"], int)
|
||||
assert generic_check_issubclass(List[int], list)
|
||||
assert generic_check_issubclass(Dict[str, int], dict)
|
||||
assert not generic_check_issubclass(ClassVar[int], int)
|
||||
assert generic_check_issubclass(TypeVar("T", int, float), (int, float))
|
||||
assert generic_check_issubclass(TypeVar("T", bound=int), (int, float))
|
||||
|
||||
|
@@ -1,8 +1,9 @@
|
||||
from typing_extensions import override
|
||||
from typing import Type, Union, Mapping, Iterable, Optional
|
||||
|
||||
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:
|
||||
@@ -12,11 +13,24 @@ def escape_text(s: str, *, escape_comma: bool = True) -> str:
|
||||
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"]):
|
||||
@classmethod
|
||||
@override
|
||||
def get_message_class(cls):
|
||||
return FakeMessage
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
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"):
|
||||
return FakeMessageSegment("node", {"content": content})
|
||||
|
||||
@override
|
||||
def is_text(self) -> bool:
|
||||
return self.type == "text"
|
||||
|
||||
|
||||
class FakeMessage(Message[FakeMessageSegment]):
|
||||
@classmethod
|
||||
@override
|
||||
def get_segment_class(cls):
|
||||
return FakeMessageSegment
|
||||
|
||||
@staticmethod
|
||||
@override
|
||||
def _construct(msg: Union[str, Iterable[Mapping]]):
|
||||
if isinstance(msg, str):
|
||||
yield FakeMessageSegment.text(msg)
|
||||
@@ -50,6 +67,7 @@ class FakeMessage(Message[FakeMessageSegment]):
|
||||
yield FakeMessageSegment(**seg)
|
||||
return
|
||||
|
||||
@override
|
||||
def __add__(
|
||||
self, other: Union[str, FakeMessageSegment, Iterable[FakeMessageSegment]]
|
||||
):
|
||||
@@ -71,30 +89,37 @@ def make_fake_event(
|
||||
Base = _base or Event
|
||||
|
||||
class FakeEvent(Base, extra=Extra.forbid):
|
||||
@override
|
||||
def get_type(self) -> str:
|
||||
return _type
|
||||
|
||||
@override
|
||||
def get_event_name(self) -> str:
|
||||
return _name
|
||||
|
||||
@override
|
||||
def get_event_description(self) -> str:
|
||||
return _description
|
||||
|
||||
@override
|
||||
def get_user_id(self) -> str:
|
||||
if _user_id is not None:
|
||||
return _user_id
|
||||
raise NotImplementedError
|
||||
|
||||
@override
|
||||
def get_session_id(self) -> str:
|
||||
if _session_id is not None:
|
||||
return _session_id
|
||||
raise NotImplementedError
|
||||
|
||||
@override
|
||||
def get_message(self) -> "Message":
|
||||
if _message is not None:
|
||||
return _message
|
||||
raise NotImplementedError
|
||||
|
||||
@override
|
||||
def is_tome(self) -> bool:
|
||||
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:
|
||||
menu:
|
||||
- category: advanced
|
||||
weight: 20
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 使用适配器
|
||||
@@ -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:
|
||||
menu:
|
||||
- category: advanced
|
||||
weight: 70
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 依赖注入
|
||||
@@ -219,7 +219,7 @@ async def _(e: Union[ActionFailed, NetworkError]): ...
|
||||
<Tabs groupId="python">
|
||||
<TabItem value="3.9" label="Python 3.9+" default>
|
||||
|
||||
```python {4,16}
|
||||
```python {5,15}
|
||||
from typing import Annotated
|
||||
|
||||
from nonebot import on_command
|
||||
@@ -241,7 +241,7 @@ async def _(event: Annotated[Event, Depends(check)]):
|
||||
</TabItem>
|
||||
<TabItem value="3.8" label="Python 3.8+">
|
||||
|
||||
```python {2,14}
|
||||
```python {3,13}
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.params import Depends
|
||||
@@ -267,7 +267,7 @@ async def _(event: Event = Depends(check)):
|
||||
|
||||
特别的,我们可以为 `Dependent` 对象定义一系列前置子依赖,它们会在参数执行前被顺序执行,且返回值将会被忽略,例如:
|
||||
|
||||
```python {2,14}
|
||||
```python {11}
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters import Event
|
||||
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>
|
||||
</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 内部就使用了这两个装饰器。
|
||||
:::
|
||||
|
||||
|
@@ -4,8 +4,8 @@ description: 选择合适的驱动器运行机器人
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: advanced
|
||||
weight: 10
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 选择驱动器
|
||||
@@ -22,21 +22,22 @@ options:
|
||||
|
||||
## 驱动器类型
|
||||
|
||||
驱动器的类型有两种:
|
||||
驱动器类型大体上可以分为两种:
|
||||
|
||||
- `ForwardDriver`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。
|
||||
- `ReverseDriver`:即服务端型驱动器,多用于使用 WebHook,接收 WebSocket 客户端连接等情形。
|
||||
- `Forward`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。
|
||||
- `Reverse`:即服务端型驱动器,多用于使用 WebHook,接收 WebSocket 客户端连接等情形。
|
||||
|
||||
客户端型驱动器具有以下两种功能:
|
||||
客户端型驱动器可以分为以下两种:
|
||||
|
||||
1. 异步发送 HTTP 请求,自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。
|
||||
2. 异步建立 WebSocket 连接上下文,自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。
|
||||
|
||||
服务端型驱动器通常为 ASGI 应用框架,具有以下功能:
|
||||
服务端型驱动器目前有:
|
||||
|
||||
1. 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。
|
||||
2. 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。
|
||||
3. 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。
|
||||
1. ASGI 应用框架,具有以下功能:
|
||||
- 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。
|
||||
- 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。
|
||||
- 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。
|
||||
|
||||
## 配置驱动器
|
||||
|
||||
@@ -79,7 +80,7 @@ DRIVER=~none
|
||||
|
||||
### FastAPI(默认)
|
||||
|
||||
**类型:**服务端驱动器
|
||||
**类型:**ASGI 服务端驱动器
|
||||
|
||||
> 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`
|
||||
|
||||
:::warning 警告
|
||||
:::caution 警告
|
||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||
|
||||
```bash
|
||||
@@ -185,7 +186,7 @@ nonebot.run(app="bot:app")
|
||||
|
||||
### Quart
|
||||
|
||||
**类型:**`ReverseDriver`
|
||||
**类型:**ASGI 服务端驱动器
|
||||
|
||||
> Quart is an asyncio reimplementation of the popular Flask microframework API.
|
||||
|
||||
@@ -199,7 +200,7 @@ DRIVER=~quart
|
||||
|
||||
##### `quart_reload`
|
||||
|
||||
:::warning 警告
|
||||
:::caution 警告
|
||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||
|
||||
```bash
|
||||
@@ -249,9 +250,9 @@ nonebot.run(app="bot:app")
|
||||
|
||||
### HTTPX
|
||||
|
||||
**类型:**`ForwardDriver`
|
||||
**类型:**HTTP 客户端驱动器
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。
|
||||
:::
|
||||
|
||||
@@ -263,9 +264,9 @@ DRIVER=~httpx
|
||||
|
||||
### websockets
|
||||
|
||||
**类型:**`ForwardDriver`
|
||||
**类型:**WebSocket 客户端驱动器
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。
|
||||
:::
|
||||
|
||||
@@ -277,7 +278,7 @@ DRIVER=~websockets
|
||||
|
||||
### AIOHTTP
|
||||
|
||||
**类型:**`ForwardDriver`
|
||||
**类型:**HTTP/WebSocket 客户端驱动器
|
||||
|
||||
> [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python.
|
||||
|
||||
|
@@ -4,8 +4,8 @@ description: 自定义事件响应器存储
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: advanced
|
||||
weight: 110
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 事件响应器存储
|
||||
|
@@ -4,8 +4,8 @@ description: 事件响应器组成与内置响应规则
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: advanced
|
||||
weight: 60
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 事件响应器进阶
|
||||
@@ -333,4 +333,6 @@ matcher2 = group.on_message()
|
||||
基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。
|
||||
标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。
|
||||
|
||||
详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna.md) 章节。
|
||||
该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。
|
||||
|
||||
详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna/README.mdx) 章节。
|
||||
|
@@ -4,8 +4,8 @@ description: 填写与获取插件相关的信息
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: advanced
|
||||
weight: 30
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 插件信息
|
||||
@@ -14,7 +14,7 @@ NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。
|
||||
|
||||
## 插件元数据
|
||||
|
||||
在 NoneBot 中,插件 [`Plugin`](../api/plugin/plugin.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。
|
||||
在 NoneBot 中,插件 [`Plugin`](../api/plugin/model.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。
|
||||
|
||||
现在,假设我们有一个插件 `example`, 它的模块结构如下:
|
||||
|
||||
|
@@ -4,8 +4,8 @@ description: 编写与加载嵌套插件
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: advanced
|
||||
weight: 40
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 嵌套插件
|
||||
|
@@ -4,8 +4,8 @@ description: 使用其他插件提供的功能
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: advanced
|
||||
weight: 50
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 跨插件访问
|
||||
|
@@ -4,15 +4,15 @@ description: 添加服务端路由规则
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: advanced
|
||||
weight: 100
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 添加路由
|
||||
|
||||
在[驱动器](./driver.md)一节中,我们了解了驱动器的两种类型。既然驱动器可以作为服务端运行,那么我们就可以向驱动器添加路由规则,从而实现自定义的 API 接口等功能。在添加路由规则时,我们需要注意驱动器的类型,详情可以参考[选择驱动器](./driver.md#配置驱动器)。
|
||||
|
||||
NoneBot 中,我们可以通过两种途径向驱动器添加路由规则:
|
||||
NoneBot 中,我们可以通过两种途径向 ASGI 驱动器添加路由规则:
|
||||
|
||||
1. 通过 NoneBot 的兼容层建立路由规则。
|
||||
2. 直接向 ASGI 应用添加路由规则。
|
||||
@@ -21,11 +21,12 @@ NoneBot 中,我们可以通过两种途径向驱动器添加路由规则:
|
||||
|
||||
在向驱动器添加路由规则时,我们需要注意驱动器是否为服务端类型,我们可以通过以下方式判断:
|
||||
|
||||
```python {3}
|
||||
```python
|
||||
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
|
||||
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:
|
||||
return Response(200, content="Hello, world!")
|
||||
|
||||
if isinstance((driver := get_driver()), ReverseDriver):
|
||||
if isinstance((driver := get_driver()), ASGIMixin):
|
||||
driver.setup_http_server(
|
||||
HTTPServerSetup(
|
||||
path=URL("/hello"),
|
||||
@@ -75,7 +76,7 @@ if isinstance((driver := get_driver()), ReverseDriver):
|
||||
|
||||
```python
|
||||
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):
|
||||
await ws.accept()
|
||||
@@ -91,7 +92,7 @@ async def ws_handler(ws: WebSocket):
|
||||
await websocket.close()
|
||||
# do some cleanup
|
||||
|
||||
if isinstance((driver := get_driver()), ReverseDriver):
|
||||
if isinstance((driver := get_driver()), ASGIMixin):
|
||||
driver.setup_websocket_server(
|
||||
WebSocketServerSetup(
|
||||
path=URL("/ws"),
|
||||
|
@@ -4,8 +4,8 @@ description: 在特定的生命周期中执行代码
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: advanced
|
||||
weight: 90
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 钩子函数
|
||||
|
@@ -4,8 +4,8 @@ description: 控制会话响应对象
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: advanced
|
||||
weight: 80
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 会话更新
|
||||
@@ -56,4 +56,4 @@ async def _(matcher: Matcher) -> Permission:
|
||||
|
||||
请注意,此处为全大写字母的 `USER` 权限,它可以匹配多个会话 ID。通过这种方式,我们可以实现多用户同时参与的会话。
|
||||
|
||||
我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store)。
|
||||
我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store/plugins)。
|
||||
|
@@ -4,14 +4,13 @@ description: 使用平台接口,完成更多功能
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: appendices
|
||||
weight: 50
|
||||
category: appendices
|
||||
---
|
||||
|
||||
# 使用平台接口
|
||||
|
||||
import Messenger from "@site/src/components/Messenger";
|
||||
import MarkdownText from "!!raw-loader!./assets/console-markdown.txt";
|
||||
import Messenger from "@/components/Messenger";
|
||||
|
||||
在 NoneBot 中,除了使用事件响应器操作发送文本消息外,我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口,完成发送特殊消息、获取信息等其他平台提供的功能。同时,在部分无法使用事件响应器的情况中,例如[定时任务](../best-practice/scheduler.md),我们也可以使用平台接口来完成需要的功能。
|
||||
|
||||
@@ -19,7 +18,7 @@ import MarkdownText from "!!raw-loader!./assets/console-markdown.txt";
|
||||
|
||||
在之前的章节中,我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md),现在我们来向用户发送平台特殊消息。
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
在以下的示例中,我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。
|
||||
:::
|
||||
|
||||
@@ -49,7 +48,11 @@ async def got_location(location: str = ArgPlainText()):
|
||||
{ position: "right", msg: "/天气" },
|
||||
{ position: "left", 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)
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现,请参考协议适配器以及平台文档。
|
||||
:::
|
||||
|
||||
|
@@ -1,6 +0,0 @@
|
||||
┏━━━━━━━━━━━━━━━━┓
|
||||
┃ 北京 ┃
|
||||
┗━━━━━━━━━━━━━━━━┛
|
||||
|
||||
• 今天
|
||||
⛅ 多云 20℃~24℃
|
@@ -4,8 +4,8 @@ description: 读取用户配置来控制插件行为
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: appendices
|
||||
weight: 10
|
||||
category: appendices
|
||||
---
|
||||
|
||||
# 配置
|
||||
@@ -62,7 +62,7 @@ export CUSTOM_CONFIG="config in environment variables"
|
||||
|
||||
那最终 NoneBot 所读取的内容为环境变量中的内容,即 `config in environment variables`。
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
NoneBot 不会自发读取未被定义的配置项的环境变量,如果需要读取某一环境变量需要在 dotenv 配置文件中进行声明。
|
||||
:::
|
||||
|
||||
@@ -482,7 +482,7 @@ nonebot.init(superusers={"123123123"})
|
||||
- **类型**: `set[str]`
|
||||
- **默认值**: `set()`
|
||||
|
||||
机器人昵称,通常协议适配器会根据用户是否 @user 或者是否以机器人昵称开头来判断是否是向机器人发送的消息。
|
||||
机器人昵称,通常协议适配器会根据用户是否 @bot 或者是否以机器人昵称开头来判断是否是向机器人发送的消息。
|
||||
|
||||
<Tabs groupId="configMethod">
|
||||
<TabItem value="dotenv" label="dotenv" default>
|
||||
|
@@ -4,8 +4,8 @@ description: 记录与控制日志
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: appendices
|
||||
weight: 70
|
||||
category: appendices
|
||||
---
|
||||
|
||||
# 日志
|
||||
|
@@ -4,8 +4,8 @@ description: 根据事件类型进行不同的处理
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: appendices
|
||||
weight: 80
|
||||
category: appendices
|
||||
---
|
||||
|
||||
# 事件类型与重载
|
||||
@@ -28,7 +28,7 @@ async def got_location(event: MessageEvent, location: str = ArgPlainText()):
|
||||
|
||||
在上面的代码中,我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
如果**基类**就能满足你的需求,那么就**不要修改**事件参数类型注解,这样可以使你的代码更加**通用**,可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理,我们将在[重载](#重载)一节中介绍。
|
||||
:::
|
||||
|
||||
@@ -63,7 +63,7 @@ async def handle_onebot(bot: OneBot):
|
||||
await bot.send_group_message(group_id=123123, message="OneBot")
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。
|
||||
|
||||
但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。
|
||||
|
@@ -4,8 +4,8 @@ description: 控制事件响应器的权限
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: appendices
|
||||
weight: 60
|
||||
category: appendices
|
||||
---
|
||||
|
||||
# 权限控制
|
||||
|
@@ -4,8 +4,8 @@ description: 自定义响应规则
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: appendices
|
||||
weight: 20
|
||||
category: appendices
|
||||
---
|
||||
|
||||
# 响应规则
|
||||
|
@@ -4,8 +4,8 @@ description: 更灵活的会话控制
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: appendices
|
||||
weight: 30
|
||||
category: appendices
|
||||
---
|
||||
|
||||
# 会话控制
|
||||
@@ -322,7 +322,7 @@ async def _(matcher: Matcher):
|
||||
matcher.stop_propagation()
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
:::caution 注意
|
||||
`stop_propagation` 操作是实例方法,需要先通过依赖注入获取事件响应器实例再进行调用。
|
||||
:::
|
||||
|
||||
@@ -374,6 +374,14 @@ async def _(matcher: Matcher):
|
||||
|
||||
`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
|
||||
|
||||
设置 / 覆盖一个 `receive` 接收的事件。
|
||||
|
@@ -4,8 +4,8 @@ description: 会话状态信息
|
||||
|
||||
options:
|
||||
menu:
|
||||
- category: appendices
|
||||
weight: 40
|
||||
category: appendices
|
||||
---
|
||||
|
||||
# 会话状态
|
||||
|
@@ -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)
|
124
website/docs/best-practice/alconna/README.mdx
Normal file
124
website/docs/best-practice/alconna/README.mdx
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: Alconna 命令解析拓展
|
||||
|
||||
slug: /best-practice/alconna/
|
||||
---
|
||||
|
||||
# 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#安装插件)来了解并选择安装插件的方式。如:
|
||||
|
||||
在**项目目录**下执行以下命令:
|
||||
|
||||
```shell
|
||||
nb plugin install nonebot-plugin-alconna
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```shell
|
||||
pip install nonebot-plugin-alconna
|
||||
```
|
||||
|
||||
## 导入插件
|
||||
|
||||
由于 `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, AlconnaMatcher, 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(matcher: AlconnaMatcher, location: Match[str]):
|
||||
if location.available:
|
||||
matcher.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
|
||||
}
|
494
website/docs/best-practice/alconna/command.md
Normal file
494
website/docs/best-practice/alconna/command.md
Normal file
@@ -0,0 +1,494 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
description: Alconna 基本介绍
|
||||
---
|
||||
|
||||
# Alconna 命令解析
|
||||
|
||||
[Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器,
|
||||
是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。
|
||||
|
||||
特点包括:
|
||||
|
||||
- 高效
|
||||
- 直观的命令组件创建方式
|
||||
- 强大的类型解析与类型转换功能
|
||||
- 自定义的帮助信息格式
|
||||
- 多语言支持
|
||||
- 易用的快捷命令创建与使用
|
||||
- 可创建命令补全会话, 以实现多轮连续的补全提示
|
||||
- 可嵌套的多级子命令
|
||||
- 正则匹配支持
|
||||
|
||||
## 命令编写
|
||||
|
||||
### 命令头
|
||||
|
||||
命令头是指命令的前缀 (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` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔:
|
||||
|
||||
```python
|
||||
from arclet.alconna import Alconna, Option, CommandMeta, Args
|
||||
|
||||
alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True))
|
||||
|
||||
assert alc.parse("test123 BARabc").matched
|
||||
```
|
||||
|
||||
这使得我们可以实现如下命令:
|
||||
|
||||
```python
|
||||
>>> from arclet.alconna import Alconna, Option, Args, append
|
||||
>>> alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True))
|
||||
>>> alc.parse("gcc -Fabc -Fdef -Fxyz").query[list[str]]("flag.content")
|
||||
['abc', 'def', 'xyz']
|
||||
```
|
||||
|
||||
当 `Option` 的 `action` 为 `count` 时,其自动支持 `compact` 特性:
|
||||
|
||||
```python
|
||||
>>> from arclet.alconna import Alconna, Option, Args, count
|
||||
>>> alc = Alconna("pp", Option("--verbose|-v", action=count, default=0))
|
||||
>>> alc.parse("pp -vvv").query[int]("verbose.value")
|
||||
3
|
||||
```
|
||||
|
||||
## 命令特性
|
||||
|
||||
### 配置
|
||||
|
||||
`arclet.alconna.Namespace` 表示某一命名空间下的默认配置:
|
||||
|
||||
```python
|
||||
from arclet.alconna import config, namespace, Namespace
|
||||
from arclet.alconna.tools import ShellTextFormatter
|
||||
|
||||
|
||||
np = Namespace("foo", prefixes=["/"]) # 创建 Namespace 对象,并进行初始配置
|
||||
|
||||
with namespace("bar") as np1:
|
||||
np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令
|
||||
np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter
|
||||
np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称
|
||||
|
||||
config.namespaces["foo"] = np # 将命名空间挂载到 config 上
|
||||
```
|
||||
|
||||
同时也提供了默认命名空间配置与修改方法:
|
||||
|
||||
```python
|
||||
from arclet.alconna import config, namespace, Namespace
|
||||
|
||||
|
||||
config.default_namespace.prefixes = [...] # 直接修改默认配置
|
||||
|
||||
np = Namespace("xxx", prefixes=[...])
|
||||
config.default_namespace = np # 更换默认的命名空间
|
||||
|
||||
with namespace(config.default_namespace.name) as np:
|
||||
np.prefixes = [...]
|
||||
```
|
||||
|
||||
### 半自动补全
|
||||
|
||||
半自动补全为用户提供了推荐后续输入的功能。
|
||||
|
||||
补全默认通过 `--comp` 或 `-cp` 或 `?` 触发:(命名空间配置可修改名称)
|
||||
|
||||
```python
|
||||
from arclet.alconna import Alconna, Args, Option
|
||||
|
||||
alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar")
|
||||
alc.parse("test ?")
|
||||
|
||||
'''
|
||||
output
|
||||
|
||||
以下是建议的输入:
|
||||
* <abc: int>
|
||||
* --help
|
||||
* -h
|
||||
* -sct
|
||||
* --shortcut
|
||||
* foo
|
||||
* bar
|
||||
'''
|
||||
```
|
||||
|
||||
### 快捷指令
|
||||
|
||||
快捷指令顾名思义,可以为基础指令创建便捷的触发方式
|
||||
|
||||
一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建,删除);
|
||||
|
||||
```python
|
||||
>>> from arclet.alconna import Alconna, Args
|
||||
>>> alc = Alconna("setu", Args["count", int])
|
||||
>>> alc.shortcut("涩图(\d+)张", {"args": ["{0}"]})
|
||||
'Alconna::setu 的快捷指令: "涩图(\\d+)张" 添加成功'
|
||||
>>> alc.parse("涩图3张").query("count")
|
||||
3
|
||||
```
|
||||
|
||||
`shortcut` 的第一个参数为快捷指令名称,第二个参数为 `ShortcutArgs`,作为快捷指令的配置
|
||||
|
||||
```python
|
||||
class ShortcutArgs(TypedDict):
|
||||
"""快捷指令参数"""
|
||||
|
||||
command: NotRequired[DataCollection[Any]]
|
||||
"""快捷指令的命令"""
|
||||
args: NotRequired[list[Any]]
|
||||
"""快捷指令的附带参数"""
|
||||
fuzzy: NotRequired[bool]
|
||||
"""是否允许命令后随参数"""
|
||||
prefix: NotRequired[bool]
|
||||
"""是否调用时保留指令前缀"""
|
||||
```
|
||||
|
||||
当 `fuzzy` 为 False 时,传入 `"涩图1张 abc"` 之类的快捷指令将视为解析失败
|
||||
|
||||
快捷指令允许三类特殊的 placeholder:
|
||||
|
||||
- `{%X}`: 如 `setu {%0}`,表示此处必须填入快捷指令后随的第 X 个参数。
|
||||
|
||||
例如,若快捷指令为 `涩图`, 配置为 `{"command": "setu {%0}"}`, 则指令 `涩图 1` 相当于 `setu 1`
|
||||
|
||||
- `{*}`: 表示此处填入所有后随参数,并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。
|
||||
- `{X}`: 表示此处填入可能的正则匹配的组:
|
||||
- 若 `command` 中存在匹配组 `(xxx)`,则 `{X}` 表示第 X 个匹配组的内容
|
||||
- 若 `command` 中存储匹配组 `(?P<xxx>...)`, 则 `{X}` 表示名字为 X 的匹配结果
|
||||
|
||||
除此之外, 通过内置选项 `--shortcut` 可以动态操作快捷指令。
|
||||
|
||||
例如:
|
||||
|
||||
- `cmd --shortcut <key> <cmd>` 来增加一个快捷指令
|
||||
- `cmd --shortcut list` 来列出当前指令的所有快捷指令
|
||||
- `cmd --shortcut delete key` 来删除一个快捷指令
|
||||
|
||||
### 使用模糊匹配
|
||||
|
||||
模糊匹配通过在 Alconna 中设置其 CommandMeta 开启。
|
||||
|
||||
模糊匹配会应用在任意需要进行名称判断的地方,如**命令名称**,**选项名称**和**参数名称**(如指定需要传入参数名称)。
|
||||
|
||||
```python
|
||||
from arclet.alconna import Alconna, CommandMeta
|
||||
|
||||
alc = Alconna("test_fuzzy", meta=CommandMeta(fuzzy_match=True))
|
||||
alc.parse("test_fuzy")
|
||||
# output: test_fuzy is not matched. Do you mean "test_fuzzy"?
|
||||
```
|
||||
|
||||
## 解析结果
|
||||
|
||||
`Alconna.parse` 会返回由 **Arparma** 承载的解析结果。
|
||||
|
||||
`Arpamar` 会有如下参数:
|
||||
|
||||
- 调试类
|
||||
|
||||
- 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
|
||||
```
|
55
website/docs/best-practice/alconna/config.md
Normal file
55
website/docs/best-practice/alconna/config.md
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
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_param
|
||||
|
||||
- **类型**: `bool`
|
||||
- **默认值**: `True`
|
||||
|
||||
是否使用特制的 Param 提供更好的依赖注入,该选项不会对使用依赖注入函数形式造成影响
|
||||
|
||||
## alconna_use_command_sep
|
||||
|
||||
- **类型**: `bool`
|
||||
- **默认值**: `False`
|
||||
|
||||
是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符
|
||||
|
||||
## alconna_global_extensions
|
||||
|
||||
- **类型**: `List[str]`
|
||||
- **默认值**: `[]`
|
||||
|
||||
全局加载的扩展, 路径以 . 分隔, 如 foo.bar.baz:DemoExtension
|
406
website/docs/best-practice/alconna/matcher.md
Normal file
406
website/docs/best-practice/alconna/matcher.md
Normal file
@@ -0,0 +1,406 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
description: 响应规则的使用
|
||||
---
|
||||
|
||||
# Alconna 响应规则
|
||||
|
||||
以下为一个使用示例:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.adapters.onebot12 import Image
|
||||
from nonebot_plugin_alconna import At, AlconnaMatches, on_alconna
|
||||
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"),
|
||||
Option("icon", Args["icon", Image])
|
||||
)
|
||||
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(Image(img))
|
||||
if result.find("add"):
|
||||
group = await create_role_group(result.query[str]("add.name"))
|
||||
if result.find("add.member"):
|
||||
ats = result.query[tuple[At, ...]]("add.member.target")
|
||||
group.extend(member.target for member in ats)
|
||||
await rg.finish("添加成功")
|
||||
```
|
||||
|
||||
## 响应器使用
|
||||
|
||||
`on_alconna` 的所有参数如下:
|
||||
|
||||
- `command: Alconna | str`: Alconna 命令
|
||||
- `skip_for_unmatch: bool = True`: 是否在命令不匹配时跳过该响应
|
||||
- `auto_send_output: bool = False`: 是否自动发送输出信息并跳过响应
|
||||
- `aliases: set[str | tuple[str, ...]] | None = None`: 命令别名, 作用类似于 `on_command` 中的 aliases
|
||||
- `comp_config: CompConfig | None = None`: 补全会话配置, 不传入则不启用补全会话
|
||||
- `extensions: list[type[Extension] | Extension] | None = None`: 需要加载的匹配扩展, 可以是扩展类或扩展实例
|
||||
- `exclude_ext: list[type[Extension] | str] | None = None`: 需要排除的匹配扩展, 可以是扩展类或扩展的id
|
||||
- `use_origin: bool = False`: 是否使用未经 to_me 等处理过的消息
|
||||
- `use_cmd_start: bool = False`: 是否使用 COMMAND_START 作为命令前缀
|
||||
- `use_cmd_sep: bool = False`: 是否使用 COMMAND_SEP 作为命令分隔符
|
||||
|
||||
`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher`,其拓展了如下方法:
|
||||
|
||||
- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理
|
||||
- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上,会以 path 对应的参数为准,读取传入 message 的最后一个消息段并验证转换
|
||||
- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`,为 `got_path` 的特化版本
|
||||
- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`,对应 `got_path`
|
||||
- `.dispatch`: 同样的分派处理,但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`
|
||||
- `.got`, `send`, `reject`, ...: 拓展了 prompt 类型,即支持使用 `UniMessage` 作为 prompt
|
||||
|
||||
用例:
|
||||
|
||||
```python
|
||||
from arclet.alconna import Alconna, Option, Args
|
||||
from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match, AlconnaMatcher, AlconnaArg, UniMessage
|
||||
|
||||
login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall")))
|
||||
|
||||
@login.assign("recall")
|
||||
async def login_exit():
|
||||
await login.finish("已退出")
|
||||
|
||||
@login.assign("password")
|
||||
async def login_handle(matcher: AlconnaMatcher, pw: Match[str] = AlconnaMatch("password")):
|
||||
matcher.set_path_arg("password", pw.result)
|
||||
|
||||
@login.got_path("password", prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请输入密码"))
|
||||
async def login_got(password: str = AlconnaArg("password")):
|
||||
assert password
|
||||
await login.send("登录成功")
|
||||
```
|
||||
|
||||
## 依赖注入
|
||||
|
||||
`Alconna` 的解析结果会放入 `Arparma` 类中,或用户指定的 `Duplication` 类。
|
||||
|
||||
`nonebot_plugin_alconna` 提供了一系列的依赖注入函数,他们包括:
|
||||
|
||||
- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
|
||||
- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
|
||||
- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
|
||||
- `AlconnaMatch`: `Match` 类型的依赖注入函数,其能够额外传入一个 middleware 函数来处理得到的参数
|
||||
- `AlconnaQuery`: `Query` 类型的依赖注入函数,其能够额外传入一个 middleware 函数来处理得到的参数
|
||||
- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数
|
||||
- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数
|
||||
|
||||
可以看到,本插件提供了几类额外的模型:
|
||||
|
||||
- `CommandResult`: 解析结果,包括了源命令 `source: Alconna` ,解析结果 `result: Arparma`,以及可能的输出信息 `output: str | None` 字段
|
||||
- `Match`: 匹配项,表示参数是否存在于 `all_matched_args` 内,可用 `Match.available` 判断是否匹配,`Match.result` 获取匹配的值
|
||||
- `Query`: 查询项,表示参数是否可由 `Arparma.query` 查询并获得结果,可用 `Query.available` 判断是否查询成功,`Query.result` 获取查询结果
|
||||
|
||||
同时,基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了三类注解:
|
||||
|
||||
- `AlcMatches`:同 `AlconnaMatches`
|
||||
- `AlcResult`:同 `AlconnaResult`
|
||||
- `AlcExecResult`: 同 `AlconnaExecResult`
|
||||
|
||||
而若设置配置项 **ALCONNA_USE_PARAM** (默认为 True) 为 True,则上述依赖注入的目标参数皆不需要使用依赖注入函数:
|
||||
|
||||
```python
|
||||
...
|
||||
@cmd.handle()
|
||||
async def handle1(
|
||||
result: CommandResult = AlconnaResult(),
|
||||
arp: Arparma = AlconnaMatches(),
|
||||
dup: Duplication = AlconnaDuplication(Duplication),
|
||||
ext: Extension = AlconnaExtension(Extension),
|
||||
foo: Match[str] = AlconnaMatch("foo"),
|
||||
bar: Query[int] = AlconnaQuery("ttt.bar", 0)
|
||||
):
|
||||
...
|
||||
|
||||
# ALCONNA_USE_PARAM 为 True 后
|
||||
|
||||
@cmd.handle()
|
||||
async def handle2(
|
||||
result: CommandResult,
|
||||
arp: Arparma,
|
||||
dup: Duplication,
|
||||
ext: Extension,
|
||||
source: Alconna,
|
||||
abc: str, # 类似 Match, 但是若匹配结果不存在对应字段则跳过该 handler
|
||||
foo: Match[str],
|
||||
bar: Query[int] = Query("ttt.bar", 0) # Query 仍然需要一个默认值来传递 path 参数
|
||||
):
|
||||
...
|
||||
```
|
||||
|
||||
该效果对于 `got_path` 下的 Arg 同样有效
|
||||
|
||||
实例:
|
||||
|
||||
```python
|
||||
...
|
||||
from nonebot import require
|
||||
require("nonebot_plugin_alconna")
|
||||
...
|
||||
|
||||
from nonebot_plugin_alconna import (
|
||||
on_alconna,
|
||||
Match,
|
||||
Query,
|
||||
AlconnaQuery,
|
||||
AlcResult
|
||||
)
|
||||
from arclet.alconna import Alconna, Args, Option, Arparma
|
||||
|
||||
test = on_alconna(
|
||||
Alconna(
|
||||
"test",
|
||||
Option("foo", Args["bar", int]),
|
||||
Option("baz", Args["qux", bool, False])
|
||||
),
|
||||
auto_send_output=True
|
||||
)
|
||||
|
||||
|
||||
@test.handle()
|
||||
async def handle_test1(result: AlcResult):
|
||||
await test.send(f"matched: {result.matched}")
|
||||
await test.send(f"maybe output: {result.output}")
|
||||
|
||||
@test.handle()
|
||||
async def handle_test2(result: Arparma):
|
||||
await test.send(f"head result: {result.header_result}")
|
||||
await test.send(f"args: {result.all_matched_args}")
|
||||
|
||||
@test.handle()
|
||||
async def handle_test3(bar: Match[int]):
|
||||
if bar.available:
|
||||
await test.send(f"foo={bar.result}")
|
||||
|
||||
@test.handle()
|
||||
async def handle_test4(qux: Query[bool] = AlconnaQuery("baz.qux", False)):
|
||||
if qux.available:
|
||||
await test.send(f"baz.qux={qux.result}")
|
||||
```
|
||||
|
||||
## 消息段标注
|
||||
|
||||
示例中使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。
|
||||
|
||||
适配器下的消息段标注会匹配特定的 `MessageSegment`:
|
||||
|
||||
而通用标注与适配器标注的区别在于,通用标注会匹配多个适配器中相似类型的消息段,并返回
|
||||
`nonebot_plugin_alconna.uniseg` 中定义的 [`Segment` 模型](./utils.md#通用消息段)
|
||||
|
||||
例如:
|
||||
|
||||
```python
|
||||
...
|
||||
ats = result.query[tuple[At, ...]]("add.member.target")
|
||||
group.extend(member.target for member in ats)
|
||||
```
|
||||
|
||||
这样插件使用者就不用考虑平台之间字段的差异
|
||||
|
||||
本插件为以下适配器提供了专门的适配器标注:
|
||||
|
||||
| 协议名称 | 路径 |
|
||||
| ------------------------------------------------------------------- | ------------------------------------ |
|
||||
| [OneBot 协议](https://github.com/nonebot/adapter-onebot) | adapters.onebot11, adapters.onebot12 |
|
||||
| [Telegram](https://github.com/nonebot/adapter-telegram) | adapters.telegram |
|
||||
| [飞书](https://github.com/nonebot/adapter-feishu) | adapters.feishu |
|
||||
| [GitHub](https://github.com/nonebot/adapter-github) | adapters.github |
|
||||
| [QQ bot](https://github.com/nonebot/adapter-qq) | adapters.qq |
|
||||
| [QQ 频道bot](https://github.com/nonebot/adapter-qq) | adapters.qqguild |
|
||||
| [钉钉](https://github.com/nonebot/adapter-ding) | adapters.ding |
|
||||
| [Console](https://github.com/nonebot/adapter-console) | adapters.console |
|
||||
| [开黑啦](https://github.com/Tian-que/nonebot-adapter-kaiheila) | adapters.kook |
|
||||
| [Mirai](https://github.com/ieew/nonebot_adapter_mirai2) | adapters.mirai |
|
||||
| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat) | adapters.ntchat |
|
||||
| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft |
|
||||
| [BiliBili Live](https://github.com/wwweww/adapter-bilibili) | adapters.bilibili |
|
||||
| [Walle-Q](https://github.com/onebot-walle/nonebot_adapter_walleq) | adapters.onebot12 |
|
||||
| [Villa](https://github.com/CMHopeSunshine/nonebot-adapter-villa) | adapters.villa |
|
||||
| [Discord](https://github.com/nonebot/adapter-discord) | adapters.discord |
|
||||
| [Red 协议](https://github.com/nonebot/adapter-red) | adapters.red |
|
||||
|
||||
## 条件控制
|
||||
|
||||
本插件可以通过 `assign` 来控制一个具体的响应函数是否在不满足条件时跳过响应。
|
||||
|
||||
```python
|
||||
...
|
||||
from nonebot import require
|
||||
require("nonebot_plugin_alconna")
|
||||
...
|
||||
|
||||
from arclet.alconna import Alconna, Subcommand, Option, Args
|
||||
from nonebot_plugin_alconna import on_alconna, CommandResult
|
||||
|
||||
pip = Alconna(
|
||||
"pip",
|
||||
Subcommand(
|
||||
"install", Args["pak", str],
|
||||
Option("--upgrade"),
|
||||
Option("--force-reinstall")
|
||||
),
|
||||
Subcommand("list", Option("--out-dated"))
|
||||
)
|
||||
|
||||
pip_cmd = on_alconna(pip)
|
||||
|
||||
@pip_cmd.assign("install.pak", "pip")
|
||||
async def update(res: CommandResult):
|
||||
...
|
||||
|
||||
# 仅在命令为 `pip list` 时响应
|
||||
@pip_cmd.assign("list")
|
||||
async def list_(res: CommandResult):
|
||||
...
|
||||
|
||||
# 仅在命令为 `pip install` 时响应
|
||||
@pip_cmd.assign("install")
|
||||
async def install(res: CommandResult):
|
||||
...
|
||||
```
|
||||
|
||||
此外,使用 `AlconnaMatcher.dispatch` 还能像 `CommandGroup` 一样为每个分发设置独立的 matcher:
|
||||
|
||||
```python
|
||||
update_cmd = pip_cmd.dispatch("install.pak", "pip")
|
||||
|
||||
@update_cmd.handle()
|
||||
async def update(arp: CommandResult = AlconnaResult()):
|
||||
...
|
||||
```
|
||||
|
||||
另外,`AlconnaMatcher` 有类似于 `got` 的 `got_path`:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna
|
||||
|
||||
test_cmd = on_alconna(Alconna("test", Args["target?", Union[str, At]]))
|
||||
|
||||
@test_cmd.handle()
|
||||
async def tt_h(matcher: AlconnaMatcher, target: Match[Union[str, At]]):
|
||||
if target.available:
|
||||
matcher.set_path_arg("target", target.result)
|
||||
|
||||
@test_cmd.got_path("target", prompt="请输入目标")
|
||||
async def tt(target: Union[str, At]):
|
||||
await test_cmd.send(UniMessage(["ok\n", target]))
|
||||
```
|
||||
|
||||
`got_path` 与 `assign`, `Match`, `Query` 等地方一样,都需要指明 `path` 参数 (即对应 Arg 验证的路径)
|
||||
|
||||
`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型,例如示例中 `target` 对应的 Arg 里要求 str 或 At,则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数。
|
||||
|
||||
:::tip
|
||||
|
||||
`path` 支持 ~XXX 语法,其会把 ~ 替换为可能的父级路径:
|
||||
|
||||
```python
|
||||
pip = Alconna(
|
||||
"pip",
|
||||
Subcommand(
|
||||
"install",
|
||||
Args["pak", str],
|
||||
Option("--upgrade|-U"),
|
||||
Option("--force-reinstall"),
|
||||
),
|
||||
Subcommand("list", Option("--out-dated")),
|
||||
)
|
||||
|
||||
pipcmd = on_alconna(pip)
|
||||
pip_install_cmd = pipcmd.dispatch("install")
|
||||
|
||||
|
||||
@pip_install_cmd.assign("~upgrade")
|
||||
async def pip1_u(pak: Query[str] = Query("~pak")):
|
||||
await pip_install_cmd.finish(f"pip upgrading {pak.result}...")
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 匹配拓展
|
||||
|
||||
本插件提供了一个 `Extension` 类,其用于自定义 AlconnaMatcher 的部分行为。
|
||||
|
||||
例如 `LLMExtension` (仅举例):
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import Extension, Alconna, on_alconna
|
||||
|
||||
class LLMExtension(Extension):
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
return 10
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return "LLMExtension"
|
||||
|
||||
def __init__(self, llm):
|
||||
self.llm = llm
|
||||
|
||||
def post_init(self, alc: Alconna) -> None:
|
||||
self.llm.add_context(alc.command, alc.meta.description)
|
||||
|
||||
async def receive_wrapper(self, bot, event, receive):
|
||||
resp = await self.llm.input(str(receive))
|
||||
return receive.__class__(resp.content)
|
||||
|
||||
matcher = on_alconna(
|
||||
Alconna(...),
|
||||
extensions=[LLMExtension(LLM)]
|
||||
)
|
||||
...
|
||||
```
|
||||
|
||||
那么使用了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息。
|
||||
|
||||
目前 `Extension` 的功能有:
|
||||
|
||||
- 对于事件的来源适配器或 bot 选择是否接受响应
|
||||
- 输出信息的自定义转换方法
|
||||
- 从传入事件中自定义提取消息的方法
|
||||
- 对于传入的alc对象的追加的自定义处理
|
||||
- 对传入的消息 (Message 或 UniMessage) 的额外处理
|
||||
- 对命令解析结果的额外处理
|
||||
- 对发送的消息 (Message 或 UniMessage) 的额外处理
|
||||
- 自定义额外的matcher api
|
||||
|
||||
例如内置的 `DiscordSlashExtension`,其可自动将 Alconna 对象翻译成 slash指令并注册,且将收到的指令交互事件转为指令供命令解析:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import Match, on_alconna
|
||||
|
||||
from nonebot_plugin_alconna.adapters.discord import DiscordSlashExtension
|
||||
|
||||
alc = Alconna(
|
||||
["/"],
|
||||
"permission",
|
||||
Subcommand("add", Args["plugin", str]["priority?", int]),
|
||||
Option("remove", Args["plugin", str]["time?", int]),
|
||||
meta=CommandMeta(description="权限管理"),
|
||||
)
|
||||
|
||||
matcher = on_alconna(alc, extensions=[DiscordSlashExtension()])
|
||||
|
||||
@matcher.assign("add")
|
||||
async def add(plugin: Match[str], priority: Match[int]):
|
||||
await matcher.finish(f"added {plugin.result} with {priority.result if priority.available else 0}")
|
||||
|
||||
@matcher.assign("remove")
|
||||
async def remove(plugin: Match[str], time: Match[int]):
|
||||
await matcher.finish(f"removed {plugin.result} with {time.result if time.available else -1}")
|
||||
```
|
299
website/docs/best-practice/alconna/uniseg.md
Normal file
299
website/docs/best-practice/alconna/uniseg.md
Normal file
@@ -0,0 +1,299 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
description: 通用消息组件
|
||||
---
|
||||
|
||||
# 通用消息组件
|
||||
|
||||
`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件,其提供了一套通用的消息组件,用于在 `nonebot-plugin-alconna` 下构建通用消息。
|
||||
|
||||
## 通用消息段
|
||||
|
||||
`nonebot-plugin-alconna.uniseg` 提供了类似 `MessageSegment` 的通用消息段,并可在 `Alconna` 下直接标注使用:
|
||||
|
||||
```python
|
||||
class Segment:
|
||||
"""基类标注"""
|
||||
|
||||
class Text(Segment):
|
||||
"""Text对象, 表示一类文本元素"""
|
||||
text: str
|
||||
style: Optional[str]
|
||||
|
||||
class At(Segment):
|
||||
"""At对象, 表示一类提醒某用户的元素"""
|
||||
type: Literal["user", "role", "channel"]
|
||||
target: str
|
||||
|
||||
class AtAll(Segment):
|
||||
"""AtAll对象, 表示一类提醒所有人的元素"""
|
||||
|
||||
class Emoji(Segment):
|
||||
"""Emoji对象, 表示一类表情元素"""
|
||||
id: str
|
||||
name: Optional[str]
|
||||
|
||||
class Media(Segment):
|
||||
url: Optional[str]
|
||||
id: Optional[str]
|
||||
path: Optional[str]
|
||||
raw: Optional[bytes]
|
||||
|
||||
class Image(Media):
|
||||
"""Image对象, 表示一类图片元素"""
|
||||
|
||||
class Audio(Media):
|
||||
"""Audio对象, 表示一类音频元素"""
|
||||
|
||||
class Voice(Media):
|
||||
"""Voice对象, 表示一类语音元素"""
|
||||
|
||||
class Video(Media):
|
||||
"""Video对象, 表示一类视频元素"""
|
||||
|
||||
class File(Segment):
|
||||
"""File对象, 表示一类文件元素"""
|
||||
id: str
|
||||
name: Optional[str]
|
||||
|
||||
class Reply(Segment):
|
||||
"""Reply对象,表示一类回复消息"""
|
||||
origin: Any
|
||||
id: str
|
||||
msg: Optional[Union[Message, str]]
|
||||
|
||||
class Card(Segment):
|
||||
type: Literal["xml", "json"]
|
||||
raw: str
|
||||
|
||||
class Other(Segment):
|
||||
"""其他 Segment"""
|
||||
```
|
||||
|
||||
来自各自适配器的消息序列都会经过这些通用消息段对应的标注转换,以达到跨平台接收消息的作用
|
||||
|
||||
## 通用消息序列
|
||||
|
||||
`nonebot-plugin-alconna.uniseg` 同时提供了一个类似于 `Message` 的 `UniMessage` 类型,其元素为经过通用标注转换后的通用消息段。
|
||||
|
||||
你可以通过提供的 `UniversalMessage` 或 `UniMsg` 依赖注入器来获取 `UniMessage`。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply
|
||||
|
||||
matcher = on_xxx(...)
|
||||
|
||||
@matcher.handle()
|
||||
async def _(msg: UniMsg):
|
||||
reply = msg[Reply, 0]
|
||||
print(reply.origin)
|
||||
if msg.has(At):
|
||||
ats = msg.get(At)
|
||||
print(ats)
|
||||
...
|
||||
```
|
||||
|
||||
不仅如此,你还可以通过 `UniMessage` 的 `export` 方法来**跨平台发送消息**。
|
||||
|
||||
`UniMessage.export` 会通过传入的 `bot: Bot` 参数读取适配器信息,并使用对应的生成方法把通用消息转为适配器对应的消息序列:
|
||||
|
||||
```python
|
||||
from nonebot import Bot, on_command
|
||||
from nonebot_plugin_alconna.uniseg import Image, UniMessage
|
||||
|
||||
test = on_command("test")
|
||||
|
||||
@test.handle()
|
||||
async def handle_test(bot: Bot):
|
||||
await test.send(await UniMessage(Image(path="path/to/img")).export(bot))
|
||||
```
|
||||
|
||||
而在 `AlconnaMatcher` 下,`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`,不需要手动调用 export 方法:
|
||||
|
||||
```python
|
||||
from arclet.alconna import Alconna, Args
|
||||
from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna
|
||||
from nonebot_plugin_alconna.uniseg import At, UniMessage
|
||||
|
||||
test_cmd = on_alconna(Alconna("test", Args["target?", At]))
|
||||
|
||||
@test_cmd.handle()
|
||||
async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
|
||||
if target.available:
|
||||
matcher.set_path_arg("target", target.result)
|
||||
|
||||
@test_cmd.got_path("target", prompt="请输入目标")
|
||||
async def tt(target: At):
|
||||
await test_cmd.send(UniMessage([target, "\ndone."]))
|
||||
```
|
||||
|
||||
### 获取消息纯文本
|
||||
|
||||
类似于 `Message.extract_plain_text()`,用于获取通用消息的纯文本。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.uniseg import UniMessage, At
|
||||
# 提取消息纯文本字符串
|
||||
assert UniMessage(
|
||||
[At("user", "1234"), "text"]
|
||||
).extract_plain_text() == "text"
|
||||
```
|
||||
|
||||
### 遍历
|
||||
|
||||
通用消息序列继承自 `List[Segment]` ,因此可以使用 `for` 循环遍历消息段。
|
||||
|
||||
```python
|
||||
for segment in message: # type: Segment
|
||||
...
|
||||
```
|
||||
|
||||
### 检查消息段
|
||||
|
||||
我们可以通过 `in` 运算符或消息序列的 `has` 方法来:
|
||||
|
||||
```python
|
||||
# 是否存在消息段
|
||||
At("user", "1234") in message
|
||||
# 是否存在指定类型的消息段
|
||||
At in message
|
||||
```
|
||||
|
||||
我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段。
|
||||
|
||||
```python
|
||||
# 是否都为 "test"
|
||||
message.only("test")
|
||||
# 是否仅包含指定类型的消息段
|
||||
message.only(Text)
|
||||
```
|
||||
|
||||
### 过滤、索引与切片
|
||||
|
||||
消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.uniseg import UniMessage, At, Text, Reply
|
||||
|
||||
message = UniMessage(
|
||||
[
|
||||
Reply(...),
|
||||
"text1",
|
||||
At("user", "1234"),
|
||||
"text2"
|
||||
]
|
||||
)
|
||||
# 索引
|
||||
message[0] == Reply(...)
|
||||
# 切片
|
||||
message[0:2] == UniMessage([Reply(...), Text("text1")])
|
||||
# 类型过滤
|
||||
message[At] == Message([At("user", "1234")])
|
||||
# 类型索引
|
||||
message[At, 0] == At("user", "1234")
|
||||
# 类型切片
|
||||
message[Text, 0:2] == UniMessage([Text("text1"), Text("text2")])
|
||||
```
|
||||
|
||||
我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤。
|
||||
|
||||
```python
|
||||
message.include(Text, At)
|
||||
message.exclude(Reply)
|
||||
```
|
||||
|
||||
同样的,消息序列对列表的 `index`、`count` 方法也进行了增强,可以用于索引指定类型的消息段。
|
||||
|
||||
```python
|
||||
# 指定类型首个消息段索引
|
||||
message.index(Text) == 1
|
||||
# 指定类型消息段数量
|
||||
message.count(Text) == 2
|
||||
```
|
||||
|
||||
此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段。
|
||||
|
||||
```python
|
||||
# 获取指定类型指定个数的消息段
|
||||
message.get(Text, 1) == UniMessage([Text("test1")])
|
||||
```
|
||||
|
||||
### 拼接消息
|
||||
|
||||
`str`、`UniMessage`、`Segment` 对象之间可以直接相加,相加均会返回一个新的 `UniMessage` 对象。
|
||||
|
||||
```python
|
||||
# 消息序列与消息段相加
|
||||
UniMessage("text") + Text("text")
|
||||
# 消息序列与字符串相加
|
||||
UniMessage([Text("text")]) + "text"
|
||||
# 消息序列与消息序列相加
|
||||
UniMessage("text") + UniMessage([Text("text")])
|
||||
# 字符串与消息序列相加
|
||||
"text" + UniMessage([Text("text")])
|
||||
# 消息段与消息段相加
|
||||
Text("text") + Text("text")
|
||||
# 消息段与字符串相加
|
||||
Text("text") + "text"
|
||||
# 消息段与消息序列相加
|
||||
Text("text") + UniMessage([Text("text")])
|
||||
# 字符串与消息段相加
|
||||
"text" + Text("text")
|
||||
```
|
||||
|
||||
如果需要在当前消息序列后直接拼接新的消息段,可以使用 `Message.append`、`Message.extend` 方法,或者使用自加。
|
||||
|
||||
```python
|
||||
msg = UniMessage([Text("text")])
|
||||
# 自加
|
||||
msg += "text"
|
||||
msg += Text("text")
|
||||
msg += UniMessage([Text("text")])
|
||||
# 附加
|
||||
msg.append(Text("text"))
|
||||
# 扩展
|
||||
msg.extend([Text("text")])
|
||||
```
|
||||
|
||||
### 使用消息模板
|
||||
|
||||
`UniMessage.template` 同样类似于 `Message.template`,可以用于格式化消息。大体用法参考 [消息模板](../../tutorial/message#使用消息模板)。
|
||||
|
||||
这里额外说明 `UniMessage.template` 的拓展控制符
|
||||
|
||||
相比 `Message`,UniMessage 对于 {:XXX} 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行
|
||||
|
||||
以 At(...) 为例:
|
||||
|
||||
```python title=使用通用消息段的拓展控制符
|
||||
>>> from nonebot_plugin_alconna.uniseg import UniMessage
|
||||
>>> UniMessage.template("{:At(user, target)}").format(target="123")
|
||||
UniMessage(At("user", "123"))
|
||||
>>> UniMessage.template("{:At(type=user, target=id)}").format(id="123")
|
||||
UniMessage(At("user", "123"))
|
||||
>>> UniMessage.template("{:At(type=user, target=123)}").format()
|
||||
UniMessage(At("user", "123"))
|
||||
```
|
||||
|
||||
而在 `AlconnaMatcher` 中,{:XXX} 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能
|
||||
|
||||
```python title=在 AlconnaMatcher 中使用通用消息段的拓展控制符
|
||||
from arclet.alconna import Alconna, Args
|
||||
from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna
|
||||
|
||||
test_cmd = on_alconna(Alconna("test", Args["target?", At]))
|
||||
|
||||
@test_cmd.handle()
|
||||
async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
|
||||
if target.available:
|
||||
matcher.set_path_arg("target", target.result)
|
||||
|
||||
@test_cmd.got_path(
|
||||
"target",
|
||||
prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请确认目标")
|
||||
)
|
||||
async def tt():
|
||||
await test_cmd.send(
|
||||
UniMessage.template("{:At(user, $event.get_user_id())} 已确认目标为 {target}")
|
||||
)
|
||||
```
|
88
website/docs/best-practice/alconna/utils.md
Normal file
88
website/docs/best-practice/alconna/utils.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
description: 杂项
|
||||
---
|
||||
|
||||
# 杂项
|
||||
|
||||
## 特殊装饰器
|
||||
|
||||
`nonebot_plugin_alconna` 提供 了一个 `funcommand` 装饰器, 其用于将一个接受任意参数,
|
||||
返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import funcommand
|
||||
|
||||
@funcommand()
|
||||
async def echo(msg: str):
|
||||
return msg
|
||||
```
|
||||
|
||||
其等同于
|
||||
|
||||
```python
|
||||
from arclet.alconna import Alconna, Args
|
||||
from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match
|
||||
|
||||
echo = on_alconna(Alconna("echo", Args["msg", str]))
|
||||
|
||||
@echo.handle()
|
||||
async def echo_exit(msg: Match[str] = AlconnaMatch("msg")):
|
||||
await echo.finish(msg.result)
|
||||
```
|
||||
|
||||
## 特殊构造器
|
||||
|
||||
`nonebot_plugin_alconna` 提供了一个 `Command` 构造器,其基于 `arclet.alconna.tools` 中的 `AlconnaString`,
|
||||
以类似 `Koishi` 中注册命令的方式来构建一个 AlconnaMatcher:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import Command, Arparma
|
||||
|
||||
book = (
|
||||
Command("book", "测试")
|
||||
.option("writer", "-w <id:int>")
|
||||
.option("writer", "--anonymous", {"id": 0})
|
||||
.usage("book [-w <id:int> | --anonymous]")
|
||||
.shortcut("测试", {"args": ["--anonymous"]})
|
||||
.build()
|
||||
)
|
||||
|
||||
@book.handle()
|
||||
async def _(arp: Arparma):
|
||||
await book.send(str(arp.options))
|
||||
```
|
||||
|
||||
甚至,你可以设置 `action` 来设定响应行为:
|
||||
|
||||
```python
|
||||
book = (
|
||||
Command("book", "测试")
|
||||
.option("writer", "-w <id:int>")
|
||||
.option("writer", "--anonymous", {"id": 0})
|
||||
.usage("book [-w <id:int> | --anonymous]")
|
||||
.shortcut("测试", {"args": ["--anonymous"]})
|
||||
.action(lambda options: str(options)) # 会自动通过 bot.send 发送
|
||||
.build()
|
||||
)
|
||||
```
|
||||
|
||||
## 中间件
|
||||
|
||||
在 `AlconnaMatch`, `AlconnaQuery` 或 `got_path` 中,你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数,
|
||||
|
||||
```python {1, 9}
|
||||
from nonebot_plugin_alconna import image_fetch
|
||||
|
||||
mask_cmd = on_alconna(
|
||||
Alconna("search", Args["img?", Image]),
|
||||
)
|
||||
|
||||
|
||||
@mask_cmd.handle()
|
||||
async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img", image_fetch)):
|
||||
result = await search_img(img.result)
|
||||
await matcher.send(result.content)
|
||||
```
|
||||
|
||||
其中,`image_fetch` 是一个中间件,其接受一个 `Image` 对象,并提取图片的二进制数据返回。
|
@@ -180,7 +180,7 @@ docker compose build
|
||||
|
||||
将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下,并将文件中高亮行中的仓库名称替换为你的仓库名称:
|
||||
|
||||
```yaml title=.github/workflows/build.yml {34}
|
||||
```yaml title=.github/workflows/build.yml
|
||||
name: Docker Hub Release
|
||||
|
||||
on:
|
||||
@@ -213,6 +213,7 @@ jobs:
|
||||
id: metadata
|
||||
with:
|
||||
images: |
|
||||
# highlight-next-line
|
||||
{organization}/{repository}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user