mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-10-07 19:26:44 +00:00
Compare commits
256 Commits
v2.0.0-bet
...
v2.0.0rc2
Author | SHA1 | Date | |
---|---|---|---|
|
d83866f03b | ||
|
cb83e76e16 | ||
|
1644615462 | ||
|
89d8abf863 | ||
|
f8cf7c94ae | ||
|
bef494615f | ||
|
6e110e725e | ||
|
85390a14b6 | ||
|
276041e314 | ||
|
2922da7b2f | ||
|
c783ab5e9b | ||
|
139190bff7 | ||
|
1524434444 | ||
|
b6857d59b8 | ||
|
a7b0eb10a0 | ||
|
0eadb44e20 | ||
|
6b43209d37 | ||
|
a50990bef2 | ||
|
f1525c1ecd | ||
|
7df9756205 | ||
|
376a720881 | ||
|
c7377647fa | ||
|
cfbd6f1e4d | ||
|
cecc853f25 | ||
|
fe5f85517e | ||
|
66040a7e44 | ||
|
81d2f017f6 | ||
|
4355025f87 | ||
|
0bc8a39578 | ||
|
7308f57776 | ||
|
7de8912edb | ||
|
42a4edc3ee | ||
|
4a12429a38 | ||
|
092f6d05f5 | ||
|
2c0c05dca1 | ||
|
31bafc832f | ||
|
e1720d8ea0 | ||
|
08be5724b9 | ||
|
828714a4e3 | ||
|
a79eeb73a6 | ||
|
c1aec637d5 | ||
|
12cc08efbd | ||
|
f410af72dc | ||
|
113021cdf4 | ||
|
fcc23f98f8 | ||
|
0b693c419e | ||
|
36e5b81510 | ||
|
71f17bebaa | ||
|
2f45f25d13 | ||
|
17d52446c3 | ||
|
2304aaf22b | ||
|
a70c37de69 | ||
|
1cabc18277 | ||
|
ed1235ed11 | ||
|
9952b4e838 | ||
|
e81390a104 | ||
|
7bc22289b4 | ||
|
bfa4a079bf | ||
|
403850262b | ||
|
ed25e7aa39 | ||
|
7ab480f044 | ||
|
d7d6152094 | ||
|
2954e58c77 | ||
|
f684b96433 | ||
|
4388d1c9e6 | ||
|
876acf3e88 | ||
|
963e73f517 | ||
|
bdc9e44142 | ||
|
4f2efb7304 | ||
|
6ae891124f | ||
|
145bd4d4e1 | ||
|
ab54049909 | ||
|
7aa554f5a2 | ||
|
5566777374 | ||
|
b2594f61de | ||
|
1ad1e0606c | ||
|
583d5060db | ||
|
eaa3dbdfa8 | ||
|
03f378690a | ||
|
512c66ccc0 | ||
|
9d20c7510a | ||
|
29ad8a6686 | ||
|
db534b8824 | ||
|
67b96528af | ||
|
a5929f80f7 | ||
|
9619477a27 | ||
|
8377680fd7 | ||
|
3e3d6f91a5 | ||
|
1d3d886004 | ||
|
ebc5a3cc9e | ||
|
1092767a51 | ||
|
ab227ee64b | ||
|
00b37fb3d2 | ||
|
e6494dc98e | ||
|
c80869f952 | ||
|
2be72eac5e | ||
|
830c4f8c6a | ||
|
138fb458b7 | ||
|
2c93f82ef3 | ||
|
77aa16c2fc | ||
|
945da7151e | ||
|
41ea0df0a5 | ||
|
cec45cf89c | ||
|
2de8c66c70 | ||
|
0bcc4277e5 | ||
|
d3dd93b36c | ||
|
e9bd81d9bb | ||
|
997d4f5042 | ||
|
3bb321c519 | ||
|
fe92d29322 | ||
|
3234871b53 | ||
|
03543f01f2 | ||
|
ba5c0303c7 | ||
|
e56fdd04ad | ||
|
9f10bb70db | ||
|
71aad502d1 | ||
|
ab85b8651e | ||
|
4fe8929441 | ||
|
5c303710f6 | ||
|
68d2ada94b | ||
|
75470fe157 | ||
|
47b3fc516a | ||
|
84c24b014f | ||
|
756cde6525 | ||
|
57ef19af94 | ||
|
5927b517e2 | ||
|
132205bfcc | ||
|
b31dfa9ab0 | ||
|
b249802c38 | ||
|
9df705aaa7 | ||
|
a0df535f0c | ||
|
31022a653d | ||
|
ba77443dde | ||
|
984f743097 | ||
|
638a9c94af | ||
|
92f1d5a4d7 | ||
|
248af2ae1a | ||
|
4c37be7312 | ||
|
2cb8eafa81 | ||
|
05bff5ec17 | ||
|
13245cb58f | ||
|
37bc7326b5 | ||
|
f6d189d8c5 | ||
|
600ef7031f | ||
|
7bedf7c8d0 | ||
|
f62ee5893c | ||
|
71234e9a68 | ||
|
3bbca0fa70 | ||
|
20f144ba93 | ||
|
4c8bc9f0cb | ||
|
064509f26b | ||
|
8c42490a7e | ||
|
179d7105c9 | ||
|
1c14e638c8 | ||
|
c6eef06b55 | ||
|
beef564a22 | ||
|
672f2ceecc | ||
|
28142402d7 | ||
|
b886329fb8 | ||
|
a0b186aff3 | ||
|
595c64e760 | ||
|
5114749073 | ||
|
af2d7b5797 | ||
|
56943c0908 | ||
|
45478deb95 | ||
|
d281ec5bf9 | ||
|
291a7cbb8b | ||
|
41259546bd | ||
|
f96038241f | ||
|
b051320d78 | ||
|
d12efac9f4 | ||
|
f87a38a30a | ||
|
bf016b3f69 | ||
|
373f5255f1 | ||
|
5a35015195 | ||
|
d3a2f1dc08 | ||
|
f1aec4eb10 | ||
|
cd30be21ba | ||
|
f150a9ee89 | ||
|
e68281f60f | ||
|
32be64485a | ||
|
c76f492305 | ||
|
29b0351644 | ||
|
ef3350fd9c | ||
|
459699de5c | ||
|
31c3eb8fd6 | ||
|
1cfdee2645 | ||
|
4e76518a58 | ||
|
b53e029df1 | ||
|
c1faf68806 | ||
|
fe64b904ff | ||
|
a621ade449 | ||
|
3fda978064 | ||
|
60ab93164c | ||
|
2b22d5abda | ||
|
e3a4834383 | ||
|
21087036af | ||
|
94d336ef4d | ||
|
07707213a5 | ||
|
7579878fb4 | ||
|
6599b6420e | ||
|
135c6e8168 | ||
|
743e7363ea | ||
|
a101428c81 | ||
|
0d2b1f693e | ||
|
e5a53dfd5c | ||
|
915c2b3e43 | ||
|
767b6a9913 | ||
|
3f8af04803 | ||
|
00af815b8a | ||
|
24df594b97 | ||
|
d6567f9288 | ||
|
4eb158245e | ||
|
ef35266d3e | ||
|
1d1beb100a | ||
|
06ab6093b7 | ||
|
c1ce7fb940 | ||
|
6e03ddbf12 | ||
|
40c8787828 | ||
|
be459e0bbb | ||
|
1056828f90 | ||
|
ef18e8943d | ||
|
92ff1df419 | ||
|
be5ac88a18 | ||
|
fac647370a | ||
|
05a3891903 | ||
|
4deae8f00c | ||
|
0f70e975b0 | ||
|
982680be91 | ||
|
96b0a863e6 | ||
|
d64bb37c6d | ||
|
51d7f1783d | ||
|
64c18379c9 | ||
|
2735f0cba9 | ||
|
660dbaf3b8 | ||
|
898c29d7ee | ||
|
cdc507bab9 | ||
|
f32bcdc1fc | ||
|
013602da21 | ||
|
4974c596ec | ||
|
0620bec51f | ||
|
8870e6a26e | ||
|
0e3ed0e7ab | ||
|
6cc3b68447 | ||
|
549a37b172 | ||
|
16394ad68b | ||
|
57e580c255 | ||
|
6c23d89494 | ||
|
675e70f579 | ||
|
7a098b96f8 | ||
|
c9794bf91d | ||
|
1766d4da69 | ||
|
6583bc8c61 | ||
|
179f16346a | ||
|
badb0c9ff4 | ||
|
ee0ea85e40 |
@@ -1,15 +0,0 @@
|
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/codespaces-linux/.devcontainer/base.Dockerfile
|
||||
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/universal:2-focal
|
||||
|
||||
# ** [Optional] Uncomment this section to install additional packages. **
|
||||
# USER root
|
||||
#
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
USER codespace
|
||||
|
||||
# [Required] Poetry
|
||||
RUN curl -sSL https://install.python-poetry.org | python - -y
|
||||
RUN poetry config virtualenvs.in-project true
|
@@ -1,32 +1,13 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/codespaces-linux
|
||||
{
|
||||
"name": "GitHub Codespaces (Default)",
|
||||
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
"name": "Default Linux Universal",
|
||||
"image": "mcr.microsoft.com/devcontainers/universal:2-linux",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-contrib/features/poetry:1": {}
|
||||
},
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"postCreateCommand": "poetry config virtualenvs.in-project true && poetry install -E all && poetry run pre-commit install && yarn install",
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"go.toolsManagement.checkForUpdates": "local",
|
||||
"go.useLanguageServer": true,
|
||||
"go.gopath": "/go",
|
||||
"python.defaultInterpreterPath": "/opt/python/latest/bin/python",
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
|
||||
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
|
||||
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
|
||||
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
|
||||
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
|
||||
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
|
||||
"python.analysis.diagnosticMode": "workspace",
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"[python]": {
|
||||
@@ -50,7 +31,6 @@
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"lldb.executable": "/usr/bin/lldb",
|
||||
"files.exclude": {
|
||||
"**/__pycache__": true
|
||||
},
|
||||
@@ -59,10 +39,7 @@
|
||||
"**/__pycache__": true
|
||||
}
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.isort",
|
||||
@@ -72,27 +49,5 @@
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"remoteUser": "codespace",
|
||||
|
||||
"overrideCommand": false,
|
||||
|
||||
"mounts": [
|
||||
"source=codespaces-linux-var-lib-docker,target=/var/lib/docker,type=volume"
|
||||
],
|
||||
|
||||
"runArgs": [
|
||||
"--cap-add=SYS_PTRACE",
|
||||
"--security-opt",
|
||||
"seccomp=unconfined",
|
||||
"--privileged",
|
||||
"--init"
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// "oryx build" will automatically install your dependencies and attempt to build your project
|
||||
"postCreateCommand": "poetry install && poetry run pre-commit install && yarn install"
|
||||
}
|
||||
}
|
||||
|
4
.github/actions/setup-node/action.yml
vendored
4
.github/actions/setup-node/action.yml
vendored
@@ -9,10 +9,10 @@ runs:
|
||||
node-version: "16"
|
||||
|
||||
- id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
|
21
.github/actions/setup-python/action.yml
vendored
21
.github/actions/setup-python/action.yml
vendored
@@ -5,27 +5,20 @@ inputs:
|
||||
python-version:
|
||||
description: Python version
|
||||
required: false
|
||||
default: "3.9"
|
||||
default: "3.10"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- id: python
|
||||
uses: actions/setup-python@v2
|
||||
- name: Install poetry
|
||||
run: pipx install poetry
|
||||
shell: bash
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
architecture: "x64"
|
||||
|
||||
- uses: Gr1N/setup-poetry@v7
|
||||
|
||||
- id: poetry-cache
|
||||
run: echo "::set-output name=dir::$(poetry config virtualenvs.path)"
|
||||
shell: bash
|
||||
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.poetry-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-poetry-${{ steps.python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
||||
cache: "poetry"
|
||||
|
||||
- run: poetry install -E all
|
||||
shell: bash
|
||||
|
2
.github/workflows/codecov.yml
vendored
2
.github/workflows/codecov.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
fail-fast: false
|
||||
env:
|
||||
|
@@ -13,20 +13,20 @@ repos:
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.6.0
|
||||
rev: 22.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.7.1
|
||||
rev: v3.0.0-alpha.4
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [javascript, jsx, ts, tsx, markdown, yaml]
|
||||
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/nonebot/nonemoji
|
||||
rev: v0.1.2
|
||||
rev: v0.1.3
|
||||
hooks:
|
||||
- id: nonemoji
|
||||
stages: [prepare-commit-msg]
|
||||
|
@@ -33,7 +33,7 @@ pre-commit install
|
||||
|
||||
### 使用 GitHub Codespaces(Dev Container)
|
||||
|
||||
使用 GitHub Codespaces 选择 `NoneBot2` 项目,然后选择 `.devcontainer/devcontainer.json` 配置即可。
|
||||
[](https://github.com/codespaces/new?hide_repo_select=true&ref=master&repo=289605524)
|
||||
|
||||
### Commit 规范
|
||||
|
||||
|
45
README.md
45
README.md
@@ -21,7 +21,7 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
||||
<a href="https://pypi.python.org/pypi/nonebot2">
|
||||
<img src="https://img.shields.io/pypi/v/nonebot2" alt="pypi">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/python-3.7.3+-blue" alt="python">
|
||||
<img src="https://img.shields.io/badge/python-3.8+-blue" alt="python">
|
||||
<a href="https://codecov.io/gh/nonebot/nonebot2">
|
||||
<img src="https://codecov.io/gh/nonebot/nonebot2/branch/master/graph/badge.svg?token=2P0G0VS7N4" alt="codecov"/>
|
||||
</a>
|
||||
@@ -38,17 +38,20 @@ _✨ 跨平台 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://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>
|
||||
<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>
|
||||
<a href="https://open.feishu.cn/document/home/index">
|
||||
<img src="https://img.shields.io/badge/%E9%A3%9E%E4%B9%A6-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xNyAyOUMyMSAyOSAyNSAyNi45MzM5IDI4IDIzLjQwNjVDMzYgMTQgNDEuNDI0MiAxNi44MTY2IDQ0IDE3Ljk5OThDMzguNSAyMC45OTk4IDQwLjUgMjkuNjIzMyAzMyAzNS45OTk4QzI4LjM4MiAzOS45MjU5IDIzLjQ5NDUgNDEuMDE0IDE5IDQxQzEyLjUyMzEgNDAuOTc5OSA2Ljg2MjI2IDM3Ljc2MzcgNCAzNS40MDYzVjE2Ljk5OTgiIHN0cm9rZT0iIzMzMyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNNS42NDgwOCAxNS44NjY5QzUuMDIyMzEgMTQuOTU2NyAzLjc3NzE1IDE0LjcyNjEgMi44NjY5NCAxNS4zNTE5QzEuOTU2NzMgMTUuOTc3NyAxLjcyNjE1IDE3LjIyMjggMi4zNTE5MiAxOC4xMzMxTDUuNjQ4MDggMTUuODY2OVpNMzYuMDAyMSAzNS43MzA5QzM2Ljk1OCAzNS4xNzc0IDM3LjI4NDMgMzMuOTUzOSAzNi43MzA5IDMyLjk5NzlDMzYuMTc3NCAzMi4wNDIgMzQuOTUzOSAzMS43MTU3IDMzLjk5NzkgMzIuMjY5MUwzNi4wMDIxIDM1LjczMDlaTTIuMzUxOTIgMTguMTMzMUM1LjI0MzUgMjIuMzM5IDEwLjc5OTIgMjguMTQ0IDE2Ljg4NjUgMzIuMjIzOUMxOS45MzQ1IDM0LjI2NjcgMjMuMjE3IDM1Ljk0NiAyNi40NDkgMzYuNzMyNEMyOS42OTQ2IDM3LjUyMiAzMy4wNDUxIDM3LjQ0MjggMzYuMDAyMSAzNS43MzA5TDMzLjk5NzkgMzIuMjY5MUMzMi4yMDQ5IDMzLjMwNzIgMjkuOTkyOSAzMy40NzggMjcuMzk0NyAzMi44NDU4QzI0Ljc4MyAzMi4yMTAzIDIxLjk0MDUgMzAuNzk1OCAxOS4xMTM1IDI4LjkwMTFDMTMuNDUwOCAyNS4xMDYgOC4yNTY1IDE5LjY2MSA1LjY0ODA4IDE1Ljg2NjlMMi4zNTE5MiAxOC4xMzMxWiIgZmlsbD0iIzMzMyIvPjxwYXRoIGQ9Ik0zMy41OTQ1IDE3QzMyLjgzOTggMTQuNzAyNyAzMC44NTQ5IDkuOTQwNTQgMjcuNTk0NSA3SDExLjU5NDVDMTUuMjE3MSAxMC42NzU3IDIzIDE2IDI3IDI0IiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+" alt="feishu">
|
||||
</a>
|
||||
<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 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>
|
||||
</a>
|
||||
<br />
|
||||
<a href="https://jq.qq.com/?_wv=1027&k=5OFifDh">
|
||||
@@ -92,17 +95,29 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
|
||||
- 生而可靠:100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://v2.nonebot.dev/docs/start/editor-support))
|
||||
- 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源))
|
||||
- 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
|
||||
- [OneBot 协议](https://onebot.dev/) (QQ 等)
|
||||
- [钉钉](https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p)
|
||||
- [Telegram](https://core.telegram.org/bots/api)
|
||||
- [飞书](https://open.feishu.cn/document/home/index)
|
||||
- [QQ 频道](https://bot.q.qq.com/wiki/)
|
||||
- 坚实后盾:支持多种 web 框架,可自定义替换
|
||||
- [FastAPI](https://fastapi.tiangolo.com/)
|
||||
- [Quart](https://pgjones.gitlab.io/quart/) (异步 Flask)
|
||||
- [aiohttp](https://docs.aiohttp.org/en/stable/)
|
||||
- [httpx](https://www.python-httpx.org/)
|
||||
- [websockets](https://websockets.readthedocs.io/en/stable/)
|
||||
|
||||
| 协议名称 | 状态 | 注释 |
|
||||
| :--------------------------------------------------------: | :--: | :----------------------------------------------------------------: |
|
||||
| [OneBot 协议](https://onebot.dev/) | ✅ | 支持 QQ、TG、微信公众号等[平台](https://onebot.dev/ecosystem.html) |
|
||||
| [Telegram](https://core.telegram.org/bots/api) | ✅ | |
|
||||
| [飞书](https://open.feishu.cn/document/home/index) | ✅ | |
|
||||
| [GitHub](https://docs.github.com/en/developers/apps) | ✅ | GitHub APP & OAuth APP |
|
||||
| [QQ 频道](https://bot.q.qq.com/wiki/) | ✅ | 官方接口调整较多 |
|
||||
| [钉钉](https://open.dingtalk.com/document/) | 🤗 | 寻找 Maintainer |
|
||||
| Console | ✅ | 控制台交互 |
|
||||
| [开黑啦](https://developer.kookapp.cn/) | ↗️ | 由社区贡献 |
|
||||
| [Mirai](https://docs.mirai.mamoe.net/mirai-api-http/) | ↗️ | 由社区贡献 |
|
||||
| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat) | ↗️ | 由社区贡献 |
|
||||
|
||||
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
|
||||
|
||||
| 驱动框架 | 类型 |
|
||||
| :--------------------------------------------------------: | :----: |
|
||||
| [FastAPI](https://fastapi.tiangolo.com/) | 服务端 |
|
||||
| [Quart](https://pgjones.gitlab.io/quart/) (异步 Flask) | 服务端 |
|
||||
| [aiohttp](https://docs.aiohttp.org/en/stable/) | 客户端 |
|
||||
| [httpx](https://www.python-httpx.org/) | 客户端 |
|
||||
| [websockets](https://websockets.readthedocs.io/en/stable/) | 客户端 |
|
||||
|
||||
更多:[概览](https://v2.nonebot.dev/docs/)
|
||||
|
||||
|
@@ -16,6 +16,7 @@
|
||||
- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`
|
||||
- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`
|
||||
- `on_regex` => {ref}``on_regex` <nonebot.plugin.on.on_regex>`
|
||||
- `on_type` => {ref}``on_type` <nonebot.plugin.on.on_type>`
|
||||
- `CommandGroup` => {ref}``CommandGroup` <nonebot.plugin.on.CommandGroup>`
|
||||
- `Matchergroup` => {ref}``MatcherGroup` <nonebot.plugin.on.MatcherGroup>`
|
||||
- `load_plugin` => {ref}``load_plugin` <nonebot.plugin.load.load_plugin>`
|
||||
@@ -29,7 +30,6 @@
|
||||
- `get_plugin_by_module_name` => {ref}``get_plugin_by_module_name` <nonebot.plugin.get_plugin_by_module_name>`
|
||||
- `get_loaded_plugins` => {ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>`
|
||||
- `get_available_plugin_names` => {ref}``get_available_plugin_names` <nonebot.plugin.get_available_plugin_names>`
|
||||
- `export` => {ref}``export` <nonebot.plugin.export.export>`
|
||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||
|
||||
FrontMatter:
|
||||
@@ -37,24 +37,24 @@ FrontMatter:
|
||||
description: nonebot 模块
|
||||
"""
|
||||
|
||||
import os
|
||||
import importlib
|
||||
from importlib.metadata import version
|
||||
from typing import Any, Dict, Type, Optional
|
||||
|
||||
import loguru
|
||||
from pydantic.env_settings import DotenvType
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.utils import escape_tag
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.log import logger, default_filter
|
||||
from nonebot.drivers import Driver, ReverseDriver, combine_driver
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
|
||||
_dist: pkg_resources.Distribution = pkg_resources.get_distribution("nonebot2")
|
||||
__version__ = _dist.version
|
||||
VERSION = _dist.parsed_version
|
||||
__version__ = version("nonebot2")
|
||||
except Exception: # pragma: no cover
|
||||
__version__ = None
|
||||
VERSION = None
|
||||
|
||||
_driver: Optional[Driver] = None
|
||||
|
||||
@@ -172,8 +172,7 @@ def get_bots() -> Dict[str, Bot]:
|
||||
bots = nonebot.get_bots()
|
||||
```
|
||||
"""
|
||||
driver = get_driver()
|
||||
return driver.bots
|
||||
return get_driver().bots
|
||||
|
||||
|
||||
def _resolve_dot_notation(
|
||||
@@ -207,7 +206,16 @@ def _resolve_combine_expr(obj_str: str) -> Type[Driver]:
|
||||
return combine_driver(DriverClass, *mixins)
|
||||
|
||||
|
||||
def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
|
||||
def _log_patcher(record: "loguru.Record"):
|
||||
record["name"] = (
|
||||
plugin.name
|
||||
if (module_name := record["name"])
|
||||
and (plugin := get_plugin_by_module_name(module_name))
|
||||
else (module_name and module_name.split(".")[0])
|
||||
)
|
||||
|
||||
|
||||
def init(*, _env_file: Optional[DotenvType] = None, **kwargs: Any) -> None:
|
||||
"""初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。
|
||||
|
||||
NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。
|
||||
@@ -227,13 +235,17 @@ def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
|
||||
if not _driver:
|
||||
logger.success("NoneBot is initializing...")
|
||||
env = Env()
|
||||
_env_file = _env_file or f".env.{env.environment}"
|
||||
config = Config(
|
||||
**kwargs,
|
||||
_common_config=env.dict(),
|
||||
_env_file=_env_file or f".env.{env.environment}",
|
||||
_env_file=(".env", _env_file)
|
||||
if isinstance(_env_file, (str, os.PathLike))
|
||||
else _env_file,
|
||||
)
|
||||
|
||||
default_filter.level = config.log_level
|
||||
logger.configure(
|
||||
extra={"nonebot_log_level": config.log_level}, patcher=_log_patcher
|
||||
)
|
||||
logger.opt(colors=True).info(
|
||||
f"Current <y><b>Env: {escape_tag(env.environment)}</b></y>"
|
||||
)
|
||||
@@ -241,7 +253,7 @@ def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
|
||||
f"Loaded <y><b>Config</b></y>: {escape_tag(str(config.dict()))}"
|
||||
)
|
||||
|
||||
DriverClass: Type[Driver] = _resolve_combine_expr(config.driver)
|
||||
DriverClass = _resolve_combine_expr(config.driver)
|
||||
_driver = DriverClass(env, config)
|
||||
|
||||
|
||||
@@ -262,7 +274,7 @@ def run(*args: Any, **kwargs: Any) -> None:
|
||||
|
||||
|
||||
from nonebot.plugin import on as on
|
||||
from nonebot.plugin import export as export
|
||||
from nonebot.plugin import on_type as on_type
|
||||
from nonebot.plugin import require as require
|
||||
from nonebot.plugin import on_regex as on_regex
|
||||
from nonebot.plugin import on_notice as on_notice
|
||||
|
@@ -7,21 +7,6 @@ FrontMatter:
|
||||
description: nonebot.adapters 模块
|
||||
"""
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
|
||||
pkg_resources.declare_namespace(__name__)
|
||||
del pkg_resources
|
||||
except ImportError:
|
||||
import pkgutil
|
||||
|
||||
__path__: Iterable[str] = pkgutil.extend_path(__path__, __name__) # type: ignore
|
||||
del pkgutil
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from nonebot.internal.adapter import Bot as Bot
|
||||
from nonebot.internal.adapter import Event as Event
|
||||
from nonebot.internal.adapter import Adapter as Adapter
|
||||
|
@@ -14,18 +14,17 @@ from datetime import timedelta
|
||||
from ipaddress import IPv4Address
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Tuple, Union, Mapping, Optional
|
||||
|
||||
from pydantic import BaseSettings, IPvAnyAddress
|
||||
from pydantic.utils import deep_update
|
||||
from pydantic import Extra, BaseSettings, IPvAnyAddress
|
||||
from pydantic.env_settings import (
|
||||
DotenvType,
|
||||
SettingsError,
|
||||
EnvSettingsSource,
|
||||
InitSettingsSource,
|
||||
SettingsSourceCallable,
|
||||
read_env_file,
|
||||
env_file_sentinel,
|
||||
)
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import escape_tag
|
||||
|
||||
|
||||
class CustomEnvSettings(EnvSettingsSource):
|
||||
@@ -33,33 +32,15 @@ class CustomEnvSettings(EnvSettingsSource):
|
||||
"""
|
||||
Build environment variables suitable for passing to the Model.
|
||||
"""
|
||||
d: Dict[str, Optional[str]] = {}
|
||||
d: Dict[str, Any] = {}
|
||||
|
||||
if settings.__config__.case_sensitive:
|
||||
env_vars: Mapping[str, Optional[str]] = os.environ # pragma: no cover
|
||||
else:
|
||||
env_vars = {k.lower(): v for k, v in os.environ.items()}
|
||||
|
||||
env_file_vars: Dict[str, Optional[str]] = {}
|
||||
env_file = (
|
||||
self.env_file
|
||||
if self.env_file != env_file_sentinel
|
||||
else settings.__config__.env_file
|
||||
)
|
||||
env_file_encoding = (
|
||||
self.env_file_encoding
|
||||
if self.env_file_encoding is not None
|
||||
else settings.__config__.env_file_encoding
|
||||
)
|
||||
if env_file is not None:
|
||||
env_path = Path(env_file)
|
||||
if env_path.is_file():
|
||||
env_file_vars = read_env_file(
|
||||
env_path,
|
||||
encoding=env_file_encoding, # type: ignore
|
||||
case_sensitive=settings.__config__.case_sensitive,
|
||||
)
|
||||
env_vars = {**env_file_vars, **env_vars}
|
||||
env_file_vars = self._read_env_files(settings.__config__.case_sensitive)
|
||||
env_vars = {**env_file_vars, **env_vars}
|
||||
|
||||
for field in settings.__fields__.values():
|
||||
env_val: Optional[str] = None
|
||||
@@ -70,29 +51,56 @@ class CustomEnvSettings(EnvSettingsSource):
|
||||
if env_val is not None:
|
||||
break
|
||||
|
||||
if env_val is None:
|
||||
continue
|
||||
is_complex, allow_parse_failure = self.field_is_complex(field)
|
||||
if is_complex:
|
||||
if env_val is None:
|
||||
if env_val_built := self.explode_env_vars(field, env_vars):
|
||||
d[field.alias] = env_val_built
|
||||
else:
|
||||
# field is complex and there's a value, decode that as JSON, then add explode_env_vars
|
||||
try:
|
||||
env_val = settings.__config__.parse_env_var(field.name, env_val)
|
||||
except ValueError as e:
|
||||
if not allow_parse_failure:
|
||||
raise SettingsError(
|
||||
f'error parsing env var "{env_name}"' # type: ignore
|
||||
) from e
|
||||
|
||||
if field.is_complex():
|
||||
try:
|
||||
env_val = settings.__config__.json_loads(env_val)
|
||||
except ValueError as e: # pragma: no cover
|
||||
raise SettingsError(
|
||||
f'error parsing JSON for "{env_name}"' # type: ignore
|
||||
) from e
|
||||
d[field.alias] = env_val
|
||||
if isinstance(env_val, dict):
|
||||
d[field.alias] = deep_update(
|
||||
env_val, self.explode_env_vars(field, env_vars)
|
||||
)
|
||||
else:
|
||||
d[field.alias] = env_val
|
||||
elif env_val is not None:
|
||||
# simplest case, field is not complex, we only need to add the value if it was found
|
||||
d[field.alias] = env_val
|
||||
|
||||
if env_file_vars:
|
||||
for env_name in env_file_vars.keys():
|
||||
env_val = env_vars[env_name]
|
||||
# remain user custom config
|
||||
for env_name in env_file_vars:
|
||||
env_val = env_vars[env_name]
|
||||
if env_val and (val_striped := env_val.strip()):
|
||||
# there's a value, decode that as JSON
|
||||
try:
|
||||
if env_val:
|
||||
env_val = settings.__config__.json_loads(env_val.strip())
|
||||
env_val = settings.__config__.parse_env_var(env_name, val_striped)
|
||||
except ValueError as e:
|
||||
logger.opt(colors=True, exception=e).trace(
|
||||
f"Error while parsing JSON for {escape_tag(env_name)}. Assumed as string."
|
||||
logger.trace(
|
||||
"Error while parsing JSON for "
|
||||
f"{env_name!r}={val_striped!r}. "
|
||||
"Assumed as string."
|
||||
)
|
||||
|
||||
# explode value when it's a nested dict
|
||||
env_name, *nested_keys = env_name.split(self.env_nested_delimiter)
|
||||
if nested_keys and (env_name not in d or isinstance(d[env_name], dict)):
|
||||
result = {}
|
||||
*keys, last_key = nested_keys
|
||||
_tmp = result
|
||||
for key in keys:
|
||||
_tmp = _tmp.setdefault(key, {})
|
||||
_tmp[last_key] = env_val
|
||||
d[env_name] = deep_update(d.get(env_name, {}), result)
|
||||
elif not nested_keys:
|
||||
d[env_name] = env_val
|
||||
|
||||
return d
|
||||
@@ -105,6 +113,9 @@ class BaseConfig(BaseSettings):
|
||||
return self.__dict__.get(name)
|
||||
|
||||
class Config:
|
||||
extra = Extra.allow
|
||||
env_nested_delimiter = "__"
|
||||
|
||||
@classmethod
|
||||
def customise_sources(
|
||||
cls,
|
||||
@@ -116,7 +127,10 @@ class BaseConfig(BaseSettings):
|
||||
return (
|
||||
init_settings,
|
||||
CustomEnvSettings(
|
||||
env_settings.env_file, env_settings.env_file_encoding
|
||||
env_settings.env_file,
|
||||
env_settings.env_file_encoding,
|
||||
env_settings.env_nested_delimiter,
|
||||
env_settings.env_prefix_len,
|
||||
),
|
||||
InitSettingsSource(common_config),
|
||||
file_secret_settings,
|
||||
@@ -136,7 +150,6 @@ class Env(BaseConfig):
|
||||
"""
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
@@ -149,8 +162,7 @@ class Config(BaseConfig):
|
||||
配置方法参考: [配置](https://v2.nonebot.dev/docs/tutorial/configuration)
|
||||
"""
|
||||
|
||||
_env_file: str = ".env"
|
||||
_common_config: Dict[str, Any] = {}
|
||||
_env_file: DotenvType = ".env", ".env.prod"
|
||||
|
||||
# nonebot configs
|
||||
driver: str = "~fastapi"
|
||||
@@ -230,8 +242,7 @@ class Config(BaseConfig):
|
||||
# or from env file using json loads
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
env_file = ".env.prod"
|
||||
env_file = ".env", ".env.prod"
|
||||
|
||||
|
||||
__autodoc__ = {
|
||||
|
@@ -4,7 +4,7 @@ FrontMatter:
|
||||
sidebar_position: 9
|
||||
description: nonebot.consts 模块
|
||||
"""
|
||||
from typing_extensions import Literal
|
||||
from typing import Literal
|
||||
|
||||
# used by Matcher
|
||||
RECEIVE_KEY: Literal["_receive_{id}"] = "_receive_{id}"
|
||||
@@ -42,3 +42,11 @@ REGEX_GROUP: Literal["_matched_groups"] = "_matched_groups"
|
||||
"""正则匹配 group 元组存储 key"""
|
||||
REGEX_DICT: Literal["_matched_dict"] = "_matched_dict"
|
||||
"""正则匹配 group 字典存储 key"""
|
||||
STARTSWITH_KEY: Literal["_startswith"] = "_startswith"
|
||||
"""响应触发前缀 key"""
|
||||
ENDSWITH_KEY: Literal["_endswith"] = "_endswith"
|
||||
"""响应触发后缀 key"""
|
||||
FULLMATCH_KEY: Literal["_fullmatch"] = "_fullmatch"
|
||||
"""响应触发完整消息 key"""
|
||||
KEYWORD_KEY: Literal["_keyword"] = "_keyword"
|
||||
"""响应触发关键字 key"""
|
||||
|
@@ -6,15 +6,31 @@ FrontMatter:
|
||||
"""
|
||||
|
||||
import abc
|
||||
import asyncio
|
||||
import inspect
|
||||
from typing import Any, Dict, List, Type, Generic, TypeVar, Callable, Optional
|
||||
from dataclasses import field, dataclass
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Type,
|
||||
Tuple,
|
||||
Generic,
|
||||
TypeVar,
|
||||
Callable,
|
||||
Iterable,
|
||||
Optional,
|
||||
Awaitable,
|
||||
cast,
|
||||
)
|
||||
|
||||
from pydantic import BaseConfig
|
||||
from pydantic.schema import get_annotation_from_field_info
|
||||
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.exception import TypeMisMatch
|
||||
from nonebot.typing import _DependentCallable
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
|
||||
from .utils import check_field_type, get_typed_signature
|
||||
@@ -31,25 +47,29 @@ class Param(abc.ABC, FieldInfo):
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: "Dependent", name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type["Param"], ...]
|
||||
) -> Optional["Param"]:
|
||||
return None
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def _check_parameterless(
|
||||
cls, dependent: "Dependent", value: Any
|
||||
cls, value: Any, allow_types: Tuple[Type["Param"], ...]
|
||||
) -> Optional["Param"]:
|
||||
return None
|
||||
return
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _solve(self, **kwargs: Any) -> Any:
|
||||
raise NotImplementedError
|
||||
|
||||
async def _check(self, **kwargs: Any) -> None:
|
||||
return
|
||||
|
||||
|
||||
class CustomConfig(BaseConfig):
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Dependent(Generic[R]):
|
||||
"""依赖注入容器
|
||||
|
||||
@@ -61,101 +81,70 @@ class Dependent(Generic[R]):
|
||||
allow_types: 允许的参数类型
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
call: Callable[..., Any],
|
||||
pre_checkers: Optional[List[Param]] = None,
|
||||
params: Optional[List[ModelField]] = None,
|
||||
parameterless: Optional[List[Param]] = None,
|
||||
allow_types: Optional[List[Type[Param]]] = None,
|
||||
) -> None:
|
||||
self.call = call
|
||||
self.pre_checkers = pre_checkers or []
|
||||
self.params = params or []
|
||||
self.parameterless = parameterless or []
|
||||
self.allow_types = allow_types or []
|
||||
call: _DependentCallable[R]
|
||||
params: Tuple[ModelField] = field(default_factory=tuple)
|
||||
parameterless: Tuple[Param] = field(default_factory=tuple)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if inspect.isfunction(self.call) or inspect.isclass(self.call):
|
||||
call_str = self.call.__name__
|
||||
else:
|
||||
call_str = repr(self.call)
|
||||
return (
|
||||
f"<Dependent call={self.call}, params={self.params},"
|
||||
f" parameterless={self.parameterless}>"
|
||||
f"Dependent(call={call_str}"
|
||||
+ (f", parameterless={self.parameterless}" if self.parameterless else "")
|
||||
+ ")"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
async def __call__(self, **kwargs: Any) -> R:
|
||||
# do pre-check
|
||||
await self.check(**kwargs)
|
||||
|
||||
# solve param values
|
||||
values = await self.solve(**kwargs)
|
||||
|
||||
# call function
|
||||
if is_coroutine_callable(self.call):
|
||||
return await self.call(**values)
|
||||
return await cast(Callable[..., Awaitable[R]], self.call)(**values)
|
||||
else:
|
||||
return await run_sync(self.call)(**values)
|
||||
return await run_sync(cast(Callable[..., R], self.call))(**values)
|
||||
|
||||
def parse_param(self, name: str, param: inspect.Parameter) -> Param:
|
||||
for allow_type in self.allow_types:
|
||||
field_info = allow_type._check_param(self, name, param)
|
||||
if field_info:
|
||||
return field_info
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unknown parameter {name} for function {self.call} with type {param.annotation}"
|
||||
)
|
||||
@staticmethod
|
||||
def parse_params(
|
||||
call: _DependentCallable[R], allow_types: Tuple[Type[Param], ...]
|
||||
) -> Tuple[ModelField]:
|
||||
fields: List[ModelField] = []
|
||||
params = get_typed_signature(call).parameters.values()
|
||||
|
||||
def parse_parameterless(self, value: Any) -> Param:
|
||||
for allow_type in self.allow_types:
|
||||
field_info = allow_type._check_parameterless(self, value)
|
||||
if field_info:
|
||||
return field_info
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unknown parameterless {value} for function {self.call} with type {type(value)}"
|
||||
)
|
||||
|
||||
def prepend_parameterless(self, value: Any) -> None:
|
||||
self.parameterless.insert(0, self.parse_parameterless(value))
|
||||
|
||||
def append_parameterless(self, value: Any) -> None:
|
||||
self.parameterless.append(self.parse_parameterless(value))
|
||||
|
||||
@classmethod
|
||||
def parse(
|
||||
cls: Type[T],
|
||||
*,
|
||||
call: Callable[..., Any],
|
||||
parameterless: Optional[List[Any]] = None,
|
||||
allow_types: Optional[List[Type[Param]]] = None,
|
||||
) -> T:
|
||||
signature = get_typed_signature(call)
|
||||
params = signature.parameters
|
||||
dependent = cls(
|
||||
call=call,
|
||||
allow_types=allow_types,
|
||||
)
|
||||
|
||||
for param_name, param in params.items():
|
||||
for param in params:
|
||||
default_value = Required
|
||||
if param.default != param.empty:
|
||||
default_value = param.default
|
||||
|
||||
if isinstance(default_value, Param):
|
||||
field_info = default_value
|
||||
default_value = field_info.default
|
||||
else:
|
||||
field_info = dependent.parse_param(param_name, param)
|
||||
default_value = field_info.default
|
||||
for allow_type in allow_types:
|
||||
if field_info := allow_type._check_param(param, allow_types):
|
||||
break
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unknown parameter {param.name} for function {call} with type {param.annotation}"
|
||||
)
|
||||
|
||||
default_value = field_info.default
|
||||
|
||||
annotation: Any = Any
|
||||
required = default_value == Required
|
||||
if param.annotation != param.empty:
|
||||
annotation = param.annotation
|
||||
annotation = get_annotation_from_field_info(
|
||||
annotation, field_info, param_name
|
||||
annotation, field_info, param.name
|
||||
)
|
||||
dependent.params.append(
|
||||
|
||||
fields.append(
|
||||
ModelField(
|
||||
name=param_name,
|
||||
name=param.name,
|
||||
type_=annotation,
|
||||
class_validators=None,
|
||||
model_config=CustomConfig,
|
||||
@@ -165,49 +154,72 @@ class Dependent(Generic[R]):
|
||||
)
|
||||
)
|
||||
|
||||
parameterless_params = [
|
||||
dependent.parse_parameterless(param) for param in (parameterless or [])
|
||||
]
|
||||
dependent.parameterless.extend(parameterless_params)
|
||||
return tuple(fields)
|
||||
|
||||
logger.trace(
|
||||
f"Parsed dependent with call={call}, "
|
||||
f"params={[param.field_info for param in dependent.params]}, "
|
||||
f"parameterless={dependent.parameterless}"
|
||||
@staticmethod
|
||||
def parse_parameterless(
|
||||
parameterless: Tuple[Any, ...], allow_types: Tuple[Type[Param], ...]
|
||||
) -> Tuple[Param, ...]:
|
||||
parameterless_params: List[Param] = []
|
||||
for value in parameterless:
|
||||
for allow_type in allow_types:
|
||||
if param := allow_type._check_parameterless(value, allow_types):
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"Unknown parameterless {value}")
|
||||
parameterless_params.append(param)
|
||||
return tuple(parameterless_params)
|
||||
|
||||
@classmethod
|
||||
def parse(
|
||||
cls,
|
||||
*,
|
||||
call: _DependentCallable[R],
|
||||
parameterless: Optional[Iterable[Any]] = None,
|
||||
allow_types: Iterable[Type[Param]],
|
||||
) -> "Dependent[R]":
|
||||
allow_types = tuple(allow_types)
|
||||
|
||||
params = cls.parse_params(call, allow_types)
|
||||
parameterless_params = (
|
||||
tuple()
|
||||
if parameterless is None
|
||||
else cls.parse_parameterless(tuple(parameterless), allow_types)
|
||||
)
|
||||
|
||||
return dependent
|
||||
return cls(call, params, parameterless_params)
|
||||
|
||||
async def solve(
|
||||
self,
|
||||
**params: Any,
|
||||
) -> Dict[str, Any]:
|
||||
values: Dict[str, Any] = {}
|
||||
async def check(self, **params: Any) -> None:
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(param._check(**params) for param in self.parameterless)
|
||||
)
|
||||
await asyncio.gather(
|
||||
*(
|
||||
cast(Param, param.field_info)._check(**params)
|
||||
for param in self.params
|
||||
)
|
||||
)
|
||||
except SkippedException as e:
|
||||
logger.trace(f"{self} skipped due to {e}")
|
||||
raise
|
||||
|
||||
for checker in self.pre_checkers:
|
||||
await checker._solve(**params)
|
||||
async def _solve_field(self, field: ModelField, params: Dict[str, Any]) -> Any:
|
||||
value = await cast(Param, field.field_info)._solve(**params)
|
||||
if value is Undefined:
|
||||
value = field.get_default()
|
||||
return check_field_type(field, value)
|
||||
|
||||
async def solve(self, **params: Any) -> Dict[str, Any]:
|
||||
# solve parameterless
|
||||
for param in self.parameterless:
|
||||
await param._solve(**params)
|
||||
|
||||
for field in self.params:
|
||||
field_info = field.field_info
|
||||
assert isinstance(field_info, Param), "Params must be subclasses of Param"
|
||||
value = await field_info._solve(**params)
|
||||
if value is Undefined:
|
||||
value = field.get_default()
|
||||
|
||||
try:
|
||||
values[field.name] = check_field_type(field, value)
|
||||
except TypeMisMatch:
|
||||
logger.debug(
|
||||
f"{field_info} "
|
||||
f"type {type(value)} not match depends {self.call} "
|
||||
f"annotation {field._type_display()}, ignored"
|
||||
)
|
||||
raise
|
||||
|
||||
return values
|
||||
# solve param values
|
||||
values = await asyncio.gather(
|
||||
*(self._solve_field(field, params) for field in self.params)
|
||||
)
|
||||
return {field.name: value for field, value in zip(self.params, values)}
|
||||
|
||||
|
||||
__autodoc__ = {"CustomConfig": False}
|
||||
|
@@ -4,11 +4,11 @@ FrontMatter:
|
||||
description: nonebot.dependencies.utils 模块
|
||||
"""
|
||||
import inspect
|
||||
from typing import Any, Dict, TypeVar, Callable
|
||||
from typing import Any, Dict, TypeVar, Callable, ForwardRef
|
||||
|
||||
from loguru import logger
|
||||
from pydantic.fields import ModelField
|
||||
from pydantic.typing import ForwardRef, evaluate_forwardref
|
||||
from pydantic.typing import evaluate_forwardref
|
||||
|
||||
from nonebot.exception import TypeMisMatch
|
||||
|
||||
@@ -28,8 +28,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
||||
)
|
||||
for param in signature.parameters.values()
|
||||
]
|
||||
typed_signature = inspect.Signature(typed_params)
|
||||
return typed_signature
|
||||
return inspect.Signature(typed_params)
|
||||
|
||||
|
||||
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import signal
|
||||
import asyncio
|
||||
import threading
|
||||
from typing import Set, Union, Callable, Awaitable
|
||||
from typing import Set, Union, Callable, Awaitable, cast
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.drivers import Driver
|
||||
@@ -9,8 +9,7 @@ from nonebot.typing import overrides
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
|
||||
STARTUP_FUNC = Callable[[], Union[None, Awaitable[None]]]
|
||||
SHUTDOWN_FUNC = Callable[[], Union[None, Awaitable[None]]]
|
||||
HOOK_FUNC = Union[Callable[[], None], Callable[[], Awaitable[None]]]
|
||||
HANDLED_SIGNALS = (
|
||||
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
||||
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
|
||||
@@ -20,8 +19,8 @@ HANDLED_SIGNALS = (
|
||||
class BlockDriver(Driver):
|
||||
def __init__(self, env: Env, config: Config):
|
||||
super().__init__(env, config)
|
||||
self.startup_funcs: Set[STARTUP_FUNC] = set()
|
||||
self.shutdown_funcs: Set[SHUTDOWN_FUNC] = set()
|
||||
self.startup_funcs: Set[HOOK_FUNC] = set()
|
||||
self.shutdown_funcs: Set[HOOK_FUNC] = set()
|
||||
self.should_exit: asyncio.Event = asyncio.Event()
|
||||
self.force_exit: bool = False
|
||||
|
||||
@@ -38,7 +37,7 @@ class BlockDriver(Driver):
|
||||
return logger
|
||||
|
||||
@overrides(Driver)
|
||||
def on_startup(self, func: STARTUP_FUNC) -> STARTUP_FUNC:
|
||||
def on_startup(self, func: HOOK_FUNC) -> HOOK_FUNC:
|
||||
"""
|
||||
注册一个启动时执行的函数
|
||||
"""
|
||||
@@ -46,7 +45,7 @@ class BlockDriver(Driver):
|
||||
return func
|
||||
|
||||
@overrides(Driver)
|
||||
def on_shutdown(self, func: SHUTDOWN_FUNC) -> SHUTDOWN_FUNC:
|
||||
def on_shutdown(self, func: HOOK_FUNC) -> HOOK_FUNC:
|
||||
"""
|
||||
注册一个停止时执行的函数
|
||||
"""
|
||||
@@ -71,7 +70,9 @@ class BlockDriver(Driver):
|
||||
async def startup(self):
|
||||
# run startup
|
||||
cors = [
|
||||
startup() if is_coroutine_callable(startup) else run_sync(startup)()
|
||||
cast(Callable[..., Awaitable[None]], startup)()
|
||||
if is_coroutine_callable(startup)
|
||||
else run_sync(startup)()
|
||||
for startup in self.startup_funcs
|
||||
]
|
||||
if cors:
|
||||
@@ -94,7 +95,9 @@ class BlockDriver(Driver):
|
||||
logger.info("Waiting for application shutdown.")
|
||||
# run shutdown
|
||||
cors = [
|
||||
shutdown() if is_coroutine_callable(shutdown) else run_sync(shutdown)()
|
||||
cast(Callable[..., Awaitable[None]], shutdown)()
|
||||
if is_coroutine_callable(shutdown)
|
||||
else run_sync(shutdown)()
|
||||
for shutdown in self.shutdown_funcs
|
||||
]
|
||||
if cors:
|
||||
|
@@ -27,7 +27,7 @@ from nonebot.drivers import HTTPVersion, ForwardMixin, ForwardDriver, combine_dr
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
except ImportError:
|
||||
except ImportError: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install aiohttp first to use this driver. `pip install nonebot2[aiohttp]`"
|
||||
) from None
|
||||
|
@@ -9,7 +9,9 @@ FrontMatter:
|
||||
description: nonebot.drivers.fastapi 模块
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
import contextlib
|
||||
from functools import wraps
|
||||
from typing import Any, List, Tuple, Union, Callable, Optional
|
||||
|
||||
@@ -186,14 +188,12 @@ class Driver(ReverseDriver):
|
||||
setup: HTTPServerSetup,
|
||||
) -> Response:
|
||||
json: Any = None
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
json = await request.json()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
data: Optional[dict] = None
|
||||
files: Optional[List[Tuple[str, FileTypes]]] = None
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
form = await request.form()
|
||||
data = {}
|
||||
files = []
|
||||
@@ -204,8 +204,7 @@ class Driver(ReverseDriver):
|
||||
)
|
||||
else:
|
||||
data[key] = value
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
http_request = BaseRequest(
|
||||
request.method,
|
||||
str(request.url),
|
||||
@@ -219,7 +218,9 @@ class Driver(ReverseDriver):
|
||||
)
|
||||
|
||||
response = await setup.handle_func(http_request)
|
||||
return Response(response.content, response.status_code, dict(response.headers))
|
||||
return Response(
|
||||
response.content, response.status_code, dict(response.headers.items())
|
||||
)
|
||||
|
||||
async def _handle_ws(self, websocket: WebSocket, setup: WebSocketServerSetup):
|
||||
request = BaseRequest(
|
||||
@@ -261,7 +262,7 @@ class FastAPIWebSocket(BaseWebSocket):
|
||||
async def close(
|
||||
self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = ""
|
||||
) -> None:
|
||||
await self.websocket.close(code)
|
||||
await self.websocket.close(code, reason)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
async def receive(self) -> Union[str, bytes]:
|
||||
|
@@ -31,7 +31,7 @@ from nonebot.drivers import (
|
||||
|
||||
try:
|
||||
import httpx
|
||||
except ImportError:
|
||||
except ImportError: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install httpx by using `pip install nonebot2[httpx]`"
|
||||
) from None
|
||||
|
@@ -37,7 +37,7 @@ try:
|
||||
from quart import Quart, Request, Response
|
||||
from quart.datastructures import FileStorage
|
||||
from quart import Websocket as QuartWebSocket
|
||||
except ImportError:
|
||||
except ImportError: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install Quart by using `pip install nonebot2[quart]`"
|
||||
) from None
|
||||
|
@@ -30,10 +30,10 @@ from nonebot.drivers import ForwardMixin, ForwardDriver, combine_driver
|
||||
try:
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
from websockets.legacy.client import Connect, WebSocketClientProtocol
|
||||
except ImportError:
|
||||
except ImportError: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install websockets by using `pip install nonebot2[websockets]`"
|
||||
)
|
||||
) from None
|
||||
|
||||
logger = logging.Logger("websockets.client", "INFO")
|
||||
logger.addHandler(LoguruHandler())
|
||||
|
@@ -46,10 +46,14 @@ class ParserExit(NoneBotException):
|
||||
self.status = status
|
||||
self.message = message
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ParserExit status={self.status} message={self.message}>"
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"ParserExit(status={self.status}"
|
||||
+ (f", message={self.message!r}" if self.message else "")
|
||||
+ ")"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
@@ -68,10 +72,10 @@ class IgnoredException(ProcessException):
|
||||
def __init__(self, reason: Any):
|
||||
self.reason: Any = reason
|
||||
|
||||
def __repr__(self):
|
||||
return f"<IgnoredException, reason={self.reason}>"
|
||||
def __repr__(self) -> str:
|
||||
return f"IgnoredException(reason={self.reason!r})"
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
@@ -99,11 +103,14 @@ class TypeMisMatch(SkippedException):
|
||||
self.param: ModelField = param
|
||||
self.value: Any = value
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TypeMisMatch, param={self.param}, value={self.value}>"
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"TypeMisMatch(param={self.param.name}, "
|
||||
f"type={self.param._type_display()}, value={self.value!r}>"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
self.__repr__()
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
class MockApiException(ProcessException):
|
||||
@@ -116,10 +123,10 @@ class MockApiException(ProcessException):
|
||||
def __init__(self, result: Any):
|
||||
self.result = result
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ApiCancelledException, result={self.result}>"
|
||||
def __repr__(self) -> str:
|
||||
return f"MockApiException(result={self.result!r})"
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
@@ -195,7 +202,8 @@ class AdapterException(NoneBotException):
|
||||
adapter_name: 标识 adapter
|
||||
"""
|
||||
|
||||
def __init__(self, adapter_name: str) -> None:
|
||||
def __init__(self, adapter_name: str, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
self.adapter_name: str = adapter_name
|
||||
|
||||
|
||||
@@ -231,4 +239,11 @@ class WebSocketClosed(DriverException):
|
||||
self.reason = reason
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<WebSocketClosed code={self.code} reason={self.reason}>"
|
||||
return (
|
||||
f"WebSocketClosed(code={self.code}"
|
||||
+ (f", reason={self.reason!r}" if self.reason else "")
|
||||
+ ")"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
@@ -33,6 +33,9 @@ class Adapter(abc.ABC):
|
||||
self.bots: Dict[str, Bot] = {}
|
||||
"""本协议适配器已建立连接的 {ref}`nonebot.adapters.Bot` 实例"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Adapter(name={self.get_name()!r})"
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_name(cls) -> str:
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import abc
|
||||
import asyncio
|
||||
from functools import partial
|
||||
from typing_extensions import Protocol
|
||||
from typing import TYPE_CHECKING, Any, Set, Union, Optional
|
||||
from typing import TYPE_CHECKING, Any, Set, Union, Optional, Protocol
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.config import Config
|
||||
@@ -14,10 +13,9 @@ if TYPE_CHECKING:
|
||||
from .adapter import Adapter
|
||||
from .message import Message, MessageSegment
|
||||
|
||||
|
||||
class _ApiCall(Protocol):
|
||||
async def __call__(self, **kwargs: Any) -> Any:
|
||||
...
|
||||
class _ApiCall(Protocol):
|
||||
async def __call__(self, **kwargs: Any) -> Any:
|
||||
...
|
||||
|
||||
|
||||
class Bot(abc.ABC):
|
||||
@@ -41,7 +39,14 @@ class Bot(abc.ABC):
|
||||
self.self_id: str = self_id
|
||||
"""机器人 ID"""
|
||||
|
||||
def __getattr__(self, name: str) -> _ApiCall:
|
||||
def __repr__(self) -> str:
|
||||
return f"Bot(type={self.type!r}, self_id={self.self_id!r})"
|
||||
|
||||
def __getattr__(self, name: str) -> "_ApiCall":
|
||||
if name.startswith("__") and name.endswith("__"):
|
||||
raise AttributeError(
|
||||
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
||||
)
|
||||
return partial(self.call_api, name)
|
||||
|
||||
@property
|
||||
@@ -72,8 +77,7 @@ class Bot(abc.ABC):
|
||||
skip_calling_api: bool = False
|
||||
exception: Optional[Exception] = None
|
||||
|
||||
coros = list(map(lambda x: x(self, api, data), self._calling_api_hook))
|
||||
if coros:
|
||||
if coros := [hook(self, api, data) for hook in self._calling_api_hook]:
|
||||
try:
|
||||
logger.debug("Running CallingAPI hooks...")
|
||||
await asyncio.gather(*coros)
|
||||
@@ -95,10 +99,9 @@ class Bot(abc.ABC):
|
||||
except Exception as e:
|
||||
exception = e
|
||||
|
||||
coros = list(
|
||||
map(lambda x: x(self, exception, api, data, result), self._called_api_hook)
|
||||
)
|
||||
if coros:
|
||||
if coros := [
|
||||
hook(self, exception, api, data, result) for hook in self._called_api_hook
|
||||
]:
|
||||
try:
|
||||
logger.debug("Running CalledAPI hooks...")
|
||||
await asyncio.gather(*coros)
|
||||
|
@@ -186,7 +186,7 @@ class Message(List[TMS], abc.ABC):
|
||||
elif isinstance(other, Iterable):
|
||||
self.extend(other)
|
||||
else:
|
||||
raise ValueError(f"Unsupported type: {type(other)}") # pragma: no cover
|
||||
raise TypeError(f"Unsupported type {type(other)!r}")
|
||||
return self
|
||||
|
||||
@overload
|
||||
|
@@ -56,6 +56,9 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
self.factory: Type[TF] = factory
|
||||
self.format_specs: Dict[str, FormatSpecFunc] = {}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"MessageTemplate({self.template!r}, factory={self.factory!r})"
|
||||
|
||||
def add_format_spec(
|
||||
self, spec: FormatSpecFunc_T, name: Optional[str] = None
|
||||
) -> FormatSpecFunc_T:
|
||||
|
@@ -40,12 +40,18 @@ class Driver(abc.ABC):
|
||||
"""环境名称"""
|
||||
self.config: Config = config
|
||||
"""全局配置对象"""
|
||||
self._clients: Dict[str, "Bot"] = {}
|
||||
self._bots: Dict[str, "Bot"] = {}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"Driver(type={self.type!r}, "
|
||||
f"adapters={len(self._adapters)}, bots={len(self._bots)})"
|
||||
)
|
||||
|
||||
@property
|
||||
def bots(self) -> Dict[str, "Bot"]:
|
||||
"""获取当前所有已连接的 Bot"""
|
||||
return self._clients
|
||||
return self._bots
|
||||
|
||||
def register_adapter(self, adapter: Type["Adapter"], **kwargs) -> None:
|
||||
"""注册一个协议适配器
|
||||
@@ -124,9 +130,9 @@ class Driver(abc.ABC):
|
||||
|
||||
def _bot_connect(self, bot: "Bot") -> None:
|
||||
"""在连接成功后,调用该函数来注册 bot 对象"""
|
||||
if bot.self_id in self._clients:
|
||||
if bot.self_id in self._bots:
|
||||
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
|
||||
self._clients[bot.self_id] = bot
|
||||
self._bots[bot.self_id] = bot
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
coros = list(
|
||||
@@ -148,8 +154,8 @@ class Driver(abc.ABC):
|
||||
|
||||
def _bot_disconnect(self, bot: "Bot") -> None:
|
||||
"""在连接断开后,调用该函数来注销 bot 对象"""
|
||||
if bot.self_id in self._clients:
|
||||
del self._clients[bot.self_id]
|
||||
if bot.self_id in self._bots:
|
||||
del self._bots[bot.self_id]
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
coros = list(
|
||||
@@ -233,13 +239,11 @@ def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Dr
|
||||
if not mixins:
|
||||
return driver
|
||||
|
||||
class CombinedDriver(*mixins, driver, ForwardDriver): # type: ignore
|
||||
@property
|
||||
def type(self) -> str:
|
||||
return (
|
||||
driver.type.__get__(self)
|
||||
+ "+"
|
||||
+ "+".join(map(lambda x: x.type.__get__(self), mixins))
|
||||
)
|
||||
def type_(self: ForwardDriver) -> str:
|
||||
return (
|
||||
driver.type.__get__(self)
|
||||
+ "+"
|
||||
+ "+".join(map(lambda x: x.type.__get__(self), mixins))
|
||||
)
|
||||
|
||||
return CombinedDriver
|
||||
return type("CombinedDriver", (*mixins, driver, ForwardDriver), {"type": property(type_)}) # type: ignore
|
||||
|
@@ -131,9 +131,7 @@ class Request:
|
||||
self.files.append((name, file_info)) # type: ignore
|
||||
|
||||
def __repr__(self) -> str:
|
||||
class_name = self.__class__.__name__
|
||||
url = str(self.url)
|
||||
return f"<{class_name}({self.method!r}, {url!r})>"
|
||||
return f"{self.__class__.__name__}(method={self.method!r}, url='{self.url!s}')"
|
||||
|
||||
|
||||
class Response:
|
||||
@@ -161,12 +159,18 @@ class Response:
|
||||
# request
|
||||
self.request: Optional[Request] = request
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(status_code={self.status_code!r})"
|
||||
|
||||
|
||||
class WebSocket(abc.ABC):
|
||||
def __init__(self, *, request: Request):
|
||||
# request
|
||||
self.request: Request = request
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}('{self.request.url!s}')"
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def closed(self) -> bool:
|
||||
@@ -320,17 +324,14 @@ class Cookies(MutableMapping):
|
||||
return len(self.jar)
|
||||
|
||||
def __iter__(self) -> Iterator[Cookie]:
|
||||
return (cookie for cookie in self.jar)
|
||||
return iter(self.jar)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
cookies_repr = ", ".join(
|
||||
[
|
||||
f"<Cookie {cookie.name}={cookie.value} for {cookie.domain} />"
|
||||
for cookie in self.jar
|
||||
]
|
||||
f"Cookie({cookie.name}={cookie.value} for {cookie.domain})"
|
||||
for cookie in self.jar
|
||||
)
|
||||
|
||||
return f"<Cookies [{cookies_repr}]>"
|
||||
return f"{self.__class__.__name__}({cookies_repr})"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
11
nonebot/internal/matcher/__init__.py
Normal file
11
nonebot/internal/matcher/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .manager import MatcherManager as MatcherManager
|
||||
from .provider import MatcherProvider as MatcherProvider
|
||||
from .provider import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
||||
|
||||
matchers = MatcherManager()
|
||||
|
||||
from .matcher import Matcher as Matcher
|
||||
from .matcher import current_bot as current_bot
|
||||
from .matcher import current_event as current_event
|
||||
from .matcher import current_handler as current_handler
|
||||
from .matcher import current_matcher as current_matcher
|
104
nonebot/internal/matcher/manager.py
Normal file
104
nonebot/internal/matcher/manager.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
List,
|
||||
Type,
|
||||
Tuple,
|
||||
Union,
|
||||
TypeVar,
|
||||
Iterator,
|
||||
KeysView,
|
||||
Optional,
|
||||
ItemsView,
|
||||
ValuesView,
|
||||
MutableMapping,
|
||||
overload,
|
||||
)
|
||||
|
||||
from .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .matcher import Matcher
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class MatcherManager(MutableMapping[int, List[Type["Matcher"]]]):
|
||||
"""事件响应器管理器
|
||||
|
||||
实现了常用字典操作,用于管理事件响应器。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.provider: MatcherProvider = DEFAULT_PROVIDER_CLASS({})
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"MatcherManager(provider={self.provider!r})"
|
||||
|
||||
def __contains__(self, o: object) -> bool:
|
||||
return o in self.provider
|
||||
|
||||
def __iter__(self) -> Iterator[int]:
|
||||
return iter(self.provider)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.provider)
|
||||
|
||||
def __getitem__(self, key: int) -> List[Type["Matcher"]]:
|
||||
return self.provider[key]
|
||||
|
||||
def __setitem__(self, key: int, value: List[Type["Matcher"]]) -> None:
|
||||
self.provider[key] = value
|
||||
|
||||
def __delitem__(self, key: int) -> None:
|
||||
del self.provider[key]
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, MatcherManager) and self.provider == other.provider
|
||||
|
||||
def keys(self) -> KeysView[int]:
|
||||
return self.provider.keys()
|
||||
|
||||
def values(self) -> ValuesView[List[Type["Matcher"]]]:
|
||||
return self.provider.values()
|
||||
|
||||
def items(self) -> ItemsView[int, List[Type["Matcher"]]]:
|
||||
return self.provider.items()
|
||||
|
||||
@overload
|
||||
def get(self, key: int) -> Optional[List[Type["Matcher"]]]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def get(self, key: int, default: T) -> Union[List[Type["Matcher"]], T]:
|
||||
...
|
||||
|
||||
def get(
|
||||
self, key: int, default: Optional[T] = None
|
||||
) -> Optional[Union[List[Type["Matcher"]], T]]:
|
||||
return self.provider.get(key, default)
|
||||
|
||||
def pop(self, key: int) -> List[Type["Matcher"]]:
|
||||
return self.provider.pop(key)
|
||||
|
||||
def popitem(self) -> Tuple[int, List[Type["Matcher"]]]:
|
||||
return self.provider.popitem()
|
||||
|
||||
def clear(self) -> None:
|
||||
self.provider.clear()
|
||||
|
||||
def update(self, __m: MutableMapping[int, List[Type["Matcher"]]]) -> None:
|
||||
self.provider.update(__m)
|
||||
|
||||
def setdefault(
|
||||
self, key: int, default: List[Type["Matcher"]]
|
||||
) -> List[Type["Matcher"]]:
|
||||
return self.provider.setdefault(key, default)
|
||||
|
||||
def set_provider(self, provider_class: Type[MatcherProvider]) -> None:
|
||||
"""设置事件响应器存储器
|
||||
|
||||
参数:
|
||||
provider_class: 事件响应器存储器类
|
||||
"""
|
||||
self.provider = provider_class(self.provider)
|
@@ -1,23 +1,32 @@
|
||||
from types import ModuleType
|
||||
from contextvars import ContextVar
|
||||
from collections import defaultdict
|
||||
from contextlib import AsyncExitStack
|
||||
from datetime import datetime, timedelta
|
||||
from contextlib import AsyncExitStack, contextmanager
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Type,
|
||||
Union,
|
||||
TypeVar,
|
||||
Callable,
|
||||
Iterable,
|
||||
NoReturn,
|
||||
Optional,
|
||||
overload,
|
||||
)
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.internal.rule import Rule
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.internal.permission import USER, User, Permission
|
||||
from nonebot.internal.adapter import (
|
||||
Bot,
|
||||
Event,
|
||||
Message,
|
||||
MessageSegment,
|
||||
MessageTemplate,
|
||||
)
|
||||
from nonebot.consts import (
|
||||
ARG_KEY,
|
||||
RECEIVE_KEY,
|
||||
@@ -34,18 +43,13 @@ from nonebot.typing import (
|
||||
T_PermissionUpdater,
|
||||
)
|
||||
from nonebot.exception import (
|
||||
TypeMisMatch,
|
||||
PausedException,
|
||||
StopPropagation,
|
||||
SkippedException,
|
||||
FinishedException,
|
||||
RejectedException,
|
||||
)
|
||||
|
||||
from .rule import Rule
|
||||
from .permission import USER, Permission
|
||||
from .adapter import Bot, Event, Message, MessageSegment, MessageTemplate
|
||||
from .params import (
|
||||
from nonebot.internal.params import (
|
||||
Depends,
|
||||
ArgParam,
|
||||
BotParam,
|
||||
@@ -56,13 +60,13 @@ from .params import (
|
||||
MatcherParam,
|
||||
)
|
||||
|
||||
from . import matchers
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
matchers: Dict[int, List[Type["Matcher"]]] = defaultdict(list)
|
||||
"""用于存储当前所有的事件响应器"""
|
||||
current_bot: ContextVar[Bot] = ContextVar("current_bot")
|
||||
current_event: ContextVar[Event] = ContextVar("current_event")
|
||||
current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
|
||||
@@ -71,29 +75,16 @@ current_handler: ContextVar[Dependent] = ContextVar("current_handler")
|
||||
|
||||
class MatcherMeta(type):
|
||||
if TYPE_CHECKING:
|
||||
module: Optional[str]
|
||||
plugin_name: Optional[str]
|
||||
module_name: Optional[str]
|
||||
module_prefix: Optional[str]
|
||||
type: str
|
||||
rule: Rule
|
||||
permission: Permission
|
||||
handlers: List[T_Handler]
|
||||
priority: int
|
||||
block: bool
|
||||
temp: bool
|
||||
expire_time: Optional[datetime]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Matcher from {self.module_name or 'unknown'}, "
|
||||
f"type={self.type}, priority={self.priority}, "
|
||||
f"temp={self.temp}>"
|
||||
f"Matcher(type={self.type!r}"
|
||||
+ (f", module={self.module_name}" if self.module_name else "")
|
||||
+ ")"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return repr(self)
|
||||
|
||||
|
||||
class Matcher(metaclass=MatcherMeta):
|
||||
"""事件响应器类"""
|
||||
@@ -132,7 +123,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
_default_permission_updater: Optional[Dependent[Permission]] = None
|
||||
"""事件响应器权限更新函数"""
|
||||
|
||||
HANDLER_PARAM_TYPES = [
|
||||
HANDLER_PARAM_TYPES = (
|
||||
DependParam,
|
||||
BotParam,
|
||||
EventParam,
|
||||
@@ -140,7 +131,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
ArgParam,
|
||||
MatcherParam,
|
||||
DefaultParam,
|
||||
]
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = self.handlers.copy()
|
||||
@@ -148,13 +139,11 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Matcher from {self.module_name or 'unknown'}, type={self.type}, "
|
||||
f"priority={self.priority}, temp={self.temp}>"
|
||||
f"Matcher(type={self.type!r}"
|
||||
+ (f", module={self.module_name}" if self.module_name else "")
|
||||
+ ")"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return repr(self)
|
||||
|
||||
@classmethod
|
||||
def new(
|
||||
cls,
|
||||
@@ -218,27 +207,35 @@ class Matcher(metaclass=MatcherMeta):
|
||||
"temp": temp,
|
||||
"expire_time": (
|
||||
expire_time
|
||||
if isinstance(expire_time, datetime)
|
||||
else expire_time and datetime.now() + expire_time
|
||||
and (
|
||||
expire_time
|
||||
if isinstance(expire_time, datetime)
|
||||
else datetime.now() + expire_time
|
||||
)
|
||||
),
|
||||
"priority": priority,
|
||||
"block": block,
|
||||
"_default_state": default_state or {},
|
||||
"_default_type_updater": (
|
||||
default_type_updater
|
||||
if isinstance(default_type_updater, Dependent)
|
||||
else default_type_updater
|
||||
and Dependent[str].parse(
|
||||
call=default_type_updater, allow_types=cls.HANDLER_PARAM_TYPES
|
||||
and (
|
||||
default_type_updater
|
||||
if isinstance(default_type_updater, Dependent)
|
||||
else Dependent[str].parse(
|
||||
call=default_type_updater,
|
||||
allow_types=cls.HANDLER_PARAM_TYPES,
|
||||
)
|
||||
)
|
||||
),
|
||||
"_default_permission_updater": (
|
||||
default_permission_updater
|
||||
if isinstance(default_permission_updater, Dependent)
|
||||
else default_permission_updater
|
||||
and Dependent[Permission].parse(
|
||||
call=default_permission_updater,
|
||||
allow_types=cls.HANDLER_PARAM_TYPES,
|
||||
and (
|
||||
default_permission_updater
|
||||
if isinstance(default_permission_updater, Dependent)
|
||||
else Dependent[Permission].parse(
|
||||
call=default_permission_updater,
|
||||
allow_types=cls.HANDLER_PARAM_TYPES,
|
||||
)
|
||||
)
|
||||
),
|
||||
},
|
||||
@@ -326,7 +323,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
@classmethod
|
||||
def append_handler(
|
||||
cls, handler: T_Handler, parameterless: Optional[List[Any]] = None
|
||||
cls, handler: T_Handler, parameterless: Optional[Iterable[Any]] = None
|
||||
) -> Dependent[Any]:
|
||||
handler_ = Dependent[Any].parse(
|
||||
call=handler,
|
||||
@@ -338,7 +335,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
@classmethod
|
||||
def handle(
|
||||
cls, parameterless: Optional[List[Any]] = None
|
||||
cls, parameterless: Optional[Iterable[Any]] = None
|
||||
) -> Callable[[T_Handler], T_Handler]:
|
||||
"""装饰一个函数来向事件响应器直接添加一个处理函数
|
||||
|
||||
@@ -354,7 +351,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
@classmethod
|
||||
def receive(
|
||||
cls, id: str = "", parameterless: Optional[List[Any]] = None
|
||||
cls, id: str = "", parameterless: Optional[Iterable[Any]] = None
|
||||
) -> Callable[[T_Handler], T_Handler]:
|
||||
"""装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数
|
||||
|
||||
@@ -372,14 +369,21 @@ class Matcher(metaclass=MatcherMeta):
|
||||
return
|
||||
await matcher.reject()
|
||||
|
||||
_parameterless = [Depends(_receive), *(parameterless or [])]
|
||||
_parameterless = (Depends(_receive), *(parameterless or tuple()))
|
||||
|
||||
def _decorator(func: T_Handler) -> T_Handler:
|
||||
|
||||
if cls.handlers and cls.handlers[-1].call is func:
|
||||
func_handler = cls.handlers[-1]
|
||||
for depend in reversed(_parameterless):
|
||||
func_handler.prepend_parameterless(depend)
|
||||
new_handler = Dependent(
|
||||
call=func_handler.call,
|
||||
params=func_handler.params,
|
||||
parameterless=Dependent.parse_parameterless(
|
||||
tuple(_parameterless), cls.HANDLER_PARAM_TYPES
|
||||
)
|
||||
+ func_handler.parameterless,
|
||||
)
|
||||
cls.handlers[-1] = new_handler
|
||||
else:
|
||||
cls.append_handler(func, parameterless=_parameterless)
|
||||
|
||||
@@ -392,7 +396,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
cls,
|
||||
key: str,
|
||||
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
||||
parameterless: Optional[List[Any]] = None,
|
||||
parameterless: Optional[Iterable[Any]] = None,
|
||||
) -> Callable[[T_Handler], T_Handler]:
|
||||
"""装饰一个函数来指示 NoneBot 获取一个参数 `key`
|
||||
|
||||
@@ -413,17 +417,21 @@ class Matcher(metaclass=MatcherMeta):
|
||||
return
|
||||
await matcher.reject(prompt)
|
||||
|
||||
_parameterless = [
|
||||
Depends(_key_getter),
|
||||
*(parameterless or []),
|
||||
]
|
||||
_parameterless = (Depends(_key_getter), *(parameterless or tuple()))
|
||||
|
||||
def _decorator(func: T_Handler) -> T_Handler:
|
||||
|
||||
if cls.handlers and cls.handlers[-1].call is func:
|
||||
func_handler = cls.handlers[-1]
|
||||
for depend in reversed(_parameterless):
|
||||
func_handler.prepend_parameterless(depend)
|
||||
new_handler = Dependent(
|
||||
call=func_handler.call,
|
||||
params=func_handler.params,
|
||||
parameterless=Dependent.parse_parameterless(
|
||||
tuple(_parameterless), cls.HANDLER_PARAM_TYPES
|
||||
)
|
||||
+ func_handler.parameterless,
|
||||
)
|
||||
cls.handlers[-1] = new_handler
|
||||
else:
|
||||
cls.append_handler(func, parameterless=_parameterless)
|
||||
|
||||
@@ -551,7 +559,17 @@ class Matcher(metaclass=MatcherMeta):
|
||||
"""
|
||||
raise SkippedException
|
||||
|
||||
def get_receive(self, id: str, default: T = None) -> Union[Event, T]:
|
||||
@overload
|
||||
def get_receive(self, id: str) -> Union[Event, None]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def get_receive(self, id: str, default: T) -> Union[Event, T]:
|
||||
...
|
||||
|
||||
def get_receive(
|
||||
self, id: str, default: Optional[T] = None
|
||||
) -> Optional[Union[Event, T]]:
|
||||
"""获取一个 `receive` 事件
|
||||
|
||||
如果没有找到对应的事件,返回 `default` 值
|
||||
@@ -563,14 +581,34 @@ class Matcher(metaclass=MatcherMeta):
|
||||
self.state[RECEIVE_KEY.format(id=id)] = event
|
||||
self.state[LAST_RECEIVE_KEY] = event
|
||||
|
||||
def get_last_receive(self, default: T = None) -> Union[Event, T]:
|
||||
@overload
|
||||
def get_last_receive(self) -> Union[Event, None]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def get_last_receive(self, default: T) -> Union[Event, T]:
|
||||
...
|
||||
|
||||
def get_last_receive(
|
||||
self, default: Optional[T] = None
|
||||
) -> Optional[Union[Event, T]]:
|
||||
"""获取最近一次 `receive` 事件
|
||||
|
||||
如果没有事件,返回 `default` 值
|
||||
"""
|
||||
return self.state.get(LAST_RECEIVE_KEY, default)
|
||||
|
||||
def get_arg(self, key: str, default: T = None) -> Union[Message, T]:
|
||||
@overload
|
||||
def get_arg(self, key: str) -> Union[Message, None]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def get_arg(self, key: str, default: T) -> Union[Message, T]:
|
||||
...
|
||||
|
||||
def get_arg(
|
||||
self, key: str, default: Optional[T] = None
|
||||
) -> Optional[Union[Message, T]]:
|
||||
"""获取一个 `got` 消息
|
||||
|
||||
如果没有找到对应的消息,返回 `default` 值
|
||||
@@ -587,7 +625,15 @@ class Matcher(metaclass=MatcherMeta):
|
||||
else:
|
||||
self.state[REJECT_TARGET] = target
|
||||
|
||||
def get_target(self, default: T = None) -> Union[str, T]:
|
||||
@overload
|
||||
def get_target(self) -> Union[str, None]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def get_target(self, default: T) -> Union[str, T]:
|
||||
...
|
||||
|
||||
def get_target(self, default: Optional[T] = None) -> Optional[Union[str, T]]:
|
||||
return self.state.get(REJECT_TARGET, default)
|
||||
|
||||
def stop_propagation(self):
|
||||
@@ -596,15 +642,21 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
async def update_type(self, bot: Bot, event: Event) -> str:
|
||||
updater = self.__class__._default_type_updater
|
||||
if not updater:
|
||||
return "message"
|
||||
return await updater(bot=bot, event=event, state=self.state, matcher=self)
|
||||
return (
|
||||
await updater(bot=bot, event=event, state=self.state, matcher=self)
|
||||
if updater
|
||||
else "message"
|
||||
)
|
||||
|
||||
async def update_permission(self, bot: Bot, event: Event) -> Permission:
|
||||
updater = self.__class__._default_permission_updater
|
||||
if not updater:
|
||||
return USER(event.get_session_id(), perm=self.permission)
|
||||
return await updater(bot=bot, event=event, state=self.state, matcher=self)
|
||||
if updater := self.__class__._default_permission_updater:
|
||||
return await updater(bot=bot, event=event, state=self.state, matcher=self)
|
||||
permission = self.permission
|
||||
if len(permission.checkers) == 1 and isinstance(
|
||||
user_perm := tuple(permission.checkers)[0].call, User
|
||||
):
|
||||
permission = user_perm.perm
|
||||
return USER(event.get_session_id(), perm=permission)
|
||||
|
||||
async def resolve_reject(self):
|
||||
handler = current_handler.get()
|
||||
@@ -612,6 +664,18 @@ class Matcher(metaclass=MatcherMeta):
|
||||
if REJECT_CACHE_TARGET in self.state:
|
||||
self.state[REJECT_TARGET] = self.state[REJECT_CACHE_TARGET]
|
||||
|
||||
@contextmanager
|
||||
def ensure_context(self, bot: Bot, event: Event):
|
||||
b_t = current_bot.set(bot)
|
||||
e_t = current_event.set(event)
|
||||
m_t = current_matcher.set(self)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
current_bot.reset(b_t)
|
||||
current_event.reset(e_t)
|
||||
current_matcher.reset(m_t)
|
||||
|
||||
async def simple_run(
|
||||
self,
|
||||
bot: Bot,
|
||||
@@ -621,43 +685,34 @@ class Matcher(metaclass=MatcherMeta):
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
):
|
||||
logger.trace(
|
||||
f"Matcher {self} run with incoming args: "
|
||||
f"bot={bot}, event={event}, state={state}"
|
||||
f"{self} run with incoming args: "
|
||||
f"bot={bot}, event={event!r}, state={state!r}"
|
||||
)
|
||||
b_t = current_bot.set(bot)
|
||||
e_t = current_event.set(event)
|
||||
m_t = current_matcher.set(self)
|
||||
try:
|
||||
# Refresh preprocess state
|
||||
self.state.update(state)
|
||||
|
||||
while self.handlers:
|
||||
handler = self.handlers.pop(0)
|
||||
current_handler.set(handler)
|
||||
logger.debug(f"Running handler {handler}")
|
||||
try:
|
||||
await handler(
|
||||
matcher=self,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
except TypeMisMatch as e:
|
||||
logger.debug(
|
||||
f"Handler {handler} param {e.param.name} value {e.value} "
|
||||
f"mismatch type {e.param._type_display()}, skipped"
|
||||
)
|
||||
except SkippedException as e:
|
||||
logger.debug(f"Handler {handler} skipped")
|
||||
except StopPropagation:
|
||||
self.block = True
|
||||
finally:
|
||||
logger.info(f"Matcher {self} running complete")
|
||||
current_bot.reset(b_t)
|
||||
current_event.reset(e_t)
|
||||
current_matcher.reset(m_t)
|
||||
with self.ensure_context(bot, event):
|
||||
try:
|
||||
# Refresh preprocess state
|
||||
self.state.update(state)
|
||||
|
||||
while self.handlers:
|
||||
handler = self.handlers.pop(0)
|
||||
current_handler.set(handler)
|
||||
logger.debug(f"Running handler {handler}")
|
||||
try:
|
||||
await handler(
|
||||
matcher=self,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
except SkippedException:
|
||||
logger.debug(f"Handler {handler} skipped")
|
||||
except StopPropagation:
|
||||
self.block = True
|
||||
finally:
|
||||
logger.info(f"{self} running complete")
|
||||
|
||||
# 运行handlers
|
||||
async def run(
|
||||
@@ -712,14 +767,3 @@ class Matcher(metaclass=MatcherMeta):
|
||||
)
|
||||
except FinishedException:
|
||||
pass
|
||||
|
||||
|
||||
__autodoc__ = {
|
||||
"MatcherMeta": False,
|
||||
"Matcher.get_target": False,
|
||||
"Matcher.set_target": False,
|
||||
"Matcher.update_type": False,
|
||||
"Matcher.update_permission": False,
|
||||
"Matcher.resolve_reject": False,
|
||||
"Matcher.simple_run": False,
|
||||
}
|
27
nonebot/internal/matcher/provider.py
Normal file
27
nonebot/internal/matcher/provider.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import abc
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, List, Type, Mapping, MutableMapping
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .matcher import Matcher
|
||||
|
||||
|
||||
class MatcherProvider(abc.ABC, MutableMapping[int, List[Type["Matcher"]]]):
|
||||
"""事件响应器存储器基类
|
||||
|
||||
参数:
|
||||
matchers: 当前存储器中已有的事件响应器
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __init__(self, matchers: Mapping[int, List[Type["Matcher"]]]):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _DictProvider(defaultdict, MatcherProvider):
|
||||
def __init__(self, matchers: Mapping[int, List[Type["Matcher"]]]):
|
||||
super().__init__(list, matchers)
|
||||
|
||||
|
||||
DEFAULT_PROVIDER_CLASS = _DictProvider
|
||||
"""默认存储器类型"""
|
@@ -1,14 +1,10 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import warnings
|
||||
from typing_extensions import Literal
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, cast
|
||||
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Type, Tuple, Literal, Callable, Optional, cast
|
||||
|
||||
from pydantic.fields import Required, Undefined, ModelField
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.exception import TypeMisMatch
|
||||
from nonebot.dependencies.utils import check_field_type
|
||||
from nonebot.dependencies import Param, Dependent, CustomConfig
|
||||
from nonebot.typing import T_State, T_Handler, T_DependencyCache
|
||||
@@ -40,7 +36,7 @@ class DependsInner:
|
||||
def __repr__(self) -> str:
|
||||
dep = get_name(self.dependency)
|
||||
cache = "" if self.use_cache else ", use_cache=False"
|
||||
return f"{self.__class__.__name__}({dep}{cache})"
|
||||
return f"DependsInner({dep}{cache})"
|
||||
|
||||
|
||||
def Depends(
|
||||
@@ -75,12 +71,12 @@ def Depends(
|
||||
class DependParam(Param):
|
||||
"""子依赖参数"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Depends({self.extra['dependent']})"
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls,
|
||||
dependent: Dependent,
|
||||
name: str,
|
||||
param: inspect.Parameter,
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["DependParam"]:
|
||||
if isinstance(param.default, DependsInner):
|
||||
dependency: T_Handler
|
||||
@@ -91,22 +87,20 @@ class DependParam(Param):
|
||||
dependency = param.default.dependency
|
||||
sub_dependent = Dependent[Any].parse(
|
||||
call=dependency,
|
||||
allow_types=dependent.allow_types,
|
||||
allow_types=allow_types,
|
||||
)
|
||||
dependent.pre_checkers.extend(sub_dependent.pre_checkers)
|
||||
sub_dependent.pre_checkers.clear()
|
||||
return cls(
|
||||
Required, use_cache=param.default.use_cache, dependent=sub_dependent
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _check_parameterless(
|
||||
cls, dependent: "Dependent", value: Any
|
||||
cls, value: Any, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["Param"]:
|
||||
if isinstance(value, DependsInner):
|
||||
assert value.dependency, "Dependency cannot be empty"
|
||||
dependent = Dependent[Any].parse(
|
||||
call=value.dependency, allow_types=dependent.allow_types
|
||||
call=value.dependency, allow_types=allow_types
|
||||
)
|
||||
return cls(Required, use_cache=value.use_cache, dependent=dependent)
|
||||
|
||||
@@ -120,8 +114,7 @@ class DependParam(Param):
|
||||
dependency_cache = {} if dependency_cache is None else dependency_cache
|
||||
|
||||
sub_dependent: Dependent = self.extra["dependent"]
|
||||
sub_dependent.call = cast(Callable[..., Any], sub_dependent.call)
|
||||
call = sub_dependent.call
|
||||
call = cast(Callable[..., Any], sub_dependent.call)
|
||||
|
||||
# solve sub dependency with current cache
|
||||
sub_values = await sub_dependent.solve(
|
||||
@@ -133,7 +126,7 @@ class DependParam(Param):
|
||||
# run dependency function
|
||||
task: asyncio.Task[Any]
|
||||
if use_cache and call in dependency_cache:
|
||||
solved = await dependency_cache[call]
|
||||
return await dependency_cache[call]
|
||||
elif is_gen_callable(call) or is_async_gen_callable(call):
|
||||
assert isinstance(
|
||||
stack, AsyncExitStack
|
||||
@@ -144,134 +137,124 @@ class DependParam(Param):
|
||||
cm = asynccontextmanager(call)(**sub_values)
|
||||
task = asyncio.create_task(stack.enter_async_context(cm))
|
||||
dependency_cache[call] = task
|
||||
solved = await task
|
||||
return await task
|
||||
elif is_coroutine_callable(call):
|
||||
task = asyncio.create_task(call(**sub_values))
|
||||
dependency_cache[call] = task
|
||||
solved = await task
|
||||
return await task
|
||||
else:
|
||||
task = asyncio.create_task(run_sync(call)(**sub_values))
|
||||
dependency_cache[call] = task
|
||||
solved = await task
|
||||
return await task
|
||||
|
||||
return solved
|
||||
|
||||
|
||||
class _BotChecker(Param):
|
||||
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
|
||||
field: ModelField = self.extra["field"]
|
||||
try:
|
||||
return check_field_type(field, bot)
|
||||
except TypeMisMatch:
|
||||
logger.debug(
|
||||
f"Bot type {type(bot)} not match "
|
||||
f"annotation {field._type_display()}, ignored"
|
||||
)
|
||||
raise
|
||||
async def _check(self, **kwargs: Any) -> None:
|
||||
# run sub dependent pre-checkers
|
||||
sub_dependent: Dependent = self.extra["dependent"]
|
||||
await sub_dependent.check(**kwargs)
|
||||
|
||||
|
||||
class BotParam(Param):
|
||||
"""{ref}`nonebot.adapters.Bot` 参数"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"BotParam("
|
||||
+ (
|
||||
repr(cast(ModelField, checker).type_)
|
||||
if (checker := self.extra.get("checker"))
|
||||
else ""
|
||||
)
|
||||
+ ")"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["BotParam"]:
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
if param.default == param.empty:
|
||||
if generic_check_issubclass(param.annotation, Bot):
|
||||
checker: Optional[ModelField] = None
|
||||
if param.annotation is not Bot:
|
||||
dependent.pre_checkers.append(
|
||||
_BotChecker(
|
||||
Required,
|
||||
field=ModelField(
|
||||
name=name,
|
||||
type_=param.annotation,
|
||||
class_validators=None,
|
||||
model_config=CustomConfig,
|
||||
default=None,
|
||||
required=True,
|
||||
),
|
||||
)
|
||||
checker = ModelField(
|
||||
name=param.name,
|
||||
type_=param.annotation,
|
||||
class_validators=None,
|
||||
model_config=CustomConfig,
|
||||
default=None,
|
||||
required=True,
|
||||
)
|
||||
return cls(Required)
|
||||
elif param.annotation == param.empty and name == "bot":
|
||||
return cls(Required, checker=checker)
|
||||
elif param.annotation == param.empty and param.name == "bot":
|
||||
return cls(Required)
|
||||
|
||||
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
|
||||
return bot
|
||||
|
||||
|
||||
class _EventChecker(Param):
|
||||
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
|
||||
field: ModelField = self.extra["field"]
|
||||
try:
|
||||
return check_field_type(field, event)
|
||||
except TypeMisMatch:
|
||||
logger.debug(
|
||||
f"Event type {type(event)} not match "
|
||||
f"annotation {field._type_display()}, ignored"
|
||||
)
|
||||
raise
|
||||
async def _check(self, bot: "Bot", **kwargs: Any) -> None:
|
||||
if checker := self.extra.get("checker"):
|
||||
check_field_type(checker, bot)
|
||||
|
||||
|
||||
class EventParam(Param):
|
||||
"""{ref}`nonebot.adapters.Event` 参数"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"EventParam("
|
||||
+ (
|
||||
repr(cast(ModelField, checker).type_)
|
||||
if (checker := self.extra.get("checker"))
|
||||
else ""
|
||||
)
|
||||
+ ")"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["EventParam"]:
|
||||
from nonebot.adapters import Event
|
||||
|
||||
if param.default == param.empty:
|
||||
if generic_check_issubclass(param.annotation, Event):
|
||||
checker: Optional[ModelField] = None
|
||||
if param.annotation is not Event:
|
||||
dependent.pre_checkers.append(
|
||||
_EventChecker(
|
||||
Required,
|
||||
field=ModelField(
|
||||
name=name,
|
||||
type_=param.annotation,
|
||||
class_validators=None,
|
||||
model_config=CustomConfig,
|
||||
default=None,
|
||||
required=True,
|
||||
),
|
||||
)
|
||||
checker = ModelField(
|
||||
name=param.name,
|
||||
type_=param.annotation,
|
||||
class_validators=None,
|
||||
model_config=CustomConfig,
|
||||
default=None,
|
||||
required=True,
|
||||
)
|
||||
return cls(Required)
|
||||
elif param.annotation == param.empty and name == "event":
|
||||
return cls(Required, checker=checker)
|
||||
elif param.annotation == param.empty and param.name == "event":
|
||||
return cls(Required)
|
||||
|
||||
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
|
||||
return event
|
||||
|
||||
|
||||
class StateInner(T_State):
|
||||
...
|
||||
|
||||
|
||||
def State() -> T_State:
|
||||
"""**Deprecated**: 事件处理状态参数,请直接使用 {ref}`nonebot.typing.T_State`"""
|
||||
warnings.warn("State() is deprecated, use `T_State` instead", DeprecationWarning)
|
||||
return StateInner()
|
||||
async def _check(self, event: "Event", **kwargs: Any) -> Any:
|
||||
if checker := self.extra.get("checker", None):
|
||||
check_field_type(checker, event)
|
||||
|
||||
|
||||
class StateParam(Param):
|
||||
"""事件处理状态参数"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "StateParam()"
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["StateParam"]:
|
||||
if isinstance(param.default, StateInner):
|
||||
return cls(Required)
|
||||
elif param.default == param.empty:
|
||||
if param.default == param.empty:
|
||||
if param.annotation is T_State:
|
||||
return cls(Required)
|
||||
elif param.annotation == param.empty and name == "state":
|
||||
elif param.annotation == param.empty and param.name == "state":
|
||||
return cls(Required)
|
||||
|
||||
async def _solve(self, state: T_State, **kwargs: Any) -> Any:
|
||||
@@ -281,14 +264,17 @@ class StateParam(Param):
|
||||
class MatcherParam(Param):
|
||||
"""事件响应器实例参数"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "MatcherParam()"
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["MatcherParam"]:
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
if generic_check_issubclass(param.annotation, Matcher) or (
|
||||
param.annotation == param.empty and name == "matcher"
|
||||
param.annotation == param.empty and param.name == "matcher"
|
||||
):
|
||||
return cls(Required)
|
||||
|
||||
@@ -303,6 +289,9 @@ class ArgInner:
|
||||
self.key = key
|
||||
self.type = type
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ArgInner(key={self.key!r}, type={self.type!r})"
|
||||
|
||||
|
||||
def Arg(key: Optional[str] = None) -> Any:
|
||||
"""`got` 的 Arg 参数消息"""
|
||||
@@ -322,12 +311,17 @@ def ArgPlainText(key: Optional[str] = None) -> str:
|
||||
class ArgParam(Param):
|
||||
"""`got` 的 Arg 参数"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ArgParam(key={self.extra['key']!r}, type={self.extra['type']!r})"
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["ArgParam"]:
|
||||
if isinstance(param.default, ArgInner):
|
||||
return cls(Required, key=param.default.key or name, type=param.default.type)
|
||||
return cls(
|
||||
Required, key=param.default.key or param.name, type=param.default.type
|
||||
)
|
||||
|
||||
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||
message = matcher.get_arg(self.extra["key"])
|
||||
@@ -344,12 +338,15 @@ class ArgParam(Param):
|
||||
class ExceptionParam(Param):
|
||||
"""`run_postprocessor` 的异常参数"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "ExceptionParam()"
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["ExceptionParam"]:
|
||||
if generic_check_issubclass(param.annotation, Exception) or (
|
||||
param.annotation == param.empty and name == "exception"
|
||||
param.annotation == param.empty and param.name == "exception"
|
||||
):
|
||||
return cls(Required)
|
||||
|
||||
@@ -360,9 +357,12 @@ class ExceptionParam(Param):
|
||||
class DefaultParam(Param):
|
||||
"""默认值参数"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"DefaultParam(default={self.default!r})"
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["DefaultParam"]:
|
||||
if param.default != param.empty:
|
||||
return cls(param.default)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import Any, Set, Tuple, Union, NoReturn, Optional, Coroutine
|
||||
from typing import Set, Tuple, Union, NoReturn, Optional
|
||||
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.utils import run_coro_with_catch
|
||||
@@ -37,16 +37,19 @@ class Permission:
|
||||
]
|
||||
|
||||
def __init__(self, *checkers: Union[T_PermissionChecker, Dependent[bool]]) -> None:
|
||||
self.checkers: Set[Dependent[bool]] = set(
|
||||
self.checkers: Set[Dependent[bool]] = {
|
||||
checker
|
||||
if isinstance(checker, Dependent)
|
||||
else Dependent[bool].parse(
|
||||
call=checker, allow_types=self.HANDLER_PARAM_TYPES
|
||||
)
|
||||
for checker in checkers
|
||||
)
|
||||
}
|
||||
"""存储 `PermissionChecker`"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Permission({', '.join(repr(checker) for checker in self.checkers)})"
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
bot: Bot,
|
||||
@@ -121,10 +124,20 @@ class User:
|
||||
self.users = users
|
||||
self.perm = perm
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"User(users={self.users}"
|
||||
+ (f", permission={self.perm})" if self.perm else "")
|
||||
+ ")"
|
||||
)
|
||||
|
||||
async def __call__(self, bot: Bot, event: Event) -> bool:
|
||||
try:
|
||||
session = event.get_session_id()
|
||||
except Exception:
|
||||
return False
|
||||
return bool(
|
||||
event.get_session_id() in self.users
|
||||
and (self.perm is None or await self.perm(bot, event))
|
||||
session in self.users and (self.perm is None or await self.perm(bot, event))
|
||||
)
|
||||
|
||||
|
||||
|
@@ -37,16 +37,19 @@ class Rule:
|
||||
]
|
||||
|
||||
def __init__(self, *checkers: Union[T_RuleChecker, Dependent[bool]]) -> None:
|
||||
self.checkers: Set[Dependent[bool]] = set(
|
||||
self.checkers: Set[Dependent[bool]] = {
|
||||
checker
|
||||
if isinstance(checker, Dependent)
|
||||
else Dependent[bool].parse(
|
||||
call=checker, allow_types=self.HANDLER_PARAM_TYPES
|
||||
)
|
||||
for checker in checkers
|
||||
)
|
||||
}
|
||||
"""存储 `RuleChecker`"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Rule({', '.join(repr(checker) for checker in self.checkers)})"
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
bot: Bot,
|
||||
|
@@ -14,16 +14,14 @@ FrontMatter:
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Union
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import loguru
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# avoid sphinx autodoc resolve annotation failed
|
||||
# because loguru module do not have `Logger` class actually
|
||||
from loguru import Logger
|
||||
|
||||
from nonebot.plugin import Plugin
|
||||
from loguru import Logger, Record
|
||||
|
||||
# logger = logging.getLogger("nonebot")
|
||||
logger: "Logger" = loguru.logger
|
||||
@@ -47,26 +45,10 @@ logger: "Logger" = loguru.logger
|
||||
# logger.addHandler(default_handler)
|
||||
|
||||
|
||||
class Filter:
|
||||
def __init__(self) -> None:
|
||||
self.level: Union[int, str] = "INFO"
|
||||
|
||||
def __call__(self, record):
|
||||
module_name: str = record["name"]
|
||||
# TODO: get plugin name instead of module name
|
||||
# module = sys.modules.get(module_name)
|
||||
# if module and hasattr(module, "__plugin__"):
|
||||
# plugin: "Plugin" = getattr(module, "__plugin__")
|
||||
# module_name = plugin.module_name
|
||||
record["name"] = module_name.split(".")[0]
|
||||
levelno = (
|
||||
logger.level(self.level).no if isinstance(self.level, str) else self.level
|
||||
)
|
||||
return record["level"].no >= levelno
|
||||
|
||||
|
||||
class LoguruHandler(logging.Handler): # pragma: no cover
|
||||
def emit(self, record):
|
||||
"""logging 与 loguru 之间的桥梁,将 logging 的日志转发到 loguru。"""
|
||||
|
||||
def emit(self, record: logging.LogRecord):
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
@@ -82,9 +64,13 @@ class LoguruHandler(logging.Handler): # pragma: no cover
|
||||
)
|
||||
|
||||
|
||||
logger.remove()
|
||||
default_filter: Filter = Filter()
|
||||
"""默认日志等级过滤器"""
|
||||
def default_filter(record: "Record"):
|
||||
"""默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。"""
|
||||
log_level = record["extra"].get("nonebot_log_level", "INFO")
|
||||
levelno = logger.level(log_level).no if isinstance(log_level, str) else log_level
|
||||
return record["level"].no >= levelno
|
||||
|
||||
|
||||
default_format: str = (
|
||||
"<g>{time:MM-DD HH:mm:ss}</g> "
|
||||
"[<lvl>{level}</lvl>] "
|
||||
@@ -93,6 +79,8 @@ default_format: str = (
|
||||
"{message}"
|
||||
)
|
||||
"""默认日志格式"""
|
||||
|
||||
logger.remove()
|
||||
logger_id = logger.add(
|
||||
sys.stdout,
|
||||
level=0,
|
||||
@@ -101,4 +89,4 @@ logger_id = logger.add(
|
||||
format=default_format,
|
||||
)
|
||||
|
||||
__autodoc__ = {"Filter": False, "LoguruHandler": False}
|
||||
__autodoc__ = {"logger_id": False}
|
||||
|
@@ -9,10 +9,16 @@ from nonebot.internal.matcher import Matcher as Matcher
|
||||
from nonebot.internal.matcher import matchers as matchers
|
||||
from nonebot.internal.matcher import current_bot as current_bot
|
||||
from nonebot.internal.matcher import current_event as current_event
|
||||
from nonebot.internal.matcher import MatcherManager as MatcherManager
|
||||
from nonebot.internal.matcher import MatcherProvider as MatcherProvider
|
||||
from nonebot.internal.matcher import current_handler as current_handler
|
||||
from nonebot.internal.matcher import current_matcher as current_matcher
|
||||
from nonebot.internal.matcher import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
||||
|
||||
__autodoc__ = {
|
||||
"Matcher": True,
|
||||
"matchers": True,
|
||||
"MatcherManager": True,
|
||||
"MatcherProvider": True,
|
||||
"DEFAULT_PROVIDER_CLASS": True,
|
||||
}
|
||||
|
@@ -8,9 +8,10 @@ FrontMatter:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from datetime import datetime
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional, Coroutine
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.rule import TrieRule
|
||||
@@ -50,14 +51,14 @@ _event_postprocessors: Set[Dependent[Any]] = set()
|
||||
_run_preprocessors: Set[Dependent[Any]] = set()
|
||||
_run_postprocessors: Set[Dependent[Any]] = set()
|
||||
|
||||
EVENT_PCS_PARAMS = [
|
||||
EVENT_PCS_PARAMS = (
|
||||
DependParam,
|
||||
BotParam,
|
||||
EventParam,
|
||||
StateParam,
|
||||
DefaultParam,
|
||||
]
|
||||
RUN_PREPCS_PARAMS = [
|
||||
)
|
||||
RUN_PREPCS_PARAMS = (
|
||||
DependParam,
|
||||
BotParam,
|
||||
EventParam,
|
||||
@@ -65,8 +66,8 @@ RUN_PREPCS_PARAMS = [
|
||||
ArgParam,
|
||||
MatcherParam,
|
||||
DefaultParam,
|
||||
]
|
||||
RUN_POSTPCS_PARAMS = [
|
||||
)
|
||||
RUN_POSTPCS_PARAMS = (
|
||||
DependParam,
|
||||
ExceptionParam,
|
||||
BotParam,
|
||||
@@ -75,7 +76,7 @@ RUN_POSTPCS_PARAMS = [
|
||||
ArgParam,
|
||||
MatcherParam,
|
||||
DefaultParam,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor:
|
||||
@@ -120,10 +121,8 @@ async def _check_matcher(
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> None:
|
||||
if Matcher.expire_time and datetime.now() > Matcher.expire_time:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
matchers[priority].remove(Matcher)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -138,11 +137,8 @@ async def _check_matcher(
|
||||
return
|
||||
|
||||
if Matcher.temp:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
matchers[priority].remove(Matcher)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await _run_matcher(Matcher, bot, event, state, stack, dependency_cache)
|
||||
|
||||
|
||||
@@ -157,73 +153,68 @@ async def _run_matcher(
|
||||
logger.info(f"Event will be handled by {Matcher}")
|
||||
|
||||
matcher = Matcher()
|
||||
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(
|
||||
x(
|
||||
matcher=matcher,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
matcher=matcher,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
_run_preprocessors,
|
||||
(SkippedException,),
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except IgnoredException:
|
||||
logger.opt(colors=True).info(
|
||||
f"Matcher {matcher} running is <b>cancelled</b>"
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPreProcessors. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
return
|
||||
for proc in _run_preprocessors
|
||||
]:
|
||||
# ensure matcher function can be correctly called
|
||||
with matcher.ensure_context(bot, event):
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except IgnoredException:
|
||||
logger.opt(colors=True).info(f"{matcher} running is <b>cancelled</b>")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPreProcessors. Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
exception = None
|
||||
|
||||
try:
|
||||
logger.debug(f"Running matcher {matcher}")
|
||||
logger.debug(f"Running {matcher}")
|
||||
await matcher.run(bot, event, state, stack, dependency_cache)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
f"<r><bg #f8bbd0>Running matcher {matcher} failed.</bg #f8bbd0></r>"
|
||||
f"<r><bg #f8bbd0>Running {matcher} failed.</bg #f8bbd0></r>"
|
||||
)
|
||||
exception = e
|
||||
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(
|
||||
x(
|
||||
matcher=matcher,
|
||||
exception=exception,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=matcher.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
matcher=matcher,
|
||||
exception=exception,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=matcher.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
_run_postprocessors,
|
||||
(SkippedException,),
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPostProcessors</bg #f8bbd0></r>"
|
||||
)
|
||||
for proc in _run_postprocessors
|
||||
]:
|
||||
# ensure matcher function can be correctly called
|
||||
with matcher.ensure_context(bot, event):
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPostProcessors</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
if matcher.block:
|
||||
raise StopPropagation
|
||||
@@ -244,7 +235,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
```
|
||||
"""
|
||||
show_log = True
|
||||
log_msg = f"<m>{escape_tag(bot.type.upper())} {escape_tag(bot.self_id)}</m> | "
|
||||
log_msg = f"<m>{escape_tag(bot.type)} {escape_tag(bot.self_id)}</m> | "
|
||||
try:
|
||||
log_msg += event.get_log_string()
|
||||
except NoLogException:
|
||||
@@ -256,22 +247,19 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
|
||||
async with AsyncExitStack() as stack:
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(
|
||||
x(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
_event_preprocessors,
|
||||
(SkippedException,),
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
for proc in _event_preprocessors
|
||||
]:
|
||||
try:
|
||||
if show_log:
|
||||
logger.debug("Running PreProcessors...")
|
||||
@@ -324,22 +312,19 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(
|
||||
x(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
_event_postprocessors,
|
||||
(SkippedException,),
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
for proc in _event_postprocessors
|
||||
]:
|
||||
try:
|
||||
if show_log:
|
||||
logger.debug("Running PostProcessors...")
|
||||
|
@@ -5,17 +5,16 @@ FrontMatter:
|
||||
description: nonebot.params 模块
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Tuple, Optional
|
||||
from typing import Any, Dict, List, Tuple, Union, Optional
|
||||
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.adapters import Event, Message
|
||||
from nonebot.internal.params import Arg as Arg
|
||||
from nonebot.internal.params import State as State
|
||||
from nonebot.internal.params import ArgStr as ArgStr
|
||||
from nonebot.internal.params import Depends as Depends
|
||||
from nonebot.internal.params import ArgParam as ArgParam
|
||||
from nonebot.internal.params import BotParam as BotParam
|
||||
from nonebot.adapters import Event, Message, MessageSegment
|
||||
from nonebot.internal.params import EventParam as EventParam
|
||||
from nonebot.internal.params import StateParam as StateParam
|
||||
from nonebot.internal.params import DependParam as DependParam
|
||||
@@ -30,10 +29,14 @@ from nonebot.consts import (
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
ENDSWITH_KEY,
|
||||
CMD_START_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
)
|
||||
|
||||
|
||||
@@ -110,15 +113,15 @@ def CommandStart() -> str:
|
||||
|
||||
|
||||
def _shell_command_args(state: T_State) -> Any:
|
||||
return state[SHELL_ARGS]
|
||||
return state[SHELL_ARGS] # Namespace or ParserExit
|
||||
|
||||
|
||||
def ShellCommandArgs():
|
||||
def ShellCommandArgs() -> Any:
|
||||
"""shell 命令解析后的参数字典"""
|
||||
return Depends(_shell_command_args, use_cache=False)
|
||||
|
||||
|
||||
def _shell_command_argv(state: T_State) -> List[str]:
|
||||
def _shell_command_argv(state: T_State) -> List[Union[str, MessageSegment]]:
|
||||
return state[SHELL_ARGV]
|
||||
|
||||
|
||||
@@ -154,6 +157,42 @@ def RegexDict() -> Dict[str, Any]:
|
||||
return Depends(_regex_dict, use_cache=False)
|
||||
|
||||
|
||||
def _startswith(state: T_State) -> str:
|
||||
return state[STARTSWITH_KEY]
|
||||
|
||||
|
||||
def Startswith() -> str:
|
||||
"""响应触发前缀"""
|
||||
return Depends(_startswith, use_cache=False)
|
||||
|
||||
|
||||
def _endswith(state: T_State) -> str:
|
||||
return state[ENDSWITH_KEY]
|
||||
|
||||
|
||||
def Endswith() -> str:
|
||||
"""响应触发后缀"""
|
||||
return Depends(_endswith, use_cache=False)
|
||||
|
||||
|
||||
def _fullmatch(state: T_State) -> str:
|
||||
return state[FULLMATCH_KEY]
|
||||
|
||||
|
||||
def Fullmatch() -> str:
|
||||
"""响应触发完整消息"""
|
||||
return Depends(_fullmatch, use_cache=False)
|
||||
|
||||
|
||||
def _keyword(state: T_State) -> str:
|
||||
return state[KEYWORD_KEY]
|
||||
|
||||
|
||||
def Keyword() -> str:
|
||||
"""响应触发关键字"""
|
||||
return Depends(_keyword, use_cache=False)
|
||||
|
||||
|
||||
def Received(id: Optional[str] = None, default: Any = None) -> Any:
|
||||
"""`receive` 事件参数"""
|
||||
|
||||
@@ -174,7 +213,6 @@ def LastReceived(default: Any = None) -> Any:
|
||||
|
||||
__autodoc__ = {
|
||||
"Arg": True,
|
||||
"State": True,
|
||||
"ArgStr": True,
|
||||
"Depends": True,
|
||||
"ArgParam": True,
|
||||
|
@@ -20,6 +20,9 @@ class Message:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Message()"
|
||||
|
||||
async def __call__(self, type: str = EventType()) -> bool:
|
||||
return type == "message"
|
||||
|
||||
@@ -29,6 +32,9 @@ class Notice:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Notice()"
|
||||
|
||||
async def __call__(self, type: str = EventType()) -> bool:
|
||||
return type == "notice"
|
||||
|
||||
@@ -38,6 +44,9 @@ class Request:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Request()"
|
||||
|
||||
async def __call__(self, type: str = EventType()) -> bool:
|
||||
return type == "request"
|
||||
|
||||
@@ -47,6 +56,9 @@ class MetaEvent:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "MetaEvent()"
|
||||
|
||||
async def __call__(self, type: str = EventType()) -> bool:
|
||||
return type == "meta_event"
|
||||
|
||||
@@ -78,16 +90,23 @@ class SuperUser:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Superuser()"
|
||||
|
||||
async def __call__(self, bot: Bot, event: Event) -> bool:
|
||||
return event.get_type() == "message" and (
|
||||
f"{bot.adapter.get_name().split(maxsplit=1)[0].lower()}:{event.get_user_id()}"
|
||||
try:
|
||||
user_id = event.get_user_id()
|
||||
except Exception:
|
||||
return False
|
||||
return (
|
||||
f"{bot.adapter.get_name().split(maxsplit=1)[0].lower()}:{user_id}"
|
||||
in bot.config.superusers
|
||||
or event.get_user_id() in bot.config.superusers # 兼容旧配置
|
||||
or user_id in bot.config.superusers # 兼容旧配置
|
||||
)
|
||||
|
||||
|
||||
SUPERUSER: Permission = Permission(SuperUser())
|
||||
"""匹配任意超级用户消息类型事件"""
|
||||
"""匹配任意超级用户事件"""
|
||||
|
||||
__autodoc__ = {
|
||||
"Permission": True,
|
||||
|
@@ -16,6 +16,7 @@
|
||||
- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`
|
||||
- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`
|
||||
- `on_regex` => {ref}``on_regex` <nonebot.plugin.on.on_regex>`
|
||||
- `on_type` => {ref}``on_type` <nonebot.plugin.on.on_type>`
|
||||
- `CommandGroup` => {ref}``CommandGroup` <nonebot.plugin.on.CommandGroup>`
|
||||
- `Matchergroup` => {ref}``MatcherGroup` <nonebot.plugin.on.MatcherGroup>`
|
||||
- `load_plugin` => {ref}``load_plugin` <nonebot.plugin.load.load_plugin>`
|
||||
@@ -25,8 +26,8 @@
|
||||
- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`
|
||||
- `load_builtin_plugin` => {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`
|
||||
- `load_builtin_plugins` => {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
|
||||
- `export` => {ref}``export` <nonebot.plugin.export.export>`
|
||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.plugin.PluginMetadata>`
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 0
|
||||
@@ -85,13 +86,12 @@ def get_plugin_by_module_name(module_name: str) -> Optional["Plugin"]:
|
||||
参数:
|
||||
module_name: 模块名,即 {ref}`nonebot.plugin.plugin.Plugin.module_name`。
|
||||
"""
|
||||
splits = module_name.split(".")
|
||||
loaded = {plugin.module_name: plugin for plugin in _plugins.values()}
|
||||
while splits:
|
||||
name = ".".join(splits)
|
||||
if name in loaded:
|
||||
return loaded[name]
|
||||
splits.pop()
|
||||
has_parent = True
|
||||
while has_parent:
|
||||
if module_name in loaded:
|
||||
return loaded[module_name]
|
||||
module_name, *has_parent = module_name.rsplit(".", 1)
|
||||
|
||||
|
||||
def get_loaded_plugins() -> Set["Plugin"]:
|
||||
@@ -106,8 +106,7 @@ def get_available_plugin_names() -> Set[str]:
|
||||
|
||||
from .on import on as on
|
||||
from .manager import PluginManager
|
||||
from .export import Export as Export
|
||||
from .export import export as export
|
||||
from .on import on_type as on_type
|
||||
from .load import require as require
|
||||
from .on import on_regex as on_regex
|
||||
from .plugin import Plugin as Plugin
|
||||
|
@@ -1,64 +0,0 @@
|
||||
"""本模块定义了插件导出的内容对象。
|
||||
|
||||
在新版插件系统中,推荐优先使用直接 import 所需要的插件内容。
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 4
|
||||
description: nonebot.plugin.export 模块
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from . import _current_plugin_chain
|
||||
|
||||
|
||||
class Export(dict):
|
||||
"""插件导出内容以使得其他插件可以获得。
|
||||
|
||||
用法:
|
||||
```python
|
||||
nonebot.export().default = "bar"
|
||||
|
||||
@nonebot.export()
|
||||
def some_function():
|
||||
pass
|
||||
|
||||
# this doesn't work before python 3.9
|
||||
# use
|
||||
# export = nonebot.export(); @export.sub
|
||||
# instead
|
||||
# See also PEP-614: https://www.python.org/dev/peps/pep-0614/
|
||||
@nonebot.export().sub
|
||||
def something_else():
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
def __call__(self, func, **kwargs):
|
||||
self[func.__name__] = func
|
||||
self.update(kwargs)
|
||||
return func
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
super().__setitem__(key, Export(value) if isinstance(value, dict) else value)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
self[name] = Export(value) if isinstance(value, dict) else value
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in self:
|
||||
self[name] = Export()
|
||||
return self[name]
|
||||
|
||||
|
||||
def export() -> Export:
|
||||
"""获取当前插件的导出内容对象"""
|
||||
warnings.warn(
|
||||
"nonebot.export() is deprecated. "
|
||||
"See https://github.com/nonebot/nonebot2/issues/935.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
plugins = _current_plugin_chain.get()
|
||||
if not plugins:
|
||||
raise RuntimeError("Export outside of the plugin!")
|
||||
return plugins[-1].export
|
@@ -5,24 +5,30 @@ FrontMatter:
|
||||
description: nonebot.plugin.load 模块
|
||||
"""
|
||||
import json
|
||||
import warnings
|
||||
from typing import Set, Iterable, Optional
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Set, Union, Iterable, Optional
|
||||
|
||||
import tomlkit
|
||||
|
||||
from .export import Export
|
||||
from nonebot.utils import path_to_module_name
|
||||
|
||||
from .plugin import Plugin
|
||||
from .manager import PluginManager
|
||||
from . import _managers, get_plugin, _module_name_to_plugin_name
|
||||
|
||||
|
||||
def load_plugin(module_path: str) -> Optional[Plugin]:
|
||||
def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
|
||||
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
|
||||
|
||||
参数:
|
||||
module_path: 插件名称 `path.to.your.plugin`
|
||||
module_path: 插件名称 `path.to.your.plugin` 或插件路径 `pathlib.Path(path/to/your/plugin)`
|
||||
"""
|
||||
|
||||
module_path = (
|
||||
path_to_module_name(module_path)
|
||||
if isinstance(module_path, Path)
|
||||
else module_path
|
||||
)
|
||||
manager = PluginManager([module_path])
|
||||
_managers.append(manager)
|
||||
return manager.load_plugin(module_path)
|
||||
@@ -74,6 +80,8 @@ def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
||||
"""
|
||||
with open(file_path, "r", encoding=encoding) as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
raise TypeError("json file must contains a dict!")
|
||||
plugins = data.get("plugins")
|
||||
plugin_dirs = data.get("plugin_dirs")
|
||||
assert isinstance(plugins, list), "plugins must be a list of plugin name"
|
||||
@@ -103,15 +111,10 @@ def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
||||
data = tomlkit.parse(f.read()) # type: ignore
|
||||
|
||||
nonebot_data = data.get("tool", {}).get("nonebot")
|
||||
if not nonebot_data:
|
||||
nonebot_data = data.get("nonebot", {}).get("plugins")
|
||||
if nonebot_data:
|
||||
warnings.warn(
|
||||
"[nonebot.plugins] table is deprecated. Use [tool.nonebot] instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
else:
|
||||
raise ValueError("Cannot find '[tool.nonebot]' in given toml file!")
|
||||
if nonebot_data is None:
|
||||
raise ValueError("Cannot find '[tool.nonebot]' in given toml file!")
|
||||
if not isinstance(nonebot_data, dict):
|
||||
raise TypeError("'[tool.nonebot]' must be a Table!")
|
||||
plugins = nonebot_data.get("plugins", [])
|
||||
plugin_dirs = nonebot_data.get("plugin_dirs", [])
|
||||
assert isinstance(plugins, list), "plugins must be a list of plugin name"
|
||||
@@ -143,7 +146,7 @@ def _find_manager_by_name(name: str) -> Optional[PluginManager]:
|
||||
return manager
|
||||
|
||||
|
||||
def require(name: str) -> Export:
|
||||
def require(name: str) -> ModuleType:
|
||||
"""获取一个插件的导出内容。
|
||||
|
||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
||||
@@ -156,11 +159,10 @@ def require(name: str) -> Export:
|
||||
"""
|
||||
plugin = get_plugin(_module_name_to_plugin_name(name))
|
||||
if not plugin:
|
||||
manager = _find_manager_by_name(name)
|
||||
if manager:
|
||||
if manager := _find_manager_by_name(name):
|
||||
plugin = manager.load_plugin(name)
|
||||
else:
|
||||
plugin = load_plugin(name)
|
||||
if not plugin:
|
||||
raise RuntimeError(f'Cannot load plugin "{name}"!')
|
||||
return plugin.export
|
||||
if not plugin:
|
||||
raise RuntimeError(f'Cannot load plugin "{name}"!')
|
||||
return plugin.module
|
||||
|
@@ -17,7 +17,7 @@ from importlib.machinery import PathFinder, SourceFileLoader
|
||||
from typing import Set, Dict, List, Union, Iterable, Optional, Sequence
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import escape_tag
|
||||
from nonebot.utils import escape_tag, path_to_module_name
|
||||
|
||||
from .plugin import Plugin, PluginMetadata
|
||||
from . import (
|
||||
@@ -51,6 +51,9 @@ class PluginManager:
|
||||
self._searched_plugin_names: Dict[str, Path] = {}
|
||||
self.prepare_plugins()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"PluginManager(plugins={self.plugins}, search_path={self.search_path})"
|
||||
|
||||
@property
|
||||
def third_party_plugins(self) -> Set[str]:
|
||||
"""返回所有独立插件名称。"""
|
||||
@@ -66,13 +69,6 @@ class PluginManager:
|
||||
"""返回当前插件管理器中可用的插件名称。"""
|
||||
return self.third_party_plugins | self.searched_plugins
|
||||
|
||||
def _path_to_module_name(self, path: Path) -> str:
|
||||
rel_path = path.resolve().relative_to(Path(".").resolve())
|
||||
if rel_path.stem == "__init__":
|
||||
return ".".join(rel_path.parts[:-1])
|
||||
else:
|
||||
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
|
||||
|
||||
def _previous_plugins(self) -> Set[str]:
|
||||
_pre_managers: List[PluginManager]
|
||||
if self in _managers:
|
||||
@@ -86,7 +82,6 @@ class PluginManager:
|
||||
|
||||
def prepare_plugins(self) -> Set[str]:
|
||||
"""搜索插件并缓存插件名称。"""
|
||||
|
||||
# get all previous ready to load plugins
|
||||
previous_plugins = self._previous_plugins()
|
||||
searched_plugins: Dict[str, Path] = {}
|
||||
@@ -118,11 +113,13 @@ class PluginManager:
|
||||
f"Plugin already exists: {module_info.name}! Check your plugin name"
|
||||
)
|
||||
|
||||
module_spec = module_info.module_finder.find_spec(module_info.name, None)
|
||||
if not module_spec:
|
||||
if not (
|
||||
module_spec := module_info.module_finder.find_spec(
|
||||
module_info.name, None
|
||||
)
|
||||
):
|
||||
continue
|
||||
module_path = module_spec.origin
|
||||
if not module_path:
|
||||
if not (module_path := module_spec.origin):
|
||||
continue
|
||||
searched_plugins[module_info.name] = Path(module_path).resolve()
|
||||
|
||||
@@ -146,7 +143,7 @@ class PluginManager:
|
||||
module = importlib.import_module(self._third_party_plugin_names[name])
|
||||
elif name in self._searched_plugin_names:
|
||||
module = importlib.import_module(
|
||||
self._path_to_module_name(self._searched_plugin_names[name])
|
||||
path_to_module_name(self._searched_plugin_names[name])
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"Plugin not found: {name}! Check your plugin name")
|
||||
@@ -154,8 +151,7 @@ class PluginManager:
|
||||
logger.opt(colors=True).success(
|
||||
f'Succeeded to import "<y>{escape_tag(name)}</y>"'
|
||||
)
|
||||
plugin = getattr(module, "__plugin__", None)
|
||||
if plugin is None:
|
||||
if (plugin := getattr(module, "__plugin__", None)) is None:
|
||||
raise RuntimeError(
|
||||
f"Module {module.__name__} is not loaded as a plugin! "
|
||||
"Make sure not to import it before loading."
|
||||
|
@@ -10,6 +10,7 @@ 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
|
||||
@@ -19,6 +20,7 @@ from nonebot.rule import (
|
||||
ArgumentParser,
|
||||
regex,
|
||||
command,
|
||||
is_type,
|
||||
keyword,
|
||||
endswith,
|
||||
fullmatch,
|
||||
@@ -30,9 +32,8 @@ from .manager import _current_plugin_chain
|
||||
|
||||
|
||||
def _store_matcher(matcher: Type[Matcher]) -> None:
|
||||
plugins = _current_plugin_chain.get()
|
||||
# only store the matcher defined in the plugin
|
||||
if plugins:
|
||||
if plugins := _current_plugin_chain.get():
|
||||
plugins[-1].matcher.add(matcher)
|
||||
|
||||
|
||||
@@ -368,7 +369,7 @@ def on_command(
|
||||
state: 默认 state
|
||||
"""
|
||||
|
||||
commands = set([cmd]) | (aliases or set())
|
||||
commands = {cmd} | (aliases or set())
|
||||
block = kwargs.pop("block", False)
|
||||
return on_message(
|
||||
command(*commands) & rule, block=block, **kwargs, _depth=_depth + 1
|
||||
@@ -403,7 +404,7 @@ def on_shell_command(
|
||||
state: 默认 state
|
||||
"""
|
||||
|
||||
commands = set([cmd]) | (aliases or set())
|
||||
commands = {cmd} | (aliases or set())
|
||||
return on_message(
|
||||
shell_command(*commands, parser=parser) & rule,
|
||||
**kwargs,
|
||||
@@ -437,7 +438,57 @@ def on_regex(
|
||||
return on_message(regex(pattern, flags) & rule, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
class CommandGroup:
|
||||
def on_type(
|
||||
types: Union[Type[Event], Tuple[Type[Event]]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
*,
|
||||
_depth: int = 0,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""注册一个事件响应器,并且当事件为指定类型时响应。
|
||||
|
||||
参数:
|
||||
types: 事件类型
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
event_types = types if isinstance(types, tuple) else (types,)
|
||||
return on(rule=is_type(*event_types) & rule, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
class _Group:
|
||||
def __init__(self, **kwargs):
|
||||
"""创建一个事件响应器组合,参数为默认值,与 `on` 一致"""
|
||||
self.matchers: List[Type[Matcher]] = []
|
||||
"""组内事件响应器列表"""
|
||||
self.base_kwargs: Dict[str, Any] = kwargs
|
||||
"""其他传递给 `on` 的参数默认值"""
|
||||
|
||||
def _get_final_kwargs(
|
||||
self, update: Dict[str, Any], *, exclude: Optional[Set[str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取最终传递给 `on` 的参数
|
||||
|
||||
参数:
|
||||
update: 更新的关键字参数
|
||||
exclude: 需要排除的参数
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(update)
|
||||
if exclude:
|
||||
for key in exclude:
|
||||
final_kwargs.pop(key, None)
|
||||
final_kwargs["_depth"] = 1
|
||||
return final_kwargs
|
||||
|
||||
|
||||
class CommandGroup(_Group):
|
||||
"""命令组,用于声明一组有相同名称前缀的命令。
|
||||
|
||||
参数:
|
||||
@@ -453,12 +504,13 @@ class CommandGroup:
|
||||
"""
|
||||
|
||||
def __init__(self, cmd: Union[str, Tuple[str, ...]], **kwargs):
|
||||
self.basecmd: Tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd
|
||||
"""命令前缀"""
|
||||
if "aliases" in kwargs:
|
||||
del kwargs["aliases"]
|
||||
self.base_kwargs: Dict[str, Any] = kwargs
|
||||
"""其他传递给 `on_command` 的参数默认值"""
|
||||
super().__init__(**kwargs)
|
||||
self.basecmd: Tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd
|
||||
self.base_kwargs.pop("aliases", None)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})"
|
||||
|
||||
def command(self, cmd: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
|
||||
"""注册一个新的命令。新参数将会覆盖命令组默认值
|
||||
@@ -477,10 +529,9 @@ class CommandGroup:
|
||||
"""
|
||||
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
|
||||
cmd = self.basecmd + sub_cmd
|
||||
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
return on_command(cmd, **final_kwargs, _depth=1)
|
||||
matcher = on_command(cmd, **self._get_final_kwargs(kwargs))
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
def shell_command(
|
||||
self, cmd: Union[str, Tuple[str, ...]], **kwargs
|
||||
@@ -502,21 +553,16 @@ class CommandGroup:
|
||||
"""
|
||||
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
|
||||
cmd = self.basecmd + sub_cmd
|
||||
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
return on_shell_command(cmd, **final_kwargs, _depth=1)
|
||||
matcher = on_shell_command(cmd, **self._get_final_kwargs(kwargs))
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
|
||||
class MatcherGroup:
|
||||
class MatcherGroup(_Group):
|
||||
"""事件响应器组合,统一管理。为 `Matcher` 创建提供默认属性。"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""创建一个事件响应器组合,参数为默认值,与 `on` 一致"""
|
||||
self.matchers: List[Type[Matcher]] = []
|
||||
"""组内事件响应器列表"""
|
||||
self.base_kwargs: Dict[str, Any] = kwargs
|
||||
"""其他传递给 `on` 的参数默认值"""
|
||||
def __repr__(self) -> str:
|
||||
return f"MatcherGroup(matchers={len(self.matchers)})"
|
||||
|
||||
def on(self, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个基础事件响应器,可自定义类型。
|
||||
@@ -532,9 +578,7 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
matcher = on(**final_kwargs, _depth=1)
|
||||
matcher = on(**self._get_final_kwargs(kwargs))
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -550,11 +594,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
final_kwargs.pop("permission", None)
|
||||
matcher = on_metaevent(**final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"})
|
||||
matcher = on_metaevent(**final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -571,10 +612,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_message(**final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_message(**final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -590,10 +629,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_notice(**final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"})
|
||||
matcher = on_notice(**final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -609,10 +646,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_request(**final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"})
|
||||
matcher = on_request(**final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -633,10 +668,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_startswith(msg, **final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_startswith(msg, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -655,10 +688,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_endswith(msg, **final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_endswith(msg, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -677,10 +708,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_fullmatch(msg, **final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_fullmatch(msg, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -698,10 +727,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_keyword(keywords, **final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_keyword(keywords, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -727,10 +754,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_command(cmd, aliases=aliases, **final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_command(cmd, aliases=aliases, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -760,12 +785,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_shell_command(
|
||||
cmd, aliases=aliases, parser=parser, **final_kwargs, _depth=1
|
||||
)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_shell_command(cmd, aliases=aliases, parser=parser, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -788,9 +809,28 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_regex(pattern, flags=flags, **final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_regex(pattern, flags=flags, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
def on_type(
|
||||
self, types: Union[Type[Event], Tuple[Type[Event]]], **kwargs
|
||||
) -> Type[Matcher]:
|
||||
"""注册一个事件响应器,并且当事件为指定类型时响应。
|
||||
|
||||
参数:
|
||||
types: 事件类型
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_type(types, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
@@ -2,6 +2,7 @@ import re
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Set, 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
|
||||
@@ -152,6 +153,18 @@ def on_regex(
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
def on_type(
|
||||
types: Union[Type[Event], Tuple[Type[Event]]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
|
||||
class CommandGroup:
|
||||
def __init__(
|
||||
@@ -171,8 +184,8 @@ class CommandGroup:
|
||||
self,
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
*,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
@@ -186,7 +199,7 @@ class CommandGroup:
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]],
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
parser: Optional[ArgumentParser] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
@@ -367,3 +380,16 @@ class MatcherGroup:
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
def on_type(
|
||||
self,
|
||||
types: Union[Type[Event], Tuple[Type[Event]]],
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
|
@@ -12,7 +12,6 @@ from pydantic import BaseModel
|
||||
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
from .export import Export
|
||||
from . import _plugins as plugins # FIXME: backport for nonebug
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -46,8 +45,6 @@ class Plugin:
|
||||
"""点分割模块路径"""
|
||||
manager: "PluginManager"
|
||||
"""导入该插件的插件管理器"""
|
||||
export: Export = field(default_factory=Export)
|
||||
"""**Deprecated:** 插件内定义的导出内容"""
|
||||
matcher: Set[Type[Matcher]] = field(default_factory=set)
|
||||
"""插件内定义的 `Matcher`"""
|
||||
parent_plugin: Optional["Plugin"] = None
|
||||
|
@@ -7,5 +7,5 @@ echo = on_command("echo", to_me())
|
||||
|
||||
|
||||
@echo.handle()
|
||||
async def echo_escape(message: Message = CommandArg()):
|
||||
async def handle_echo(message: Message = CommandArg()):
|
||||
await echo.send(message=message)
|
||||
|
@@ -15,8 +15,7 @@ async def matcher_mutex(event: Event) -> AsyncGenerator[bool, None]:
|
||||
yield result
|
||||
else:
|
||||
current_event_id = id(event)
|
||||
event_id = _running_matcher.get(session_id, None)
|
||||
if event_id:
|
||||
if event_id := _running_matcher.get(session_id, None):
|
||||
result = event_id != current_event_id
|
||||
else:
|
||||
_running_matcher[session_id] = current_event_id
|
||||
|
361
nonebot/rule.py
361
nonebot/rule.py
@@ -10,11 +10,26 @@ FrontMatter:
|
||||
|
||||
import re
|
||||
import shlex
|
||||
from itertools import product
|
||||
from argparse import Namespace
|
||||
from typing_extensions import TypedDict
|
||||
from argparse import Action
|
||||
from argparse import ArgumentError
|
||||
from itertools import chain, product
|
||||
from argparse import Namespace as Namespace
|
||||
from argparse import ArgumentParser as ArgParser
|
||||
from typing import Any, List, Tuple, Union, Optional, Sequence, NamedTuple
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
List,
|
||||
Type,
|
||||
Tuple,
|
||||
Union,
|
||||
TypeVar,
|
||||
Optional,
|
||||
Sequence,
|
||||
TypedDict,
|
||||
NamedTuple,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from pygtrie import CharTrie
|
||||
|
||||
@@ -23,15 +38,8 @@ from nonebot.log import logger
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.exception import ParserExit
|
||||
from nonebot.internal.rule import Rule as Rule
|
||||
from nonebot.params import Command, EventToMe, CommandArg
|
||||
from nonebot.adapters import Bot, Event, Message, MessageSegment
|
||||
from nonebot.params import (
|
||||
Command,
|
||||
EventToMe,
|
||||
EventType,
|
||||
CommandArg,
|
||||
EventMessage,
|
||||
EventPlainText,
|
||||
)
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
PREFIX_KEY,
|
||||
@@ -39,12 +47,18 @@ from nonebot.consts import (
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
ENDSWITH_KEY,
|
||||
CMD_START_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
CMD_RESULT = TypedDict(
|
||||
"CMD_RESULT",
|
||||
{
|
||||
@@ -83,8 +97,7 @@ class TrieRule:
|
||||
message_seg: MessageSegment = message[0]
|
||||
if message_seg.is_text():
|
||||
segment_text = str(message_seg).lstrip()
|
||||
pf = cls.prefix.longest_prefix(segment_text)
|
||||
if pf:
|
||||
if pf := cls.prefix.longest_prefix(segment_text):
|
||||
value: TRIE_VALUE = pf.value
|
||||
prefix[RAW_CMD_KEY] = pf.key
|
||||
prefix[CMD_START_KEY] = value.command_start
|
||||
@@ -113,19 +126,35 @@ class StartswithRule:
|
||||
self.msg = msg
|
||||
self.ignorecase = ignorecase
|
||||
|
||||
async def __call__(
|
||||
self, type: str = EventType(), text: str = EventPlainText()
|
||||
) -> Any:
|
||||
if type != "message":
|
||||
return False
|
||||
return bool(
|
||||
re.match(
|
||||
f"^(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})",
|
||||
text,
|
||||
re.IGNORECASE if self.ignorecase else 0,
|
||||
)
|
||||
def __repr__(self) -> str:
|
||||
return f"Startswith(msg={self.msg}, ignorecase={self.ignorecase})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
isinstance(other, StartswithRule)
|
||||
and frozenset(self.msg) == frozenset(other.msg)
|
||||
and self.ignorecase == other.ignorecase
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.msg), self.ignorecase))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
return False
|
||||
if match := re.match(
|
||||
f"^(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})",
|
||||
text,
|
||||
re.IGNORECASE if self.ignorecase else 0,
|
||||
):
|
||||
state[STARTSWITH_KEY] = match.group()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def startswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
||||
"""匹配消息纯文本开头。
|
||||
@@ -154,19 +183,35 @@ class EndswithRule:
|
||||
self.msg = msg
|
||||
self.ignorecase = ignorecase
|
||||
|
||||
async def __call__(
|
||||
self, type: str = EventType(), text: str = EventPlainText()
|
||||
) -> Any:
|
||||
if type != "message":
|
||||
return False
|
||||
return bool(
|
||||
re.search(
|
||||
f"(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})$",
|
||||
text,
|
||||
re.IGNORECASE if self.ignorecase else 0,
|
||||
)
|
||||
def __repr__(self) -> str:
|
||||
return f"Endswith(msg={self.msg}, ignorecase={self.ignorecase})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
isinstance(other, EndswithRule)
|
||||
and frozenset(self.msg) == frozenset(other.msg)
|
||||
and self.ignorecase == other.ignorecase
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.msg), self.ignorecase))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
return False
|
||||
if match := re.search(
|
||||
f"(?:{'|'.join(re.escape(suffix) for suffix in self.msg)})$",
|
||||
text,
|
||||
re.IGNORECASE if self.ignorecase else 0,
|
||||
):
|
||||
state[ENDSWITH_KEY] = match.group()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def endswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
||||
"""匹配消息纯文本结尾。
|
||||
@@ -192,17 +237,37 @@ class FullmatchRule:
|
||||
__slots__ = ("msg", "ignorecase")
|
||||
|
||||
def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False):
|
||||
self.msg = frozenset(map(str.casefold, msg) if ignorecase else msg)
|
||||
self.msg = tuple(map(str.casefold, msg) if ignorecase else msg)
|
||||
self.ignorecase = ignorecase
|
||||
|
||||
async def __call__(
|
||||
self, type_: str = EventType(), text: str = EventPlainText()
|
||||
) -> bool:
|
||||
def __repr__(self) -> str:
|
||||
return f"Fullmatch(msg={self.msg}, ignorecase={self.ignorecase})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
type_ == "message"
|
||||
and (text.casefold() if self.ignorecase else text) in self.msg
|
||||
isinstance(other, FullmatchRule)
|
||||
and frozenset(self.msg) == frozenset(other.msg)
|
||||
and self.ignorecase == other.ignorecase
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.msg), self.ignorecase))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
return False
|
||||
if not text:
|
||||
return False
|
||||
text = text.casefold() if self.ignorecase else text
|
||||
if text in self.msg:
|
||||
state[FULLMATCH_KEY] = text
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def fullmatch(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
||||
"""完全匹配消息。
|
||||
@@ -229,12 +294,30 @@ class KeywordsRule:
|
||||
def __init__(self, *keywords: str):
|
||||
self.keywords = keywords
|
||||
|
||||
async def __call__(
|
||||
self, type: str = EventType(), text: str = EventPlainText()
|
||||
) -> bool:
|
||||
if type != "message":
|
||||
def __repr__(self) -> str:
|
||||
return f"Keywords(keywords={self.keywords})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, KeywordsRule) and frozenset(
|
||||
self.keywords
|
||||
) == frozenset(other.keywords)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(frozenset(self.keywords))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
return bool(text and any(keyword in text for keyword in self.keywords))
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
return False
|
||||
if not text:
|
||||
return False
|
||||
if key := next((k for k in self.keywords if k in text), None):
|
||||
state[KEYWORD_KEY] = key
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def keyword(*keywords: str) -> Rule:
|
||||
@@ -257,14 +340,22 @@ class CommandRule:
|
||||
__slots__ = ("cmds",)
|
||||
|
||||
def __init__(self, cmds: List[Tuple[str, ...]]):
|
||||
self.cmds = cmds
|
||||
self.cmds = tuple(cmds)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Command(cmds={self.cmds})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, CommandRule) and frozenset(self.cmds) == frozenset(
|
||||
other.cmds
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.cmds),))
|
||||
|
||||
async def __call__(self, cmd: Optional[Tuple[str, ...]] = Command()) -> bool:
|
||||
return cmd in self.cmds
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Command {self.cmds}>"
|
||||
|
||||
|
||||
def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
|
||||
"""匹配消息命令。
|
||||
@@ -320,25 +411,48 @@ class ArgumentParser(ArgParser):
|
||||
参考文档: [argparse](https://docs.python.org/3/library/argparse.html)
|
||||
"""
|
||||
|
||||
def _print_message(self, message, file=None):
|
||||
old_message: str = getattr(self, "message", "")
|
||||
if old_message:
|
||||
old_message += "\n"
|
||||
old_message += message
|
||||
setattr(self, "message", old_message)
|
||||
if TYPE_CHECKING:
|
||||
|
||||
def exit(self, status: int = 0, message: Optional[str] = None):
|
||||
raise ParserExit(
|
||||
status=status, message=message or getattr(self, "message", None)
|
||||
@overload
|
||||
def parse_args(
|
||||
self, args: Optional[Sequence[Union[str, MessageSegment]]] = ...
|
||||
) -> Namespace:
|
||||
...
|
||||
|
||||
@overload
|
||||
def parse_args(
|
||||
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: None
|
||||
) -> Namespace:
|
||||
... # type: ignore[misc]
|
||||
|
||||
@overload
|
||||
def parse_args(
|
||||
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
|
||||
) -> T:
|
||||
...
|
||||
|
||||
def parse_args(
|
||||
self,
|
||||
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
||||
namespace: Optional[T] = None,
|
||||
) -> Union[Namespace, T]:
|
||||
...
|
||||
|
||||
def _parse_optional(
|
||||
self, arg_string: Union[str, MessageSegment]
|
||||
) -> Optional[Tuple[Optional[Action], str, Optional[str]]]:
|
||||
return (
|
||||
super()._parse_optional(arg_string) if isinstance(arg_string, str) else None
|
||||
)
|
||||
|
||||
def parse_args(
|
||||
self,
|
||||
args: Optional[Sequence[str]] = None,
|
||||
namespace: Optional[Namespace] = None,
|
||||
) -> Namespace:
|
||||
setattr(self, "message", "")
|
||||
return super().parse_args(args=args, namespace=namespace) # type: ignore
|
||||
def _print_message(self, message: str, file: Optional[IO[str]] = None):
|
||||
if message:
|
||||
setattr(self, "_message", getattr(self, "_message", "") + message)
|
||||
|
||||
def exit(self, status: int = 0, message: Optional[str] = None):
|
||||
if message:
|
||||
self._print_message(message)
|
||||
raise ParserExit(status=status, message=getattr(self, "_message", None))
|
||||
|
||||
|
||||
class ShellCommandRule:
|
||||
@@ -352,28 +466,48 @@ class ShellCommandRule:
|
||||
__slots__ = ("cmds", "parser")
|
||||
|
||||
def __init__(self, cmds: List[Tuple[str, ...]], parser: Optional[ArgumentParser]):
|
||||
self.cmds = cmds
|
||||
self.cmds = tuple(cmds)
|
||||
self.parser = parser
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ShellCommand(cmds={self.cmds}, parser={self.parser})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
isinstance(other, ShellCommandRule)
|
||||
and frozenset(self.cmds) == frozenset(other.cmds)
|
||||
and self.parser is other.parser
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.cmds), self.parser))
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
state: T_State,
|
||||
cmd: Optional[Tuple[str, ...]] = Command(),
|
||||
msg: Optional[Message] = CommandArg(),
|
||||
) -> bool:
|
||||
if cmd in self.cmds and msg is not None:
|
||||
message = str(msg)
|
||||
state[SHELL_ARGV] = shlex.split(message)
|
||||
if self.parser:
|
||||
try:
|
||||
args = self.parser.parse_args(state[SHELL_ARGV])
|
||||
state[SHELL_ARGS] = args
|
||||
except ParserExit as e:
|
||||
state[SHELL_ARGS] = e
|
||||
return True
|
||||
else:
|
||||
if cmd not in self.cmds or msg is None:
|
||||
return False
|
||||
|
||||
state[SHELL_ARGV] = list(
|
||||
chain.from_iterable(
|
||||
shlex.split(str(seg)) if cast(MessageSegment, seg).is_text() else (seg,)
|
||||
for seg in msg
|
||||
)
|
||||
)
|
||||
|
||||
if self.parser:
|
||||
try:
|
||||
args = self.parser.parse_args(state[SHELL_ARGV])
|
||||
state[SHELL_ARGS] = args
|
||||
except ArgumentError as e:
|
||||
state[SHELL_ARGS] = ParserExit(status=2, message=str(e))
|
||||
except ParserExit as e:
|
||||
state[SHELL_ARGS] = e
|
||||
return True
|
||||
|
||||
|
||||
def shell_command(
|
||||
*cmds: Union[str, Tuple[str, ...]], parser: Optional[ArgumentParser] = None
|
||||
@@ -452,16 +586,27 @@ class RegexRule:
|
||||
self.regex = regex
|
||||
self.flags = flags
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
state: T_State,
|
||||
type: str = EventType(),
|
||||
msg: Message = EventMessage(),
|
||||
) -> bool:
|
||||
if type != "message":
|
||||
def __repr__(self) -> str:
|
||||
return f"Regex(regex={self.regex!r}, flags={self.flags})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
isinstance(other, RegexRule)
|
||||
and self.regex == other.regex
|
||||
and self.flags == other.flags
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.regex, self.flags))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
matched = re.search(self.regex, str(msg), self.flags)
|
||||
if matched:
|
||||
try:
|
||||
msg = event.get_message()
|
||||
except Exception:
|
||||
return False
|
||||
if matched := re.search(self.regex, str(msg), self.flags):
|
||||
state[REGEX_MATCHED] = matched.group()
|
||||
state[REGEX_GROUP] = matched.groups()
|
||||
state[REGEX_DICT] = matched.groupdict()
|
||||
@@ -498,6 +643,15 @@ class ToMeRule:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "ToMe()"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, ToMeRule)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.__class__,))
|
||||
|
||||
async def __call__(self, to_me: bool = EventToMe()) -> bool:
|
||||
return to_me
|
||||
|
||||
@@ -508,6 +662,37 @@ def to_me() -> Rule:
|
||||
return Rule(ToMeRule())
|
||||
|
||||
|
||||
class IsTypeRule:
|
||||
"""检查事件类型是否为指定类型。"""
|
||||
|
||||
__slots__ = ("types",)
|
||||
|
||||
def __init__(self, *types: Type[Event]):
|
||||
self.types = types
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"IsType(types={tuple(type.__name__ for type in self.types)})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, IsTypeRule) and self.types == other.types
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.types,))
|
||||
|
||||
async def __call__(self, event: Event) -> bool:
|
||||
return isinstance(event, self.types)
|
||||
|
||||
|
||||
def is_type(*types: Type[Event]) -> Rule:
|
||||
"""匹配事件类型。
|
||||
|
||||
参数:
|
||||
types: 事件类型
|
||||
"""
|
||||
|
||||
return Rule(IsTypeRule(*types))
|
||||
|
||||
|
||||
__autodoc__ = {
|
||||
"Rule": True,
|
||||
"Rule.__call__": True,
|
||||
|
@@ -11,6 +11,7 @@ FrontMatter:
|
||||
sidebar_position: 11
|
||||
description: nonebot.typing 模块
|
||||
"""
|
||||
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@@ -28,6 +29,8 @@ if TYPE_CHECKING:
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.permission import Permission
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
T_Wrapped = TypeVar("T_Wrapped", bound=Callable)
|
||||
|
||||
|
||||
@@ -41,10 +44,14 @@ def overrides(InterfaceClass: object) -> Callable[[T_Wrapped], T_Wrapped]:
|
||||
return overrider
|
||||
|
||||
|
||||
# state
|
||||
T_State = Dict[Any, Any]
|
||||
"""事件处理状态 State 类型"""
|
||||
|
||||
T_BotConnectionHook = Callable[..., Awaitable[Any]]
|
||||
_DependentCallable = Union[Callable[..., T], Callable[..., Awaitable[T]]]
|
||||
|
||||
# driver hooks
|
||||
T_BotConnectionHook = _DependentCallable[Any]
|
||||
"""Bot 连接建立时钩子函数
|
||||
|
||||
依赖参数:
|
||||
@@ -53,7 +60,7 @@ T_BotConnectionHook = Callable[..., Awaitable[Any]]
|
||||
- BotParam: Bot 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_BotDisconnectionHook = Callable[..., Awaitable[Any]]
|
||||
T_BotDisconnectionHook = _DependentCallable[Any]
|
||||
"""Bot 连接断开时钩子函数
|
||||
|
||||
依赖参数:
|
||||
@@ -62,6 +69,8 @@ T_BotDisconnectionHook = Callable[..., Awaitable[Any]]
|
||||
- BotParam: Bot 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
|
||||
# api hooks
|
||||
T_CallingAPIHook = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]]
|
||||
"""`bot.call_api` 钩子函数"""
|
||||
T_CalledAPIHook = Callable[
|
||||
@@ -69,7 +78,8 @@ T_CalledAPIHook = Callable[
|
||||
]
|
||||
"""`bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result"""
|
||||
|
||||
T_EventPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
# event hooks
|
||||
T_EventPreProcessor = _DependentCallable[Any]
|
||||
"""事件预处理函数 EventPreProcessor 类型
|
||||
|
||||
依赖参数:
|
||||
@@ -80,7 +90,7 @@ T_EventPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
- StateParam: State 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_EventPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
T_EventPostProcessor = _DependentCallable[Any]
|
||||
"""事件预处理函数 EventPostProcessor 类型
|
||||
|
||||
依赖参数:
|
||||
@@ -91,7 +101,9 @@ T_EventPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
- StateParam: State 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_RunPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
|
||||
# matcher run hooks
|
||||
T_RunPreProcessor = _DependentCallable[Any]
|
||||
"""事件响应器运行前预处理函数 RunPreProcessor 类型
|
||||
|
||||
依赖参数:
|
||||
@@ -103,7 +115,7 @@ T_RunPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
- MatcherParam: Matcher 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_RunPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
T_RunPostProcessor = _DependentCallable[Any]
|
||||
"""事件响应器运行后后处理函数 RunPostProcessor 类型
|
||||
|
||||
依赖参数:
|
||||
@@ -117,7 +129,8 @@ T_RunPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
|
||||
T_RuleChecker = Callable[..., Union[bool, Awaitable[bool]]]
|
||||
# rule, permission
|
||||
T_RuleChecker = _DependentCallable[bool]
|
||||
"""RuleChecker 即判断是否响应事件的处理函数。
|
||||
|
||||
依赖参数:
|
||||
@@ -128,7 +141,7 @@ T_RuleChecker = Callable[..., Union[bool, Awaitable[bool]]]
|
||||
- StateParam: State 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_PermissionChecker = Callable[..., Union[bool, Awaitable[bool]]]
|
||||
T_PermissionChecker = _DependentCallable[bool]
|
||||
"""PermissionChecker 即判断事件是否满足权限的处理函数。
|
||||
|
||||
依赖参数:
|
||||
@@ -139,9 +152,9 @@ T_PermissionChecker = Callable[..., Union[bool, Awaitable[bool]]]
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
|
||||
T_Handler = Callable[..., Any]
|
||||
T_Handler = _DependentCallable[Any]
|
||||
"""Handler 处理函数。"""
|
||||
T_TypeUpdater = Callable[..., Union[str, Awaitable[str]]]
|
||||
T_TypeUpdater = _DependentCallable[str]
|
||||
"""TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。默认会更新为 `message`。
|
||||
|
||||
依赖参数:
|
||||
@@ -153,7 +166,7 @@ T_TypeUpdater = Callable[..., Union[str, Awaitable[str]]]
|
||||
- MatcherParam: Matcher 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_PermissionUpdater = Callable[..., Union["Permission", Awaitable["Permission"]]]
|
||||
T_PermissionUpdater = _DependentCallable["Permission"]
|
||||
"""PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新会话对象权限。默认会更新为当前事件的触发对象。
|
||||
|
||||
依赖参数:
|
||||
@@ -165,5 +178,5 @@ T_PermissionUpdater = Callable[..., Union["Permission", Awaitable["Permission"]]
|
||||
- MatcherParam: Matcher 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_DependencyCache = Dict[Callable[..., Any], "Task[Any]"]
|
||||
T_DependencyCache = Dict[_DependentCallable[Any], "Task[Any]"]
|
||||
"""依赖缓存, 用于存储依赖函数的返回值"""
|
||||
|
@@ -10,6 +10,7 @@ import json
|
||||
import asyncio
|
||||
import inspect
|
||||
import dataclasses
|
||||
from pathlib import Path
|
||||
from functools import wraps, partial
|
||||
from contextlib import asynccontextmanager
|
||||
from typing_extensions import ParamSpec, get_args, get_origin
|
||||
@@ -24,6 +25,7 @@ from typing import (
|
||||
Coroutine,
|
||||
AsyncGenerator,
|
||||
ContextManager,
|
||||
overload,
|
||||
)
|
||||
|
||||
from pydantic.typing import is_union, is_none_type
|
||||
@@ -129,11 +131,28 @@ async def run_sync_ctx_manager(
|
||||
await run_sync(cm.__exit__)(None, None, None)
|
||||
|
||||
|
||||
@overload
|
||||
async def run_coro_with_catch(
|
||||
coro: Coroutine[Any, Any, T],
|
||||
exc: Tuple[Type[Exception], ...],
|
||||
return_on_err: R = None,
|
||||
) -> Union[T, None]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
async def run_coro_with_catch(
|
||||
coro: Coroutine[Any, Any, T],
|
||||
exc: Tuple[Type[Exception], ...],
|
||||
return_on_err: R,
|
||||
) -> Union[T, R]:
|
||||
...
|
||||
|
||||
|
||||
async def run_coro_with_catch(
|
||||
coro: Coroutine[Any, Any, T],
|
||||
exc: Tuple[Type[Exception], ...],
|
||||
return_on_err: Optional[R] = None,
|
||||
) -> Optional[Union[T, R]]:
|
||||
try:
|
||||
return await coro
|
||||
except exc:
|
||||
@@ -147,6 +166,14 @@ def get_name(obj: Any) -> str:
|
||||
return obj.__class__.__name__
|
||||
|
||||
|
||||
def path_to_module_name(path: Path) -> str:
|
||||
rel_path = path.resolve().relative_to(Path(".").resolve())
|
||||
if rel_path.stem == "__init__":
|
||||
return ".".join(rel_path.parts[:-1])
|
||||
else:
|
||||
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
|
||||
|
||||
|
||||
class DataclassEncoder(json.JSONEncoder):
|
||||
"""在JSON序列化 {re}`nonebot.adapters._message.Message` (List[Dataclass]) 时使用的 `JSONEncoder`"""
|
||||
|
||||
@@ -173,7 +200,7 @@ def logger_wrapper(logger_name: str):
|
||||
|
||||
def log(level: str, message: str, exception: Optional[Exception] = None):
|
||||
logger.opt(colors=True, exception=exception).log(
|
||||
level, f"<m>{escape_tag(logger_name)}</m> | " + message
|
||||
level, f"<m>{escape_tag(logger_name)}</m> | {message}"
|
||||
)
|
||||
|
||||
return log
|
||||
|
@@ -17,7 +17,7 @@ _✨ NoneBot 本地文档插件 ✨_
|
||||
<a href="https://pypi.python.org/pypi/nonebot-plugin-docs">
|
||||
<img src="https://img.shields.io/pypi/v/nonebot-plugin-docs.svg" alt="pypi">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/python-3.7+-blue.svg" alt="python">
|
||||
<img src="https://img.shields.io/badge/python-3.8+-blue.svg" alt="python">
|
||||
</p>
|
||||
|
||||
## 使用方式
|
||||
|
@@ -12,7 +12,7 @@ include = ["nonebot_plugin_docs/dist/**/*"]
|
||||
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7.3"
|
||||
python = "^3.8"
|
||||
nonebot2 = "^2.0.0-beta.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
1392
poetry.lock
generated
1392
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "nonebot2"
|
||||
version = "2.0.0-beta.5"
|
||||
version = "2.0.0rc2"
|
||||
description = "An asynchronous python bot framework."
|
||||
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||
license = "MIT"
|
||||
@@ -22,29 +22,34 @@ packages = [
|
||||
include = ["nonebot/py.typed"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7.3"
|
||||
python = "^3.8"
|
||||
yarl = "^1.7.2"
|
||||
loguru = "^0.6.0"
|
||||
pygtrie = "^2.4.1"
|
||||
fastapi = "^0.79.0"
|
||||
fastapi = ">=0.87.0,<1.0.0"
|
||||
tomlkit = ">=0.10.0,<1.0.0"
|
||||
typing-extensions = ">=3.10.0,<5.0.0"
|
||||
Quart = { version = "^0.17.0", optional = true }
|
||||
websockets = { version="^10.0", optional = true }
|
||||
pydantic = { version = "~1.9.0", extras = ["dotenv"] }
|
||||
uvicorn = { version = "^0.18.0", extras = ["standard"] }
|
||||
aiohttp = { version = "^3.7.4", extras = ["speedups"], optional = true }
|
||||
httpx = { version = ">=0.20.0, <1.0.0", extras = ["http2"], optional = true }
|
||||
pydantic = { version = "^1.10.0", extras = ["dotenv"] }
|
||||
uvicorn = { version = ">=0.20.0,<1.0.0", extras = ["standard"] }
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
websockets = { version="^10.0", optional = true }
|
||||
Quart = { version = ">=0.18.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 }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
isort = "^5.10.1"
|
||||
black = "^22.1.0"
|
||||
nonemoji = "^0.1.2"
|
||||
pytest-cov = "^3.0.0"
|
||||
pre-commit = "^2.16.0"
|
||||
pytest-xdist = "^2.5.0"
|
||||
pytest-asyncio = "^0.19.0"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
pytest-cov = "^4.0.0"
|
||||
pytest-xdist = "^3.0.2"
|
||||
pytest-asyncio = "^0.20.0"
|
||||
nonebug = { git = "https://github.com/nonebot/nonebug.git" }
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
nb-autodoc = { git = "https://github.com/nonebot/nb-autodoc.git" }
|
||||
|
||||
[tool.poetry.extras]
|
||||
@@ -64,7 +69,7 @@ filterwarnings = [
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ["py37", "py38", "py39", "py310"]
|
||||
target-version = ["py38", "py39", "py310", "py311"]
|
||||
include = '\.pyi?$'
|
||||
extend-exclude = '''
|
||||
'''
|
||||
|
@@ -1,6 +1,7 @@
|
||||
[report]
|
||||
exclude_lines =
|
||||
def __repr__
|
||||
def __str__
|
||||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
@(abc\.)?abstractmethod
|
||||
|
@@ -1,2 +1,3 @@
|
||||
ENVIRONMENT=test
|
||||
COMMON_CONFIG=common
|
||||
COMMON_OVERRIDE=old
|
||||
|
@@ -1,5 +1,13 @@
|
||||
LOG_LEVEL=TRACE
|
||||
NICKNAME=["test"]
|
||||
SUPERUSERS=["test", "fake:faketest"]
|
||||
COMMON_OVERRIDE=new
|
||||
CONFIG_FROM_ENV=
|
||||
CONFIG_OVERRIDE=old
|
||||
NESTED_DICT={"a": 1}
|
||||
NESTED_DICT__B=2
|
||||
NESTED_DICT__C__D=3
|
||||
NESTED_MISSING_DICT__A=1
|
||||
NESTED_MISSING_DICT__B__C=2
|
||||
NOT_NESTED=some string
|
||||
NOT_NESTED__A=1
|
||||
|
1
tests/plugins.invalid.json
Normal file
1
tests/plugins.invalid.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
2
tests/plugins.invalid.toml
Normal file
2
tests/plugins.invalid.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[tool]
|
||||
nonebot = []
|
4
tests/plugins.json
Normal file
4
tests/plugins.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"plugins": [],
|
||||
"plugin_dirs": ["plugins"]
|
||||
}
|
3
tests/plugins.toml
Normal file
3
tests/plugins.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[tool.nonebot]
|
||||
plugins = []
|
||||
plugin_dirs = ["plugins"]
|
@@ -1,6 +1,2 @@
|
||||
from nonebot import export
|
||||
|
||||
|
||||
@export()
|
||||
def test():
|
||||
return "export"
|
||||
|
@@ -3,5 +3,7 @@ from datetime import datetime, timedelta
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
test_temp_matcher = Matcher.new("test", temp=True)
|
||||
test_datetime_matcher = Matcher.new("test", expire_time=datetime.now())
|
||||
test_datetime_matcher = Matcher.new(
|
||||
"test", expire_time=datetime.now() - timedelta(seconds=1)
|
||||
)
|
||||
test_timedelta_matcher = Matcher.new("test", expire_time=timedelta(seconds=-1))
|
||||
|
@@ -1,10 +1,14 @@
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.permission import Permission
|
||||
from nonebot.permission import USER, Permission
|
||||
|
||||
default_permission = Permission()
|
||||
|
||||
test_permission_updater = Matcher.new(permission=default_permission)
|
||||
|
||||
test_user_permission_updater = Matcher.new(
|
||||
permission=USER("test", perm=default_permission)
|
||||
)
|
||||
|
||||
test_custom_updater = Matcher.new(permission=default_permission)
|
||||
|
||||
|
||||
|
@@ -4,10 +4,14 @@ from nonebot.typing import T_State
|
||||
from nonebot.adapters import Message
|
||||
from nonebot.params import (
|
||||
Command,
|
||||
Keyword,
|
||||
Endswith,
|
||||
Fullmatch,
|
||||
RegexDict,
|
||||
CommandArg,
|
||||
RawCommand,
|
||||
RegexGroup,
|
||||
Startswith,
|
||||
CommandStart,
|
||||
RegexMatched,
|
||||
ShellCommandArgs,
|
||||
@@ -65,3 +69,19 @@ async def regex_group(regex_group: Tuple = RegexGroup()) -> Tuple:
|
||||
|
||||
async def regex_matched(regex_matched: str = RegexMatched()) -> str:
|
||||
return regex_matched
|
||||
|
||||
|
||||
async def startswith(startswith: str = Startswith()) -> str:
|
||||
return startswith
|
||||
|
||||
|
||||
async def endswith(endswith: str = Endswith()) -> str:
|
||||
return endswith
|
||||
|
||||
|
||||
async def fullmatch(fullmatch: str = Fullmatch()) -> str:
|
||||
return fullmatch
|
||||
|
||||
|
||||
async def keyword(keyword: str = Keyword()) -> str:
|
||||
return keyword
|
||||
|
1
tests/plugins/plugin/__init__.py
Normal file
1
tests/plugins/plugin/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import matchers
|
243
tests/plugins/plugin/matchers.py
Normal file
243
tests/plugins/plugin/matchers.py
Normal file
@@ -0,0 +1,243 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot import (
|
||||
CommandGroup,
|
||||
MatcherGroup,
|
||||
on,
|
||||
on_type,
|
||||
on_regex,
|
||||
on_notice,
|
||||
on_command,
|
||||
on_keyword,
|
||||
on_message,
|
||||
on_request,
|
||||
on_endswith,
|
||||
on_fullmatch,
|
||||
on_metaevent,
|
||||
on_startswith,
|
||||
on_shell_command,
|
||||
)
|
||||
|
||||
|
||||
async def rule() -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def permission() -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def handler():
|
||||
return
|
||||
|
||||
|
||||
expire_time = datetime.now(timezone.utc)
|
||||
priority = 100
|
||||
state = {"test": "test"}
|
||||
|
||||
|
||||
matcher_on = on(
|
||||
"test",
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
matcher_on_metaevent = on_metaevent(
|
||||
rule=rule,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
matcher_on_message = on_message(
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
matcher_on_notice = on_notice(
|
||||
rule=rule,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
matcher_on_request = on_request(
|
||||
rule=rule,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
matcher_on_startswith = on_startswith(
|
||||
"test",
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
matcher_on_endswith = on_endswith(
|
||||
"test",
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
matcher_on_fullmatch = on_fullmatch(
|
||||
"test",
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
matcher_on_keyword = on_keyword(
|
||||
{"test"},
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
matcher_on_command = on_command(
|
||||
"test",
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
matcher_on_shell_command = on_shell_command(
|
||||
"test",
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
matcher_on_regex = on_regex(
|
||||
"test",
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
class TestEvent(Event):
|
||||
...
|
||||
|
||||
|
||||
matcher_on_type = on_type(
|
||||
TestEvent,
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
cmd_group = CommandGroup(
|
||||
"test",
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
matcher_sub_cmd = cmd_group.command("sub")
|
||||
matcher_sub_shell_cmd = cmd_group.shell_command("sub")
|
||||
|
||||
|
||||
matcher_group = MatcherGroup(
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
matcher_group_on = matcher_group.on(type="test")
|
||||
matcher_group_on_metaevent = matcher_group.on_metaevent()
|
||||
matcher_group_on_message = matcher_group.on_message()
|
||||
matcher_group_on_notice = matcher_group.on_notice()
|
||||
matcher_group_on_request = matcher_group.on_request()
|
||||
matcher_group_on_startswith = matcher_group.on_startswith("test")
|
||||
matcher_group_on_endswith = matcher_group.on_endswith("test")
|
||||
matcher_group_on_fullmatch = matcher_group.on_fullmatch("test")
|
||||
matcher_group_on_keyword = matcher_group.on_keyword({"test"})
|
||||
matcher_group_on_command = matcher_group.on_command("test")
|
||||
matcher_group_on_shell_command = matcher_group.on_shell_command("test")
|
||||
matcher_group_on_regex = matcher_group.on_regex("test")
|
||||
matcher_group_on_type = matcher_group.on_type(TestEvent)
|
@@ -11,11 +11,11 @@ def test_template_basis():
|
||||
|
||||
def test_template_message():
|
||||
Message = make_fake_message()
|
||||
template = Message.template("{a:custom}{b:text}{c:image}")
|
||||
template = Message.template("{a:custom}{b:text}{c:image}/{d}")
|
||||
|
||||
@template.add_format_spec
|
||||
def custom(input: str) -> str:
|
||||
return input + "-custom!"
|
||||
return f"{input}-custom!"
|
||||
|
||||
try:
|
||||
template.add_format_spec(custom)
|
||||
@@ -24,12 +24,17 @@ def test_template_message():
|
||||
else:
|
||||
raise AssertionError("Should raise ValueError")
|
||||
|
||||
format_args = {"a": "custom", "b": "text", "c": "https://example.com/test"}
|
||||
format_args = {
|
||||
"a": "custom",
|
||||
"b": "text",
|
||||
"c": "https://example.com/test",
|
||||
"d": 114,
|
||||
}
|
||||
formatted = template.format(**format_args)
|
||||
|
||||
assert template.format_map(format_args) == formatted
|
||||
assert formatted.extract_plain_text() == "custom-custom!text"
|
||||
assert str(formatted) == "custom-custom!text[fake:image]"
|
||||
assert formatted.extract_plain_text() == "custom-custom!text/114"
|
||||
assert str(formatted) == "custom-custom!text[fake:image]/114"
|
||||
|
||||
|
||||
def test_rich_template_message():
|
||||
|
@@ -78,3 +78,27 @@ async def test_reverse_driver(app: App):
|
||||
assert await ws.receive_bytes() == b"pong"
|
||||
|
||||
await ws.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"nonebug_init, driver_type",
|
||||
[
|
||||
pytest.param(
|
||||
{"driver": "nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin"},
|
||||
"fastapi+aiohttp",
|
||||
id="fastapi+aiohttp",
|
||||
),
|
||||
pytest.param(
|
||||
{"driver": "~httpx:Driver+~websockets"},
|
||||
"block_driver+httpx+websockets",
|
||||
id="httpx+websockets",
|
||||
),
|
||||
],
|
||||
indirect=["nonebug_init"],
|
||||
)
|
||||
async def test_combine_driver(app: App, driver_type: str):
|
||||
import nonebot
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
assert driver.type == driver_type
|
||||
|
@@ -29,6 +29,10 @@ async def test_init(nonebug_init):
|
||||
assert config.config_override == "new"
|
||||
assert config.config_from_init == "init"
|
||||
assert config.common_config == "common"
|
||||
assert config.common_override == "new"
|
||||
assert config.nested_dict == {"a": 1, "b": 2, "c": {"d": 3}}
|
||||
assert config.nested_missing_dict == {"a": 1, "b": {"c": 2}}
|
||||
assert config.not_nested == "some string"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -61,7 +65,7 @@ async def test_get(monkeypatch: pytest.MonkeyPatch, nonebug_clear):
|
||||
with pytest.raises(ValueError):
|
||||
get_bot()
|
||||
|
||||
monkeypatch.setattr(driver, "_clients", {"test": "test"})
|
||||
monkeypatch.setattr(driver, "_bots", {"test": "test"})
|
||||
assert get_bot() == "test"
|
||||
assert get_bot("test") == "test"
|
||||
assert get_bots() == {"test": "test"}
|
||||
|
@@ -104,6 +104,7 @@ async def test_permission_updater(app: App, load_plugin):
|
||||
default_permission,
|
||||
test_custom_updater,
|
||||
test_permission_updater,
|
||||
test_user_permission_updater,
|
||||
)
|
||||
|
||||
event = make_fake_event(_session_id="test")()
|
||||
@@ -119,6 +120,19 @@ async def test_permission_updater(app: App, load_plugin):
|
||||
assert checker.users == ("test",)
|
||||
assert checker.perm is default_permission
|
||||
|
||||
user_permission = list(test_user_permission_updater.permission.checkers)[0].call
|
||||
assert isinstance(user_permission, User)
|
||||
assert user_permission.perm is default_permission
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
matcher = test_user_permission_updater()
|
||||
new_perm = await matcher.update_permission(bot, event)
|
||||
assert len(new_perm.checkers) == 1
|
||||
checker = list(new_perm.checkers)[0].call
|
||||
assert isinstance(checker, User)
|
||||
assert checker.users == ("test",)
|
||||
assert checker.perm is default_permission
|
||||
|
||||
assert test_custom_updater.permission is default_permission
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
11
tests/test_matcher/test_provider.py
Normal file
11
tests/test_matcher/test_provider.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_manager(app: App, load_plugin):
|
||||
from nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers
|
||||
|
||||
default_provider = matchers.provider
|
||||
matchers.set_provider(DEFAULT_PROVIDER_CLASS)
|
||||
assert matchers.provider == default_provider
|
@@ -168,15 +168,23 @@ async def test_state(app: App, load_plugin):
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
ENDSWITH_KEY,
|
||||
CMD_START_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
)
|
||||
from plugins.param.param_state import (
|
||||
state,
|
||||
command,
|
||||
keyword,
|
||||
endswith,
|
||||
fullmatch,
|
||||
regex_dict,
|
||||
startswith,
|
||||
command_arg,
|
||||
raw_command,
|
||||
regex_group,
|
||||
@@ -201,6 +209,10 @@ async def test_state(app: App, load_plugin):
|
||||
REGEX_MATCHED: "[cq:test,arg=value]",
|
||||
REGEX_GROUP: ("test", "arg=value"),
|
||||
REGEX_DICT: {"type": "test", "arg": "value"},
|
||||
STARTSWITH_KEY: "startswith",
|
||||
ENDSWITH_KEY: "endswith",
|
||||
FULLMATCH_KEY: "fullmatch",
|
||||
KEYWORD_KEY: "keyword",
|
||||
}
|
||||
|
||||
async with app.test_dependent(state, allow_types=[StateParam]) as ctx:
|
||||
@@ -271,6 +283,30 @@ async def test_state(app: App, load_plugin):
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[REGEX_DICT])
|
||||
|
||||
async with app.test_dependent(
|
||||
startswith, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[STARTSWITH_KEY])
|
||||
|
||||
async with app.test_dependent(
|
||||
endswith, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[ENDSWITH_KEY])
|
||||
|
||||
async with app.test_dependent(
|
||||
fullmatch, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[FULLMATCH_KEY])
|
||||
|
||||
async with app.test_dependent(
|
||||
keyword, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[KEYWORD_KEY])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher(app: App, load_plugin):
|
||||
|
@@ -1,3 +1,5 @@
|
||||
from typing import Tuple, Optional
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
@@ -142,10 +144,11 @@ async def test_metaevent(
|
||||
("message", "test", True),
|
||||
("message", "foo", False),
|
||||
("message", "faketest", True),
|
||||
("notice", "test", False),
|
||||
("message", None, False),
|
||||
("notice", "test", True),
|
||||
],
|
||||
)
|
||||
async def test_startswith(
|
||||
async def test_superuser(
|
||||
app: App,
|
||||
type: str,
|
||||
user_id: str,
|
||||
@@ -163,3 +166,29 @@ async def test_startswith(
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert await dependent(bot=bot, event=event) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"session_ids,session_id,expected",
|
||||
[
|
||||
(("user", "foo"), "user", True),
|
||||
(("user", "foo"), "bar", False),
|
||||
(("user", "foo"), None, False),
|
||||
],
|
||||
)
|
||||
async def test_user(
|
||||
app: App, session_ids: Tuple[str, ...], session_id: Optional[str], expected: bool
|
||||
):
|
||||
from nonebot.permission import USER, User
|
||||
|
||||
dependent = list(USER(*session_ids).checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
assert isinstance(checker, User)
|
||||
|
||||
event = make_fake_event(_session_id=session_id)()
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert await dependent(bot=bot, event=event) == expected
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from dataclasses import asdict
|
||||
from typing import TYPE_CHECKING, Set
|
||||
|
||||
@@ -10,7 +11,21 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
async def test_load_plugin(app: App):
|
||||
import nonebot
|
||||
|
||||
# check regular
|
||||
assert nonebot.load_plugin("plugins.metadata")
|
||||
|
||||
# check path
|
||||
assert nonebot.load_plugin(Path("plugins/export"))
|
||||
|
||||
# check not found
|
||||
assert nonebot.load_plugin("some_plugin_not_exist") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_plugins(app: App, load_plugin: Set["Plugin"]):
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager
|
||||
|
||||
@@ -34,9 +49,6 @@ async def test_load_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
with pytest.raises(RuntimeError):
|
||||
PluginManager(search_path=["plugins"]).load_all_plugins()
|
||||
|
||||
# check not found
|
||||
assert nonebot.load_plugin("some_plugin_not_exist") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_nested_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
@@ -51,6 +63,29 @@ async def test_load_nested_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
assert parent_plugin.sub_plugins == {sub_plugin, sub_plugin2}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_json(app: App):
|
||||
import nonebot
|
||||
|
||||
nonebot.load_from_json("./plugins.json")
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
nonebot.load_from_json("./plugins.invalid.json")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_toml(app: App):
|
||||
import nonebot
|
||||
|
||||
nonebot.load_from_toml("./plugins.toml")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
nonebot.load_from_toml("./plugins.empty.toml")
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
nonebot.load_from_toml("./plugins.invalid.toml")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bad_plugin(app: App):
|
||||
import nonebot
|
||||
|
116
tests/test_plugin/test_on.py
Normal file
116
tests/test_plugin/test_on.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from typing import Type, Optional
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on(app: App, load_plugin):
|
||||
import nonebot
|
||||
import plugins.plugin.matchers as module
|
||||
from nonebot.typing import T_RuleChecker
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
from nonebot.rule import (
|
||||
RegexRule,
|
||||
IsTypeRule,
|
||||
CommandRule,
|
||||
EndswithRule,
|
||||
KeywordsRule,
|
||||
FullmatchRule,
|
||||
StartswithRule,
|
||||
ShellCommandRule,
|
||||
)
|
||||
from plugins.plugin.matchers import (
|
||||
TestEvent,
|
||||
rule,
|
||||
state,
|
||||
handler,
|
||||
priority,
|
||||
matcher_on,
|
||||
permission,
|
||||
expire_time,
|
||||
matcher_on_type,
|
||||
matcher_sub_cmd,
|
||||
matcher_group_on,
|
||||
matcher_on_regex,
|
||||
matcher_on_notice,
|
||||
matcher_on_command,
|
||||
matcher_on_keyword,
|
||||
matcher_on_message,
|
||||
matcher_on_request,
|
||||
matcher_on_endswith,
|
||||
matcher_on_fullmatch,
|
||||
matcher_on_metaevent,
|
||||
matcher_group_on_type,
|
||||
matcher_on_startswith,
|
||||
matcher_sub_shell_cmd,
|
||||
matcher_group_on_regex,
|
||||
matcher_group_on_notice,
|
||||
matcher_group_on_command,
|
||||
matcher_group_on_keyword,
|
||||
matcher_group_on_message,
|
||||
matcher_group_on_request,
|
||||
matcher_on_shell_command,
|
||||
matcher_group_on_endswith,
|
||||
matcher_group_on_fullmatch,
|
||||
matcher_group_on_metaevent,
|
||||
matcher_group_on_startswith,
|
||||
matcher_group_on_shell_command,
|
||||
)
|
||||
|
||||
plugin = nonebot.get_plugin("plugin")
|
||||
|
||||
def _check(
|
||||
matcher: Type[Matcher],
|
||||
pre_rule: Optional[T_RuleChecker],
|
||||
has_permission: bool,
|
||||
):
|
||||
assert {dependent.call for dependent in matcher.rule.checkers} == (
|
||||
{pre_rule, rule} if pre_rule else {rule}
|
||||
)
|
||||
if has_permission:
|
||||
assert {dependent.call for dependent in matcher.permission.checkers} == {
|
||||
permission
|
||||
}
|
||||
else:
|
||||
assert not matcher.permission.checkers
|
||||
assert [dependent.call for dependent in matcher.handlers] == [handler]
|
||||
assert matcher.temp is True
|
||||
assert matcher.expire_time == expire_time
|
||||
assert matcher in matchers[priority]
|
||||
assert matcher.block is True
|
||||
assert matcher._default_state == state
|
||||
|
||||
assert matcher.plugin is plugin
|
||||
assert matcher.module is module
|
||||
assert matcher.plugin_name == "plugin"
|
||||
assert matcher.module_name == "plugins.plugin.matchers"
|
||||
|
||||
_check(matcher_on, None, True)
|
||||
_check(matcher_on_metaevent, None, False)
|
||||
_check(matcher_on_message, None, True)
|
||||
_check(matcher_on_notice, None, False)
|
||||
_check(matcher_on_request, None, False)
|
||||
_check(matcher_on_startswith, StartswithRule(("test",)), True)
|
||||
_check(matcher_on_endswith, EndswithRule(("test",)), True)
|
||||
_check(matcher_on_fullmatch, FullmatchRule(("test",)), True)
|
||||
_check(matcher_on_keyword, KeywordsRule("test"), True)
|
||||
_check(matcher_on_command, CommandRule([("test",)]), True)
|
||||
_check(matcher_on_shell_command, ShellCommandRule([("test",)], None), True)
|
||||
_check(matcher_on_regex, RegexRule("test"), True)
|
||||
_check(matcher_on_type, IsTypeRule(TestEvent), True)
|
||||
_check(matcher_sub_cmd, CommandRule([("test", "sub")]), True)
|
||||
_check(matcher_sub_shell_cmd, ShellCommandRule([("test", "sub")], None), True)
|
||||
_check(matcher_group_on, None, True)
|
||||
_check(matcher_group_on_metaevent, None, False)
|
||||
_check(matcher_group_on_message, None, True)
|
||||
_check(matcher_group_on_notice, None, False)
|
||||
_check(matcher_group_on_request, None, False)
|
||||
_check(matcher_group_on_startswith, StartswithRule(("test",)), True)
|
||||
_check(matcher_group_on_endswith, EndswithRule(("test",)), True)
|
||||
_check(matcher_group_on_fullmatch, FullmatchRule(("test",)), True)
|
||||
_check(matcher_group_on_keyword, KeywordsRule("test"), True)
|
||||
_check(matcher_group_on_command, CommandRule([("test",)]), True)
|
||||
_check(matcher_group_on_shell_command, ShellCommandRule([("test",)], None), True)
|
||||
_check(matcher_group_on_regex, RegexRule("test"), True)
|
||||
_check(matcher_group_on_type, IsTypeRule(TestEvent), True)
|
@@ -1,4 +1,5 @@
|
||||
from typing import Tuple, Union
|
||||
import sys
|
||||
from typing import Dict, Tuple, Union, Optional
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
@@ -51,6 +52,7 @@ async def test_rule(app: App):
|
||||
("prefix", True, "message", "Prefix_", True),
|
||||
("prefix", False, "message", "prefoo", False),
|
||||
("prefix", False, "message", "fooprefix", False),
|
||||
("prefix", False, "message", None, False),
|
||||
(("prefix", "foo"), False, "message", "fooprefix", True),
|
||||
("prefix", False, "notice", "foo", False),
|
||||
],
|
||||
@@ -60,22 +62,27 @@ async def test_startswith(
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
ignorecase: bool,
|
||||
type: str,
|
||||
text: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.consts import STARTSWITH_KEY
|
||||
from nonebot.rule import StartswithRule, startswith
|
||||
|
||||
test_startswith = startswith(msg, ignorecase)
|
||||
dependent = list(test_startswith.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
msg = (msg,) if isinstance(msg, str) else msg
|
||||
|
||||
assert isinstance(checker, StartswithRule)
|
||||
assert checker.msg == (msg,) if isinstance(msg, str) else msg
|
||||
assert checker.msg == msg
|
||||
assert checker.ignorecase == ignorecase
|
||||
|
||||
message = make_fake_message()(text)
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
assert await dependent(event=event) == expected
|
||||
for prefix in msg:
|
||||
state = {STARTSWITH_KEY: prefix}
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -88,6 +95,7 @@ async def test_startswith(
|
||||
("suffix", True, "message", "_Suffix", True),
|
||||
("suffix", False, "message", "suffoo", False),
|
||||
("suffix", False, "message", "suffixfoo", False),
|
||||
("suffix", False, "message", None, False),
|
||||
(("suffix", "foo"), False, "message", "suffixfoo", True),
|
||||
("suffix", False, "notice", "foo", False),
|
||||
],
|
||||
@@ -97,22 +105,27 @@ async def test_endswith(
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
ignorecase: bool,
|
||||
type: str,
|
||||
text: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.consts import ENDSWITH_KEY
|
||||
from nonebot.rule import EndswithRule, endswith
|
||||
|
||||
test_endswith = endswith(msg, ignorecase)
|
||||
dependent = list(test_endswith.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
msg = (msg,) if isinstance(msg, str) else msg
|
||||
|
||||
assert isinstance(checker, EndswithRule)
|
||||
assert checker.msg == (msg,) if isinstance(msg, str) else msg
|
||||
assert checker.msg == msg
|
||||
assert checker.ignorecase == ignorecase
|
||||
|
||||
message = make_fake_message()(text)
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
assert await dependent(event=event) == expected
|
||||
for suffix in msg:
|
||||
state = {ENDSWITH_KEY: suffix}
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -125,6 +138,7 @@ async def test_endswith(
|
||||
("fullmatch", True, "message", "Fullmatch", True),
|
||||
("fullmatch", False, "message", "fullfoo", False),
|
||||
("fullmatch", False, "message", "_fullmatch_", False),
|
||||
("fullmatch", False, "message", None, False),
|
||||
(("fullmatch", "foo"), False, "message", "fullmatchfoo", False),
|
||||
("fullmatch", False, "notice", "foo", False),
|
||||
],
|
||||
@@ -134,22 +148,27 @@ async def test_fullmatch(
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
ignorecase: bool,
|
||||
type: str,
|
||||
text: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.consts import FULLMATCH_KEY
|
||||
from nonebot.rule import FullmatchRule, fullmatch
|
||||
|
||||
test_fullmatch = fullmatch(msg, ignorecase)
|
||||
dependent = list(test_fullmatch.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
msg = (msg,) if isinstance(msg, str) else msg
|
||||
|
||||
assert isinstance(checker, FullmatchRule)
|
||||
assert checker.msg == {msg} if isinstance(msg, str) else {*msg}
|
||||
assert checker.msg == msg
|
||||
assert checker.ignorecase == ignorecase
|
||||
|
||||
message = make_fake_message()(text)
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
assert await dependent(event=event) == expected
|
||||
for full in msg:
|
||||
state = {FULLMATCH_KEY: full}
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -158,16 +177,19 @@ async def test_fullmatch(
|
||||
[
|
||||
(("key",), "message", "_key_", True),
|
||||
(("key", "foo"), "message", "_foo_", True),
|
||||
(("key",), "message", None, False),
|
||||
(("key",), "notice", "foo", False),
|
||||
(("key",), "message", "foo", False),
|
||||
],
|
||||
)
|
||||
async def test_keyword(
|
||||
app: App,
|
||||
kws: Tuple[str, ...],
|
||||
type: str,
|
||||
text: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.consts import KEYWORD_KEY
|
||||
from nonebot.rule import KeywordsRule, keyword
|
||||
|
||||
test_keyword = keyword(*kws)
|
||||
@@ -177,9 +199,11 @@ async def test_keyword(
|
||||
assert isinstance(checker, KeywordsRule)
|
||||
assert checker.keywords == kws
|
||||
|
||||
message = make_fake_message()(text)
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
assert await dependent(event=event) == expected
|
||||
for kw in kws:
|
||||
state = {KEYWORD_KEY: kw}
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -195,16 +219,157 @@ async def test_command(app: App, cmds: Tuple[Tuple[str, ...]]):
|
||||
checker = dependent.call
|
||||
|
||||
assert isinstance(checker, CommandRule)
|
||||
assert checker.cmds == list(cmds)
|
||||
assert checker.cmds == cmds
|
||||
|
||||
for cmd in cmds:
|
||||
state = {PREFIX_KEY: {CMD_KEY: cmd}}
|
||||
assert await dependent(state=state)
|
||||
|
||||
|
||||
# TODO: shell command
|
||||
@pytest.mark.asyncio
|
||||
async def test_shell_command(app: App):
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.exception import ParserExit
|
||||
from nonebot.consts import CMD_KEY, PREFIX_KEY, SHELL_ARGS, SHELL_ARGV, CMD_ARG_KEY
|
||||
from nonebot.rule import Namespace, ArgumentParser, ShellCommandRule, shell_command
|
||||
|
||||
# TODO: regex
|
||||
state: T_State
|
||||
CMD = ("test",)
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
test_not_cmd = shell_command(CMD)
|
||||
dependent = list(test_not_cmd.checkers)[0]
|
||||
checker = dependent.call
|
||||
assert isinstance(checker, ShellCommandRule)
|
||||
message = Message()
|
||||
event = make_fake_event(_message=message)()
|
||||
state = {PREFIX_KEY: {CMD_KEY: ("not",), CMD_ARG_KEY: message}}
|
||||
assert not await dependent(event=event, state=state)
|
||||
|
||||
test_no_parser = shell_command(CMD)
|
||||
dependent = list(test_no_parser.checkers)[0]
|
||||
checker = dependent.call
|
||||
assert isinstance(checker, ShellCommandRule)
|
||||
message = Message()
|
||||
event = make_fake_event(_message=message)()
|
||||
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
|
||||
assert await dependent(event=event, state=state)
|
||||
assert state[SHELL_ARGV] == []
|
||||
assert SHELL_ARGS not in state
|
||||
|
||||
parser = ArgumentParser("test")
|
||||
parser.add_argument("-a", required=True)
|
||||
|
||||
test_simple_parser = shell_command(CMD, parser=parser)
|
||||
dependent = list(test_simple_parser.checkers)[0]
|
||||
checker = dependent.call
|
||||
assert isinstance(checker, ShellCommandRule)
|
||||
message = Message("-a 1")
|
||||
event = make_fake_event(_message=message)()
|
||||
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
|
||||
assert await dependent(event=event, state=state)
|
||||
assert state[SHELL_ARGV] == ["-a", "1"]
|
||||
assert state[SHELL_ARGS] == Namespace(a="1")
|
||||
|
||||
test_parser_help = shell_command(CMD, parser=parser)
|
||||
dependent = list(test_parser_help.checkers)[0]
|
||||
checker = dependent.call
|
||||
assert isinstance(checker, ShellCommandRule)
|
||||
message = Message("-h")
|
||||
event = make_fake_event(_message=message)()
|
||||
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
|
||||
assert await dependent(event=event, state=state)
|
||||
assert state[SHELL_ARGV] == ["-h"]
|
||||
assert isinstance(state[SHELL_ARGS], ParserExit)
|
||||
assert state[SHELL_ARGS].status == 0
|
||||
assert state[SHELL_ARGS].message == parser.format_help()
|
||||
|
||||
test_parser_error = shell_command(CMD, parser=parser)
|
||||
dependent = list(test_parser_error.checkers)[0]
|
||||
checker = dependent.call
|
||||
assert isinstance(checker, ShellCommandRule)
|
||||
message = Message()
|
||||
event = make_fake_event(_message=message)()
|
||||
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
|
||||
assert await dependent(event=event, state=state)
|
||||
assert state[SHELL_ARGV] == []
|
||||
assert isinstance(state[SHELL_ARGS], ParserExit)
|
||||
assert state[SHELL_ARGS].status != 0
|
||||
|
||||
test_message_parser = shell_command(CMD, parser=parser)
|
||||
dependent = list(test_message_parser.checkers)[0]
|
||||
checker = dependent.call
|
||||
assert isinstance(checker, ShellCommandRule)
|
||||
message = MessageSegment.text("-a") + MessageSegment.image("test")
|
||||
event = make_fake_event(_message=message)()
|
||||
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
|
||||
assert await dependent(event=event, state=state)
|
||||
assert state[SHELL_ARGV] == ["-a", MessageSegment.image("test")]
|
||||
assert state[SHELL_ARGS] == Namespace(a=MessageSegment.image("test"))
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
parser = ArgumentParser("test", exit_on_error=False)
|
||||
parser.add_argument("-a", required=True)
|
||||
|
||||
test_not_exit = shell_command(CMD, parser=parser)
|
||||
dependent = list(test_not_exit.checkers)[0]
|
||||
checker = dependent.call
|
||||
assert isinstance(checker, ShellCommandRule)
|
||||
message = Message()
|
||||
event = make_fake_event(_message=message)()
|
||||
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
|
||||
assert await dependent(event=event, state=state)
|
||||
assert state[SHELL_ARGV] == []
|
||||
assert isinstance(state[SHELL_ARGS], ParserExit)
|
||||
assert state[SHELL_ARGS].status != 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"pattern,type,text,expected,matched,group,dict",
|
||||
[
|
||||
(
|
||||
r"(?P<key>key\d)",
|
||||
"message",
|
||||
"_key1_",
|
||||
True,
|
||||
"key1",
|
||||
("key1",),
|
||||
{"key": "key1"},
|
||||
),
|
||||
(r"foo", "message", None, False, None, None, None),
|
||||
(r"foo", "notice", "foo", False, None, None, None),
|
||||
],
|
||||
)
|
||||
async def test_regex(
|
||||
app: App,
|
||||
pattern: str,
|
||||
type: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
matched: Optional[str],
|
||||
group: Optional[Tuple[str, ...]],
|
||||
dict: Optional[Dict[str, str]],
|
||||
):
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.rule import RegexRule, regex
|
||||
from nonebot.consts import REGEX_DICT, REGEX_GROUP, REGEX_MATCHED
|
||||
|
||||
test_regex = regex(pattern)
|
||||
dependent = list(test_regex.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
assert isinstance(checker, RegexRule)
|
||||
assert checker.regex == pattern
|
||||
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
state = {}
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
assert state.get(REGEX_MATCHED) == matched
|
||||
assert state.get(REGEX_GROUP) == group
|
||||
assert state.get(REGEX_DICT) == dict
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -212,11 +377,32 @@ async def test_command(app: App, cmds: Tuple[Tuple[str, ...]]):
|
||||
async def test_to_me(app: App, expected: bool):
|
||||
from nonebot.rule import ToMeRule, to_me
|
||||
|
||||
test_keyword = to_me()
|
||||
dependent = list(test_keyword.checkers)[0]
|
||||
test_to_me = to_me()
|
||||
dependent = list(test_to_me.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
assert isinstance(checker, ToMeRule)
|
||||
|
||||
event = make_fake_event(_to_me=expected)()
|
||||
assert await dependent(event=event) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_type(app: App):
|
||||
from nonebot.rule import IsTypeRule, is_type
|
||||
|
||||
Event1 = make_fake_event()
|
||||
Event2 = make_fake_event()
|
||||
Event3 = make_fake_event()
|
||||
|
||||
test_type = is_type(Event1, Event2)
|
||||
dependent = list(test_type.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
assert isinstance(checker, IsTypeRule)
|
||||
|
||||
event = Event1()
|
||||
assert await dependent(event=event)
|
||||
|
||||
event = Event3()
|
||||
assert not await dependent(event=event)
|
||||
|
@@ -65,7 +65,7 @@ def make_fake_event(
|
||||
_type: str = "message",
|
||||
_name: str = "test",
|
||||
_description: str = "test",
|
||||
_user_id: str = "test",
|
||||
_user_id: Optional[str] = "test",
|
||||
_session_id: Optional[str] = "test",
|
||||
_message: Optional["Message"] = None,
|
||||
_to_me: bool = True,
|
||||
@@ -86,7 +86,9 @@ def make_fake_event(
|
||||
return _description
|
||||
|
||||
def get_user_id(self) -> str:
|
||||
return _user_id
|
||||
if _user_id is not None:
|
||||
return _user_id
|
||||
raise NotImplementedError
|
||||
|
||||
def get_session_id(self) -> str:
|
||||
if _session_id is not None:
|
||||
|
@@ -8,7 +8,7 @@ slug: /
|
||||
|
||||
NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架,它基于 Python 的类型注解和异步特性,能够为你的需求实现提供便捷灵活的支持。
|
||||
|
||||
需要注意的是,NoneBot2 仅支持 **Python 3.7.3 以上版本**
|
||||
需要注意的是,NoneBot2 仅支持 **Python 3.8 以上版本**
|
||||
|
||||
## 特色
|
||||
|
||||
|
@@ -13,7 +13,7 @@ import Asciinema from "@site/src/components/Asciinema";
|
||||
# 安装 NoneBot2
|
||||
|
||||
:::warning 注意
|
||||
请确保你的 Python 版本 >= 3.7.3。
|
||||
请确保你的 Python 版本 >= 3.8。
|
||||
:::
|
||||
|
||||
:::warning 注意
|
||||
|
@@ -277,7 +277,7 @@ async def _(foo: str = CommandStart()): ...
|
||||
|
||||
### ShellCommandArgs
|
||||
|
||||
获取 shell 命令解析后的参数。
|
||||
获取 shell 命令解析后的参数,支持 MessageSegment 富文本(如:图片)。
|
||||
|
||||
:::tip 提示
|
||||
如果参数解析失败,则为 [`ParserExit`](../../api/exception.md#ParserExit) 异常,并携带错误码与错误信息。
|
||||
@@ -288,21 +288,28 @@ async def _(foo: str = CommandStart()): ...
|
||||
```python {8,12}
|
||||
from nonebot import on_shell_command
|
||||
from nonebot.params import ShellCommandArgs
|
||||
from nonebot.rule import Namespace, ArgumentParser
|
||||
|
||||
parser = ArgumentParser("demo")
|
||||
# parser.add_argument ...
|
||||
matcher = on_shell_command("cmd", parser)
|
||||
|
||||
# 解析失败
|
||||
@matcher.handle()
|
||||
async def _(foo: ParserExit = ShellCommandArgs()): ...
|
||||
async def _(foo: ParserExit = ShellCommandArgs()):
|
||||
if foo.status == 0:
|
||||
foo.message # help message
|
||||
else:
|
||||
foo.message # error message
|
||||
|
||||
# 解析成功
|
||||
@matcher.handle()
|
||||
async def _(foo: Dict[str, Any] = ShellCommandArgs()): ...
|
||||
async def _(foo: Namespace = ShellCommandArgs()): ...
|
||||
```
|
||||
|
||||
### ShellCommandArgv
|
||||
|
||||
获取 shell 命令解析前的参数列表。
|
||||
获取 shell 命令解析前的参数列表,支持 MessageSegment 富文本(如:图片)。
|
||||
|
||||
```python {7}
|
||||
from nonebot import on_shell_command
|
||||
@@ -311,7 +318,7 @@ from nonebot.params import ShellCommandArgs
|
||||
matcher = on_shell_command("cmd")
|
||||
|
||||
@matcher.handle()
|
||||
async def _(foo: List[str] = ShellCommandArgv()): ...
|
||||
async def _(foo: List[Union[str, MessageSegment]] = ShellCommandArgv()): ...
|
||||
```
|
||||
|
||||
### RegexMatched
|
||||
@@ -356,6 +363,62 @@ matcher = on_regex("regex")
|
||||
async def _(foo: Dict[str, Any] = RegexDict()): ...
|
||||
```
|
||||
|
||||
### Startswith
|
||||
|
||||
获取触发响应器的消息前缀字符串。
|
||||
|
||||
```python {7}
|
||||
from nonebot import on_startswith
|
||||
from nonebot.params import Startswith
|
||||
|
||||
matcher = on_startswith("prefix")
|
||||
|
||||
@matcher.handle()
|
||||
async def _(foo: str = Startswith()): ...
|
||||
```
|
||||
|
||||
### Endswith
|
||||
|
||||
获取触发响应器的消息后缀字符串。
|
||||
|
||||
```python {7}
|
||||
from nonebot import on_endswith
|
||||
from nonebot.params import Endswith
|
||||
|
||||
matcher = on_endswith("suffix")
|
||||
|
||||
@matcher.handle()
|
||||
async def _(foo: str = Endswith()): ...
|
||||
```
|
||||
|
||||
### Fullmatch
|
||||
|
||||
获取触发响应器的消息字符串。
|
||||
|
||||
```python {7}
|
||||
from nonebot import on_fullmatch
|
||||
from nonebot.params import Fullmatch
|
||||
|
||||
matcher = on_fullmatch("fullmatch")
|
||||
|
||||
@matcher.handle()
|
||||
async def _(foo: str = Fullmatch()): ...
|
||||
```
|
||||
|
||||
### Keyword
|
||||
|
||||
获取触发响应器的关键字字符串。
|
||||
|
||||
```python {7}
|
||||
from nonebot import on_keyword
|
||||
from nonebot.params import Keyword
|
||||
|
||||
matcher = on_keyword({"keyword"})
|
||||
|
||||
@matcher.handle()
|
||||
async def _(foo: str = Keyword()): ...
|
||||
```
|
||||
|
||||
### Matcher
|
||||
|
||||
获取当前事件响应器实例。
|
||||
|
@@ -5,6 +5,181 @@ toc_max_heading_level: 2
|
||||
|
||||
# 更新日志
|
||||
|
||||
## v2.0.0rc2
|
||||
|
||||
### 💥 破坏性变更
|
||||
|
||||
- Feature: 使用 `importlib.metadata` 替换 `pkg_resources` [@A-kirami](https://github.com/A-kirami) ([#1388](https://github.com/nonebot/nonebot2/pull/1388))
|
||||
|
||||
### 🚀 新功能
|
||||
|
||||
- Feature: 支持自定义 matchers 存储管理 [@yanyongyu](https://github.com/yanyongyu) ([#1395](https://github.com/nonebot/nonebot2/pull/1395))
|
||||
- Feature: 升级 devcontainer 配置 [@yanyongyu](https://github.com/yanyongyu) ([#1392](https://github.com/nonebot/nonebot2/pull/1392))
|
||||
- Feature: 使用 `importlib.metadata` 替换 `pkg_resources` [@A-kirami](https://github.com/A-kirami) ([#1388](https://github.com/nonebot/nonebot2/pull/1388))
|
||||
- CI: 测试环境添加 Python 3.11 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1366](https://github.com/nonebot/nonebot2/pull/1366))
|
||||
- Feature: 新增 dotenv 嵌套配置项支持 [@yanyongyu](https://github.com/yanyongyu) ([#1324](https://github.com/nonebot/nonebot2/pull/1324))
|
||||
- Feature: 添加 State 响应器触发消息注入 [@A-kirami](https://github.com/A-kirami) ([#1315](https://github.com/nonebot/nonebot2/pull/1315))
|
||||
- Remove: 移除无用的 namespace 声明 [@yanyongyu](https://github.com/yanyongyu) ([#1306](https://github.com/nonebot/nonebot2/pull/1306))
|
||||
|
||||
### 🐛 Bug 修复
|
||||
|
||||
- Fix: Bot `__getattr__` 不再对 `__xxx__` 方法返回 [@synodriver](https://github.com/synodriver) ([#1398](https://github.com/nonebot/nonebot2/pull/1398))
|
||||
- Fix: 修复 run pre/post hook 没有在正确的上下文中运行 [@yanyongyu](https://github.com/yanyongyu) ([#1391](https://github.com/nonebot/nonebot2/pull/1391))
|
||||
|
||||
### 📝 文档
|
||||
|
||||
- Docs: 添加 ntchat 社区适配器 [@JustUndertaker](https://github.com/JustUndertaker) ([#1414](https://github.com/nonebot/nonebot2/pull/1414))
|
||||
|
||||
### 💫 杂项
|
||||
|
||||
- Plugin: b 站用户信息查询 [@Ikaros-521](https://github.com/Ikaros-521) ([#1410](https://github.com/nonebot/nonebot2/pull/1410))
|
||||
- Plugin: 由于 Sena-nana 项目拆分,之前的插件地址更改 [@sena-nana](https://github.com/sena-nana) ([#1378](https://github.com/nonebot/nonebot2/pull/1378))
|
||||
- Plugin: 更新 ayaka 插件的主页链接 [@bridgeL](https://github.com/bridgeL) ([#1346](https://github.com/nonebot/nonebot2/pull/1346))
|
||||
- Plugin: 补充 novelai 插件信息 [@sena-nana](https://github.com/sena-nana) ([#1333](https://github.com/nonebot/nonebot2/pull/1333))
|
||||
- Bot: 修改 Inkar Suki 描述 [@HornCopper](https://github.com/HornCopper) ([#1312](https://github.com/nonebot/nonebot2/pull/1312))
|
||||
- Plugin: 修改插件 MCQQ MCRcon 主页地址 [@17TheWord](https://github.com/17TheWord) ([#1303](https://github.com/nonebot/nonebot2/pull/1303))
|
||||
|
||||
### 🍻 插件发布
|
||||
|
||||
- Plugin: 谁在窥屏 [@yanyongyu](https://github.com/yanyongyu) ([#1416](https://github.com/nonebot/nonebot2/pull/1416))
|
||||
- Plugin: 免费版 NovelAI 生图插件 [@yanyongyu](https://github.com/yanyongyu) ([#1408](https://github.com/nonebot/nonebot2/pull/1408))
|
||||
- Plugin: sky 光遇 [@yanyongyu](https://github.com/yanyongyu) ([#1394](https://github.com/nonebot/nonebot2/pull/1394))
|
||||
- Plugin: Colab-NovelAI [@yanyongyu](https://github.com/yanyongyu) ([#1390](https://github.com/nonebot/nonebot2/pull/1390))
|
||||
- Plugin: b 站用户直播号、粉丝、舰团数查询 [@yanyongyu](https://github.com/yanyongyu) ([#1385](https://github.com/nonebot/nonebot2/pull/1385))
|
||||
- Plugin: 投胎模拟器 [@yanyongyu](https://github.com/yanyongyu) ([#1382](https://github.com/nonebot/nonebot2/pull/1382))
|
||||
- Plugin: Apex API Query [@yanyongyu](https://github.com/yanyongyu) ([#1375](https://github.com/nonebot/nonebot2/pull/1375))
|
||||
- Plugin: 随个人 [@yanyongyu](https://github.com/yanyongyu) ([#1373](https://github.com/nonebot/nonebot2/pull/1373))
|
||||
- Plugin: 动漫资源获取 [@yanyongyu](https://github.com/yanyongyu) ([#1371](https://github.com/nonebot/nonebot2/pull/1371))
|
||||
- Plugin: 日麻小工具 [@yanyongyu](https://github.com/yanyongyu) ([#1365](https://github.com/nonebot/nonebot2/pull/1365))
|
||||
- Plugin: 图像超分辨率增强 [@yanyongyu](https://github.com/yanyongyu) ([#1362](https://github.com/nonebot/nonebot2/pull/1362))
|
||||
- Plugin: 二次元化图像 [@yanyongyu](https://github.com/yanyongyu) ([#1360](https://github.com/nonebot/nonebot2/pull/1360))
|
||||
- Plugin: 日麻寄分器 [@yanyongyu](https://github.com/yanyongyu) ([#1357](https://github.com/nonebot/nonebot2/pull/1357))
|
||||
- Plugin: 文本生成器 [@yanyongyu](https://github.com/yanyongyu) ([#1355](https://github.com/nonebot/nonebot2/pull/1355))
|
||||
- Plugin: 反嘴臭插件 [@yanyongyu](https://github.com/yanyongyu) ([#1350](https://github.com/nonebot/nonebot2/pull/1350))
|
||||
- Plugin: 用户\&群聊黑名单 [@yanyongyu](https://github.com/yanyongyu) ([#1348](https://github.com/nonebot/nonebot2/pull/1348))
|
||||
- Plugin: NoneBot SQLAlchemy 封装 [@yanyongyu](https://github.com/yanyongyu) ([#1345](https://github.com/nonebot/nonebot2/pull/1345))
|
||||
- Plugin: 通用抽图/语音 [@yanyongyu](https://github.com/yanyongyu) ([#1341](https://github.com/nonebot/nonebot2/pull/1341))
|
||||
- Plugin: kfcrazy [@yanyongyu](https://github.com/yanyongyu) ([#1339](https://github.com/nonebot/nonebot2/pull/1339))
|
||||
- Plugin: 二次元图像鉴赏 [@yanyongyu](https://github.com/yanyongyu) ([#1337](https://github.com/nonebot/nonebot2/pull/1337))
|
||||
- Plugin: ayaka 衍生插件 - 坏词撤回 [@yanyongyu](https://github.com/yanyongyu) ([#1335](https://github.com/nonebot/nonebot2/pull/1335))
|
||||
- Plugin: ayaka 衍生插件 - 时区助手 [@yanyongyu](https://github.com/yanyongyu) ([#1332](https://github.com/nonebot/nonebot2/pull/1332))
|
||||
- Plugin: ayaka 衍生插件 - 谁是卧底 [@yanyongyu](https://github.com/yanyongyu) ([#1330](https://github.com/nonebot/nonebot2/pull/1330))
|
||||
- Plugin: ayaka 衍生插件 - 小游戏合集 [@yanyongyu](https://github.com/yanyongyu) ([#1328](https://github.com/nonebot/nonebot2/pull/1328))
|
||||
- Plugin: bnhhsh -「不能好好说话!」 [@yanyongyu](https://github.com/yanyongyu) ([#1326](https://github.com/nonebot/nonebot2/pull/1326))
|
||||
- Plugin: AI 绘图 [@yanyongyu](https://github.com/yanyongyu) ([#1323](https://github.com/nonebot/nonebot2/pull/1323))
|
||||
- Plugin: novelai [@yanyongyu](https://github.com/yanyongyu) ([#1319](https://github.com/nonebot/nonebot2/pull/1319))
|
||||
- Plugin: 游戏王小程序查价 [@yanyongyu](https://github.com/yanyongyu) ([#1317](https://github.com/nonebot/nonebot2/pull/1317))
|
||||
- Plugin: 监测群事件 [@yanyongyu](https://github.com/yanyongyu) ([#1320](https://github.com/nonebot/nonebot2/pull/1320))
|
||||
- Plugin: 轮盘禁言小游戏 [@yanyongyu](https://github.com/yanyongyu) ([#1311](https://github.com/nonebot/nonebot2/pull/1311))
|
||||
- Plugin: 真白萌自动签到 [@yanyongyu](https://github.com/yanyongyu) ([#1308](https://github.com/nonebot/nonebot2/pull/1308))
|
||||
- Plugin: BiliRequestAll [@yanyongyu](https://github.com/yanyongyu) ([#1302](https://github.com/nonebot/nonebot2/pull/1302))
|
||||
- Plugin: 监听者 [@yanyongyu](https://github.com/yanyongyu) ([#1299](https://github.com/nonebot/nonebot2/pull/1299))
|
||||
|
||||
### 🍻 机器人发布
|
||||
|
||||
- Bot: Bread Dog Bot [@yanyongyu](https://github.com/yanyongyu) ([#1380](https://github.com/nonebot/nonebot2/pull/1380))
|
||||
- Bot: hsbot [@yanyongyu](https://github.com/yanyongyu) ([#1369](https://github.com/nonebot/nonebot2/pull/1369))
|
||||
|
||||
### 🍻 适配器发布
|
||||
|
||||
- Adapter: Ntchat [@yanyongyu](https://github.com/yanyongyu) ([#1314](https://github.com/nonebot/nonebot2/pull/1314))
|
||||
|
||||
## v2.0.0-rc.1
|
||||
|
||||
### 💥 破坏性变更
|
||||
|
||||
- Feature: `SUPERUSER` 权限匹配任意超管事件 [@AkiraXie](https://github.com/AkiraXie) ([#1275](https://github.com/nonebot/nonebot2/pull/1275))
|
||||
- Remove: 移除过时的 State 注入参数 [@yanyongyu](https://github.com/yanyongyu) ([#1160](https://github.com/nonebot/nonebot2/pull/1160))
|
||||
- Remove: 移除过时的 `nonebot.plugins` toml 配置 [@yanyongyu](https://github.com/yanyongyu) ([#1151](https://github.com/nonebot/nonebot2/pull/1151))
|
||||
- Remove: 移除 Python 3.7 支持 [@yanyongyu](https://github.com/yanyongyu) ([#1148](https://github.com/nonebot/nonebot2/pull/1148))
|
||||
- Remove: 删除过时的 Export 功能 [@yanyongyu](https://github.com/yanyongyu) ([#1125](https://github.com/nonebot/nonebot2/pull/1125))
|
||||
|
||||
### 🚀 新功能
|
||||
|
||||
- Feature: `SUPERUSER` 权限匹配任意超管事件 [@AkiraXie](https://github.com/AkiraXie) ([#1275](https://github.com/nonebot/nonebot2/pull/1275))
|
||||
- Feature: 改进 `CommandGroup` 与 `MatcherGroup` 的结构 [@A-kirami](https://github.com/A-kirami) ([#1240](https://github.com/nonebot/nonebot2/pull/1240))
|
||||
- Feature: 调整日志输出格式与等级 [@yanyongyu](https://github.com/yanyongyu) ([#1233](https://github.com/nonebot/nonebot2/pull/1233))
|
||||
- Feature: 优化依赖注入结构 [@yanyongyu](https://github.com/yanyongyu) ([#1227](https://github.com/nonebot/nonebot2/pull/1227))
|
||||
- Featue: `load_plugin` 支持 `pathlib.Path` [@Lancercmd](https://github.com/Lancercmd) ([#1194](https://github.com/nonebot/nonebot2/pull/1194))
|
||||
- Feature: 新增事件类型过滤 rule [@yanyongyu](https://github.com/yanyongyu) ([#1183](https://github.com/nonebot/nonebot2/pull/1183))
|
||||
- Feature: shell command 添加富文本支持 [@yanyongyu](https://github.com/yanyongyu) ([#1171](https://github.com/nonebot/nonebot2/pull/1171))
|
||||
|
||||
### 🐛 Bug 修复
|
||||
|
||||
- Fix: 内置规则和权限没有捕获错误 [@yanyongyu](https://github.com/yanyongyu) ([#1291](https://github.com/nonebot/nonebot2/pull/1291))
|
||||
- Fix: 修复 User 会话权限更新嵌套问题 [@yanyongyu](https://github.com/yanyongyu) ([#1208](https://github.com/nonebot/nonebot2/pull/1208))
|
||||
- Fix: 修复当消息与不支持的类型相加时抛出的异常类型错误 [@mnixry](https://github.com/mnixry) ([#1166](https://github.com/nonebot/nonebot2/pull/1166))
|
||||
|
||||
### 💫 杂项
|
||||
|
||||
- Fix: 修正 GenshinUID 的发布类型 [@A-kirami](https://github.com/A-kirami) ([#1243](https://github.com/nonebot/nonebot2/pull/1243))
|
||||
- Remove: 移除未使用的导入 [@A-kirami](https://github.com/A-kirami) ([#1236](https://github.com/nonebot/nonebot2/pull/1236))
|
||||
- Plugin: 更新插件米游社辅助工具 tag [@Ljzd-PRO](https://github.com/Ljzd-PRO) ([#1221](https://github.com/nonebot/nonebot2/pull/1221))
|
||||
- Plugin: 修改插件多功能简易群管信息 [@HuYihe2008](https://github.com/HuYihe2008) ([#1180](https://github.com/nonebot/nonebot2/pull/1180))
|
||||
- Plugin: 修改插件多功能简易群管信息 [@HuYihe2008](https://github.com/HuYihe2008) ([#1159](https://github.com/nonebot/nonebot2/pull/1159))
|
||||
- Plugin: 修改 QQ 续火花插件信息 [@GC-ZF](https://github.com/GC-ZF) ([#1158](https://github.com/nonebot/nonebot2/pull/1158))
|
||||
- Plugin: 修改插件多功能简易群管信息 [@HuYihe2008](https://github.com/HuYihe2008) ([#1154](https://github.com/nonebot/nonebot2/pull/1154))
|
||||
|
||||
### 🍻 插件发布
|
||||
|
||||
- Plugin: 文字识别 [@yanyongyu](https://github.com/yanyongyu) ([#1295](https://github.com/nonebot/nonebot2/pull/1295))
|
||||
- Plugin: 在线编曲 [@yanyongyu](https://github.com/yanyongyu) ([#1293](https://github.com/nonebot/nonebot2/pull/1293))
|
||||
- Plugin: 图灵机器人 [@yanyongyu](https://github.com/yanyongyu) ([#1289](https://github.com/nonebot/nonebot2/pull/1289))
|
||||
- Plugin: PicStatus [@yanyongyu](https://github.com/yanyongyu) ([#1287](https://github.com/nonebot/nonebot2/pull/1287))
|
||||
- Plugin: 阿里云盘福利码自动兑换 [@yanyongyu](https://github.com/yanyongyu) ([#1283](https://github.com/nonebot/nonebot2/pull/1283))
|
||||
- Plugin: gal 角色语音生成 [@yanyongyu](https://github.com/yanyongyu) ([#1281](https://github.com/nonebot/nonebot2/pull/1281))
|
||||
- Plugin: 漂流瓶 [@yanyongyu](https://github.com/yanyongyu) ([#1279](https://github.com/nonebot/nonebot2/pull/1279))
|
||||
- Plugin: BWIKI 助手移植版 [@yanyongyu](https://github.com/yanyongyu) ([#1274](https://github.com/nonebot/nonebot2/pull/1274))
|
||||
- Plugin: nonebot 物联网插件 [@yanyongyu](https://github.com/yanyongyu) ([#1265](https://github.com/nonebot/nonebot2/pull/1265))
|
||||
- Plugin: 狼人杀插件 [@yanyongyu](https://github.com/yanyongyu) ([#1252](https://github.com/nonebot/nonebot2/pull/1252))
|
||||
- Plugin: ayaka - 文字游戏开发辅助插件 [@yanyongyu](https://github.com/yanyongyu) ([#1254](https://github.com/nonebot/nonebot2/pull/1254))
|
||||
- Plugin: 图像超分辨率重建 [@yanyongyu](https://github.com/yanyongyu) ([#1250](https://github.com/nonebot/nonebot2/pull/1250))
|
||||
- Plugin: Minecraft Server 聊天同步 [@yanyongyu](https://github.com/yanyongyu) ([#1245](https://github.com/nonebot/nonebot2/pull/1245))
|
||||
- Plugin: 查询 ETH 合并日期 [@yanyongyu](https://github.com/yanyongyu) ([#1232](https://github.com/nonebot/nonebot2/pull/1232))
|
||||
- Plugin: 星际战甲事件查询 [@yanyongyu](https://github.com/yanyongyu) ([#1220](https://github.com/nonebot/nonebot2/pull/1220))
|
||||
- Plugin: 米游社辅助工具 [@yanyongyu](https://github.com/yanyongyu) ([#1218](https://github.com/nonebot/nonebot2/pull/1218))
|
||||
- Plugin: 原神每日材料查询 [@yanyongyu](https://github.com/yanyongyu) ([#1216](https://github.com/nonebot/nonebot2/pull/1216))
|
||||
- Plugin: MC_QQ_MCRcon [@yanyongyu](https://github.com/yanyongyu) ([#1211](https://github.com/nonebot/nonebot2/pull/1211))
|
||||
- Plugin: 原神角色展柜查询 [@yanyongyu](https://github.com/yanyongyu) ([#1209](https://github.com/nonebot/nonebot2/pull/1209))
|
||||
- Plugin: 修仙模拟器 [@yanyongyu](https://github.com/yanyongyu) ([#1202](https://github.com/nonebot/nonebot2/pull/1202))
|
||||
- Plugin: 赛博浅草寺 [@yanyongyu](https://github.com/yanyongyu) ([#1206](https://github.com/nonebot/nonebot2/pull/1206))
|
||||
- Plugin: 不背单词 [@yanyongyu](https://github.com/yanyongyu) ([#1204](https://github.com/nonebot/nonebot2/pull/1204))
|
||||
- Plugin: 自识别 todo [@yanyongyu](https://github.com/yanyongyu) ([#1193](https://github.com/nonebot/nonebot2/pull/1193))
|
||||
- Plugin: 雨课堂自动签到 [@yanyongyu](https://github.com/yanyongyu) ([#1189](https://github.com/nonebot/nonebot2/pull/1189))
|
||||
- Plugin: 反馈及通知 [@yanyongyu](https://github.com/yanyongyu) ([#1187](https://github.com/nonebot/nonebot2/pull/1187))
|
||||
- Plugin: MagiaDice 骰娘及 TRPGLOG [@yanyongyu](https://github.com/yanyongyu) ([#1185](https://github.com/nonebot/nonebot2/pull/1185))
|
||||
- Plugin: 面麻小助手 [@yanyongyu](https://github.com/yanyongyu) ([#1191](https://github.com/nonebot/nonebot2/pull/1191))
|
||||
- Plugin: 话痨排行榜 [@yanyongyu](https://github.com/yanyongyu) ([#1182](https://github.com/nonebot/nonebot2/pull/1182))
|
||||
- Plugin: 保存群聊闪照 [@yanyongyu](https://github.com/yanyongyu) ([#1179](https://github.com/nonebot/nonebot2/pull/1179))
|
||||
- Plugin: 课表查询 [@yanyongyu](https://github.com/yanyongyu) ([#1168](https://github.com/nonebot/nonebot2/pull/1168))
|
||||
- Plugin: 业余无线电助手 [@yanyongyu](https://github.com/yanyongyu) ([#1173](https://github.com/nonebot/nonebot2/pull/1173))
|
||||
- Plugin: NoneBot 树形帮助插件 [@yanyongyu](https://github.com/yanyongyu) ([#1177](https://github.com/nonebot/nonebot2/pull/1177))
|
||||
- Plugin: 工作性价比 [@yanyongyu](https://github.com/yanyongyu) ([#1175](https://github.com/nonebot/nonebot2/pull/1175))
|
||||
- Plugin: 娶群友 [@yanyongyu](https://github.com/yanyongyu) ([#1170](https://github.com/nonebot/nonebot2/pull/1170))
|
||||
- Plugin: PixivBot [@yanyongyu](https://github.com/yanyongyu) ([#1165](https://github.com/nonebot/nonebot2/pull/1165))
|
||||
- Plugin: 日韩中 VITS 模型原神拟声 [@yanyongyu](https://github.com/yanyongyu) ([#1162](https://github.com/nonebot/nonebot2/pull/1162))
|
||||
- Plugin: 每日人品 [@yanyongyu](https://github.com/yanyongyu) ([#1156](https://github.com/nonebot/nonebot2/pull/1156))
|
||||
- Plugin: nonebot-plugin-drawer [@yanyongyu](https://github.com/yanyongyu) ([#1146](https://github.com/nonebot/nonebot2/pull/1146))
|
||||
- Plugin: 小游戏合集 [@yanyongyu](https://github.com/yanyongyu) ([#1150](https://github.com/nonebot/nonebot2/pull/1150))
|
||||
- Plugin: 简易群管(带入群欢迎) [@yanyongyu](https://github.com/yanyongyu) ([#1142](https://github.com/nonebot/nonebot2/pull/1142))
|
||||
- Plugin: wiki 条目搜索、获取简介 [@yanyongyu](https://github.com/yanyongyu) ([#1133](https://github.com/nonebot/nonebot2/pull/1133))
|
||||
- Plugin: bangumi 搜索 [@yanyongyu](https://github.com/yanyongyu) ([#1137](https://github.com/nonebot/nonebot2/pull/1137))
|
||||
- Plugin: 疫情小助手-频道版 [@yanyongyu](https://github.com/yanyongyu) ([#1131](https://github.com/nonebot/nonebot2/pull/1131))
|
||||
- Plugin: MC_QQ 通信 [@yanyongyu](https://github.com/yanyongyu) ([#1127](https://github.com/nonebot/nonebot2/pull/1127))
|
||||
- Plugin: BAWiki [@yanyongyu](https://github.com/yanyongyu) ([#1129](https://github.com/nonebot/nonebot2/pull/1129))
|
||||
|
||||
### 🍻 机器人发布
|
||||
|
||||
- Bot: IdhagnBot [@yanyongyu](https://github.com/yanyongyu) ([#1267](https://github.com/nonebot/nonebot2/pull/1267))
|
||||
- Bot: LittlePaimon [@yanyongyu](https://github.com/yanyongyu) ([#1256](https://github.com/nonebot/nonebot2/pull/1256))
|
||||
- Bot: GenshinUID [@yanyongyu](https://github.com/yanyongyu) ([#1226](https://github.com/nonebot/nonebot2/pull/1226))
|
||||
- Bot: 小白机器人 [@yanyongyu](https://github.com/yanyongyu) ([#1224](https://github.com/nonebot/nonebot2/pull/1224))
|
||||
|
||||
### 🍻 适配器发布
|
||||
|
||||
- Adapter: GitHub [@yanyongyu](https://github.com/yanyongyu) ([#1297](https://github.com/nonebot/nonebot2/pull/1297))
|
||||
- Adapter: Console [@yanyongyu](https://github.com/yanyongyu) ([#1213](https://github.com/nonebot/nonebot2/pull/1213))
|
||||
|
||||
## v2.0.0-beta.5
|
||||
|
||||
### 🚀 新功能
|
||||
|
@@ -78,5 +78,40 @@
|
||||
"homepage": "https://onebot.adapters.nonebot.dev/",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot.adapters.console",
|
||||
"project_link": "nonebot-adapter-console",
|
||||
"name": "Console",
|
||||
"desc": "基于终端的交互式适配器",
|
||||
"author": "Melodyknit",
|
||||
"homepage": "https://github.com/nonebot/adapter-console",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot.adapters.github",
|
||||
"project_link": "nonebot-adapter-github",
|
||||
"name": "GitHub",
|
||||
"desc": "GitHub APP & OAuth APP integration",
|
||||
"author": "yanyongyu",
|
||||
"homepage": "https://github.com/nonebot/adapter-github",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot.adapters.ntchat",
|
||||
"project_link": "nonebot-adapter-ntchat",
|
||||
"name": "Ntchat",
|
||||
"desc": "pc hook的微信客户端适配",
|
||||
"author": "JustUndertaker",
|
||||
"homepage": "https://github.com/JustUndertaker/adapter-ntchat",
|
||||
"tags": [
|
||||
{
|
||||
"label": "微信",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
}
|
||||
]
|
||||
]
|
||||
|
@@ -123,7 +123,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Inkar Suki",
|
||||
"desc": "一个十分方便的Bot,支持包括Webhook、群管、互动等一系列功能",
|
||||
"desc": "一个十分方便的Bot,支持包括Webhook、群管、剑网3等一系列功能,持续更新中……",
|
||||
"author": "HornCopper",
|
||||
"homepage": "https://github.com/HornCopper/Inkar-Suki",
|
||||
"tags": [
|
||||
@@ -136,8 +136,8 @@
|
||||
"color": "#374fd0"
|
||||
},
|
||||
{
|
||||
"label": "群管",
|
||||
"color": "#4ede39"
|
||||
"label": "剑网3",
|
||||
"color": "#ff0033"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
@@ -233,5 +233,82 @@
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "小白机器人",
|
||||
"desc": "一个高度依赖数据库的群管理机器人",
|
||||
"author": "SDIJF1521",
|
||||
"homepage": "https://github.com/SDIJF1521/qqai",
|
||||
"tags": [
|
||||
{
|
||||
"label": "群管理",
|
||||
"color": "#ea5252"
|
||||
},
|
||||
{
|
||||
"label": "新人作品",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "LittlePaimon",
|
||||
"desc": "小派蒙,多功能原神机器人。",
|
||||
"author": "CMHopeSunshine",
|
||||
"homepage": "https://github.com/CMHopeSunshine/LittlePaimon",
|
||||
"tags": [
|
||||
{
|
||||
"label": "原神",
|
||||
"color": "#7a52ea"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "IdhagnBot",
|
||||
"desc": "🐱🤖 一个以娱乐功能为主的缝合怪(划掉)QQ机器人,包含一定Furry要素但是不会卖萌(就是逊啦!)",
|
||||
"author": "su226",
|
||||
"homepage": "https://github.com/su226/IdhagnBot",
|
||||
"tags": [
|
||||
{
|
||||
"label": "a:OneBot",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "hsbot",
|
||||
"desc": "服务于《炉石传说》玩家的机器人,上线至今已有加入十余个个炉石相关群聊,上千名用户使用,响应请求数万次。 数据使用:HSreplay, Fbigame, Hearthstone API",
|
||||
"author": "gzy02",
|
||||
"homepage": "https://github.com/gzy02/hsbot",
|
||||
"tags": [
|
||||
{
|
||||
"label": "炉石传说",
|
||||
"color": "#526fea"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "Bread Dog Bot",
|
||||
"desc": "Terraria TShock QQ 机器人",
|
||||
"author": "Qianyiovo",
|
||||
"homepage": "https://github.com/Qianyiovo/bread_dog_bot",
|
||||
"tags": [
|
||||
{
|
||||
"label": "TShock",
|
||||
"color": "#ea5252"
|
||||
},
|
||||
{
|
||||
"label": "泰拉瑞亚",
|
||||
"color": "#5dea52"
|
||||
},
|
||||
{
|
||||
"label": "Terraria",
|
||||
"color": "#5dea52"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
}
|
||||
]
|
||||
]
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,106 +0,0 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
description: nonebot.dependencies 模块
|
||||
---
|
||||
|
||||
# nonebot.dependencies
|
||||
|
||||
本模块模块实现了依赖注入的定义与处理。
|
||||
|
||||
## _abstract class_ `Param(default=PydanticUndefined, **kwargs)` {#Param}
|
||||
|
||||
- **说明**
|
||||
|
||||
依赖注入的基本单元 —— 参数。
|
||||
|
||||
继承自 `pydantic.fields.FieldInfo`,用于描述参数信息(不包括参数名)。
|
||||
|
||||
- **参数**
|
||||
|
||||
- `default` (Any)
|
||||
|
||||
- `**kwargs` (Any)
|
||||
|
||||
## _class_ `Dependent(*, call, pre_checkers=None, params=None, parameterless=None, allow_types=None)` {#Dependent}
|
||||
|
||||
- **说明**
|
||||
|
||||
依赖注入容器
|
||||
|
||||
- **参数**
|
||||
|
||||
- `call` ((\*Any, \*\*Any) -> Any): 依赖注入的可调用对象,可以是任何 Callable 对象
|
||||
|
||||
- `pre_checkers` (list[[Param](#Param)] | None): 依赖注入解析前的参数检查
|
||||
|
||||
- `params` (list[pydantic.fields.ModelField] | None): 具名参数列表
|
||||
|
||||
- `parameterless` (list[[Param](#Param)] | None): 匿名参数列表
|
||||
|
||||
- `allow_types` (list[Type[[Param](#Param)]] | None): 允许的参数类型
|
||||
|
||||
### _method_ `append_parameterless(self, value)` {#Dependent-append_parameterless}
|
||||
|
||||
- **参数**
|
||||
|
||||
- `value` (Any)
|
||||
|
||||
- **返回**
|
||||
|
||||
- None
|
||||
|
||||
### _classmethod_ `parse(cls, *, call, parameterless=None, allow_types=None)` {#Dependent-parse}
|
||||
|
||||
- **参数**
|
||||
|
||||
- `call` ((\*Any, \*\*Any) -> Any)
|
||||
|
||||
- `parameterless` (list[Any] | None)
|
||||
|
||||
- `allow_types` (list[Type[[Param](#Param)]] | None)
|
||||
|
||||
- **返回**
|
||||
|
||||
- (~ T)
|
||||
|
||||
### _method_ `parse_param(self, name, param)` {#Dependent-parse_param}
|
||||
|
||||
- **参数**
|
||||
|
||||
- `name` (str)
|
||||
|
||||
- `param` (inspect.Parameter)
|
||||
|
||||
- **返回**
|
||||
|
||||
- [Param](#Param)
|
||||
|
||||
### _method_ `parse_parameterless(self, value)` {#Dependent-parse_parameterless}
|
||||
|
||||
- **参数**
|
||||
|
||||
- `value` (Any)
|
||||
|
||||
- **返回**
|
||||
|
||||
- [Param](#Param)
|
||||
|
||||
### _method_ `prepend_parameterless(self, value)` {#Dependent-prepend_parameterless}
|
||||
|
||||
- **参数**
|
||||
|
||||
- `value` (Any)
|
||||
|
||||
- **返回**
|
||||
|
||||
- None
|
||||
|
||||
### _async method_ `solve(self, **params)` {#Dependent-solve}
|
||||
|
||||
- **参数**
|
||||
|
||||
- `**params` (Any)
|
||||
|
||||
- **返回**
|
||||
|
||||
- dict[str, Any]
|
@@ -1,45 +0,0 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
description: nonebot.plugin.export 模块
|
||||
---
|
||||
|
||||
# nonebot.plugin.export
|
||||
|
||||
本模块定义了插件导出的内容对象。
|
||||
|
||||
在新版插件系统中,推荐优先使用直接 import 所需要的插件内容。
|
||||
|
||||
## _class_ `Export()` {#Export}
|
||||
|
||||
- **说明**
|
||||
|
||||
插件导出内容以使得其他插件可以获得。
|
||||
|
||||
- **用法**
|
||||
|
||||
```python
|
||||
nonebot.export().default = "bar"
|
||||
|
||||
@nonebot.export()
|
||||
def some_function():
|
||||
pass
|
||||
|
||||
# this doesn't work before python 3.9
|
||||
# use
|
||||
# export = nonebot.export(); @export.sub
|
||||
# instead
|
||||
# See also PEP-614: https://www.python.org/dev/peps/pep-0614/
|
||||
@nonebot.export().sub
|
||||
def something_else():
|
||||
pass
|
||||
```
|
||||
|
||||
## _def_ `export()` {#export}
|
||||
|
||||
- **说明**
|
||||
|
||||
获取当前插件的导出内容对象
|
||||
|
||||
- **返回**
|
||||
|
||||
- [Export](#Export)
|
@@ -8,7 +8,7 @@ slug: /
|
||||
|
||||
NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架,它基于 Python 的类型注解和异步特性,能够为你的需求实现提供便捷灵活的支持。
|
||||
|
||||
需要注意的是,NoneBot2 仅支持 **Python 3.7.3 以上版本**
|
||||
需要注意的是,NoneBot2 仅支持 **Python 3.8 以上版本**
|
||||
|
||||
## 特色
|
||||
|
Before Width: | Height: | Size: 376 KiB After Width: | Height: | Size: 376 KiB |
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user