mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-10-07 11:16:43 +00:00
Compare commits
154 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f2b0b1752b | ||
|
81dcc65f99 | ||
|
ac90df929e | ||
|
555268239f | ||
|
7009c8e8c1 | ||
|
2f3cc84f82 | ||
|
9444e01f0f | ||
|
23b7a94b9a | ||
|
70ece41b66 | ||
|
a5bb6e4220 | ||
|
4fc99771c5 | ||
|
6601def5f7 | ||
|
b2edea141e | ||
|
38886b9651 | ||
|
1b225cbbca | ||
|
b4f004c500 | ||
|
7a345714aa | ||
|
cb9fcae64c | ||
|
6ebeefed79 | ||
|
6dc87a9455 | ||
|
7dd7c927bf | ||
|
e167865686 | ||
|
29364679c4 | ||
|
ebbe8beec0 | ||
|
a04580e79e | ||
|
bfe9e7e253 | ||
|
720398198f | ||
|
5ebf349886 | ||
|
f8f5750c3b | ||
|
8d9be61406 | ||
|
42ea650509 | ||
|
a941a0f292 | ||
|
89f8745425 | ||
|
cc476528d8 | ||
|
64f6c2dd4c | ||
|
81d9531b42 | ||
|
3512b0ab98 | ||
|
ab3e916770 | ||
|
21376a5bfa | ||
|
5046b2a86e | ||
|
910c768910 | ||
|
5a526ddb40 | ||
|
4c5c97dca6 | ||
|
b3e0fb4830 | ||
|
258aa7d2d7 | ||
|
5c72fd5ba7 | ||
|
26e4f23a67 | ||
|
28fc6c35f0 | ||
|
3ef1d7d5d7 | ||
|
8474d8987e | ||
|
13ddfa1bdd | ||
|
ec8be10f26 | ||
|
511c521a68 | ||
|
0ef5940d0f | ||
|
eecc881cd8 | ||
|
770141cf0a | ||
|
b2b20ffc4a | ||
|
94a6067a4b | ||
|
77220d9d1f | ||
|
647ad9ff8f | ||
|
04182eefba | ||
|
7b4aa08c54 | ||
|
0033d7c686 | ||
|
c40b95f3e9 | ||
|
1fa44ca5c1 | ||
|
381f6633f6 | ||
|
d617508e32 | ||
|
8248e88686 | ||
|
25649373a6 | ||
|
3bee189598 | ||
|
c1b1742b20 | ||
|
3e826cab72 | ||
|
4ef4bb0042 | ||
|
25ac653623 | ||
|
b35bdfe6dc | ||
|
f06efca8cc | ||
|
a899523607 | ||
|
2c162335cb | ||
|
3a12984d4b | ||
|
7211f24a7d | ||
|
649624ed80 | ||
|
c03ff4e676 | ||
|
0b5a18cb63 | ||
|
518bf16082 | ||
|
b625a5d19a | ||
|
acca22e179 | ||
|
a3009d45dc | ||
|
fd3d1bb115 | ||
|
7282da8b04 | ||
|
7a3c7476fb | ||
|
f1046cfb11 | ||
|
8de25447b3 | ||
|
3cdbf35dc6 | ||
|
0228e255e1 | ||
|
353d16ebfd | ||
|
3d5dd5969c | ||
|
fe21cbfa1d | ||
|
c20f65636f | ||
|
eade8face6 | ||
|
ab75133e9d | ||
|
89596fb708 | ||
|
eedcf0779d | ||
|
05333260b7 | ||
|
55fd447230 | ||
|
263e6b25e2 | ||
|
e00890033e | ||
|
20d3d62bd5 | ||
|
080b876d93 | ||
|
27a3d1f0bb | ||
|
7a47985c2b | ||
|
8d97081948 | ||
|
f4ffa07c8b | ||
|
1b1ddc5c0f | ||
|
30dbd270a6 | ||
|
7d3c7c4933 | ||
|
8c8436a94f | ||
|
8601942ed3 | ||
|
4cc958ca17 | ||
|
472a2c7866 | ||
|
222609182e | ||
|
dccf2f3ca8 | ||
|
156807c365 | ||
|
50941f5259 | ||
|
2de1524a89 | ||
|
bdd17b62cc | ||
|
3a9e800a58 | ||
|
cb8d48c362 | ||
|
a5981c05d5 | ||
|
4cb87e596d | ||
|
2725a0a324 | ||
|
f6b0809e5f | ||
|
6181c1760f | ||
|
324277091c | ||
|
6eef863b70 | ||
|
7d52f5af4d | ||
|
0a70721ec0 | ||
|
f430f061ec | ||
|
572be1eb47 | ||
|
29cf7de1a6 | ||
|
c61e3cab90 | ||
|
77bdc5ecba | ||
|
16054d18c6 | ||
|
f0361295c3 | ||
|
9bd1964ae2 | ||
|
9141c88f77 | ||
|
491855876b | ||
|
6df28dd2a8 | ||
|
142d0f4d95 | ||
|
0127d765ae | ||
|
207c6b3c15 | ||
|
d2e699a13a | ||
|
ce9ba7dd9b | ||
|
2af23c9d89 | ||
|
8ee0f5efc4 |
@@ -10,9 +10,11 @@
|
||||
"settings": {
|
||||
"python.analysis.diagnosticMode": "workspace",
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"ruff.organizeImports": false,
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.ruff": true,
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
@@ -44,6 +46,7 @@
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.isort",
|
||||
"ms-python.black-formatter",
|
||||
"charliermarsh.ruff",
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
|
36
.github/ISSUE_TEMPLATE/plugin_publish.yml
vendored
36
.github/ISSUE_TEMPLATE/plugin_publish.yml
vendored
@@ -3,22 +3,6 @@ title: "Plugin: {name}"
|
||||
description: 发布插件到 NoneBot 官方商店
|
||||
labels: ["Plugin"]
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: 插件名称
|
||||
description: 插件名称
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: description
|
||||
attributes:
|
||||
label: 插件描述
|
||||
description: 插件描述
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: pypi
|
||||
attributes:
|
||||
@@ -37,15 +21,6 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: homepage
|
||||
attributes:
|
||||
label: 插件项目仓库/主页链接
|
||||
description: 插件项目仓库/主页链接
|
||||
placeholder: e.g. https://github.com/xxx/xxx
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: tags
|
||||
attributes:
|
||||
@@ -55,3 +30,14 @@ body:
|
||||
value: "[]"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: 插件配置项
|
||||
description: 插件配置项
|
||||
render: dotenv
|
||||
placeholder: |
|
||||
# e.g.
|
||||
# KEY=VALUE
|
||||
# KEY2=VALUE2
|
||||
|
14
.github/workflows/noneflow.yml
vendored
14
.github/workflows/noneflow.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
plugin_test:
|
||||
@@ -40,6 +40,7 @@ jobs:
|
||||
outputs:
|
||||
result: ${{ steps.plugin-test.outputs.RESULT }}
|
||||
output: ${{ steps.plugin-test.outputs.OUTPUT }}
|
||||
metadata: ${{ steps.plugin-test.outputs.METADATA }}
|
||||
steps:
|
||||
- name: Install Poetry
|
||||
if: ${{ !startsWith(github.event_name, 'pull_request') }}
|
||||
@@ -71,6 +72,12 @@ jobs:
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Cache pre-commit hooks
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .cache/.pre-commit
|
||||
key: noneflow-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
|
||||
- name: NoneFlow
|
||||
uses: docker://ghcr.io/nonebot/noneflow:latest
|
||||
with:
|
||||
@@ -84,5 +91,10 @@ jobs:
|
||||
env:
|
||||
PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }}
|
||||
PLUGIN_TEST_OUTPUT: ${{ needs.plugin_test.outputs.output }}
|
||||
PLUGIN_TEST_METADATA: ${{ needs.plugin_test.outputs.metadata }}
|
||||
APP_ID: ${{ secrets.APP_ID }}
|
||||
PRIVATE_KEY: ${{ secrets.APP_KEY }}
|
||||
PRE_COMMIT_HOME: /github/workspace/.cache/.pre-commit
|
||||
|
||||
- name: Fix permission
|
||||
run: sudo chown -R $(whoami):$(id -ng) .cache/.pre-commit
|
||||
|
26
.github/workflows/pyright.yml
vendored
Normal file
26
.github/workflows/pyright.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Pyright Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
paths:
|
||||
- "nonebot/**"
|
||||
- "packages/**"
|
||||
- "tests/**"
|
||||
|
||||
jobs:
|
||||
pyright:
|
||||
name: Pyright Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Python environment
|
||||
uses: ./.github/actions/setup-python
|
||||
|
||||
- run: echo "$(poetry env info --path)/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Run Pyright
|
||||
uses: jakebailey/pyright-action@v1
|
21
.github/workflows/ruff.yml
vendored
Normal file
21
.github/workflows/ruff.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Ruff Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
paths:
|
||||
- "nonebot/**"
|
||||
- "packages/**"
|
||||
- "tests/**"
|
||||
|
||||
jobs:
|
||||
ruff:
|
||||
name: Ruff Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Run Ruff Lint
|
||||
uses: chartboost/ruff-action@v1
|
@@ -6,11 +6,12 @@ ci:
|
||||
autoupdate_schedule: monthly
|
||||
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
||||
repos:
|
||||
- repo: https://github.com/hadialqattan/pycln
|
||||
rev: v2.1.3
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.276
|
||||
hooks:
|
||||
- id: pycln
|
||||
args: [--config, pyproject.toml]
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.12.0
|
||||
|
21
README.md
21
README.md
@@ -19,9 +19,19 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
||||
<img src="https://img.shields.io/github/license/nonebot/nonebot2" alt="license">
|
||||
</a>
|
||||
<a href="https://pypi.python.org/pypi/nonebot2">
|
||||
<img src="https://img.shields.io/pypi/v/nonebot2" alt="pypi">
|
||||
<img src="https://img.shields.io/pypi/v/nonebot2?logo=python&logoColor=edb641" alt="pypi">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/python-3.8+-blue" alt="python">
|
||||
<img src="https://img.shields.io/badge/python-3.8+-blue?logo=python&logoColor=edb641" alt="python">
|
||||
<a href="https://github.com/psf/black">
|
||||
<img src="https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=edb641" alt="black">
|
||||
</a>
|
||||
<a href="https://github.com/Microsoft/pyright">
|
||||
<img src="https://img.shields.io/badge/types-pyright-797952.svg?logo=python&logoColor=edb641" alt="pyright">
|
||||
</a>
|
||||
<a href="https://github.com/astral-sh/ruff">
|
||||
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="ruff">
|
||||
</a>
|
||||
<br />
|
||||
<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>
|
||||
@@ -31,6 +41,12 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
||||
<a href="https://results.pre-commit.ci/latest/github/nonebot/nonebot2/master">
|
||||
<img src="https://results.pre-commit.ci/badge/github/nonebot/nonebot2/master.svg" alt="pre-commit" />
|
||||
</a>
|
||||
<a href="https://github.com/nonebot/nonebot2/actions/workflows/pyright.yml">
|
||||
<img src="https://github.com/nonebot/nonebot2/actions/workflows/pyright.yml/badge.svg?branch=master&event=push" alt="pyright">
|
||||
</a>
|
||||
<a href="https://github.com/nonebot/nonebot2/actions/workflows/ruff.yml">
|
||||
<img src="https://github.com/nonebot/nonebot2/actions/workflows/ruff.yml/badge.svg?branch=master&event=push" alt="ruff">
|
||||
</a>
|
||||
<br />
|
||||
<a href="https://onebot.dev/">
|
||||
<img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
|
||||
@@ -109,6 +125,7 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
|
||||
| MineCraft([仓库](https://github.com/17TheWord/nonebot-adapter-minecraft)) | ↗️ | 由社区贡献 |
|
||||
| BiliBili Live([仓库](https://github.com/wwweww/adapter-bilibili)) | ↗️ | 由社区贡献 |
|
||||
| Walle-Q([仓库](https://github.com/onebot-walle/nonebot_adapter_walleq)) | ↗️ | QQ 协议,由社区贡献 |
|
||||
| Villa([仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa)) | ↗️ | 米游社大别野 Bot 协议,由社区贡献 |
|
||||
|
||||
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
|
||||
|
||||
|
@@ -24,12 +24,17 @@
|
||||
- `load_all_plugins` => {ref}``load_all_plugins` <nonebot.plugin.load.load_all_plugins>`
|
||||
- `load_from_json` => {ref}``load_from_json` <nonebot.plugin.load.load_from_json>`
|
||||
- `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>`
|
||||
- `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>`
|
||||
- `get_plugin` => {ref}``get_plugin` <nonebot.plugin.get_plugin>`
|
||||
- `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>`
|
||||
- `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>`
|
||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||
|
||||
FrontMatter:
|
||||
@@ -69,7 +74,8 @@ def get_driver() -> Driver:
|
||||
全局 {ref}`nonebot.drivers.Driver` 对象
|
||||
|
||||
异常:
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
用法:
|
||||
```python
|
||||
@@ -83,23 +89,33 @@ def get_driver() -> Driver:
|
||||
|
||||
@overload
|
||||
def get_adapter(name: str) -> Adapter:
|
||||
...
|
||||
"""
|
||||
参数:
|
||||
name: 适配器名称
|
||||
|
||||
返回:
|
||||
指定名称的 {ref}`nonebot.adapters.Adapter` 对象
|
||||
"""
|
||||
|
||||
|
||||
@overload
|
||||
def get_adapter(name: Type[A]) -> A:
|
||||
...
|
||||
"""
|
||||
参数:
|
||||
name: 适配器类型
|
||||
|
||||
返回:
|
||||
指定类型的 {ref}`nonebot.adapters.Adapter` 对象
|
||||
"""
|
||||
|
||||
|
||||
def get_adapter(name: Union[str, Type[Adapter]]) -> Adapter:
|
||||
"""获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
||||
|
||||
返回:
|
||||
指定名称或类型的 {ref}`nonebot.adapters.Adapter` 对象
|
||||
|
||||
异常:
|
||||
ValueError: 指定的 {ref}`nonebot.adapters.Adapter` 未注册
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
用法:
|
||||
```python
|
||||
@@ -121,7 +137,8 @@ def get_adapters() -> Dict[str, Adapter]:
|
||||
所有 {ref}`nonebot.adapters.Adapter` 实例字典
|
||||
|
||||
异常:
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
用法:
|
||||
```python
|
||||
@@ -139,7 +156,8 @@ def get_app() -> Any:
|
||||
|
||||
异常:
|
||||
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ReverseDriver` 类型
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
用法:
|
||||
```python
|
||||
@@ -154,14 +172,16 @@ def get_app() -> Any:
|
||||
|
||||
|
||||
def get_asgi() -> Any:
|
||||
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应 [ASGI](https://asgi.readthedocs.io/) 对象。
|
||||
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应
|
||||
[ASGI](https://asgi.readthedocs.io/) 对象。
|
||||
|
||||
返回:
|
||||
ASGI 对象
|
||||
|
||||
异常:
|
||||
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ReverseDriver` 类型
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
用法:
|
||||
```python
|
||||
@@ -182,7 +202,8 @@ def get_bot(self_id: Optional[str] = None) -> Bot:
|
||||
当不提供时,返回一个 {ref}`nonebot.adapters.Bot`。
|
||||
|
||||
参数:
|
||||
self_id: 用来识别 {ref}`nonebot.adapters.Bot` 的 {ref}`nonebot.adapters.Bot.self_id` 属性
|
||||
self_id: 用来识别 {ref}`nonebot.adapters.Bot` 的
|
||||
{ref}`nonebot.adapters.Bot.self_id` 属性
|
||||
|
||||
返回:
|
||||
{ref}`nonebot.adapters.Bot` 对象
|
||||
@@ -190,7 +211,8 @@ def get_bot(self_id: Optional[str] = None) -> Bot:
|
||||
异常:
|
||||
KeyError: 对应 self_id 的 Bot 不存在
|
||||
ValueError: 没有传入 self_id 且没有 Bot 可用
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
用法:
|
||||
```python
|
||||
@@ -213,10 +235,12 @@ def get_bots() -> Dict[str, Bot]:
|
||||
"""获取所有连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。
|
||||
|
||||
返回:
|
||||
一个以 {ref}`nonebot.adapters.Bot.self_id` 为键,{ref}`nonebot.adapters.Bot` 对象为值的字典
|
||||
一个以 {ref}`nonebot.adapters.Bot.self_id` 为键
|
||||
{ref}`nonebot.adapters.Bot` 对象为值的字典
|
||||
|
||||
异常:
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
用法:
|
||||
```python
|
||||
|
@@ -1,19 +1,23 @@
|
||||
"""本模块定义了 NoneBot 本身运行所需的配置项。
|
||||
|
||||
NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。
|
||||
NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及
|
||||
[`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。
|
||||
|
||||
配置项需符合特殊格式或 json 序列化格式。详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。
|
||||
配置项需符合特殊格式或 json 序列化格式
|
||||
详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 1
|
||||
description: nonebot.config 模块
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from ipaddress import IPv4Address
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Tuple, Union, Mapping, Optional
|
||||
|
||||
from pydantic.utils import deep_update
|
||||
from pydantic.fields import Undefined, UndefinedType
|
||||
from pydantic import Extra, Field, BaseSettings, IPvAnyAddress
|
||||
from pydantic.env_settings import (
|
||||
DotenvType,
|
||||
@@ -28,9 +32,8 @@ from nonebot.log import logger
|
||||
|
||||
class CustomEnvSettings(EnvSettingsSource):
|
||||
def __call__(self, settings: BaseSettings) -> Dict[str, Any]:
|
||||
"""
|
||||
Build environment variables suitable for passing to the Model.
|
||||
"""
|
||||
"""从环境变量和 dotenv 配置文件中读取配置项。"""
|
||||
|
||||
d: Dict[str, Any] = {}
|
||||
|
||||
if settings.__config__.case_sensitive:
|
||||
@@ -42,21 +45,25 @@ class CustomEnvSettings(EnvSettingsSource):
|
||||
env_vars = {**env_file_vars, **env_vars}
|
||||
|
||||
for field in settings.__fields__.values():
|
||||
env_val: Optional[str] = None
|
||||
env_val: Union[str, None, UndefinedType] = Undefined
|
||||
for env_name in field.field_info.extra["env_names"]:
|
||||
env_val = env_vars.get(env_name)
|
||||
env_val = env_vars.get(env_name, Undefined)
|
||||
if env_name in env_file_vars:
|
||||
del env_file_vars[env_name]
|
||||
if env_val is not None:
|
||||
if env_val is not Undefined:
|
||||
break
|
||||
|
||||
is_complex, allow_parse_failure = self.field_is_complex(field)
|
||||
if is_complex:
|
||||
if env_val is None:
|
||||
if isinstance(env_val, UndefinedType):
|
||||
# field is complex but no value found so far, try explode_env_vars
|
||||
if env_val_built := self.explode_env_vars(field, env_vars):
|
||||
d[field.alias] = env_val_built
|
||||
elif env_val is None:
|
||||
d[field.alias] = env_val
|
||||
else:
|
||||
# field is complex and there's a value, decode that as JSON, then add explode_env_vars
|
||||
# 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:
|
||||
@@ -71,8 +78,9 @@ class CustomEnvSettings(EnvSettingsSource):
|
||||
)
|
||||
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
|
||||
elif not isinstance(env_val, UndefinedType):
|
||||
# simplest case, field is not complex
|
||||
# we only need to add the value if it was found
|
||||
d[field.alias] = env_val
|
||||
|
||||
# remain user custom config
|
||||
@@ -82,7 +90,7 @@ class CustomEnvSettings(EnvSettingsSource):
|
||||
# there's a value, decode that as JSON
|
||||
try:
|
||||
env_val = settings.__config__.parse_env_var(env_name, val_striped)
|
||||
except ValueError as e:
|
||||
except ValueError:
|
||||
logger.trace(
|
||||
"Error while parsing JSON for "
|
||||
f"{env_name!r}={val_striped!r}. "
|
||||
@@ -139,7 +147,7 @@ class BaseConfig(BaseSettings):
|
||||
class Env(BaseConfig):
|
||||
"""运行环境配置。大小写不敏感。
|
||||
|
||||
将会从 `环境变量` > `.env 环境配置文件` 的优先级读取环境信息。
|
||||
将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。
|
||||
"""
|
||||
|
||||
environment: str = "prod"
|
||||
@@ -170,15 +178,17 @@ class Config(BaseConfig):
|
||||
配置格式为 `<module>[:<Driver>][+<module>[:<Mixin>]]*`。
|
||||
|
||||
`~` 为 `nonebot.drivers.` 的缩写。
|
||||
|
||||
配置方法参考: [配置驱动器](https://nonebot.dev/docs/advanced/driver#%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E5%99%A8)
|
||||
"""
|
||||
host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore
|
||||
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。"""
|
||||
port: int = Field(default=8080, ge=1, le=65535)
|
||||
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。"""
|
||||
log_level: Union[int, str] = "INFO"
|
||||
"""NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称
|
||||
"""NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。
|
||||
|
||||
参考 [`loguru 日志等级`](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。
|
||||
参考 [记录日志](https://nonebot.dev/docs/appendices/log),[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。
|
||||
|
||||
:::tip 提示
|
||||
日志等级名称应为大写,如 `INFO`。
|
||||
@@ -209,6 +219,8 @@ class Config(BaseConfig):
|
||||
command_start: Set[str] = {"/"}
|
||||
"""命令的起始标记,用于判断一条消息是不是命令。
|
||||
|
||||
参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。
|
||||
|
||||
用法:
|
||||
```conf
|
||||
COMMAND_START=["/", ""]
|
||||
@@ -217,6 +229,8 @@ class Config(BaseConfig):
|
||||
command_sep: Set[str] = {"."}
|
||||
"""命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。
|
||||
|
||||
参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。
|
||||
|
||||
用法:
|
||||
```conf
|
||||
COMMAND_SEP=["."]
|
||||
|
@@ -4,6 +4,7 @@ FrontMatter:
|
||||
sidebar_position: 9
|
||||
description: nonebot.consts 模块
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
@@ -82,8 +82,8 @@ class Dependent(Generic[R]):
|
||||
"""
|
||||
|
||||
call: _DependentCallable[R]
|
||||
params: Tuple[ModelField] = field(default_factory=tuple)
|
||||
parameterless: Tuple[Param] = field(default_factory=tuple)
|
||||
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):
|
||||
@@ -129,7 +129,8 @@ class Dependent(Generic[R]):
|
||||
break
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unknown parameter {param.name} for function {call} with type {param.annotation}"
|
||||
f"Unknown parameter {param.name} "
|
||||
f"for function {call} with type {param.annotation}"
|
||||
)
|
||||
|
||||
default_value = field_info.default
|
||||
@@ -182,7 +183,7 @@ class Dependent(Generic[R]):
|
||||
|
||||
params = cls.parse_params(call, allow_types)
|
||||
parameterless_params = (
|
||||
tuple()
|
||||
()
|
||||
if parameterless is None
|
||||
else cls.parse_parameterless(tuple(parameterless), allow_types)
|
||||
)
|
||||
|
@@ -3,6 +3,7 @@ FrontMatter:
|
||||
sidebar_position: 1
|
||||
description: nonebot.dependencies.utils 模块
|
||||
"""
|
||||
|
||||
import inspect
|
||||
from typing import Any, Dict, TypeVar, Callable, ForwardRef
|
||||
|
||||
@@ -17,6 +18,7 @@ V = TypeVar("V")
|
||||
|
||||
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
||||
"""获取可调用对象签名"""
|
||||
|
||||
signature = inspect.signature(call)
|
||||
globalns = getattr(call, "__globals__", {})
|
||||
typed_params = [
|
||||
@@ -33,6 +35,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
||||
|
||||
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
|
||||
"""获取参数的类型注解"""
|
||||
|
||||
annotation = param.annotation
|
||||
if isinstance(annotation, str):
|
||||
annotation = ForwardRef(annotation)
|
||||
@@ -47,6 +50,8 @@ def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) ->
|
||||
|
||||
|
||||
def check_field_type(field: ModelField, value: V) -> V:
|
||||
"""检查字段类型是否匹配"""
|
||||
|
||||
_, errs_ = field.validate(value, {}, loc=())
|
||||
if errs_:
|
||||
raise TypeMisMatch(field, value)
|
||||
|
@@ -1,10 +1,11 @@
|
||||
from typing_extensions import TypeAlias
|
||||
from typing import Any, List, Union, Callable, Awaitable, cast
|
||||
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
|
||||
SYNC_LIFESPAN_FUNC = Callable[[], Any]
|
||||
ASYNC_LIFESPAN_FUNC = Callable[[], Awaitable[Any]]
|
||||
LIFESPAN_FUNC = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
|
||||
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
|
||||
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
|
||||
LIFESPAN_FUNC: TypeAlias = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
|
||||
|
||||
|
||||
class Lifespan:
|
||||
|
@@ -15,10 +15,10 @@ FrontMatter:
|
||||
description: nonebot.drivers.aiohttp 模块
|
||||
"""
|
||||
|
||||
from typing_extensions import override
|
||||
from typing import Type, AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.drivers import Request, Response
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
@@ -29,7 +29,8 @@ try:
|
||||
import aiohttp
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install aiohttp first to use this driver. `pip install nonebot2[aiohttp]`"
|
||||
"Please install aiohttp first to use this driver. "
|
||||
"Install with pip: `pip install nonebot2[aiohttp]`"
|
||||
) from e
|
||||
|
||||
|
||||
@@ -37,11 +38,11 @@ class Mixin(ForwardMixin):
|
||||
"""AIOHTTP Mixin"""
|
||||
|
||||
@property
|
||||
@overrides(ForwardMixin)
|
||||
@override
|
||||
def type(self) -> str:
|
||||
return "aiohttp"
|
||||
|
||||
@overrides(ForwardMixin)
|
||||
@override
|
||||
async def request(self, setup: Request) -> Response:
|
||||
if setup.version == HTTPVersion.H10:
|
||||
version = aiohttp.HttpVersion10
|
||||
@@ -51,11 +52,12 @@ class Mixin(ForwardMixin):
|
||||
raise RuntimeError(f"Unsupported HTTP version: {setup.version}")
|
||||
|
||||
timeout = aiohttp.ClientTimeout(setup.timeout)
|
||||
files = None
|
||||
|
||||
data = setup.data
|
||||
if setup.files:
|
||||
files = aiohttp.FormData()
|
||||
data = aiohttp.FormData(data or {})
|
||||
for name, file in setup.files:
|
||||
files.add_field(name, file[1], content_type=file[2], filename=file[0])
|
||||
data.add_field(name, file[1], content_type=file[2], filename=file[0])
|
||||
|
||||
cookies = {
|
||||
cookie.name: cookie.value for cookie in setup.cookies if cookie.value
|
||||
@@ -66,7 +68,7 @@ class Mixin(ForwardMixin):
|
||||
async with session.request(
|
||||
setup.method,
|
||||
setup.url,
|
||||
data=setup.content or setup.data or files,
|
||||
data=setup.content or data,
|
||||
json=setup.json,
|
||||
headers=setup.headers,
|
||||
timeout=timeout,
|
||||
@@ -79,7 +81,7 @@ class Mixin(ForwardMixin):
|
||||
request=setup,
|
||||
)
|
||||
|
||||
@overrides(ForwardMixin)
|
||||
@override
|
||||
@asynccontextmanager
|
||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||
if setup.version == HTTPVersion.H10:
|
||||
@@ -115,15 +117,15 @@ class WebSocket(BaseWebSocket):
|
||||
self.websocket = websocket
|
||||
|
||||
@property
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
def closed(self):
|
||||
return self.websocket.closed
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def accept(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def close(self, code: int = 1000):
|
||||
await self.websocket.close(code=code)
|
||||
await self.session.close()
|
||||
@@ -134,7 +136,7 @@ class WebSocket(BaseWebSocket):
|
||||
raise WebSocketClosed(self.websocket.close_code or 1006)
|
||||
return msg
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def receive(self) -> str:
|
||||
msg = await self._receive()
|
||||
if msg.type not in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY):
|
||||
@@ -143,7 +145,7 @@ class WebSocket(BaseWebSocket):
|
||||
)
|
||||
return msg.data
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def receive_text(self) -> str:
|
||||
msg = await self._receive()
|
||||
if msg.type != aiohttp.WSMsgType.TEXT:
|
||||
@@ -152,7 +154,7 @@ class WebSocket(BaseWebSocket):
|
||||
)
|
||||
return msg.data
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def receive_bytes(self) -> bytes:
|
||||
msg = await self._receive()
|
||||
if msg.type != aiohttp.WSMsgType.BINARY:
|
||||
@@ -161,11 +163,11 @@ class WebSocket(BaseWebSocket):
|
||||
)
|
||||
return msg.data
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def send_text(self, data: str) -> None:
|
||||
await self.websocket.send_str(data)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def send_bytes(self, data: bytes) -> None:
|
||||
await self.websocket.send_bytes(data)
|
||||
|
||||
|
@@ -19,12 +19,12 @@ FrontMatter:
|
||||
import logging
|
||||
import contextlib
|
||||
from functools import wraps
|
||||
from typing_extensions import override
|
||||
from typing import Any, Dict, List, Tuple, Union, Optional
|
||||
|
||||
from pydantic import BaseSettings
|
||||
|
||||
from nonebot.config import Env
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.internal.driver import FileTypes
|
||||
from nonebot.config import Config as NoneBotConfig
|
||||
@@ -41,7 +41,8 @@ try:
|
||||
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install FastAPI by using `pip install nonebot2[fastapi]`"
|
||||
"Please install FastAPI first to use this driver. "
|
||||
"Install with pip: `pip install nonebot2[fastapi]`"
|
||||
) from e
|
||||
|
||||
|
||||
@@ -90,7 +91,7 @@ class Driver(ReverseDriver):
|
||||
"""FastAPI 驱动框架。"""
|
||||
|
||||
def __init__(self, env: Env, config: NoneBotConfig):
|
||||
super(Driver, self).__init__(env, config)
|
||||
super().__init__(env, config)
|
||||
|
||||
self.fastapi_config: Config = Config(**config.dict())
|
||||
|
||||
@@ -105,30 +106,30 @@ class Driver(ReverseDriver):
|
||||
)
|
||||
|
||||
@property
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def type(self) -> str:
|
||||
"""驱动名称: `fastapi`"""
|
||||
return "fastapi"
|
||||
|
||||
@property
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def server_app(self) -> FastAPI:
|
||||
"""`FastAPI APP` 对象"""
|
||||
return self._server_app
|
||||
|
||||
@property
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def asgi(self) -> FastAPI:
|
||||
"""`FastAPI APP` 对象"""
|
||||
return self._server_app
|
||||
|
||||
@property
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def logger(self) -> logging.Logger:
|
||||
"""fastapi 使用的 logger"""
|
||||
return logging.getLogger("fastapi")
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def setup_http_server(self, setup: HTTPServerSetup):
|
||||
async def _handle(request: Request) -> Response:
|
||||
return await self._handle_http(request, setup)
|
||||
@@ -141,7 +142,7 @@ class Driver(ReverseDriver):
|
||||
include_in_schema=self.fastapi_config.fastapi_include_adapter_schema,
|
||||
)
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def setup_websocket_server(self, setup: WebSocketServerSetup) -> None:
|
||||
async def _handle(websocket: WebSocket) -> None:
|
||||
await self._handle_ws(websocket, setup)
|
||||
@@ -152,11 +153,11 @@ class Driver(ReverseDriver):
|
||||
name=setup.name,
|
||||
)
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
return self._lifespan.on_startup(func)
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
return self._lifespan.on_shutdown(func)
|
||||
|
||||
@@ -168,7 +169,7 @@ class Driver(ReverseDriver):
|
||||
finally:
|
||||
await self._lifespan.shutdown()
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def run(
|
||||
self,
|
||||
host: Optional[str] = None,
|
||||
@@ -267,30 +268,30 @@ class Driver(ReverseDriver):
|
||||
class FastAPIWebSocket(BaseWebSocket):
|
||||
"""FastAPI WebSocket Wrapper"""
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
def __init__(self, *, request: BaseRequest, websocket: WebSocket):
|
||||
super().__init__(request=request)
|
||||
self.websocket = websocket
|
||||
|
||||
@property
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
def closed(self) -> bool:
|
||||
return (
|
||||
self.websocket.client_state == WebSocketState.DISCONNECTED
|
||||
or self.websocket.application_state == WebSocketState.DISCONNECTED
|
||||
)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def accept(self) -> None:
|
||||
await self.websocket.accept()
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def close(
|
||||
self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = ""
|
||||
) -> None:
|
||||
await self.websocket.close(code, reason)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def receive(self) -> Union[str, bytes]:
|
||||
# assert self.websocket.application_state == WebSocketState.CONNECTED
|
||||
msg = await self.websocket.receive()
|
||||
@@ -298,21 +299,21 @@ class FastAPIWebSocket(BaseWebSocket):
|
||||
raise WebSocketClosed(msg["code"])
|
||||
return msg["text"] if "text" in msg else msg["bytes"]
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
@catch_closed
|
||||
async def receive_text(self) -> str:
|
||||
return await self.websocket.receive_text()
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
@catch_closed
|
||||
async def receive_bytes(self) -> bytes:
|
||||
return await self.websocket.receive_bytes()
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def send_text(self, data: str) -> None:
|
||||
await self.websocket.send({"type": "websocket.send", "text": data})
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def send_bytes(self, data: bytes) -> None:
|
||||
await self.websocket.send({"type": "websocket.send", "bytes": data})
|
||||
|
||||
|
@@ -14,10 +14,11 @@ FrontMatter:
|
||||
sidebar_position: 3
|
||||
description: nonebot.drivers.httpx 模块
|
||||
"""
|
||||
|
||||
from typing_extensions import override
|
||||
from typing import Type, AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.drivers import (
|
||||
Request,
|
||||
@@ -33,7 +34,8 @@ try:
|
||||
import httpx
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install httpx by using `pip install nonebot2[httpx]`"
|
||||
"Please install httpx first to use this driver. "
|
||||
"Install with pip: `pip install nonebot2[httpx]`"
|
||||
) from e
|
||||
|
||||
|
||||
@@ -41,11 +43,11 @@ class Mixin(ForwardMixin):
|
||||
"""HTTPX Mixin"""
|
||||
|
||||
@property
|
||||
@overrides(ForwardMixin)
|
||||
@override
|
||||
def type(self) -> str:
|
||||
return "httpx"
|
||||
|
||||
@overrides(ForwardMixin)
|
||||
@override
|
||||
async def request(self, setup: Request) -> Response:
|
||||
async with httpx.AsyncClient(
|
||||
cookies=setup.cookies.jar,
|
||||
@@ -70,7 +72,7 @@ class Mixin(ForwardMixin):
|
||||
request=setup,
|
||||
)
|
||||
|
||||
@overrides(ForwardMixin)
|
||||
@override
|
||||
@asynccontextmanager
|
||||
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
|
||||
async with super(Mixin, self).websocket(setup) as ws:
|
||||
|
@@ -9,14 +9,13 @@ FrontMatter:
|
||||
description: nonebot.drivers.none 模块
|
||||
"""
|
||||
|
||||
|
||||
import signal
|
||||
import asyncio
|
||||
import threading
|
||||
from typing_extensions import override
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.consts import WINDOWS
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.drivers import Driver as BaseDriver
|
||||
|
||||
@@ -42,28 +41,28 @@ class Driver(BaseDriver):
|
||||
self.force_exit: bool = False
|
||||
|
||||
@property
|
||||
@overrides(BaseDriver)
|
||||
@override
|
||||
def type(self) -> str:
|
||||
"""驱动名称: `none`"""
|
||||
return "none"
|
||||
|
||||
@property
|
||||
@overrides(BaseDriver)
|
||||
@override
|
||||
def logger(self):
|
||||
"""none driver 使用的 logger"""
|
||||
return logger
|
||||
|
||||
@overrides(BaseDriver)
|
||||
@override
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""注册一个启动时执行的函数"""
|
||||
return self._lifespan.on_startup(func)
|
||||
|
||||
@overrides(BaseDriver)
|
||||
@override
|
||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""注册一个停止时执行的函数"""
|
||||
return self._lifespan.on_shutdown(func)
|
||||
|
||||
@overrides(BaseDriver)
|
||||
@override
|
||||
def run(self, *args, **kwargs):
|
||||
"""启动 none driver"""
|
||||
super().run(*args, **kwargs)
|
||||
|
@@ -17,12 +17,23 @@ FrontMatter:
|
||||
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
from typing import Any, Dict, List, Tuple, Union, TypeVar, Callable, Optional, Coroutine
|
||||
from typing_extensions import override
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Tuple,
|
||||
Union,
|
||||
TypeVar,
|
||||
Callable,
|
||||
Optional,
|
||||
Coroutine,
|
||||
cast,
|
||||
)
|
||||
|
||||
from pydantic import BaseSettings
|
||||
|
||||
from nonebot.config import Env
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.internal.driver import FileTypes
|
||||
from nonebot.config import Config as NoneBotConfig
|
||||
@@ -33,13 +44,15 @@ from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
|
||||
try:
|
||||
import uvicorn
|
||||
from quart import request as _request
|
||||
from quart import websocket as _websocket
|
||||
from quart.ctx import WebsocketContext
|
||||
from quart.globals import websocket_ctx
|
||||
from quart import Quart, Request, Response
|
||||
from quart.datastructures import FileStorage
|
||||
from quart import Websocket as QuartWebSocket
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install Quart by using `pip install nonebot2[quart]`"
|
||||
"Please install Quart first to use this driver. "
|
||||
"Install with pip: `pip install nonebot2[quart]`"
|
||||
) from e
|
||||
|
||||
_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine])
|
||||
@@ -89,30 +102,30 @@ class Driver(ReverseDriver):
|
||||
)
|
||||
|
||||
@property
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def type(self) -> str:
|
||||
"""驱动名称: `quart`"""
|
||||
return "quart"
|
||||
|
||||
@property
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def server_app(self) -> Quart:
|
||||
"""`Quart` 对象"""
|
||||
return self._server_app
|
||||
|
||||
@property
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def asgi(self):
|
||||
"""`Quart` 对象"""
|
||||
return self._server_app
|
||||
|
||||
@property
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def logger(self):
|
||||
"""Quart 使用的 logger"""
|
||||
return self._server_app.logger
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def setup_http_server(self, setup: HTTPServerSetup):
|
||||
async def _handle() -> Response:
|
||||
return await self._handle_http(setup)
|
||||
@@ -124,7 +137,7 @@ class Driver(ReverseDriver):
|
||||
view_func=_handle,
|
||||
)
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def setup_websocket_server(self, setup: WebSocketServerSetup) -> None:
|
||||
async def _handle() -> None:
|
||||
return await self._handle_ws(setup)
|
||||
@@ -135,17 +148,17 @@ class Driver(ReverseDriver):
|
||||
view_func=_handle,
|
||||
)
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def on_startup(self, func: _AsyncCallable) -> _AsyncCallable:
|
||||
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
|
||||
return self.server_app.before_serving(func) # type: ignore
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def on_shutdown(self, func: _AsyncCallable) -> _AsyncCallable:
|
||||
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
|
||||
return self.server_app.after_serving(func) # type: ignore
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
@override
|
||||
def run(
|
||||
self,
|
||||
host: Optional[str] = None,
|
||||
@@ -188,9 +201,7 @@ class Driver(ReverseDriver):
|
||||
async def _handle_http(self, setup: HTTPServerSetup) -> Response:
|
||||
request: Request = _request
|
||||
|
||||
json = None
|
||||
if request.is_json:
|
||||
json = await request.get_json()
|
||||
json = await request.get_json() if request.is_json else None
|
||||
|
||||
data = await request.form
|
||||
files_dict = await request.files
|
||||
@@ -223,7 +234,8 @@ class Driver(ReverseDriver):
|
||||
)
|
||||
|
||||
async def _handle_ws(self, setup: WebSocketServerSetup) -> None:
|
||||
websocket: QuartWebSocket = _websocket
|
||||
ctx = cast(WebsocketContext, websocket_ctx.copy())
|
||||
websocket = websocket_ctx.websocket
|
||||
|
||||
http_request = BaseRequest(
|
||||
websocket.method,
|
||||
@@ -233,7 +245,7 @@ class Driver(ReverseDriver):
|
||||
version=websocket.http_version,
|
||||
)
|
||||
|
||||
ws = WebSocket(request=http_request, websocket=websocket)
|
||||
ws = WebSocket(request=http_request, websocket_ctx=ctx)
|
||||
|
||||
await setup.handle_func(ws)
|
||||
|
||||
@@ -241,30 +253,34 @@ class Driver(ReverseDriver):
|
||||
class WebSocket(BaseWebSocket):
|
||||
"""Quart WebSocket Wrapper"""
|
||||
|
||||
def __init__(self, *, request: BaseRequest, websocket: QuartWebSocket):
|
||||
def __init__(self, *, request: BaseRequest, websocket_ctx: WebsocketContext):
|
||||
super().__init__(request=request)
|
||||
self.websocket = websocket
|
||||
self.websocket_ctx = websocket_ctx
|
||||
|
||||
@property
|
||||
@overrides(BaseWebSocket)
|
||||
def websocket(self) -> QuartWebSocket:
|
||||
return self.websocket_ctx.websocket
|
||||
|
||||
@property
|
||||
@override
|
||||
def closed(self):
|
||||
# FIXME
|
||||
return True
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def accept(self):
|
||||
await self.websocket.accept()
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def close(self, code: int = 1000, reason: str = ""):
|
||||
await self.websocket.close(code, reason)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
@catch_closed
|
||||
async def receive(self) -> Union[str, bytes]:
|
||||
return await self.websocket.receive()
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
@catch_closed
|
||||
async def receive_text(self) -> str:
|
||||
msg = await self.websocket.receive()
|
||||
@@ -272,7 +288,7 @@ class WebSocket(BaseWebSocket):
|
||||
raise TypeError("WebSocket received unexpected frame type: bytes")
|
||||
return msg
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
@catch_closed
|
||||
async def receive_bytes(self) -> bytes:
|
||||
msg = await self.websocket.receive()
|
||||
@@ -280,11 +296,11 @@ class WebSocket(BaseWebSocket):
|
||||
raise TypeError("WebSocket received unexpected frame type: str")
|
||||
return msg
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def send_text(self, data: str):
|
||||
await self.websocket.send(data)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def send_bytes(self, data: bytes):
|
||||
await self.websocket.send(data)
|
||||
|
||||
|
@@ -14,12 +14,13 @@ FrontMatter:
|
||||
sidebar_position: 4
|
||||
description: nonebot.drivers.websockets 模块
|
||||
"""
|
||||
|
||||
import logging
|
||||
from functools import wraps
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Type, Union, AsyncGenerator
|
||||
from typing_extensions import ParamSpec, override
|
||||
from typing import Type, Union, TypeVar, Callable, Awaitable, AsyncGenerator
|
||||
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.log import LoguruHandler
|
||||
from nonebot.drivers import Request, Response
|
||||
from nonebot.exception import WebSocketClosed
|
||||
@@ -32,16 +33,20 @@ try:
|
||||
from websockets.legacy.client import Connect, WebSocketClientProtocol
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install websockets by using `pip install nonebot2[websockets]`"
|
||||
"Please install websockets first to use this driver. "
|
||||
"Install with pip: `pip install nonebot2[websockets]`"
|
||||
) from e
|
||||
|
||||
T = TypeVar("T")
|
||||
P = ParamSpec("P")
|
||||
|
||||
logger = logging.Logger("websockets.client", "INFO")
|
||||
logger.addHandler(LoguruHandler())
|
||||
|
||||
|
||||
def catch_closed(func):
|
||||
def catch_closed(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
||||
@wraps(func)
|
||||
async def decorator(*args, **kwargs):
|
||||
async def decorator(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except ConnectionClosed as e:
|
||||
@@ -57,15 +62,15 @@ class Mixin(ForwardMixin):
|
||||
"""Websockets Mixin"""
|
||||
|
||||
@property
|
||||
@overrides(ForwardMixin)
|
||||
@override
|
||||
def type(self) -> str:
|
||||
return "websockets"
|
||||
|
||||
@overrides(ForwardMixin)
|
||||
@override
|
||||
async def request(self, setup: Request) -> Response:
|
||||
return await super(Mixin, self).request(setup)
|
||||
|
||||
@overrides(ForwardMixin)
|
||||
@override
|
||||
@asynccontextmanager
|
||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||
connection = Connect(
|
||||
@@ -80,30 +85,30 @@ class Mixin(ForwardMixin):
|
||||
class WebSocket(BaseWebSocket):
|
||||
"""Websockets WebSocket Wrapper"""
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
def __init__(self, *, request: Request, websocket: WebSocketClientProtocol):
|
||||
super().__init__(request=request)
|
||||
self.websocket = websocket
|
||||
|
||||
@property
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
def closed(self) -> bool:
|
||||
return self.websocket.closed
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def accept(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def close(self, code: int = 1000, reason: str = ""):
|
||||
await self.websocket.close(code, reason)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
@catch_closed
|
||||
async def receive(self) -> Union[str, bytes]:
|
||||
return await self.websocket.recv()
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
@catch_closed
|
||||
async def receive_text(self) -> str:
|
||||
msg = await self.websocket.recv()
|
||||
@@ -111,7 +116,7 @@ class WebSocket(BaseWebSocket):
|
||||
raise TypeError("WebSocket received unexpected frame type: bytes")
|
||||
return msg
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
@catch_closed
|
||||
async def receive_bytes(self) -> bytes:
|
||||
msg = await self.websocket.recv()
|
||||
@@ -119,11 +124,11 @@ class WebSocket(BaseWebSocket):
|
||||
raise TypeError("WebSocket received unexpected frame type: str")
|
||||
return msg
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def send_text(self, data: str) -> None:
|
||||
await self.websocket.send(data)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@override
|
||||
async def send_bytes(self, data: bytes) -> None:
|
||||
await self.websocket.send(data)
|
||||
|
||||
|
@@ -43,9 +43,9 @@ class NoneBotException(Exception):
|
||||
|
||||
# Rule Exception
|
||||
class ParserExit(NoneBotException):
|
||||
"""{ref}`nonebot.rule.shell_command` 处理消息失败时返回的异常"""
|
||||
"""{ref}`nonebot.rule.shell_command` 处理消息失败时返回的异常。"""
|
||||
|
||||
def __init__(self, status: int = 0, message: Optional[str] = None):
|
||||
def __init__(self, status: int = 0, message: Optional[str] = None) -> None:
|
||||
self.status = status
|
||||
self.message = message
|
||||
|
||||
@@ -69,7 +69,7 @@ class IgnoredException(ProcessException):
|
||||
reason: 忽略事件的原因
|
||||
"""
|
||||
|
||||
def __init__(self, reason: Any):
|
||||
def __init__(self, reason: Any) -> None:
|
||||
self.reason: Any = reason
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -96,7 +96,7 @@ class SkippedException(ProcessException):
|
||||
class TypeMisMatch(SkippedException):
|
||||
"""当前 `Handler` 的参数类型不匹配。"""
|
||||
|
||||
def __init__(self, param: ModelField, value: Any):
|
||||
def __init__(self, param: ModelField, value: Any) -> None:
|
||||
self.param: ModelField = param
|
||||
self.value: Any = value
|
||||
|
||||
@@ -108,7 +108,8 @@ class TypeMisMatch(SkippedException):
|
||||
|
||||
|
||||
class MockApiException(ProcessException):
|
||||
"""指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。可由 api hook 抛出。
|
||||
"""指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。
|
||||
可由 api hook 抛出。
|
||||
|
||||
参数:
|
||||
result: 返回的内容
|
||||
@@ -144,7 +145,8 @@ class MatcherException(NoneBotException):
|
||||
|
||||
|
||||
class PausedException(MatcherException):
|
||||
"""指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。可用于用户输入新信息。
|
||||
"""指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。
|
||||
可用于用户输入新信息。
|
||||
|
||||
可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.pause` 抛出。
|
||||
|
||||
@@ -158,7 +160,8 @@ class PausedException(MatcherException):
|
||||
|
||||
|
||||
class RejectedException(MatcherException):
|
||||
"""指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。可用于用户重新输入。
|
||||
"""指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。
|
||||
可用于用户重新输入。
|
||||
|
||||
可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.reject` 抛出。
|
||||
|
||||
@@ -187,7 +190,7 @@ class FinishedException(MatcherException):
|
||||
|
||||
# Adapter Exceptions
|
||||
class AdapterException(NoneBotException):
|
||||
"""代表 `Adapter` 抛出的异常,所有的 `Adapter` 都要在内部继承自这个 `Exception`
|
||||
"""代表 `Adapter` 抛出的异常,所有的 `Adapter` 都要在内部继承自这个 `Exception`。
|
||||
|
||||
参数:
|
||||
adapter_name: 标识 adapter
|
||||
@@ -210,7 +213,9 @@ class ApiNotAvailable(AdapterException):
|
||||
|
||||
|
||||
class NetworkError(AdapterException):
|
||||
"""在网络出现问题时抛出,如: API 请求地址不正确, API 请求无返回或返回状态非正常等。"""
|
||||
"""在网络出现问题时抛出,
|
||||
如: API 请求地址不正确, API 请求无返回或返回状态非正常等。
|
||||
"""
|
||||
|
||||
|
||||
class ActionFailed(AdapterException):
|
||||
@@ -219,13 +224,13 @@ class ActionFailed(AdapterException):
|
||||
|
||||
# Driver Exceptions
|
||||
class DriverException(NoneBotException):
|
||||
"""`Driver` 抛出的异常基类"""
|
||||
"""`Driver` 抛出的异常基类。"""
|
||||
|
||||
|
||||
class WebSocketClosed(DriverException):
|
||||
"""WebSocket 连接已关闭"""
|
||||
"""WebSocket 连接已关闭。"""
|
||||
|
||||
def __init__(self, code: int, reason: Optional[str] = None):
|
||||
def __init__(self, code: int, reason: Optional[str] = None) -> None:
|
||||
self.code = code
|
||||
self.reason = reason
|
||||
|
||||
|
@@ -44,10 +44,11 @@ class Event(abc.ABC, BaseModel):
|
||||
def get_log_string(self) -> str:
|
||||
"""获取事件日志信息的方法。
|
||||
|
||||
通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,可以抛出 `NoLogException` 异常。
|
||||
通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,
|
||||
可以抛出 `NoLogException` 异常。
|
||||
|
||||
异常:
|
||||
NoLogException:
|
||||
NoLogException: 希望 NoneBot 隐藏该事件日志
|
||||
"""
|
||||
return f"[{self.get_event_name()}]: {self.get_event_description()}"
|
||||
|
||||
@@ -58,7 +59,9 @@ class Event(abc.ABC, BaseModel):
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_session_id(self) -> str:
|
||||
"""获取会话 id 的方法,用于判断当前事件属于哪一个会话,通常是用户 id、群组 id 组合。"""
|
||||
"""获取会话 id 的方法,用于判断当前事件属于哪一个会话,
|
||||
通常是用户 id、群组 id 组合。
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
|
@@ -98,7 +98,7 @@ class MessageSegment(abc.ABC, Generic[TM]):
|
||||
|
||||
|
||||
class Message(List[TMS], abc.ABC):
|
||||
"""消息数组
|
||||
"""消息序列
|
||||
|
||||
参数:
|
||||
message: 消息内容
|
||||
@@ -124,9 +124,9 @@ class Message(List[TMS], abc.ABC):
|
||||
def template(cls, format_string: Union[str, TM]) -> MessageTemplate[Self]:
|
||||
"""创建消息模板。
|
||||
|
||||
用法和 `str.format` 大致相同, 但是可以输出消息对象, 并且支持以 `Message` 对象作为消息模板
|
||||
|
||||
并且提供了拓展的格式化控制符, 可以用适用于该消息类型的 `MessageSegment` 的工厂方法创建消息
|
||||
用法和 `str.format` 大致相同,支持以 `Message` 对象作为消息模板并输出消息对象。
|
||||
并且提供了拓展的格式化控制符,
|
||||
可以通过该消息类型的 `MessageSegment` 工厂方法创建消息。
|
||||
|
||||
参数:
|
||||
format_string: 格式化模板
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import functools
|
||||
from string import Formatter
|
||||
from typing_extensions import TypeAlias
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@@ -25,7 +26,7 @@ if TYPE_CHECKING:
|
||||
TM = TypeVar("TM", bound="Message")
|
||||
TF = TypeVar("TF", str, "Message")
|
||||
|
||||
FormatSpecFunc = Callable[[Any], str]
|
||||
FormatSpecFunc: TypeAlias = Callable[[Any], str]
|
||||
FormatSpecFunc_T = TypeVar("FormatSpecFunc_T", bound=FormatSpecFunc)
|
||||
|
||||
|
||||
|
@@ -89,9 +89,7 @@ class Driver(abc.ABC):
|
||||
|
||||
@abc.abstractmethod
|
||||
def run(self, *args, **kwargs):
|
||||
"""
|
||||
启动驱动框架
|
||||
"""
|
||||
"""启动驱动框架"""
|
||||
logger.opt(colors=True).debug(
|
||||
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
|
||||
)
|
||||
@@ -152,8 +150,10 @@ class Driver(abc.ABC):
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running WebSocketConnection hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
"<r><bg #f8bbd0>"
|
||||
"Error when running WebSocketConnection hook. "
|
||||
"Running cancelled!"
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
asyncio.create_task(_run_hook(bot))
|
||||
@@ -177,8 +177,10 @@ class Driver(abc.ABC):
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
"<r><bg #f8bbd0>"
|
||||
"Error when running WebSocketDisConnection hook. "
|
||||
"Running cancelled!"
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
asyncio.create_task(_run_hook(bot))
|
||||
@@ -241,7 +243,7 @@ def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Dr
|
||||
# check first
|
||||
assert issubclass(driver, Driver), "`driver` must be subclass of Driver"
|
||||
assert all(
|
||||
map(lambda m: issubclass(m, ForwardMixin), mixins)
|
||||
issubclass(m, ForwardMixin) for m in mixins
|
||||
), "`mixins` must be subclass of ForwardMixin"
|
||||
|
||||
if not mixins:
|
||||
@@ -251,7 +253,9 @@ def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Dr
|
||||
return (
|
||||
driver.type.__get__(self)
|
||||
+ "+"
|
||||
+ "+".join(map(lambda x: x.type.__get__(self), mixins))
|
||||
+ "+".join(x.type.__get__(self) for x in mixins)
|
||||
)
|
||||
|
||||
return type("CombinedDriver", (*mixins, driver, ForwardDriver), {"type": property(type_)}) # type: ignore
|
||||
return type(
|
||||
"CombinedDriver", (*mixins, driver, ForwardDriver), {"type": property(type_)}
|
||||
) # type: ignore
|
||||
|
@@ -2,6 +2,7 @@ import abc
|
||||
import urllib.request
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from typing_extensions import TypeAlias
|
||||
from http.cookiejar import Cookie, CookieJar
|
||||
from typing import (
|
||||
IO,
|
||||
@@ -21,28 +22,30 @@ from typing import (
|
||||
from yarl import URL as URL
|
||||
from multidict import CIMultiDict
|
||||
|
||||
RawURL = Tuple[bytes, bytes, Optional[int], bytes]
|
||||
RawURL: TypeAlias = Tuple[bytes, bytes, Optional[int], bytes]
|
||||
|
||||
SimpleQuery = Union[str, int, float]
|
||||
QueryVariable = Union[SimpleQuery, List[SimpleQuery]]
|
||||
QueryTypes = Union[
|
||||
SimpleQuery: TypeAlias = Union[str, int, float]
|
||||
QueryVariable: TypeAlias = Union[SimpleQuery, List[SimpleQuery]]
|
||||
QueryTypes: TypeAlias = Union[
|
||||
None, str, Mapping[str, QueryVariable], List[Tuple[str, QueryVariable]]
|
||||
]
|
||||
|
||||
HeaderTypes = Union[
|
||||
HeaderTypes: TypeAlias = Union[
|
||||
None,
|
||||
CIMultiDict[str],
|
||||
Dict[str, str],
|
||||
List[Tuple[str, str]],
|
||||
]
|
||||
|
||||
CookieTypes = Union[None, "Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]]
|
||||
CookieTypes: TypeAlias = Union[
|
||||
None, "Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]
|
||||
]
|
||||
|
||||
ContentTypes = Union[str, bytes, None]
|
||||
DataTypes = Union[dict, None]
|
||||
FileContent = Union[IO[bytes], bytes]
|
||||
FileType = Tuple[Optional[str], FileContent, Optional[str]]
|
||||
FileTypes = Union[
|
||||
ContentTypes: TypeAlias = Union[str, bytes, None]
|
||||
DataTypes: TypeAlias = Union[dict, None]
|
||||
FileContent: TypeAlias = Union[IO[bytes], bytes]
|
||||
FileType: TypeAlias = Tuple[Optional[str], FileContent, Optional[str]]
|
||||
FileTypes: TypeAlias = Union[
|
||||
# file (or bytes)
|
||||
FileContent,
|
||||
# (filename, file (or bytes))
|
||||
@@ -50,7 +53,7 @@ FileTypes = Union[
|
||||
# (filename, file (or bytes), content_type)
|
||||
FileType,
|
||||
]
|
||||
FilesTypes = Union[Dict[str, FileTypes], List[Tuple[str, FileTypes]], None]
|
||||
FilesTypes: TypeAlias = Union[Dict[str, FileTypes], List[Tuple[str, FileTypes]], None]
|
||||
|
||||
|
||||
class HTTPVersion(Enum):
|
||||
@@ -160,7 +163,6 @@ class Response:
|
||||
|
||||
class WebSocket(abc.ABC):
|
||||
def __init__(self, *, request: Request):
|
||||
# request
|
||||
self.request: Request = request
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -169,9 +171,7 @@ class WebSocket(abc.ABC):
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def closed(self) -> bool:
|
||||
"""
|
||||
连接是否已经关闭
|
||||
"""
|
||||
"""连接是否已经关闭"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
|
@@ -29,6 +29,13 @@ from nonebot.internal.adapter import (
|
||||
MessageSegment,
|
||||
MessageTemplate,
|
||||
)
|
||||
from nonebot.typing import (
|
||||
T_State,
|
||||
T_Handler,
|
||||
T_TypeUpdater,
|
||||
T_DependencyCache,
|
||||
T_PermissionUpdater,
|
||||
)
|
||||
from nonebot.consts import (
|
||||
ARG_KEY,
|
||||
RECEIVE_KEY,
|
||||
@@ -36,14 +43,6 @@ from nonebot.consts import (
|
||||
LAST_RECEIVE_KEY,
|
||||
REJECT_CACHE_TARGET,
|
||||
)
|
||||
from nonebot.typing import (
|
||||
Any,
|
||||
T_State,
|
||||
T_Handler,
|
||||
T_TypeUpdater,
|
||||
T_DependencyCache,
|
||||
T_PermissionUpdater,
|
||||
)
|
||||
from nonebot.exception import (
|
||||
PausedException,
|
||||
StopPropagation,
|
||||
@@ -376,7 +375,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
return
|
||||
await matcher.reject()
|
||||
|
||||
_parameterless = (Depends(_receive), *(parameterless or tuple()))
|
||||
_parameterless = (Depends(_receive), *(parameterless or ()))
|
||||
|
||||
def _decorator(func: T_Handler) -> T_Handler:
|
||||
if cls.handlers and cls.handlers[-1].call is func:
|
||||
@@ -406,7 +405,8 @@ class Matcher(metaclass=MatcherMeta):
|
||||
) -> Callable[[T_Handler], T_Handler]:
|
||||
"""装饰一个函数来指示 NoneBot 获取一个参数 `key`
|
||||
|
||||
当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数,如果 `key` 已存在则直接继续运行
|
||||
当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数,
|
||||
如果 `key` 已存在则直接继续运行
|
||||
|
||||
参数:
|
||||
key: 参数名
|
||||
@@ -423,7 +423,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
return
|
||||
await matcher.reject(prompt)
|
||||
|
||||
_parameterless = (Depends(_key_getter), *(parameterless or tuple()))
|
||||
_parameterless = (Depends(_key_getter), *(parameterless or ()))
|
||||
|
||||
def _decorator(func: T_Handler) -> T_Handler:
|
||||
if cls.handlers and cls.handlers[-1].call is func:
|
||||
@@ -454,7 +454,8 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
参数:
|
||||
message: 消息内容
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,请参考对应 adapter 的 bot 对象 api
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||
请参考对应 adapter 的 bot 对象 api
|
||||
"""
|
||||
bot = current_bot.get()
|
||||
event = current_event.get()
|
||||
@@ -475,7 +476,8 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
参数:
|
||||
message: 消息内容
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,请参考对应 adapter 的 bot 对象 api
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||
请参考对应 adapter 的 bot 对象 api
|
||||
"""
|
||||
if message is not None:
|
||||
await cls.send(message, **kwargs)
|
||||
@@ -491,7 +493,8 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
参数:
|
||||
prompt: 消息内容
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,请参考对应 adapter 的 bot 对象 api
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||
请参考对应 adapter 的 bot 对象 api
|
||||
"""
|
||||
if prompt is not None:
|
||||
await cls.send(prompt, **kwargs)
|
||||
@@ -508,7 +511,8 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
参数:
|
||||
prompt: 消息内容
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,请参考对应 adapter 的 bot 对象 api
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||
请参考对应 adapter 的 bot 对象 api
|
||||
"""
|
||||
if prompt is not None:
|
||||
await cls.send(prompt, **kwargs)
|
||||
@@ -527,7 +531,8 @@ class Matcher(metaclass=MatcherMeta):
|
||||
参数:
|
||||
key: 参数名
|
||||
prompt: 消息内容
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,请参考对应 adapter 的 bot 对象 api
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||
请参考对应 adapter 的 bot 对象 api
|
||||
"""
|
||||
matcher = current_matcher.get()
|
||||
matcher.set_target(ARG_KEY.format(key=key))
|
||||
@@ -548,7 +553,8 @@ class Matcher(metaclass=MatcherMeta):
|
||||
参数:
|
||||
id: 消息 id
|
||||
prompt: 消息内容
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,请参考对应 adapter 的 bot 对象 api
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||
请参考对应 adapter 的 bot 对象 api
|
||||
"""
|
||||
matcher = current_matcher.get()
|
||||
matcher.set_target(RECEIVE_KEY.format(id=id))
|
||||
|
@@ -63,7 +63,10 @@ def Depends(
|
||||
finally:
|
||||
...
|
||||
|
||||
async def handler(param_name: Any = Depends(depend_func), gen: Any = Depends(depend_gen_func)):
|
||||
async def handler(
|
||||
param_name: Any = Depends(depend_func),
|
||||
gen: Any = Depends(depend_gen_func),
|
||||
):
|
||||
...
|
||||
```
|
||||
"""
|
||||
@@ -317,7 +320,17 @@ class MatcherParam(Param):
|
||||
|
||||
# param type is Matcher(s) or subclass(es) of Matcher or None
|
||||
if generic_check_issubclass(param.annotation, Matcher):
|
||||
return cls(Required)
|
||||
checker: Optional[ModelField] = None
|
||||
if param.annotation is not Matcher:
|
||||
checker = ModelField(
|
||||
name=param.name,
|
||||
type_=param.annotation,
|
||||
class_validators=None,
|
||||
model_config=CustomConfig,
|
||||
default=None,
|
||||
required=True,
|
||||
)
|
||||
return cls(Required, checker=checker)
|
||||
# legacy: param is named "matcher" and has no type annotation
|
||||
elif param.annotation == param.empty and param.name == "matcher":
|
||||
return cls(Required)
|
||||
@@ -325,6 +338,10 @@ class MatcherParam(Param):
|
||||
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||
return matcher
|
||||
|
||||
async def _check(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||
if checker := self.extra.get("checker", None):
|
||||
check_field_type(checker, matcher)
|
||||
|
||||
|
||||
class ArgInner:
|
||||
def __init__(
|
||||
@@ -372,6 +389,10 @@ class ArgParam(Param):
|
||||
return cls(
|
||||
Required, key=param.default.key or param.name, type=param.default.type
|
||||
)
|
||||
elif get_origin(param.annotation) is Annotated:
|
||||
for arg in get_args(param.annotation):
|
||||
if isinstance(arg, ArgInner):
|
||||
return cls(Required, key=arg.key or param.name, type=arg.type)
|
||||
|
||||
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||
key: str = self.extra["key"]
|
||||
|
@@ -54,7 +54,7 @@ class LoguruHandler(logging.Handler): # pragma: no cover
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
|
||||
frame, depth = logging.currentframe(), 2
|
||||
frame, depth = sys._getframe(6), 6
|
||||
while frame and frame.f_code.co_filename == logging.__file__:
|
||||
frame = frame.f_back
|
||||
depth += 1
|
||||
@@ -88,5 +88,6 @@ logger_id = logger.add(
|
||||
filter=default_filter,
|
||||
format=default_format,
|
||||
)
|
||||
"""默认日志处理器 id"""
|
||||
|
||||
__autodoc__ = {"logger_id": False}
|
||||
|
@@ -166,7 +166,7 @@ async def _apply_event_preprocessors(
|
||||
for proc in _event_preprocessors
|
||||
)
|
||||
)
|
||||
except IgnoredException as e:
|
||||
except IgnoredException:
|
||||
logger.opt(colors=True).info(
|
||||
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
|
||||
)
|
||||
@@ -293,7 +293,7 @@ async def _apply_run_postprocessors(
|
||||
) -> None:
|
||||
"""运行事件响应器运行后处理。
|
||||
|
||||
Args:
|
||||
参数:
|
||||
bot: Bot 对象
|
||||
event: Event 对象
|
||||
matcher: 事件响应器
|
||||
|
@@ -1,7 +1,8 @@
|
||||
"""本模块是 {ref}`nonebot.matcher.Matcher.permission` 的类型定义。
|
||||
|
||||
每个 {ref}`nonebot.matcher.Matcher` 拥有一个 {ref}`nonebot.permission.Permission` ,
|
||||
其中是 `PermissionChecker` 的集合,只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。
|
||||
每个{ref}`事件响应器 <nonebot.matcher.Matcher>`
|
||||
拥有一个 {ref}`nonebot.permission.Permission`,其中是 `PermissionChecker` 的集合。
|
||||
只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 6
|
||||
|
@@ -24,8 +24,10 @@
|
||||
- `load_all_plugins` => {ref}``load_all_plugins` <nonebot.plugin.load.load_all_plugins>`
|
||||
- `load_from_json` => {ref}``load_from_json` <nonebot.plugin.load.load_from_json>`
|
||||
- `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>`
|
||||
- `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>`
|
||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.plugin.PluginMetadata>`
|
||||
|
||||
@@ -42,7 +44,7 @@ from typing import Set, Dict, List, Tuple, Optional
|
||||
_plugins: Dict[str, "Plugin"] = {}
|
||||
_managers: List["PluginManager"] = []
|
||||
_current_plugin_chain: ContextVar[Tuple["Plugin", ...]] = ContextVar(
|
||||
"_current_plugin_chain", default=tuple()
|
||||
"_current_plugin_chain", default=()
|
||||
)
|
||||
|
||||
|
||||
@@ -132,3 +134,4 @@ from .plugin import PluginMetadata as PluginMetadata
|
||||
from .load import load_all_plugins as load_all_plugins
|
||||
from .load import load_builtin_plugin as load_builtin_plugin
|
||||
from .load import load_builtin_plugins as load_builtin_plugins
|
||||
from .load import inherit_supported_adapters as inherit_supported_adapters
|
||||
|
@@ -4,6 +4,7 @@ FrontMatter:
|
||||
sidebar_position: 1
|
||||
description: nonebot.plugin.load 模块
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
@@ -25,7 +26,8 @@ def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
|
||||
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
|
||||
|
||||
参数:
|
||||
module_path: 插件名称 `path.to.your.plugin` 或插件路径 `pathlib.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)
|
||||
@@ -63,7 +65,8 @@ def load_all_plugins(
|
||||
|
||||
|
||||
def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
||||
"""导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件,以 `_` 开头的插件不会被导入!
|
||||
"""导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件。
|
||||
以 `_` 开头的插件不会被导入!
|
||||
|
||||
参数:
|
||||
file_path: 指定 json 文件路径
|
||||
@@ -81,7 +84,7 @@ def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
||||
nonebot.load_from_json("plugins.json")
|
||||
```
|
||||
"""
|
||||
with open(file_path, "r", encoding=encoding) as f:
|
||||
with open(file_path, encoding=encoding) as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
raise TypeError("json file must contains a dict!")
|
||||
@@ -93,7 +96,9 @@ def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
||||
|
||||
|
||||
def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
||||
"""导入指定 toml 文件 `[tool.nonebot]` 中的 `plugins` 以及 `plugin_dirs` 下多个插件,以 `_` 开头的插件不会被导入!
|
||||
"""导入指定 toml 文件 `[tool.nonebot]` 中的
|
||||
`plugins` 以及 `plugin_dirs` 下多个插件。
|
||||
以 `_` 开头的插件不会被导入!
|
||||
|
||||
参数:
|
||||
file_path: 指定 toml 文件路径
|
||||
@@ -110,7 +115,7 @@ def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
||||
nonebot.load_from_toml("pyproject.toml")
|
||||
```
|
||||
"""
|
||||
with open(file_path, "r", encoding=encoding) as f:
|
||||
with open(file_path, encoding=encoding) as f:
|
||||
data = tomllib.loads(f.read())
|
||||
|
||||
nonebot_data = data.get("tool", {}).get("nonebot")
|
||||
@@ -177,3 +182,39 @@ def require(name: str) -> ModuleType:
|
||||
if not plugin:
|
||||
raise RuntimeError(f'Cannot load plugin "{name}"!')
|
||||
return plugin.module
|
||||
|
||||
|
||||
def inherit_supported_adapters(*names: str) -> Optional[Set[str]]:
|
||||
"""获取已加载插件的适配器支持状态集合。
|
||||
|
||||
如果传入了多个插件名称,返回值会自动取交集。
|
||||
|
||||
参数:
|
||||
names: 插件名称列表。
|
||||
|
||||
异常:
|
||||
RuntimeError: 插件未加载
|
||||
ValueError: 插件缺少元数据
|
||||
"""
|
||||
final_supported: Optional[Set[str]] = None
|
||||
|
||||
for name in names:
|
||||
plugin = get_plugin(_module_name_to_plugin_name(name))
|
||||
if plugin is None:
|
||||
raise RuntimeError(f'Plugin "{name}" is not loaded!')
|
||||
meta = plugin.metadata
|
||||
if meta is None:
|
||||
raise ValueError(f'Plugin "{name}" has no metadata!')
|
||||
support = meta.supported_adapters
|
||||
if support is None:
|
||||
continue
|
||||
final_supported = (
|
||||
support if final_supported is None else (final_supported & support)
|
||||
)
|
||||
|
||||
return final_supported and {
|
||||
f"nonebot.adapters.{adapter_name[1:]}"
|
||||
if adapter_name.startswith("~")
|
||||
else adapter_name
|
||||
for adapter_name in final_supported
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ FrontMatter:
|
||||
sidebar_position: 5
|
||||
description: nonebot.plugin.manager 模块
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pkgutil
|
||||
import importlib
|
||||
|
@@ -4,6 +4,7 @@ FrontMatter:
|
||||
sidebar_position: 2
|
||||
description: nonebot.plugin.on 模块
|
||||
"""
|
||||
|
||||
import re
|
||||
import inspect
|
||||
from types import ModuleType
|
||||
@@ -322,7 +323,8 @@ def on_shell_command(
|
||||
|
||||
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
|
||||
|
||||
并将用户输入的原始参数列表保存在 `state["argv"]`, `parser` 处理的参数保存在 `state["args"]` 中
|
||||
可以通过 {ref}`nonebot.params.ShellCommandArgv` 获取原始参数列表,
|
||||
通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典。
|
||||
|
||||
参数:
|
||||
cmd: 指定命令内容
|
||||
@@ -427,6 +429,7 @@ class CommandGroup(_Group):
|
||||
|
||||
参数:
|
||||
cmd: 指定命令内容
|
||||
prefix_aliases: 是否影响命令别名,给命令别名加前缀
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
@@ -437,11 +440,14 @@ class CommandGroup(_Group):
|
||||
state: 默认 state
|
||||
"""
|
||||
|
||||
def __init__(self, cmd: Union[str, Tuple[str, ...]], **kwargs):
|
||||
def __init__(
|
||||
self, cmd: Union[str, Tuple[str, ...]], prefix_aliases: bool = False, **kwargs
|
||||
):
|
||||
"""命令前缀"""
|
||||
super().__init__(**kwargs)
|
||||
self.basecmd: Tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd
|
||||
self.base_kwargs.pop("aliases", None)
|
||||
self.prefix_aliases = prefix_aliases
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})"
|
||||
@@ -464,6 +470,11 @@ class CommandGroup(_Group):
|
||||
"""
|
||||
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
|
||||
cmd = self.basecmd + sub_cmd
|
||||
if self.prefix_aliases and (aliases := kwargs.get("aliases")):
|
||||
kwargs["aliases"] = {
|
||||
self.basecmd + ((alias,) if isinstance(alias, str) else alias)
|
||||
for alias in aliases
|
||||
}
|
||||
matcher = on_command(cmd, **self._get_final_kwargs(kwargs))
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
@@ -488,6 +499,11 @@ class CommandGroup(_Group):
|
||||
"""
|
||||
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
|
||||
cmd = self.basecmd + sub_cmd
|
||||
if self.prefix_aliases and (aliases := kwargs.get("aliases")):
|
||||
kwargs["aliases"] = {
|
||||
self.basecmd + ((alias,) if isinstance(alias, str) else alias)
|
||||
for alias in aliases
|
||||
}
|
||||
matcher = on_shell_command(cmd, **self._get_final_kwargs(kwargs))
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
@@ -712,7 +728,8 @@ class MatcherGroup(_Group):
|
||||
|
||||
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
|
||||
|
||||
并将用户输入的原始参数列表保存在 `state["argv"]`, `parser` 处理的参数保存在 `state["args"]` 中
|
||||
可以通过 {ref}`nonebot.params.ShellCommandArgv` 获取原始参数列表,
|
||||
通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典。
|
||||
|
||||
参数:
|
||||
cmd: 指定命令内容
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import re
|
||||
from typing import Any
|
||||
from types import ModuleType
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Set, List, Type, Tuple, Union, Optional
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.matcher import Matcher
|
||||
@@ -12,399 +12,409 @@ from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecke
|
||||
|
||||
from .plugin import Plugin
|
||||
|
||||
def store_matcher(matcher: Type[Matcher]) -> None: ...
|
||||
def get_matcher_plugin(depth: int = ...) -> Optional[Plugin]: ...
|
||||
def get_matcher_module(depth: int = ...) -> Optional[ModuleType]: ...
|
||||
def store_matcher(matcher: type[Matcher]) -> None: ...
|
||||
def get_matcher_plugin(depth: int = ...) -> Plugin | None: ...
|
||||
def get_matcher_module(depth: int = ...) -> ModuleType | None: ...
|
||||
def on(
|
||||
type: str = "",
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_metaevent(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_message(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_notice(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_request(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_startswith(
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
msg: str | tuple[str, ...],
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
ignorecase: bool = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_endswith(
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
msg: str | tuple[str, ...],
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
ignorecase: bool = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_fullmatch(
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
msg: str | tuple[str, ...],
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
ignorecase: bool = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_keyword(
|
||||
keywords: Set[str],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
keywords: set[str],
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_command(
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
force_whitespace: Optional[Union[str, bool]] = ...,
|
||||
cmd: str | tuple[str, ...],
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
aliases: set[str | tuple[str, ...]] | None = ...,
|
||||
force_whitespace: str | bool | None = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_shell_command(
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
parser: Optional[ArgumentParser] = ...,
|
||||
cmd: str | tuple[str, ...],
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
aliases: set[str | tuple[str, ...]] | None = ...,
|
||||
parser: ArgumentParser | None = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_regex(
|
||||
pattern: str,
|
||||
flags: Union[int, re.RegexFlag] = ...,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
flags: int | re.RegexFlag = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_type(
|
||||
types: Union[Type[Event], Tuple[Type[Event], ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
types: type[Event] | tuple[type[Event], ...],
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
|
||||
class CommandGroup:
|
||||
class _Group:
|
||||
matchers: list[type[Matcher]] = ...
|
||||
base_kwargs: dict[str, Any] = ...
|
||||
def _get_final_kwargs(
|
||||
self, update: dict[str, Any], *, exclude: set[str] | None = None
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
class CommandGroup(_Group):
|
||||
basecmd: tuple[str, ...] = ...
|
||||
prefix_aliases: bool = ...
|
||||
def __init__(
|
||||
self,
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
cmd: str | tuple[str, ...],
|
||||
prefix_aliases: bool = ...,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
state: T_State | None = ...,
|
||||
): ...
|
||||
def command(
|
||||
self,
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
cmd: str | tuple[str, ...],
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
force_whitespace: Optional[Union[str, bool]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
aliases: set[str | tuple[str, ...]] | None = ...,
|
||||
force_whitespace: str | bool | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def shell_command(
|
||||
self,
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
cmd: str | tuple[str, ...],
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
parser: Optional[ArgumentParser] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
aliases: set[str | tuple[str, ...]] | None = ...,
|
||||
parser: ArgumentParser | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
|
||||
class MatcherGroup:
|
||||
class MatcherGroup(_Group):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
type: str = ...,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
state: T_State | None = ...,
|
||||
): ...
|
||||
def on(
|
||||
self,
|
||||
*,
|
||||
type: str = ...,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_metaevent(
|
||||
self,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_message(
|
||||
self,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_notice(
|
||||
self,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_request(
|
||||
self,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_startswith(
|
||||
self,
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
msg: str | tuple[str, ...],
|
||||
*,
|
||||
ignorecase: bool = ...,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_endswith(
|
||||
self,
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
msg: str | tuple[str, ...],
|
||||
*,
|
||||
ignorecase: bool = ...,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_fullmatch(
|
||||
self,
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
msg: str | tuple[str, ...],
|
||||
*,
|
||||
ignorecase: bool = ...,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_keyword(
|
||||
self,
|
||||
keywords: Set[str],
|
||||
keywords: set[str],
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_command(
|
||||
self,
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
force_whitespace: Optional[Union[str, bool]] = ...,
|
||||
cmd: str | tuple[str, ...],
|
||||
aliases: set[str | tuple[str, ...]] | None = ...,
|
||||
force_whitespace: str | bool | None = ...,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_shell_command(
|
||||
self,
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
parser: Optional[ArgumentParser] = ...,
|
||||
cmd: str | tuple[str, ...],
|
||||
aliases: set[str | tuple[str, ...]] | None = ...,
|
||||
parser: ArgumentParser | None = ...,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_regex(
|
||||
self,
|
||||
pattern: str,
|
||||
flags: Union[int, re.RegexFlag] = ...,
|
||||
flags: int | re.RegexFlag = ...,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
def on_type(
|
||||
self,
|
||||
types: Union[Type[Event], Tuple[Type[Event]]],
|
||||
types: type[Event] | tuple[type[Event]],
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
state: T_State | None = ...,
|
||||
) -> type[Matcher]: ...
|
||||
|
@@ -1,7 +1,18 @@
|
||||
from nonebot import on_command
|
||||
from nonebot.rule import to_me
|
||||
from nonebot.adapters import Message
|
||||
from nonebot.params import CommandArg
|
||||
from nonebot.plugin import on_command
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="echo",
|
||||
description="重复你说的话",
|
||||
usage="/echo [text]",
|
||||
type="application",
|
||||
homepage="https://github.com/nonebot/nonebot2/blob/master/nonebot/plugins/echo.py",
|
||||
config=None,
|
||||
supported_adapters=None,
|
||||
)
|
||||
|
||||
echo = on_command("echo", to_me())
|
||||
|
||||
|
@@ -2,8 +2,19 @@ from typing import Dict, AsyncGenerator
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.params import Depends
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.message import IgnoredException, event_preprocessor
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="唯一会话",
|
||||
description="限制同一会话内同时只能运行一个响应器",
|
||||
usage="加载插件后自动生效",
|
||||
type="application",
|
||||
homepage="https://github.com/nonebot/nonebot2/blob/master/nonebot/plugins/single_session.py",
|
||||
config=None,
|
||||
supported_adapters=None,
|
||||
)
|
||||
|
||||
_running_matcher: Dict[str, int] = {}
|
||||
|
||||
|
||||
@@ -15,7 +26,7 @@ async def matcher_mutex(event: Event) -> AsyncGenerator[bool, None]:
|
||||
yield result
|
||||
else:
|
||||
current_event_id = id(event)
|
||||
if event_id := _running_matcher.get(session_id, None):
|
||||
if event_id := _running_matcher.get(session_id):
|
||||
result = event_id != current_event_id
|
||||
else:
|
||||
_running_matcher[session_id] = current_event_id
|
||||
|
@@ -1,7 +1,8 @@
|
||||
"""本模块是 {ref}`nonebot.matcher.Matcher.rule` 的类型定义。
|
||||
|
||||
每个事件响应器 {ref}`nonebot.matcher.Matcher` 拥有一个匹配规则 {ref}`nonebot.rule.Rule`
|
||||
其中是 `RuleChecker` 的集合,只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。
|
||||
每个{ref}`事件响应器 <nonebot.matcher.Matcher>`拥有一个
|
||||
{ref}`nonebot.rule.Rule`,其中是 `RuleChecker` 的集合。
|
||||
只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 5
|
||||
@@ -60,20 +61,19 @@ from nonebot.consts import (
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
CMD_RESULT = TypedDict(
|
||||
"CMD_RESULT",
|
||||
{
|
||||
"command": Optional[Tuple[str, ...]],
|
||||
"raw_command": Optional[str],
|
||||
"command_arg": Optional[Message],
|
||||
"command_start": Optional[str],
|
||||
"command_whitespace": Optional[str],
|
||||
},
|
||||
)
|
||||
|
||||
TRIE_VALUE = NamedTuple(
|
||||
"TRIE_VALUE", [("command_start", str), ("command", Tuple[str, ...])]
|
||||
)
|
||||
class CMD_RESULT(TypedDict):
|
||||
command: Optional[Tuple[str, ...]]
|
||||
raw_command: Optional[str]
|
||||
command_arg: Optional[Message]
|
||||
command_start: Optional[str]
|
||||
command_whitespace: Optional[str]
|
||||
|
||||
|
||||
class TRIE_VALUE(NamedTuple):
|
||||
command_start: str
|
||||
command: Tuple[str, ...]
|
||||
|
||||
|
||||
parser_message: ContextVar[str] = ContextVar("parser_message")
|
||||
|
||||
@@ -406,7 +406,7 @@ def command(
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
|
||||
用法:
|
||||
使用默认 `command_start`, `command_sep` 配置
|
||||
使用默认 `command_start`, `command_sep` 配置情况下:
|
||||
|
||||
命令 `("test",)` 可以匹配: `/test` 开头的消息
|
||||
命令 `("test", "sub")` 可以匹配: `/test.sub` 开头的消息
|
||||
@@ -441,6 +441,8 @@ def command(
|
||||
class ArgumentParser(ArgParser):
|
||||
"""`shell_like` 命令参数解析器,解析出错时不会退出程序。
|
||||
|
||||
支持 {ref}`nonebot.adapters.Message` 富文本解析。
|
||||
|
||||
用法:
|
||||
用法与 `argparse.ArgumentParser` 相同,
|
||||
参考文档: [argparse](https://docs.python.org/3/library/argparse.html)
|
||||
@@ -588,10 +590,14 @@ def shell_command(
|
||||
根据配置里提供的 {ref}``command_start` <nonebot.config.Config.command_start>`,
|
||||
{ref}``command_sep` <nonebot.config.Config.command_sep>` 判断消息是否为命令。
|
||||
|
||||
可以通过 {ref}`nonebot.params.Command` 获取匹配成功的命令(例: `("test",)`),
|
||||
通过 {ref}`nonebot.params.RawCommand` 获取匹配成功的原始命令文本(例: `"/test"`),
|
||||
通过 {ref}`nonebot.params.ShellCommandArgv` 获取解析前的参数列表(例: `["arg", "-h"]`),
|
||||
通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典(例: `{"arg": "arg", "h": True}`)。
|
||||
可以通过 {ref}`nonebot.params.Command` 获取匹配成功的命令
|
||||
(例: `("test",)`),
|
||||
通过 {ref}`nonebot.params.RawCommand` 获取匹配成功的原始命令文本
|
||||
(例: `"/test"`),
|
||||
通过 {ref}`nonebot.params.ShellCommandArgv` 获取解析前的参数列表
|
||||
(例: `["arg", "-h"]`),
|
||||
通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典
|
||||
(例: `{"arg": "arg", "h": True}`)。
|
||||
|
||||
:::warning 警告
|
||||
如果参数解析失败,则通过 {ref}`nonebot.params.ShellCommandArgs`
|
||||
@@ -603,7 +609,8 @@ def shell_command(
|
||||
parser: {ref}`nonebot.rule.ArgumentParser` 对象
|
||||
|
||||
用法:
|
||||
使用默认 `command_start`, `command_sep` 配置,更多示例参考 `argparse` 标准库文档。
|
||||
使用默认 `command_start`, `command_sep` 配置,更多示例参考
|
||||
[argparse](https://docs.python.org/3/library/argparse.html) 标准库文档。
|
||||
|
||||
```python
|
||||
from nonebot.rule import ArgumentParser
|
||||
@@ -698,7 +705,8 @@ def regex(regex: str, flags: Union[int, re.RegexFlag] = 0) -> Rule:
|
||||
:::
|
||||
|
||||
:::tip 提示
|
||||
正则表达式匹配使用 `EventMessage` 的 `str` 字符串,而非 `EventMessage` 的 `PlainText` 纯文本字符串
|
||||
正则表达式匹配使用 `EventMessage` 的 `str` 字符串,
|
||||
而非 `EventMessage` 的 `PlainText` 纯文本字符串
|
||||
:::
|
||||
"""
|
||||
|
||||
|
@@ -1,17 +1,17 @@
|
||||
"""本模块定义了 NoneBot 模块中共享的一些类型。
|
||||
|
||||
下面的文档中,「类型」部分使用 Python 的 Type Hint 语法,
|
||||
使用 Python 的 Type Hint 语法,
|
||||
参考 [`PEP 484`](https://www.python.org/dev/peps/pep-0484/),
|
||||
[`PEP 526`](https://www.python.org/dev/peps/pep-0526/) 和
|
||||
[`typing`](https://docs.python.org/3/library/typing.html)。
|
||||
|
||||
除了 Python 内置的类型,下面还出现了如下 NoneBot 自定类型,实际上它们是 Python 内置类型的别名。
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 11
|
||||
description: nonebot.typing 模块
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from typing_extensions import ParamSpec, TypeAlias, override
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@@ -30,28 +30,31 @@ if TYPE_CHECKING:
|
||||
from nonebot.permission import Permission
|
||||
|
||||
T = TypeVar("T")
|
||||
P = ParamSpec("P")
|
||||
|
||||
T_Wrapped = TypeVar("T_Wrapped", bound=Callable)
|
||||
T_Wrapped: TypeAlias = Callable[P, T]
|
||||
|
||||
|
||||
def overrides(InterfaceClass: object) -> Callable[[T_Wrapped], T_Wrapped]:
|
||||
def overrides(InterfaceClass: object):
|
||||
"""标记一个方法为父类 interface 的 implement"""
|
||||
|
||||
def overrider(func: T_Wrapped) -> T_Wrapped:
|
||||
assert func.__name__ in dir(InterfaceClass), f"Error method: {func.__name__}"
|
||||
return func
|
||||
|
||||
return overrider
|
||||
warnings.warn(
|
||||
"overrides is deprecated and will be removed in a future version, "
|
||||
"use @typing_extensions.override instead. "
|
||||
"See [PEP 698](https://peps.python.org/pep-0698/) for more details.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return override
|
||||
|
||||
|
||||
# state
|
||||
T_State = Dict[Any, Any]
|
||||
T_State: TypeAlias = Dict[Any, Any]
|
||||
"""事件处理状态 State 类型"""
|
||||
|
||||
_DependentCallable = Union[Callable[..., T], Callable[..., Awaitable[T]]]
|
||||
_DependentCallable: TypeAlias = Union[Callable[..., T], Callable[..., Awaitable[T]]]
|
||||
|
||||
# driver hooks
|
||||
T_BotConnectionHook = _DependentCallable[Any]
|
||||
T_BotConnectionHook: TypeAlias = _DependentCallable[Any]
|
||||
"""Bot 连接建立时钩子函数
|
||||
|
||||
依赖参数:
|
||||
@@ -60,7 +63,7 @@ T_BotConnectionHook = _DependentCallable[Any]
|
||||
- BotParam: Bot 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_BotDisconnectionHook = _DependentCallable[Any]
|
||||
T_BotDisconnectionHook: TypeAlias = _DependentCallable[Any]
|
||||
"""Bot 连接断开时钩子函数
|
||||
|
||||
依赖参数:
|
||||
@@ -71,15 +74,15 @@ T_BotDisconnectionHook = _DependentCallable[Any]
|
||||
"""
|
||||
|
||||
# api hooks
|
||||
T_CallingAPIHook = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]]
|
||||
T_CallingAPIHook: TypeAlias = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]]
|
||||
"""`bot.call_api` 钩子函数"""
|
||||
T_CalledAPIHook = Callable[
|
||||
T_CalledAPIHook: TypeAlias = Callable[
|
||||
["Bot", Optional[Exception], str, Dict[str, Any], Any], Awaitable[Any]
|
||||
]
|
||||
"""`bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result"""
|
||||
|
||||
# event hooks
|
||||
T_EventPreProcessor = _DependentCallable[Any]
|
||||
T_EventPreProcessor: TypeAlias = _DependentCallable[Any]
|
||||
"""事件预处理函数 EventPreProcessor 类型
|
||||
|
||||
依赖参数:
|
||||
@@ -90,7 +93,7 @@ T_EventPreProcessor = _DependentCallable[Any]
|
||||
- StateParam: State 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_EventPostProcessor = _DependentCallable[Any]
|
||||
T_EventPostProcessor: TypeAlias = _DependentCallable[Any]
|
||||
"""事件预处理函数 EventPostProcessor 类型
|
||||
|
||||
依赖参数:
|
||||
@@ -103,7 +106,7 @@ T_EventPostProcessor = _DependentCallable[Any]
|
||||
"""
|
||||
|
||||
# matcher run hooks
|
||||
T_RunPreProcessor = _DependentCallable[Any]
|
||||
T_RunPreProcessor: TypeAlias = _DependentCallable[Any]
|
||||
"""事件响应器运行前预处理函数 RunPreProcessor 类型
|
||||
|
||||
依赖参数:
|
||||
@@ -115,7 +118,7 @@ T_RunPreProcessor = _DependentCallable[Any]
|
||||
- MatcherParam: Matcher 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_RunPostProcessor = _DependentCallable[Any]
|
||||
T_RunPostProcessor: TypeAlias = _DependentCallable[Any]
|
||||
"""事件响应器运行后后处理函数 RunPostProcessor 类型
|
||||
|
||||
依赖参数:
|
||||
@@ -130,7 +133,7 @@ T_RunPostProcessor = _DependentCallable[Any]
|
||||
"""
|
||||
|
||||
# rule, permission
|
||||
T_RuleChecker = _DependentCallable[bool]
|
||||
T_RuleChecker: TypeAlias = _DependentCallable[bool]
|
||||
"""RuleChecker 即判断是否响应事件的处理函数。
|
||||
|
||||
依赖参数:
|
||||
@@ -141,7 +144,7 @@ T_RuleChecker = _DependentCallable[bool]
|
||||
- StateParam: State 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_PermissionChecker = _DependentCallable[bool]
|
||||
T_PermissionChecker: TypeAlias = _DependentCallable[bool]
|
||||
"""PermissionChecker 即判断事件是否满足权限的处理函数。
|
||||
|
||||
依赖参数:
|
||||
@@ -152,10 +155,11 @@ T_PermissionChecker = _DependentCallable[bool]
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
|
||||
T_Handler = _DependentCallable[Any]
|
||||
T_Handler: TypeAlias = _DependentCallable[Any]
|
||||
"""Handler 处理函数。"""
|
||||
T_TypeUpdater = _DependentCallable[str]
|
||||
"""TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。默认会更新为 `message`。
|
||||
T_TypeUpdater: TypeAlias = _DependentCallable[str]
|
||||
"""TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。
|
||||
默认会更新为 `message`。
|
||||
|
||||
依赖参数:
|
||||
|
||||
@@ -166,8 +170,9 @@ T_TypeUpdater = _DependentCallable[str]
|
||||
- MatcherParam: Matcher 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_PermissionUpdater = _DependentCallable["Permission"]
|
||||
"""PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新会话对象权限。默认会更新为当前事件的触发对象。
|
||||
T_PermissionUpdater: TypeAlias = _DependentCallable["Permission"]
|
||||
"""PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新会话对象权限。
|
||||
默认会更新为当前事件的触发对象。
|
||||
|
||||
依赖参数:
|
||||
|
||||
@@ -178,5 +183,5 @@ T_PermissionUpdater = _DependentCallable["Permission"]
|
||||
- MatcherParam: Matcher 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_DependencyCache = Dict[_DependentCallable[Any], "Task[Any]"]
|
||||
T_DependencyCache: TypeAlias = Dict[_DependentCallable[Any], "Task[Any]"]
|
||||
"""依赖缓存, 用于存储依赖函数的返回值"""
|
||||
|
@@ -15,7 +15,7 @@ from pathlib import Path
|
||||
from contextvars import copy_context
|
||||
from functools import wraps, partial
|
||||
from contextlib import asynccontextmanager
|
||||
from typing_extensions import ParamSpec, get_args, get_origin
|
||||
from typing_extensions import ParamSpec, get_args, override, get_origin
|
||||
from typing import (
|
||||
Any,
|
||||
Type,
|
||||
@@ -33,7 +33,6 @@ from typing import (
|
||||
from pydantic.typing import is_union, is_none_type
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.typing import overrides
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
@@ -58,8 +57,13 @@ def generic_check_issubclass(
|
||||
) -> bool:
|
||||
"""检查 cls 是否是 class_or_tuple 中的一个类型子类。
|
||||
|
||||
特别的,如果 cls 是 `typing.Union` 或 `types.UnionType` 类型,
|
||||
特别的:
|
||||
|
||||
- 如果 cls 是 `typing.Union` 或 `types.UnionType` 类型,
|
||||
则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。
|
||||
- 如果 cls 是 `typing.TypeVar` 类型,
|
||||
则会检查其 `__bound__` 或 `__constraints__`
|
||||
是否是 class_or_tuple 中一个类型的子类或 None。
|
||||
"""
|
||||
try:
|
||||
return issubclass(cls, class_or_tuple)
|
||||
@@ -70,8 +74,18 @@ def generic_check_issubclass(
|
||||
is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
|
||||
for type_ in get_args(cls)
|
||||
)
|
||||
# ensure generic List, Dict can be checked
|
||||
elif origin:
|
||||
return issubclass(origin, class_or_tuple)
|
||||
elif isinstance(cls, TypeVar):
|
||||
if cls.__constraints__:
|
||||
return all(
|
||||
is_none_type(type_)
|
||||
or generic_check_issubclass(type_, class_or_tuple)
|
||||
for type_ in cls.__constraints__
|
||||
)
|
||||
elif cls.__bound__:
|
||||
return generic_check_issubclass(cls.__bound__, class_or_tuple)
|
||||
return False
|
||||
|
||||
|
||||
@@ -157,6 +171,17 @@ async def run_coro_with_catch(
|
||||
exc: Tuple[Type[Exception], ...],
|
||||
return_on_err: Optional[R] = None,
|
||||
) -> Optional[Union[T, R]]:
|
||||
"""运行协程并当遇到指定异常时返回指定值。
|
||||
|
||||
参数:
|
||||
coro: 要运行的协程
|
||||
exc: 要捕获的异常
|
||||
return_on_err: 当发生异常时返回的值
|
||||
|
||||
返回:
|
||||
协程的返回值或发生异常时的指定值
|
||||
"""
|
||||
|
||||
try:
|
||||
return await coro
|
||||
except exc:
|
||||
@@ -196,9 +221,9 @@ def resolve_dot_notation(
|
||||
|
||||
|
||||
class DataclassEncoder(json.JSONEncoder):
|
||||
"""在JSON序列化 {ref}`nonebot.adapters.Message` (List[Dataclass]) 时使用的 `JSONEncoder`"""
|
||||
"""可以序列化 {ref}`nonebot.adapters.Message`(List[Dataclass]) 的 `JSONEncoder`"""
|
||||
|
||||
@overrides(json.JSONEncoder)
|
||||
@override
|
||||
def default(self, o):
|
||||
if dataclasses.is_dataclass(o):
|
||||
return {f.name: getattr(o, f.name) for f in dataclasses.fields(o)}
|
||||
@@ -214,6 +239,8 @@ def logger_wrapper(logger_name: str):
|
||||
返回:
|
||||
日志记录函数
|
||||
|
||||
日志记录函数的参数:
|
||||
|
||||
- level: 日志等级
|
||||
- message: 日志信息
|
||||
- exception: 异常信息
|
||||
|
@@ -11,10 +11,12 @@
|
||||
"start": "yarn workspace nonebot start",
|
||||
"serve": "yarn workspace nonebot serve",
|
||||
"clear": "yarn workspace nonebot clear",
|
||||
"prettier": "prettier --config ./.prettierrc --write \"./website/\""
|
||||
"prettier": "prettier --config ./.prettierrc --write \"./website/\"",
|
||||
"pyright": "pyright"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"prettier": "^2.5.0"
|
||||
"prettier": "^2.5.0",
|
||||
"pyright": "^1.1.317"
|
||||
}
|
||||
}
|
||||
|
@@ -22,6 +22,6 @@ _✨ NoneBot 本地文档插件 ✨_
|
||||
|
||||
## 使用方式
|
||||
|
||||
加载插件并启动 Bot ,在浏览器内打开 `http://host:port/docs/`。
|
||||
加载插件并启动 Bot ,在浏览器内打开 `http://host:port/website/`。
|
||||
|
||||
具体网址会在控制台内输出。
|
||||
|
@@ -2,6 +2,17 @@ import importlib
|
||||
|
||||
import nonebot
|
||||
from nonebot.log import logger
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="NoneBot 离线文档",
|
||||
description="在本地查看 NoneBot 文档",
|
||||
usage="启动机器人后访问 http://localhost:port/website/ 查看文档",
|
||||
type="application",
|
||||
homepage="https://github.com/nonebot/nonebot2/blob/master/packages/nonebot-plugin-docs",
|
||||
config=None,
|
||||
supported_adapters=None,
|
||||
)
|
||||
|
||||
|
||||
def init():
|
||||
@@ -17,7 +28,7 @@ def init():
|
||||
register_route(driver)
|
||||
host = str(driver.config.host)
|
||||
port = driver.config.port
|
||||
if host in ["0.0.0.0", "127.0.0.1"]:
|
||||
if host in {"0.0.0.0", "127.0.0.1"}:
|
||||
host = "localhost"
|
||||
logger.opt(colors=True).info(
|
||||
f"Nonebot docs will be running at: "
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "nonebot-plugin-docs"
|
||||
version = "2.0.0-beta.1"
|
||||
version = "2.0.0"
|
||||
description = "View NoneBot2 Docs Locally"
|
||||
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||
license = "MIT"
|
||||
@@ -13,7 +13,7 @@ include = ["nonebot_plugin_docs/dist/**/*"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
nonebot2 = "^2.0.0-beta.1"
|
||||
nonebot2 = "^2.0.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
|
1242
poetry.lock
generated
1242
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"
|
||||
version = "2.0.1"
|
||||
description = "An asynchronous python bot framework."
|
||||
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||
license = "MIT"
|
||||
@@ -43,18 +43,19 @@ httpx = { version = ">=0.20.0,<1.0.0", extras = ["http2"], optional = true }
|
||||
uvicorn = { version = ">=0.20.0,<1.0.0", extras = ["standard"], optional = true }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pycln = "^2.1.2"
|
||||
isort = "^5.10.1"
|
||||
black = "^23.1.0"
|
||||
nonemoji = "^0.1.2"
|
||||
pre-commit = "^3.0.0"
|
||||
ruff = ">=0.0.272,<1.0.0"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
nonebug = "^0.3.0"
|
||||
werkzeug = "^2.3.6"
|
||||
pytest-cov = "^4.0.0"
|
||||
pytest-xdist = "^3.0.2"
|
||||
pytest-asyncio = "^0.21.0"
|
||||
coverage-conditional-plugin = "^0.8.0"
|
||||
coverage-conditional-plugin = "^0.9.0"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
nb-autodoc = "^1.0.0a5"
|
||||
@@ -68,7 +69,7 @@ fastapi = ["fastapi", "uvicorn"]
|
||||
all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_mode = "strict"
|
||||
addopts = "--cov=nonebot --cov-append --cov-report=term-missing"
|
||||
filterwarnings = [
|
||||
"error",
|
||||
@@ -91,12 +92,18 @@ force_sort_within_sections = true
|
||||
src_paths = ["nonebot", "tests"]
|
||||
extra_standard_library = ["typing_extensions"]
|
||||
|
||||
[tool.pycln]
|
||||
path = "."
|
||||
all = false
|
||||
[tool.ruff]
|
||||
select = ["E", "W", "F", "UP", "C", "T", "PYI", "PT", "Q"]
|
||||
ignore = ["E402", "C901"]
|
||||
|
||||
line-length = 88
|
||||
target-version = "py38"
|
||||
|
||||
[tool.ruff.flake8-pytest-style]
|
||||
fixture-parentheses = false
|
||||
mark-parentheses = false
|
||||
|
||||
[tool.pyright]
|
||||
reportShadowedImports = false
|
||||
pythonVersion = "3.8"
|
||||
pythonPlatform = "All"
|
||||
executionEnvironments = [
|
||||
@@ -104,6 +111,9 @@ executionEnvironments = [
|
||||
{ root = "./" },
|
||||
]
|
||||
|
||||
typeCheckingMode = "basic"
|
||||
reportShadowedImports = false
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry_core>=1.0.0"]
|
||||
|
@@ -1,6 +1,8 @@
|
||||
LOG_LEVEL=TRACE
|
||||
NICKNAME=["test"]
|
||||
SUPERUSERS=["test", "fake:faketest"]
|
||||
API_TIMEOUT
|
||||
SIMPLE_NONE
|
||||
COMMON_OVERRIDE=new
|
||||
CONFIG_FROM_ENV=
|
||||
CONFIG_OVERRIDE=old
|
||||
|
@@ -1,11 +1,15 @@
|
||||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Set
|
||||
from typing import TYPE_CHECKING, Set, Generator
|
||||
|
||||
import pytest
|
||||
from nonebug import NONEBOT_INIT_KWARGS
|
||||
from werkzeug.serving import BaseWSGIServer, make_server
|
||||
|
||||
import nonebot
|
||||
from nonebot.drivers import URL
|
||||
from fake_server import request_handler
|
||||
|
||||
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
|
||||
os.environ["CONFIG_OVERRIDE"] = "new"
|
||||
@@ -28,3 +32,20 @@ def load_plugin(nonebug_init: None) -> Set["Plugin"]:
|
||||
def load_builtin_plugin(nonebug_init: None) -> Set["Plugin"]:
|
||||
# preload builtin plugins
|
||||
return nonebot.load_builtin_plugins("echo", "single_session")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def server() -> Generator[BaseWSGIServer, None, None]:
|
||||
server = make_server("127.0.0.1", 0, app=request_handler)
|
||||
thread = threading.Thread(target=server.serve_forever)
|
||||
thread.start()
|
||||
try:
|
||||
yield server
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def server_url(server: BaseWSGIServer) -> URL:
|
||||
return URL(f"http://{server.host}:{server.port}")
|
||||
|
69
tests/fake_server.py
Normal file
69
tests/fake_server.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import json
|
||||
import base64
|
||||
from typing import Dict, List, Union, TypeVar
|
||||
|
||||
from werkzeug import Request, Response
|
||||
from werkzeug.datastructures import MultiDict
|
||||
|
||||
K = TypeVar("K")
|
||||
V = TypeVar("V")
|
||||
|
||||
|
||||
def json_safe(string, content_type="application/octet-stream") -> str:
|
||||
try:
|
||||
string = string.decode("utf-8")
|
||||
json.dumps(string)
|
||||
return string
|
||||
except (ValueError, TypeError):
|
||||
return b"".join(
|
||||
[
|
||||
b"data:",
|
||||
content_type.encode("utf-8"),
|
||||
b";base64,",
|
||||
base64.b64encode(string),
|
||||
]
|
||||
).decode("utf-8")
|
||||
|
||||
|
||||
def flattern(d: "MultiDict[K, V]") -> Dict[K, Union[V, List[V]]]:
|
||||
return {k: v[0] if len(v) == 1 else v for k, v in d.to_dict(flat=False).items()}
|
||||
|
||||
|
||||
@Request.application
|
||||
def request_handler(request: Request) -> Response:
|
||||
try:
|
||||
_json = json.loads(request.data.decode("utf-8"))
|
||||
except (ValueError, TypeError):
|
||||
_json = None
|
||||
|
||||
return Response(
|
||||
json.dumps(
|
||||
{
|
||||
"url": request.url,
|
||||
"method": request.method,
|
||||
"origin": request.headers.get("X-Forwarded-For", request.remote_addr),
|
||||
"headers": flattern(
|
||||
MultiDict((k, v) for k, v in request.headers.items())
|
||||
),
|
||||
"args": flattern(request.args),
|
||||
"form": flattern(request.form),
|
||||
"data": json_safe(request.data),
|
||||
"json": _json,
|
||||
"files": flattern(
|
||||
MultiDict(
|
||||
(
|
||||
k,
|
||||
json_safe(
|
||||
v.read(),
|
||||
request.files[k].content_type
|
||||
or "application/octet-stream",
|
||||
),
|
||||
)
|
||||
for k, v in request.files.items()
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
status=200,
|
||||
content_type="application/json",
|
||||
)
|
@@ -1 +1,3 @@
|
||||
assert False
|
||||
import pytest
|
||||
|
||||
pytest.fail("should not be imported")
|
||||
|
@@ -2,6 +2,7 @@ from nonebot.matcher import Matcher
|
||||
from nonebot.permission import USER, Permission
|
||||
|
||||
default_permission = Permission()
|
||||
new_permission = Permission()
|
||||
|
||||
test_permission_updater = Matcher.new(permission=default_permission)
|
||||
|
||||
@@ -14,4 +15,4 @@ test_custom_updater = Matcher.new(permission=default_permission)
|
||||
|
||||
@test_custom_updater.permission_updater
|
||||
async def _() -> Permission:
|
||||
return default_permission
|
||||
return new_permission
|
||||
|
11
tests/plugins/metadata_2.py
Normal file
11
tests/plugins/metadata_2.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="测试插件2",
|
||||
description="测试继承适配器",
|
||||
usage="无法使用",
|
||||
type="application",
|
||||
homepage="https://nonebot.dev",
|
||||
supported_adapters={"~onebot.v11", "~onebot.v12"},
|
||||
extra={"author": "NoneBot"},
|
||||
)
|
@@ -1,6 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager, _managers
|
||||
|
||||
manager = PluginManager(
|
||||
|
@@ -1 +1 @@
|
||||
from .nested_subplugin2 import a # nopycln: import
|
||||
from .nested_subplugin2 import a # noqa: F401
|
||||
|
@@ -1,4 +1,6 @@
|
||||
from nonebot.adapters import Event, Message
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from nonebot.adapters import Message
|
||||
from nonebot.params import Arg, ArgStr, ArgPlainText
|
||||
|
||||
|
||||
@@ -12,3 +14,15 @@ async def arg_str(key: str = ArgStr()) -> str:
|
||||
|
||||
async def arg_plain_text(key: str = ArgPlainText()) -> str:
|
||||
return key
|
||||
|
||||
|
||||
async def annotated_arg(key: Annotated[Message, Arg()]) -> Message:
|
||||
return key
|
||||
|
||||
|
||||
async def annotated_arg_str(key: Annotated[str, ArgStr()]) -> str:
|
||||
return key
|
||||
|
||||
|
||||
async def annotated_arg_plain_text(key: Annotated[str, ArgPlainText()]) -> str:
|
||||
return key
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from typing import Union
|
||||
from typing import Union, TypeVar
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
@@ -31,5 +31,19 @@ async def union_bot(b: Union[FooBot, BarBot]) -> Union[FooBot, BarBot]:
|
||||
return b
|
||||
|
||||
|
||||
B = TypeVar("B", bound=Bot)
|
||||
|
||||
|
||||
async def generic_bot(b: B) -> B:
|
||||
return b
|
||||
|
||||
|
||||
CB = TypeVar("CB", Bot, None)
|
||||
|
||||
|
||||
async def generic_bot_none(b: CB) -> CB:
|
||||
return b
|
||||
|
||||
|
||||
async def not_bot(b: Union[int, Bot]):
|
||||
...
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from typing import Union
|
||||
from typing import Union, TypeVar
|
||||
|
||||
from nonebot.adapters import Event, Message
|
||||
from nonebot.params import EventToMe, EventType, EventMessage, EventPlainText
|
||||
@@ -32,6 +32,20 @@ async def union_event(e: Union[FooEvent, BarEvent]) -> Union[FooEvent, BarEvent]
|
||||
return e
|
||||
|
||||
|
||||
E = TypeVar("E", bound=Event)
|
||||
|
||||
|
||||
async def generic_event(e: E) -> E:
|
||||
return e
|
||||
|
||||
|
||||
CE = TypeVar("CE", Event, None)
|
||||
|
||||
|
||||
async def generic_event_none(e: CE) -> CE:
|
||||
return e
|
||||
|
||||
|
||||
async def not_event(e: Union[int, Event]):
|
||||
...
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
from typing import Union, TypeVar
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.params import Received, LastReceived
|
||||
@@ -7,6 +9,50 @@ async def matcher(m: Matcher) -> Matcher:
|
||||
return m
|
||||
|
||||
|
||||
async def legacy_matcher(matcher):
|
||||
return matcher
|
||||
|
||||
|
||||
async def not_legacy_matcher(matcher: int):
|
||||
...
|
||||
|
||||
|
||||
class FooMatcher(Matcher):
|
||||
...
|
||||
|
||||
|
||||
async def sub_matcher(m: FooMatcher) -> FooMatcher:
|
||||
return m
|
||||
|
||||
|
||||
class BarMatcher(Matcher):
|
||||
...
|
||||
|
||||
|
||||
async def union_matcher(
|
||||
m: Union[FooMatcher, BarMatcher]
|
||||
) -> Union[FooMatcher, BarMatcher]:
|
||||
return m
|
||||
|
||||
|
||||
M = TypeVar("M", bound=Matcher)
|
||||
|
||||
|
||||
async def generic_matcher(m: M) -> M:
|
||||
return m
|
||||
|
||||
|
||||
CM = TypeVar("CM", Matcher, None)
|
||||
|
||||
|
||||
async def generic_matcher_none(m: CM) -> CM:
|
||||
return m
|
||||
|
||||
|
||||
async def not_matcher(m: Union[int, Matcher]):
|
||||
...
|
||||
|
||||
|
||||
async def receive(e: Event = Received("test")) -> Event:
|
||||
return e
|
||||
|
||||
|
@@ -1 +1 @@
|
||||
from . import matchers
|
||||
from . import matchers as matchers
|
||||
|
@@ -220,7 +220,7 @@ matcher_on_type = on_type(
|
||||
|
||||
|
||||
cmd_group = CommandGroup(
|
||||
"test",
|
||||
"prefix",
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
@@ -230,8 +230,30 @@ cmd_group = CommandGroup(
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
matcher_sub_cmd = cmd_group.command("sub")
|
||||
matcher_sub_shell_cmd = cmd_group.shell_command("sub")
|
||||
matcher_prefix_cmd = cmd_group.command("sub", aliases={"help", ("help", "foo")})
|
||||
matcher_prefix_shell_cmd = cmd_group.shell_command(
|
||||
"sub", aliases={"help", ("help", "foo")}
|
||||
)
|
||||
|
||||
|
||||
cmd_group_prefix_aliases = CommandGroup(
|
||||
"prefix",
|
||||
prefix_aliases=True,
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
matcher_prefix_aliases_cmd = cmd_group_prefix_aliases.command(
|
||||
"sub", aliases={"help", ("help", "foo")}
|
||||
)
|
||||
matcher_prefix_aliases_shell_cmd = cmd_group_prefix_aliases.shell_command(
|
||||
"sub", aliases={"help", ("help", "foo")}
|
||||
)
|
||||
|
||||
|
||||
matcher_group = MatcherGroup(
|
||||
|
@@ -4,4 +4,5 @@ test_require = require("export").test
|
||||
|
||||
from plugins.export import test
|
||||
|
||||
assert test is test_require and test() == "export", "Export Require Error"
|
||||
assert test is test_require, "Export Require Error"
|
||||
assert test() == "export", "Export Require Error"
|
||||
|
@@ -1,135 +1,136 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError, parse_obj_as
|
||||
|
||||
from utils import make_fake_message
|
||||
from nonebot.adapters import Message
|
||||
from utils import FakeMessage, FakeMessageSegment
|
||||
|
||||
|
||||
def test_segment_add():
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
assert MessageSegment.text("text") + MessageSegment.text("text") == Message(
|
||||
[MessageSegment.text("text"), MessageSegment.text("text")]
|
||||
)
|
||||
|
||||
assert MessageSegment.text("text") + "text" == Message(
|
||||
[MessageSegment.text("text"), MessageSegment.text("text")]
|
||||
)
|
||||
|
||||
assert (
|
||||
MessageSegment.text("text") + Message([MessageSegment.text("text")])
|
||||
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
|
||||
|
||||
assert "text" + MessageSegment.text("text") == Message(
|
||||
[MessageSegment.text("text"), MessageSegment.text("text")]
|
||||
)
|
||||
|
||||
|
||||
def test_segment_validate():
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
assert parse_obj_as(
|
||||
MessageSegment,
|
||||
{"type": "text", "data": {"text": "text"}, "extra": "should be ignored"},
|
||||
) == MessageSegment.text("text")
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
parse_obj_as(MessageSegment, "some str")
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
parse_obj_as(MessageSegment, {"data": {}})
|
||||
|
||||
|
||||
def test_segment_join():
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
seg = MessageSegment.text("test")
|
||||
iterable = [
|
||||
MessageSegment.text("first"),
|
||||
Message([MessageSegment.text("second"), MessageSegment.text("third")]),
|
||||
]
|
||||
|
||||
assert seg.join(iterable) == Message(
|
||||
[
|
||||
MessageSegment.text("first"),
|
||||
MessageSegment.text("test"),
|
||||
MessageSegment.text("second"),
|
||||
MessageSegment.text("third"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_segment():
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
assert len(MessageSegment.text("text")) == 4
|
||||
assert MessageSegment.text("text") != MessageSegment.text("other")
|
||||
assert MessageSegment.text("text").get("data") == {"text": "text"}
|
||||
assert list(MessageSegment.text("text").keys()) == ["type", "data"]
|
||||
assert list(MessageSegment.text("text").values()) == ["text", {"text": "text"}]
|
||||
assert list(MessageSegment.text("text").items()) == [
|
||||
def test_segment_data():
|
||||
assert len(FakeMessageSegment.text("text")) == 4
|
||||
assert FakeMessageSegment.text("text").get("data") == {"text": "text"}
|
||||
assert list(FakeMessageSegment.text("text").keys()) == ["type", "data"]
|
||||
assert list(FakeMessageSegment.text("text").values()) == ["text", {"text": "text"}]
|
||||
assert list(FakeMessageSegment.text("text").items()) == [
|
||||
("type", "text"),
|
||||
("data", {"text": "text"}),
|
||||
]
|
||||
|
||||
origin = MessageSegment.text("text")
|
||||
|
||||
def test_segment_equal():
|
||||
assert FakeMessageSegment("text", {"text": "text"}) == FakeMessageSegment(
|
||||
"text", {"text": "text"}
|
||||
)
|
||||
assert FakeMessageSegment("text", {"text": "text"}) != FakeMessageSegment(
|
||||
"text", {"text": "other"}
|
||||
)
|
||||
assert FakeMessageSegment("text", {"text": "text"}) != FakeMessageSegment(
|
||||
"other", {"text": "text"}
|
||||
)
|
||||
|
||||
|
||||
def test_segment_add():
|
||||
assert FakeMessageSegment.text("text") + FakeMessageSegment.text(
|
||||
"text"
|
||||
) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")])
|
||||
|
||||
assert FakeMessageSegment.text("text") + "text" == FakeMessage(
|
||||
[FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]
|
||||
)
|
||||
|
||||
assert (
|
||||
FakeMessageSegment.text("text") + FakeMessage([FakeMessageSegment.text("text")])
|
||||
) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")])
|
||||
|
||||
assert "text" + FakeMessageSegment.text("text") == FakeMessage(
|
||||
[FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]
|
||||
)
|
||||
|
||||
|
||||
def test_segment_validate():
|
||||
assert parse_obj_as(
|
||||
FakeMessageSegment,
|
||||
{"type": "text", "data": {"text": "text"}, "extra": "should be ignored"},
|
||||
) == FakeMessageSegment.text("text")
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
parse_obj_as(FakeMessageSegment, "some str")
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
parse_obj_as(FakeMessageSegment, {"data": {}})
|
||||
|
||||
|
||||
def test_segment_join():
|
||||
seg = FakeMessageSegment.text("test")
|
||||
iterable = [
|
||||
FakeMessageSegment.text("first"),
|
||||
FakeMessage(
|
||||
[FakeMessageSegment.text("second"), FakeMessageSegment.text("third")]
|
||||
),
|
||||
]
|
||||
|
||||
assert seg.join(iterable) == FakeMessage(
|
||||
[
|
||||
FakeMessageSegment.text("first"),
|
||||
FakeMessageSegment.text("test"),
|
||||
FakeMessageSegment.text("second"),
|
||||
FakeMessageSegment.text("third"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_segment_copy():
|
||||
origin = FakeMessageSegment.text("text")
|
||||
copy = origin.copy()
|
||||
assert origin is not copy
|
||||
assert origin == copy
|
||||
|
||||
|
||||
def test_message_add():
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
assert (
|
||||
Message([MessageSegment.text("text")]) + MessageSegment.text("text")
|
||||
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
|
||||
FakeMessage([FakeMessageSegment.text("text")]) + FakeMessageSegment.text("text")
|
||||
) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")])
|
||||
|
||||
assert Message([MessageSegment.text("text")]) + "text" == Message(
|
||||
[MessageSegment.text("text"), MessageSegment.text("text")]
|
||||
assert FakeMessage([FakeMessageSegment.text("text")]) + "text" == FakeMessage(
|
||||
[FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]
|
||||
)
|
||||
|
||||
assert (
|
||||
Message([MessageSegment.text("text")]) + Message([MessageSegment.text("text")])
|
||||
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
|
||||
FakeMessage([FakeMessageSegment.text("text")])
|
||||
+ FakeMessage([FakeMessageSegment.text("text")])
|
||||
) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")])
|
||||
|
||||
assert "text" + Message([MessageSegment.text("text")]) == Message(
|
||||
[MessageSegment.text("text"), MessageSegment.text("text")]
|
||||
assert "text" + FakeMessage([FakeMessageSegment.text("text")]) == FakeMessage(
|
||||
[FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]
|
||||
)
|
||||
|
||||
msg = Message([MessageSegment.text("text")])
|
||||
msg += MessageSegment.text("text")
|
||||
assert msg == Message([MessageSegment.text("text"), MessageSegment.text("text")])
|
||||
msg = FakeMessage([FakeMessageSegment.text("text")])
|
||||
msg += FakeMessageSegment.text("text")
|
||||
assert msg == FakeMessage(
|
||||
[FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]
|
||||
)
|
||||
|
||||
|
||||
def test_message_getitem():
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
message = Message(
|
||||
message = FakeMessage(
|
||||
[
|
||||
MessageSegment.text("test"),
|
||||
MessageSegment.image("test2"),
|
||||
MessageSegment.image("test3"),
|
||||
MessageSegment.text("test4"),
|
||||
FakeMessageSegment.text("test"),
|
||||
FakeMessageSegment.image("test2"),
|
||||
FakeMessageSegment.image("test3"),
|
||||
FakeMessageSegment.text("test4"),
|
||||
]
|
||||
)
|
||||
|
||||
assert message[0] == MessageSegment.text("test")
|
||||
assert message[0] == FakeMessageSegment.text("test")
|
||||
|
||||
assert message[:2] == Message(
|
||||
[MessageSegment.text("test"), MessageSegment.image("test2")]
|
||||
assert message[:2] == FakeMessage(
|
||||
[FakeMessageSegment.text("test"), FakeMessageSegment.image("test2")]
|
||||
)
|
||||
|
||||
assert message["image"] == Message(
|
||||
[MessageSegment.image("test2"), MessageSegment.image("test3")]
|
||||
assert message["image"] == FakeMessage(
|
||||
[FakeMessageSegment.image("test2"), FakeMessageSegment.image("test3")]
|
||||
)
|
||||
|
||||
assert message["image", 0] == MessageSegment.image("test2")
|
||||
assert message["image", 0] == FakeMessageSegment.image("test2")
|
||||
assert message["image", 0:2] == message["image"]
|
||||
|
||||
assert message.index(message[0]) == 0
|
||||
@@ -137,153 +138,137 @@ def test_message_getitem():
|
||||
|
||||
assert message.get("image") == message["image"]
|
||||
assert message.get("image", 114514) == message["image"]
|
||||
assert message.get("image", 1) == Message([message["image", 0]])
|
||||
assert message.get("image", 1) == FakeMessage([message["image", 0]])
|
||||
|
||||
assert message.count("image") == 2
|
||||
|
||||
|
||||
def test_message_validate():
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
Message_ = make_fake_message()
|
||||
|
||||
assert parse_obj_as(Message, Message([])) == Message([])
|
||||
assert parse_obj_as(FakeMessage, FakeMessage([])) == FakeMessage([])
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
parse_obj_as(Message, Message_([]))
|
||||
parse_obj_as(type("FakeMessage2", (Message,), {}), FakeMessage([]))
|
||||
|
||||
assert parse_obj_as(Message, "text") == Message([MessageSegment.text("text")])
|
||||
|
||||
assert parse_obj_as(Message, {"type": "text", "data": {"text": "text"}}) == Message(
|
||||
[MessageSegment.text("text")]
|
||||
assert parse_obj_as(FakeMessage, "text") == FakeMessage(
|
||||
[FakeMessageSegment.text("text")]
|
||||
)
|
||||
|
||||
assert parse_obj_as(
|
||||
Message,
|
||||
[MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}],
|
||||
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
|
||||
FakeMessage, {"type": "text", "data": {"text": "text"}}
|
||||
) == FakeMessage([FakeMessageSegment.text("text")])
|
||||
|
||||
assert parse_obj_as(
|
||||
FakeMessage,
|
||||
[FakeMessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}],
|
||||
) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")])
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
parse_obj_as(Message, object())
|
||||
parse_obj_as(FakeMessage, object())
|
||||
|
||||
|
||||
def test_message_contains():
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
message = Message(
|
||||
message = FakeMessage(
|
||||
[
|
||||
MessageSegment.text("test"),
|
||||
MessageSegment.image("test2"),
|
||||
MessageSegment.image("test3"),
|
||||
MessageSegment.text("test4"),
|
||||
FakeMessageSegment.text("test"),
|
||||
FakeMessageSegment.image("test2"),
|
||||
FakeMessageSegment.image("test3"),
|
||||
FakeMessageSegment.text("test4"),
|
||||
]
|
||||
)
|
||||
|
||||
assert message.has(MessageSegment.text("test")) is True
|
||||
assert MessageSegment.text("test") in message
|
||||
assert message.has(FakeMessageSegment.text("test")) is True
|
||||
assert FakeMessageSegment.text("test") in message
|
||||
assert message.has("image") is True
|
||||
assert "image" in message
|
||||
|
||||
assert message.has(MessageSegment.text("foo")) is False
|
||||
assert MessageSegment.text("foo") not in message
|
||||
assert message.has(FakeMessageSegment.text("foo")) is False
|
||||
assert FakeMessageSegment.text("foo") not in message
|
||||
assert message.has("foo") is False
|
||||
assert "foo" not in message
|
||||
|
||||
|
||||
def test_message_only():
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
message = Message(
|
||||
message = FakeMessage(
|
||||
[
|
||||
MessageSegment.text("test"),
|
||||
MessageSegment.text("test2"),
|
||||
FakeMessageSegment.text("test"),
|
||||
FakeMessageSegment.text("test2"),
|
||||
]
|
||||
)
|
||||
|
||||
assert message.only("text") is True
|
||||
assert message.only(MessageSegment.text("test")) is False
|
||||
assert message.only(FakeMessageSegment.text("test")) is False
|
||||
|
||||
message = Message(
|
||||
message = FakeMessage(
|
||||
[
|
||||
MessageSegment.text("test"),
|
||||
MessageSegment.image("test2"),
|
||||
MessageSegment.image("test3"),
|
||||
MessageSegment.text("test4"),
|
||||
FakeMessageSegment.text("test"),
|
||||
FakeMessageSegment.image("test2"),
|
||||
FakeMessageSegment.image("test3"),
|
||||
FakeMessageSegment.text("test4"),
|
||||
]
|
||||
)
|
||||
|
||||
assert message.only("text") is False
|
||||
|
||||
message = Message(
|
||||
message = FakeMessage(
|
||||
[
|
||||
MessageSegment.text("test"),
|
||||
MessageSegment.text("test"),
|
||||
FakeMessageSegment.text("test"),
|
||||
FakeMessageSegment.text("test"),
|
||||
]
|
||||
)
|
||||
|
||||
assert message.only(MessageSegment.text("test")) is True
|
||||
assert message.only(FakeMessageSegment.text("test")) is True
|
||||
|
||||
|
||||
def test_message_join():
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
msg = Message([MessageSegment.text("test")])
|
||||
msg = FakeMessage([FakeMessageSegment.text("test")])
|
||||
iterable = [
|
||||
MessageSegment.text("first"),
|
||||
Message([MessageSegment.text("second"), MessageSegment.text("third")]),
|
||||
FakeMessageSegment.text("first"),
|
||||
FakeMessage(
|
||||
[FakeMessageSegment.text("second"), FakeMessageSegment.text("third")]
|
||||
),
|
||||
]
|
||||
|
||||
assert msg.join(iterable) == Message(
|
||||
assert msg.join(iterable) == FakeMessage(
|
||||
[
|
||||
MessageSegment.text("first"),
|
||||
MessageSegment.text("test"),
|
||||
MessageSegment.text("second"),
|
||||
MessageSegment.text("third"),
|
||||
FakeMessageSegment.text("first"),
|
||||
FakeMessageSegment.text("test"),
|
||||
FakeMessageSegment.text("second"),
|
||||
FakeMessageSegment.text("third"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_message_include():
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
message = Message(
|
||||
message = FakeMessage(
|
||||
[
|
||||
MessageSegment.text("test"),
|
||||
MessageSegment.image("test2"),
|
||||
MessageSegment.image("test3"),
|
||||
MessageSegment.text("test4"),
|
||||
FakeMessageSegment.text("test"),
|
||||
FakeMessageSegment.image("test2"),
|
||||
FakeMessageSegment.image("test3"),
|
||||
FakeMessageSegment.text("test4"),
|
||||
]
|
||||
)
|
||||
|
||||
assert message.include("text") == Message(
|
||||
assert message.include("text") == FakeMessage(
|
||||
[
|
||||
MessageSegment.text("test"),
|
||||
MessageSegment.text("test4"),
|
||||
FakeMessageSegment.text("test"),
|
||||
FakeMessageSegment.text("test4"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_message_exclude():
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
message = Message(
|
||||
message = FakeMessage(
|
||||
[
|
||||
MessageSegment.text("test"),
|
||||
MessageSegment.image("test2"),
|
||||
MessageSegment.image("test3"),
|
||||
MessageSegment.text("test4"),
|
||||
FakeMessageSegment.text("test"),
|
||||
FakeMessageSegment.image("test2"),
|
||||
FakeMessageSegment.image("test3"),
|
||||
FakeMessageSegment.text("test4"),
|
||||
]
|
||||
)
|
||||
|
||||
assert message.exclude("image") == Message(
|
||||
assert message.exclude("image") == FakeMessage(
|
||||
[
|
||||
MessageSegment.text("test"),
|
||||
MessageSegment.text("test4"),
|
||||
FakeMessageSegment.text("test"),
|
||||
FakeMessageSegment.text("test4"),
|
||||
]
|
||||
)
|
||||
|
@@ -1,5 +1,5 @@
|
||||
from nonebot.adapters import MessageTemplate
|
||||
from utils import escape_text, make_fake_message
|
||||
from utils import FakeMessage, FakeMessageSegment, escape_text
|
||||
|
||||
|
||||
def test_template_basis():
|
||||
@@ -9,8 +9,7 @@ def test_template_basis():
|
||||
|
||||
|
||||
def test_template_message():
|
||||
Message = make_fake_message()
|
||||
template = Message.template("{a:custom}{b:text}{c:image}/{d}")
|
||||
template = FakeMessage.template("{a:custom}{b:text}{c:image}/{d}")
|
||||
|
||||
@template.add_format_spec
|
||||
def custom(input: str) -> str:
|
||||
@@ -37,29 +36,24 @@ def test_template_message():
|
||||
|
||||
|
||||
def test_rich_template_message():
|
||||
Message = make_fake_message()
|
||||
MS = Message.get_segment_class()
|
||||
|
||||
pic1, pic2, pic3 = (
|
||||
MS.image("file:///pic1.jpg"),
|
||||
MS.image("file:///pic2.jpg"),
|
||||
MS.image("file:///pic3.jpg"),
|
||||
FakeMessageSegment.image("file:///pic1.jpg"),
|
||||
FakeMessageSegment.image("file:///pic2.jpg"),
|
||||
FakeMessageSegment.image("file:///pic3.jpg"),
|
||||
)
|
||||
|
||||
template = Message.template("{}{}" + pic2 + "{}")
|
||||
template = FakeMessage.template("{}{}" + pic2 + "{}")
|
||||
|
||||
result = template.format(pic1, "[fake:image]", pic3)
|
||||
|
||||
assert result["image"] == Message([pic1, pic2, pic3])
|
||||
assert result["image"] == FakeMessage([pic1, pic2, pic3])
|
||||
assert str(result) == (
|
||||
"[fake:image]" + escape_text("[fake:image]") + "[fake:image]" + "[fake:image]"
|
||||
)
|
||||
|
||||
|
||||
def test_message_injection():
|
||||
Message = make_fake_message()
|
||||
|
||||
template = Message.template("{name}Is Bad")
|
||||
template = FakeMessage.template("{name}Is Bad")
|
||||
message = template.format(name="[fake:image]")
|
||||
|
||||
assert message.extract_plain_text() == escape_text("[fake:image]Is Bad")
|
||||
|
@@ -234,7 +234,7 @@ async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
bot = ctx.create_bot()
|
||||
event = make_fake_event()()
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(event, "test", True, bot)
|
||||
ctx.should_call_send(event, "test", True, bot=bot)
|
||||
|
||||
assert runned, "run_preprocessor should runned"
|
||||
|
||||
@@ -346,7 +346,7 @@ async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
bot = ctx.create_bot()
|
||||
event = make_fake_event()()
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(event, "test", True, bot)
|
||||
ctx.should_call_send(event, "test", True, bot=bot)
|
||||
|
||||
assert runned, "run_postprocessor should runned"
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Any, Set, cast
|
||||
from typing import Any, Set, Optional, cast
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
@@ -79,13 +79,37 @@ async def test_lifespan():
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_reverse_driver(app: App, driver: Driver):
|
||||
async def test_http_server(app: App, driver: Driver):
|
||||
driver = cast(ReverseDriver, driver)
|
||||
|
||||
async def _handle_http(request: Request) -> Response:
|
||||
assert request.content in (b"test", "test")
|
||||
return Response(200, content="test")
|
||||
|
||||
http_setup = HTTPServerSetup(URL("/http_test"), "POST", "http_test", _handle_http)
|
||||
driver.setup_http_server(http_setup)
|
||||
|
||||
async with app.test_server(driver.asgi) as ctx:
|
||||
client = ctx.get_client()
|
||||
response = await client.post("/http_test", data="test")
|
||||
assert response.status_code == 200
|
||||
assert response.text == "test"
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"),
|
||||
pytest.param("nonebot.drivers.quart:Driver", id="quart"),
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_websocket_server(app: App, driver: Driver):
|
||||
driver = cast(ReverseDriver, driver)
|
||||
|
||||
async def _handle_ws(ws: WebSocket) -> None:
|
||||
await ws.accept()
|
||||
data = await ws.receive()
|
||||
@@ -107,17 +131,11 @@ async def test_reverse_driver(app: App, driver: Driver):
|
||||
with pytest.raises(WebSocketClosed):
|
||||
await ws.receive()
|
||||
|
||||
http_setup = HTTPServerSetup(URL("/http_test"), "POST", "http_test", _handle_http)
|
||||
driver.setup_http_server(http_setup)
|
||||
|
||||
ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws)
|
||||
driver.setup_websocket_server(ws_setup)
|
||||
|
||||
async with app.test_server(driver.asgi) as ctx:
|
||||
client = ctx.get_client()
|
||||
response = await client.post("/http_test", data="test")
|
||||
assert response.status_code == 200
|
||||
assert response.text == "test"
|
||||
|
||||
async with client.websocket_connect("/ws_test") as ws:
|
||||
await ws.send_text("ping")
|
||||
@@ -136,6 +154,63 @@ async def test_reverse_driver(app: App, driver: Driver):
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"),
|
||||
pytest.param("nonebot.drivers.quart:Driver", id="quart"),
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_cross_context(app: App, driver: Driver):
|
||||
driver = cast(ReverseDriver, driver)
|
||||
|
||||
ws: Optional[WebSocket] = None
|
||||
ws_ready = asyncio.Event()
|
||||
ws_should_close = asyncio.Event()
|
||||
|
||||
async def background_task():
|
||||
try:
|
||||
await ws_ready.wait()
|
||||
assert ws is not None
|
||||
|
||||
await ws.send("ping")
|
||||
data = await ws.receive()
|
||||
assert data == "pong"
|
||||
finally:
|
||||
ws_should_close.set()
|
||||
|
||||
task = asyncio.create_task(background_task())
|
||||
|
||||
async def _handle_ws(websocket: WebSocket) -> None:
|
||||
nonlocal ws
|
||||
await websocket.accept()
|
||||
ws = websocket
|
||||
ws_ready.set()
|
||||
|
||||
await ws_should_close.wait()
|
||||
await websocket.close()
|
||||
|
||||
ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws)
|
||||
driver.setup_websocket_server(ws_setup)
|
||||
|
||||
async with app.test_server(driver.asgi) as ctx:
|
||||
client = ctx.get_client()
|
||||
|
||||
async with client.websocket_connect("/ws_test") as websocket:
|
||||
try:
|
||||
data = await websocket.receive_text()
|
||||
assert data == "ping"
|
||||
await websocket.send_text("pong")
|
||||
except Exception as e:
|
||||
if not e.args or "websocket.close" not in str(e.args[0]):
|
||||
raise
|
||||
|
||||
await task
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
@@ -145,43 +220,59 @@ async def test_reverse_driver(app: App, driver: Driver):
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_http_driver(driver: Driver):
|
||||
async def test_http_client(driver: Driver, server_url: URL):
|
||||
driver = cast(ForwardDriver, driver)
|
||||
|
||||
# simple post with query, headers, cookies and content
|
||||
request = Request(
|
||||
"POST",
|
||||
"https://httpbin.org/post",
|
||||
server_url,
|
||||
params={"param": "test"},
|
||||
headers={"X-Test": "test"},
|
||||
cookies={"session": "test"},
|
||||
content="test",
|
||||
)
|
||||
response = await driver.request(request)
|
||||
assert response.status_code == 200 and response.content
|
||||
assert response.status_code == 200
|
||||
assert response.content
|
||||
data = json.loads(response.content)
|
||||
assert data["method"] == "POST"
|
||||
assert data["args"] == {"param": "test"}
|
||||
assert data["headers"].get("X-Test") == "test"
|
||||
assert data["headers"].get("Cookie") == "session=test"
|
||||
assert data["data"] == "test"
|
||||
|
||||
request = Request("POST", "https://httpbin.org/post", data={"form": "test"})
|
||||
# post with data body
|
||||
request = Request("POST", server_url, data={"form": "test"})
|
||||
response = await driver.request(request)
|
||||
assert response.status_code == 200 and response.content
|
||||
assert response.status_code == 200
|
||||
assert response.content
|
||||
data = json.loads(response.content)
|
||||
assert data["method"] == "POST"
|
||||
assert data["form"] == {"form": "test"}
|
||||
|
||||
request = Request("POST", "https://httpbin.org/post", json={"json": "test"})
|
||||
# post with json body
|
||||
request = Request("POST", server_url, json={"json": "test"})
|
||||
response = await driver.request(request)
|
||||
assert response.status_code == 200 and response.content
|
||||
assert response.status_code == 200
|
||||
assert response.content
|
||||
data = json.loads(response.content)
|
||||
assert data["method"] == "POST"
|
||||
assert data["json"] == {"json": "test"}
|
||||
|
||||
# post with files and form data
|
||||
request = Request(
|
||||
"POST", "https://httpbin.org/post", files={"test": ("test.txt", b"test")}
|
||||
"POST",
|
||||
server_url,
|
||||
data={"form": "test"},
|
||||
files={"test": ("test.txt", b"test")},
|
||||
)
|
||||
response = await driver.request(request)
|
||||
assert response.status_code == 200 and response.content
|
||||
assert response.status_code == 200
|
||||
assert response.content
|
||||
data = json.loads(response.content)
|
||||
assert data["method"] == "POST"
|
||||
assert data["form"] == {"form": "test"}
|
||||
assert data["files"] == {"test": "test"}
|
||||
|
||||
await asyncio.sleep(1)
|
||||
@@ -189,7 +280,7 @@ async def test_http_driver(driver: Driver):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver, driver_type",
|
||||
("driver", "driver_type"),
|
||||
[
|
||||
pytest.param(
|
||||
"nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin",
|
||||
@@ -232,7 +323,6 @@ async def test_bot_connect_hook(app: App, driver: Driver):
|
||||
@driver.on_bot_connect
|
||||
async def conn_hook(foo: Bot, dep: int = Depends(dependency), default: int = 1):
|
||||
nonlocal conn_should_be_called
|
||||
conn_should_be_called = True
|
||||
|
||||
if foo is not bot:
|
||||
pytest.fail("on_bot_connect hook called with wrong bot")
|
||||
@@ -241,12 +331,13 @@ async def test_bot_connect_hook(app: App, driver: Driver):
|
||||
if default != 1:
|
||||
pytest.fail("on_bot_connect hook called with wrong default value")
|
||||
|
||||
conn_should_be_called = True
|
||||
|
||||
@driver.on_bot_disconnect
|
||||
async def disconn_hook(
|
||||
foo: Bot, dep: int = Depends(dependency), default: int = 1
|
||||
):
|
||||
nonlocal disconn_should_be_called
|
||||
disconn_should_be_called = True
|
||||
|
||||
if foo is not bot:
|
||||
pytest.fail("on_bot_disconnect hook called with wrong bot")
|
||||
@@ -255,6 +346,8 @@ async def test_bot_connect_hook(app: App, driver: Driver):
|
||||
if default != 1:
|
||||
pytest.fail("on_bot_connect hook called with wrong default value")
|
||||
|
||||
disconn_should_be_called = True
|
||||
|
||||
if conn_hook not in {hook.call for hook in conn_hooks}:
|
||||
pytest.fail("on_bot_connect hook not registered")
|
||||
if disconn_hook not in {hook.call for hook in disconn_hooks}:
|
||||
@@ -264,6 +357,7 @@ async def test_bot_connect_hook(app: App, driver: Driver):
|
||||
bot = ctx.create_bot()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if not conn_should_be_called:
|
||||
pytest.fail("on_bot_connect hook not called")
|
||||
if not disconn_should_be_called:
|
||||
|
@@ -20,6 +20,11 @@ async def test_init():
|
||||
assert env == "test"
|
||||
|
||||
config = nonebot.get_driver().config
|
||||
assert config.nickname == {"test"}
|
||||
assert config.superusers == {"test", "fake:faketest"}
|
||||
assert config.api_timeout is None
|
||||
|
||||
assert config.simple_none is None
|
||||
assert config.config_from_env == {"test": "test"}
|
||||
assert config.config_override == "new"
|
||||
assert config.config_from_init == "init"
|
||||
@@ -31,17 +36,29 @@ async def test_init():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
async def test_get_driver(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(nonebot, "_driver", None)
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="initialized"):
|
||||
get_driver()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_asgi(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
driver = get_driver()
|
||||
assert isinstance(driver, ReverseDriver)
|
||||
assert get_asgi() == driver.asgi
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_app(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
driver = get_driver()
|
||||
assert isinstance(driver, ReverseDriver)
|
||||
assert get_app() == driver.server_app
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_adapter(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
async with app.test_api() as ctx:
|
||||
adapter = ctx.create_adapter()
|
||||
adapter_name = adapter.get_name()
|
||||
@@ -51,24 +68,38 @@ async def test_get(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
assert get_adapters() == {adapter_name: adapter}
|
||||
assert get_adapter(adapter_name) is adapter
|
||||
assert get_adapter(adapter.__class__) is adapter
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="registered"):
|
||||
get_adapter("not exist")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
runned = False
|
||||
|
||||
def mock_run(*args, **kwargs):
|
||||
nonlocal runned
|
||||
runned = True
|
||||
assert args == ("arg",) and kwargs == {"kwarg": "kwarg"}
|
||||
assert args == ("arg",)
|
||||
assert kwargs == {"kwarg": "kwarg"}
|
||||
|
||||
monkeypatch.setattr(driver, "run", mock_run)
|
||||
driver = get_driver()
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(driver, "run", mock_run)
|
||||
nonebot.run("arg", kwarg="kwarg")
|
||||
|
||||
assert runned
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_bot(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
driver = get_driver()
|
||||
|
||||
with pytest.raises(ValueError, match="no bots"):
|
||||
get_bot()
|
||||
|
||||
monkeypatch.setattr(driver, "_bots", {"test": "test"})
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(driver, "_bots", {"test": "test"})
|
||||
assert get_bot() == "test"
|
||||
assert get_bot("test") == "test"
|
||||
assert get_bots() == {"test": "test"}
|
||||
|
@@ -3,25 +3,16 @@ from nonebug import App
|
||||
|
||||
from nonebot.permission import User
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
from utils import FakeMessage, make_fake_event
|
||||
from nonebot.message import check_and_run_matcher
|
||||
from utils import make_fake_event, make_fake_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher(app: App):
|
||||
from plugins.matcher.matcher_process import (
|
||||
test_got,
|
||||
test_handle,
|
||||
test_preset,
|
||||
test_combine,
|
||||
test_receive,
|
||||
test_overload,
|
||||
)
|
||||
async def test_matcher_handle(app: App):
|
||||
from plugins.matcher.matcher_process import test_handle
|
||||
|
||||
message = make_fake_message()("text")
|
||||
message = FakeMessage("text")
|
||||
event = make_fake_event(_message=message)()
|
||||
message_next = make_fake_message()("text_next")
|
||||
event_next = make_fake_event(_message=message_next)()
|
||||
|
||||
assert len(test_handle.handlers) == 1
|
||||
async with app.test_matcher(test_handle) as ctx:
|
||||
@@ -30,6 +21,16 @@ async def test_matcher(app: App):
|
||||
ctx.should_call_send(event, "send", "result", at_sender=True)
|
||||
ctx.should_finished()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher_got(app: App):
|
||||
from plugins.matcher.matcher_process import test_got
|
||||
|
||||
message = FakeMessage("text")
|
||||
event = make_fake_event(_message=message)()
|
||||
message_next = FakeMessage("text_next")
|
||||
event_next = make_fake_event(_message=message_next)()
|
||||
|
||||
assert len(test_got.handlers) == 1
|
||||
async with app.test_matcher(test_got) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
@@ -42,6 +43,14 @@ async def test_matcher(app: App):
|
||||
ctx.should_rejected()
|
||||
ctx.receive_event(bot, event_next)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher_receive(app: App):
|
||||
from plugins.matcher.matcher_process import test_receive
|
||||
|
||||
message = FakeMessage("text")
|
||||
event = make_fake_event(_message=message)()
|
||||
|
||||
assert len(test_receive.handlers) == 1
|
||||
async with app.test_matcher(test_receive) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
@@ -51,7 +60,17 @@ async def test_matcher(app: App):
|
||||
ctx.should_call_send(event, "pause", "result", at_sender=True)
|
||||
ctx.should_paused()
|
||||
|
||||
assert len(test_receive.handlers) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher_(app: App):
|
||||
from plugins.matcher.matcher_process import test_combine
|
||||
|
||||
message = FakeMessage("text")
|
||||
event = make_fake_event(_message=message)()
|
||||
message_next = FakeMessage("text_next")
|
||||
event_next = make_fake_event(_message=message_next)()
|
||||
|
||||
assert len(test_combine.handlers) == 1
|
||||
async with app.test_matcher(test_combine) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
ctx.receive_event(bot, event)
|
||||
@@ -64,6 +83,16 @@ async def test_matcher(app: App):
|
||||
ctx.should_rejected()
|
||||
ctx.receive_event(bot, event_next)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher_preset(app: App):
|
||||
from plugins.matcher.matcher_process import test_preset
|
||||
|
||||
message = FakeMessage("text")
|
||||
event = make_fake_event(_message=message)()
|
||||
message_next = FakeMessage("text_next")
|
||||
event_next = make_fake_event(_message=message_next)()
|
||||
|
||||
assert len(test_preset.handlers) == 2
|
||||
async with app.test_matcher(test_preset) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
@@ -72,6 +101,14 @@ async def test_matcher(app: App):
|
||||
ctx.should_rejected()
|
||||
ctx.receive_event(bot, event_next)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher_overload(app: App):
|
||||
from plugins.matcher.matcher_process import test_overload
|
||||
|
||||
message = FakeMessage("text")
|
||||
event = make_fake_event(_message=message)()
|
||||
|
||||
assert len(test_overload.handlers) == 2
|
||||
async with app.test_matcher(test_overload) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
@@ -83,7 +120,7 @@ async def test_matcher(app: App):
|
||||
async def test_matcher_destroy(app: App):
|
||||
from plugins.matcher.matcher_process import test_destroy
|
||||
|
||||
async with app.test_matcher(test_destroy) as ctx:
|
||||
async with app.test_matcher(test_destroy):
|
||||
assert len(matchers) == 1
|
||||
assert len(matchers[test_destroy.priority]) == 1
|
||||
assert matchers[test_destroy.priority][0] is test_destroy
|
||||
@@ -115,12 +152,10 @@ async def test_type_updater(app: App):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permission_updater(app: App):
|
||||
async def test_default_permission_updater(app: App):
|
||||
from plugins.matcher.matcher_permission import (
|
||||
default_permission,
|
||||
test_custom_updater,
|
||||
test_permission_updater,
|
||||
test_user_permission_updater,
|
||||
)
|
||||
|
||||
event = make_fake_event(_session_id="test")()
|
||||
@@ -136,6 +171,15 @@ async def test_permission_updater(app: App):
|
||||
assert checker.users == ("test",)
|
||||
assert checker.perm is default_permission
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_permission_updater(app: App):
|
||||
from plugins.matcher.matcher_permission import (
|
||||
default_permission,
|
||||
test_user_permission_updater,
|
||||
)
|
||||
|
||||
event = make_fake_event(_session_id="test")()
|
||||
user_permission = list(test_user_permission_updater.permission.checkers)[0].call
|
||||
assert isinstance(user_permission, User)
|
||||
assert user_permission.perm is default_permission
|
||||
@@ -149,12 +193,22 @@ async def test_permission_updater(app: App):
|
||||
assert checker.users == ("test",)
|
||||
assert checker.perm is default_permission
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_permission_updater(app: App):
|
||||
from plugins.matcher.matcher_permission import (
|
||||
new_permission,
|
||||
default_permission,
|
||||
test_custom_updater,
|
||||
)
|
||||
|
||||
event = make_fake_event(_session_id="test")()
|
||||
assert test_custom_updater.permission is default_permission
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
matcher = test_custom_updater()
|
||||
new_perm = await matcher.update_permission(bot, event)
|
||||
assert new_perm is default_permission
|
||||
assert new_perm is new_permission
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -189,12 +243,8 @@ async def test_run(app: App):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expire(app: App):
|
||||
from plugins.matcher.matcher_expire import (
|
||||
test_temp_matcher,
|
||||
test_datetime_matcher,
|
||||
test_timedelta_matcher,
|
||||
)
|
||||
async def test_temp(app: App):
|
||||
from plugins.matcher.matcher_expire import test_temp_matcher
|
||||
|
||||
event = make_fake_event(_type="test")()
|
||||
async with app.test_api() as ctx:
|
||||
@@ -203,6 +253,11 @@ async def test_expire(app: App):
|
||||
await check_and_run_matcher(test_temp_matcher, bot, event, {})
|
||||
assert test_temp_matcher not in matchers[test_temp_matcher.priority]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_datetime_expire(app: App):
|
||||
from plugins.matcher.matcher_expire import test_datetime_matcher
|
||||
|
||||
event = make_fake_event()()
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
@@ -210,6 +265,11 @@ async def test_expire(app: App):
|
||||
await check_and_run_matcher(test_datetime_matcher, bot, event, {})
|
||||
assert test_datetime_matcher not in matchers[test_datetime_matcher.priority]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timedelta_expire(app: App):
|
||||
from plugins.matcher.matcher_expire import test_timedelta_matcher
|
||||
|
||||
event = make_fake_event()()
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
|
@@ -6,7 +6,7 @@ from nonebug import App
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.exception import TypeMisMatch
|
||||
from utils import make_fake_event, make_fake_message
|
||||
from utils import FakeMessage, make_fake_event
|
||||
from nonebot.params import (
|
||||
ArgParam,
|
||||
BotParam,
|
||||
@@ -33,6 +33,8 @@ from nonebot.consts import (
|
||||
CMD_WHITESPACE_KEY,
|
||||
)
|
||||
|
||||
UNKNOWN_PARAM = "Unknown parameter"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_depend(app: App):
|
||||
@@ -50,7 +52,8 @@ async def test_depend(app: App):
|
||||
async with app.test_dependent(depends, allow_types=[DependParam]) as ctx:
|
||||
ctx.should_return(1)
|
||||
|
||||
assert len(runned) == 1 and runned[0] == 1
|
||||
assert len(runned) == 1
|
||||
assert runned[0] == 1
|
||||
|
||||
runned.clear()
|
||||
|
||||
@@ -59,7 +62,8 @@ async def test_depend(app: App):
|
||||
event_next = make_fake_event()()
|
||||
ctx.receive_event(bot, event_next)
|
||||
|
||||
assert len(runned) == 2 and runned[0] == runned[1] == 1
|
||||
assert len(runned) == 2
|
||||
assert runned[0] == runned[1] == 1
|
||||
|
||||
runned.clear()
|
||||
|
||||
@@ -90,7 +94,9 @@ async def test_bot(app: App):
|
||||
sub_bot,
|
||||
union_bot,
|
||||
legacy_bot,
|
||||
generic_bot,
|
||||
not_legacy_bot,
|
||||
generic_bot_none,
|
||||
)
|
||||
|
||||
async with app.test_dependent(get_bot, allow_types=[BotParam]) as ctx:
|
||||
@@ -103,16 +109,15 @@ async def test_bot(app: App):
|
||||
ctx.pass_params(bot=bot)
|
||||
ctx.should_return(bot)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
async with app.test_dependent(not_legacy_bot, allow_types=[BotParam]) as ctx:
|
||||
...
|
||||
with pytest.raises(ValueError, match=UNKNOWN_PARAM):
|
||||
app.test_dependent(not_legacy_bot, allow_types=[BotParam])
|
||||
|
||||
async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx:
|
||||
bot = ctx.create_bot(base=FooBot)
|
||||
ctx.pass_params(bot=bot)
|
||||
ctx.should_return(bot)
|
||||
|
||||
with pytest.raises(TypeMisMatch):
|
||||
with pytest.raises(TypeMisMatch): # noqa: PT012
|
||||
async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
ctx.pass_params(bot=bot)
|
||||
@@ -122,9 +127,18 @@ async def test_bot(app: App):
|
||||
ctx.pass_params(bot=bot)
|
||||
ctx.should_return(bot)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
async with app.test_dependent(not_bot, allow_types=[BotParam]) as ctx:
|
||||
...
|
||||
async with app.test_dependent(generic_bot, allow_types=[BotParam]) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
ctx.pass_params(bot=bot)
|
||||
ctx.should_return(bot)
|
||||
|
||||
async with app.test_dependent(generic_bot_none, allow_types=[BotParam]) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
ctx.pass_params(bot=bot)
|
||||
ctx.should_return(bot)
|
||||
|
||||
with pytest.raises(ValueError, match=UNKNOWN_PARAM):
|
||||
app.test_dependent(not_bot, allow_types=[BotParam])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -139,11 +153,13 @@ async def test_event(app: App):
|
||||
union_event,
|
||||
legacy_event,
|
||||
event_message,
|
||||
generic_event,
|
||||
event_plain_text,
|
||||
not_legacy_event,
|
||||
generic_event_none,
|
||||
)
|
||||
|
||||
fake_message = make_fake_message()("text")
|
||||
fake_message = FakeMessage("text")
|
||||
fake_event = make_fake_event(_message=fake_message)()
|
||||
fake_fooevent = make_fake_event(_base=FooEvent)()
|
||||
|
||||
@@ -155,17 +171,14 @@ async def test_event(app: App):
|
||||
ctx.pass_params(event=fake_event)
|
||||
ctx.should_return(fake_event)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
async with app.test_dependent(
|
||||
not_legacy_event, allow_types=[EventParam]
|
||||
) as ctx:
|
||||
...
|
||||
with pytest.raises(ValueError, match=UNKNOWN_PARAM):
|
||||
app.test_dependent(not_legacy_event, allow_types=[EventParam])
|
||||
|
||||
async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx:
|
||||
ctx.pass_params(event=fake_fooevent)
|
||||
ctx.should_return(fake_fooevent)
|
||||
|
||||
with pytest.raises(TypeMisMatch):
|
||||
with pytest.raises(TypeMisMatch): # noqa: PT012
|
||||
async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx:
|
||||
ctx.pass_params(event=fake_event)
|
||||
|
||||
@@ -173,9 +186,16 @@ async def test_event(app: App):
|
||||
ctx.pass_params(event=fake_fooevent)
|
||||
ctx.should_return(fake_event)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
async with app.test_dependent(not_event, allow_types=[EventParam]) as ctx:
|
||||
...
|
||||
async with app.test_dependent(generic_event, allow_types=[EventParam]) as ctx:
|
||||
ctx.pass_params(event=fake_event)
|
||||
ctx.should_return(fake_event)
|
||||
|
||||
async with app.test_dependent(generic_event_none, allow_types=[EventParam]) as ctx:
|
||||
ctx.pass_params(event=fake_event)
|
||||
ctx.should_return(fake_event)
|
||||
|
||||
with pytest.raises(ValueError, match=UNKNOWN_PARAM):
|
||||
app.test_dependent(not_event, allow_types=[EventParam])
|
||||
|
||||
async with app.test_dependent(
|
||||
event_type, allow_types=[EventParam, DependParam]
|
||||
@@ -225,7 +245,7 @@ async def test_state(app: App):
|
||||
shell_command_argv,
|
||||
)
|
||||
|
||||
fake_message = make_fake_message()("text")
|
||||
fake_message = FakeMessage("text")
|
||||
fake_matched = re.match(r"\[cq:(?P<type>.*?),(?P<arg>.*?)\]", "[cq:test,arg=value]")
|
||||
fake_state = {
|
||||
PREFIX_KEY: {
|
||||
@@ -252,11 +272,8 @@ async def test_state(app: App):
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
async with app.test_dependent(
|
||||
not_legacy_state, allow_types=[StateParam]
|
||||
) as ctx:
|
||||
...
|
||||
with pytest.raises(ValueError, match=UNKNOWN_PARAM):
|
||||
app.test_dependent(not_legacy_state, allow_types=[StateParam])
|
||||
|
||||
async with app.test_dependent(
|
||||
command, allow_types=[StateParam, DependParam]
|
||||
@@ -351,14 +368,59 @@ async def test_state(app: App):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher(app: App):
|
||||
from plugins.param.param_matcher import matcher, receive, last_receive
|
||||
from plugins.param.param_matcher import (
|
||||
FooMatcher,
|
||||
matcher,
|
||||
receive,
|
||||
not_matcher,
|
||||
sub_matcher,
|
||||
last_receive,
|
||||
union_matcher,
|
||||
legacy_matcher,
|
||||
generic_matcher,
|
||||
not_legacy_matcher,
|
||||
generic_matcher_none,
|
||||
)
|
||||
|
||||
fake_matcher = Matcher()
|
||||
foo_matcher = FooMatcher()
|
||||
|
||||
async with app.test_dependent(matcher, allow_types=[MatcherParam]) as ctx:
|
||||
ctx.pass_params(matcher=fake_matcher)
|
||||
ctx.should_return(fake_matcher)
|
||||
|
||||
async with app.test_dependent(legacy_matcher, allow_types=[MatcherParam]) as ctx:
|
||||
ctx.pass_params(matcher=fake_matcher)
|
||||
ctx.should_return(fake_matcher)
|
||||
|
||||
with pytest.raises(ValueError, match=UNKNOWN_PARAM):
|
||||
app.test_dependent(not_legacy_matcher, allow_types=[MatcherParam])
|
||||
|
||||
async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx:
|
||||
ctx.pass_params(matcher=foo_matcher)
|
||||
ctx.should_return(foo_matcher)
|
||||
|
||||
with pytest.raises(TypeMisMatch): # noqa: PT012
|
||||
async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx:
|
||||
ctx.pass_params(matcher=fake_matcher)
|
||||
|
||||
async with app.test_dependent(union_matcher, allow_types=[MatcherParam]) as ctx:
|
||||
ctx.pass_params(matcher=foo_matcher)
|
||||
ctx.should_return(foo_matcher)
|
||||
|
||||
async with app.test_dependent(generic_matcher, allow_types=[MatcherParam]) as ctx:
|
||||
ctx.pass_params(matcher=fake_matcher)
|
||||
ctx.should_return(fake_matcher)
|
||||
|
||||
async with app.test_dependent(
|
||||
generic_matcher_none, allow_types=[MatcherParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(matcher=fake_matcher)
|
||||
ctx.should_return(fake_matcher)
|
||||
|
||||
with pytest.raises(ValueError, match=UNKNOWN_PARAM):
|
||||
app.test_dependent(not_matcher, allow_types=[MatcherParam])
|
||||
|
||||
event = make_fake_event()()
|
||||
fake_matcher.set_receive("test", event)
|
||||
event_next = make_fake_event()()
|
||||
@@ -379,10 +441,17 @@ async def test_matcher(app: App):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_arg(app: App):
|
||||
from plugins.param.param_arg import arg, arg_str, arg_plain_text
|
||||
from plugins.param.param_arg import (
|
||||
arg,
|
||||
arg_str,
|
||||
annotated_arg,
|
||||
arg_plain_text,
|
||||
annotated_arg_str,
|
||||
annotated_arg_plain_text,
|
||||
)
|
||||
|
||||
matcher = Matcher()
|
||||
message = make_fake_message()("text")
|
||||
message = FakeMessage("text")
|
||||
matcher.set_arg("key", message)
|
||||
|
||||
async with app.test_dependent(arg, allow_types=[ArgParam]) as ctx:
|
||||
@@ -397,6 +466,20 @@ async def test_arg(app: App):
|
||||
ctx.pass_params(matcher=matcher)
|
||||
ctx.should_return(message.extract_plain_text())
|
||||
|
||||
async with app.test_dependent(annotated_arg, allow_types=[ArgParam]) as ctx:
|
||||
ctx.pass_params(matcher=matcher)
|
||||
ctx.should_return(message)
|
||||
|
||||
async with app.test_dependent(annotated_arg_str, allow_types=[ArgParam]) as ctx:
|
||||
ctx.pass_params(matcher=matcher)
|
||||
ctx.should_return(str(message))
|
||||
|
||||
async with app.test_dependent(
|
||||
annotated_arg_plain_text, allow_types=[ArgParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(matcher=matcher)
|
||||
ctx.should_return(message.extract_plain_text())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exception(app: App):
|
||||
|
@@ -47,15 +47,15 @@ async def test_permission(app: App):
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert await Permission(falsy)(bot, event) == False
|
||||
assert await Permission(truthy)(bot, event) == True
|
||||
assert await Permission(skipped)(bot, event) == False
|
||||
assert await Permission(truthy, falsy)(bot, event) == True
|
||||
assert await Permission(truthy, skipped)(bot, event) == True
|
||||
assert await Permission(falsy)(bot, event) is False
|
||||
assert await Permission(truthy)(bot, event) is True
|
||||
assert await Permission(skipped)(bot, event) is False
|
||||
assert await Permission(truthy, falsy)(bot, event) is True
|
||||
assert await Permission(truthy, skipped)(bot, event) is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("type, expected", [("message", True), ("notice", False)])
|
||||
@pytest.mark.parametrize(("type", "expected"), [("message", True), ("notice", False)])
|
||||
async def test_message(type: str, expected: bool):
|
||||
dependent = list(MESSAGE.checkers)[0]
|
||||
checker = dependent.call
|
||||
@@ -67,7 +67,7 @@ async def test_message(type: str, expected: bool):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("type, expected", [("message", False), ("notice", True)])
|
||||
@pytest.mark.parametrize(("type", "expected"), [("message", False), ("notice", True)])
|
||||
async def test_notice(type: str, expected: bool):
|
||||
dependent = list(NOTICE.checkers)[0]
|
||||
checker = dependent.call
|
||||
@@ -79,7 +79,7 @@ async def test_notice(type: str, expected: bool):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("type, expected", [("message", False), ("request", True)])
|
||||
@pytest.mark.parametrize(("type", "expected"), [("message", False), ("request", True)])
|
||||
async def test_request(type: str, expected: bool):
|
||||
dependent = list(REQUEST.checkers)[0]
|
||||
checker = dependent.call
|
||||
@@ -91,7 +91,9 @@ async def test_request(type: str, expected: bool):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("type, expected", [("message", False), ("meta_event", True)])
|
||||
@pytest.mark.parametrize(
|
||||
("type", "expected"), [("message", False), ("meta_event", True)]
|
||||
)
|
||||
async def test_metaevent(type: str, expected: bool):
|
||||
dependent = list(METAEVENT.checkers)[0]
|
||||
checker = dependent.call
|
||||
@@ -104,7 +106,7 @@ async def test_metaevent(type: str, expected: bool):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"type, user_id, expected",
|
||||
("type", "user_id", "expected"),
|
||||
[
|
||||
("message", "test", True),
|
||||
("message", "foo", False),
|
||||
@@ -128,7 +130,7 @@ async def test_superuser(app: App, type: str, user_id: str, expected: bool):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"session_ids, session_id, expected",
|
||||
("session_ids", "session_id", "expected"),
|
||||
[
|
||||
(("user", "foo"), "user", True),
|
||||
(("user", "foo"), "bar", False),
|
||||
|
@@ -6,7 +6,7 @@ from dataclasses import asdict
|
||||
import pytest
|
||||
|
||||
import nonebot
|
||||
from nonebot.plugin import Plugin, PluginManager, _managers
|
||||
from nonebot.plugin import Plugin, PluginManager, _managers, inherit_supported_adapters
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -49,7 +49,9 @@ async def test_load_nested_plugin():
|
||||
parent_plugin = nonebot.get_plugin("nested")
|
||||
sub_plugin = nonebot.get_plugin("nested_subplugin")
|
||||
sub_plugin2 = nonebot.get_plugin("nested_subplugin2")
|
||||
assert parent_plugin and sub_plugin and sub_plugin2
|
||||
assert parent_plugin
|
||||
assert sub_plugin
|
||||
assert sub_plugin2
|
||||
assert sub_plugin.parent_plugin is parent_plugin
|
||||
assert sub_plugin2.parent_plugin is parent_plugin
|
||||
assert parent_plugin.sub_plugins == {sub_plugin, sub_plugin2}
|
||||
@@ -67,7 +69,7 @@ async def test_load_json():
|
||||
async def test_load_toml():
|
||||
nonebot.load_from_toml("./plugins.toml")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="Cannot find"):
|
||||
nonebot.load_from_toml("./plugins.empty.toml")
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
@@ -145,3 +147,35 @@ async def test_plugin_metadata():
|
||||
}
|
||||
|
||||
assert plugin.metadata.get_supported_adapters() == {FakeAdapter}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_inherit_supported_adapters():
|
||||
with pytest.raises(RuntimeError):
|
||||
inherit_supported_adapters("some_plugin_not_exist")
|
||||
|
||||
with pytest.raises(ValueError, match="has no metadata!"):
|
||||
inherit_supported_adapters("export")
|
||||
|
||||
echo = nonebot.get_plugin("echo")
|
||||
assert echo
|
||||
assert echo.metadata
|
||||
assert inherit_supported_adapters("echo") is None
|
||||
|
||||
plugin_1 = nonebot.get_plugin("metadata")
|
||||
assert plugin_1
|
||||
assert plugin_1.metadata
|
||||
assert inherit_supported_adapters("metadata") == {
|
||||
"nonebot.adapters.onebot.v11",
|
||||
"plugins.metadata:FakeAdapter",
|
||||
}
|
||||
|
||||
plugin_2 = nonebot.get_plugin("metadata_2")
|
||||
assert plugin_2
|
||||
assert plugin_2.metadata
|
||||
assert inherit_supported_adapters("metadata", "metadata_2") == {
|
||||
"nonebot.adapters.onebot.v11"
|
||||
}
|
||||
assert inherit_supported_adapters("metadata", "echo", "metadata_2") == {
|
||||
"nonebot.adapters.onebot.v11"
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ from nonebot.rule import (
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"matcher_name, pre_rule_factory, has_permission",
|
||||
("matcher_name", "pre_rule_factory", "has_permission"),
|
||||
[
|
||||
pytest.param("matcher_on", None, True),
|
||||
pytest.param("matcher_on_metaevent", None, False),
|
||||
@@ -41,10 +41,30 @@ from nonebot.rule import (
|
||||
),
|
||||
pytest.param("matcher_on_regex", lambda e: RegexRule("test"), True),
|
||||
pytest.param("matcher_on_type", lambda e: IsTypeRule(e), True),
|
||||
pytest.param("matcher_sub_cmd", lambda e: CommandRule([("test", "sub")]), True),
|
||||
pytest.param(
|
||||
"matcher_sub_shell_cmd",
|
||||
lambda e: ShellCommandRule([("test", "sub")], None),
|
||||
"matcher_prefix_cmd",
|
||||
lambda e: CommandRule([("prefix", "sub"), ("help",), ("help", "foo")]),
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"matcher_prefix_shell_cmd",
|
||||
lambda e: ShellCommandRule(
|
||||
[("prefix", "sub"), ("help",), ("help", "foo")], None
|
||||
),
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"matcher_prefix_aliases_cmd",
|
||||
lambda e: CommandRule(
|
||||
[("prefix", "sub"), ("prefix", "help"), ("prefix", "help", "foo")]
|
||||
),
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"matcher_prefix_aliases_shell_cmd",
|
||||
lambda e: ShellCommandRule(
|
||||
[("prefix", "sub"), ("prefix", "help"), ("prefix", "help", "foo")], None
|
||||
),
|
||||
True,
|
||||
),
|
||||
pytest.param("matcher_group_on", None, True),
|
||||
|
@@ -6,8 +6,8 @@ import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot.typing import T_State
|
||||
from utils import make_fake_event, make_fake_message
|
||||
from nonebot.exception import ParserExit, SkippedException
|
||||
from utils import FakeMessage, FakeMessageSegment, make_fake_event
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
PREFIX_KEY,
|
||||
@@ -74,35 +74,32 @@ async def test_rule(app: App):
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert await Rule(falsy)(bot, event, {}) == False
|
||||
assert await Rule(truthy)(bot, event, {}) == True
|
||||
assert await Rule(skipped)(bot, event, {}) == False
|
||||
assert await Rule(truthy, falsy)(bot, event, {}) == False
|
||||
assert await Rule(truthy, skipped)(bot, event, {}) == False
|
||||
assert await Rule(falsy)(bot, event, {}) is False
|
||||
assert await Rule(truthy)(bot, event, {}) is True
|
||||
assert await Rule(skipped)(bot, event, {}) is False
|
||||
assert await Rule(truthy, falsy)(bot, event, {}) is False
|
||||
assert await Rule(truthy, skipped)(bot, event, {}) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trie(app: App):
|
||||
TrieRule.add_prefix("/fake-prefix", TRIE_VALUE("/", ("fake-prefix",)))
|
||||
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
message = Message("/fake-prefix some args")
|
||||
message = FakeMessage("/fake-prefix some args")
|
||||
event = make_fake_event(_message=message)()
|
||||
state = {}
|
||||
TrieRule.get_value(bot, event, state)
|
||||
assert state[PREFIX_KEY] == CMD_RESULT(
|
||||
command=("fake-prefix",),
|
||||
raw_command="/fake-prefix",
|
||||
command_arg=Message("some args"),
|
||||
command_arg=FakeMessage("some args"),
|
||||
command_start="/",
|
||||
command_whitespace=" ",
|
||||
)
|
||||
|
||||
message = MessageSegment.text("/fake-prefix ") + MessageSegment.image(
|
||||
message = FakeMessageSegment.text("/fake-prefix ") + FakeMessageSegment.image(
|
||||
"fake url"
|
||||
)
|
||||
event = make_fake_event(_message=message)()
|
||||
@@ -111,7 +108,7 @@ async def test_trie(app: App):
|
||||
assert state[PREFIX_KEY] == CMD_RESULT(
|
||||
command=("fake-prefix",),
|
||||
raw_command="/fake-prefix",
|
||||
command_arg=Message(MessageSegment.image("fake url")),
|
||||
command_arg=FakeMessage(FakeMessageSegment.image("fake url")),
|
||||
command_start="/",
|
||||
command_whitespace=" ",
|
||||
)
|
||||
@@ -121,7 +118,7 @@ async def test_trie(app: App):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"msg, ignorecase, type, text, expected",
|
||||
("msg", "ignorecase", "type", "text", "expected"),
|
||||
[
|
||||
("prefix", False, "message", "prefix_", True),
|
||||
("prefix", False, "message", "Prefix_", False),
|
||||
@@ -152,7 +149,7 @@ async def test_startswith(
|
||||
assert checker.msg == msg
|
||||
assert checker.ignorecase == ignorecase
|
||||
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
message = text if text is None else FakeMessage(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
for prefix in msg:
|
||||
state = {STARTSWITH_KEY: prefix}
|
||||
@@ -161,7 +158,7 @@ async def test_startswith(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"msg, ignorecase, type, text, expected",
|
||||
("msg", "ignorecase", "type", "text", "expected"),
|
||||
[
|
||||
("suffix", False, "message", "_suffix", True),
|
||||
("suffix", False, "message", "_Suffix", False),
|
||||
@@ -192,7 +189,7 @@ async def test_endswith(
|
||||
assert checker.msg == msg
|
||||
assert checker.ignorecase == ignorecase
|
||||
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
message = text if text is None else FakeMessage(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
for suffix in msg:
|
||||
state = {ENDSWITH_KEY: suffix}
|
||||
@@ -201,7 +198,7 @@ async def test_endswith(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"msg, ignorecase, type, text, expected",
|
||||
("msg", "ignorecase", "type", "text", "expected"),
|
||||
[
|
||||
("fullmatch", False, "message", "fullmatch", True),
|
||||
("fullmatch", False, "message", "Fullmatch", False),
|
||||
@@ -232,7 +229,7 @@ async def test_fullmatch(
|
||||
assert checker.msg == msg
|
||||
assert checker.ignorecase == ignorecase
|
||||
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
message = text if text is None else FakeMessage(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
for full in msg:
|
||||
state = {FULLMATCH_KEY: full}
|
||||
@@ -241,7 +238,7 @@ async def test_fullmatch(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"kws, type, text, expected",
|
||||
("kws", "type", "text", "expected"),
|
||||
[
|
||||
(("key",), "message", "_key_", True),
|
||||
(("key", "foo"), "message", "_foo_", True),
|
||||
@@ -264,7 +261,7 @@ async def test_keyword(
|
||||
assert isinstance(checker, KeywordsRule)
|
||||
assert checker.keywords == kws
|
||||
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
message = text if text is None else FakeMessage(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
for kw in kws:
|
||||
state = {KEYWORD_KEY: kw}
|
||||
@@ -273,26 +270,26 @@ async def test_keyword(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"cmds, force_whitespace, cmd, whitespace, arg_text, expected",
|
||||
("cmds", "force_whitespace", "cmd", "whitespace", "arg_text", "expected"),
|
||||
[
|
||||
# command tests
|
||||
[(("help",),), None, ("help",), None, None, True],
|
||||
[(("help",),), None, ("foo",), None, None, False],
|
||||
[(("help", "foo"),), None, ("help", "foo"), None, None, True],
|
||||
[(("help", "foo"),), None, ("help", "bar"), None, None, False],
|
||||
[(("help",), ("foo",)), None, ("help",), None, None, True],
|
||||
[(("help",), ("foo",)), None, ("bar",), None, None, False],
|
||||
((("help",),), None, ("help",), None, None, True),
|
||||
((("help",),), None, ("foo",), None, None, False),
|
||||
((("help", "foo"),), None, ("help", "foo"), None, None, True),
|
||||
((("help", "foo"),), None, ("help", "bar"), None, None, False),
|
||||
((("help",), ("foo",)), None, ("help",), None, None, True),
|
||||
((("help",), ("foo",)), None, ("bar",), None, None, False),
|
||||
# whitespace tests
|
||||
[(("help",),), True, ("help",), " ", "arg", True],
|
||||
[(("help",),), True, ("help",), None, "arg", False],
|
||||
[(("help",),), True, ("help",), None, None, True],
|
||||
[(("help",),), False, ("help",), " ", "arg", False],
|
||||
[(("help",),), False, ("help",), None, "arg", True],
|
||||
[(("help",),), False, ("help",), None, None, True],
|
||||
[(("help",),), " ", ("help",), " ", "arg", True],
|
||||
[(("help",),), " ", ("help",), "\n", "arg", False],
|
||||
[(("help",),), " ", ("help",), None, "arg", False],
|
||||
[(("help",),), " ", ("help",), None, None, True],
|
||||
((("help",),), True, ("help",), " ", "arg", True),
|
||||
((("help",),), True, ("help",), None, "arg", False),
|
||||
((("help",),), True, ("help",), None, None, True),
|
||||
((("help",),), False, ("help",), " ", "arg", False),
|
||||
((("help",),), False, ("help",), None, "arg", True),
|
||||
((("help",),), False, ("help",), None, None, True),
|
||||
((("help",),), " ", ("help",), " ", "arg", True),
|
||||
((("help",),), " ", ("help",), "\n", "arg", False),
|
||||
((("help",),), " ", ("help",), None, "arg", False),
|
||||
((("help",),), " ", ("help",), None, None, True),
|
||||
],
|
||||
)
|
||||
async def test_command(
|
||||
@@ -310,7 +307,7 @@ async def test_command(
|
||||
assert isinstance(checker, CommandRule)
|
||||
assert checker.cmds == cmds
|
||||
|
||||
arg = arg_text if arg_text is None else make_fake_message()(arg_text)
|
||||
arg = arg_text if arg_text is None else FakeMessage(arg_text)
|
||||
state = {
|
||||
PREFIX_KEY: {CMD_KEY: cmd, CMD_WHITESPACE_KEY: whitespace, CMD_ARG_KEY: arg}
|
||||
}
|
||||
@@ -321,7 +318,7 @@ async def test_command(
|
||||
async def test_shell_command():
|
||||
state: T_State
|
||||
CMD = ("test",)
|
||||
Message = make_fake_message()
|
||||
Message = FakeMessage
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
test_not_cmd = shell_command(CMD)
|
||||
@@ -427,7 +424,7 @@ async def test_shell_command():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"pattern, type, text, expected, matched",
|
||||
("pattern", "type", "text", "expected", "matched"),
|
||||
[
|
||||
(
|
||||
r"(?P<key>key\d)",
|
||||
@@ -455,7 +452,7 @@ async def test_regex(
|
||||
assert isinstance(checker, RegexRule)
|
||||
assert checker.regex == pattern
|
||||
|
||||
message = text if text is None else make_fake_message()(text)
|
||||
message = text if text is None else FakeMessage(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
state = {}
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
|
@@ -16,21 +16,21 @@ async def test_matcher_mutex():
|
||||
event_3 = make_fake_event(_session_id=None)()
|
||||
|
||||
async with am(event) as ctx:
|
||||
assert ctx == False
|
||||
assert ctx is False
|
||||
assert not _running_matcher
|
||||
|
||||
async with am(event) as ctx:
|
||||
async with am(event_1) as ctx_1:
|
||||
assert ctx == False
|
||||
assert ctx_1 == True
|
||||
assert ctx is False
|
||||
assert ctx_1 is True
|
||||
assert not _running_matcher
|
||||
|
||||
async with am(event) as ctx:
|
||||
async with am(event_2) as ctx_2:
|
||||
assert ctx == False
|
||||
assert ctx_2 == False
|
||||
assert ctx is False
|
||||
assert ctx_2 is False
|
||||
assert not _running_matcher
|
||||
|
||||
async with am(event_3) as ctx_3:
|
||||
assert ctx_3 == False
|
||||
assert ctx_3 is False
|
||||
assert not _running_matcher
|
||||
|
@@ -1,18 +1,126 @@
|
||||
import json
|
||||
from typing import Dict, List, Union, TypeVar
|
||||
|
||||
from utils import make_fake_message
|
||||
from nonebot.utils import DataclassEncoder
|
||||
from utils import FakeMessage, FakeMessageSegment
|
||||
from nonebot.utils import (
|
||||
DataclassEncoder,
|
||||
escape_tag,
|
||||
is_gen_callable,
|
||||
is_async_gen_callable,
|
||||
is_coroutine_callable,
|
||||
generic_check_issubclass,
|
||||
)
|
||||
|
||||
|
||||
def test_loguru_escape_tag():
|
||||
assert escape_tag("<red>red</red>") == r"\<red>red\</red>"
|
||||
assert escape_tag("<fg #fff>white</fg #fff>") == r"\<fg #fff>white\</fg #fff>"
|
||||
assert escape_tag("<fg\n#fff>white</fg\n#fff>") == "\\<fg\n#fff>white\\</fg\n#fff>"
|
||||
assert escape_tag("<bg #fff>white</bg #fff>") == r"\<bg #fff>white\</bg #fff>"
|
||||
assert escape_tag("<bg\n#fff>white</bg\n#fff>") == "\\<bg\n#fff>white\\</bg\n#fff>"
|
||||
|
||||
|
||||
def test_generic_check_issubclass():
|
||||
assert generic_check_issubclass(int, (int, float))
|
||||
assert not generic_check_issubclass(str, (int, float))
|
||||
assert generic_check_issubclass(Union[int, float, None], (int, float))
|
||||
assert generic_check_issubclass(List[int], list)
|
||||
assert generic_check_issubclass(Dict[str, int], dict)
|
||||
assert generic_check_issubclass(TypeVar("T", int, float), (int, float))
|
||||
assert generic_check_issubclass(TypeVar("T", bound=int), (int, float))
|
||||
|
||||
|
||||
def test_is_coroutine_callable():
|
||||
async def test1():
|
||||
...
|
||||
|
||||
def test2():
|
||||
...
|
||||
|
||||
class TestClass1:
|
||||
async def __call__(self):
|
||||
...
|
||||
|
||||
class TestClass2:
|
||||
def __call__(self):
|
||||
...
|
||||
|
||||
assert is_coroutine_callable(test1)
|
||||
assert not is_coroutine_callable(test2)
|
||||
assert not is_coroutine_callable(TestClass1)
|
||||
assert is_coroutine_callable(TestClass1())
|
||||
assert not is_coroutine_callable(TestClass2)
|
||||
|
||||
|
||||
def test_is_gen_callable():
|
||||
def test1():
|
||||
yield
|
||||
|
||||
async def test2():
|
||||
yield
|
||||
|
||||
def test3():
|
||||
...
|
||||
|
||||
class TestClass1:
|
||||
def __call__(self):
|
||||
yield
|
||||
|
||||
class TestClass2:
|
||||
async def __call__(self):
|
||||
yield
|
||||
|
||||
class TestClass3:
|
||||
def __call__(self):
|
||||
...
|
||||
|
||||
assert is_gen_callable(test1)
|
||||
assert not is_gen_callable(test2)
|
||||
assert not is_gen_callable(test3)
|
||||
assert is_gen_callable(TestClass1())
|
||||
assert not is_gen_callable(TestClass2())
|
||||
assert not is_gen_callable(TestClass3())
|
||||
|
||||
|
||||
def test_is_async_gen_callable():
|
||||
async def test1():
|
||||
yield
|
||||
|
||||
def test2():
|
||||
yield
|
||||
|
||||
async def test3():
|
||||
...
|
||||
|
||||
class TestClass1:
|
||||
async def __call__(self):
|
||||
yield
|
||||
|
||||
class TestClass2:
|
||||
def __call__(self):
|
||||
yield
|
||||
|
||||
class TestClass3:
|
||||
async def __call__(self):
|
||||
...
|
||||
|
||||
assert is_async_gen_callable(test1)
|
||||
assert not is_async_gen_callable(test2)
|
||||
assert not is_async_gen_callable(test3)
|
||||
assert is_async_gen_callable(TestClass1())
|
||||
assert not is_async_gen_callable(TestClass2())
|
||||
assert not is_async_gen_callable(TestClass3())
|
||||
|
||||
|
||||
def test_dataclass_encoder():
|
||||
simple = json.dumps("123", cls=DataclassEncoder)
|
||||
assert simple == '"123"'
|
||||
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
ms = MessageSegment.nested(Message(MessageSegment.text("text")))
|
||||
ms = FakeMessageSegment.nested(FakeMessage(FakeMessageSegment.text("text")))
|
||||
s = json.dumps(ms, cls=DataclassEncoder)
|
||||
assert (
|
||||
s
|
||||
== '{"type": "node", "data": {"content": [{"type": "text", "data": {"text": "text"}}]}}'
|
||||
assert s == (
|
||||
"{"
|
||||
'"type": "node", '
|
||||
'"data": {"content": [{"type": "text", "data": {"text": "text"}}]}'
|
||||
"}"
|
||||
)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from typing import Type, Union, Mapping, Iterable, Optional
|
||||
|
||||
from pydantic import create_model
|
||||
from pydantic import Extra, create_model
|
||||
|
||||
from nonebot.adapters import Event, Message, MessageSegment
|
||||
|
||||
@@ -12,7 +12,6 @@ def escape_text(s: str, *, escape_comma: bool = True) -> str:
|
||||
return s
|
||||
|
||||
|
||||
def make_fake_message():
|
||||
class FakeMessageSegment(MessageSegment["FakeMessage"]):
|
||||
@classmethod
|
||||
def get_message_class(cls):
|
||||
@@ -36,6 +35,7 @@ def make_fake_message():
|
||||
def is_text(self) -> bool:
|
||||
return self.type == "text"
|
||||
|
||||
|
||||
class FakeMessage(Message[FakeMessageSegment]):
|
||||
@classmethod
|
||||
def get_segment_class(cls):
|
||||
@@ -56,8 +56,6 @@ def make_fake_message():
|
||||
other = escape_text(other) if isinstance(other, str) else other
|
||||
return super().__add__(other)
|
||||
|
||||
return FakeMessage
|
||||
|
||||
|
||||
def make_fake_event(
|
||||
_base: Optional[Type[Event]] = None,
|
||||
@@ -70,9 +68,9 @@ def make_fake_event(
|
||||
_to_me: bool = True,
|
||||
**fields,
|
||||
) -> Type[Event]:
|
||||
_Fake = create_model("_Fake", __base__=_base or Event, **fields)
|
||||
Base = _base or Event
|
||||
|
||||
class FakeEvent(_Fake):
|
||||
class FakeEvent(Base, extra=Extra.forbid):
|
||||
def get_type(self) -> str:
|
||||
return _type
|
||||
|
||||
@@ -100,7 +98,4 @@ def make_fake_event(
|
||||
def is_tome(self) -> bool:
|
||||
return _to_me
|
||||
|
||||
class Config:
|
||||
extra = "forbid"
|
||||
|
||||
return FakeEvent
|
||||
return create_model("FakeEvent", __base__=FakeEvent, **fields)
|
||||
|
@@ -71,7 +71,9 @@ async def _(foo: str = "bar"): ...
|
||||
|
||||
获取当前事件的 Bot 对象。
|
||||
|
||||
通过标注参数为 `Bot` 类型,或者一系列 `Bot` 类型,即可获取到当前事件的 Bot 对象。为兼容性考虑,如果参数名为 `bot` 且无类型注解,也会视为 `Bot` 依赖注入。
|
||||
通过标注参数为 `Bot` 类型,或者一系列 `Bot` 类型,即可获取到当前事件的 Bot 对象。为兼容性考虑,如果参数名为 `bot` 且无类型注解,也会视为 Bot 依赖注入。
|
||||
|
||||
Bot 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。
|
||||
|
||||
<Tabs groupId="python">
|
||||
<TabItem value="3.10" label="Python 3.10+" default>
|
||||
@@ -108,7 +110,9 @@ async def _(bot): ... # 兼容性处理
|
||||
|
||||
获取当前事件。
|
||||
|
||||
通过标注参数为 `Event` 类型,或者一系列 `Event` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `event` 且无类型注解,也会视为 `Event` 依赖注入。
|
||||
通过标注参数为 `Event` 类型,或者一系列 `Event` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `event` 且无类型注解,也会视为 Event 依赖注入。
|
||||
|
||||
Event 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。
|
||||
|
||||
<Tabs groupId="python">
|
||||
<TabItem value="3.10" label="Python 3.10+" default>
|
||||
@@ -143,6 +147,8 @@ async def _(event): ... # 兼容性处理
|
||||
|
||||
获取当前[会话状态](../appendices/session-state.md)。
|
||||
|
||||
通过标注参数为 `T_State` 类型,即可获取到当前会话状态。为兼容性考虑,如果参数名为 `state` 且无类型注解,也会视为 State 依赖注入。
|
||||
|
||||
```python
|
||||
from nonebot.typing import T_State
|
||||
|
||||
@@ -153,10 +159,15 @@ async def _(foo: T_State): ...
|
||||
|
||||
获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。
|
||||
|
||||
通过标注参数为 `Matcher` 类型,或者一系列 `Matcher` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `matcher` 且无类型注解,也会视为 Matcher 依赖注入。
|
||||
|
||||
Matcher 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。
|
||||
|
||||
```python
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
async def _(matcher: Matcher): ...
|
||||
async def _(foo: Matcher): ...
|
||||
async def _(matcher): ... # 兼容性处理
|
||||
```
|
||||
|
||||
### Exception
|
||||
@@ -1155,8 +1166,8 @@ from typing import Annotated
|
||||
from nonebot.params import ArgStr
|
||||
|
||||
@matcher.got("key")
|
||||
async def _(key: str = ArgStr()): ...
|
||||
async def _(foo: str = ArgStr("key")): ...
|
||||
async def _(key: Annotated[str, ArgStr()]): ...
|
||||
async def _(foo: Annotated[str, ArgStr("key")]): ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -1166,8 +1177,8 @@ async def _(foo: str = ArgStr("key")): ...
|
||||
from nonebot.params import ArgStr
|
||||
|
||||
@matcher.got("key")
|
||||
async def _(key: Annotated[str, ArgStr()]): ...
|
||||
async def _(foo: Annotated[str, ArgStr("key")]): ...
|
||||
async def _(key: str = ArgStr()): ...
|
||||
async def _(foo: str = ArgStr("key")): ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
@@ -12,6 +12,10 @@ options:
|
||||
|
||||
在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,内置的响应规则,与第三方响应规则拓展。
|
||||
|
||||
:::tip 提示
|
||||
事件响应器允许继承,你可以通过直接继承 `Matcher` 类来创建一个新的事件响应器。
|
||||
:::
|
||||
|
||||
## 事件响应器组成
|
||||
|
||||
### 事件响应器类型
|
||||
@@ -287,6 +291,19 @@ sub_cmd = group.command("sub")
|
||||
help_cmd = group.command("help")
|
||||
```
|
||||
|
||||
命令别名 aliases 默认不会添加 `CommandGroup` 设定的前缀,如果需要为 aliases 添加前缀,可以添加 `prefix_aliases=True` 参数:
|
||||
|
||||
```python
|
||||
from nonebot import CommandGroup
|
||||
|
||||
group = CommandGroup("cmd", prefix_aliases=True)
|
||||
|
||||
cmd = group.command(tuple())
|
||||
help_cmd = group.command("help", aliases={"帮助"})
|
||||
```
|
||||
|
||||
这样就能成功匹配 `/cmd`、`/cmd.help`、`/cmd.帮助` 命令。如果未设置,将默认匹配 `/cmd`、`/cmd.help`、`/帮助` 命令。
|
||||
|
||||
### `MatcherGroup`
|
||||
|
||||
`MatcherGroup` 可以用于管理一系列具有相同属性的响应器。
|
||||
@@ -307,114 +324,13 @@ matcher2 = group.on_message()
|
||||
|
||||
### Alconna
|
||||
|
||||
[`nonebot-plugin-alconna`](https://github.com/ArcletProject/nonebot-plugin-alconna) 是一类提供了拓展响应规则的插件。
|
||||
[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。
|
||||
该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器,
|
||||
是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。
|
||||
|
||||
特点包括:
|
||||
|
||||
- 高效
|
||||
- 直观的命令组件创建方式
|
||||
- 强大的类型解析与类型转换功能
|
||||
- 自定义的帮助信息格式
|
||||
- 多语言支持
|
||||
- 易用的快捷命令创建与使用
|
||||
- 可创建命令补全会话, 以实现多轮连续的补全提示
|
||||
- 可嵌套的多级子命令
|
||||
- 正则匹配支持
|
||||
|
||||
该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。
|
||||
|
||||
同时,基于 [Annotated 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解 `AlcMatches` 与 `AlcResult`
|
||||
|
||||
该插件还可以通过 `handle(parameterless)` 来控制一个具体的响应函数是否在不满足条件时跳过响应:
|
||||
|
||||
- `pip.handle([Check(assign("add.name", "nb"))])` 表示仅在命令为 `role-group add` 并且 name 为 `nb` 时响应
|
||||
- `pip.handle([Check(assign("list"))])` 表示仅在命令为 `role-group list` 时响应
|
||||
- `pip.handle([Check(assign("add"))])` 表示仅在命令为 `role-group add` 时响应
|
||||
|
||||
基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。
|
||||
标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。
|
||||
|
||||
#### 插件安装
|
||||
|
||||
```shell
|
||||
nb plugin install nonebot-plugin-alconna
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```shell
|
||||
pip install nonebot-plugin-alconna
|
||||
```
|
||||
|
||||
#### 示例
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.adapters import At
|
||||
from nonebot.adapters.onebot.v12 import Message
|
||||
from nonebot_plugin_alconna.adapters.onebot12 import Image
|
||||
from nonebot_plugin_alconna import AlconnaMatches, on_alconna
|
||||
from nonebot.adapters.onebot.v12 import MessageSegment as Ob12MS
|
||||
from arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand
|
||||
|
||||
alc = Alconna(
|
||||
"role-group",
|
||||
Subcommand(
|
||||
"add",
|
||||
Args["name", str],
|
||||
Option("member", Args["target", MultiVar(At)]),
|
||||
),
|
||||
Option("list"),
|
||||
)
|
||||
rg = on_alconna(alc, auto_send_output=True)
|
||||
|
||||
|
||||
@rg.handle()
|
||||
async def _(result: Arparma = AlconnaMatches()):
|
||||
if result.find("list"):
|
||||
img = await gen_role_group_list_image()
|
||||
await rg.finish(Message([Image(img)]))
|
||||
if result.find("add"):
|
||||
group = await create_role_group(result["add.name"])
|
||||
if result.find("add.member"):
|
||||
ats: tuple[Ob12MS, ...] = result["add.member.target"]
|
||||
group.extend(member.data["user_id"] for member in ats)
|
||||
await rg.finish("添加成功")
|
||||
```
|
||||
|
||||
我们可以看到主要的两大组件:`Option` 与 `Subcommand`。
|
||||
|
||||
`Option` 可以传入一组别名,如 `Option("--foo|-F|--FOO|-f")` 或 `Option("--foo", alias=["-F"]`
|
||||
|
||||
`Subcommand` 则可以传入自己的 `Option` 与 `Subcommand`:
|
||||
|
||||
他们拥有如下共同参数:
|
||||
|
||||
- `help_text`: 传入该组件的帮助信息
|
||||
- `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name)
|
||||
- `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换
|
||||
- `default`: 默认值,在该组件未被解析时使用使用该值替换。
|
||||
|
||||
其次使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。
|
||||
|
||||
`on_alconna` 的所有参数如下:
|
||||
|
||||
- `command: Alconna | str`: Alconna 命令
|
||||
- `skip_for_unmatch: bool = True`: 是否在命令不匹配时跳过该响应
|
||||
- `auto_send_output: bool = False`: 是否自动发送输出信息并跳过响应
|
||||
- `output_converter: TConvert | None = None`: 输出信息字符串转换为消息序列方法
|
||||
- `aliases: set[str | tuple[str, ...]] | None = None`: 命令别名, 作用类似于 `on_command` 中的 aliases
|
||||
- `comp_config: CompConfig | None = None`: 补全会话配置, 不传入则不启用补全会话
|
||||
|
||||
`AlconnaMatches` 是一个依赖注入函数,可注入 `Alconna` 命令解析结果。
|
||||
|
||||
#### 参考
|
||||
|
||||
插件文档: [📦 这里](https://github.com/ArcletProject/nonebot-plugin-alconna/blob/master/docs.md)
|
||||
|
||||
官方文档: [👉 指路](https://arclet.top/)
|
||||
|
||||
QQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH)
|
||||
|
||||
友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html)
|
||||
详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna.md) 章节。
|
||||
|
@@ -31,7 +31,7 @@ NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。
|
||||
|
||||
我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据,如下所示:
|
||||
|
||||
```python {1,5-11} title=example/__init__.py
|
||||
```python {1,5-12} title=example/__init__.py
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from .config import Config
|
||||
@@ -40,12 +40,19 @@ __plugin_meta__ = PluginMetadata(
|
||||
name="示例插件",
|
||||
description="这是一个示例插件",
|
||||
usage="没什么用",
|
||||
type="application",
|
||||
config=Config,
|
||||
extra={},
|
||||
)
|
||||
```
|
||||
|
||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有两个可选的属性。`config` 属性用于指定插件的[配置类](../appendices/config.mdx#插件配置),`extra` 属性,它是一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。
|
||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#填写插件元数据)章节):
|
||||
|
||||
- `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能);
|
||||
- `homepage`:插件项目主页,发布插件必填;
|
||||
- `config`:插件的[配置类](../appendices/config.mdx#插件配置),如无配置类可不填;
|
||||
- `supported_adapters`:支持的适配器模块名集合,若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写;
|
||||
- `extra`:一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。
|
||||
|
||||
请注意,这里的**插件名称**是供使用者或机器人用户查看的,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。
|
||||
|
||||
|
@@ -25,6 +25,10 @@ options:
|
||||
这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
@driver.on_startup
|
||||
async def do_something():
|
||||
pass
|
||||
@@ -35,6 +39,10 @@ async def do_something():
|
||||
这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
@driver.on_shutdown
|
||||
async def do_something():
|
||||
pass
|
||||
@@ -45,6 +53,10 @@ async def do_something():
|
||||
这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
@driver.on_bot_connect
|
||||
async def do_something(bot: Bot):
|
||||
pass
|
||||
@@ -55,6 +67,10 @@ async def do_something(bot: Bot):
|
||||
这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
@driver.on_bot_disconnect
|
||||
async def do_something(bot: Bot):
|
||||
pass
|
||||
@@ -82,6 +98,7 @@ async def do_something(event: Event):
|
||||
|
||||
```python
|
||||
from nonebot.message import event_postprocessor
|
||||
|
||||
@event_postprocessor
|
||||
async def do_something(event: Event):
|
||||
pass
|
||||
@@ -93,6 +110,7 @@ async def do_something(event: Event):
|
||||
|
||||
```python
|
||||
from nonebot.message import run_preprocessor
|
||||
|
||||
@run_preprocessor
|
||||
async def do_something(event: Event, matcher: Matcher):
|
||||
pass
|
||||
|
@@ -224,6 +224,35 @@ weather = on_command(
|
||||
发布插件应该为自身的事件响应器提供可配置的优先级,以便插件使用者可以自定义多个插件间的响应顺序。
|
||||
:::
|
||||
|
||||
由于插件配置项是从全局配置中读取的,通常我们需要在配置项名称前面添加前缀名,以防止配置项冲突。例如在上方的示例中,我们就添加了配置项前缀 `weather_`。但是这样会导致在使用配置项时过长的变量名,因此我们可以使用 `pydantic` 的 `alias` 或者通过配置 scope 来简化配置项名称。这里我们以 scope 配置为例:
|
||||
|
||||
```python title=weather/config.py
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ScopedConfig(BaseModel):
|
||||
api_key: str
|
||||
command_priority: int = 10
|
||||
plugin_enabled: bool = True
|
||||
|
||||
class Config(BaseModel):
|
||||
weather: ScopedConfig
|
||||
```
|
||||
|
||||
```python title=weather/__init__.py
|
||||
from nonebot import get_driver
|
||||
|
||||
from .config import Config
|
||||
|
||||
plugin_config = Config.parse_obj(get_driver().config).weather
|
||||
```
|
||||
|
||||
这样我们就可以省略插件配置项名称中的前缀 `weather_` 了。但需要注意的是,如果我们使用了 scope 配置,那么在配置文件中也需要使用 [`env_nested_delimiter` 格式](#配置项解析),例如:
|
||||
|
||||
```dotenv
|
||||
WEATHER__API_KEY=123456
|
||||
WEATHER__COMMAND_PRIORITY=10
|
||||
```
|
||||
|
||||
## 内置配置项
|
||||
|
||||
配置项 API 文档可以前往 [Config 类](../api/config.md#Config)查看。
|
||||
|
@@ -75,7 +75,7 @@ logger.add(
|
||||
sys.stdout,
|
||||
level=0,
|
||||
diagnose=True,
|
||||
format="<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{full_name}</u></c> | {message}",
|
||||
format="<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}",
|
||||
filter=default_filter
|
||||
)
|
||||
```
|
||||
|
@@ -66,7 +66,7 @@ async def handle_onebot(bot: OneBot):
|
||||
:::warning 注意
|
||||
重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。
|
||||
|
||||
但 Bot 和 Event 二者的参数类型注解具有最高检查优先级,如果二者类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。
|
||||
但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。
|
||||
:::
|
||||
|
||||
:::tip 提示
|
||||
|
288
website/docs/best-practice/alconna.md
Normal file
288
website/docs/best-practice/alconna.md
Normal file
@@ -0,0 +1,288 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
description: Alconna 命令解析拓展
|
||||
---
|
||||
|
||||
# Alconna 命令解析
|
||||
|
||||
[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。
|
||||
该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器,
|
||||
是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。
|
||||
|
||||
特点包括:
|
||||
|
||||
- 高效
|
||||
- 直观的命令组件创建方式
|
||||
- 强大的类型解析与类型转换功能
|
||||
- 自定义的帮助信息格式
|
||||
- 多语言支持
|
||||
- 易用的快捷命令创建与使用
|
||||
- 可创建命令补全会话, 以实现多轮连续的补全提示
|
||||
- 可嵌套的多级子命令
|
||||
- 正则匹配支持
|
||||
|
||||
该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。
|
||||
|
||||
同时,基于 [Annotated 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解 `AlcMatches` 与 `AlcResult`
|
||||
|
||||
该插件还可以通过 `handle(parameterless)` 来控制一个具体的响应函数是否在不满足条件时跳过响应:
|
||||
|
||||
- `pip.handle([Check(assign("add.name", "nb"))])` 表示仅在命令为 `role-group add` 并且 name 为 `nb` 时响应
|
||||
- `pip.handle([Check(assign("list"))])` 表示仅在命令为 `role-group list` 时响应
|
||||
- `pip.handle([Check(assign("add"))])` 表示仅在命令为 `role-group add` 时响应
|
||||
|
||||
基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。
|
||||
标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。
|
||||
|
||||
## 安装插件
|
||||
|
||||
在使用前请先安装 `nonebot-plugin-alconna` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如:
|
||||
|
||||
在**项目目录**下执行以下命令:
|
||||
|
||||
```shell
|
||||
nb plugin install nonebot-plugin-alconna
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```shell
|
||||
pip install nonebot-plugin-alconna
|
||||
```
|
||||
|
||||
## 使用插件
|
||||
|
||||
以下为一个简单的使用示例:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.adapters import At
|
||||
from nonebot.adapters.onebot.v12 import Message
|
||||
from nonebot_plugin_alconna.adapters.onebot12 import Image
|
||||
from nonebot_plugin_alconna import AlconnaMatches, on_alconna
|
||||
from nonebot.adapters.onebot.v12 import MessageSegment as Ob12MS
|
||||
from arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand
|
||||
|
||||
alc = Alconna(
|
||||
["/", "!"],
|
||||
"role-group",
|
||||
Subcommand(
|
||||
"add",
|
||||
Args["name", str],
|
||||
Option("member", Args["target", MultiVar(At)]),
|
||||
),
|
||||
Option("list"),
|
||||
)
|
||||
rg = on_alconna(alc, auto_send_output=True)
|
||||
|
||||
|
||||
@rg.handle()
|
||||
async def _(result: Arparma = AlconnaMatches()):
|
||||
if result.find("list"):
|
||||
img = await gen_role_group_list_image()
|
||||
await rg.finish(Message([Image(img)]))
|
||||
if result.find("add"):
|
||||
group = await create_role_group(result["add.name"])
|
||||
if result.find("add.member"):
|
||||
ats: tuple[Ob12MS, ...] = result["add.member.target"]
|
||||
group.extend(member.data["user_id"] for member in ats)
|
||||
await rg.finish("添加成功")
|
||||
```
|
||||
|
||||
### 导入插件
|
||||
|
||||
由于 `nonebot-plugin-alconna` 作为插件,因此需要在使用前对其进行**加载**并**导入**其中的 `on_alconna` 来使用命令拓展。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解。
|
||||
|
||||
```python
|
||||
from nonebot import require
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
|
||||
from nonebot_plugin_alconna import on_alconna
|
||||
```
|
||||
|
||||
### 命令编写
|
||||
|
||||
我们可以看到主要的两大组件:`Option` 与 `Subcommand`。
|
||||
|
||||
`Option` 可以传入一组别名,如 `Option("--foo|-F|--FOO|-f")` 或 `Option("--foo", alias=["-F"]`
|
||||
|
||||
`Subcommand` 则可以传入自己的 `Option` 与 `Subcommand`:
|
||||
|
||||
他们拥有如下共同参数:
|
||||
|
||||
- `help_text`: 传入该组件的帮助信息
|
||||
- `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name)
|
||||
- `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换
|
||||
- `default`: 默认值,在该组件未被解析时使用使用该值替换。
|
||||
|
||||
然后是 `Args` 与 `MultiVar`,他们是用于解析参数的组件。
|
||||
|
||||
`Args` 是参数解析的基础组件,构造方法形如 `Args["foo", str]["bar", int]["baz", bool, False]`,
|
||||
与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。
|
||||
|
||||
`MultiVar` 则是一个特殊的标注,用于告知解析器该参数可以接受多个值,其构造方法形如 `MultiVar(str)`。
|
||||
同样的还有 `KeyWordVar`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。
|
||||
|
||||
:::tip
|
||||
`MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,其构造方法形如 `MultiVar(KeyWordVar(str))`
|
||||
|
||||
`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值。
|
||||
|
||||
`MultiVar` 不能在 `KeyWordVar` 之后传入。
|
||||
:::
|
||||
|
||||
### 参数标注
|
||||
|
||||
`Args` 的参数类型表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例。
|
||||
|
||||
```python
|
||||
from arclet.alconna import Args
|
||||
from nepattern import BasePattern
|
||||
|
||||
# 表示 foo 参数需要匹配一个 @number 样式的字符串
|
||||
args = Args["foo", BasePattern("@\d+")]
|
||||
```
|
||||
|
||||
示例中传入的 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]`。
|
||||
|
||||
默认支持的类型有:
|
||||
|
||||
- `str`: 匹配任意字符串
|
||||
- `int`: 匹配整数
|
||||
- `float`: 匹配浮点数
|
||||
- `bool`: 匹配 `True` 与 `False` 以及他们小写形式
|
||||
- `hex`: 匹配 `0x` 开头的十六进制字符串
|
||||
- `url`: 匹配网址
|
||||
- `email`: 匹配 `xxxx@xxx` 的字符串
|
||||
- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串
|
||||
- `list`: 匹配类似 `["foo","bar","baz"]` 的字符串
|
||||
- `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串
|
||||
- `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳
|
||||
- `Any`: 匹配任意类型
|
||||
- `AnyString`: 匹配任意类型,转为 `str`
|
||||
- `Number`: 匹配 `int` 与 `float`,转为 `int`
|
||||
|
||||
同时可以使用 typing 中的类型:
|
||||
|
||||
- `Literal[X]`: 匹配其中的任意一个值
|
||||
- `Union[X, Y]`: 匹配其中的任意一个类型
|
||||
- `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值
|
||||
- `List[X]`: 匹配一个列表,其中的元素为 `X` 类型
|
||||
- `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型,value 为 `Y` 类型
|
||||
- ...
|
||||
|
||||
:::tip
|
||||
几类特殊的传入标记:
|
||||
|
||||
- `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联)
|
||||
- `RawStr("foo")`: 匹配字符串 "foo" (不会被 `BasePattern` 替换)
|
||||
- `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz"
|
||||
- `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型
|
||||
- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值
|
||||
- `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0]
|
||||
- `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象
|
||||
- `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值)
|
||||
- ...
|
||||
|
||||
:::
|
||||
|
||||
### 消息段标注
|
||||
|
||||
示例中使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。
|
||||
|
||||
消息段标注会匹配特定的 `MessageSegment`:
|
||||
|
||||
```python
|
||||
...
|
||||
ats: tuple[Ob12MS, ...] = result["add.member.target"]
|
||||
group.extend(member.data["user_id"] for member in ats)
|
||||
```
|
||||
|
||||
:::tip
|
||||
通用标注与适配器标注的区别在于,通用标注会匹配多个适配器中相似类型的消息段。
|
||||
|
||||
通用标注返回的是 `nonebot_plugin_alconna.adapters` 中定义的 `Segment` 模型:
|
||||
|
||||
```python
|
||||
class Segment:
|
||||
"""基类标注"""
|
||||
origin: MessageSegment
|
||||
|
||||
class At(Segment):
|
||||
"""At对象, 表示一类提醒某用户的元素"""
|
||||
target: str
|
||||
|
||||
class Emoji(Segment):
|
||||
"""Emoji对象, 表示一类表情元素"""
|
||||
id: str
|
||||
name: Optional[str]
|
||||
|
||||
class Media(Segment):
|
||||
url: Optional[str]
|
||||
id: Optional[str]
|
||||
|
||||
class Image(Media):
|
||||
"""Image对象, 表示一类图片元素"""
|
||||
|
||||
class Audio(Media):
|
||||
"""Audio对象, 表示一类音频元素"""
|
||||
|
||||
class Voice(Media):
|
||||
"""Voice对象, 表示一类语音元素"""
|
||||
|
||||
class Video(Media):
|
||||
"""Video对象, 表示一类视频元素"""
|
||||
|
||||
class File(Segment):
|
||||
"""File对象, 表示一类文件元素"""
|
||||
id: str
|
||||
name: Optional[str] = field(default=None)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 响应器使用
|
||||
|
||||
`on_alconna` 的所有参数如下:
|
||||
|
||||
- `command: Alconna | str`: Alconna 命令
|
||||
- `skip_for_unmatch: bool = True`: 是否在命令不匹配时跳过该响应
|
||||
- `auto_send_output: bool = False`: 是否自动发送输出信息并跳过响应
|
||||
- `output_converter: TConvert | None = None`: 输出信息字符串转换为消息序列方法
|
||||
- `aliases: set[str | tuple[str, ...]] | None = None`: 命令别名, 作用类似于 `on_command` 中的 aliases
|
||||
- `comp_config: CompConfig | None = None`: 补全会话配置, 不传入则不启用补全会话
|
||||
|
||||
`AlconnaMatches` 是一个依赖注入函数,可注入 `Alconna` 命令解析结果。
|
||||
|
||||
### 配置项
|
||||
|
||||
#### alconna_auto_send_output
|
||||
|
||||
- **类型**: `bool`
|
||||
- **默认值**: `False`
|
||||
|
||||
"是否全局启用输出信息自动发送,不启用则会在触特殊内置选项后仍然将解析结果传递至响应器。
|
||||
|
||||
#### alconna_use_command_start
|
||||
|
||||
- **类型**: `bool`
|
||||
- **默认值**: `False`
|
||||
|
||||
是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀
|
||||
|
||||
#### alconna_auto_completion
|
||||
|
||||
- **类型**: `bool`
|
||||
- **默认值**: `False`
|
||||
|
||||
是否全局启用命令自动补全,启用后会在参数缺失或触发 `--comp` 选项时自自动启用交互式补全。
|
||||
|
||||
## 文档参考
|
||||
|
||||
插件文档: [📦 这里](https://github.com/nonebot/plugin-alconna/blob/master/docs.md)
|
||||
|
||||
官方文档: [👉 指路](https://arclet.top/)
|
||||
|
||||
QQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH)
|
||||
|
||||
友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html)
|
@@ -1,6 +0,0 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
description: 在商店发布自己的插件
|
||||
---
|
||||
|
||||
# 发布插件
|
202
website/docs/developer/plugin-publishing.mdx
Normal file
202
website/docs/developer/plugin-publishing.mdx
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
description: 在商店发布自己的插件
|
||||
---
|
||||
|
||||
# 发布插件
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
||||
NoneBot 为开发者提供了分享插件给大家使用的方式——商店。本章节将会介绍如何将我们写好的插件发布到商店。
|
||||
|
||||
:::tip 提示
|
||||
本章节仅包含插件发布流程指导,插件开发请查阅前述章节。
|
||||
:::
|
||||
|
||||
## 准备工作
|
||||
|
||||
### 插件命名规范
|
||||
|
||||
NoneBot 插件使用下述命名规范:
|
||||
|
||||
- 对于**项目名**,建议统一以 `nonebot-plugin-` 开头,之后为拟定的插件名字,词间用横杠 `-` 分隔;
|
||||
- **项目名**用于代码仓库名称、PyPI 包的发布名称等;
|
||||
- 本文使用 `nonebot-plugin-{your-plugin-name}` 为例。
|
||||
- 对于**模块名**,建议与**项目名**一致,但词间用下划线 `_` 分隔,即统一以 `nonebot_plugin_` 开头,之后为拟定的插件名字;
|
||||
- **模块名**用于程序导入使用,应为插件文件(夹)的名称;
|
||||
- 本文使用 `nonebot_plugin_{your_plugin_name}` 为例。
|
||||
|
||||
### 项目结构
|
||||
|
||||
:::tip 提示
|
||||
本段所述的项目结构仅作推荐,不做强制要求,保证实际可用性即可。
|
||||
:::
|
||||
|
||||
插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。
|
||||
|
||||
插件项目的一种组织结构如下:
|
||||
|
||||
```tree
|
||||
📦 nonebot-plugin-{your-plugin-name}
|
||||
├── 📂 nonebot_plugin_{your_plugin_name}
|
||||
│ ├── 📜 __init__.py
|
||||
│ └── 📜 config.py
|
||||
├── 📜 pyproject.toml
|
||||
└── 📜 README.md
|
||||
```
|
||||
|
||||
#### 第三方项目模板
|
||||
|
||||
一些社区用户可能会分享自己制作的项目模板方便大家使用,如:[A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template) 等。
|
||||
|
||||
:::tip 提示
|
||||
本文档**不保证**第三方模板的适用性。
|
||||
|
||||
根据项目模板提供的使用指导补全/修改相应内容后上传到 GitHub 即可。
|
||||
:::
|
||||
|
||||
### 插件依赖
|
||||
|
||||
本段指导填写插件依赖,避免不正确的依赖信息导致插件无法正常工作。
|
||||
|
||||
依赖填写的基本原则:程序直接导入了什么第三方库,就添加什么第三方包依赖;能用哪些第三方库的特性,就根据使用的特性锁定第三方包版本。
|
||||
|
||||
:::warning 注意
|
||||
|
||||
1. 插件需要添加 `nonebot2` 为依赖以避免“幽灵依赖”;
|
||||
2. 插件需要将使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖;
|
||||
3. 由于 `nonebot` 是指 `nonebot1` **而非** `nonebot2`,因此要注意**不要**将 `nonebot` 添加为插件的依赖,以免造成冲突;
|
||||
4. 尽可能避免使用 `==` 锁定单一版本,增强与其它插件的兼容性。
|
||||
|
||||
:::
|
||||
|
||||
### 填写插件元数据
|
||||
|
||||
请注意,插件发布要求**必须**填写元数据才能通过审核。
|
||||
|
||||
下面是一个示例:
|
||||
|
||||
```python title=nonebot_plugin_{your_plugin_name}/__init__.py
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from .config import Config
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="{插件名称}",
|
||||
description="{插件介绍}",
|
||||
usage="{插件用法}",
|
||||
|
||||
type="{插件分类}",
|
||||
# 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。
|
||||
|
||||
homepage="{项目主页}",
|
||||
# 发布必填。
|
||||
|
||||
config=Config,
|
||||
# 插件配置项类,如无需配置可不填写。
|
||||
|
||||
supported_adapters={"~onebot.v11", "~telegram"},
|
||||
# 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。
|
||||
# 若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写,否则应该列出插件支持的适配器。
|
||||
)
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
`__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。
|
||||
|
||||
一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。
|
||||
:::
|
||||
|
||||
:::tip 提示
|
||||
带花括号 `{}` 的内容需要自行替换,注意**一定要把原有的花括号去掉**。
|
||||
:::
|
||||
|
||||
### 准备项目主页
|
||||
|
||||
通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。
|
||||
|
||||
内容大致包括:
|
||||
|
||||
- 插件功能介绍
|
||||
- 安装方法(建议至少有 `nb-cli` 方式安装,**不要**使用旧式的 `bot.py` 配置)
|
||||
- 插件配置项(若无可跳过)
|
||||
- 插件设置的触发规则(若无可跳过)
|
||||
- 插件的其它用法(按需编写)
|
||||
|
||||
:::tip 提示
|
||||
可以参考[第三方项目模板](#第三方项目模板)。
|
||||
:::
|
||||
|
||||
### 发布至 [PyPI](https://pypi.org)
|
||||
|
||||
根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。
|
||||
|
||||
:::tip 提示
|
||||
不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm.fming.dev/latest/),
|
||||
[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)
|
||||
构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。
|
||||
:::
|
||||
|
||||
<Tabs groupId="publishMethod">
|
||||
<TabItem value="poetry" label="Poetry" default>
|
||||
|
||||
```bash
|
||||
poetry publish --build # 构建并发布
|
||||
|
||||
# 等效于以下两个命令
|
||||
poetry build # 只构建
|
||||
poetry publish # 只发布先前的构建
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pdm" label="PDM" default>
|
||||
|
||||
```bash
|
||||
pdm publish # 构建并发布
|
||||
|
||||
# 等效于以下两个命令
|
||||
pdm build # 只构建
|
||||
pdm publish --no-build # 只发布先前的构建
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="setuptools" label="Setuptools (PEP 517)" default>
|
||||
|
||||
```bash
|
||||
pip install build twine # 安装通用构建与发布工具
|
||||
|
||||
python -m build --sdist --wheel . # 只构建
|
||||
twine upload dist/* # 只发布先前的构建
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::tip 提示
|
||||
发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。
|
||||
:::
|
||||
|
||||
## 商店审核
|
||||
|
||||
### 提交申请
|
||||
|
||||
完成在 PyPI 的插件发布流程后,前往[商店](/store)页面,切换到插件页签,点击 **发布插件** 按钮。
|
||||
|
||||
在弹出的插件信息提交表单内,填入您所要发布的相应插件信息。请注意,如果插件需要必要配置项才能正常导入,请在“插件配置项”中填写必要的内容(请勿填写密钥等敏感信息)。
|
||||
|
||||
完成填写后,点击 **发布** 按钮,这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 `Submit new issue` 按钮进行最终提交即可。
|
||||
|
||||
### 等待插件审核
|
||||
|
||||
插件发布 Issue 创建后,将会经过 **NoneFlow Bot** 的自动前置检查,以确保插件信息正确无误、插件能被正确加载。
|
||||
|
||||
:::tip 提示
|
||||
若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后在当前 Issue 追加任意内容的评论(如“已更新”等)即可重新触发插件检查。
|
||||
:::
|
||||
|
||||
之后,NoneBot 的维护者和一些插件开发者会初步检查插件代码,帮助减少该插件的问题。
|
||||
|
||||
完成这些步骤后,您的插件将会被自动合并到[商店](/store),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。
|
@@ -148,6 +148,15 @@ MessageSegment.text("text") in message
|
||||
"text" in message
|
||||
```
|
||||
|
||||
我们还可以使用消息序列的 `only` 方法来检查消息中是否仅包含指定的消息段。
|
||||
|
||||
```python
|
||||
# 是否都为指定消息段
|
||||
message.only(MessageSegment.text("test"))
|
||||
# 是否仅包含指定类型的消息段
|
||||
message.only("text")
|
||||
```
|
||||
|
||||
### 过滤、索引与切片
|
||||
|
||||
消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。
|
||||
|
@@ -29,12 +29,9 @@ export default function Plugin(): JSX.Element {
|
||||
const currentPlugins = filteredPlugins.slice(startIndex, endIndex + 1);
|
||||
|
||||
const [form, setForm] = useState<{
|
||||
name: string;
|
||||
desc: string;
|
||||
projectLink: string;
|
||||
moduleName: string;
|
||||
homepage: string;
|
||||
}>({ name: "", desc: "", projectLink: "", moduleName: "", homepage: "" });
|
||||
}>({ projectLink: "", moduleName: "" });
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
@@ -48,13 +45,13 @@ export default function Plugin(): JSX.Element {
|
||||
setModalOpen(false);
|
||||
const queries: { key: string; value: string }[] = [
|
||||
{ key: "template", value: "plugin_publish.yml" },
|
||||
{ key: "title", value: form.name && `Plugin: ${form.name}` },
|
||||
{
|
||||
key: "title",
|
||||
value: form.projectLink && `Plugin: ${form.projectLink}`,
|
||||
},
|
||||
{ key: "labels", value: "Plugin" },
|
||||
{ key: "name", value: form.name },
|
||||
{ key: "description", value: form.desc },
|
||||
{ key: "pypi", value: form.projectLink },
|
||||
{ key: "module", value: form.moduleName },
|
||||
{ key: "homepage", value: form.homepage },
|
||||
{ key: "tags", value: JSON.stringify(tags) },
|
||||
];
|
||||
const urlQueries = queries
|
||||
@@ -95,10 +92,6 @@ export default function Plugin(): JSX.Element {
|
||||
const delTag = (index: number) => {
|
||||
setTags(tags.filter((_, i) => i !== index));
|
||||
};
|
||||
const insertTagType = (text: string) => {
|
||||
setLabel(text + label);
|
||||
ref.current.value = text + label;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -137,25 +130,6 @@ export default function Plugin(): JSX.Element {
|
||||
<ModalContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="grid grid-cols-1 gap-4 p-4">
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">插件名称:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
maxLength={20}
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">插件介绍:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="desc"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">PyPI 项目名:</span>
|
||||
<input
|
||||
@@ -174,15 +148,6 @@ export default function Plugin(): JSX.Element {
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-wrap">
|
||||
<span className="mr-2">仓库/主页:</span>
|
||||
<input
|
||||
type="text"
|
||||
name="homepage"
|
||||
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<div className="px-4">
|
||||
@@ -211,21 +176,7 @@ export default function Plugin(): JSX.Element {
|
||||
disableAlpha={true}
|
||||
onChangeComplete={onChangeColor}
|
||||
/>
|
||||
<div className="flex flex-wrap mt-2 items-center">
|
||||
<span className="mr-2">Type:</span>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => insertTagType("a:")}
|
||||
>
|
||||
Adapter
|
||||
</button>
|
||||
<button
|
||||
className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"
|
||||
onClick={() => insertTagType("t:")}
|
||||
>
|
||||
Topic
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex mt-2">
|
||||
<TagComponent label={label} color={color} />
|
||||
<button
|
||||
|
@@ -5,6 +5,104 @@ toc_max_heading_level: 2
|
||||
|
||||
# 更新日志
|
||||
|
||||
## v2.0.1
|
||||
|
||||
### 🚀 新功能
|
||||
|
||||
- Develop: 添加 Pyright 检查 [@yanyongyu](https://github.com/yanyongyu) ([#2194](https://github.com/nonebot/nonebot2/pull/2194))
|
||||
- Feature: 使用 `typing.override` 标记 [@yanyongyu](https://github.com/yanyongyu) ([#2193](https://github.com/nonebot/nonebot2/pull/2193))
|
||||
- Feature: 补充响应器组属性 [@eya46](https://github.com/eya46) ([#2154](https://github.com/nonebot/nonebot2/pull/2154))
|
||||
- Feature: CommandGroup 支持命令别名添加前缀选项 [@eya46](https://github.com/eya46) ([#2134](https://github.com/nonebot/nonebot2/pull/2134))
|
||||
- Feature: 添加用于动态继承支持适配器数据的方法 [@NCBM](https://github.com/NCBM) ([#2127](https://github.com/nonebot/nonebot2/pull/2127))
|
||||
- Feature: 添加内置插件的插件元数据 [@yanyongyu](https://github.com/yanyongyu) ([#2113](https://github.com/nonebot/nonebot2/pull/2113))
|
||||
- Feature: 插件商店适配最新的插件元数据 [@he0119](https://github.com/he0119) ([#2094](https://github.com/nonebot/nonebot2/pull/2094))
|
||||
- Feature: 依赖注入支持 Generic TypeVar 和 Matcher 重载 [@yanyongyu](https://github.com/yanyongyu) ([#2089](https://github.com/nonebot/nonebot2/pull/2089))
|
||||
|
||||
### 🐛 Bug 修复
|
||||
|
||||
- Fix: 修复 Quart WS task 上下文错误 [@yanyongyu](https://github.com/yanyongyu) ([#2192](https://github.com/nonebot/nonebot2/pull/2192))
|
||||
- Fix: 修复 dotenv 配置项为 None 将会跳过赋值 [@eya46](https://github.com/eya46) ([#2143](https://github.com/nonebot/nonebot2/pull/2143))
|
||||
- Fix: 修复 `ArgParam` 不支持 `Annotated` [@eya46](https://github.com/eya46) ([#2124](https://github.com/nonebot/nonebot2/pull/2124))
|
||||
- Fix: aiohttp 请求时 data 和 file 不能同时存在 [@j1g5awi](https://github.com/j1g5awi) ([#2088](https://github.com/nonebot/nonebot2/pull/2088))
|
||||
- Fix: 修复因 loguru 更新导致的启动和关闭日志 name 不正常 [@DiheChen](https://github.com/DiheChen) ([#2080](https://github.com/nonebot/nonebot2/pull/2080))
|
||||
|
||||
### 📝 文档
|
||||
|
||||
- Docs: 移动 Alconna 文档至最佳实践 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2208](https://github.com/nonebot/nonebot2/pull/2208))
|
||||
- Docs: 移除商店中不符合现规范的 tag [@j1g5awi](https://github.com/j1g5awi) ([#2205](https://github.com/nonebot/nonebot2/pull/2205))
|
||||
- Docs: 添加 scoped 插件配置指南 [@yanyongyu](https://github.com/yanyongyu) ([#2198](https://github.com/nonebot/nonebot2/pull/2198))
|
||||
- Docs: 钩子函数代码片段补充 [@A-kirami](https://github.com/A-kirami) ([#2173](https://github.com/nonebot/nonebot2/pull/2173))
|
||||
- Docs: 格式化钩子函数中的代码片段 [@A-kirami](https://github.com/A-kirami) ([#2172](https://github.com/nonebot/nonebot2/pull/2172))
|
||||
- Docs: 补充 Message.only 文档 [@eya46](https://github.com/eya46) ([#2155](https://github.com/nonebot/nonebot2/pull/2155))
|
||||
- Docs: 修复日志自定义文档 typo [@17TheWord](https://github.com/17TheWord) ([#2140](https://github.com/nonebot/nonebot2/pull/2140))
|
||||
- Docs: 修复依赖注入文档 `ArgStr` 3.9+ 和 3.8+ 版本代码写反 [@eya46](https://github.com/eya46) ([#2126](https://github.com/nonebot/nonebot2/pull/2126))
|
||||
- Docs: 删除商店插件发布多余模块 [@forchannot](https://github.com/forchannot) ([#2095](https://github.com/nonebot/nonebot2/pull/2095))
|
||||
- Docs: 微调插件元数据的部分描述 [@NCBM](https://github.com/NCBM) ([#2096](https://github.com/nonebot/nonebot2/pull/2096))
|
||||
- Docs: 完成发布插件教程 [@NCBM](https://github.com/NCBM) ([#2078](https://github.com/nonebot/nonebot2/pull/2078))
|
||||
- Docs: 更新插件元数据的相关描述 [@NCBM](https://github.com/NCBM) ([#2087](https://github.com/nonebot/nonebot2/pull/2087))
|
||||
- Docs: 添加 Villa 适配器到 README [@CMHopeSunshine](https://github.com/CMHopeSunshine) ([#2086](https://github.com/nonebot/nonebot2/pull/2086))
|
||||
|
||||
### 💫 杂项
|
||||
|
||||
- Plugin: 黑白名单添加标签 [@A-kirami](https://github.com/A-kirami) ([#2170](https://github.com/nonebot/nonebot2/pull/2170))
|
||||
- Plugin: 修改 nonebot-plugin-ocgbot-v2 插件名称 [@fireinsect](https://github.com/fireinsect) ([#2147](https://github.com/nonebot/nonebot2/pull/2147))
|
||||
- Plugin: 更新 SparkGPT 插件描述 [@canxin121](https://github.com/canxin121) ([#2144](https://github.com/nonebot/nonebot2/pull/2144))
|
||||
- Plugin: 修改 nonebot-plugin-ocgbot-v2 插件名称 [@fireinsect](https://github.com/fireinsect) ([#2141](https://github.com/nonebot/nonebot2/pull/2141))
|
||||
- Plugin: 删除 nonebot-plugin-phlogo [@kexue-z](https://github.com/kexue-z) ([#2128](https://github.com/nonebot/nonebot2/pull/2128))
|
||||
- Plugin: 修改 `nonebot-plugin-gw2` 模块名 [@Agnes4m](https://github.com/Agnes4m) ([#2123](https://github.com/nonebot/nonebot2/pull/2123))
|
||||
- Develop: 添加 ruff linter [@yanyongyu](https://github.com/yanyongyu) ([#2114](https://github.com/nonebot/nonebot2/pull/2114))
|
||||
- Plugin: 更新 `nonebot-plugin-msgbuf` 插件的名称等信息 [@NCBM](https://github.com/NCBM) ([#2119](https://github.com/nonebot/nonebot2/pull/2119))
|
||||
- Plugin: 修改插件信息和仓库地址 [@Agnes4m](https://github.com/Agnes4m) ([#2115](https://github.com/nonebot/nonebot2/pull/2115))
|
||||
- Test: 移除 httpbin 并整理测试 [@yanyongyu](https://github.com/yanyongyu) ([#2110](https://github.com/nonebot/nonebot2/pull/2110))
|
||||
- CI: 缓存 NoneFlow 所需的 pre-commit hooks [@he0119](https://github.com/he0119) ([#2104](https://github.com/nonebot/nonebot2/pull/2104))
|
||||
- Plugin: 移除过时未更新的插件\&Bot [@FYWinds](https://github.com/FYWinds) ([#2072](https://github.com/nonebot/nonebot2/pull/2072))
|
||||
- Plugin: 删除插件 nonebot_plugin_r6s [@BalconyJH](https://github.com/BalconyJH) ([#2071](https://github.com/nonebot/nonebot2/pull/2071))
|
||||
|
||||
### 🍻 插件发布
|
||||
|
||||
- Plugin: 方寸狭间 [@noneflow](https://github.com/noneflow) ([#2207](https://github.com/nonebot/nonebot2/pull/2207))
|
||||
- Plugin: DALL-E 绘图 [@noneflow](https://github.com/noneflow) ([#2204](https://github.com/nonebot/nonebot2/pull/2204))
|
||||
- Plugin: 指定戳一戳 [@noneflow](https://github.com/noneflow) ([#2202](https://github.com/nonebot/nonebot2/pull/2202))
|
||||
- Plugin: templates_render [@noneflow](https://github.com/noneflow) ([#2197](https://github.com/nonebot/nonebot2/pull/2197))
|
||||
- Plugin: MongoDB [@noneflow](https://github.com/noneflow) ([#2189](https://github.com/nonebot/nonebot2/pull/2189))
|
||||
- Plugin: pjsk 表情 [@noneflow](https://github.com/noneflow) ([#2187](https://github.com/nonebot/nonebot2/pull/2187))
|
||||
- Plugin: nonebot-plugin-wenan [@noneflow](https://github.com/noneflow) ([#2184](https://github.com/nonebot/nonebot2/pull/2184))
|
||||
- Plugin: nonebot-plugin-picture-api [@noneflow](https://github.com/noneflow) ([#2180](https://github.com/nonebot/nonebot2/pull/2180))
|
||||
- Plugin: Blocker [@noneflow](https://github.com/noneflow) ([#2178](https://github.com/nonebot/nonebot2/pull/2178))
|
||||
- Plugin: nonebot-plugin-nobahpicture [@noneflow](https://github.com/noneflow) ([#2176](https://github.com/nonebot/nonebot2/pull/2176))
|
||||
- Plugin: 过期事件过滤器 [@noneflow](https://github.com/noneflow) ([#2169](https://github.com/nonebot/nonebot2/pull/2169))
|
||||
- Plugin: 猫猫虫咖波图片发送 [@noneflow](https://github.com/noneflow) ([#2167](https://github.com/nonebot/nonebot2/pull/2167))
|
||||
- Plugin: nonebot-plugin-splatoon3 [@noneflow](https://github.com/noneflow) ([#2165](https://github.com/nonebot/nonebot2/pull/2165))
|
||||
- Plugin: nonebot-plugin-cfassistant [@noneflow](https://github.com/noneflow) ([#2164](https://github.com/nonebot/nonebot2/pull/2164))
|
||||
- Plugin: 算法竞赛比赛查询 [@noneflow](https://github.com/noneflow) ([#2159](https://github.com/nonebot/nonebot2/pull/2159))
|
||||
- Plugin: nonebot-plugin-update [@noneflow](https://github.com/noneflow) ([#2153](https://github.com/nonebot/nonebot2/pull/2153))
|
||||
- Plugin: 远程同意好友 [@noneflow](https://github.com/noneflow) ([#2146](https://github.com/nonebot/nonebot2/pull/2146))
|
||||
- Plugin: 戳一戳事件 [@noneflow](https://github.com/noneflow) ([#2139](https://github.com/nonebot/nonebot2/pull/2139))
|
||||
- Plugin: EitherChoice [@noneflow](https://github.com/noneflow) ([#2137](https://github.com/nonebot/nonebot2/pull/2137))
|
||||
- Plugin: 用户信息 [@noneflow](https://github.com/noneflow) ([#2133](https://github.com/nonebot/nonebot2/pull/2133))
|
||||
- Plugin: Diablo4 地狱狂潮 boss 提醒小助手 [@noneflow](https://github.com/noneflow) ([#2122](https://github.com/nonebot/nonebot2/pull/2122))
|
||||
- Plugin: nonbot-plugin-ocgbot-v2 [@noneflow](https://github.com/noneflow) ([#2120](https://github.com/nonebot/nonebot2/pull/2120))
|
||||
- Plugin: 错误告警 [@noneflow](https://github.com/noneflow) ([#2117](https://github.com/nonebot/nonebot2/pull/2117))
|
||||
- Plugin: follow_withdraw [@noneflow](https://github.com/noneflow) ([#2112](https://github.com/nonebot/nonebot2/pull/2112))
|
||||
- Plugin: 战雷查水表 [@noneflow](https://github.com/noneflow) ([#2103](https://github.com/nonebot/nonebot2/pull/2103))
|
||||
- Plugin: bili_push [@noneflow](https://github.com/noneflow) ([#2101](https://github.com/nonebot/nonebot2/pull/2101))
|
||||
- Plugin: AI 作曲 [@noneflow](https://github.com/noneflow) ([#2093](https://github.com/nonebot/nonebot2/pull/2093))
|
||||
- Plugin: pcrjjc [@noneflow](https://github.com/noneflow) ([#2091](https://github.com/nonebot/nonebot2/pull/2091))
|
||||
- Plugin: twitter 订阅 [@noneflow](https://github.com/noneflow) ([#2082](https://github.com/nonebot/nonebot2/pull/2082))
|
||||
- Plugin: 链接防夹 [@noneflow](https://github.com/noneflow) ([#2074](https://github.com/nonebot/nonebot2/pull/2074))
|
||||
- Plugin: 碧蓝航线攻略 [@noneflow](https://github.com/noneflow) ([#2076](https://github.com/nonebot/nonebot2/pull/2076))
|
||||
|
||||
### 🍻 机器人发布
|
||||
|
||||
- Bot: 米缸 [@noneflow](https://github.com/noneflow) ([#2191](https://github.com/nonebot/nonebot2/pull/2191))
|
||||
- Bot: 林汐 [@noneflow](https://github.com/noneflow) ([#2182](https://github.com/nonebot/nonebot2/pull/2182))
|
||||
- Bot: web_bot [@noneflow](https://github.com/noneflow) ([#2131](https://github.com/nonebot/nonebot2/pull/2131))
|
||||
- Bot: ReimeiBot-黎明机器人 [@noneflow](https://github.com/noneflow) ([#2107](https://github.com/nonebot/nonebot2/pull/2107))
|
||||
|
||||
### 🍻 适配器发布
|
||||
|
||||
- Adapter: 大别野 [@noneflow](https://github.com/noneflow) ([#2085](https://github.com/nonebot/nonebot2/pull/2085))
|
||||
|
||||
## v2.0.0
|
||||
|
||||
### 💥 破坏性变更
|
||||
|
@@ -153,5 +153,20 @@
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot.adapters.villa",
|
||||
"project_link": "nonebot-adapter-villa",
|
||||
"name": "大别野",
|
||||
"desc": "米游社大别野官方Bot适配",
|
||||
"author": "CMHopeSunshine",
|
||||
"homepage": "https://github.com/CMHopeSunshine/nonebot-adapter-villa",
|
||||
"tags": [
|
||||
{
|
||||
"label": "米哈游",
|
||||
"color": "#e10909"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
}
|
||||
]
|
||||
|
@@ -71,14 +71,6 @@
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "Takker",
|
||||
"desc": "综合了各种娱乐功能的Bot",
|
||||
"author": "FYWinds",
|
||||
"homepage": "https://github.com/FYWinds/takker",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "剑网三bot",
|
||||
"desc": "网络游戏《剑侠情缘三》的群聊机器人,数据使用:www.jx3api.com",
|
||||
@@ -163,10 +155,6 @@
|
||||
{
|
||||
"label": "AI",
|
||||
"color": "#ea5252"
|
||||
},
|
||||
{
|
||||
"label": "a:onebot",
|
||||
"color": "#000000"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
@@ -261,12 +249,7 @@
|
||||
"desc": "🐱🤖 一个以娱乐功能为主的缝合怪(划掉)QQ机器人,包含一定Furry要素但是不会卖萌(就是逊啦!)",
|
||||
"author": "su226",
|
||||
"homepage": "https://github.com/su226/IdhagnBot",
|
||||
"tags": [
|
||||
{
|
||||
"label": "a:OneBot",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
@@ -339,15 +322,15 @@
|
||||
"homepage": "https://github.com/Rinfair-CSP-A016/SuzunoBot-AGLAS",
|
||||
"tags": [
|
||||
{
|
||||
"label": "t:maimaiDX",
|
||||
"label": "maimaiDX",
|
||||
"color": "#189ede"
|
||||
},
|
||||
{
|
||||
"label": "t:Arcaea",
|
||||
"label": "Arcaea",
|
||||
"color": "#d551ef"
|
||||
},
|
||||
{
|
||||
"label": "t:coc",
|
||||
"label": "coc",
|
||||
"color": "#7fe4d0"
|
||||
}
|
||||
],
|
||||
@@ -434,10 +417,6 @@
|
||||
{
|
||||
"label": "maimai",
|
||||
"color": "#52eaa5"
|
||||
},
|
||||
{
|
||||
"label": "a:onebot",
|
||||
"color": "#000000"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
@@ -447,12 +426,7 @@
|
||||
"desc": "一个会拆家的高性能缝合萝卜子",
|
||||
"author": "tkgs0",
|
||||
"homepage": "https://github.com/tkgs0/Momoko",
|
||||
"tags": [
|
||||
{
|
||||
"label": "a:onebot",
|
||||
"color": "#000000"
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
@@ -468,12 +442,7 @@
|
||||
"desc": "简单的QQ功能型机器人",
|
||||
"author": "This-is-XiaoDeng",
|
||||
"homepage": "https://github.com/ITCraftDevelopmentTeam/XDbot2",
|
||||
"tags": [
|
||||
{
|
||||
"label": "a:onebot",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
@@ -521,14 +490,56 @@
|
||||
"homepage": "https://github.com/bingqiu456/shouyun",
|
||||
"tags": [
|
||||
{
|
||||
"label": "a:onebot",
|
||||
"color": "#ea5252"
|
||||
},
|
||||
{
|
||||
"label": "t:shouyun",
|
||||
"label": "shouyun",
|
||||
"color": "#52ea7a"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "ReimeiBot-黎明机器人",
|
||||
"desc": "流星飞逝,黎明终将到来。",
|
||||
"author": "ThirdBlood",
|
||||
"homepage": "https://github.com/3rdBit/ReimeiBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "web_bot",
|
||||
"desc": "把机器人搬到网络上",
|
||||
"author": "wsdtl",
|
||||
"homepage": "https://github.com/wsdtl/web_bot",
|
||||
"tags": [
|
||||
{
|
||||
"label": "xiaonan",
|
||||
"color": "#775151"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "林汐",
|
||||
"desc": "多平台功能型Bot",
|
||||
"author": "mute23-code",
|
||||
"homepage": "https://github.com/netsora/SoraBot",
|
||||
"tags": [
|
||||
{
|
||||
"label": "QQ频道",
|
||||
"color": "#f47070"
|
||||
},
|
||||
{
|
||||
"label": "OneBot v11",
|
||||
"color": "#212121"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "米缸",
|
||||
"desc": "基于nonebot2的米缸Bot",
|
||||
"author": "LambdaYH",
|
||||
"homepage": "https://github.com/LambdaYH/MigangBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
}
|
||||
]
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1155,8 +1155,8 @@ from typing import Annotated
|
||||
from nonebot.params import ArgStr
|
||||
|
||||
@matcher.got("key")
|
||||
async def _(key: str = ArgStr()): ...
|
||||
async def _(foo: str = ArgStr("key")): ...
|
||||
async def _(key: Annotated[str, ArgStr()]): ...
|
||||
async def _(foo: Annotated[str, ArgStr("key")]): ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -1166,8 +1166,8 @@ async def _(foo: str = ArgStr("key")): ...
|
||||
from nonebot.params import ArgStr
|
||||
|
||||
@matcher.got("key")
|
||||
async def _(key: Annotated[str, ArgStr()]): ...
|
||||
async def _(foo: Annotated[str, ArgStr("key")]): ...
|
||||
async def _(key: str = ArgStr()): ...
|
||||
async def _(foo: str = ArgStr("key")): ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
@@ -25,6 +25,10 @@ options:
|
||||
这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
@driver.on_startup
|
||||
async def do_something():
|
||||
pass
|
||||
@@ -35,6 +39,10 @@ async def do_something():
|
||||
这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
@driver.on_shutdown
|
||||
async def do_something():
|
||||
pass
|
||||
@@ -45,6 +53,10 @@ async def do_something():
|
||||
这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
@driver.on_bot_connect
|
||||
async def do_something(bot: Bot):
|
||||
pass
|
||||
@@ -55,6 +67,10 @@ async def do_something(bot: Bot):
|
||||
这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
@driver.on_bot_disconnect
|
||||
async def do_something(bot: Bot):
|
||||
pass
|
||||
@@ -82,6 +98,7 @@ async def do_something(event: Event):
|
||||
|
||||
```python
|
||||
from nonebot.message import event_postprocessor
|
||||
|
||||
@event_postprocessor
|
||||
async def do_something(event: Event):
|
||||
pass
|
||||
@@ -93,6 +110,7 @@ async def do_something(event: Event):
|
||||
|
||||
```python
|
||||
from nonebot.message import run_preprocessor
|
||||
|
||||
@run_preprocessor
|
||||
async def do_something(event: Event, matcher: Matcher):
|
||||
pass
|
||||
|
@@ -148,6 +148,15 @@ MessageSegment.text("text") in message
|
||||
"text" in message
|
||||
```
|
||||
|
||||
我们还可以使用消息序列的 `only` 方法来检查消息中是否仅包含指定的消息段。
|
||||
|
||||
```python
|
||||
# 是否都为指定消息段
|
||||
message.only(MessageSegment.text("test"))
|
||||
# 是否仅包含指定类型的消息段
|
||||
message.only("text")
|
||||
```
|
||||
|
||||
### 过滤、索引与切片
|
||||
|
||||
消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。
|
||||
|
@@ -1139,8 +1139,8 @@ from typing import Annotated
|
||||
from nonebot.params import ArgStr
|
||||
|
||||
@matcher.got("key")
|
||||
async def _(key: str = ArgStr()): ...
|
||||
async def _(foo: str = ArgStr("key")): ...
|
||||
async def _(key: Annotated[str, ArgStr()]): ...
|
||||
async def _(foo: Annotated[str, ArgStr("key")]): ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -1150,8 +1150,8 @@ async def _(foo: str = ArgStr("key")): ...
|
||||
from nonebot.params import ArgStr
|
||||
|
||||
@matcher.got("key")
|
||||
async def _(key: Annotated[str, ArgStr()]): ...
|
||||
async def _(foo: Annotated[str, ArgStr("key")]): ...
|
||||
async def _(key: str = ArgStr()): ...
|
||||
async def _(foo: str = ArgStr("key")): ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
@@ -25,6 +25,10 @@ options:
|
||||
这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
@driver.on_startup
|
||||
async def do_something():
|
||||
pass
|
||||
@@ -35,6 +39,10 @@ async def do_something():
|
||||
这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
@driver.on_shutdown
|
||||
async def do_something():
|
||||
pass
|
||||
@@ -45,6 +53,10 @@ async def do_something():
|
||||
这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
@driver.on_bot_connect
|
||||
async def do_something(bot: Bot):
|
||||
pass
|
||||
@@ -55,6 +67,10 @@ async def do_something(bot: Bot):
|
||||
这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
@driver.on_bot_disconnect
|
||||
async def do_something(bot: Bot):
|
||||
pass
|
||||
@@ -82,6 +98,7 @@ async def do_something(event: Event):
|
||||
|
||||
```python
|
||||
from nonebot.message import event_postprocessor
|
||||
|
||||
@event_postprocessor
|
||||
async def do_something(event: Event):
|
||||
pass
|
||||
@@ -93,6 +110,7 @@ async def do_something(event: Event):
|
||||
|
||||
```python
|
||||
from nonebot.message import run_preprocessor
|
||||
|
||||
@run_preprocessor
|
||||
async def do_something(event: Event, matcher: Matcher):
|
||||
pass
|
||||
|
49
website/versioned_docs/version-2.0.1/README.md
Normal file
49
website/versioned_docs/version-2.0.1/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
id: index
|
||||
slug: /
|
||||
---
|
||||
|
||||
# 概览
|
||||
|
||||
NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架(下称 NoneBot),它基于 Python 的类型注解和异步优先特性(兼容同步),能够为你的需求实现提供便捷灵活的支持。同时,NoneBot 拥有大量的开发者为其开发插件,用户无需编写任何代码,仅需完成环境配置及插件安装,就可以正常使用 NoneBot。
|
||||
|
||||
需要注意的是,NoneBot 仅支持 **Python 3.8 以上版本**
|
||||
|
||||
## 特色
|
||||
|
||||
### 异步优先
|
||||
|
||||
NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。
|
||||
|
||||
### 完整的类型注解
|
||||
|
||||
NoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 Pyright(Pylance) 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./editor-support))。
|
||||
|
||||
### 开箱即用
|
||||
|
||||
NoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。
|
||||
|
||||
### 插件系统
|
||||
|
||||
插件系统是 NoneBot 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。
|
||||
|
||||
### 依赖注入系统
|
||||
|
||||
NoneBot 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。
|
||||
|
||||
#### 什么是依赖注入
|
||||
|
||||
[**『依赖注入』**](https://zh.m.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**『依赖』**。
|
||||
|
||||
系统(在这里是指 NoneBot)将负责做任何需要的事情,为你的代码提供这些必要依赖(即**『注入』**依赖性)
|
||||
|
||||
这在你有以下情形的需求时非常有用:
|
||||
|
||||
- 这部分代码拥有共享的逻辑(同样的代码逻辑多次重复)
|
||||
- 共享数据库以及网络请求连接会话
|
||||
- 比如 `httpx.AsyncClient`、`aiohttp.ClientSession` 和 `sqlalchemy.Session`
|
||||
- 机器人用户权限检查以及认证
|
||||
- 还有更多...
|
||||
|
||||
它在完成上述工作的同时,还能尽量减少代码的耦合和重复
|
161
website/versioned_docs/version-2.0.1/advanced/adapter.md
Normal file
161
website/versioned_docs/version-2.0.1/advanced/adapter.md
Normal file
@@ -0,0 +1,161 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: 注册适配器与指定平台交互
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 20
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 使用适配器
|
||||
|
||||
适配器 (Adapter) 是机器人与平台交互的核心桥梁,它负责在驱动器和机器人插件之间转换与传递消息。
|
||||
|
||||
## 适配器功能与组成
|
||||
|
||||
适配器通常有两种功能,分别是**接收事件**和**调用平台接口**。其中,接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型,然后交由机器人插件处理;调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式,然后交由驱动器发送,并接收接口返回数据。
|
||||
|
||||
为了实现这两种功能,适配器通常由四个部分组成:
|
||||
|
||||
- **Adapter**:负责转换事件和调用接口,正确创建 Bot 对象并注册到 NoneBot 中。
|
||||
- **Bot**:负责存储平台机器人相关信息,并提供回复事件的方法。
|
||||
- **Event**:负责定义事件内容,以及事件主体对象。
|
||||
- **Message**:负责正确序列化消息,以便机器人插件处理。
|
||||
|
||||
## 注册适配器
|
||||
|
||||
在使用适配器之前,我们需要先将适配器注册到驱动器中,这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例,来看看如何注册适配器:
|
||||
|
||||
```python {2,5} title=bot.py
|
||||
import nonebot
|
||||
from nonebot.adapters.console import Adapter
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
driver.register_adapter(Adapter)
|
||||
```
|
||||
|
||||
我们首先需要从适配器模块中导入所需要的适配器类,然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。如果我们需要多平台支持,可以多次调用 `register_adapter` 方法来注册多个适配器。
|
||||
|
||||
## 获取已注册的适配器
|
||||
|
||||
NoneBot 提供了 `get_adapter` 方法来获取已注册的适配器,我们可以通过适配器的名称或类型来获取指定的适配器实例:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
from nonebot.adapters.console import Adapter
|
||||
|
||||
adapters = nonebot.get_adapters()
|
||||
console_adapter = nonebot.get_adapter(Adapter)
|
||||
console_adapter = nonebot.get_adapter(Adapter.get_name())
|
||||
```
|
||||
|
||||
## 获取 Bot 对象
|
||||
|
||||
当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取,这是一个以机器人 ID 为键的字典:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
bots = nonebot.get_bots()
|
||||
```
|
||||
|
||||
我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数,将会返回所有 Bot 中的第一个:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
bot = nonebot.get_bot("bot_id")
|
||||
```
|
||||
|
||||
如果需要获取指定适配器连接的 Bot 对象,我们可以通过适配器的 `bots` 属性获取,这也是一个以机器人 ID 为键的字典:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
from nonebot.adapters.console import Adapter
|
||||
|
||||
console_adapter = nonebot.get_adapter(Adapter)
|
||||
bots = console_adapter.bots
|
||||
```
|
||||
|
||||
Bot 对象都具有一个 `self_id` 属性,它是机器人的唯一 ID,由适配器填写,通常为机器人的帐号 ID 或者 APP ID。
|
||||
|
||||
## 获取事件通用信息
|
||||
|
||||
适配器的所有事件模型均继承自 `Event` 基类,在[事件类型与重载](../appendices/overload.md)一节中,我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息:
|
||||
|
||||
### 事件类型
|
||||
|
||||
事件类型通常为 `meta_event`、`message`、`notice`、`request`。
|
||||
|
||||
```python
|
||||
type: str = event.get_type()
|
||||
```
|
||||
|
||||
### 事件名称
|
||||
|
||||
事件名称由适配器定义,通常用于日志记录。
|
||||
|
||||
```python
|
||||
name: str = event.get_event_name()
|
||||
```
|
||||
|
||||
### 事件描述
|
||||
|
||||
事件描述由适配器定义,通常用于日志记录。
|
||||
|
||||
```python
|
||||
description: str = event.get_event_description()
|
||||
```
|
||||
|
||||
### 事件日志字符串
|
||||
|
||||
事件日志字符串由事件名称和事件描述组成,用于日志记录。
|
||||
|
||||
```python
|
||||
log: str = event.get_log_string()
|
||||
```
|
||||
|
||||
### 事件主体 ID
|
||||
|
||||
事件主体 ID 通常为机器人用户 ID。
|
||||
|
||||
```python
|
||||
user_id: str = event.get_user_id()
|
||||
```
|
||||
|
||||
### 事件会话 ID
|
||||
|
||||
事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。
|
||||
|
||||
```python
|
||||
session_id: str = event.get_session_id()
|
||||
```
|
||||
|
||||
### 事件消息
|
||||
|
||||
如果事件包含消息,则可以通过该方法获取,否则会产生异常。
|
||||
|
||||
```python
|
||||
message: Message = event.get_message()
|
||||
```
|
||||
|
||||
### 事件纯文本消息
|
||||
|
||||
通常为事件消息的纯文本内容,如果事件不包含消息,则会产生异常。
|
||||
|
||||
```python
|
||||
text: str = event.get_plaintext()
|
||||
```
|
||||
|
||||
### 事件是否与机器人有关
|
||||
|
||||
由适配器实现的判断,通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。
|
||||
|
||||
```python
|
||||
is_tome: bool = event.is_tome()
|
||||
```
|
||||
|
||||
## 更多
|
||||
|
||||
官方支持的适配器和社区贡献的适配器均可在[商店](/store)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。
|
1216
website/versioned_docs/version-2.0.1/advanced/dependency.mdx
Normal file
1216
website/versioned_docs/version-2.0.1/advanced/dependency.mdx
Normal file
File diff suppressed because it is too large
Load Diff
286
website/versioned_docs/version-2.0.1/advanced/driver.md
Normal file
286
website/versioned_docs/version-2.0.1/advanced/driver.md
Normal file
@@ -0,0 +1,286 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
description: 选择合适的驱动器运行机器人
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 10
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 选择驱动器
|
||||
|
||||
驱动器 (Driver) 是机器人运行的基石,它是机器人初始化的第一步,主要负责数据收发。
|
||||
|
||||
:::important 提示
|
||||
驱动器的选择通常与机器人所使用的协议适配器相关,如果不知道该选择哪个驱动器,可以先阅读相关协议适配器文档说明。
|
||||
:::
|
||||
|
||||
:::tip 提示
|
||||
如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。
|
||||
:::
|
||||
|
||||
## 驱动器类型
|
||||
|
||||
驱动器的类型有两种:
|
||||
|
||||
- `ForwardDriver`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。
|
||||
- `ReverseDriver`:即服务端型驱动器,多用于使用 WebHook,接收 WebSocket 客户端连接等情形。
|
||||
|
||||
客户端型驱动器具有以下两种功能:
|
||||
|
||||
1. 异步发送 HTTP 请求,自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。
|
||||
2. 异步建立 WebSocket 连接上下文,自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。
|
||||
|
||||
服务端型驱动器通常为 ASGI 应用框架,具有以下功能:
|
||||
|
||||
1. 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。
|
||||
2. 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。
|
||||
3. 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。
|
||||
|
||||
## 配置驱动器
|
||||
|
||||
驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍,这里将详细介绍驱动器配置的格式。
|
||||
|
||||
NoneBot 中的客户端和服务端型驱动器可以相互配合使用,但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类,即驱动器类,他可以作为驱动器单独运行。同时,客户端驱动器模块中还会提供一个 `Mixin` 子类,用于在与其他驱动器配合使用时加载。因此,驱动器配置格式采用特殊语法:`<module>[:<Driver>][+<module>[:<Mixin>]]*`。
|
||||
|
||||
其中,`<module>` 代表**驱动器模块路径**;`<Driver>` 代表**驱动器类名**,默认为 `Driver`;`<Mixin>` 代表**驱动器混入类名**,默认为 `Mixin`。即,我们需要选择一个主要驱动器,然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型,但混入类驱动器只能为客户端类型。
|
||||
|
||||
特别的,为了简化内置驱动器模块路径,我们可以使用 `~` 符号作为内置驱动器模块路径的前缀,如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配,但需要安装额外依赖才能使用,具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下:
|
||||
|
||||
```dotenv
|
||||
DRIVER=~fastapi
|
||||
DRIVER=~aiohttp
|
||||
DRIVER=~httpx+~websockets
|
||||
DRIVER=~fastapi+~httpx+~websockets
|
||||
```
|
||||
|
||||
## 获取驱动器
|
||||
|
||||
在 NoneBot 框架初始化完成后,我们就可以通过 `get_driver()` 方法获取全局驱动器实例:
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
```
|
||||
|
||||
## 内置驱动器
|
||||
|
||||
### None
|
||||
|
||||
**类型:**服务端驱动器
|
||||
|
||||
NoneBot 内置的空驱动器,不提供任何收发数据功能,可以在不需要外部网络连接时使用。
|
||||
|
||||
```env
|
||||
DRIVER=~none
|
||||
```
|
||||
|
||||
### FastAPI(默认)
|
||||
|
||||
**类型:**服务端驱动器
|
||||
|
||||
> FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
|
||||
|
||||
[FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架,具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能,也可以挂载其他 ASGI、WSGI 应用。
|
||||
|
||||
```env
|
||||
DRIVER=~fastapi
|
||||
```
|
||||
|
||||
#### FastAPI 配置项
|
||||
|
||||
##### `fastapi_openapi_url`
|
||||
|
||||
类型:`str | None`
|
||||
默认值:`None`
|
||||
说明:`FastAPI` 提供的 `OpenAPI` JSON 定义地址,如果为 `None`,则不提供 `OpenAPI` JSON 定义。
|
||||
|
||||
##### `fastapi_docs_url`
|
||||
|
||||
类型:`str | None`
|
||||
默认值:`None`
|
||||
说明:`FastAPI` 提供的 `Swagger` 文档地址,如果为 `None`,则不提供 `Swagger` 文档。
|
||||
|
||||
##### `fastapi_redoc_url`
|
||||
|
||||
类型:`str | None`
|
||||
默认值:`None`
|
||||
说明:`FastAPI` 提供的 `ReDoc` 文档地址,如果为 `None`,则不提供 `ReDoc` 文档。
|
||||
|
||||
##### `fastapi_include_adapter_schema`
|
||||
|
||||
类型:`bool`
|
||||
默认值:`True`
|
||||
说明:`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`。
|
||||
|
||||
##### `fastapi_reload`
|
||||
|
||||
:::warning 警告
|
||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||
|
||||
```bash
|
||||
nb run --reload
|
||||
```
|
||||
|
||||
开启该功能后,在 uvicorn 运行时(FastAPI 提供的 ASGI 底层,即 reload 功能的实际来源),asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`。
|
||||
|
||||
> 相关信息参考 [uvicorn#529](https://github.com/encode/uvicorn/issues/529),[uvicorn#1070](https://github.com/encode/uvicorn/pull/1070),[uvicorn#1257](https://github.com/encode/uvicorn/pull/1257)
|
||||
|
||||
后者(`SelectorEventLoop`)在 Windows 平台的可使用性不如前者(`ProactorEventLoop`),包括但不限于
|
||||
|
||||
1. 不支持创建子进程
|
||||
2. 最多只支持 512 个套接字
|
||||
3. ...
|
||||
|
||||
> 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows)
|
||||
|
||||
所以,一些使用了 asyncio 的库因此可能无法正常工作,如:
|
||||
|
||||
1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows)
|
||||
|
||||
如果在开启该功能后,原本**正常运行**的代码报错,且打印的异常堆栈信息和 asyncio 有关(异常一般为 `NotImplementedError`),
|
||||
你可能就需要考虑相关库对事件循环的支持,以及是否启用该功能。
|
||||
:::
|
||||
|
||||
类型:`bool`
|
||||
默认值:`False`
|
||||
说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。
|
||||
|
||||
```python title=bot.py
|
||||
app = nonebot.get_asgi()
|
||||
nonebot.run(app="bot:app")
|
||||
```
|
||||
|
||||
##### `fastapi_reload_dirs`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:重载监控文件夹列表,默认为 uvicorn 默认值
|
||||
|
||||
##### `fastapi_reload_delay`
|
||||
|
||||
类型:`float | None`
|
||||
默认值:`None`
|
||||
说明:重载延迟,默认为 uvicorn 默认值
|
||||
|
||||
##### `fastapi_reload_includes`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
||||
|
||||
##### `fastapi_reload_excludes`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
||||
|
||||
##### `fastapi_extra`
|
||||
|
||||
类型:`Dist[str, Any]`
|
||||
默认值:`{}`
|
||||
说明:传递给 `FastAPI` 的其他参数
|
||||
|
||||
### Quart
|
||||
|
||||
**类型:**`ReverseDriver`
|
||||
|
||||
> Quart is an asyncio reimplementation of the popular Flask microframework API.
|
||||
|
||||
[Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本,拥有与 Flask 非常相似的接口和使用方法。
|
||||
|
||||
```env
|
||||
DRIVER=~quart
|
||||
```
|
||||
|
||||
#### Quart 配置项
|
||||
|
||||
##### `quart_reload`
|
||||
|
||||
:::warning 警告
|
||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||
|
||||
```bash
|
||||
nb run --reload
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
类型:`bool`
|
||||
默认值:`False`
|
||||
说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。
|
||||
|
||||
```python title=bot.py
|
||||
app = nonebot.get_asgi()
|
||||
nonebot.run(app="bot:app")
|
||||
```
|
||||
|
||||
##### `quart_reload_dirs`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:重载监控文件夹列表,默认为 uvicorn 默认值
|
||||
|
||||
##### `quart_reload_delay`
|
||||
|
||||
类型:`float | None`
|
||||
默认值:`None`
|
||||
说明:重载延迟,默认为 uvicorn 默认值
|
||||
|
||||
##### `quart_reload_includes`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
||||
|
||||
##### `quart_reload_excludes`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
||||
|
||||
##### `quart_extra`
|
||||
|
||||
类型:`Dist[str, Any]`
|
||||
默认值:`{}`
|
||||
说明:传递给 `Quart` 的其他参数
|
||||
|
||||
### HTTPX
|
||||
|
||||
**类型:**`ForwardDriver`
|
||||
|
||||
:::warning 注意
|
||||
本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。
|
||||
:::
|
||||
|
||||
> [HTTPX](https://www.python-httpx.org/) is a fully featured HTTP client for Python 3, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2.
|
||||
|
||||
```env
|
||||
DRIVER=~httpx
|
||||
```
|
||||
|
||||
### websockets
|
||||
|
||||
**类型:**`ForwardDriver`
|
||||
|
||||
:::warning 注意
|
||||
本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。
|
||||
:::
|
||||
|
||||
> [websockets](https://websockets.readthedocs.io/) is a library for building WebSocket servers and clients in Python with a focus on correctness, simplicity, robustness, and performance.
|
||||
|
||||
```env
|
||||
DRIVER=~websockets
|
||||
```
|
||||
|
||||
### AIOHTTP
|
||||
|
||||
**类型:**`ForwardDriver`
|
||||
|
||||
> [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python.
|
||||
|
||||
```env
|
||||
DRIVER=~aiohttp
|
||||
```
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user