Compare commits

..

1 Commits

Author SHA1 Message Date
N791
d8c36e8eff 🍻 publish plugin nonebot-plugin-ehentai-search (#2884) 2024-08-17 10:48:42 +08:00
427 changed files with 13044 additions and 18708 deletions

View File

@@ -9,11 +9,13 @@
"vscode": { "vscode": {
"settings": { "settings": {
"python.analysis.diagnosticMode": "workspace", "python.analysis.diagnosticMode": "workspace",
"python.analysis.typeCheckingMode": "basic",
"ruff.organizeImports": false,
"[python]": { "[python]": {
"editor.defaultFormatter": "charliermarsh.ruff", "editor.defaultFormatter": "ms-python.black-formatter",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit", "source.fixAll.ruff": true,
"source.organizeImports": "explicit" "source.organizeImports": true
} }
}, },
"[javascript]": { "[javascript]": {
@@ -42,6 +44,8 @@
"extensions": [ "extensions": [
"ms-python.python", "ms-python.python",
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"ms-python.isort",
"ms-python.black-formatter",
"charliermarsh.ruff", "charliermarsh.ruff",
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,2 @@
open_collective: nonebot open_collective: nonebot
custom: ["https://afdian.com/@nonebot"] custom: ["https://afdian.net/@nonebot"]

View File

@@ -1,7 +1,7 @@
name: 发布适配器 name: 发布适配器
title: "Adapter: {name}" title: "Adapter: {name}"
description: 发布适配器到 NoneBot 官方商店 description: 发布适配器到 NoneBot 官方商店
labels: ["Adapter", "Publish"] labels: ["Adapter"]
body: body:
- type: input - type: input
id: name id: name

View File

@@ -1,7 +1,7 @@
name: 发布机器人 name: 发布机器人
title: "Bot: {name}" title: "Bot: {name}"
description: 发布机器人到 NoneBot 官方商店 description: 发布机器人到 NoneBot 官方商店
labels: ["Bot", "Publish"] labels: ["Bot"]
body: body:
- type: input - type: input
id: name id: name

View File

@@ -1,7 +1,7 @@
name: 发布插件 name: 发布插件
title: "Plugin: {name}" title: "Plugin: {name}"
description: 发布插件到 NoneBot 官方商店 description: 发布插件到 NoneBot 官方商店
labels: ["Plugin", "Publish"] labels: ["Plugin"]
body: body:
- type: input - type: input
id: pypi id: pypi

View File

@@ -35,12 +35,3 @@ updates:
actions: actions:
patterns: patterns:
- "*" - "*"
- package-ecosystem: devcontainers
directory: "/"
schedule:
interval: daily
groups:
devcontainers:
patterns:
- "*"

View File

@@ -48,21 +48,11 @@ jobs:
cd ./envs/${{ matrix.env }} cd ./envs/${{ matrix.env }}
poetry run bash "../../scripts/run-tests.sh" poetry run bash "../../scripts/run-tests.sh"
- name: Upload test results
uses: codecov/test-results-action@v1
with:
env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION
files: ./tests/junit.xml
flags: unittests
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Upload coverage report - name: Upload coverage report
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v4
with: with:
env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION
files: ./tests/coverage.xml files: ./tests/coverage.xml
flags: unittests flags: unittests
fail_ci_if_error: true
env: env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -15,9 +15,9 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
noneflow: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: noneflow name: check
# do not run on forked PRs, do not run on not related issues, do not run on pr comments # do not run on forked PRs, do not run on not related issues, do not run on pr comments
if: | if: |
!( !(
@@ -36,6 +36,70 @@ jobs:
github.event_name == 'issue_comment' && github.event.issue.pull_request github.event_name == 'issue_comment' && github.event.issue.pull_request
) )
) )
steps:
- run: echo "Check passed"
reaction:
runs-on: ubuntu-latest
name: reaction
needs: check
if: |
(
github.event_name == 'issue_comment' &&
github.event.action == 'created'
) ||
(
github.event_name == 'issues' &&
github.event.action == 'opened'
)
steps:
- name: Generate token
id: generate-token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_KEY }}
- name: Reaction on issue
if: github.event_name == 'issues'
run: |
gh api --method POST /repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/reactions -f "content=rocket"
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
- name: Reaction on issue comment
if: github.event_name == 'issue_comment'
run: |
gh api --method POST /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions -f "content=rocket"
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
plugin_test:
runs-on: ubuntu-latest
name: nonebot2 plugin test
needs: check
permissions:
issues: read
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') }}
run: pipx install poetry
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Test Plugin
id: plugin-test
run: |
curl -sSL https://github.com/nonebot/noneflow/releases/latest/download/plugin_test.py | python -
noneflow:
runs-on: ubuntu-latest
name: noneflow
needs: plugin_test
steps: steps:
- name: Generate token - name: Generate token
id: generate-token id: generate-token
@@ -49,17 +113,29 @@ jobs:
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
- name: Cache pre-commit hooks
uses: actions/cache@v4
with:
path: .cache/.pre-commit
key: noneflow-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
- name: NoneFlow - name: NoneFlow
uses: docker://ghcr.io/nonebot/noneflow:latest uses: docker://ghcr.io/nonebot/noneflow:latest
with: with:
config: > config: >
{ {
"base": "master", "base": "master",
"plugin_path": "assets/plugins.json5", "plugin_path": "assets/plugins.json",
"bot_path": "assets/bots.json5", "bot_path": "assets/bots.json",
"adapter_path": "assets/adapters.json5", "adapter_path": "assets/adapters.json"
"registry_repository": "nonebot/registry"
} }
env: 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 }} APP_ID: ${{ secrets.APP_ID }}
PRIVATE_KEY: ${{ secrets.APP_KEY }} 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

View File

@@ -40,7 +40,7 @@ jobs:
- name: Update Changelog - name: Update Changelog
uses: docker://ghcr.io/nonebot/auto-changelog:master uses: docker://ghcr.io/nonebot/auto-changelog:master
with: with:
changelog_file: website/src/changelog/changelog.md changelog_file: website/src/pages/changelog.md
latest_changes_position: '# 更新日志\n\n' latest_changes_position: '# 更新日志\n\n'
latest_changes_title: "## 最近更新" latest_changes_title: "## 最近更新"
replace_regex: '(?<=## 最近更新\n)[\s\S]*?(?=\n## )' replace_regex: '(?<=## 最近更新\n)[\s\S]*?(?=\n## )'

View File

@@ -32,7 +32,7 @@ jobs:
- name: Archive Changelog - name: Archive Changelog
uses: docker://ghcr.io/nonebot/auto-changelog:master uses: docker://ghcr.io/nonebot/auto-changelog:master
with: with:
changelog_file: website/src/changelog/changelog.md changelog_file: website/src/pages/changelog.md
archive_regex: '(?<=## )最近更新(?=\n)' archive_regex: '(?<=## )最近更新(?=\n)'
archive_title: ${{ env.TAG_NAME }} archive_title: ${{ env.TAG_NAME }}
commit_and_push: false commit_and_push: false

View File

@@ -45,16 +45,11 @@ jobs:
- name: Restore Context - name: Restore Context
run: | run: |
PR_NUMBER=$(cat ./pr-number) cat action.env >> $GITHUB_ENV
if ! [[ "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then
echo "Invalid PR number: ${PR_NUMBER}"
exit 1
fi
echo "PR_NUMBER=${PR_NUMBER}" >> "${GITHUB_ENV}"
- name: Set Deploy Name - name: Set Deploy Name
run: | run: |
echo "DEPLOY_NAME=deploy-preview-${PR_NUMBER}" >> "${GITHUB_ENV}" echo "DEPLOY_NAME=deploy-preview-${{ env.PR_NUMBER }}" >> $GITHUB_ENV
- name: Deploy to Netlify - name: Deploy to Netlify
id: deploy id: deploy

View File

@@ -30,7 +30,7 @@ jobs:
- name: Export Context - name: Export Context
run: | run: |
echo "${{ github.event.pull_request.number }}" > ./pr-number echo "PR_NUMBER=${{ github.event.number }}" >> ./action.env
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -38,5 +38,5 @@ jobs:
name: website-preview name: website-preview
path: | path: |
./website/build ./website/build
./pr-number ./action.env
retention-days: 1 retention-days: 1

1
.gitignore vendored
View File

@@ -7,7 +7,6 @@ docs_build/_build
!tests/.env !tests/.env
.docusaurus .docusaurus
website/docs/api/**/*.md website/docs/api/**/*.md
website/src/pages/changelog/**/*
# Created by https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux # Created by https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux
# Edit at https://www.toptal.com/developers/gitignore?templates=python,node,visualstudiocode,jetbrains,macos,windows,linux # Edit at https://www.toptal.com/developers/gitignore?templates=python,node,visualstudiocode,jetbrains,macos,windows,linux

View File

@@ -7,13 +7,30 @@ ci:
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks" autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.1 rev: v0.5.6
hooks: hooks:
- id: ruff - id: ruff
args: [--fix] args: [--fix, --exit-non-zero-on-fix]
stages: [pre-commit] stages: [commit]
- id: ruff-format
stages: [pre-commit] - repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
stages: [commit]
- repo: https://github.com/psf/black
rev: 24.8.0
hooks:
- id: black
stages: [commit]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
stages: [commit]
- repo: https://github.com/nonebot/nonemoji - repo: https://github.com/nonebot/nonemoji
rev: v0.1.4 rev: v0.1.4

View File

@@ -1,3 +1,3 @@
# Changelog # Changelog
See [changelog.md](./website/src/changelog/changelog.md) or <https://nonebot.dev/changelog> See [changelog.md](./website/src/pages/changelog.md) or <https://nonebot.dev/changelog>

View File

@@ -126,6 +126,7 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
| Mirai[仓库](https://github.com/nonebot/adapter-mirai)[协议](https://docs.mirai.mamoe.net/mirai-api-http/) | ✅ | QQ 协议 | | Mirai[仓库](https://github.com/nonebot/adapter-mirai)[协议](https://docs.mirai.mamoe.net/mirai-api-http/) | ✅ | QQ 协议 |
| 钉钉([仓库](https://github.com/nonebot/adapter-ding)[协议](https://open.dingtalk.com/document/) | 🤗 | 寻找 Maintainer暂不可用 | | 钉钉([仓库](https://github.com/nonebot/adapter-ding)[协议](https://open.dingtalk.com/document/) | 🤗 | 寻找 Maintainer暂不可用 |
| 开黑啦([仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila)[协议](https://developer.kookapp.cn/) | ↗️ | 由社区贡献 | | 开黑啦([仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila)[协议](https://developer.kookapp.cn/) | ↗️ | 由社区贡献 |
| Mirai[仓库](https://github.com/ieew/nonebot_adapter_mirai2)[协议](https://docs.mirai.mamoe.net/mirai-api-http/) | ↗️ | QQ 协议,由社区贡献 |
| Ntchat[仓库](https://github.com/JustUndertaker/adapter-ntchat) | ↗️ | 微信协议,由社区贡献 | | Ntchat[仓库](https://github.com/JustUndertaker/adapter-ntchat) | ↗️ | 微信协议,由社区贡献 |
| MineCraft[仓库](https://github.com/17TheWord/nonebot-adapter-minecraft) | ↗️ | 由社区贡献 | | MineCraft[仓库](https://github.com/17TheWord/nonebot-adapter-minecraft) | ↗️ | 由社区贡献 |
| BiliBili Live[仓库](https://github.com/wwweww/adapter-bilibili) | ↗️ | 由社区贡献 | | BiliBili Live[仓库](https://github.com/wwweww/adapter-bilibili) | ↗️ | 由社区贡献 |
@@ -133,7 +134,6 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
| Villa[仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa) | ❌ | 米游社大别野 Bot 协议,官方已下线 | | Villa[仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa) | ❌ | 米游社大别野 Bot 协议,官方已下线 |
| Rocket.Chat[仓库](https://github.com/IUnlimit/nonebot-adapter-rocketchat)[协议](https://developer.rocket.chat/) | ↗️ | Rocket.Chat Bot 协议,由社区贡献 | | Rocket.Chat[仓库](https://github.com/IUnlimit/nonebot-adapter-rocketchat)[协议](https://developer.rocket.chat/) | ↗️ | Rocket.Chat Bot 协议,由社区贡献 |
| Tailchat[仓库](https://github.com/eya46/nonebot-adapter-tailchat)[协议](https://tailchat.msgbyte.com/) | ↗️ | Tailchat 开放平台 Bot 协议,由社区贡献 | | Tailchat[仓库](https://github.com/eya46/nonebot-adapter-tailchat)[协议](https://tailchat.msgbyte.com/) | ↗️ | Tailchat 开放平台 Bot 协议,由社区贡献 |
| Mail[仓库](https://github.com/mobyw/nonebot-adapter-mail) | ↗️ | 邮件收发协议,由社区贡献 |
- 坚实后盾:支持多种 web 框架,可自定义替换、组合 - 坚实后盾:支持多种 web 框架,可自定义替换、组合

View File

@@ -4,7 +4,7 @@
"project_link": "nonebot-adapter-onebot", "project_link": "nonebot-adapter-onebot",
"name": "OneBot V11", "name": "OneBot V11",
"desc": "OneBot V11 协议", "desc": "OneBot V11 协议",
"author_id": 42488585, "author": "yanyongyu",
"homepage": "https://onebot.adapters.nonebot.dev/", "homepage": "https://onebot.adapters.nonebot.dev/",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -14,7 +14,7 @@
"project_link": "nonebot-adapter-ding", "project_link": "nonebot-adapter-ding",
"name": "钉钉", "name": "钉钉",
"desc": "钉钉协议", "desc": "钉钉协议",
"author_id": 1184028, "author": "Artin",
"homepage": "https://github.com/nonebot/adapter-ding", "homepage": "https://github.com/nonebot/adapter-ding",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -24,7 +24,7 @@
"project_link": "nonebot-adapter-feishu", "project_link": "nonebot-adapter-feishu",
"name": "飞书", "name": "飞书",
"desc": "飞书协议", "desc": "飞书协议",
"author_id": 14922941, "author": "StarHeartHunt",
"homepage": "https://github.com/nonebot/adapter-feishu", "homepage": "https://github.com/nonebot/adapter-feishu",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -34,7 +34,7 @@
"project_link": "nonebot-adapter-telegram", "project_link": "nonebot-adapter-telegram",
"name": "Telegram", "name": "Telegram",
"desc": "Telegram 协议", "desc": "Telegram 协议",
"author_id": 50312681, "author": "j1g5awi",
"homepage": "https://github.com/nonebot/adapter-telegram", "homepage": "https://github.com/nonebot/adapter-telegram",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -44,7 +44,7 @@
"project_link": "nonebot-adapter-qq", "project_link": "nonebot-adapter-qq",
"name": "QQ", "name": "QQ",
"desc": "QQ 官方机器人", "desc": "QQ 官方机器人",
"author_id": 42488585, "author": "yanyongyu",
"homepage": "https://github.com/nonebot/adapter-qq", "homepage": "https://github.com/nonebot/adapter-qq",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -54,7 +54,7 @@
"project_link": "nonebot-adapter-kaiheila", "project_link": "nonebot-adapter-kaiheila",
"name": "开黑啦", "name": "开黑啦",
"desc": "开黑啦协议适配", "desc": "开黑啦协议适配",
"author_id": 37477320, "author": "Tian-que",
"homepage": "https://github.com/Tian-que/nonebot-adapter-kaiheila", "homepage": "https://github.com/Tian-que/nonebot-adapter-kaiheila",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -64,17 +64,27 @@
"project_link": "nonebot-adapter-mirai", "project_link": "nonebot-adapter-mirai",
"name": "Mirai", "name": "Mirai",
"desc": "mirai-api-http v2 协议适配", "desc": "mirai-api-http v2 协议适配",
"author_id": 42648639, "author": "RF-Tar-Railt",
"homepage": "https://github.com/nonebot/adapter-mirai", "homepage": "https://github.com/nonebot/adapter-mirai",
"tags": [], "tags": [],
"is_official": true "is_official": true
}, },
{
"module_name": "nonebot.adapters.mirai2",
"project_link": "nonebot_adapter_mirai2",
"name": "mirai2",
"desc": "为 nonebot2 添加 mirai_api_http2.x的兼容适配器",
"author": "ieew",
"homepage": "https://github.com/ieew/nonebot_adapter_mirai2",
"tags": [],
"is_official": false
},
{ {
"module_name": "nonebot.adapters.onebot.v12", "module_name": "nonebot.adapters.onebot.v12",
"project_link": "nonebot-adapter-onebot", "project_link": "nonebot-adapter-onebot",
"name": "OneBot V12", "name": "OneBot V12",
"desc": "OneBot V12 协议", "desc": "OneBot V12 协议",
"author_id": 42488585, "author": "yanyongyu",
"homepage": "https://onebot.adapters.nonebot.dev/", "homepage": "https://onebot.adapters.nonebot.dev/",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -84,7 +94,7 @@
"project_link": "nonebot-adapter-console", "project_link": "nonebot-adapter-console",
"name": "Console", "name": "Console",
"desc": "基于终端的交互式适配器", "desc": "基于终端的交互式适配器",
"author_id": 50488999, "author": "Melodyknit",
"homepage": "https://github.com/nonebot/adapter-console", "homepage": "https://github.com/nonebot/adapter-console",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -94,7 +104,7 @@
"project_link": "nonebot-adapter-github", "project_link": "nonebot-adapter-github",
"name": "GitHub", "name": "GitHub",
"desc": "GitHub APP & OAuth APP integration", "desc": "GitHub APP & OAuth APP integration",
"author_id": 42488585, "author": "yanyongyu",
"homepage": "https://github.com/nonebot/adapter-github", "homepage": "https://github.com/nonebot/adapter-github",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -104,7 +114,7 @@
"project_link": "nonebot-adapter-ntchat", "project_link": "nonebot-adapter-ntchat",
"name": "Ntchat", "name": "Ntchat",
"desc": "pc hook的微信客户端适配", "desc": "pc hook的微信客户端适配",
"author_id": 37363867, "author": "JustUndertaker",
"homepage": "https://github.com/JustUndertaker/adapter-ntchat", "homepage": "https://github.com/JustUndertaker/adapter-ntchat",
"tags": [ "tags": [
{ {
@@ -119,7 +129,7 @@
"project_link": "nonebot-adapter-minecraft", "project_link": "nonebot-adapter-minecraft",
"name": "Minecraft", "name": "Minecraft",
"desc": "MineCraft通信适配支持Rcon", "desc": "MineCraft通信适配支持Rcon",
"author_id": 54731914, "author": "17TheWord",
"homepage": "https://github.com/17TheWord/nonebot-adapter-minecraft", "homepage": "https://github.com/17TheWord/nonebot-adapter-minecraft",
"tags": [ "tags": [
{ {
@@ -134,7 +144,7 @@
"project_link": "nonebot-adapter-bilibili", "project_link": "nonebot-adapter-bilibili",
"name": "BilibiliLive", "name": "BilibiliLive",
"desc": "b站直播间ws协议", "desc": "b站直播间ws协议",
"author_id": 39620657, "author": "wwweww",
"homepage": "https://github.com/wwweww/adapter-bilibili", "homepage": "https://github.com/wwweww/adapter-bilibili",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -144,7 +154,7 @@
"project_link": "nonebot-adapter-walleq", "project_link": "nonebot-adapter-walleq",
"name": "Walle-Q", "name": "Walle-Q",
"desc": "内置 QQ 协议实现", "desc": "内置 QQ 协议实现",
"author_id": 18395948, "author": "abrahum",
"homepage": "https://github.com/onebot-walle/nonebot_adapter_walleq", "homepage": "https://github.com/onebot-walle/nonebot_adapter_walleq",
"tags": [ "tags": [
{ {
@@ -159,7 +169,7 @@
"project_link": "nonebot-adapter-villa", "project_link": "nonebot-adapter-villa",
"name": "大别野", "name": "大别野",
"desc": "米游社大别野官方Bot适配", "desc": "米游社大别野官方Bot适配",
"author_id": 63870437, "author": "CMHopeSunshine",
"homepage": "https://github.com/CMHopeSunshine/nonebot-adapter-villa", "homepage": "https://github.com/CMHopeSunshine/nonebot-adapter-villa",
"tags": [ "tags": [
{ {
@@ -174,7 +184,7 @@
"project_link": "nonebot-adapter-red", "project_link": "nonebot-adapter-red",
"name": "RedProtocol", "name": "RedProtocol",
"desc": "QQNT RedProtocol 适配", "desc": "QQNT RedProtocol 适配",
"author_id": 55650833, "author": "zhaomaoniu",
"homepage": "https://github.com/nonebot/adapter-red", "homepage": "https://github.com/nonebot/adapter-red",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -184,7 +194,7 @@
"project_link": "nonebot-adapter-discord", "project_link": "nonebot-adapter-discord",
"name": "Discord", "name": "Discord",
"desc": "Discord 官方 Bot 协议适配", "desc": "Discord 官方 Bot 协议适配",
"author_id": 63870437, "author": "CMHopeSunshine",
"homepage": "https://github.com/nonebot/adapter-discord", "homepage": "https://github.com/nonebot/adapter-discord",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -194,7 +204,7 @@
"project_link": "nonebot-adapter-satori", "project_link": "nonebot-adapter-satori",
"name": "Satori", "name": "Satori",
"desc": "Satori 协议适配器", "desc": "Satori 协议适配器",
"author_id": 42648639, "author": "RF-Tar-Railt",
"homepage": "https://github.com/nonebot/adapter-satori", "homepage": "https://github.com/nonebot/adapter-satori",
"tags": [ "tags": [
{ {
@@ -209,7 +219,7 @@
"project_link": "nonebot-adapter-dodo", "project_link": "nonebot-adapter-dodo",
"name": "DoDo", "name": "DoDo",
"desc": "DoDo Bot 协议适配器", "desc": "DoDo Bot 协议适配器",
"author_id": 63870437, "author": "CMHopeSunshine",
"homepage": "https://github.com/nonebot/adapter-dodo", "homepage": "https://github.com/nonebot/adapter-dodo",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -219,7 +229,7 @@
"project_link": "nonebot-adapter-rocketchat", "project_link": "nonebot-adapter-rocketchat",
"name": "RocketChat", "name": "RocketChat",
"desc": "RocketChat adapter for nonebot2", "desc": "RocketChat adapter for nonebot2",
"author_id": 78360471, "author": "IllTamer",
"homepage": "https://github.com/IUnlimit/nonebot-adapter-rocketchat", "homepage": "https://github.com/IUnlimit/nonebot-adapter-rocketchat",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -229,7 +239,7 @@
"project_link": "nonebot-adapter-kritor", "project_link": "nonebot-adapter-kritor",
"name": "Kritor", "name": "Kritor",
"desc": "Kritor 协议适配", "desc": "Kritor 协议适配",
"author_id": 42648639, "author": "RF-Tar-Railt",
"homepage": "https://github.com/nonebot/adapter-kritor", "homepage": "https://github.com/nonebot/adapter-kritor",
"tags": [ "tags": [
{ {
@@ -244,19 +254,9 @@
"project_link": "nonebot-adapter-tailchat", "project_link": "nonebot-adapter-tailchat",
"name": "Tailchat", "name": "Tailchat",
"desc": "Tailchat 适配器", "desc": "Tailchat 适配器",
"author_id": 61458340, "author": "eya46",
"homepage": "https://github.com/eya46/nonebot-adapter-tailchat", "homepage": "https://github.com/eya46/nonebot-adapter-tailchat",
"tags": [], "tags": [],
"is_official": false "is_official": false
}, }
{
"module_name": "nonebot.adapters.mail",
"project_link": "nonebot-adapter-mail",
"name": "Mail",
"desc": "邮件收发协议",
"author_id": 44370805,
"homepage": "https://github.com/mobyw/nonebot-adapter-mail",
"tags": [],
"is_official": false
},
] ]

View File

@@ -2,7 +2,7 @@
{ {
"name": "HarukaBot", "name": "HarukaBot",
"desc": "将B站UP主的动态和直播信息推送至QQ", "desc": "将B站UP主的动态和直播信息推送至QQ",
"author_id": 36433929, "author": "SK-415",
"homepage": "https://github.com/SK-415/HarukaBot", "homepage": "https://github.com/SK-415/HarukaBot",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -10,7 +10,7 @@
{ {
"name": "Omega Miya", "name": "Omega Miya",
"desc": "B站推送Pixiv搜图识番求签抽卡表情包还有其他杂七杂八的功能", "desc": "B站推送Pixiv搜图识番求签抽卡表情包还有其他杂七杂八的功能",
"author_id": 41713304, "author": "Ailitonia",
"homepage": "https://github.com/Ailitonia/omega-miya", "homepage": "https://github.com/Ailitonia/omega-miya",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -18,7 +18,7 @@
{ {
"name": "Github Bot", "name": "Github Bot",
"desc": "在QQ获取/处理Github repo/pr/issue", "desc": "在QQ获取/处理Github repo/pr/issue",
"author_id": 42488585, "author": "yanyongyu",
"homepage": "https://github.com/cscs181/QQ-GitHub-Bot", "homepage": "https://github.com/cscs181/QQ-GitHub-Bot",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -26,7 +26,7 @@
{ {
"name": "YanXiBot", "name": "YanXiBot",
"desc": "动漫资源查找与娱乐机器人", "desc": "动漫资源查找与娱乐机器人",
"author_id": 50488999, "author": "Melodyknit",
"homepage": "https://github.com/Melodyknit/YanXiBot", "homepage": "https://github.com/Melodyknit/YanXiBot",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -34,7 +34,7 @@
{ {
"name": "绪山真寻bot", "name": "绪山真寻bot",
"desc": "含有不少的娱乐功能同时稍稍有一些实用的功能 :P", "desc": "含有不少的娱乐功能同时稍稍有一些实用的功能 :P",
"author_id": 45528451, "author": "HibiKier",
"homepage": "https://github.com/HibiKier/zhenxun_bot", "homepage": "https://github.com/HibiKier/zhenxun_bot",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -42,7 +42,7 @@
{ {
"name": "ATRI", "name": "ATRI",
"desc": "高性能文爱萝卜子,糅杂了各类有趣小功能", "desc": "高性能文爱萝卜子,糅杂了各类有趣小功能",
"author_id": 37587870, "author": "Kyomotoi",
"homepage": "https://github.com/Kyomotoi/ATRI", "homepage": "https://github.com/Kyomotoi/ATRI",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -50,7 +50,7 @@
{ {
"name": "dumbot傻瓜机器人", "name": "dumbot傻瓜机器人",
"desc": "猜一猜游戏、新闻一览、英文每日一词一短语等等含一键启动及docker容器部署就绪", "desc": "猜一猜游戏、新闻一览、英文每日一词一短语等等含一键启动及docker容器部署就绪",
"author_id": 52522252, "author": "ffreemt",
"homepage": "https://github.com/ffreemt/koyeb-nb2", "homepage": "https://github.com/ffreemt/koyeb-nb2",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -58,7 +58,7 @@
{ {
"name": "DicePP", "name": "DicePP",
"desc": "TRPG骰娘, 带先攻, 查询等功能, 主要面向DND5E. 面对骰主推出的船新版本, 内置Windows/Linux详细部署指南以及方便的自定义骰娘方法, 从回复文本到查询资料库都可轻松配置~", "desc": "TRPG骰娘, 带先攻, 查询等功能, 主要面向DND5E. 面对骰主推出的船新版本, 内置Windows/Linux详细部署指南以及方便的自定义骰娘方法, 从回复文本到查询资料库都可轻松配置~",
"author_id": 88259371, "author": "pear-studio",
"homepage": "https://github.com/pear-studio/nonebot-dicepp", "homepage": "https://github.com/pear-studio/nonebot-dicepp",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -66,7 +66,7 @@
{ {
"name": "SetuBot", "name": "SetuBot",
"desc": "每个群配置文件独立,可以控制频率,socks http代理,R18开关,支持多tag,自建API lolicon Pixiv热度榜", "desc": "每个群配置文件独立,可以控制频率,socks http代理,R18开关,支持多tag,自建API lolicon Pixiv热度榜",
"author_id": 39484884, "author": "yuban10703",
"homepage": "https://github.com/yuban10703/setu-nonebot2", "homepage": "https://github.com/yuban10703/setu-nonebot2",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -74,7 +74,7 @@
{ {
"name": "剑网三bot", "name": "剑网三bot",
"desc": "网络游戏《剑侠情缘三》的群聊机器人数据使用www.jx3api.com", "desc": "网络游戏《剑侠情缘三》的群聊机器人数据使用www.jx3api.com",
"author_id": 37363867, "author": "JustUndertaker",
"homepage": "https://github.com/JustUndertaker/mini_jx3_bot", "homepage": "https://github.com/JustUndertaker/mini_jx3_bot",
"tags": [ "tags": [
{ {
@@ -87,7 +87,7 @@
{ {
"name": "PixivBot", "name": "PixivBot",
"desc": "顾名思义是Pixiv的bot随机推荐插画、随机指定关键词插画、随机书签、查看排行榜、查看指定id插画", "desc": "顾名思义是Pixiv的bot随机推荐插画、随机指定关键词插画、随机书签、查看排行榜、查看指定id插画",
"author_id": 17331698, "author": "ssttkkl",
"homepage": "https://github.com/ssttkkl/PixivBot", "homepage": "https://github.com/ssttkkl/PixivBot",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -95,7 +95,7 @@
{ {
"name": "SeaBot_QQ", "name": "SeaBot_QQ",
"desc": "一个能够获取新闻资讯并推送至QQ的群聊机器人。", "desc": "一个能够获取新闻资讯并推送至QQ的群聊机器人。",
"author_id": 31682561, "author": "B1ue1nWh1te",
"homepage": "https://github.com/B1ue1nWh1te/SeaBot_QQ", "homepage": "https://github.com/B1ue1nWh1te/SeaBot_QQ",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -103,7 +103,7 @@
{ {
"name": "琪露诺Bot", "name": "琪露诺Bot",
"desc": "用QQ机器人控制Minecraft服务器服务器状态查询/服务器白名单/插件列表/玩家查询/转发服务器消息/执行指令... 其他实用娱乐功能三步即可成功部署的QQ bot", "desc": "用QQ机器人控制Minecraft服务器服务器状态查询/服务器白名单/插件列表/玩家查询/转发服务器消息/执行指令... 其他实用娱乐功能三步即可成功部署的QQ bot",
"author_id": 56951617, "author": "summerkirakira",
"homepage": "https://github.com/summerkirakira/CirnoBot", "homepage": "https://github.com/summerkirakira/CirnoBot",
"tags": [ "tags": [
{ {
@@ -116,7 +116,7 @@
{ {
"name": "Inkar Suki", "name": "Inkar Suki",
"desc": "一个十分方便的Bot支持包括Webhook、群管、剑网3等一系列功能持续更新中……", "desc": "一个十分方便的Bot支持包括Webhook、群管、剑网3等一系列功能持续更新中……",
"author_id": 68726147, "author": "HornCopper",
"homepage": "https://github.com/HornCopper/Inkar-Suki", "homepage": "https://github.com/HornCopper/Inkar-Suki",
"tags": [ "tags": [
{ {
@@ -137,7 +137,7 @@
{ {
"name": "屑岛风Bot", "name": "屑岛风Bot",
"desc": "自家用屑Bot", "desc": "自家用屑Bot",
"author_id": 71873002, "author": "kexue-z",
"homepage": "https://github.com/kexue-z/Dao-bot", "homepage": "https://github.com/kexue-z/Dao-bot",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -145,7 +145,7 @@
{ {
"name": "LiteyukiBot-轻雪机器人", "name": "LiteyukiBot-轻雪机器人",
"desc": "一个有各种琐事功能的bot有AI接口能陪聊", "desc": "一个有各种琐事功能的bot有AI接口能陪聊",
"author_id": 79104275, "author": "snowyfirefly",
"homepage": "https://github.com/snowyfirefly/Liteyuki", "homepage": "https://github.com/snowyfirefly/Liteyuki",
"tags": [ "tags": [
{ {
@@ -162,7 +162,7 @@
{ {
"name": "nya_bot", "name": "nya_bot",
"desc": "喵服——战魂铭人联机服务器兼机器人", "desc": "喵服——战魂铭人联机服务器兼机器人",
"author_id": 31379266, "author": "nikissXI",
"homepage": "https://github.com/nikissXI/nya_bot", "homepage": "https://github.com/nikissXI/nya_bot",
"tags": [ "tags": [
{ {
@@ -175,7 +175,7 @@
{ {
"name": "真宵Bot", "name": "真宵Bot",
"desc": "专注群聊的QQ机器人", "desc": "专注群聊的QQ机器人",
"author_id": 71173418, "author": "Shine-Light",
"homepage": "https://github.com/Shine-Light/Nonebot_Bot_MayaFey", "homepage": "https://github.com/Shine-Light/Nonebot_Bot_MayaFey",
"tags": [ "tags": [
{ {
@@ -196,7 +196,7 @@
{ {
"name": "SkadiBot", "name": "SkadiBot",
"desc": "明日方舟主题机器人—斯卡蒂", "desc": "明日方舟主题机器人—斯卡蒂",
"author_id": 101615359, "author": "yuyuziYYZ",
"homepage": "https://github.com/yuyuziYYZ/skadi_bot", "homepage": "https://github.com/yuyuziYYZ/skadi_bot",
"tags": [ "tags": [
{ {
@@ -217,7 +217,7 @@
{ {
"name": "小白机器人", "name": "小白机器人",
"desc": "一个高度依赖数据库的群管理机器人", "desc": "一个高度依赖数据库的群管理机器人",
"author_id": 69745333, "author": "SDIJF1521",
"homepage": "https://github.com/SDIJF1521/qqai", "homepage": "https://github.com/SDIJF1521/qqai",
"tags": [ "tags": [
{ {
@@ -234,7 +234,7 @@
{ {
"name": "LittlePaimon", "name": "LittlePaimon",
"desc": "小派蒙,多功能原神机器人。", "desc": "小派蒙,多功能原神机器人。",
"author_id": 63870437, "author": "CMHopeSunshine",
"homepage": "https://github.com/CMHopeSunshine/LittlePaimon", "homepage": "https://github.com/CMHopeSunshine/LittlePaimon",
"tags": [ "tags": [
{ {
@@ -247,7 +247,7 @@
{ {
"name": "IdhagnBot", "name": "IdhagnBot",
"desc": "🐱🤖 一个以娱乐功能为主的缝合怪划掉QQ机器人包含一定Furry要素但是不会卖萌就是逊啦", "desc": "🐱🤖 一个以娱乐功能为主的缝合怪划掉QQ机器人包含一定Furry要素但是不会卖萌就是逊啦",
"author_id": 17371317, "author": "su226",
"homepage": "https://github.com/su226/IdhagnBot", "homepage": "https://github.com/su226/IdhagnBot",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -255,7 +255,7 @@
{ {
"name": "hsbot", "name": "hsbot",
"desc": "服务于《炉石传说》玩家的机器人,上线至今已有加入十余个个炉石相关群聊,上千名用户使用,响应请求数万次。 数据使用HSreplay, Fbigame, Hearthstone API", "desc": "服务于《炉石传说》玩家的机器人,上线至今已有加入十余个个炉石相关群聊,上千名用户使用,响应请求数万次。 数据使用HSreplay, Fbigame, Hearthstone API",
"author_id": 67055520, "author": "gzy02",
"homepage": "https://github.com/gzy02/hsbot", "homepage": "https://github.com/gzy02/hsbot",
"tags": [ "tags": [
{ {
@@ -268,7 +268,7 @@
{ {
"name": "Bread Dog Bot", "name": "Bread Dog Bot",
"desc": "Terraria TShock QQ 机器人", "desc": "Terraria TShock QQ 机器人",
"author_id": 160252668, "author": "Qianyiovo",
"homepage": "https://github.com/Qianyiovo/bread_dog_bot", "homepage": "https://github.com/Qianyiovo/bread_dog_bot",
"tags": [ "tags": [
{ {
@@ -289,7 +289,7 @@
{ {
"name": "RanBot", "name": "RanBot",
"desc": "不@会很安静的Bot", "desc": "不@会很安静的Bot",
"author_id": 88923783, "author": "IAXRetailer",
"homepage": "https://github.com/Hecatia-Hell-Workshop/RanBot", "homepage": "https://github.com/Hecatia-Hell-Workshop/RanBot",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -297,7 +297,7 @@
{ {
"name": "辞辞(cici)Bot", "name": "辞辞(cici)Bot",
"desc": "一个集成娱乐和群管为一体的机器人", "desc": "一个集成娱乐和群管为一体的机器人",
"author_id": 90902259, "author": "mengxinyuan638",
"homepage": "https://github.com/mengxinyuan638/cici-bot", "homepage": "https://github.com/mengxinyuan638/cici-bot",
"tags": [ "tags": [
{ {
@@ -318,7 +318,7 @@
{ {
"name": "SuzunoBot", "name": "SuzunoBot",
"desc": "多功能音游bot主要服务maimaiDX、Arcaea", "desc": "多功能音游bot主要服务maimaiDX、Arcaea",
"author_id": 29980586, "author": "Rinfair-CSP-A016",
"homepage": "https://github.com/Rinfair-CSP-A016/SuzunoBot-AGLAS", "homepage": "https://github.com/Rinfair-CSP-A016/SuzunoBot-AGLAS",
"tags": [ "tags": [
{ {
@@ -339,7 +339,7 @@
{ {
"name": "青岚", "name": "青岚",
"desc": "基于NoneBot的与Minecraft Server互通消息的机器人", "desc": "基于NoneBot的与Minecraft Server互通消息的机器人",
"author_id": 54731914, "author": "17TheWord",
"homepage": "https://github.com/17TheWord/qinglan_bot", "homepage": "https://github.com/17TheWord/qinglan_bot",
"tags": [ "tags": [
{ {
@@ -352,7 +352,7 @@
{ {
"name": "ChensQBOTv2", "name": "ChensQBOTv2",
"desc": "多功能QQ群机器人权限管理/联ban/社工等等等等,以及拥有一个强大的开发者", "desc": "多功能QQ群机器人权限管理/联ban/社工等等等等,以及拥有一个强大的开发者",
"author_id": 116929900, "author": "cnchens",
"homepage": "https://github.com/cnchens/ChensQBOTv2", "homepage": "https://github.com/cnchens/ChensQBOTv2",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -360,7 +360,7 @@
{ {
"name": "koishi", "name": "koishi",
"desc": "支持爬取 codeforces, atcoder, 牛客上程序设计赛事的 bot。", "desc": "支持爬取 codeforces, atcoder, 牛客上程序设计赛事的 bot。",
"author_id": 71639222, "author": "CupidsBow",
"homepage": "https://github.com/CupidsBow/koishi", "homepage": "https://github.com/CupidsBow/koishi",
"tags": [ "tags": [
{ {
@@ -381,7 +381,7 @@
{ {
"name": "脑积水", "name": "脑积水",
"desc": "一个超级缝合怪...", "desc": "一个超级缝合怪...",
"author_id": 66541860, "author": "zhulinyv",
"homepage": "https://github.com/zhulinyv/NJS", "homepage": "https://github.com/zhulinyv/NJS",
"tags": [ "tags": [
{ {
@@ -394,7 +394,7 @@
{ {
"name": "LOVE酱", "name": "LOVE酱",
"desc": "为铁锈战争游戏群服务的虚拟少女,内置了爬取铁锈房间列表功能,以及游戏内单位查询功能,并制作了教学系统以及铁锈相关游戏群的收集功能。", "desc": "为铁锈战争游戏群服务的虚拟少女,内置了爬取铁锈房间列表功能,以及游戏内单位查询功能,并制作了教学系统以及铁锈相关游戏群的收集功能。",
"author_id": 106828088, "author": "allureluoli",
"homepage": "https://github.com/allureluoli/LOVE-", "homepage": "https://github.com/allureluoli/LOVE-",
"tags": [ "tags": [
{ {
@@ -411,7 +411,7 @@
{ {
"name": "fubot", "name": "fubot",
"desc": "基于nonebot与go-cqhttp的QQ娱乐bot提供群日常娱乐功能与舞萌DX游戏相关的信息查询功能。", "desc": "基于nonebot与go-cqhttp的QQ娱乐bot提供群日常娱乐功能与舞萌DX游戏相关的信息查询功能。",
"author_id": 54059896, "author": "HCskia",
"homepage": "https://github.com/HCskia/fu-Bot", "homepage": "https://github.com/HCskia/fu-Bot",
"tags": [ "tags": [
{ {
@@ -424,7 +424,7 @@
{ {
"name": "桃桃酱", "name": "桃桃酱",
"desc": "一个会拆家的高性能缝合萝卜子", "desc": "一个会拆家的高性能缝合萝卜子",
"author_id": 107618388, "author": "tkgs0",
"homepage": "https://github.com/tkgs0/Momoko", "homepage": "https://github.com/tkgs0/Momoko",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -432,7 +432,7 @@
{ {
"name": "CoolQBot", "name": "CoolQBot",
"desc": "基于 NoneBot2 的聊天机器人", "desc": "基于 NoneBot2 的聊天机器人",
"author_id": 5219550, "author": "he0119",
"homepage": "https://github.com/he0119/CoolQBot", "homepage": "https://github.com/he0119/CoolQBot",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -440,7 +440,7 @@
{ {
"name": "XDbot2", "name": "XDbot2",
"desc": "简单的QQ功能型机器人", "desc": "简单的QQ功能型机器人",
"author_id": 104149371, "author": "This-is-XiaoDeng",
"homepage": "https://github.com/ITCraftDevelopmentTeam/XDbot2", "homepage": "https://github.com/ITCraftDevelopmentTeam/XDbot2",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -448,7 +448,7 @@
{ {
"name": "March7th", "name": "March7th",
"desc": "三月七 - 崩坏:星穹铁道机器人", "desc": "三月七 - 崩坏:星穹铁道机器人",
"author_id": 44370805, "author": "mobyw",
"homepage": "https://github.com/Mar-7th/March7th", "homepage": "https://github.com/Mar-7th/March7th",
"tags": [ "tags": [
{ {
@@ -465,7 +465,7 @@
{ {
"name": "ay机器人", "name": "ay机器人",
"desc": "codeforces和洛谷卷王监视、股票监控、ai聊天", "desc": "codeforces和洛谷卷王监视、股票监控、ai聊天",
"author_id": 77315378, "author": "863109569",
"homepage": "https://github.com/863109569/qqbot", "homepage": "https://github.com/863109569/qqbot",
"tags": [ "tags": [
{ {
@@ -486,7 +486,7 @@
{ {
"name": "狐尾", "name": "狐尾",
"desc": "一个整合了兽云祭api的机器人支持账号令牌操作以及上传兽图", "desc": "一个整合了兽云祭api的机器人支持账号令牌操作以及上传兽图",
"author_id": 99388013, "author": "bingqiu456",
"homepage": "https://github.com/bingqiu456/shouyun", "homepage": "https://github.com/bingqiu456/shouyun",
"tags": [ "tags": [
{ {
@@ -499,7 +499,7 @@
{ {
"name": "ReimeiBot-黎明机器人", "name": "ReimeiBot-黎明机器人",
"desc": "流星飞逝,黎明终将到来。", "desc": "流星飞逝,黎明终将到来。",
"author_id": 65395090, "author": "ThirdBlood",
"homepage": "https://github.com/3rdBit/ReimeiBot", "homepage": "https://github.com/3rdBit/ReimeiBot",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -507,7 +507,7 @@
{ {
"name": "web_bot", "name": "web_bot",
"desc": "把机器人搬到网络上", "desc": "把机器人搬到网络上",
"author_id": 63489103, "author": "wsdtl",
"homepage": "https://github.com/wsdtl/web_bot", "homepage": "https://github.com/wsdtl/web_bot",
"tags": [ "tags": [
{ {
@@ -520,7 +520,7 @@
{ {
"name": "林汐", "name": "林汐",
"desc": "多平台功能型Bot", "desc": "多平台功能型Bot",
"author_id": 110453675, "author": "mute23-code",
"homepage": "https://github.com/netsora/SoraBot", "homepage": "https://github.com/netsora/SoraBot",
"tags": [ "tags": [
{ {
@@ -537,7 +537,7 @@
{ {
"name": "米缸", "name": "米缸",
"desc": "基于nonebot2的米缸Bot", "desc": "基于nonebot2的米缸Bot",
"author_id": 13503375, "author": "LambdaYH",
"homepage": "https://github.com/LambdaYH/MigangBot", "homepage": "https://github.com/LambdaYH/MigangBot",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -545,7 +545,7 @@
{ {
"name": "不正经的妹妹", "name": "不正经的妹妹",
"desc": "一款功能丰富、简单易用、自定义性强、扩展性强的可爱的QQ娱乐机器人", "desc": "一款功能丰富、简单易用、自定义性强、扩展性强的可爱的QQ娱乐机器人",
"author_id": 104713034, "author": "itsevin",
"homepage": "https://github.com/itsevin/sister_bot", "homepage": "https://github.com/itsevin/sister_bot",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -553,7 +553,7 @@
{ {
"name": "星见Kirami", "name": "星见Kirami",
"desc": "🌟 读作 Kirami写作星见简明轻快的聊天机器人应用。", "desc": "🌟 读作 Kirami写作星见简明轻快的聊天机器人应用。",
"author_id": 66513481, "author": "A-kirami",
"homepage": "https://kiramibot.dev/", "homepage": "https://kiramibot.dev/",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -561,7 +561,7 @@
{ {
"name": "OCNbot", "name": "OCNbot",
"desc": "OI Contest Notifier bot一个可以推送洛谷、cf、atcoder、牛客比赛通知的bot", "desc": "OI Contest Notifier bot一个可以推送洛谷、cf、atcoder、牛客比赛通知的bot",
"author_id": 91535478, "author": "ACnoway",
"homepage": "https://github.com/ACnoway/OCNbot", "homepage": "https://github.com/ACnoway/OCNbot",
"tags": [ "tags": [
{ {
@@ -578,7 +578,7 @@
{ {
"name": "妃爱", "name": "妃爱",
"desc": "超可爱的妃爱QQ群聊机器人", "desc": "超可爱的妃爱QQ群聊机器人",
"author_id": 52267304, "author": "jiangyuxiaoxiao",
"homepage": "https://github.com/jiangyuxiaoxiao/Hiyori", "homepage": "https://github.com/jiangyuxiaoxiao/Hiyori",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -586,7 +586,7 @@
{ {
"name": "芙芙", "name": "芙芙",
"desc": "供 Mooncell Wiki 协作使用的跨平台机器人", "desc": "供 Mooncell Wiki 协作使用的跨平台机器人",
"author_id": 14922941, "author": "StarHeartHunt",
"homepage": "https://github.com/MooncellWiki/BotFooChan", "homepage": "https://github.com/MooncellWiki/BotFooChan",
"tags": [], "tags": [],
"is_official": false "is_official": false
@@ -594,7 +594,7 @@
{ {
"name": "Sakiko", "name": "Sakiko",
"desc": "基于 LiteLoaderBDS 的 Minecraft 基岩版 Bot", "desc": "基于 LiteLoaderBDS 的 Minecraft 基岩版 Bot",
"author_id": 55650833, "author": "zhaomaoniu",
"homepage": "https://github.com/zhaomaoniu/Sakiko", "homepage": "https://github.com/zhaomaoniu/Sakiko",
"tags": [ "tags": [
{ {
@@ -608,10 +608,18 @@
], ],
"is_official": false "is_official": false
}, },
{
"name": "星辰 Bot",
"desc": "欢迎使用 星辰 Bot 项目!这是一款基于 NoneBot2 打造的智能 QQ 机器人旨在为用户提供丰富的功能体验。无论是获取一言的灵感探索历史上的今天还是穿梭60s世界星辰 Bot 为您打开了全新的交流之门。快来尝试吧!",
"author": "StarXinXin",
"homepage": "https://github.com/StarXinXin/StarsBot",
"tags": [],
"is_official": false
},
{ {
"name": "Minecraft_QQBot", "name": "Minecraft_QQBot",
"desc": "基于 NoneBot2 的 Minecraft 群服互联 QQ 机器人,支持多服务器多种方式连接。", "desc": "基于 NoneBot2 的 Minecraft 群服互联 QQ 机器人,支持多服务器多种方式连接。",
"author_id": 90964775, "author": "Lonely-Sails",
"homepage": "https://github.com/Minecraft-QQBot/BotServer", "homepage": "https://github.com/Minecraft-QQBot/BotServer",
"tags": [ "tags": [
{ {
@@ -624,43 +632,5 @@
} }
], ],
"is_official": false "is_official": false
},
{
"name": "小安提Bot",
"desc": "服务于音游 舞萌DX 的多功能Bot",
"author_id": 186144551,
"homepage": "https://github.com/Ant1816/Ant1Bot",
"tags": [
{
"label": "maimaiDX",
"color": "#52ea9a"
},
{
"label": "音游",
"color": "#f74b18"
} }
],
"is_official": false
},
{
"name": "CanrotBot",
"desc": "有很多实用功能的bot也有很多没什么用的娱乐功能接入了大模型并且有一部分功能可以被大模型调用。主打一个全都有",
"author_id": 18070676,
"homepage": "https://github.com/wangyw15/CanrotBot",
"tags": [],
"is_official": false
},
{
"name": "Mio澪",
"desc": "超可爱多功能Qbot",
"author_id": 50508678,
"homepage": "https://github.com/EienSakura/mio",
"tags": [
{
"label": "娱乐",
"color": "#ea5252"
}
],
"is_official": false
},
] ]

View File

@@ -4,7 +4,7 @@
"project_link": "", "project_link": "",
"name": "None", "name": "None",
"desc": "None 驱动器", "desc": "None 驱动器",
"author_id": 42488585, "author": "yanyongyu",
"homepage": "/docs/advanced/driver", "homepage": "/docs/advanced/driver",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -14,7 +14,7 @@
"project_link": "nonebot2[fastapi]", "project_link": "nonebot2[fastapi]",
"name": "FastAPI", "name": "FastAPI",
"desc": "FastAPI 驱动器", "desc": "FastAPI 驱动器",
"author_id": 42488585, "author": "yanyongyu",
"homepage": "/docs/advanced/driver", "homepage": "/docs/advanced/driver",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -24,7 +24,7 @@
"project_link": "nonebot2[quart]", "project_link": "nonebot2[quart]",
"name": "Quart", "name": "Quart",
"desc": "Quart 驱动器", "desc": "Quart 驱动器",
"author_id": 42488585, "author": "yanyongyu",
"homepage": "/docs/advanced/driver", "homepage": "/docs/advanced/driver",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -34,7 +34,7 @@
"project_link": "nonebot2[httpx]", "project_link": "nonebot2[httpx]",
"name": "HTTPX", "name": "HTTPX",
"desc": "HTTPX 驱动器", "desc": "HTTPX 驱动器",
"author_id": 42488585, "author": "yanyongyu",
"homepage": "/docs/advanced/driver", "homepage": "/docs/advanced/driver",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -44,7 +44,7 @@
"project_link": "nonebot2[websockets]", "project_link": "nonebot2[websockets]",
"name": "websockets", "name": "websockets",
"desc": "websockets 驱动器", "desc": "websockets 驱动器",
"author_id": 42488585, "author": "yanyongyu",
"homepage": "/docs/advanced/driver", "homepage": "/docs/advanced/driver",
"tags": [], "tags": [],
"is_official": true "is_official": true
@@ -54,9 +54,9 @@
"project_link": "nonebot2[aiohttp]", "project_link": "nonebot2[aiohttp]",
"name": "AIOHTTP", "name": "AIOHTTP",
"desc": "AIOHTTP 驱动器", "desc": "AIOHTTP 驱动器",
"author_id": 42488585, "author": "yanyongyu",
"homepage": "/docs/advanced/driver", "homepage": "/docs/advanced/driver",
"tags": [], "tags": [],
"is_official": true "is_official": true
}, }
] ]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1469
envs/test/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,11 @@ packages = [{ include = "nonebot-test.py" }]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9" python = "^3.9"
trio = "^0.27.0" nonebug = "^0.3.7"
nonebug = "^0.4.1"
wsproto = "^1.2.0" wsproto = "^1.2.0"
pytest-cov = "^6.0.0" pytest-cov = "^5.0.0"
pytest-xdist = "^3.0.2" pytest-xdist = "^3.0.2"
pytest-asyncio = "^0.23.2"
werkzeug = ">=2.3.6,<4.0.0" werkzeug = ">=2.3.6,<4.0.0"
coverage-conditional-plugin = "^0.9.0" coverage-conditional-plugin = "^0.9.0"

View File

@@ -39,24 +39,22 @@
- `require` => {ref}``require` <nonebot.plugin.load.require>` - `require` => {ref}``require` <nonebot.plugin.load.require>`
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 0 sidebar_position: 0
description: nonebot 模块 description: nonebot 模块
""" """
from importlib.metadata import version
import os import os
from typing import Any, Optional, TypeVar, Union, overload from importlib.metadata import version
from typing import Any, Union, TypeVar, Optional, overload
import loguru import loguru
from nonebot.adapters import Adapter, Bot
from nonebot.compat import model_dump from nonebot.compat import model_dump
from nonebot.config import DOTENV_TYPE, Config, Env
from nonebot.drivers import ASGIMixin, Driver, combine_driver
from nonebot.log import logger as logger from nonebot.log import logger as logger
from nonebot.adapters import Bot, Adapter
from nonebot.config import DOTENV_TYPE, Env, Config
from nonebot.utils import escape_tag, resolve_dot_notation from nonebot.utils import escape_tag, resolve_dot_notation
from nonebot.drivers import Driver, ASGIMixin, combine_driver
try: try:
__version__ = version("nonebot2") __version__ = version("nonebot2")
@@ -337,31 +335,31 @@ def run(*args: Any, **kwargs: Any) -> None:
get_driver().run(*args, **kwargs) get_driver().run(*args, **kwargs)
from nonebot.plugin import CommandGroup as CommandGroup
from nonebot.plugin import MatcherGroup as MatcherGroup
from nonebot.plugin import get_available_plugin_names as get_available_plugin_names
from nonebot.plugin import get_loaded_plugins as get_loaded_plugins
from nonebot.plugin import get_plugin as get_plugin
from nonebot.plugin import get_plugin_by_module_name as get_plugin_by_module_name
from nonebot.plugin import get_plugin_config as get_plugin_config
from nonebot.plugin import load_all_plugins as load_all_plugins
from nonebot.plugin import load_builtin_plugin as load_builtin_plugin
from nonebot.plugin import load_builtin_plugins as load_builtin_plugins
from nonebot.plugin import load_from_json as load_from_json
from nonebot.plugin import load_from_toml as load_from_toml
from nonebot.plugin import load_plugin as load_plugin
from nonebot.plugin import load_plugins as load_plugins
from nonebot.plugin import on as on from nonebot.plugin import on as on
from nonebot.plugin import on_command as on_command
from nonebot.plugin import on_endswith as on_endswith
from nonebot.plugin import on_fullmatch as on_fullmatch
from nonebot.plugin import on_keyword as on_keyword
from nonebot.plugin import on_message as on_message
from nonebot.plugin import on_metaevent as on_metaevent
from nonebot.plugin import on_notice as on_notice
from nonebot.plugin import on_regex as on_regex
from nonebot.plugin import on_request as on_request
from nonebot.plugin import on_shell_command as on_shell_command
from nonebot.plugin import on_startswith as on_startswith
from nonebot.plugin import on_type as on_type from nonebot.plugin import on_type as on_type
from nonebot.plugin import require as require from nonebot.plugin import require as require
from nonebot.plugin import on_regex as on_regex
from nonebot.plugin import on_notice as on_notice
from nonebot.plugin import get_plugin as get_plugin
from nonebot.plugin import on_command as on_command
from nonebot.plugin import on_keyword as on_keyword
from nonebot.plugin import on_message as on_message
from nonebot.plugin import on_request as on_request
from nonebot.plugin import load_plugin as load_plugin
from nonebot.plugin import on_endswith as on_endswith
from nonebot.plugin import CommandGroup as CommandGroup
from nonebot.plugin import MatcherGroup as MatcherGroup
from nonebot.plugin import load_plugins as load_plugins
from nonebot.plugin import on_fullmatch as on_fullmatch
from nonebot.plugin import on_metaevent as on_metaevent
from nonebot.plugin import on_startswith as on_startswith
from nonebot.plugin import load_from_json as load_from_json
from nonebot.plugin import load_from_toml as load_from_toml
from nonebot.plugin import load_all_plugins as load_all_plugins
from nonebot.plugin import on_shell_command as on_shell_command
from nonebot.plugin import get_plugin_config as get_plugin_config
from nonebot.plugin import get_loaded_plugins as get_loaded_plugins
from nonebot.plugin import load_builtin_plugin as load_builtin_plugin
from nonebot.plugin import load_builtin_plugins as load_builtin_plugins
from nonebot.plugin import get_plugin_by_module_name as get_plugin_by_module_name
from nonebot.plugin import get_available_plugin_names as get_available_plugin_names

View File

@@ -3,15 +3,13 @@
使用 {ref}`nonebot.drivers.Driver.register_adapter` 注册适配器。 使用 {ref}`nonebot.drivers.Driver.register_adapter` 注册适配器。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 0 sidebar_position: 0
description: nonebot.adapters 模块 description: nonebot.adapters 模块
""" """
from nonebot.internal.adapter import Adapter as Adapter
from nonebot.internal.adapter import Bot as Bot from nonebot.internal.adapter import Bot as Bot
from nonebot.internal.adapter import Event as Event from nonebot.internal.adapter import Event as Event
from nonebot.internal.adapter import Adapter as Adapter
from nonebot.internal.adapter import Message as Message from nonebot.internal.adapter import Message as Message
from nonebot.internal.adapter import MessageSegment as MessageSegment from nonebot.internal.adapter import MessageSegment as MessageSegment
from nonebot.internal.adapter import MessageTemplate as MessageTemplate from nonebot.internal.adapter import MessageTemplate as MessageTemplate

View File

@@ -3,28 +3,26 @@
为兼容 Pydantic V1 与 V2 版本,定义了一系列兼容函数与类供使用。 为兼容 Pydantic V1 与 V2 版本,定义了一系列兼容函数与类供使用。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 16 sidebar_position: 16
description: nonebot.compat 模块 description: nonebot.compat 模块
""" """
from collections.abc import Generator from collections.abc import Generator
from dataclasses import dataclass, is_dataclass
from functools import cached_property from functools import cached_property
from dataclasses import dataclass, is_dataclass
from typing_extensions import Self, get_args, get_origin, is_typeddict
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Annotated,
Any, Any,
Callable, Union,
Generic, Generic,
TypeVar,
Callable,
Optional, Optional,
Protocol, Protocol,
TypeVar, Annotated,
Union,
overload, overload,
) )
from typing_extensions import Self, get_args, get_origin, is_typeddict
from pydantic import VERSION, BaseModel from pydantic import VERSION, BaseModel
@@ -44,21 +42,21 @@ if TYPE_CHECKING:
__all__ = ( __all__ = (
"DEFAULT_CONFIG", "Required",
"ConfigDict",
"FieldInfo",
"ModelField",
"PydanticUndefined", "PydanticUndefined",
"PydanticUndefinedType", "PydanticUndefinedType",
"Required", "ConfigDict",
"DEFAULT_CONFIG",
"FieldInfo",
"ModelField",
"TypeAdapter", "TypeAdapter",
"custom_validation",
"extract_field_info", "extract_field_info",
"model_fields",
"model_config", "model_config",
"model_dump", "model_dump",
"model_fields",
"type_validate_json",
"type_validate_python", "type_validate_python",
"type_validate_json",
"custom_validation",
) )
__autodoc__ = { __autodoc__ = {
@@ -70,9 +68,9 @@ __autodoc__ = {
if PYDANTIC_V2: # pragma: pydantic-v2 if PYDANTIC_V2: # pragma: pydantic-v2
from pydantic import GetCoreSchemaHandler from pydantic import GetCoreSchemaHandler
from pydantic import TypeAdapter as TypeAdapter from pydantic import TypeAdapter as TypeAdapter
from pydantic_core import CoreSchema, core_schema
from pydantic._internal._repr import display_as_type from pydantic._internal._repr import display_as_type
from pydantic.fields import FieldInfo as BaseFieldInfo from pydantic.fields import FieldInfo as BaseFieldInfo
from pydantic_core import CoreSchema, core_schema
Required = Ellipsis Required = Ellipsis
"""Alias of Ellipsis for compatibility with pydantic v1""" """Alias of Ellipsis for compatibility with pydantic v1"""
@@ -253,8 +251,9 @@ if PYDANTIC_V2: # pragma: pydantic-v2
return class_ return class_
else: # pragma: pydantic-v1 else: # pragma: pydantic-v1
from pydantic import Extra
from pydantic import parse_obj_as, parse_raw_as
from pydantic import BaseConfig as PydanticConfig from pydantic import BaseConfig as PydanticConfig
from pydantic import Extra, parse_obj_as, parse_raw_as
from pydantic.fields import FieldInfo as BaseFieldInfo from pydantic.fields import FieldInfo as BaseFieldInfo
from pydantic.fields import ModelField as BaseModelField from pydantic.fields import ModelField as BaseModelField
from pydantic.schema import get_annotation_from_field_info from pydantic.schema import get_annotation_from_field_info

View File

@@ -7,26 +7,27 @@ NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及
详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。 详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 1 sidebar_position: 1
description: nonebot.config 模块 description: nonebot.config 模块
""" """
import os
import abc import abc
from collections.abc import Mapping import json
from pathlib import Path
from datetime import timedelta from datetime import timedelta
from ipaddress import IPv4Address from ipaddress import IPv4Address
import json from collections.abc import Mapping
import os from typing import TYPE_CHECKING, Any, Union, Optional
from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional, Union
from typing_extensions import TypeAlias, get_args, get_origin from typing_extensions import TypeAlias, get_args, get_origin
from dotenv import dotenv_values from dotenv import dotenv_values
from pydantic import BaseModel, Field from pydantic import Field, BaseModel
from pydantic.networks import IPvAnyAddress from pydantic.networks import IPvAnyAddress
from nonebot.log import logger
from nonebot.typing import origin_is_union
from nonebot.utils import deep_update, type_is_complex, lenient_issubclass
from nonebot.compat import ( from nonebot.compat import (
PYDANTIC_V2, PYDANTIC_V2,
ConfigDict, ConfigDict,
@@ -36,9 +37,6 @@ from nonebot.compat import (
model_config, model_config,
model_fields, model_fields,
) )
from nonebot.log import logger
from nonebot.typing import origin_is_union
from nonebot.utils import deep_update, lenient_issubclass, type_is_complex
DOTENV_TYPE: TypeAlias = Union[ DOTENV_TYPE: TypeAlias = Union[
Path, str, list[Union[Path, str]], tuple[Union[Path, str], ...] Path, str, list[Union[Path, str]], tuple[Union[Path, str], ...]

View File

@@ -1,8 +1,6 @@
"""本模块包含了 NoneBot 事件处理过程中使用到的常量。 """本模块包含了 NoneBot 事件处理过程中使用到的常量。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 9 sidebar_position: 9
description: nonebot.consts 模块 description: nonebot.consts 模块
""" """
@@ -22,10 +20,6 @@ REJECT_TARGET: Literal["_current_target"] = "_current_target"
"""当前 `reject` 目标存储 key""" """当前 `reject` 目标存储 key"""
REJECT_CACHE_TARGET: Literal["_next_target"] = "_next_target" REJECT_CACHE_TARGET: Literal["_next_target"] = "_next_target"
"""下一个 `reject` 目标存储 key""" """下一个 `reject` 目标存储 key"""
PAUSE_PROMPT_RESULT_KEY: Literal["_pause_result"] = "_pause_result"
"""`pause` prompt 发送结果存储 key"""
REJECT_PROMPT_RESULT_KEY: Literal["_reject_{key}_result"] = "_reject_{key}_result"
"""`reject` prompt 发送结果存储 key"""
# used by Rule # used by Rule
PREFIX_KEY: Literal["_prefix"] = "_prefix" PREFIX_KEY: Literal["_prefix"] = "_prefix"

View File

@@ -1,32 +1,22 @@
"""本模块模块实现了依赖注入的定义与处理。 """本模块模块实现了依赖注入的定义与处理。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 0 sidebar_position: 0
description: nonebot.dependencies 模块 description: nonebot.dependencies 模块
""" """
import abc import abc
from collections.abc import Awaitable, Iterable import asyncio
from dataclasses import dataclass, field
from functools import partial
import inspect import inspect
from typing import Any, Callable, Generic, Optional, TypeVar, cast from dataclasses import field, dataclass
from collections.abc import Iterable, Awaitable
from typing import Any, Generic, TypeVar, Callable, Optional, cast
import anyio
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.compat import FieldInfo, ModelField, PydanticUndefined
from nonebot.exception import SkippedException
from nonebot.log import logger from nonebot.log import logger
from nonebot.typing import _DependentCallable from nonebot.typing import _DependentCallable
from nonebot.utils import ( from nonebot.exception import SkippedException
flatten_exception_group, from nonebot.utils import run_sync, is_coroutine_callable
is_coroutine_callable, from nonebot.compat import FieldInfo, ModelField, PydanticUndefined
run_coro_with_shield,
run_sync,
)
from .utils import check_field_type, get_typed_signature from .utils import check_field_type, get_typed_signature
@@ -92,16 +82,7 @@ class Dependent(Generic[R]):
) )
async def __call__(self, **kwargs: Any) -> R: async def __call__(self, **kwargs: Any) -> R:
exception: Optional[BaseExceptionGroup[SkippedException]] = None try:
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
nonlocal exception
exception = exc_group
# raise one of the exceptions instead
excs = list(flatten_exception_group(exc_group))
logger.trace(f"{self} skipped due to {excs}")
with catch({SkippedException: _handle_skipped}):
# do pre-check # do pre-check
await self.check(**kwargs) await self.check(**kwargs)
@@ -113,8 +94,9 @@ class Dependent(Generic[R]):
return await cast(Callable[..., Awaitable[R]], self.call)(**values) return await cast(Callable[..., Awaitable[R]], self.call)(**values)
else: else:
return await run_sync(cast(Callable[..., R], self.call))(**values) return await run_sync(cast(Callable[..., R], self.call))(**values)
except SkippedException as e:
raise exception logger.trace(f"{self} skipped due to {e}")
raise
@staticmethod @staticmethod
def parse_params( def parse_params(
@@ -182,16 +164,9 @@ class Dependent(Generic[R]):
return cls(call, params, parameterless_params) return cls(call, params, parameterless_params)
async def check(self, **params: Any) -> None: async def check(self, **params: Any) -> None:
if self.parameterless: await asyncio.gather(*(param._check(**params) for param in self.parameterless))
async with anyio.create_task_group() as tg: await asyncio.gather(
for param in self.parameterless: *(cast(Param, param.field_info)._check(**params) for param in self.params)
tg.start_soon(partial(param._check, **params))
if self.params:
async with anyio.create_task_group() as tg:
for param in self.params:
tg.start_soon(
partial(cast(Param, param.field_info)._check, **params)
) )
async def _solve_field(self, field: ModelField, params: dict[str, Any]) -> Any: async def _solve_field(self, field: ModelField, params: dict[str, Any]) -> Any:
@@ -208,22 +183,10 @@ class Dependent(Generic[R]):
await param._solve(**params) await param._solve(**params)
# solve param values # solve param values
result: dict[str, Any] = {} values = await asyncio.gather(
if not self.params: *(self._solve_field(field, params) for field in self.params)
return result )
return {field.name: value for field, value in zip(self.params, values)}
async def _solve_field(field: ModelField, params: dict[str, Any]) -> None:
value = await self._solve_field(field, params)
result[field.name] = value
async with anyio.create_task_group() as tg:
for field in self.params:
# shield the task to prevent cancellation
# when one of the tasks raises an exception
# this will improve the dependency cache reusability
tg.start_soon(run_coro_with_shield, _solve_field(field, params))
return result
__autodoc__ = {"CustomConfig": False} __autodoc__ = {"CustomConfig": False}

View File

@@ -1,7 +1,5 @@
""" """
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 1 sidebar_position: 1
description: nonebot.dependencies.utils 模块 description: nonebot.dependencies.utils 模块
""" """

View File

@@ -3,31 +3,29 @@
各驱动请继承以下基类。 各驱动请继承以下基类。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 0 sidebar_position: 0
description: nonebot.drivers 模块 description: nonebot.drivers 模块
""" """
from nonebot.internal.driver import URL as URL from nonebot.internal.driver import URL as URL
from nonebot.internal.driver import ASGIMixin as ASGIMixin
from nonebot.internal.driver import Cookies as Cookies
from nonebot.internal.driver import Driver as Driver
from nonebot.internal.driver import ForwardDriver as ForwardDriver
from nonebot.internal.driver import ForwardMixin as ForwardMixin
from nonebot.internal.driver import HTTPClientMixin as HTTPClientMixin
from nonebot.internal.driver import HTTPClientSession as HTTPClientSession
from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup
from nonebot.internal.driver import HTTPVersion as HTTPVersion
from nonebot.internal.driver import Mixin as Mixin from nonebot.internal.driver import Mixin as Mixin
from nonebot.internal.driver import Driver as Driver
from nonebot.internal.driver import Cookies as Cookies
from nonebot.internal.driver import Request as Request from nonebot.internal.driver import Request as Request
from nonebot.internal.driver import Response as Response from nonebot.internal.driver import Response as Response
from nonebot.internal.driver import ReverseDriver as ReverseDriver from nonebot.internal.driver import ASGIMixin as ASGIMixin
from nonebot.internal.driver import ReverseMixin as ReverseMixin
from nonebot.internal.driver import WebSocket as WebSocket from nonebot.internal.driver import WebSocket as WebSocket
from nonebot.internal.driver import HTTPVersion as HTTPVersion
from nonebot.internal.driver import ForwardMixin as ForwardMixin
from nonebot.internal.driver import ReverseMixin as ReverseMixin
from nonebot.internal.driver import ForwardDriver as ForwardDriver
from nonebot.internal.driver import ReverseDriver as ReverseDriver
from nonebot.internal.driver import combine_driver as combine_driver
from nonebot.internal.driver import HTTPClientMixin as HTTPClientMixin
from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup
from nonebot.internal.driver import HTTPClientSession as HTTPClientSession
from nonebot.internal.driver import WebSocketClientMixin as WebSocketClientMixin from nonebot.internal.driver import WebSocketClientMixin as WebSocketClientMixin
from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup
from nonebot.internal.driver import combine_driver as combine_driver
__autodoc__ = { __autodoc__ = {
"URL": True, "URL": True,

View File

@@ -11,33 +11,29 @@ pip install nonebot2[aiohttp]
::: :::
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 2 sidebar_position: 2
description: nonebot.drivers.aiohttp 模块 description: nonebot.drivers.aiohttp 模块
""" """
from typing_extensions import override
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Optional, Union from typing import TYPE_CHECKING, Union, Optional
from typing_extensions import override
from multidict import CIMultiDict from multidict import CIMultiDict
from nonebot.exception import WebSocketClosed
from nonebot.drivers import URL, Request, Response
from nonebot.drivers.none import Driver as NoneDriver
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.internal.driver import Cookies, QueryTypes, CookieTypes, HeaderTypes
from nonebot.drivers import ( from nonebot.drivers import (
URL, HTTPVersion,
HTTPClientMixin, HTTPClientMixin,
HTTPClientSession, HTTPClientSession,
HTTPVersion,
Request,
Response,
WebSocketClientMixin, WebSocketClientMixin,
combine_driver, combine_driver,
) )
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.drivers.none import Driver as NoneDriver
from nonebot.exception import WebSocketClosed
from nonebot.internal.driver import Cookies, CookieTypes, HeaderTypes, QueryTypes
try: try:
import aiohttp import aiohttp
@@ -90,7 +86,9 @@ class Session(HTTPClientSession):
@override @override
async def request(self, setup: Request) -> Response: async def request(self, setup: Request) -> Response:
if self._params: if self._params:
url = setup.url.with_query({**self._params, **setup.url.query}) params = self._params.copy()
params.update(setup.url.query)
url = setup.url.with_query(params)
else: else:
url = setup.url url = setup.url
@@ -170,13 +168,11 @@ class Mixin(HTTPClientMixin, WebSocketClientMixin):
else: else:
raise RuntimeError(f"Unsupported HTTP version: {setup.version}") raise RuntimeError(f"Unsupported HTTP version: {setup.version}")
timeout = aiohttp.ClientWSTimeout(ws_close=setup.timeout or 10.0) # type: ignore
async with aiohttp.ClientSession(version=version, trust_env=True) as session: async with aiohttp.ClientSession(version=version, trust_env=True) as session:
async with session.ws_connect( async with session.ws_connect(
setup.url, setup.url,
method=setup.method, method=setup.method,
timeout=timeout, timeout=setup.timeout or 10,
headers=setup.headers, headers=setup.headers,
proxy=setup.proxy, proxy=setup.proxy,
) as ws: ) as ws:

View File

@@ -11,35 +11,34 @@ pip install nonebot2[fastapi]
::: :::
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 1 sidebar_position: 1
description: nonebot.drivers.fastapi 模块 description: nonebot.drivers.fastapi 模块
""" """
import logging
import contextlib import contextlib
from functools import wraps from functools import wraps
import logging
from typing import Any, Optional, Union
from typing_extensions import override from typing_extensions import override
from typing import Any, Union, Optional
from pydantic import BaseModel from pydantic import BaseModel
from nonebot.compat import model_dump, type_validate_python
from nonebot.config import Config as NoneBotConfig
from nonebot.config import Env from nonebot.config import Env
from nonebot.drivers import ASGIMixin, HTTPServerSetup, WebSocketServerSetup from nonebot.drivers import ASGIMixin
from nonebot.drivers import Driver as BaseDriver
from nonebot.drivers import Request as BaseRequest
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.exception import WebSocketClosed from nonebot.exception import WebSocketClosed
from nonebot.internal.driver import FileTypes from nonebot.internal.driver import FileTypes
from nonebot.drivers import Driver as BaseDriver
from nonebot.config import Config as NoneBotConfig
from nonebot.drivers import Request as BaseRequest
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.compat import model_dump, type_validate_python
from nonebot.drivers import HTTPServerSetup, WebSocketServerSetup
try: try:
from fastapi import FastAPI, Request, UploadFile, status
from fastapi.responses import Response
from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState
import uvicorn import uvicorn
from fastapi.responses import Response
from fastapi import FastAPI, Request, UploadFile, status
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
except ModuleNotFoundError as e: # pragma: no cover except ModuleNotFoundError as e: # pragma: no cover
raise ImportError( raise ImportError(
"Please install FastAPI first to use this driver. " "Please install FastAPI first to use this driver. "

View File

@@ -11,28 +11,26 @@ pip install nonebot2[httpx]
::: :::
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 3 sidebar_position: 3
description: nonebot.drivers.httpx 模块 description: nonebot.drivers.httpx 模块
""" """
from typing import TYPE_CHECKING, Optional, Union
from typing_extensions import override from typing_extensions import override
from typing import TYPE_CHECKING, Union, Optional
from multidict import CIMultiDict from multidict import CIMultiDict
from nonebot.drivers.none import Driver as NoneDriver
from nonebot.internal.driver import Cookies, QueryTypes, CookieTypes, HeaderTypes
from nonebot.drivers import ( from nonebot.drivers import (
URL, URL,
HTTPClientMixin,
HTTPClientSession,
HTTPVersion,
Request, Request,
Response, Response,
HTTPVersion,
HTTPClientMixin,
HTTPClientSession,
combine_driver, combine_driver,
) )
from nonebot.drivers.none import Driver as NoneDriver
from nonebot.internal.driver import Cookies, CookieTypes, HeaderTypes, QueryTypes
try: try:
import httpx import httpx
@@ -82,8 +80,6 @@ class Session(HTTPClientSession):
data=setup.data, data=setup.data,
files=setup.files, files=setup.files,
json=setup.json, json=setup.json,
# ensure the params priority
params=setup.url.raw_query_string,
headers=tuple(setup.headers.items()), headers=tuple(setup.headers.items()),
cookies=setup.cookies.jar, cookies=setup.cookies.jar,
timeout=setup.timeout, timeout=setup.timeout,
@@ -104,7 +100,7 @@ class Session(HTTPClientSession):
headers=self._headers, headers=self._headers,
cookies=self._cookies.jar, cookies=self._cookies.jar,
http2=self._version == HTTPVersion.H2, http2=self._version == HTTPVersion.H2,
proxy=self._proxy, proxies=self._proxy,
follow_redirects=True, follow_redirects=True,
) )
await self._client.__aenter__() await self._client.__aenter__()

View File

@@ -5,25 +5,19 @@
::: :::
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 6 sidebar_position: 6
description: nonebot.drivers.none 模块 description: nonebot.drivers.none 模块
""" """
import signal import signal
from typing import Optional import asyncio
import threading
from typing_extensions import override from typing_extensions import override
import anyio
from anyio.abc import TaskGroup
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.config import Config, Env
from nonebot.consts import WINDOWS
from nonebot.drivers import Driver as BaseDriver
from nonebot.log import logger from nonebot.log import logger
from nonebot.utils import flatten_exception_group from nonebot.consts import WINDOWS
from nonebot.config import Env, Config
from nonebot.drivers import Driver as BaseDriver
HANDLED_SIGNALS = ( HANDLED_SIGNALS = (
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
@@ -39,8 +33,8 @@ class Driver(BaseDriver):
def __init__(self, env: Env, config: Config): def __init__(self, env: Env, config: Config):
super().__init__(env, config) super().__init__(env, config)
self.should_exit: anyio.Event = anyio.Event() self.should_exit: asyncio.Event = asyncio.Event()
self.force_exit: anyio.Event = anyio.Event() self.force_exit: bool = False
@property @property
@override @override
@@ -58,97 +52,84 @@ class Driver(BaseDriver):
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
"""启动 none driver""" """启动 none driver"""
super().run(*args, **kwargs) super().run(*args, **kwargs)
anyio.run(self._serve) loop = asyncio.get_event_loop()
loop.run_until_complete(self._serve())
async def _serve(self): async def _serve(self):
async with anyio.create_task_group() as driver_tg: self._install_signal_handlers()
driver_tg.start_soon(self._handle_signals)
driver_tg.start_soon(self._listen_force_exit, driver_tg)
driver_tg.start_soon(self._handle_lifespan, driver_tg)
async def _handle_signals(self):
try:
with anyio.open_signal_receiver(*HANDLED_SIGNALS) as signal_receiver:
async for sig in signal_receiver:
self.exit(force=self.should_exit.is_set())
except NotImplementedError:
# Windows
for sig in HANDLED_SIGNALS:
signal.signal(sig, self._handle_legacy_signal)
# backport for Windows signal handling
def _handle_legacy_signal(self, sig, frame):
self.exit(force=self.should_exit.is_set())
async def _handle_lifespan(self, tg: TaskGroup):
try:
await self._startup() await self._startup()
if self.should_exit.is_set(): if self.should_exit.is_set():
return return
await self._main_loop()
await self._listen_exit()
await self._shutdown() await self._shutdown()
finally:
tg.cancel_scope.cancel()
async def _startup(self): async def _startup(self):
def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None: try:
self.should_exit.set() await self._lifespan.startup()
except Exception as e:
for exc in flatten_exception_group(exc_group): logger.opt(colors=True, exception=e).error(
logger.opt(colors=True, exception=exc).error(
"<r><bg #f8bbd0>Error occurred while running startup hook."
"</bg #f8bbd0></r>"
)
logger.error(
"<r><bg #f8bbd0>Application startup failed. " "<r><bg #f8bbd0>Application startup failed. "
"Exiting.</bg #f8bbd0></r>" "Exiting.</bg #f8bbd0></r>"
) )
self.should_exit.set()
return
with catch({Exception: handle_exception}):
await self._lifespan.startup()
if not self.should_exit.is_set():
logger.info("Application startup completed.") logger.info("Application startup completed.")
async def _listen_exit(self, tg: Optional[TaskGroup] = None): async def _main_loop(self):
await self.should_exit.wait() await self.should_exit.wait()
if tg is not None:
tg.cancel_scope.cancel()
async def _shutdown(self): async def _shutdown(self):
logger.info("Shutting down") logger.info("Shutting down")
logger.info("Waiting for application shutdown. (CTRL+C to force quit)")
error_occurred: bool = False logger.info("Waiting for application shutdown.")
def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None: try:
nonlocal error_occurred
error_occurred = True
for exc in flatten_exception_group(exc_group):
logger.opt(colors=True, exception=exc).error(
"<r><bg #f8bbd0>Error occurred while running shutdown hook."
"</bg #f8bbd0></r>"
)
logger.error(
"<r><bg #f8bbd0>Application shutdown failed. "
"Exiting.</bg #f8bbd0></r>"
)
with catch({Exception: handle_exception}):
await self._lifespan.shutdown() await self._lifespan.shutdown()
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running shutdown function. "
"Ignored!</bg #f8bbd0></r>"
)
for task in asyncio.all_tasks():
if task is not asyncio.current_task() and not task.done():
task.cancel()
await asyncio.sleep(0.1)
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
if tasks and not self.force_exit:
logger.info("Waiting for tasks to finish. (CTRL+C to force quit)")
while tasks and not self.force_exit:
await asyncio.sleep(0.1)
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
if not error_occurred:
logger.info("Application shutdown complete.") logger.info("Application shutdown complete.")
loop = asyncio.get_event_loop()
loop.stop()
async def _listen_force_exit(self, tg: TaskGroup): def _install_signal_handlers(self) -> None:
await self.force_exit.wait() if threading.current_thread() is not threading.main_thread():
tg.cancel_scope.cancel() # Signals can only be listened to from the main thread.
return
loop = asyncio.get_event_loop()
try:
for sig in HANDLED_SIGNALS:
loop.add_signal_handler(sig, self._handle_exit, sig, None)
except NotImplementedError:
# Windows
for sig in HANDLED_SIGNALS:
signal.signal(sig, self._handle_exit)
def _handle_exit(self, sig, frame):
self.exit(force=self.should_exit.is_set())
def exit(self, force: bool = False): def exit(self, force: bool = False):
"""退出 none driver """退出 none driver
@@ -159,4 +140,4 @@ class Driver(BaseDriver):
if not self.should_exit.is_set(): if not self.should_exit.is_set():
self.should_exit.set() self.should_exit.set()
if force: if force:
self.force_exit.set() self.force_exit = True

View File

@@ -11,37 +11,36 @@ pip install nonebot2[quart]
::: :::
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 5 sidebar_position: 5
description: nonebot.drivers.quart 模块 description: nonebot.drivers.quart 模块
""" """
import asyncio import asyncio
from functools import wraps from functools import wraps
from typing import Any, Optional, Union, cast
from typing_extensions import override from typing_extensions import override
from typing import Any, Union, Optional, cast
from pydantic import BaseModel from pydantic import BaseModel
from nonebot.compat import model_dump, type_validate_python
from nonebot.config import Config as NoneBotConfig
from nonebot.config import Env from nonebot.config import Env
from nonebot.drivers import ASGIMixin, HTTPServerSetup, WebSocketServerSetup from nonebot.drivers import ASGIMixin
from nonebot.drivers import Driver as BaseDriver
from nonebot.drivers import Request as BaseRequest
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.exception import WebSocketClosed from nonebot.exception import WebSocketClosed
from nonebot.internal.driver import FileTypes from nonebot.internal.driver import FileTypes
from nonebot.drivers import Driver as BaseDriver
from nonebot.config import Config as NoneBotConfig
from nonebot.drivers import Request as BaseRequest
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.compat import model_dump, type_validate_python
from nonebot.drivers import HTTPServerSetup, WebSocketServerSetup
try: try:
from quart import Quart, Request, Response import uvicorn
from quart import Websocket as QuartWebSocket
from quart import request as _request from quart import request as _request
from quart.ctx import WebsocketContext from quart.ctx import WebsocketContext
from quart.datastructures import FileStorage
from quart.globals import websocket_ctx from quart.globals import websocket_ctx
import uvicorn from quart import Quart, Request, Response
from quart.datastructures import FileStorage
from quart import Websocket as QuartWebSocket
except ModuleNotFoundError as e: # pragma: no cover except ModuleNotFoundError as e: # pragma: no cover
raise ImportError( raise ImportError(
"Please install Quart first to use this driver. " "Please install Quart first to use this driver. "

View File

@@ -11,24 +11,23 @@ pip install nonebot2[websockets]
::: :::
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 4 sidebar_position: 4
description: nonebot.drivers.websockets 模块 description: nonebot.drivers.websockets 模块
""" """
from collections.abc import AsyncGenerator, Coroutine
from contextlib import asynccontextmanager
from functools import wraps
import logging import logging
from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union from functools import wraps
from contextlib import asynccontextmanager
from typing_extensions import ParamSpec, override from typing_extensions import ParamSpec, override
from collections.abc import Coroutine, AsyncGenerator
from typing import TYPE_CHECKING, Any, Union, TypeVar, Callable
from nonebot.drivers import Request, WebSocketClientMixin, combine_driver from nonebot.drivers import Request
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.drivers.none import Driver as NoneDriver
from nonebot.exception import WebSocketClosed
from nonebot.log import LoguruHandler from nonebot.log import LoguruHandler
from nonebot.exception import WebSocketClosed
from nonebot.drivers.none import Driver as NoneDriver
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.drivers import WebSocketClientMixin, combine_driver
try: try:
from websockets.exceptions import ConnectionClosed from websockets.exceptions import ConnectionClosed
@@ -47,7 +46,7 @@ logger.addHandler(LoguruHandler())
def catch_closed( def catch_closed(
func: Callable[P, Coroutine[Any, Any, T]], func: Callable[P, Coroutine[Any, Any, T]]
) -> Callable[P, Coroutine[Any, Any, T]]: ) -> Callable[P, Coroutine[Any, Any, T]]:
@wraps(func) @wraps(func)
async def decorator(*args: P.args, **kwargs: P.kwargs) -> T: async def decorator(*args: P.args, **kwargs: P.kwargs) -> T:
@@ -70,8 +69,6 @@ class Mixin(WebSocketClientMixin):
@override @override
@asynccontextmanager @asynccontextmanager
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]: async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
if setup.proxy is not None:
logger.warning("proxy is not supported by websockets driver")
connection = Connect( connection = Connect(
str(setup.url), str(setup.url),
extra_headers={**setup.headers, **setup.cookies.as_header(setup)}, extra_headers={**setup.headers, **setup.cookies.as_header(setup)},

View File

@@ -25,8 +25,6 @@ NoneBotException
``` ```
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 10 sidebar_position: 10
description: nonebot.exception 模块 description: nonebot.exception 模块
""" """

View File

@@ -1,6 +1,6 @@
from .adapter import Adapter as Adapter
from .bot import Bot as Bot from .bot import Bot as Bot
from .event import Event as Event from .event import Event as Event
from .adapter import Adapter as Adapter
from .message import Message as Message from .message import Message as Message
from .message import MessageSegment as MessageSegment from .message import MessageSegment as MessageSegment
from .template import MessageTemplate as MessageTemplate from .template import MessageTemplate as MessageTemplate

View File

@@ -1,21 +1,21 @@
import abc import abc
from typing import Any
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Any
from nonebot.config import Config from nonebot.config import Config
from nonebot.internal.driver._lifespan import LIFESPAN_FUNC
from nonebot.internal.driver import ( from nonebot.internal.driver import (
ASGIMixin,
Driver, Driver,
HTTPClientMixin,
HTTPServerSetup,
Request, Request,
Response, Response,
ASGIMixin,
WebSocket, WebSocket,
HTTPClientMixin,
HTTPServerSetup,
WebSocketClientMixin, WebSocketClientMixin,
WebSocketServerSetup, WebSocketServerSetup,
) )
from nonebot.internal.driver._lifespan import LIFESPAN_FUNC
from .bot import Bot from .bot import Bot

View File

@@ -1,19 +1,16 @@
import abc import abc
import asyncio
from functools import partial from functools import partial
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Protocol, Union from typing import TYPE_CHECKING, Any, Union, ClassVar, Optional, Protocol
import anyio
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.log import logger
from nonebot.config import Config from nonebot.config import Config
from nonebot.exception import MockApiException from nonebot.exception import MockApiException
from nonebot.log import logger
from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook
from nonebot.utils import flatten_exception_group
if TYPE_CHECKING: if TYPE_CHECKING:
from .adapter import Adapter
from .event import Event from .event import Event
from .adapter import Adapter
from .message import Message, MessageSegment from .message import Message, MessageSegment
class _ApiCall(Protocol): class _ApiCall(Protocol):
@@ -79,99 +76,48 @@ class Bot(abc.ABC):
skip_calling_api: bool = False skip_calling_api: bool = False
exception: Optional[Exception] = None exception: Optional[Exception] = None
if self._calling_api_hook: if coros := [hook(self, api, data) for hook in self._calling_api_hook]:
try:
logger.debug("Running CallingAPI hooks...") logger.debug("Running CallingAPI hooks...")
await asyncio.gather(*coros)
def _handle_mock_api_exception( except MockApiException as e:
exc_group: BaseExceptionGroup[MockApiException],
) -> None:
nonlocal skip_calling_api, result
excs = [
exc
for exc in flatten_exception_group(exc_group)
if isinstance(exc, MockApiException)
]
if not excs:
return
elif len(excs) > 1:
logger.warning(
"Multiple hooks want to mock API result. Use the first one."
)
skip_calling_api = True skip_calling_api = True
result = excs[0].result result = e.result
logger.debug( logger.debug(
f"Calling API {api} is cancelled. Return {result!r} instead." f"Calling API {api} is cancelled. Return {result} instead."
) )
except Exception as e:
def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None: logger.opt(colors=True, exception=e).error(
for exc in flatten_exception_group(exc_group):
logger.opt(colors=True, exception=exc).error(
"<r><bg #f8bbd0>Error when running CallingAPI hook. " "<r><bg #f8bbd0>Error when running CallingAPI hook. "
"Running cancelled!</bg #f8bbd0></r>" "Running cancelled!</bg #f8bbd0></r>"
) )
with catch(
{
MockApiException: _handle_mock_api_exception,
Exception: _handle_exception,
}
):
async with anyio.create_task_group() as tg:
for hook in self._calling_api_hook:
tg.start_soon(hook, self, api, data)
if not skip_calling_api: if not skip_calling_api:
try: try:
result = await self.adapter._call_api(self, api, **data) result = await self.adapter._call_api(self, api, **data)
except Exception as e: except Exception as e:
exception = e exception = e
if self._called_api_hook: if coros := [
hook(self, exception, api, data, result) for hook in self._called_api_hook
]:
try:
logger.debug("Running CalledAPI hooks...") logger.debug("Running CalledAPI hooks...")
await asyncio.gather(*coros)
def _handle_mock_api_exception( except MockApiException as e:
exc_group: BaseExceptionGroup[MockApiException], # mock api result
) -> None: result = e.result
nonlocal result, exception # ignore exception
excs = [
exc
for exc in flatten_exception_group(exc_group)
if isinstance(exc, MockApiException)
]
if not excs:
return
elif len(excs) > 1:
logger.warning(
"Multiple hooks want to mock API result. Use the first one."
)
result = excs[0].result
exception = None exception = None
logger.debug( logger.debug(
f"Calling API {api} result is mocked. Return {result} instead." f"Calling API {api} result is mocked. Return {result} instead."
) )
except Exception as e:
def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None: logger.opt(colors=True, exception=e).error(
for exc in flatten_exception_group(exc_group):
logger.opt(colors=True, exception=exc).error(
"<r><bg #f8bbd0>Error when running CalledAPI hook. " "<r><bg #f8bbd0>Error when running CalledAPI hook. "
"Running cancelled!</bg #f8bbd0></r>" "Running cancelled!</bg #f8bbd0></r>"
) )
with catch(
{
MockApiException: _handle_mock_api_exception,
Exception: _handle_exception,
}
):
async with anyio.create_task_group() as tg:
for hook in self._called_api_hook:
tg.start_soon(hook, self, exception, api, data, result)
if exception: if exception:
raise exception raise exception
return result return result

View File

@@ -3,8 +3,8 @@ from typing import Any, TypeVar
from pydantic import BaseModel from pydantic import BaseModel
from nonebot.compat import PYDANTIC_V2, ConfigDict
from nonebot.utils import DataclassEncoder from nonebot.utils import DataclassEncoder
from nonebot.compat import PYDANTIC_V2, ConfigDict
from .message import Message from .message import Message

View File

@@ -1,18 +1,18 @@
import abc import abc
from collections.abc import Iterable
from copy import deepcopy from copy import deepcopy
from dataclasses import asdict, dataclass, field from typing_extensions import Self
from collections.abc import Iterable
from dataclasses import field, asdict, dataclass
from typing import ( # noqa: UP035 from typing import ( # noqa: UP035
Any, Any,
Type,
Union,
Generic, Generic,
TypeVar,
Optional, Optional,
SupportsIndex, SupportsIndex,
Type,
TypeVar,
Union,
overload, overload,
) )
from typing_extensions import Self
from nonebot.compat import custom_validation, type_validate_python from nonebot.compat import custom_validation, type_validate_python
@@ -329,9 +329,8 @@ class Message(list[TMS], abc.ABC):
return self[type_] return self[type_]
iterator, filtered = ( iterator, filtered = (
(seg for seg in self if seg.type == type_), seg for seg in self if seg.type == type_
self.__class__(), ), self.__class__()
)
for _ in range(count): for _ in range(count):
seg = next(iterator, None) seg = next(iterator, None)
if seg is None: if seg is None:

View File

@@ -1,19 +1,20 @@
from _string import formatter_field_name_split # type: ignore
from collections.abc import Mapping, Sequence
import functools import functools
from string import Formatter from string import Formatter
from typing_extensions import TypeAlias
from collections.abc import Mapping, Sequence
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
Callable,
Generic,
Optional,
TypeVar,
Union, Union,
Generic,
TypeVar,
Callable,
Optional,
cast, cast,
overload, overload,
) )
from typing_extensions import TypeAlias
from _string import formatter_field_name_split # type: ignore
if TYPE_CHECKING: if TYPE_CHECKING:
from .message import Message, MessageSegment from .message import Message, MessageSegment

View File

@@ -1,31 +1,31 @@
from .abstract import ASGIMixin as ASGIMixin from .model import URL as URL
from .model import RawURL as RawURL
from .abstract import Mixin as Mixin
from .model import Cookies as Cookies
from .model import Request as Request
from .abstract import Driver as Driver from .abstract import Driver as Driver
from .abstract import ForwardDriver as ForwardDriver from .model import FileType as FileType
from .model import Response as Response
from .model import DataTypes as DataTypes
from .model import FileTypes as FileTypes
from .model import WebSocket as WebSocket
from .model import FilesTypes as FilesTypes
from .model import QueryTypes as QueryTypes
from .abstract import ASGIMixin as ASGIMixin
from .model import CookieTypes as CookieTypes
from .model import FileContent as FileContent
from .model import HTTPVersion as HTTPVersion
from .model import HeaderTypes as HeaderTypes
from .model import SimpleQuery as SimpleQuery
from .model import ContentTypes as ContentTypes
from .model import QueryVariable as QueryVariable
from .abstract import ForwardMixin as ForwardMixin from .abstract import ForwardMixin as ForwardMixin
from .abstract import ReverseMixin as ReverseMixin
from .abstract import ForwardDriver as ForwardDriver
from .abstract import ReverseDriver as ReverseDriver
from .combine import combine_driver as combine_driver
from .model import HTTPServerSetup as HTTPServerSetup
from .abstract import HTTPClientMixin as HTTPClientMixin from .abstract import HTTPClientMixin as HTTPClientMixin
from .abstract import HTTPClientSession as HTTPClientSession from .abstract import HTTPClientSession as HTTPClientSession
from .abstract import Mixin as Mixin
from .abstract import ReverseDriver as ReverseDriver
from .abstract import ReverseMixin as ReverseMixin
from .abstract import WebSocketClientMixin as WebSocketClientMixin
from .combine import combine_driver as combine_driver
from .model import URL as URL
from .model import ContentTypes as ContentTypes
from .model import Cookies as Cookies
from .model import CookieTypes as CookieTypes
from .model import DataTypes as DataTypes
from .model import FileContent as FileContent
from .model import FilesTypes as FilesTypes
from .model import FileType as FileType
from .model import FileTypes as FileTypes
from .model import HeaderTypes as HeaderTypes
from .model import HTTPServerSetup as HTTPServerSetup
from .model import HTTPVersion as HTTPVersion
from .model import QueryTypes as QueryTypes
from .model import QueryVariable as QueryVariable
from .model import RawURL as RawURL
from .model import Request as Request
from .model import Response as Response
from .model import SimpleQuery as SimpleQuery
from .model import WebSocket as WebSocket
from .model import WebSocketServerSetup as WebSocketServerSetup from .model import WebSocketServerSetup as WebSocketServerSetup
from .abstract import WebSocketClientMixin as WebSocketClientMixin

View File

@@ -1,13 +1,8 @@
from collections.abc import Awaitable, Iterable from collections.abc import Awaitable
from types import TracebackType
from typing import Any, Callable, Optional, Union, cast
from typing_extensions import TypeAlias from typing_extensions import TypeAlias
from typing import Any, Union, Callable, cast
import anyio from nonebot.utils import run_sync, is_coroutine_callable
from anyio.abc import TaskGroup
from exceptiongroup import suppress
from nonebot.utils import is_coroutine_callable, run_sync
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any] SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]] ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
@@ -16,24 +11,10 @@ LIFESPAN_FUNC: TypeAlias = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
class Lifespan: class Lifespan:
def __init__(self) -> None: def __init__(self) -> None:
self._task_group: Optional[TaskGroup] = None
self._startup_funcs: list[LIFESPAN_FUNC] = [] self._startup_funcs: list[LIFESPAN_FUNC] = []
self._ready_funcs: list[LIFESPAN_FUNC] = [] self._ready_funcs: list[LIFESPAN_FUNC] = []
self._shutdown_funcs: list[LIFESPAN_FUNC] = [] self._shutdown_funcs: list[LIFESPAN_FUNC] = []
@property
def task_group(self) -> TaskGroup:
if self._task_group is None:
raise RuntimeError("Lifespan not started")
return self._task_group
@task_group.setter
def task_group(self, task_group: TaskGroup) -> None:
if self._task_group is not None:
raise RuntimeError("Lifespan already started")
self._task_group = task_group
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
self._startup_funcs.append(func) self._startup_funcs.append(func)
return func return func
@@ -48,7 +29,7 @@ class Lifespan:
@staticmethod @staticmethod
async def _run_lifespan_func( async def _run_lifespan_func(
funcs: Iterable[LIFESPAN_FUNC], funcs: list[LIFESPAN_FUNC],
) -> None: ) -> None:
for func in funcs: for func in funcs:
if is_coroutine_callable(func): if is_coroutine_callable(func):
@@ -57,44 +38,18 @@ class Lifespan:
await run_sync(cast(SYNC_LIFESPAN_FUNC, func))() await run_sync(cast(SYNC_LIFESPAN_FUNC, func))()
async def startup(self) -> None: async def startup(self) -> None:
# create background task group
self.task_group = anyio.create_task_group()
await self.task_group.__aenter__()
# run startup funcs
if self._startup_funcs: if self._startup_funcs:
await self._run_lifespan_func(self._startup_funcs) await self._run_lifespan_func(self._startup_funcs)
# run ready funcs
if self._ready_funcs: if self._ready_funcs:
await self._run_lifespan_func(self._ready_funcs) await self._run_lifespan_func(self._ready_funcs)
async def shutdown( async def shutdown(self) -> None:
self,
*,
exc_type: Optional[type[BaseException]] = None,
exc_val: Optional[BaseException] = None,
exc_tb: Optional[TracebackType] = None,
) -> None:
if self._shutdown_funcs: if self._shutdown_funcs:
# reverse shutdown funcs to ensure stack order await self._run_lifespan_func(self._shutdown_funcs)
await self._run_lifespan_func(reversed(self._shutdown_funcs))
# shutdown background task group
self.task_group.cancel_scope.cancel()
with suppress(Exception):
await self.task_group.__aexit__(exc_type, exc_val, exc_tb)
self._task_group = None
async def __aenter__(self) -> None: async def __aenter__(self) -> None:
await self.startup() await self.startup()
async def __aexit__( async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
self, await self.shutdown()
exc_type: Optional[type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
await self.shutdown(exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb)

View File

@@ -1,41 +1,38 @@
import abc import abc
from collections.abc import AsyncGenerator import asyncio
from contextlib import AsyncExitStack, asynccontextmanager
from types import TracebackType from types import TracebackType
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union from collections.abc import AsyncGenerator
from typing_extensions import Self, TypeAlias from typing_extensions import Self, TypeAlias
from contextlib import AsyncExitStack, asynccontextmanager
from typing import TYPE_CHECKING, Any, Union, ClassVar, Optional
from anyio import CancelScope, create_task_group from nonebot.log import logger
from anyio.abc import TaskGroup from nonebot.config import Env, Config
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.config import Config, Env
from nonebot.dependencies import Dependent from nonebot.dependencies import Dependent
from nonebot.exception import SkippedException from nonebot.exception import SkippedException
from nonebot.internal.params import BotParam, DefaultParam, DependParam from nonebot.utils import escape_tag, run_coro_with_catch
from nonebot.log import logger from nonebot.internal.params import BotParam, DependParam, DefaultParam
from nonebot.typing import ( from nonebot.typing import (
T_DependencyCache,
T_BotConnectionHook, T_BotConnectionHook,
T_BotDisconnectionHook, T_BotDisconnectionHook,
T_DependencyCache,
) )
from nonebot.utils import escape_tag, flatten_exception_group, run_coro_with_catch
from ._lifespan import LIFESPAN_FUNC, Lifespan from ._lifespan import LIFESPAN_FUNC, Lifespan
from .model import ( from .model import (
CookieTypes,
HeaderTypes,
HTTPServerSetup,
HTTPVersion,
QueryTypes,
Request, Request,
Response, Response,
WebSocket, WebSocket,
QueryTypes,
CookieTypes,
HeaderTypes,
HTTPVersion,
HTTPServerSetup,
WebSocketServerSetup, WebSocketServerSetup,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from nonebot.internal.adapter import Adapter, Bot from nonebot.internal.adapter import Bot, Adapter
BOT_HOOK_PARAMS = [DependParam, BotParam, DefaultParam] BOT_HOOK_PARAMS = [DependParam, BotParam, DefaultParam]
@@ -64,6 +61,7 @@ class Driver(abc.ABC):
self.config: Config = config self.config: Config = config
"""全局配置对象""" """全局配置对象"""
self._bots: dict[str, "Bot"] = {} self._bots: dict[str, "Bot"] = {}
self._bot_tasks: set[asyncio.Task] = set()
self._lifespan = Lifespan() self._lifespan = Lifespan()
def __repr__(self) -> str: def __repr__(self) -> str:
@@ -77,10 +75,6 @@ class Driver(abc.ABC):
"""获取当前所有已连接的 Bot""" """获取当前所有已连接的 Bot"""
return self._bots return self._bots
@property
def task_group(self) -> TaskGroup:
return self._lifespan.task_group
def register_adapter(self, adapter: type["Adapter"], **kwargs) -> None: def register_adapter(self, adapter: type["Adapter"], **kwargs) -> None:
"""注册一个协议适配器 """注册一个协议适配器
@@ -114,10 +108,12 @@ class Driver(abc.ABC):
@abc.abstractmethod @abc.abstractmethod
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
"""启动驱动框架""" """启动驱动框架"""
logger.opt(colors=True).success( logger.opt(colors=True).debug(
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>" f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
) )
self.on_shutdown(self._cleanup)
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""注册一个启动时执行的函数""" """注册一个启动时执行的函数"""
return self._lifespan.on_startup(func) return self._lifespan.on_startup(func)
@@ -158,63 +154,66 @@ class Driver(abc.ABC):
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}") raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
self._bots[bot.self_id] = bot self._bots[bot.self_id] = bot
if not self._bot_connection_hook: async def _run_hook(bot: "Bot") -> None:
return dependency_cache: T_DependencyCache = {}
async with AsyncExitStack() as stack:
def handle_exception(exc_group: BaseExceptionGroup) -> None: if coros := [
for exc in flatten_exception_group(exc_group): run_coro_with_catch(
logger.opt(colors=True, exception=exc).error( hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
(SkippedException,),
)
for hook in self._bot_connection_hook
]:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>" "<r><bg #f8bbd0>"
"Error when running WebSocketConnection hook:" "Error when running WebSocketConnection hook. "
"Running cancelled!"
"</bg #f8bbd0></r>" "</bg #f8bbd0></r>"
) )
async def _run_hook(bot: "Bot") -> None: task = asyncio.create_task(_run_hook(bot))
dependency_cache: T_DependencyCache = {} task.add_done_callback(self._bot_tasks.discard)
with CancelScope(shield=True), catch({Exception: handle_exception}): self._bot_tasks.add(task)
async with AsyncExitStack() as stack, create_task_group() as tg:
for hook in self._bot_connection_hook:
tg.start_soon(
run_coro_with_catch,
hook(
bot=bot, stack=stack, dependency_cache=dependency_cache
),
(SkippedException,),
)
self.task_group.start_soon(_run_hook, bot)
def _bot_disconnect(self, bot: "Bot") -> None: def _bot_disconnect(self, bot: "Bot") -> None:
"""在连接断开后,调用该函数来注销 bot 对象""" """在连接断开后,调用该函数来注销 bot 对象"""
if bot.self_id in self._bots: if bot.self_id in self._bots:
del self._bots[bot.self_id] del self._bots[bot.self_id]
if not self._bot_disconnection_hook: async def _run_hook(bot: "Bot") -> None:
return dependency_cache: T_DependencyCache = {}
async with AsyncExitStack() as stack:
def handle_exception(exc_group: BaseExceptionGroup) -> None: if coros := [
for exc in flatten_exception_group(exc_group): run_coro_with_catch(
logger.opt(colors=True, exception=exc).error( hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
(SkippedException,),
)
for hook in self._bot_disconnection_hook
]:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>" "<r><bg #f8bbd0>"
"Error when running WebSocketDisConnection hook:" "Error when running WebSocketDisConnection hook. "
"Running cancelled!"
"</bg #f8bbd0></r>" "</bg #f8bbd0></r>"
) )
async def _run_hook(bot: "Bot") -> None: task = asyncio.create_task(_run_hook(bot))
dependency_cache: T_DependencyCache = {} task.add_done_callback(self._bot_tasks.discard)
# shield cancellation to ensure bot disconnect hooks are always run self._bot_tasks.add(task)
with CancelScope(shield=True), catch({Exception: handle_exception}):
async with create_task_group() as tg, AsyncExitStack() as stack:
for hook in self._bot_disconnection_hook:
tg.start_soon(
run_coro_with_catch,
hook(
bot=bot, stack=stack, dependency_cache=dependency_cache
),
(SkippedException,),
)
self.task_group.start_soon(_run_hook, bot) async def _cleanup(self) -> None:
"""清理驱动器资源"""
if self._bot_tasks:
logger.opt(colors=True).debug(
"<y>Waiting for running bot connection hooks...</y>"
)
await asyncio.gather(*self._bot_tasks, return_exceptions=True)
class Mixin(abc.ABC): class Mixin(abc.ABC):

View File

@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING, TypeVar, Union, overload from typing import TYPE_CHECKING, Union, TypeVar, overload
from .abstract import Driver, Mixin from .abstract import Mixin, Driver
D = TypeVar("D", bound="Driver") D = TypeVar("D", bound="Driver")
@@ -39,4 +39,6 @@ def combine_driver(
+ "+".join(x.type.__get__(self) for x in mixins) # type: ignore + "+".join(x.type.__get__(self) for x in mixins) # type: ignore
) )
return type("CombinedDriver", (*mixins, driver), {"type": property(type_)}) # type: ignore return type(
"CombinedDriver", (*mixins, driver), {"type": property(type_)}
) # type: ignore

View File

@@ -1,14 +1,14 @@
import abc import abc
from collections.abc import Awaitable, Iterator, Mapping, MutableMapping
from dataclasses import dataclass
from enum import Enum
from http.cookiejar import Cookie, CookieJar
from typing import IO, Any, Callable, Optional, Union
from typing_extensions import TypeAlias
import urllib.request 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, Any, Union, Callable, Optional
from collections.abc import Mapping, Iterator, Awaitable, MutableMapping
from multidict import CIMultiDict
from yarl import URL as URL from yarl import URL as URL
from multidict import CIMultiDict
RawURL: TypeAlias = tuple[bytes, bytes, Optional[int], bytes] RawURL: TypeAlias = tuple[bytes, bytes, Optional[int], bytes]

View File

@@ -1,12 +1,12 @@
from .manager import MatcherManager as MatcherManager from .manager import MatcherManager as MatcherManager
from .provider import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
from .provider import MatcherProvider as MatcherProvider from .provider import MatcherProvider as MatcherProvider
from .provider import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
matchers = MatcherManager() matchers = MatcherManager()
from .matcher import Matcher as Matcher from .matcher import Matcher as Matcher
from .matcher import MatcherSource as MatcherSource
from .matcher import current_bot as current_bot from .matcher import current_bot as current_bot
from .matcher import MatcherSource as MatcherSource
from .matcher import current_event as current_event from .matcher import current_event as current_event
from .matcher import current_handler as current_handler from .matcher import current_handler as current_handler
from .matcher import current_matcher as current_matcher from .matcher import current_matcher as current_matcher

View File

@@ -1,5 +1,5 @@
from collections.abc import ItemsView, Iterator, KeysView, MutableMapping, ValuesView from typing import TYPE_CHECKING, Union, TypeVar, Optional, overload
from typing import TYPE_CHECKING, Optional, TypeVar, Union, overload from collections.abc import Iterator, KeysView, ItemsView, ValuesView, MutableMapping
from .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider from .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider
@@ -74,9 +74,9 @@ class MatcherManager(MutableMapping[int, list[type["Matcher"]]]):
self.provider.clear() self.provider.clear()
def update( # pyright: ignore[reportIncompatibleMethodOverride] def update( # pyright: ignore[reportIncompatibleMethodOverride]
self, m: MutableMapping[int, list[type["Matcher"]]], / self, __m: MutableMapping[int, list[type["Matcher"]]]
) -> None: ) -> None:
self.provider.update(m) self.provider.update(__m)
def setdefault( def setdefault(
self, key: int, default: list[type["Matcher"]] self, key: int, default: list[type["Matcher"]]

View File

@@ -1,46 +1,32 @@
from collections.abc import Iterable
from contextlib import AsyncExitStack, contextmanager
from contextvars import ContextVar
from dataclasses import dataclass
from datetime import datetime, timedelta
import inspect
from pathlib import Path
import sys import sys
import inspect
import warnings
from pathlib import Path
from types import ModuleType from types import ModuleType
from dataclasses import dataclass
from contextvars import ContextVar
from typing_extensions import Self
from collections.abc import Iterable
from datetime import datetime, timedelta
from contextlib import AsyncExitStack, contextmanager
from typing import ( # noqa: UP035 from typing import ( # noqa: UP035
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
Type,
Union,
TypeVar,
Callable, Callable,
ClassVar, ClassVar,
NoReturn, NoReturn,
Optional, Optional,
Type,
TypeVar,
Union,
overload, overload,
) )
from typing_extensions import Self
import warnings
from exceptiongroup import BaseExceptionGroup, catch from nonebot.log import logger
from nonebot.internal.rule import Rule
from nonebot.consts import ( from nonebot.utils import classproperty
ARG_KEY, from nonebot.dependencies import Param, Dependent
LAST_RECEIVE_KEY, from nonebot.internal.permission import User, Permission
PAUSE_PROMPT_RESULT_KEY,
RECEIVE_KEY,
REJECT_CACHE_TARGET,
REJECT_PROMPT_RESULT_KEY,
REJECT_TARGET,
)
from nonebot.dependencies import Dependent, Param
from nonebot.exception import (
FinishedException,
PausedException,
RejectedException,
SkippedException,
StopPropagation,
)
from nonebot.internal.adapter import ( from nonebot.internal.adapter import (
Bot, Bot,
Event, Event,
@@ -48,27 +34,37 @@ from nonebot.internal.adapter import (
MessageSegment, MessageSegment,
MessageTemplate, MessageTemplate,
) )
from nonebot.typing import (
T_State,
T_Handler,
T_TypeUpdater,
T_DependencyCache,
T_PermissionUpdater,
)
from nonebot.consts import (
ARG_KEY,
RECEIVE_KEY,
REJECT_TARGET,
LAST_RECEIVE_KEY,
REJECT_CACHE_TARGET,
)
from nonebot.exception import (
PausedException,
StopPropagation,
SkippedException,
FinishedException,
RejectedException,
)
from nonebot.internal.params import ( from nonebot.internal.params import (
Depends,
ArgParam, ArgParam,
BotParam, BotParam,
DefaultParam,
DependParam,
Depends,
EventParam, EventParam,
MatcherParam,
StateParam, StateParam,
DependParam,
DefaultParam,
MatcherParam,
) )
from nonebot.internal.permission import Permission, User
from nonebot.internal.rule import Rule
from nonebot.log import logger
from nonebot.typing import (
T_DependencyCache,
T_Handler,
T_PermissionUpdater,
T_State,
T_TypeUpdater,
)
from nonebot.utils import classproperty, flatten_exception_group
from . import matchers from . import matchers
@@ -562,8 +558,8 @@ class Matcher(metaclass=MatcherMeta):
""" """
bot = current_bot.get() bot = current_bot.get()
event = current_event.get() event = current_event.get()
if isinstance(message, MessageTemplate):
state = current_matcher.get().state state = current_matcher.get().state
if isinstance(message, MessageTemplate):
_message = message.format(**state) _message = message.format(**state)
else: else:
_message = message _message = message
@@ -599,15 +595,8 @@ class Matcher(metaclass=MatcherMeta):
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数, kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
请参考对应 adapter 的 bot 对象 api 请参考对应 adapter 的 bot 对象 api
""" """
try:
matcher = current_matcher.get()
except Exception:
matcher = None
if prompt is not None: if prompt is not None:
result = await cls.send(prompt, **kwargs) await cls.send(prompt, **kwargs)
if matcher is not None:
matcher.state[PAUSE_PROMPT_RESULT_KEY] = result
raise PausedException raise PausedException
@classmethod @classmethod
@@ -624,19 +613,8 @@ class Matcher(metaclass=MatcherMeta):
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数, kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
请参考对应 adapter 的 bot 对象 api 请参考对应 adapter 的 bot 对象 api
""" """
try:
matcher = current_matcher.get()
key = matcher.get_target()
except Exception:
matcher = None
key = None
key = REJECT_PROMPT_RESULT_KEY.format(key=key) if key is not None else None
if prompt is not None: if prompt is not None:
result = await cls.send(prompt, **kwargs) await cls.send(prompt, **kwargs)
if key is not None and matcher:
matcher.state[key] = result
raise RejectedException raise RejectedException
@classmethod @classmethod
@@ -656,12 +634,9 @@ class Matcher(metaclass=MatcherMeta):
请参考对应 adapter 的 bot 对象 api 请参考对应 adapter 的 bot 对象 api
""" """
matcher = current_matcher.get() matcher = current_matcher.get()
arg_key = ARG_KEY.format(key=key) matcher.set_target(ARG_KEY.format(key=key))
matcher.set_target(arg_key)
if prompt is not None: if prompt is not None:
result = await cls.send(prompt, **kwargs) await cls.send(prompt, **kwargs)
matcher.state[REJECT_PROMPT_RESULT_KEY.format(key=arg_key)] = result
raise RejectedException raise RejectedException
@classmethod @classmethod
@@ -681,12 +656,9 @@ class Matcher(metaclass=MatcherMeta):
请参考对应 adapter 的 bot 对象 api 请参考对应 adapter 的 bot 对象 api
""" """
matcher = current_matcher.get() matcher = current_matcher.get()
receive_key = RECEIVE_KEY.format(id=id) matcher.set_target(RECEIVE_KEY.format(id=id))
matcher.set_target(receive_key)
if prompt is not None: if prompt is not None:
result = await cls.send(prompt, **kwargs) await cls.send(prompt, **kwargs)
matcher.state[REJECT_PROMPT_RESULT_KEY.format(key=receive_key)] = result
raise RejectedException raise RejectedException
@classmethod @classmethod
@@ -840,12 +812,8 @@ class Matcher(metaclass=MatcherMeta):
f"bot={bot}, event={event!r}, state={state!r}" f"bot={bot}, event={event!r}, state={state!r}"
) )
def _handle_stop_propagation(exc_group: BaseExceptionGroup[StopPropagation]):
self.block = True
with self.ensure_context(bot, event): with self.ensure_context(bot, event):
try: try:
with catch({StopPropagation: _handle_stop_propagation}):
# Refresh preprocess state # Refresh preprocess state
self.state.update(state) self.state.update(state)
@@ -853,13 +821,7 @@ class Matcher(metaclass=MatcherMeta):
handler = self.remain_handlers.pop(0) handler = self.remain_handlers.pop(0)
current_handler.set(handler) current_handler.set(handler)
logger.debug(f"Running handler {handler}") logger.debug(f"Running handler {handler}")
try:
def _handle_skipped(
exc_group: BaseExceptionGroup[SkippedException],
):
logger.debug(f"Handler {handler} skipped")
with catch({SkippedException: _handle_skipped}):
await handler( await handler(
matcher=self, matcher=self,
bot=bot, bot=bot,
@@ -868,6 +830,10 @@ class Matcher(metaclass=MatcherMeta):
stack=stack, stack=stack,
dependency_cache=dependency_cache, dependency_cache=dependency_cache,
) )
except SkippedException:
logger.debug(f"Handler {handler} skipped")
except StopPropagation:
self.block = True
finally: finally:
logger.info(f"{self} running complete") logger.info(f"{self} running complete")
@@ -880,54 +846,10 @@ class Matcher(metaclass=MatcherMeta):
stack: Optional[AsyncExitStack] = None, stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None, dependency_cache: Optional[T_DependencyCache] = None,
): ):
exc: Optional[Union[FinishedException, RejectedException, PausedException]] = ( try:
None
)
def _handle_special_exception(
exc_group: BaseExceptionGroup[
Union[FinishedException, RejectedException, PausedException]
],
):
nonlocal exc
excs = list(flatten_exception_group(exc_group))
if len(excs) > 1:
logger.warning(
"Multiple session control exceptions occurred. "
"NoneBot will choose the proper one."
)
finished_exc = next(
(e for e in excs if isinstance(e, FinishedException)),
None,
)
rejected_exc = next(
(e for e in excs if isinstance(e, RejectedException)),
None,
)
paused_exc = next(
(e for e in excs if isinstance(e, PausedException)),
None,
)
exc = finished_exc or rejected_exc or paused_exc
elif isinstance(
excs[0], (FinishedException, RejectedException, PausedException)
):
exc = excs[0]
with catch(
{
(
FinishedException,
RejectedException,
PausedException,
): _handle_special_exception
}
):
await self.simple_run(bot, event, state, stack, dependency_cache) await self.simple_run(bot, event, state, stack, dependency_cache)
if isinstance(exc, FinishedException): except RejectedException:
pass
elif isinstance(exc, RejectedException):
await self.resolve_reject() await self.resolve_reject()
type_ = await self.update_type(bot, event, stack, dependency_cache) type_ = await self.update_type(bot, event, stack, dependency_cache)
permission = await self.update_permission( permission = await self.update_permission(
@@ -948,7 +870,7 @@ class Matcher(metaclass=MatcherMeta):
default_type_updater=self.__class__._default_type_updater, default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater, default_permission_updater=self.__class__._default_permission_updater,
) )
elif isinstance(exc, PausedException): except PausedException:
type_ = await self.update_type(bot, event, stack, dependency_cache) type_ = await self.update_type(bot, event, stack, dependency_cache)
permission = await self.update_permission( permission = await self.update_permission(
bot, event, stack, dependency_cache bot, event, stack, dependency_cache
@@ -968,3 +890,5 @@ class Matcher(metaclass=MatcherMeta):
default_type_updater=self.__class__._default_type_updater, default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater, default_permission_updater=self.__class__._default_permission_updater,
) )
except FinishedException:
pass

View File

@@ -1,7 +1,7 @@
import abc import abc
from typing import TYPE_CHECKING
from collections import defaultdict from collections import defaultdict
from collections.abc import Mapping, MutableMapping from collections.abc import Mapping, MutableMapping
from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from .matcher import Matcher from .matcher import Matcher

View File

@@ -1,47 +1,43 @@
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager import asyncio
from enum import Enum
import inspect import inspect
from typing_extensions import Self, get_args, override, get_origin
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Annotated,
Any, Any,
Callable,
Literal,
Optional,
Union, Union,
Literal,
Callable,
Optional,
Annotated,
cast, cast,
) )
from typing_extensions import Self, get_args, get_origin, override
import anyio
from exceptiongroup import BaseExceptionGroup, catch
from pydantic.fields import FieldInfo as PydanticFieldInfo from pydantic.fields import FieldInfo as PydanticFieldInfo
from nonebot.compat import FieldInfo, ModelField, PydanticUndefined, extract_field_info from nonebot.dependencies import Param, Dependent
from nonebot.consts import ARG_KEY, REJECT_PROMPT_RESULT_KEY
from nonebot.dependencies import Dependent, Param
from nonebot.dependencies.utils import check_field_type from nonebot.dependencies.utils import check_field_type
from nonebot.exception import SkippedException from nonebot.compat import FieldInfo, ModelField, PydanticUndefined, extract_field_info
from nonebot.typing import ( from nonebot.typing import (
_STATE_FLAG, _STATE_FLAG,
T_DependencyCache,
T_Handler,
T_State, T_State,
T_Handler,
T_DependencyCache,
origin_is_annotated, origin_is_annotated,
) )
from nonebot.utils import ( from nonebot.utils import (
generic_check_issubclass,
get_name, get_name,
run_sync,
is_gen_callable,
run_sync_ctx_manager,
is_async_gen_callable, is_async_gen_callable,
is_coroutine_callable, is_coroutine_callable,
is_gen_callable, generic_check_issubclass,
run_sync,
run_sync_ctx_manager,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from nonebot.adapters import Bot, Event, Message
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.adapters import Bot, Event
class DependsInner: class DependsInner:
@@ -97,78 +93,6 @@ def Depends(
return DependsInner(dependency, use_cache=use_cache, validate=validate) return DependsInner(dependency, use_cache=use_cache, validate=validate)
class CacheState(str, Enum):
"""子依赖缓存状态"""
PENDING = "PENDING"
FINISHED = "FINISHED"
class DependencyCache:
"""子依赖结果缓存。
用于缓存子依赖的结果,以避免重复计算。
"""
def __init__(self):
self._state = CacheState.PENDING
self._result: Any = None
self._exception: Optional[BaseException] = None
self._waiter = anyio.Event()
def done(self) -> bool:
return self._state == CacheState.FINISHED
def result(self) -> Any:
"""获取子依赖结果"""
if self._state != CacheState.FINISHED:
raise RuntimeError("Result is not ready")
if self._exception is not None:
raise self._exception
return self._result
def exception(self) -> Optional[BaseException]:
"""获取子依赖异常"""
if self._state != CacheState.FINISHED:
raise RuntimeError("Result is not ready")
return self._exception
def set_result(self, result: Any) -> None:
"""设置子依赖结果"""
if self._state != CacheState.PENDING:
raise RuntimeError(f"Cache state invalid: {self._state}")
self._result = result
self._state = CacheState.FINISHED
self._waiter.set()
def set_exception(self, exception: BaseException) -> None:
"""设置子依赖异常"""
if self._state != CacheState.PENDING:
raise RuntimeError(f"Cache state invalid: {self._state}")
self._exception = exception
self._state = CacheState.FINISHED
self._waiter.set()
async def wait(self):
"""等待子依赖结果"""
await self._waiter.wait()
if self._state != CacheState.FINISHED:
raise RuntimeError("Invalid cache state")
if self._exception is not None:
raise self._exception
return self._result
class DependParam(Param): class DependParam(Param):
"""子依赖注入参数。 """子依赖注入参数。
@@ -270,27 +194,17 @@ class DependParam(Param):
call = cast(Callable[..., Any], sub_dependent.call) call = cast(Callable[..., Any], sub_dependent.call)
# solve sub dependency with current cache # solve sub dependency with current cache
exc: Optional[BaseExceptionGroup[SkippedException]] = None
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
nonlocal exc
exc = exc_group
with catch({SkippedException: _handle_skipped}):
sub_values = await sub_dependent.solve( sub_values = await sub_dependent.solve(
stack=stack, stack=stack,
dependency_cache=dependency_cache, dependency_cache=dependency_cache,
**kwargs, **kwargs,
) )
if exc is not None:
raise exc
# run dependency function # run dependency function
task: asyncio.Task[Any]
if use_cache and call in dependency_cache: if use_cache and call in dependency_cache:
return await dependency_cache[call].wait() return await dependency_cache[call]
elif is_gen_callable(call) or is_async_gen_callable(call):
if is_gen_callable(call) or is_async_gen_callable(call):
assert isinstance( assert isinstance(
stack, AsyncExitStack stack, AsyncExitStack
), "Generator dependency should be called in context" ), "Generator dependency should be called in context"
@@ -298,28 +212,17 @@ class DependParam(Param):
cm = run_sync_ctx_manager(contextmanager(call)(**sub_values)) cm = run_sync_ctx_manager(contextmanager(call)(**sub_values))
else: else:
cm = asynccontextmanager(call)(**sub_values) cm = asynccontextmanager(call)(**sub_values)
task = asyncio.create_task(stack.enter_async_context(cm))
target = stack.enter_async_context(cm) dependency_cache[call] = task
return await task
elif is_coroutine_callable(call): elif is_coroutine_callable(call):
target = call(**sub_values) task = asyncio.create_task(call(**sub_values))
dependency_cache[call] = task
return await task
else: else:
target = run_sync(call)(**sub_values) task = asyncio.create_task(run_sync(call)(**sub_values))
dependency_cache[call] = task
dependency_cache[call] = cache = DependencyCache() return await task
try:
result = await target
except Exception as e:
cache.set_exception(e)
raise
except BaseException as e:
cache.set_exception(e)
# remove cache when base exception occurs
# e.g. CancelledError
dependency_cache.pop(call, None)
raise
else:
cache.set_result(result)
return result
@override @override
async def _check(self, **kwargs: Any) -> None: async def _check(self, **kwargs: Any) -> None:
@@ -523,10 +426,10 @@ class MatcherParam(Param):
class ArgInner: class ArgInner:
def __init__( def __init__(
self, key: Optional[str], type: Literal["message", "str", "plaintext", "prompt"] self, key: Optional[str], type: Literal["message", "str", "plaintext"]
) -> None: ) -> None:
self.key: Optional[str] = key self.key: Optional[str] = key
self.type: Literal["message", "str", "plaintext", "prompt"] = type self.type: Literal["message", "str", "plaintext"] = type
def __repr__(self) -> str: def __repr__(self) -> str:
return f"ArgInner(key={self.key!r}, type={self.type!r})" return f"ArgInner(key={self.key!r}, type={self.type!r})"
@@ -547,11 +450,6 @@ def ArgPlainText(key: Optional[str] = None) -> str:
return ArgInner(key, "plaintext") # type: ignore return ArgInner(key, "plaintext") # type: ignore
def ArgPromptResult(key: Optional[str] = None) -> Any:
"""`arg` prompt 发送结果"""
return ArgInner(key, "prompt")
class ArgParam(Param): class ArgParam(Param):
"""Arg 注入参数 """Arg 注入参数
@@ -565,7 +463,7 @@ class ArgParam(Param):
self, self,
*args, *args,
key: str, key: str,
type: Literal["message", "str", "plaintext", "prompt"], type: Literal["message", "str", "plaintext"],
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -590,32 +488,15 @@ class ArgParam(Param):
async def _solve( # pyright: ignore[reportIncompatibleMethodOverride] async def _solve( # pyright: ignore[reportIncompatibleMethodOverride]
self, matcher: "Matcher", **kwargs: Any self, matcher: "Matcher", **kwargs: Any
) -> Any: ) -> Any:
message = matcher.get_arg(self.key)
if message is None:
return message
if self.type == "message": if self.type == "message":
return self._solve_message(matcher) return message
elif self.type == "str": elif self.type == "str":
return self._solve_str(matcher) return str(message)
elif self.type == "plaintext":
return self._solve_plaintext(matcher)
elif self.type == "prompt":
return self._solve_prompt(matcher)
else: else:
raise ValueError(f"Unknown Arg type: {self.type}") return message.extract_plain_text()
def _solve_message(self, matcher: "Matcher") -> Optional["Message"]:
return matcher.get_arg(self.key)
def _solve_str(self, matcher: "Matcher") -> Optional[str]:
message = matcher.get_arg(self.key)
return str(message) if message is not None else None
def _solve_plaintext(self, matcher: "Matcher") -> Optional[str]:
message = matcher.get_arg(self.key)
return message.extract_plain_text() if message is not None else None
def _solve_prompt(self, matcher: "Matcher") -> Optional[Any]:
return matcher.state.get(
REJECT_PROMPT_RESULT_KEY.format(key=ARG_KEY.format(key=self.key))
)
class ExceptionParam(Param): class ExceptionParam(Param):

View File

@@ -1,16 +1,15 @@
from contextlib import AsyncExitStack import asyncio
from typing import ClassVar, NoReturn, Optional, Union
from typing_extensions import Self from typing_extensions import Self
from contextlib import AsyncExitStack
import anyio from typing import Union, ClassVar, NoReturn, Optional
from nonebot.dependencies import Dependent from nonebot.dependencies import Dependent
from nonebot.utils import run_coro_with_catch
from nonebot.exception import SkippedException from nonebot.exception import SkippedException
from nonebot.typing import T_DependencyCache, T_PermissionChecker from nonebot.typing import T_DependencyCache, T_PermissionChecker
from nonebot.utils import run_coro_with_catch
from .adapter import Bot, Event from .adapter import Bot, Event
from .params import BotParam, DefaultParam, DependParam, EventParam, Param from .params import Param, BotParam, EventParam, DependParam, DefaultParam
class Permission: class Permission:
@@ -71,26 +70,22 @@ class Permission:
""" """
if not self.checkers: if not self.checkers:
return True return True
results = await asyncio.gather(
result = False *(
run_coro_with_catch(
async def _run_checker(checker: Dependent[bool]) -> None:
nonlocal result
# calculate the result first to avoid data racing
is_passed = await run_coro_with_catch(
checker( checker(
bot=bot, event=event, stack=stack, dependency_cache=dependency_cache bot=bot,
event=event,
stack=stack,
dependency_cache=dependency_cache,
), ),
(SkippedException,), (SkippedException,),
False, False,
) )
result |= is_passed for checker in self.checkers
),
async with anyio.create_task_group() as tg: )
for checker in self.checkers: return any(results)
tg.start_soon(_run_checker, checker)
return result
def __and__(self, other: object) -> NoReturn: def __and__(self, other: object) -> NoReturn:
raise RuntimeError("And operation between Permissions is not allowed.") raise RuntimeError("And operation between Permissions is not allowed.")
@@ -124,7 +119,7 @@ class User:
perm: 需同时满足的权限 perm: 需同时满足的权限
""" """
__slots__ = ("perm", "users") __slots__ = ("users", "perm")
def __init__( def __init__(
self, users: tuple[str, ...], perm: Optional[Permission] = None self, users: tuple[str, ...], perm: Optional[Permission] = None

View File

@@ -1,15 +1,13 @@
import asyncio
from contextlib import AsyncExitStack from contextlib import AsyncExitStack
from typing import ClassVar, NoReturn, Optional, Union from typing import Union, ClassVar, NoReturn, Optional
import anyio
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.dependencies import Dependent from nonebot.dependencies import Dependent
from nonebot.exception import SkippedException from nonebot.exception import SkippedException
from nonebot.typing import T_DependencyCache, T_RuleChecker, T_State from nonebot.typing import T_State, T_RuleChecker, T_DependencyCache
from .adapter import Bot, Event from .adapter import Bot, Event
from .params import BotParam, DefaultParam, DependParam, EventParam, Param, StateParam from .params import Param, BotParam, EventParam, StateParam, DependParam, DefaultParam
class Rule: class Rule:
@@ -73,33 +71,22 @@ class Rule:
""" """
if not self.checkers: if not self.checkers:
return True return True
try:
result = True results = await asyncio.gather(
*(
def _handle_skipped_exception( checker(
exc_group: BaseExceptionGroup[SkippedException],
) -> None:
nonlocal result
result = False
async def _run_checker(checker: Dependent[bool]) -> None:
nonlocal result
# calculate the result first to avoid data racing
is_passed = await checker(
bot=bot, bot=bot,
event=event, event=event,
state=state, state=state,
stack=stack, stack=stack,
dependency_cache=dependency_cache, dependency_cache=dependency_cache,
) )
result &= is_passed for checker in self.checkers
)
with catch({SkippedException: _handle_skipped_exception}): )
async with anyio.create_task_group() as tg: except SkippedException:
for checker in self.checkers: return False
tg.start_soon(_run_checker, checker) return all(results)
return result
def __and__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule": def __and__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
if other is None: if other is None:

View File

@@ -8,15 +8,13 @@ NoneBot 使用 [`loguru`][loguru] 来记录日志信息。
[loguru]: https://github.com/Delgan/loguru [loguru]: https://github.com/Delgan/loguru
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 7 sidebar_position: 7
description: nonebot.log 模块 description: nonebot.log 模块
""" """
import sys
import inspect import inspect
import logging import logging
import sys
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import loguru import loguru

View File

@@ -1,22 +1,20 @@
"""本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。 """本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 3 sidebar_position: 3
description: nonebot.matcher 模块 description: nonebot.matcher 模块
""" """
from nonebot.internal.matcher import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
from nonebot.internal.matcher import Matcher as Matcher from nonebot.internal.matcher import Matcher as Matcher
from nonebot.internal.matcher import matchers as matchers
from nonebot.internal.matcher import current_bot as current_bot
from nonebot.internal.matcher import MatcherSource as MatcherSource
from nonebot.internal.matcher import current_event as current_event
from nonebot.internal.matcher import MatcherManager as MatcherManager from nonebot.internal.matcher import MatcherManager as MatcherManager
from nonebot.internal.matcher import MatcherProvider as MatcherProvider from nonebot.internal.matcher import MatcherProvider as MatcherProvider
from nonebot.internal.matcher import MatcherSource as MatcherSource
from nonebot.internal.matcher import current_bot as current_bot
from nonebot.internal.matcher import current_event as current_event
from nonebot.internal.matcher import current_handler as current_handler from nonebot.internal.matcher import current_handler as current_handler
from nonebot.internal.matcher import current_matcher as current_matcher from nonebot.internal.matcher import current_matcher as current_matcher
from nonebot.internal.matcher import matchers as matchers from nonebot.internal.matcher import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
__autodoc__ = { __autodoc__ = {
"Matcher": True, "Matcher": True,

View File

@@ -3,53 +3,44 @@
NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。 NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 2 sidebar_position: 2
description: nonebot.message 模块 description: nonebot.message 模块
""" """
import asyncio
import contextlib import contextlib
from contextlib import AsyncExitStack
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING, Any, Callable, Optional from contextlib import AsyncExitStack
from typing import TYPE_CHECKING, Any, Optional
import anyio
from exceptiongroup import BaseExceptionGroup, catch
from nonebot.log import logger
from nonebot.rule import TrieRule
from nonebot.dependencies import Dependent from nonebot.dependencies import Dependent
from nonebot.matcher import Matcher, matchers
from nonebot.utils import escape_tag, run_coro_with_catch
from nonebot.exception import ( from nonebot.exception import (
IgnoredException,
NoLogException, NoLogException,
SkippedException,
StopPropagation, StopPropagation,
IgnoredException,
SkippedException,
)
from nonebot.typing import (
T_State,
T_DependencyCache,
T_RunPreProcessor,
T_RunPostProcessor,
T_EventPreProcessor,
T_EventPostProcessor,
) )
from nonebot.internal.params import ( from nonebot.internal.params import (
ArgParam, ArgParam,
BotParam, BotParam,
DefaultParam,
DependParam,
EventParam, EventParam,
ExceptionParam,
MatcherParam,
StateParam, StateParam,
) DependParam,
from nonebot.log import logger DefaultParam,
from nonebot.matcher import Matcher, matchers MatcherParam,
from nonebot.rule import TrieRule ExceptionParam,
from nonebot.typing import (
T_DependencyCache,
T_EventPostProcessor,
T_EventPreProcessor,
T_RunPostProcessor,
T_RunPreProcessor,
T_State,
)
from nonebot.utils import (
escape_tag,
flatten_exception_group,
run_coro_with_catch,
run_coro_with_shield,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -132,21 +123,6 @@ def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
return func return func
def _handle_ignored_exception(msg: str) -> Callable[[BaseExceptionGroup], None]:
def _handle(exc_group: BaseExceptionGroup[IgnoredException]) -> None:
logger.opt(colors=True).info(msg)
return _handle
def _handle_exception(msg: str) -> Callable[[BaseExceptionGroup], None]:
def _handle(exc_group: BaseExceptionGroup[Exception]) -> None:
for exc in flatten_exception_group(exc_group):
logger.opt(colors=True, exception=exc).error(msg)
return _handle
async def _apply_event_preprocessors( async def _apply_event_preprocessors(
bot: "Bot", bot: "Bot",
event: "Event", event: "Event",
@@ -174,21 +150,10 @@ async def _apply_event_preprocessors(
if show_log: if show_log:
logger.debug("Running PreProcessors...") logger.debug("Running PreProcessors...")
with catch( try:
{ await asyncio.gather(
IgnoredException: _handle_ignored_exception( *(
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>" run_coro_with_catch(
),
Exception: _handle_exception(
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
"Event ignored!</bg #f8bbd0></r>"
),
}
):
async with anyio.create_task_group() as tg:
for proc in _event_preprocessors:
tg.start_soon(
run_coro_with_catch,
proc( proc(
bot=bot, bot=bot,
event=event, event=event,
@@ -198,11 +163,23 @@ async def _apply_event_preprocessors(
), ),
(SkippedException,), (SkippedException,),
) )
for proc in _event_preprocessors
)
)
except IgnoredException:
logger.opt(colors=True).info(
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
)
return False
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
"Event ignored!</bg #f8bbd0></r>"
)
return False
return True return True
return False
async def _apply_event_postprocessors( async def _apply_event_postprocessors(
bot: "Bot", bot: "Bot",
@@ -228,17 +205,10 @@ async def _apply_event_postprocessors(
if show_log: if show_log:
logger.debug("Running PostProcessors...") logger.debug("Running PostProcessors...")
with catch( try:
{ await asyncio.gather(
Exception: _handle_exception( *(
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>" run_coro_with_catch(
)
}
):
async with anyio.create_task_group() as tg:
for proc in _event_postprocessors:
tg.start_soon(
run_coro_with_catch,
proc( proc(
bot=bot, bot=bot,
event=event, event=event,
@@ -248,6 +218,13 @@ async def _apply_event_postprocessors(
), ),
(SkippedException,), (SkippedException,),
) )
for proc in _event_postprocessors
)
)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
)
async def _apply_run_preprocessors( async def _apply_run_preprocessors(
@@ -275,24 +252,11 @@ async def _apply_run_preprocessors(
return True return True
# ensure matcher function can be correctly called # ensure matcher function can be correctly called
with ( with matcher.ensure_context(bot, event):
matcher.ensure_context(bot, event), try:
catch( await asyncio.gather(
{ *(
IgnoredException: _handle_ignored_exception( run_coro_with_catch(
f"{matcher} running is <b>cancelled</b>"
),
Exception: _handle_exception(
"<r><bg #f8bbd0>Error when running RunPreProcessors. "
"Running cancelled!</bg #f8bbd0></r>"
),
}
),
):
async with anyio.create_task_group() as tg:
for proc in _run_preprocessors:
tg.start_soon(
run_coro_with_catch,
proc( proc(
matcher=matcher, matcher=matcher,
bot=bot, bot=bot,
@@ -303,11 +267,21 @@ async def _apply_run_preprocessors(
), ),
(SkippedException,), (SkippedException,),
) )
for proc in _run_preprocessors
)
)
except IgnoredException:
logger.opt(colors=True).info(f"{matcher} running is <b>cancelled</b>")
return False
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running RunPreProcessors. "
"Running cancelled!</bg #f8bbd0></r>"
)
return False
return True return True
return False
async def _apply_run_postprocessors( async def _apply_run_postprocessors(
bot: "Bot", bot: "Bot",
@@ -330,21 +304,11 @@ async def _apply_run_postprocessors(
if not _run_postprocessors: if not _run_postprocessors:
return return
with ( with matcher.ensure_context(bot, event):
matcher.ensure_context(bot, event), try:
catch( await asyncio.gather(
{ *(
Exception: _handle_exception( run_coro_with_catch(
"<r><bg #f8bbd0>Error when running RunPostProcessors"
"</bg #f8bbd0></r>"
)
}
),
):
async with anyio.create_task_group() as tg:
for proc in _run_postprocessors:
tg.start_soon(
run_coro_with_catch,
proc( proc(
matcher=matcher, matcher=matcher,
exception=exception, exception=exception,
@@ -356,6 +320,13 @@ async def _apply_run_postprocessors(
), ),
(SkippedException,), (SkippedException,),
) )
for proc in _run_postprocessors
)
)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running RunPostProcessors</bg #f8bbd0></r>"
)
async def _check_matcher( async def _check_matcher(
@@ -452,9 +423,8 @@ async def _run_matcher(
exception = None exception = None
logger.debug(f"Running {matcher}")
try: try:
logger.debug(f"Running {matcher}")
await matcher.run(bot, event, state, stack, dependency_cache) await matcher.run(bot, event, state, stack, dependency_cache)
except Exception as e: except Exception as e:
logger.opt(colors=True, exception=e).error( logger.opt(colors=True, exception=e).error(
@@ -522,7 +492,8 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
用法: 用法:
```python ```python
driver.task_group.start_soon(handle_event, bot, event) import asyncio
asyncio.create_task(handle_event(bot, event))
``` ```
""" """
show_log = True show_log = True
@@ -557,13 +528,6 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
) )
break_flag = False break_flag = False
def _handle_stop_propagation(exc_group: BaseExceptionGroup) -> None:
nonlocal break_flag
break_flag = True
logger.debug("Stop event propagation")
# iterate through all priority until stop propagation # iterate through all priority until stop propagation
for priority in sorted(matchers.keys()): for priority in sorted(matchers.keys()):
if break_flag: if break_flag:
@@ -572,29 +536,22 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
if show_log: if show_log:
logger.debug(f"Checking for matchers in priority {priority}...") logger.debug(f"Checking for matchers in priority {priority}...")
if not (priority_matchers := matchers[priority]): pending_tasks = [
continue
with catch(
{
StopPropagation: _handle_stop_propagation,
Exception: _handle_exception(
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
),
}
):
async with anyio.create_task_group() as tg:
for matcher in priority_matchers:
tg.start_soon(
run_coro_with_shield,
check_and_run_matcher( check_and_run_matcher(
matcher, matcher, bot, event, state.copy(), stack, dependency_cache
bot, )
event, for matcher in matchers[priority]
state.copy(), ]
stack, results = await asyncio.gather(*pending_tasks, return_exceptions=True)
dependency_cache, for result in results:
), if not isinstance(result, Exception):
continue
if isinstance(result, StopPropagation):
break_flag = True
logger.debug("Stop event propagation")
else:
logger.opt(colors=True, exception=result).error(
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
) )
if show_log: if show_log:

View File

@@ -1,49 +1,43 @@
"""本模块定义了依赖注入的各类参数。 """本模块定义了依赖注入的各类参数。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 4 sidebar_position: 4
description: nonebot.params 模块 description: nonebot.params 模块
""" """
from re import Match from re import Match
from typing import Any, Callable, Literal, Optional, Union, overload from typing import Any, Union, Literal, Callable, Optional, overload
from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.internal.params import Arg as Arg
from nonebot.internal.params import ArgStr as ArgStr
from nonebot.internal.params import Depends as Depends
from nonebot.internal.params import ArgParam as ArgParam
from nonebot.internal.params import BotParam as BotParam
from nonebot.adapters import Event, Message, MessageSegment from nonebot.adapters import Event, Message, MessageSegment
from nonebot.internal.params import EventParam as EventParam
from nonebot.internal.params import StateParam as StateParam
from nonebot.internal.params import DependParam as DependParam
from nonebot.internal.params import ArgPlainText as ArgPlainText
from nonebot.internal.params import DefaultParam as DefaultParam
from nonebot.internal.params import MatcherParam as MatcherParam
from nonebot.internal.params import ExceptionParam as ExceptionParam
from nonebot.consts import ( from nonebot.consts import (
CMD_ARG_KEY,
CMD_KEY, CMD_KEY,
CMD_START_KEY,
CMD_WHITESPACE_KEY,
ENDSWITH_KEY,
FULLMATCH_KEY,
KEYWORD_KEY,
PAUSE_PROMPT_RESULT_KEY,
PREFIX_KEY, PREFIX_KEY,
RAW_CMD_KEY,
RECEIVE_KEY,
REGEX_MATCHED,
REJECT_PROMPT_RESULT_KEY,
SHELL_ARGS, SHELL_ARGS,
SHELL_ARGV, SHELL_ARGV,
CMD_ARG_KEY,
KEYWORD_KEY,
RAW_CMD_KEY,
ENDSWITH_KEY,
CMD_START_KEY,
FULLMATCH_KEY,
REGEX_MATCHED,
STARTSWITH_KEY, STARTSWITH_KEY,
CMD_WHITESPACE_KEY,
) )
from nonebot.internal.params import Arg as Arg
from nonebot.internal.params import ArgParam as ArgParam
from nonebot.internal.params import ArgPlainText as ArgPlainText
from nonebot.internal.params import ArgPromptResult as ArgPromptResult
from nonebot.internal.params import ArgStr as ArgStr
from nonebot.internal.params import BotParam as BotParam
from nonebot.internal.params import DefaultParam as DefaultParam
from nonebot.internal.params import DependParam as DependParam
from nonebot.internal.params import Depends as Depends
from nonebot.internal.params import EventParam as EventParam
from nonebot.internal.params import ExceptionParam as ExceptionParam
from nonebot.internal.params import MatcherParam as MatcherParam
from nonebot.internal.params import StateParam as StateParam
from nonebot.matcher import Matcher
from nonebot.typing import T_State
async def _event_type(event: Event) -> str: async def _event_type(event: Event) -> str:
@@ -155,7 +149,7 @@ def RegexMatched() -> Match[str]:
def _regex_str( def _regex_str(
groups: tuple[Union[str, int], ...], groups: tuple[Union[str, int], ...]
) -> Callable[[T_State], Union[str, tuple[Union[str, Any], ...], Any]]: ) -> Callable[[T_State], Union[str, tuple[Union[str, Any], ...], Any]]:
def _regex_str_dependency( def _regex_str_dependency(
state: T_State, state: T_State,
@@ -166,16 +160,16 @@ def _regex_str(
@overload @overload
def RegexStr(group: Literal[0] = 0, /) -> str: ... def RegexStr(__group: Literal[0] = 0) -> str: ...
@overload @overload
def RegexStr(group: Union[str, int], /) -> Union[str, Any]: ... def RegexStr(__group: Union[str, int]) -> Union[str, Any]: ...
@overload @overload
def RegexStr( def RegexStr(
group1: Union[str, int], group2: Union[str, int], /, *groups: Union[str, int] __group1: Union[str, int], __group2: Union[str, int], *groups: Union[str, int]
) -> tuple[Union[str, Any], ...]: ... ) -> tuple[Union[str, Any], ...]: ...
@@ -256,26 +250,6 @@ def LastReceived(default: Any = None) -> Any:
return Depends(_last_received, use_cache=False) return Depends(_last_received, use_cache=False)
def ReceivePromptResult(id: Optional[str] = None) -> Any:
"""`receive` prompt 发送结果"""
def _receive_prompt_result(matcher: "Matcher") -> Any:
return matcher.state.get(
REJECT_PROMPT_RESULT_KEY.format(key=RECEIVE_KEY.format(id=id))
)
return Depends(_receive_prompt_result, use_cache=False)
def PausePromptResult() -> Any:
"""`pause` prompt 发送结果"""
def _pause_prompt_result(matcher: "Matcher") -> Any:
return matcher.state.get(PAUSE_PROMPT_RESULT_KEY)
return Depends(_pause_prompt_result, use_cache=False)
__autodoc__ = { __autodoc__ = {
"Arg": True, "Arg": True,
"ArgStr": True, "ArgStr": True,
@@ -289,5 +263,4 @@ __autodoc__ = {
"DefaultParam": True, "DefaultParam": True,
"MatcherParam": True, "MatcherParam": True,
"ExceptionParam": True, "ExceptionParam": True,
"ArgPromptResult": True,
} }

View File

@@ -5,17 +5,15 @@
只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。 只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 6 sidebar_position: 6
description: nonebot.permission 模块 description: nonebot.permission 模块
""" """
from nonebot.params import EventType
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.internal.permission import USER as USER from nonebot.internal.permission import USER as USER
from nonebot.internal.permission import Permission as Permission
from nonebot.internal.permission import User as User from nonebot.internal.permission import User as User
from nonebot.params import EventType from nonebot.internal.permission import Permission as Permission
class Message: class Message:

View File

@@ -32,16 +32,14 @@
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.model.PluginMetadata>` - `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.model.PluginMetadata>`
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 0 sidebar_position: 0
description: nonebot.plugin 模块 description: nonebot.plugin 模块
""" """
from contextvars import ContextVar
from itertools import chain from itertools import chain
from types import ModuleType from types import ModuleType
from typing import Optional, TypeVar from contextvars import ContextVar
from typing import TypeVar, Optional
from pydantic import BaseModel from pydantic import BaseModel
@@ -175,30 +173,30 @@ def get_plugin_config(config: type[C]) -> C:
return type_validate_python(config, model_dump(get_driver().config)) return type_validate_python(config, model_dump(get_driver().config))
from .load import inherit_supported_adapters as inherit_supported_adapters from .on import on as on
from .manager import PluginManager
from .on import on_type as on_type
from .model import Plugin as Plugin
from .load import require as require
from .on import on_regex as on_regex
from .on import on_notice as on_notice
from .on import on_command as on_command
from .on import on_keyword as on_keyword
from .on import on_message as on_message
from .on import on_request as on_request
from .on import on_endswith as on_endswith
from .load import load_plugin as load_plugin
from .on import CommandGroup as CommandGroup
from .on import MatcherGroup as MatcherGroup
from .on import on_fullmatch as on_fullmatch
from .on import on_metaevent as on_metaevent
from .load import load_plugins as load_plugins
from .on import on_startswith as on_startswith
from .load import load_from_json as load_from_json
from .load import load_from_toml as load_from_toml
from .model import PluginMetadata as PluginMetadata
from .on import on_shell_command as on_shell_command
from .load import load_all_plugins as load_all_plugins from .load import load_all_plugins as load_all_plugins
from .load import load_builtin_plugin as load_builtin_plugin from .load import load_builtin_plugin as load_builtin_plugin
from .load import load_builtin_plugins as load_builtin_plugins from .load import load_builtin_plugins as load_builtin_plugins
from .load import load_from_json as load_from_json from .load import inherit_supported_adapters as inherit_supported_adapters
from .load import load_from_toml as load_from_toml
from .load import load_plugin as load_plugin
from .load import load_plugins as load_plugins
from .load import require as require
from .manager import PluginManager
from .model import Plugin as Plugin
from .model import PluginMetadata as PluginMetadata
from .on import CommandGroup as CommandGroup
from .on import MatcherGroup as MatcherGroup
from .on import on as on
from .on import on_command as on_command
from .on import on_endswith as on_endswith
from .on import on_fullmatch as on_fullmatch
from .on import on_keyword as on_keyword
from .on import on_message as on_message
from .on import on_metaevent as on_metaevent
from .on import on_notice as on_notice
from .on import on_regex as on_regex
from .on import on_request as on_request
from .on import on_shell_command as on_shell_command
from .on import on_startswith as on_startswith
from .on import on_type as on_type

View File

@@ -1,28 +1,26 @@
"""本模块定义插件加载接口。 """本模块定义插件加载接口。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 1 sidebar_position: 1
description: nonebot.plugin.load 模块 description: nonebot.plugin.load 模块
""" """
from collections.abc import Iterable
import json import json
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import Optional, Union from typing import Union, Optional
from collections.abc import Iterable
from nonebot.utils import path_to_module_name from nonebot.utils import path_to_module_name
from . import _managers, _module_name_to_plugin_id, get_plugin
from .manager import PluginManager
from .model import Plugin from .model import Plugin
from .manager import PluginManager
from . import _managers, get_plugin, _module_name_to_plugin_id
try: # pragma: py-gte-311 try: # pragma: py-gte-311
import tomllib # pyright: ignore[reportMissingImports] import tomllib # pyright: ignore[reportMissingImports]
except ModuleNotFoundError: # pragma: py-lt-311 except ModuleNotFoundError: # pragma: py-lt-311
import tomli as tomllib # pyright: ignore[reportMissingImports] import tomli as tomllib
def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]: def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:

View File

@@ -3,34 +3,32 @@
参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/) 参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/)
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 5 sidebar_position: 5
description: nonebot.plugin.manager 模块 description: nonebot.plugin.manager 模块
""" """
from collections.abc import Iterable, Sequence
import importlib
from importlib.abc import MetaPathFinder
from importlib.machinery import PathFinder, SourceFileLoader
from itertools import chain
from pathlib import Path
import pkgutil
import sys import sys
from types import ModuleType import pkgutil
import importlib
from pathlib import Path
from itertools import chain
from typing import Optional from typing import Optional
from types import ModuleType
from importlib.abc import MetaPathFinder
from collections.abc import Iterable, Sequence
from importlib.machinery import PathFinder, SourceFileLoader
from nonebot.log import logger from nonebot.log import logger
from nonebot.utils import escape_tag, path_to_module_name from nonebot.utils import escape_tag, path_to_module_name
from .model import Plugin, PluginMetadata
from . import ( from . import (
_current_plugin,
_managers, _managers,
_module_name_to_plugin_id,
_new_plugin, _new_plugin,
_revert_plugin, _revert_plugin,
_current_plugin,
_module_name_to_plugin_id,
) )
from .model import Plugin, PluginMetadata
class PluginManager: class PluginManager:

View File

@@ -1,16 +1,14 @@
"""本模块定义插件相关信息。 """本模块定义插件相关信息。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 3 sidebar_position: 3
description: nonebot.plugin.model 模块 description: nonebot.plugin.model 模块
""" """
import contextlib import contextlib
from dataclasses import dataclass, field
from types import ModuleType from types import ModuleType
from typing import TYPE_CHECKING, Any, Optional, Type # noqa: UP035 from dataclasses import field, dataclass
from typing import TYPE_CHECKING, Any, Type, Optional # noqa: UP035
from pydantic import BaseModel from pydantic import BaseModel

View File

@@ -1,40 +1,38 @@
"""本模块定义事件响应器便携定义函数。 """本模块定义事件响应器便携定义函数。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 2 sidebar_position: 2
description: nonebot.plugin.on 模块 description: nonebot.plugin.on 模块
""" """
from datetime import datetime, timedelta
import inspect
import re import re
from types import ModuleType import inspect
from typing import Any, Optional, Union
import warnings import warnings
from types import ModuleType
from typing import Any, Union, Optional
from datetime import datetime, timedelta
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.permission import Permission
from nonebot.dependencies import Dependent from nonebot.dependencies import Dependent
from nonebot.matcher import Matcher, MatcherSource from nonebot.matcher import Matcher, MatcherSource
from nonebot.permission import Permission from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
from nonebot.rule import ( from nonebot.rule import (
ArgumentParser,
Rule, Rule,
ArgumentParser,
regex,
command, command,
endswith,
fullmatch,
is_type, is_type,
keyword, keyword,
regex, endswith,
shell_command, fullmatch,
startswith, startswith,
shell_command,
) )
from nonebot.typing import T_Handler, T_PermissionChecker, T_RuleChecker, T_State
from . import get_plugin_by_module_name
from .manager import _current_plugin
from .model import Plugin from .model import Plugin
from .manager import _current_plugin
from . import get_plugin_by_module_name
def store_matcher(matcher: type[Matcher]) -> None: def store_matcher(matcher: type[Matcher]) -> None:

View File

@@ -1,14 +1,14 @@
from datetime import datetime, timedelta
import re import re
from types import ModuleType
from typing import Any from typing import Any
from types import ModuleType
from datetime import datetime, timedelta
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.dependencies import Dependent
from nonebot.matcher import Matcher, MatcherSource
from nonebot.permission import Permission from nonebot.permission import Permission
from nonebot.rule import ArgumentParser, Rule from nonebot.dependencies import Dependent
from nonebot.typing import T_Handler, T_PermissionChecker, T_RuleChecker, T_State from nonebot.rule import Rule, ArgumentParser
from nonebot.matcher import Matcher, MatcherSource
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
from .model import Plugin from .model import Plugin

View File

@@ -1,8 +1,8 @@
from nonebot import on_command from nonebot import on_command
from nonebot.rule import to_me
from nonebot.adapters import Message from nonebot.adapters import Message
from nonebot.params import CommandArg from nonebot.params import CommandArg
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from nonebot.rule import to_me
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="echo", name="echo",

View File

@@ -1,9 +1,9 @@
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.message import IgnoredException, event_preprocessor
from nonebot.params import Depends from nonebot.params import Depends
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from nonebot.message import IgnoredException, event_preprocessor
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="唯一会话", name="唯一会话",

View File

@@ -5,29 +5,28 @@
只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。 只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 5 sidebar_position: 5
description: nonebot.rule 模块 description: nonebot.rule 模块
""" """
from argparse import Action, ArgumentError
from argparse import ArgumentParser as ArgParser
from argparse import Namespace as Namespace
from collections.abc import Sequence
from contextvars import ContextVar
from gettext import gettext
from itertools import chain, product
import re import re
import shlex import shlex
from argparse import Action
from gettext import gettext
from argparse import ArgumentError
from contextvars import ContextVar
from collections.abc import Sequence
from itertools import chain, product
from argparse import Namespace as Namespace
from argparse import ArgumentParser as ArgParser
from typing import ( from typing import (
IO, IO,
TYPE_CHECKING, TYPE_CHECKING,
NamedTuple, Union,
TypeVar,
Optional, Optional,
TypedDict, TypedDict,
TypeVar, NamedTuple,
Union,
cast, cast,
overload, overload,
) )
@@ -35,27 +34,27 @@ from typing import (
from pygtrie import CharTrie from pygtrie import CharTrie
from nonebot import get_driver from nonebot import get_driver
from nonebot.adapters import Bot, Event, Message, MessageSegment from nonebot.log import logger
from nonebot.consts import ( from nonebot.typing import T_State
CMD_ARG_KEY,
CMD_KEY,
CMD_START_KEY,
CMD_WHITESPACE_KEY,
ENDSWITH_KEY,
FULLMATCH_KEY,
KEYWORD_KEY,
PREFIX_KEY,
RAW_CMD_KEY,
REGEX_MATCHED,
SHELL_ARGS,
SHELL_ARGV,
STARTSWITH_KEY,
)
from nonebot.exception import ParserExit from nonebot.exception import ParserExit
from nonebot.internal.rule import Rule as Rule from nonebot.internal.rule import Rule as Rule
from nonebot.log import logger from nonebot.adapters import Bot, Event, Message, MessageSegment
from nonebot.params import Command, CommandArg, CommandWhitespace, EventToMe from nonebot.params import Command, EventToMe, CommandArg, CommandWhitespace
from nonebot.typing import T_State from nonebot.consts import (
CMD_KEY,
PREFIX_KEY,
SHELL_ARGS,
SHELL_ARGV,
CMD_ARG_KEY,
KEYWORD_KEY,
RAW_CMD_KEY,
ENDSWITH_KEY,
CMD_START_KEY,
FULLMATCH_KEY,
REGEX_MATCHED,
STARTSWITH_KEY,
CMD_WHITESPACE_KEY,
)
T = TypeVar("T") T = TypeVar("T")
@@ -145,7 +144,7 @@ class StartswithRule:
ignorecase: 是否忽略大小写 ignorecase: 是否忽略大小写
""" """
__slots__ = ("ignorecase", "msg") __slots__ = ("msg", "ignorecase")
def __init__(self, msg: tuple[str, ...], ignorecase: bool = False): def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):
self.msg = msg self.msg = msg
@@ -200,7 +199,7 @@ class EndswithRule:
ignorecase: 是否忽略大小写 ignorecase: 是否忽略大小写
""" """
__slots__ = ("ignorecase", "msg") __slots__ = ("msg", "ignorecase")
def __init__(self, msg: tuple[str, ...], ignorecase: bool = False): def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):
self.msg = msg self.msg = msg
@@ -255,7 +254,7 @@ class FullmatchRule:
ignorecase: 是否忽略大小写 ignorecase: 是否忽略大小写
""" """
__slots__ = ("ignorecase", "msg") __slots__ = ("msg", "ignorecase")
def __init__(self, msg: tuple[str, ...], ignorecase: bool = False): def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):
self.msg = tuple(map(str.casefold, msg) if ignorecase else msg) self.msg = tuple(map(str.casefold, msg) if ignorecase else msg)
@@ -654,7 +653,7 @@ class RegexRule:
flags: 正则表达式标记 flags: 正则表达式标记
""" """
__slots__ = ("flags", "regex") __slots__ = ("regex", "flags")
def __init__(self, regex: str, flags: int = 0): def __init__(self, regex: str, flags: int = 0):
self.regex = regex self.regex = regex

View File

@@ -6,23 +6,22 @@
[`typing`](https://docs.python.org/3/library/typing.html)。 [`typing`](https://docs.python.org/3/library/typing.html)。
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 11 sidebar_position: 11
description: nonebot.typing 模块 description: nonebot.typing 模块
""" """
import sys import sys
import types import types
import typing as t
from typing import TYPE_CHECKING, TypeVar
import typing_extensions as t_ext
from typing_extensions import ParamSpec, TypeAlias, get_args, get_origin, override
import warnings import warnings
import typing as t
import typing_extensions as t_ext
from typing import TYPE_CHECKING, TypeVar
from typing_extensions import ParamSpec, TypeAlias, get_args, override, get_origin
if TYPE_CHECKING: if TYPE_CHECKING:
from asyncio import Task
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.internal.params import DependencyCache
from nonebot.permission import Permission from nonebot.permission import Permission
T = TypeVar("T") T = TypeVar("T")
@@ -257,5 +256,5 @@ T_PermissionUpdater: TypeAlias = _DependentCallable["Permission"]
- MatcherParam: Matcher 对象 - MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数 - DefaultParam: 带有默认值的参数
""" """
T_DependencyCache: TypeAlias = dict[_DependentCallable[t.Any], "DependencyCache"] T_DependencyCache: TypeAlias = dict[_DependentCallable[t.Any], "Task[t.Any]"]
"""依赖缓存, 用于存储依赖函数的返回值""" """依赖缓存, 用于存储依赖函数的返回值"""

View File

@@ -1,38 +1,35 @@
"""本模块包含了 NoneBot 的一些工具函数 """本模块包含了 NoneBot 的一些工具函数
FrontMatter: FrontMatter:
mdx:
format: md
sidebar_position: 8 sidebar_position: 8
description: nonebot.utils 模块 description: nonebot.utils 模块
""" """
from collections import deque
from collections.abc import AsyncGenerator, Coroutine, Generator, Mapping, Sequence
import contextlib
from contextlib import AbstractContextManager, asynccontextmanager
import dataclasses
from functools import partial, wraps
import importlib
import inspect
import json
from pathlib import Path
import re import re
from typing import Any, Callable, Generic, Optional, TypeVar, Union, overload import json
from typing_extensions import ParamSpec, get_args, get_origin, override import asyncio
import inspect
import importlib
import contextlib
import dataclasses
from pathlib import Path
from collections import deque
from contextvars import copy_context
from functools import wraps, partial
from contextlib import AbstractContextManager, asynccontextmanager
from typing_extensions import ParamSpec, get_args, override, get_origin
from collections.abc import Mapping, Sequence, Coroutine, AsyncGenerator
from typing import Any, Union, Generic, TypeVar, Callable, Optional, overload
import anyio
import anyio.to_thread
from exceptiongroup import BaseExceptionGroup, catch
from pydantic import BaseModel from pydantic import BaseModel
from nonebot.log import logger from nonebot.log import logger
from nonebot.typing import ( from nonebot.typing import (
all_literal_values,
is_none_type, is_none_type,
origin_is_literal,
origin_is_union,
type_has_args, type_has_args,
origin_is_union,
origin_is_literal,
all_literal_values,
) )
P = ParamSpec("P") P = ParamSpec("P")
@@ -40,7 +37,6 @@ R = TypeVar("R")
T = TypeVar("T") T = TypeVar("T")
K = TypeVar("K") K = TypeVar("K")
V = TypeVar("V") V = TypeVar("V")
E = TypeVar("E", bound=BaseException)
def escape_tag(s: str) -> str: def escape_tag(s: str) -> str:
@@ -180,9 +176,11 @@ def run_sync(call: Callable[P, R]) -> Callable[P, Coroutine[None, None, R]]:
@wraps(call) @wraps(call)
async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R: async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return await anyio.to_thread.run_sync( loop = asyncio.get_running_loop()
partial(call, *args, **kwargs), abandon_on_cancel=True pfunc = partial(call, *args, **kwargs)
) context = copy_context()
result = await loop.run_in_executor(None, partial(context.run, pfunc))
return result
return _wrapper return _wrapper
@@ -234,36 +232,12 @@ async def run_coro_with_catch(
协程的返回值或发生异常时的指定值 协程的返回值或发生异常时的指定值
""" """
with catch({exc: lambda exc_group: None}): try:
return await coro return await coro
except exc:
return return_on_err return return_on_err
async def run_coro_with_shield(coro: Coroutine[Any, Any, T]) -> T:
"""运行协程并在取消时屏蔽取消异常。
参数:
coro: 要运行的协程
返回:
协程的返回值
"""
with anyio.CancelScope(shield=True):
return await coro
def flatten_exception_group(
exc_group: BaseExceptionGroup[E],
) -> Generator[E, None, None]:
for exc in exc_group.exceptions:
if isinstance(exc, BaseExceptionGroup):
yield from flatten_exception_group(exc)
else:
yield exc
def get_name(obj: Any) -> str: def get_name(obj: Any) -> str:
"""获取对象的名称""" """获取对象的名称"""
if inspect.isfunction(obj) or inspect.isclass(obj): if inspect.isfunction(obj) or inspect.isclass(obj):

3167
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "nonebot2" name = "nonebot2"
version = "2.4.1" version = "2.3.2"
description = "An asynchronous python bot framework." description = "An asynchronous python bot framework."
authors = ["yanyongyu <yyy@nonebot.dev>"] authors = ["yanyongyu <yyy@nonebot.dev>"]
license = "MIT" license = "MIT"
@@ -22,33 +22,33 @@ include = ["nonebot/py.typed"]
[tool.poetry.urls] [tool.poetry.urls]
"Bug Tracker" = "https://github.com/nonebot/nonebot2/issues" "Bug Tracker" = "https://github.com/nonebot/nonebot2/issues"
"Changelog" = "https://nonebot.dev/changelog" "Changelog" = "https://nonebot.dev/changelog"
"Funding" = "https://afdian.com/@nonebot" "Funding" = "https://afdian.net/@nonebot"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9" python = "^3.9"
yarl = "^1.7.2" yarl = "^1.7.2"
anyio = "^4.4.0"
pygtrie = "^2.4.1" pygtrie = "^2.4.1"
exceptiongroup = "^1.2.2"
loguru = ">=0.6.0,<1.0.0" loguru = ">=0.6.0,<1.0.0"
python-dotenv = ">=0.21.0,<2.0.0" python-dotenv = ">=0.21.0,<2.0.0"
typing-extensions = ">=4.4.0,<5.0.0" typing-extensions = ">=4.4.0,<5.0.0"
pydantic = ">=1.10.0,<3.0.0,!=2.5.0,!=2.5.1"
tomli = { version = "^2.0.1", python = "<3.11" } tomli = { version = "^2.0.1", python = "<3.11" }
pydantic = ">=1.10.0,<3.0.0,!=2.5.0,!=2.5.1,!=2.10.0,!=2.10.1"
websockets = { version = ">=10.0", optional = true } websockets = { version = ">=10.0", optional = true }
Quart = { version = ">=0.18.0,<1.0.0", optional = true } Quart = { version = ">=0.18.0,<1.0.0", optional = true }
fastapi = { version = ">=0.93.0,<1.0.0", optional = true } fastapi = { version = ">=0.93.0,<1.0.0", optional = true }
aiohttp = { version = "^3.11.0", extras = ["speedups"], optional = true } aiohttp = { version = "^3.9.0b0", extras = ["speedups"], optional = true }
httpx = { version = ">=0.26.0,<1.0.0", extras = ["http2"], optional = true } httpx = { version = ">=0.20.0,<1.0.0", extras = ["http2"], optional = true }
uvicorn = { version = ">=0.20.0,<1.0.0", extras = [ uvicorn = { version = ">=0.20.0,<1.0.0", extras = [
"standard", "standard",
], optional = true } ], optional = true }
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
ruff = "^0.8.0" ruff = "^0.4.0"
isort = "^5.10.1"
black = "^24.0.0"
nonemoji = "^0.1.2" nonemoji = "^0.1.2"
pre-commit = "^4.0.0" pre-commit = "^3.0.0"
[tool.poetry.group.test.dependencies] [tool.poetry.group.test.dependencies]
nonebot-test = { path = "./envs/test/", develop = false } nonebot-test = { path = "./envs/test/", develop = false }
@@ -65,22 +65,35 @@ fastapi = ["fastapi", "uvicorn"]
all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"] all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_mode = "strict"
addopts = "--cov=nonebot --cov-report=term-missing" addopts = "--cov=nonebot --cov-report=term-missing"
filterwarnings = ["error", "ignore::DeprecationWarning"] filterwarnings = ["error", "ignore::DeprecationWarning"]
[tool.black]
line-length = 88
target-version = ["py39", "py310", "py311", "py312"]
include = '\.pyi?$'
extend-exclude = '''
'''
[tool.isort]
profile = "black"
line_length = 88
length_sort = true
skip_gitignore = true
force_sort_within_sections = true
src_paths = ["nonebot", "tests"]
extra_standard_library = ["typing_extensions"]
[tool.ruff] [tool.ruff]
line-length = 88 line-length = 88
target-version = "py39" target-version = "py39"
[tool.ruff.format]
line-ending = "lf"
[tool.ruff.lint] [tool.ruff.lint]
select = [ select = [
"F", # Pyflakes "F", # Pyflakes
"W", # pycodestyle warnings "W", # pycodestyle warnings
"E", # pycodestyle errors "E", # pycodestyle errors
"I", # isort
"UP", # pyupgrade "UP", # pyupgrade
"ASYNC", # flake8-async "ASYNC", # flake8-async
"C4", # flake8-comprehensions "C4", # flake8-comprehensions
@@ -89,7 +102,6 @@ select = [
"PYI", # flake8-pyi "PYI", # flake8-pyi
"PT", # flake8-pytest-style "PT", # flake8-pytest-style
"Q", # flake8-quotes "Q", # flake8-quotes
"TID", # flake8-tidy-imports
"RUF", # Ruff-specific rules "RUF", # Ruff-specific rules
] ]
ignore = [ ignore = [
@@ -100,19 +112,10 @@ ignore = [
"RUF003", # ambiguous-unicode-character-comment "RUF003", # ambiguous-unicode-character-comment
] ]
[tool.ruff.lint.isort]
force-sort-within-sections = true
known-first-party = ["nonebot", "tests/*"]
extra-standard-library = ["typing_extensions"]
[tool.ruff.lint.flake8-pytest-style] [tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false fixture-parentheses = false
mark-parentheses = false mark-parentheses = false
[tool.ruff.lint.pyupgrade]
keep-runtime-typing = true
[tool.pyright] [tool.pyright]
pythonVersion = "3.9" pythonVersion = "3.9"
pythonPlatform = "All" pythonPlatform = "All"

View File

@@ -4,4 +4,4 @@
cd "$(dirname "$0")/../tests" cd "$(dirname "$0")/../tests"
# Run the tests # Run the tests
pytest -n auto --cov-append --cov-report xml --junitxml=./junit.xml $@ pytest -n auto --cov-append --cov-report xml $@

View File

@@ -1,20 +1,18 @@
from collections.abc import Generator
from functools import wraps
import os import os
from pathlib import Path
import threading import threading
from typing import TYPE_CHECKING, Callable, TypeVar from pathlib import Path
from typing_extensions import ParamSpec from typing import TYPE_CHECKING
from collections.abc import Generator
from nonebug import NONEBOT_INIT_KWARGS
import pytest import pytest
from nonebug import NONEBOT_INIT_KWARGS
from werkzeug.serving import BaseWSGIServer, make_server from werkzeug.serving import BaseWSGIServer, make_server
from fake_server import request_handler
import nonebot import nonebot
from nonebot import _resolve_combine_expr
from nonebot.config import Env from nonebot.config import Env
from fake_server import request_handler
from nonebot.drivers import URL, Driver from nonebot.drivers import URL, Driver
from nonebot import _resolve_combine_expr
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}' os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
os.environ["CONFIG_OVERRIDE"] = "new" os.environ["CONFIG_OVERRIDE"] = "new"
@@ -22,9 +20,6 @@ os.environ["CONFIG_OVERRIDE"] = "new"
if TYPE_CHECKING: if TYPE_CHECKING:
from nonebot.plugin import Plugin from nonebot.plugin import Plugin
P = ParamSpec("P")
R = TypeVar("R")
collect_ignore = ["plugins/", "dynamic/", "bad_plugins/"] collect_ignore = ["plugins/", "dynamic/", "bad_plugins/"]
@@ -43,36 +38,14 @@ def load_driver(request: pytest.FixtureRequest) -> Driver:
return DriverClass(Env(environment=global_driver.env), global_driver.config) return DriverClass(Env(environment=global_driver.env), global_driver.config)
@pytest.fixture(scope="session", params=[pytest.param("asyncio"), pytest.param("trio")])
def anyio_backend(request: pytest.FixtureRequest):
return request.param
def run_once(func: Callable[P, R]) -> Callable[P, R]:
result = ...
@wraps(func)
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
nonlocal result
if result is not Ellipsis:
return result
result = func(*args, **kwargs)
return result
return _wrapper
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)
@run_once def load_plugin(nonebug_init: None) -> set["Plugin"]:
def load_plugin(anyio_backend, nonebug_init: None) -> set["Plugin"]:
# preload global plugins # preload global plugins
return nonebot.load_plugins(str(Path(__file__).parent / "plugins")) return nonebot.load_plugins(str(Path(__file__).parent / "plugins"))
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)
@run_once def load_builtin_plugin(nonebug_init: None) -> set["Plugin"]:
def load_builtin_plugin(anyio_backend, nonebug_init: None) -> set["Plugin"]:
# preload builtin plugins # preload builtin plugins
return nonebot.load_builtin_plugins("echo", "single_session") return nonebot.load_builtin_plugins("echo", "single_session")

View File

@@ -1,20 +1,15 @@
import base64
import json import json
import base64
import socket import socket
from typing import TypeVar, Union from typing import Union, TypeVar
from wsproto.events import Ping
from werkzeug import Request, Response from werkzeug import Request, Response
from werkzeug.datastructures import MultiDict from werkzeug.datastructures import MultiDict
from wsproto import ConnectionType, WSConnection
from wsproto.events import (
AcceptConnection,
BytesMessage,
CloseConnection,
Ping,
TextMessage,
)
from wsproto.events import Request as WSRequest
from wsproto.frame_protocol import CloseReason from wsproto.frame_protocol import CloseReason
from wsproto.events import Request as WSRequest
from wsproto import WSConnection, ConnectionType
from wsproto.events import TextMessage, BytesMessage, CloseConnection, AcceptConnection
K = TypeVar("K") K = TypeVar("K")
V = TypeVar("V") V = TypeVar("V")

View File

@@ -1,7 +1,7 @@
from nonebot import on_message from nonebot import on_message
from nonebot.adapters import Event, Message
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.params import ArgStr, EventMessage, LastReceived, Received from nonebot.adapters import Event, Message
from nonebot.params import ArgStr, Received, EventMessage, LastReceived
test_handle = on_message() test_handle = on_message()

View File

@@ -1,7 +1,7 @@
from typing import Annotated, Any from typing import Annotated
from nonebot.adapters import Message from nonebot.adapters import Message
from nonebot.params import Arg, ArgPlainText, ArgPromptResult, ArgStr from nonebot.params import Arg, ArgStr, ArgPlainText
async def arg(key: Message = Arg()) -> Message: async def arg(key: Message = Arg()) -> Message:
@@ -28,16 +28,12 @@ async def annotated_arg_plain_text(key: Annotated[str, ArgPlainText()]) -> str:
return key return key
async def annotated_arg_prompt_result(key: Annotated[Any, ArgPromptResult()]) -> Any:
return key
# test dependency priority # test dependency priority
async def annotated_prior_arg(key: Annotated[str, ArgStr("foo")] = ArgPlainText()): async def annotated_prior_arg(key: Annotated[str, ArgStr("foo")] = ArgPlainText()):
return key return key
async def annotated_multi_arg( async def annotated_multi_arg(
key: Annotated[Annotated[str, ArgStr("foo")], ArgPlainText()], key: Annotated[Annotated[str, ArgStr("foo")], ArgPlainText()]
): ):
return key return key

View File

@@ -1,4 +1,4 @@
from typing import TypeVar, Union from typing import Union, TypeVar
from nonebot.adapters import Bot from nonebot.adapters import Bot

View File

@@ -1,7 +1,6 @@
from dataclasses import dataclass
from typing import Annotated from typing import Annotated
from dataclasses import dataclass
import anyio
from pydantic import Field from pydantic import Field
from nonebot import on_message from nonebot import on_message
@@ -74,13 +73,13 @@ async def annotated_class_depend(c: Annotated[ClassDependency, Depends()]):
# test dependency priority # test dependency priority
async def annotated_prior_depend( async def annotated_prior_depend(
x: Annotated[int, Depends(lambda: 2)] = Depends(dependency), x: Annotated[int, Depends(lambda: 2)] = Depends(dependency)
): ):
return x return x
async def annotated_multi_depend( async def annotated_multi_depend(
x: Annotated[Annotated[int, Depends(lambda: 2)], Depends(dependency)], x: Annotated[Annotated[int, Depends(lambda: 2)], Depends(dependency)]
): ):
return x return x
@@ -106,26 +105,3 @@ async def validate_field(x: int = Depends(lambda: "1", validate=Field(gt=0))):
async def validate_field_fail(x: int = Depends(lambda: "0", validate=Field(gt=0))): async def validate_field_fail(x: int = Depends(lambda: "0", validate=Field(gt=0))):
return x return x
async def _dep():
await anyio.sleep(1)
return 1
def _dep_mismatch():
return 1
async def cache_exception_func1(
dep: int = Depends(_dep),
mismatch: dict = Depends(_dep_mismatch),
):
raise RuntimeError("Never reach here")
async def cache_exception_func2(
dep: int = Depends(_dep),
match: int = Depends(_dep_mismatch),
):
return dep

View File

@@ -1,7 +1,7 @@
from typing import TypeVar, Union from typing import Union, TypeVar
from nonebot.adapters import Event, Message from nonebot.adapters import Event, Message
from nonebot.params import EventMessage, EventPlainText, EventToMe, EventType from nonebot.params import EventToMe, EventType, EventMessage, EventPlainText
async def event(e: Event) -> Event: async def event(e: Event) -> Event:

View File

@@ -1,13 +1,8 @@
from typing import Any, TypeVar, Union from typing import Union, TypeVar
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.params import ( from nonebot.params import Received, LastReceived
LastReceived,
PausePromptResult,
Received,
ReceivePromptResult,
)
async def matcher(m: Matcher) -> Matcher: async def matcher(m: Matcher) -> Matcher:
@@ -64,11 +59,3 @@ async def receive(e: Event = Received("test")) -> Event:
async def last_receive(e: Event = LastReceived()) -> Event: async def last_receive(e: Event = LastReceived()) -> Event:
return e return e
async def receive_prompt_result(result: Any = ReceivePromptResult("test")) -> Any:
return result
async def pause_prompt_result(result: Any = PausePromptResult()) -> Any:
return result

View File

@@ -1,24 +1,24 @@
from re import Match from re import Match
from nonebot.typing import T_State
from nonebot.adapters import Message from nonebot.adapters import Message
from nonebot.params import ( from nonebot.params import (
Command, Command,
CommandArg,
CommandStart,
CommandWhitespace,
Endswith,
Fullmatch,
Keyword, Keyword,
RawCommand, Endswith,
RegexDict,
RegexGroup,
RegexMatched,
RegexStr, RegexStr,
Fullmatch,
RegexDict,
CommandArg,
RawCommand,
RegexGroup,
Startswith,
CommandStart,
RegexMatched,
ShellCommandArgs, ShellCommandArgs,
ShellCommandArgv, ShellCommandArgv,
Startswith, CommandWhitespace,
) )
from nonebot.typing import T_State
async def state(x: T_State) -> T_State: async def state(x: T_State) -> T_State:

View File

@@ -1,9 +1,9 @@
from typing import Optional from typing import Optional
from nonebot.adapters import Bot, Event, Message from nonebot.typing import T_State
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.params import Arg, Depends from nonebot.params import Arg, Depends
from nonebot.typing import T_State from nonebot.adapters import Bot, Event, Message
def dependency(): def dependency():

View File

@@ -1,24 +1,24 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from nonebot.adapters import Event
from nonebot.matcher import Matcher
from nonebot import ( from nonebot import (
CommandGroup, CommandGroup,
MatcherGroup, MatcherGroup,
on, on,
on_type,
on_regex,
on_notice,
on_command, on_command,
on_endswith,
on_fullmatch,
on_keyword, on_keyword,
on_message, on_message,
on_metaevent,
on_notice,
on_regex,
on_request, on_request,
on_shell_command, on_endswith,
on_fullmatch,
on_metaevent,
on_startswith, on_startswith,
on_type, on_shell_command,
) )
from nonebot.adapters import Event
from nonebot.matcher import Matcher
async def rule() -> bool: async def rule() -> bool:

View File

@@ -1,5 +0,0 @@
[tool.ruff]
extend = "../pyproject.toml"
[tool.ruff.lint.isort]
known-first-party = ["nonebot", "fake_server", "utils"]

View File

@@ -1,23 +1,23 @@
from contextlib import asynccontextmanager
from typing import Optional from typing import Optional
from contextlib import asynccontextmanager
from nonebug import App
import pytest import pytest
from nonebug import App
from utils import FakeAdapter
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.drivers import ( from nonebot.drivers import (
URL, URL,
Driver, Driver,
HTTPServerSetup,
Request, Request,
Response, Response,
WebSocket, WebSocket,
HTTPServerSetup,
WebSocketServerSetup, WebSocketServerSetup,
) )
from utils import FakeAdapter
@pytest.mark.anyio @pytest.mark.asyncio
async def test_adapter_connect(app: App, driver: Driver): async def test_adapter_connect(app: App, driver: Driver):
last_connect_bot: Optional[Bot] = None last_connect_bot: Optional[Bot] = None
last_disconnect_bot: Optional[Bot] = None last_disconnect_bot: Optional[Bot] = None
@@ -45,6 +45,7 @@ async def test_adapter_connect(app: App, driver: Driver):
assert bot.self_id not in adapter.bots assert bot.self_id not in adapter.bots
@pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"driver", "driver",
[ [
@@ -74,7 +75,7 @@ async def test_adapter_connect(app: App, driver: Driver):
], ],
indirect=True, indirect=True,
) )
def test_adapter_server(driver: Driver): async def test_adapter_server(driver: Driver):
last_http_setup: Optional[HTTPServerSetup] = None last_http_setup: Optional[HTTPServerSetup] = None
last_ws_setup: Optional[WebSocketServerSetup] = None last_ws_setup: Optional[WebSocketServerSetup] = None
@@ -111,7 +112,7 @@ def test_adapter_server(driver: Driver):
assert last_ws_setup is setup assert last_ws_setup is setup
@pytest.mark.anyio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"driver", "driver",
[ [
@@ -158,7 +159,7 @@ async def test_adapter_http_client(driver: Driver):
assert last_request is request assert last_request is request
@pytest.mark.anyio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"driver", "driver",
[ [

View File

@@ -1,14 +1,13 @@
from typing import Any, Optional from typing import Any, Optional
import anyio
from nonebug import App
import pytest import pytest
from nonebug import App
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.exception import MockApiException from nonebot.exception import MockApiException
@pytest.mark.anyio @pytest.mark.asyncio
async def test_bot_call_api(app: App): async def test_bot_call_api(app: App):
async with app.test_api() as ctx: async with app.test_api() as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
@@ -24,7 +23,7 @@ async def test_bot_call_api(app: App):
await bot.call_api("test") await bot.call_api("test")
@pytest.mark.anyio @pytest.mark.asyncio
async def test_bot_calling_api_hook_simple(app: App): async def test_bot_calling_api_hook_simple(app: App):
runned: bool = False runned: bool = False
@@ -50,7 +49,7 @@ async def test_bot_calling_api_hook_simple(app: App):
assert result is True assert result is True
@pytest.mark.anyio @pytest.mark.asyncio
async def test_bot_calling_api_hook_mock(app: App): async def test_bot_calling_api_hook_mock(app: App):
runned: bool = False runned: bool = False
@@ -77,47 +76,7 @@ async def test_bot_calling_api_hook_mock(app: App):
assert result is False assert result is False
@pytest.mark.anyio @pytest.mark.asyncio
async def test_bot_calling_api_hook_multi_mock(app: App):
runned1: bool = False
runned2: bool = False
event = anyio.Event()
async def calling_api_hook1(bot: Bot, api: str, data: dict[str, Any]):
nonlocal runned1
runned1 = True
event.set()
raise MockApiException(1)
async def calling_api_hook2(bot: Bot, api: str, data: dict[str, Any]):
nonlocal runned2
runned2 = True
with anyio.fail_after(1):
await event.wait()
raise MockApiException(2)
hooks = set()
with pytest.MonkeyPatch.context() as m:
m.setattr(Bot, "_calling_api_hook", hooks)
Bot.on_calling_api(calling_api_hook1)
Bot.on_calling_api(calling_api_hook2)
assert hooks == {calling_api_hook1, calling_api_hook2}
async with app.test_api() as ctx:
bot = ctx.create_bot()
result = await bot.call_api("test")
assert runned1 is True
assert runned2 is True
assert result == 1
@pytest.mark.anyio
async def test_bot_called_api_hook_simple(app: App): async def test_bot_called_api_hook_simple(app: App):
runned: bool = False runned: bool = False
@@ -149,7 +108,7 @@ async def test_bot_called_api_hook_simple(app: App):
assert result is True assert result is True
@pytest.mark.anyio @pytest.mark.asyncio
async def test_bot_called_api_hook_mock(app: App): async def test_bot_called_api_hook_mock(app: App):
runned: bool = False runned: bool = False
@@ -191,56 +150,3 @@ async def test_bot_called_api_hook_mock(app: App):
assert runned is True assert runned is True
assert result is False assert result is False
@pytest.mark.anyio
async def test_bot_called_api_hook_multi_mock(app: App):
runned1: bool = False
runned2: bool = False
event = anyio.Event()
async def called_api_hook1(
bot: Bot,
exception: Optional[Exception],
api: str,
data: dict[str, Any],
result: Any,
):
nonlocal runned1
runned1 = True
event.set()
raise MockApiException(1)
async def called_api_hook2(
bot: Bot,
exception: Optional[Exception],
api: str,
data: dict[str, Any],
result: Any,
):
nonlocal runned2
runned2 = True
with anyio.fail_after(1):
await event.wait()
raise MockApiException(2)
hooks = set()
with pytest.MonkeyPatch.context() as m:
m.setattr(Bot, "_called_api_hook", hooks)
Bot.on_called_api(called_api_hook1)
Bot.on_called_api(called_api_hook2)
assert hooks == {called_api_hook1, called_api_hook2}
async with app.test_api() as ctx:
bot = ctx.create_bot()
ctx.should_call_api("test", {}, True)
result = await bot.call_api("test")
assert runned1 is True
assert runned2 is True
assert result == 1

View File

@@ -1,9 +1,9 @@
from pydantic import ValidationError
import pytest import pytest
from pydantic import ValidationError
from nonebot.adapters import Message, MessageSegment
from nonebot.compat import type_validate_python from nonebot.compat import type_validate_python
from utils import FakeMessage, FakeMessageSegment from utils import FakeMessage, FakeMessageSegment
from nonebot.adapters import Message, MessageSegment
def test_segment_data(): def test_segment_data():

View File

@@ -1,31 +1,31 @@
import sys import sys
from typing import Optional from typing import Optional
from nonebug import App
import pytest import pytest
from nonebug import App
from nonebot import on_message from nonebot import on_message
from nonebot.adapters import Bot, Event
from nonebot.exception import IgnoredException
from nonebot.log import default_filter, default_format, logger
from nonebot.matcher import Matcher
import nonebot.message as message import nonebot.message as message
from nonebot.message import ( from utils import make_fake_event
event_postprocessor,
event_preprocessor,
run_postprocessor,
run_preprocessor,
)
from nonebot.params import Depends from nonebot.params import Depends
from nonebot.typing import T_State from nonebot.typing import T_State
from utils import make_fake_event from nonebot.matcher import Matcher
from nonebot.adapters import Bot, Event
from nonebot.exception import IgnoredException
from nonebot.log import logger, default_filter, default_format
from nonebot.message import (
run_preprocessor,
run_postprocessor,
event_preprocessor,
event_postprocessor,
)
async def _dependency() -> int: async def _dependency() -> int:
return 1 return 1
@pytest.mark.anyio @pytest.mark.asyncio
async def test_event_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch): async def test_event_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m: with monkeypatch.context() as m:
m.setattr(message, "_event_preprocessors", set()) m.setattr(message, "_event_preprocessors", set())
@@ -58,7 +58,7 @@ async def test_event_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
assert runned, "event_preprocessor should runned" assert runned, "event_preprocessor should runned"
@pytest.mark.anyio @pytest.mark.asyncio
async def test_event_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch): async def test_event_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m: with monkeypatch.context() as m:
m.setattr(message, "_event_preprocessors", set()) m.setattr(message, "_event_preprocessors", set())
@@ -88,7 +88,7 @@ async def test_event_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPat
assert not runned, "matcher should not runned" assert not runned, "matcher should not runned"
@pytest.mark.anyio @pytest.mark.asyncio
async def test_event_preprocessor_exception( async def test_event_preprocessor_exception(
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
): ):
@@ -132,7 +132,7 @@ async def test_event_preprocessor_exception(
assert "RuntimeError: test" in capsys.readouterr().out assert "RuntimeError: test" in capsys.readouterr().out
@pytest.mark.anyio @pytest.mark.asyncio
async def test_event_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch): async def test_event_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m: with monkeypatch.context() as m:
m.setattr(message, "_event_postprocessors", set()) m.setattr(message, "_event_postprocessors", set())
@@ -165,7 +165,7 @@ async def test_event_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
assert runned, "event_postprocessor should runned" assert runned, "event_postprocessor should runned"
@pytest.mark.anyio @pytest.mark.asyncio
async def test_event_postprocessor_exception( async def test_event_postprocessor_exception(
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
): ):
@@ -202,7 +202,7 @@ async def test_event_postprocessor_exception(
assert "RuntimeError: test" in capsys.readouterr().out assert "RuntimeError: test" in capsys.readouterr().out
@pytest.mark.anyio @pytest.mark.asyncio
async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch): async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m: with monkeypatch.context() as m:
m.setattr(message, "_run_preprocessors", set()) m.setattr(message, "_run_preprocessors", set())
@@ -239,7 +239,7 @@ async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
assert runned, "run_preprocessor should runned" assert runned, "run_preprocessor should runned"
@pytest.mark.anyio @pytest.mark.asyncio
async def test_run_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch): async def test_run_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m: with monkeypatch.context() as m:
m.setattr(message, "_run_preprocessors", set()) m.setattr(message, "_run_preprocessors", set())
@@ -269,7 +269,7 @@ async def test_run_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch
assert not runned, "matcher should not runned" assert not runned, "matcher should not runned"
@pytest.mark.anyio @pytest.mark.asyncio
async def test_run_preprocessor_exception( async def test_run_preprocessor_exception(
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
): ):
@@ -313,7 +313,7 @@ async def test_run_preprocessor_exception(
assert "RuntimeError: test" in capsys.readouterr().out assert "RuntimeError: test" in capsys.readouterr().out
@pytest.mark.anyio @pytest.mark.asyncio
async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch): async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m: with monkeypatch.context() as m:
m.setattr(message, "_run_postprocessors", set()) m.setattr(message, "_run_postprocessors", set())
@@ -351,7 +351,7 @@ async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
assert runned, "run_postprocessor should runned" assert runned, "run_postprocessor should runned"
@pytest.mark.anyio @pytest.mark.asyncio
async def test_run_postprocessor_exception( async def test_run_postprocessor_exception(
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
): ):

View File

@@ -1,28 +1,30 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Annotated, Any, Optional from typing import Any, Optional, Annotated
from pydantic import BaseModel, ValidationError
import pytest import pytest
from pydantic import BaseModel, ValidationError
from nonebot.compat import ( from nonebot.compat import (
DEFAULT_CONFIG, DEFAULT_CONFIG,
FieldInfo,
PydanticUndefined,
Required, Required,
FieldInfo,
TypeAdapter, TypeAdapter,
custom_validation, PydanticUndefined,
model_dump, model_dump,
custom_validation,
type_validate_json, type_validate_json,
type_validate_python, type_validate_python,
) )
def test_default_config(): @pytest.mark.asyncio
async def test_default_config():
assert DEFAULT_CONFIG.get("extra") == "allow" assert DEFAULT_CONFIG.get("extra") == "allow"
assert DEFAULT_CONFIG.get("arbitrary_types_allowed") is True assert DEFAULT_CONFIG.get("arbitrary_types_allowed") is True
def test_field_info(): @pytest.mark.asyncio
async def test_field_info():
# required should be convert to PydanticUndefined # required should be convert to PydanticUndefined
assert FieldInfo(Required).default is PydanticUndefined assert FieldInfo(Required).default is PydanticUndefined
@@ -30,7 +32,8 @@ def test_field_info():
assert FieldInfo(test="test").extra["test"] == "test" assert FieldInfo(test="test").extra["test"] == "test"
def test_type_adapter(): @pytest.mark.asyncio
async def test_type_adapter():
t = TypeAdapter(Annotated[int, FieldInfo(ge=1)]) t = TypeAdapter(Annotated[int, FieldInfo(ge=1)])
assert t.validate_python(2) == 2 assert t.validate_python(2) == 2
@@ -44,7 +47,8 @@ def test_type_adapter():
t.validate_json("0") t.validate_json("0")
def test_model_dump(): @pytest.mark.asyncio
async def test_model_dump():
class TestModel(BaseModel): class TestModel(BaseModel):
test1: int test1: int
test2: int test2: int
@@ -53,7 +57,8 @@ def test_model_dump():
assert model_dump(TestModel(test1=1, test2=2), exclude={"test1"}) == {"test2": 2} assert model_dump(TestModel(test1=1, test2=2), exclude={"test1"}) == {"test2": 2}
def test_custom_validation(): @pytest.mark.asyncio
async def test_custom_validation():
called = [] called = []
@custom_validation @custom_validation
@@ -80,7 +85,8 @@ def test_custom_validation():
assert called == [1, 2] assert called == [1, 2]
def test_validate_json(): @pytest.mark.asyncio
async def test_validate_json():
class TestModel(BaseModel): class TestModel(BaseModel):
test1: int test1: int
test2: str test2: str

View File

@@ -1,10 +1,10 @@
from typing import TYPE_CHECKING, Optional, Union from typing import TYPE_CHECKING, Union, Optional
from pydantic import BaseModel, Field
import pytest import pytest
from pydantic import Field, BaseModel
from nonebot.compat import PYDANTIC_V2 from nonebot.compat import PYDANTIC_V2
from nonebot.config import DOTENV_TYPE, BaseSettings, SettingsConfig, SettingsError from nonebot.config import DOTENV_TYPE, BaseSettings, SettingsError, SettingsConfig
class Simple(BaseModel): class Simple(BaseModel):
@@ -50,14 +50,16 @@ class ExampleWithoutDelimiter(Example):
env_nested_delimiter = None env_nested_delimiter = None
def test_config_no_env(): @pytest.mark.asyncio
async def test_config_no_env():
config = Example(_env_file=None) config = Example(_env_file=None)
assert config.simple == "" assert config.simple == ""
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
config.common_config config.common_config
def test_config_with_env(): @pytest.mark.asyncio
async def test_config_with_env():
config = Example(_env_file=(".env", ".env.example")) config = Example(_env_file=(".env", ".env.example"))
assert config.simple == "simple" assert config.simple == "simple"
@@ -100,7 +102,8 @@ def test_config_with_env():
config.other_nested_inner__b config.other_nested_inner__b
def test_config_error_env(): @pytest.mark.asyncio
async def test_config_error_env():
with pytest.MonkeyPatch().context() as m: with pytest.MonkeyPatch().context() as m:
m.setenv("COMPLEX", "not json") m.setenv("COMPLEX", "not json")
@@ -108,7 +111,8 @@ def test_config_error_env():
Example(_env_file=(".env", ".env.example")) Example(_env_file=(".env", ".env.example"))
def test_config_without_delimiter(): @pytest.mark.asyncio
async def test_config_without_delimiter():
config = ExampleWithoutDelimiter() config = ExampleWithoutDelimiter()
assert config.nested.a == 1 assert config.nested.a == 1
assert config.nested.b == 0 assert config.nested.b == 0

View File

@@ -1,31 +1,31 @@
from http.cookies import SimpleCookie
import json import json
import asyncio
from typing import Any, Optional from typing import Any, Optional
from http.cookies import SimpleCookie
import anyio
from nonebug import App
import pytest import pytest
from nonebug import App
from utils import FakeAdapter
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.params import Depends
from nonebot.dependencies import Dependent from nonebot.dependencies import Dependent
from nonebot.exception import WebSocketClosed
from nonebot.drivers import ( from nonebot.drivers import (
URL, URL,
ASGIMixin,
Driver, Driver,
HTTPClientMixin,
HTTPServerSetup,
Request, Request,
Response, Response,
ASGIMixin,
WebSocket, WebSocket,
HTTPClientMixin,
HTTPServerSetup,
WebSocketClientMixin, WebSocketClientMixin,
WebSocketServerSetup, WebSocketServerSetup,
) )
from nonebot.exception import WebSocketClosed
from nonebot.params import Depends
from utils import FakeAdapter
@pytest.mark.anyio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"driver", [pytest.param("nonebot.drivers.none:Driver", id="none")], indirect=True "driver", [pytest.param("nonebot.drivers.none:Driver", id="none")], indirect=True
) )
@@ -59,22 +59,22 @@ async def test_lifespan(driver: Driver):
@driver.on_shutdown @driver.on_shutdown
async def _shutdown1(): async def _shutdown1():
assert shutdown_log == [2] assert shutdown_log == []
shutdown_log.append(1) shutdown_log.append(1)
@driver.on_shutdown @driver.on_shutdown
async def _shutdown2(): async def _shutdown2():
assert shutdown_log == [] assert shutdown_log == [1]
shutdown_log.append(2) shutdown_log.append(2)
async with driver._lifespan: async with driver._lifespan:
assert start_log == [1, 2] assert start_log == [1, 2]
assert ready_log == [1, 2] assert ready_log == [1, 2]
assert shutdown_log == [2, 1] assert shutdown_log == [1, 2]
@pytest.mark.anyio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"driver", "driver",
[ [
@@ -99,10 +99,10 @@ async def test_http_server(app: App, driver: Driver):
assert response.status_code == 200 assert response.status_code == 200
assert response.text == "test" assert response.text == "test"
await anyio.sleep(1) await asyncio.sleep(1)
@pytest.mark.anyio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"driver", "driver",
[ [
@@ -155,10 +155,10 @@ async def test_websocket_server(app: App, driver: Driver):
await ws.close(code=1000) await ws.close(code=1000)
await anyio.sleep(1) await asyncio.sleep(1)
@pytest.mark.anyio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"driver", "driver",
[ [
@@ -171,10 +171,9 @@ async def test_cross_context(app: App, driver: Driver):
assert isinstance(driver, ASGIMixin) assert isinstance(driver, ASGIMixin)
ws: Optional[WebSocket] = None ws: Optional[WebSocket] = None
ws_ready = anyio.Event() ws_ready = asyncio.Event()
ws_should_close = anyio.Event() ws_should_close = asyncio.Event()
# create a background task before the ws connection established
async def background_task(): async def background_task():
try: try:
await ws_ready.wait() await ws_ready.wait()
@@ -186,6 +185,8 @@ async def test_cross_context(app: App, driver: Driver):
finally: finally:
ws_should_close.set() ws_should_close.set()
task = asyncio.create_task(background_task())
async def _handle_ws(websocket: WebSocket) -> None: async def _handle_ws(websocket: WebSocket) -> None:
nonlocal ws nonlocal ws
await websocket.accept() await websocket.accept()
@@ -198,9 +199,7 @@ async def test_cross_context(app: App, driver: Driver):
ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws) ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws)
driver.setup_websocket_server(ws_setup) driver.setup_websocket_server(ws_setup)
async with anyio.create_task_group() as tg, app.test_server(driver.asgi) as ctx: async with app.test_server(driver.asgi) as ctx:
tg.start_soon(background_task)
client = ctx.get_client() client = ctx.get_client()
async with client.websocket_connect("/ws_test") as websocket: async with client.websocket_connect("/ws_test") as websocket:
@@ -212,10 +211,11 @@ async def test_cross_context(app: App, driver: Driver):
if not e.args or "websocket.close" not in str(e.args[0]): if not e.args or "websocket.close" not in str(e.args[0]):
raise raise
await anyio.sleep(1) await task
await asyncio.sleep(1)
@pytest.mark.anyio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"driver", "driver",
[ [
@@ -304,10 +304,10 @@ async def test_http_client(driver: Driver, server_url: URL):
"test3": "test", "test3": "test",
}, "file parsing error" }, "file parsing error"
await anyio.sleep(1) await asyncio.sleep(1)
@pytest.mark.anyio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"driver", "driver",
[ [
@@ -419,10 +419,10 @@ async def test_http_client_session(driver: Driver, server_url: URL):
"test3": "test", "test3": "test",
}, "file parsing error" }, "file parsing error"
await anyio.sleep(1) await asyncio.sleep(1)
@pytest.mark.anyio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"driver", "driver",
[ [
@@ -452,9 +452,10 @@ async def test_websocket_client(driver: Driver, server_url: URL):
with pytest.raises(WebSocketClosed, match=r"code=1000"): with pytest.raises(WebSocketClosed, match=r"code=1000"):
await ws.receive() await ws.receive()
await anyio.sleep(1) await asyncio.sleep(1)
@pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
("driver", "driver_type"), ("driver", "driver_type"),
[ [
@@ -471,11 +472,11 @@ async def test_websocket_client(driver: Driver, server_url: URL):
], ],
indirect=["driver"], indirect=["driver"],
) )
def test_combine_driver(driver: Driver, driver_type: str): async def test_combine_driver(driver: Driver, driver_type: str):
assert driver.type == driver_type assert driver.type == driver_type
@pytest.mark.anyio @pytest.mark.asyncio
async def test_bot_connect_hook(app: App, driver: Driver): async def test_bot_connect_hook(app: App, driver: Driver):
with pytest.MonkeyPatch.context() as m: with pytest.MonkeyPatch.context() as m:
conn_hooks: set[Dependent[Any]] = set() conn_hooks: set[Dependent[Any]] = set()
@@ -532,7 +533,7 @@ async def test_bot_connect_hook(app: App, driver: Driver):
async with app.test_api() as ctx: async with app.test_api() as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
await anyio.sleep(1) await asyncio.sleep(1)
if not conn_should_be_called: if not conn_should_be_called:
pytest.fail("on_bot_connect hook not called") pytest.fail("on_bot_connect hook not called")

View File

@@ -1,10 +1,10 @@
from nonebug import App
import pytest import pytest
from nonebug import App
from utils import FakeMessage, FakeMessageSegment, make_fake_event from utils import FakeMessage, FakeMessageSegment, make_fake_event
@pytest.mark.anyio @pytest.mark.asyncio
async def test_echo(app: App): async def test_echo(app: App):
from nonebot.plugins.echo import echo from nonebot.plugins.echo import echo

View File

@@ -1,20 +1,21 @@
from nonebug import App
import pytest import pytest
from nonebug import App
import nonebot import nonebot
from nonebot.drivers import Driver, ASGIMixin, ReverseDriver
from nonebot import ( from nonebot import (
get_adapter,
get_adapters,
get_app, get_app,
get_asgi,
get_bot, get_bot,
get_asgi,
get_bots, get_bots,
get_driver, get_driver,
get_adapter,
get_adapters,
) )
from nonebot.drivers import ASGIMixin, Driver, ReverseDriver
def test_init(): @pytest.mark.asyncio
async def test_init():
env = nonebot.get_driver().env env = nonebot.get_driver().env
assert env == "test" assert env == "test"
@@ -34,28 +35,31 @@ def test_init():
assert config.not_nested == "some string" assert config.not_nested == "some string"
def test_get_driver(monkeypatch: pytest.MonkeyPatch): @pytest.mark.asyncio
async def test_get_driver(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m: with monkeypatch.context() as m:
m.setattr(nonebot, "_driver", None) m.setattr(nonebot, "_driver", None)
with pytest.raises(ValueError, match="initialized"): with pytest.raises(ValueError, match="initialized"):
get_driver() get_driver()
def test_get_asgi(): @pytest.mark.asyncio
async def test_get_asgi(app: App, monkeypatch: pytest.MonkeyPatch):
driver = get_driver() driver = get_driver()
assert isinstance(driver, ReverseDriver) assert isinstance(driver, ReverseDriver)
assert isinstance(driver, ASGIMixin) assert isinstance(driver, ASGIMixin)
assert get_asgi() == driver.asgi assert get_asgi() == driver.asgi
def test_get_app(): @pytest.mark.asyncio
async def test_get_app(app: App, monkeypatch: pytest.MonkeyPatch):
driver = get_driver() driver = get_driver()
assert isinstance(driver, ReverseDriver) assert isinstance(driver, ReverseDriver)
assert isinstance(driver, ASGIMixin) assert isinstance(driver, ASGIMixin)
assert get_app() == driver.server_app assert get_app() == driver.server_app
@pytest.mark.anyio @pytest.mark.asyncio
async def test_get_adapter(app: App, monkeypatch: pytest.MonkeyPatch): async def test_get_adapter(app: App, monkeypatch: pytest.MonkeyPatch):
async with app.test_api() as ctx: async with app.test_api() as ctx:
adapter = ctx.create_adapter() adapter = ctx.create_adapter()
@@ -70,7 +74,8 @@ async def test_get_adapter(app: App, monkeypatch: pytest.MonkeyPatch):
get_adapter("not exist") get_adapter("not exist")
def test_run(monkeypatch: pytest.MonkeyPatch): @pytest.mark.asyncio
async def test_run(app: App, monkeypatch: pytest.MonkeyPatch):
runned = False runned = False
def mock_run(*args, **kwargs): def mock_run(*args, **kwargs):
@@ -88,7 +93,8 @@ def test_run(monkeypatch: pytest.MonkeyPatch):
assert runned assert runned
def test_get_bot(app: App, monkeypatch: pytest.MonkeyPatch): @pytest.mark.asyncio
async def test_get_bot(app: App, monkeypatch: pytest.MonkeyPatch):
driver = get_driver() driver = get_driver()
with pytest.raises(ValueError, match="no bots"): with pytest.raises(ValueError, match="no bots"):

View File

@@ -1,18 +1,19 @@
from pathlib import Path
import sys import sys
from pathlib import Path
from nonebug import App
import pytest import pytest
from nonebug import App
from nonebot.rule import Rule
from nonebot import get_plugin from nonebot import get_plugin
from nonebot.matcher import Matcher, matchers from nonebot.matcher import Matcher, matchers
from nonebot.message import _check_matcher, check_and_run_matcher
from nonebot.permission import Permission, User
from nonebot.rule import Rule
from utils import FakeMessage, make_fake_event from utils import FakeMessage, make_fake_event
from nonebot.permission import User, Permission
from nonebot.message import _check_matcher, check_and_run_matcher
def test_matcher_info(app: App): @pytest.mark.asyncio
async def test_matcher_info(app: App):
from plugins.matcher.matcher_info import matcher from plugins.matcher.matcher_info import matcher
assert issubclass(matcher, Matcher) assert issubclass(matcher, Matcher)
@@ -42,7 +43,7 @@ def test_matcher_info(app: App):
assert matcher._source.lineno == 3 assert matcher._source.lineno == 3
@pytest.mark.anyio @pytest.mark.asyncio
async def test_matcher_check(app: App): async def test_matcher_check(app: App):
async def falsy(): async def falsy():
return False return False
@@ -86,7 +87,7 @@ async def test_matcher_check(app: App):
assert await _check_matcher(test_rule_error, bot, event, {}) is False assert await _check_matcher(test_rule_error, bot, event, {}) is False
@pytest.mark.anyio @pytest.mark.asyncio
async def test_matcher_handle(app: App): async def test_matcher_handle(app: App):
from plugins.matcher.matcher_process import test_handle from plugins.matcher.matcher_process import test_handle
@@ -101,7 +102,7 @@ async def test_matcher_handle(app: App):
ctx.should_finished() ctx.should_finished()
@pytest.mark.anyio @pytest.mark.asyncio
async def test_matcher_got(app: App): async def test_matcher_got(app: App):
from plugins.matcher.matcher_process import test_got from plugins.matcher.matcher_process import test_got
@@ -123,7 +124,7 @@ async def test_matcher_got(app: App):
ctx.receive_event(bot, event_next) ctx.receive_event(bot, event_next)
@pytest.mark.anyio @pytest.mark.asyncio
async def test_matcher_receive(app: App): async def test_matcher_receive(app: App):
from plugins.matcher.matcher_process import test_receive from plugins.matcher.matcher_process import test_receive
@@ -140,7 +141,7 @@ async def test_matcher_receive(app: App):
ctx.should_paused() ctx.should_paused()
@pytest.mark.anyio @pytest.mark.asyncio
async def test_matcher_combine(app: App): async def test_matcher_combine(app: App):
from plugins.matcher.matcher_process import test_combine from plugins.matcher.matcher_process import test_combine
@@ -163,7 +164,7 @@ async def test_matcher_combine(app: App):
ctx.receive_event(bot, event_next) ctx.receive_event(bot, event_next)
@pytest.mark.anyio @pytest.mark.asyncio
async def test_matcher_preset(app: App): async def test_matcher_preset(app: App):
from plugins.matcher.matcher_process import test_preset from plugins.matcher.matcher_process import test_preset
@@ -181,7 +182,7 @@ async def test_matcher_preset(app: App):
ctx.receive_event(bot, event_next) ctx.receive_event(bot, event_next)
@pytest.mark.anyio @pytest.mark.asyncio
async def test_matcher_overload(app: App): async def test_matcher_overload(app: App):
from plugins.matcher.matcher_process import test_overload from plugins.matcher.matcher_process import test_overload
@@ -195,7 +196,7 @@ async def test_matcher_overload(app: App):
ctx.should_finished() ctx.should_finished()
@pytest.mark.anyio @pytest.mark.asyncio
async def test_matcher_destroy(app: App): async def test_matcher_destroy(app: App):
from plugins.matcher.matcher_process import test_destroy from plugins.matcher.matcher_process import test_destroy
@@ -209,9 +210,9 @@ async def test_matcher_destroy(app: App):
assert len(matchers[test_destroy.priority]) == 0 assert len(matchers[test_destroy.priority]) == 0
@pytest.mark.anyio @pytest.mark.asyncio
async def test_type_updater(app: App): async def test_type_updater(app: App):
from plugins.matcher.matcher_type import test_custom_updater, test_type_updater from plugins.matcher.matcher_type import test_type_updater, test_custom_updater
event = make_fake_event()() event = make_fake_event()()
@@ -230,7 +231,7 @@ async def test_type_updater(app: App):
assert new_type == "custom" assert new_type == "custom"
@pytest.mark.anyio @pytest.mark.asyncio
async def test_default_permission_updater(app: App): async def test_default_permission_updater(app: App):
from plugins.matcher.matcher_permission import ( from plugins.matcher.matcher_permission import (
default_permission, default_permission,
@@ -251,7 +252,7 @@ async def test_default_permission_updater(app: App):
assert checker.perm is default_permission assert checker.perm is default_permission
@pytest.mark.anyio @pytest.mark.asyncio
async def test_user_permission_updater(app: App): async def test_user_permission_updater(app: App):
from plugins.matcher.matcher_permission import ( from plugins.matcher.matcher_permission import (
default_permission, default_permission,
@@ -273,11 +274,11 @@ async def test_user_permission_updater(app: App):
assert checker.perm is default_permission assert checker.perm is default_permission
@pytest.mark.anyio @pytest.mark.asyncio
async def test_custom_permission_updater(app: App): async def test_custom_permission_updater(app: App):
from plugins.matcher.matcher_permission import ( from plugins.matcher.matcher_permission import (
default_permission,
new_permission, new_permission,
default_permission,
test_custom_updater, test_custom_updater,
) )
@@ -290,7 +291,7 @@ async def test_custom_permission_updater(app: App):
assert new_perm is new_permission assert new_perm is new_permission
@pytest.mark.anyio @pytest.mark.asyncio
async def test_run(app: App): async def test_run(app: App):
with app.provider.context({}): with app.provider.context({}):
assert not matchers assert not matchers
@@ -321,12 +322,11 @@ async def test_run(app: App):
assert len(matchers[0][0].handlers) == 0 assert len(matchers[0][0].handlers) == 0
@pytest.mark.anyio @pytest.mark.asyncio
async def test_temp(app: App): async def test_temp(app: App):
from plugins.matcher.matcher_expire import test_temp_matcher from plugins.matcher.matcher_expire import test_temp_matcher
event = make_fake_event(_type="test")() event = make_fake_event(_type="test")()
with app.provider.context({test_temp_matcher.priority: [test_temp_matcher]}):
async with app.test_api() as ctx: async with app.test_api() as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
assert test_temp_matcher in matchers[test_temp_matcher.priority] assert test_temp_matcher in matchers[test_temp_matcher.priority]
@@ -334,33 +334,25 @@ async def test_temp(app: App):
assert test_temp_matcher not in matchers[test_temp_matcher.priority] assert test_temp_matcher not in matchers[test_temp_matcher.priority]
@pytest.mark.anyio @pytest.mark.asyncio
async def test_datetime_expire(app: App): async def test_datetime_expire(app: App):
from plugins.matcher.matcher_expire import test_datetime_matcher from plugins.matcher.matcher_expire import test_datetime_matcher
event = make_fake_event()() event = make_fake_event()()
with app.provider.context( async with app.test_api() as ctx:
{test_datetime_matcher.priority: [test_datetime_matcher]}
):
async with app.test_matcher(test_datetime_matcher) as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
assert test_datetime_matcher in matchers[test_datetime_matcher.priority] assert test_datetime_matcher in matchers[test_datetime_matcher.priority]
await check_and_run_matcher(test_datetime_matcher, bot, event, {}) await check_and_run_matcher(test_datetime_matcher, bot, event, {})
assert test_datetime_matcher not in matchers[test_datetime_matcher.priority] assert test_datetime_matcher not in matchers[test_datetime_matcher.priority]
@pytest.mark.anyio @pytest.mark.asyncio
async def test_timedelta_expire(app: App): async def test_timedelta_expire(app: App):
from plugins.matcher.matcher_expire import test_timedelta_matcher from plugins.matcher.matcher_expire import test_timedelta_matcher
event = make_fake_event()() event = make_fake_event()()
with app.provider.context(
{test_timedelta_matcher.priority: [test_timedelta_matcher]}
):
async with app.test_api() as ctx: async with app.test_api() as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
assert test_timedelta_matcher in matchers[test_timedelta_matcher.priority] assert test_timedelta_matcher in matchers[test_timedelta_matcher.priority]
await check_and_run_matcher(test_timedelta_matcher, bot, event, {}) await check_and_run_matcher(test_timedelta_matcher, bot, event, {})
assert ( assert test_timedelta_matcher not in matchers[test_timedelta_matcher.priority]
test_timedelta_matcher not in matchers[test_timedelta_matcher.priority]
)

View File

@@ -1,9 +1,11 @@
import pytest
from nonebug import App from nonebug import App
from nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers from nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers
def test_manager(app: App): @pytest.mark.asyncio
async def test_manager(app: App):
try: try:
default_provider = matchers.provider default_provider = matchers.provider
matchers.set_provider(DEFAULT_PROVIDER_CLASS) matchers.set_provider(DEFAULT_PROVIDER_CLASS)

View File

@@ -1,64 +1,58 @@
from contextlib import suppress
import re import re
from exceptiongroup import BaseExceptionGroup
from nonebug import App
import pytest import pytest
from nonebug import App
from nonebot.consts import (
ARG_KEY,
CMD_ARG_KEY,
CMD_KEY,
CMD_START_KEY,
CMD_WHITESPACE_KEY,
ENDSWITH_KEY,
FULLMATCH_KEY,
KEYWORD_KEY,
PREFIX_KEY,
RAW_CMD_KEY,
RECEIVE_KEY,
REGEX_MATCHED,
SHELL_ARGS,
SHELL_ARGV,
STARTSWITH_KEY,
)
from nonebot.dependencies import Dependent
from nonebot.exception import PausedException, RejectedException, TypeMisMatch
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.dependencies import Dependent
from nonebot.exception import TypeMisMatch
from utils import FakeMessage, make_fake_event
from nonebot.params import ( from nonebot.params import (
ArgParam, ArgParam,
BotParam, BotParam,
DefaultParam,
DependParam,
EventParam, EventParam,
ExceptionParam,
MatcherParam,
StateParam, StateParam,
DependParam,
DefaultParam,
MatcherParam,
ExceptionParam,
)
from nonebot.consts import (
CMD_KEY,
PREFIX_KEY,
SHELL_ARGS,
SHELL_ARGV,
CMD_ARG_KEY,
KEYWORD_KEY,
RAW_CMD_KEY,
ENDSWITH_KEY,
CMD_START_KEY,
FULLMATCH_KEY,
REGEX_MATCHED,
STARTSWITH_KEY,
CMD_WHITESPACE_KEY,
) )
from utils import FakeMessage, make_fake_event
UNKNOWN_PARAM = "Unknown parameter" UNKNOWN_PARAM = "Unknown parameter"
@pytest.mark.anyio @pytest.mark.asyncio
async def test_depend(app: App): async def test_depend(app: App):
from plugins.param.param_depend import ( from plugins.param.param_depend import (
ClassDependency, ClassDependency,
annotated_class_depend,
annotated_depend,
annotated_multi_depend,
annotated_prior_depend,
cache_exception_func1,
cache_exception_func2,
class_depend,
depends,
runned, runned,
sub_type_mismatch, depends,
test_depends,
validate, validate,
class_depend,
test_depends,
validate_fail, validate_fail,
validate_field, validate_field,
annotated_depend,
sub_type_mismatch,
validate_field_fail, validate_field_fail,
annotated_class_depend,
annotated_multi_depend,
annotated_prior_depend,
) )
async with app.test_dependent(depends, allow_types=[DependParam]) as ctx: async with app.test_dependent(depends, allow_types=[DependParam]) as ctx:
@@ -96,79 +90,48 @@ async def test_depend(app: App):
assert runned == [1, 1, 1] assert runned == [1, 1, 1]
runned.clear()
async with app.test_dependent( async with app.test_dependent(
annotated_class_depend, allow_types=[DependParam] annotated_class_depend, allow_types=[DependParam]
) as ctx: ) as ctx:
ctx.should_return(ClassDependency(x=1, y=2)) ctx.should_return(ClassDependency(x=1, y=2))
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: # noqa: PT012 with pytest.raises(TypeMisMatch): # noqa: PT012
async with app.test_dependent( async with app.test_dependent(
sub_type_mismatch, allow_types=[DependParam, BotParam] sub_type_mismatch, allow_types=[DependParam, BotParam]
) as ctx: ) as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
ctx.pass_params(bot=bot) ctx.pass_params(bot=bot)
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
async with app.test_dependent(validate, allow_types=[DependParam]) as ctx: async with app.test_dependent(validate, allow_types=[DependParam]) as ctx:
ctx.should_return(1) ctx.should_return(1)
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: with pytest.raises(TypeMisMatch):
async with app.test_dependent(validate_fail, allow_types=[DependParam]) as ctx: async with app.test_dependent(validate_fail, allow_types=[DependParam]) as ctx:
... ...
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
async with app.test_dependent(validate_field, allow_types=[DependParam]) as ctx: async with app.test_dependent(validate_field, allow_types=[DependParam]) as ctx:
ctx.should_return(1) ctx.should_return(1)
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: with pytest.raises(TypeMisMatch):
async with app.test_dependent( async with app.test_dependent(
validate_field_fail, allow_types=[DependParam] validate_field_fail, allow_types=[DependParam]
) as ctx: ) as ctx:
... ...
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
# test cache reuse when exception raised @pytest.mark.asyncio
dependency_cache = {}
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:
async with app.test_dependent(
cache_exception_func1, allow_types=[DependParam]
) as ctx:
ctx.pass_params(dependency_cache=dependency_cache)
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
# dependency solve tasks should be shielded even if one of them raises an exception
assert len(dependency_cache) == 2
async with app.test_dependent(
cache_exception_func2, allow_types=[DependParam]
) as ctx:
ctx.pass_params(dependency_cache=dependency_cache)
ctx.should_return(1)
@pytest.mark.anyio
async def test_bot(app: App): async def test_bot(app: App):
from plugins.param.param_bot import ( from plugins.param.param_bot import (
FooBot, FooBot,
generic_bot,
generic_bot_none,
get_bot, get_bot,
legacy_bot,
not_bot, not_bot,
not_legacy_bot,
postpone_bot,
sub_bot, sub_bot,
union_bot, union_bot,
legacy_bot,
generic_bot,
postpone_bot,
not_legacy_bot,
generic_bot_none,
) )
async with app.test_dependent(get_bot, allow_types=[BotParam]) as ctx: async with app.test_dependent(get_bot, allow_types=[BotParam]) as ctx:
@@ -194,14 +157,11 @@ async def test_bot(app: App):
ctx.pass_params(bot=bot) ctx.pass_params(bot=bot)
ctx.should_return(bot) ctx.should_return(bot)
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: # noqa: PT012 with pytest.raises(TypeMisMatch): # noqa: PT012
async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx: async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot() bot = ctx.create_bot()
ctx.pass_params(bot=bot) ctx.pass_params(bot=bot)
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
async with app.test_dependent(union_bot, allow_types=[BotParam]) as ctx: async with app.test_dependent(union_bot, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot(base=FooBot) bot = ctx.create_bot(base=FooBot)
ctx.pass_params(bot=bot) ctx.pass_params(bot=bot)
@@ -221,23 +181,23 @@ async def test_bot(app: App):
app.test_dependent(not_bot, allow_types=[BotParam]) app.test_dependent(not_bot, allow_types=[BotParam])
@pytest.mark.anyio @pytest.mark.asyncio
async def test_event(app: App): async def test_event(app: App):
from plugins.param.param_event import ( from plugins.param.param_event import (
FooEvent, FooEvent,
event, event,
event_message,
event_plain_text,
event_to_me,
event_type,
generic_event,
generic_event_none,
legacy_event,
not_event, not_event,
not_legacy_event,
postpone_event,
sub_event, sub_event,
event_type,
event_to_me,
union_event, union_event,
legacy_event,
event_message,
generic_event,
postpone_event,
event_plain_text,
not_legacy_event,
generic_event_none,
) )
fake_message = FakeMessage("text") fake_message = FakeMessage("text")
@@ -263,13 +223,10 @@ async def test_event(app: App):
ctx.pass_params(event=fake_fooevent) ctx.pass_params(event=fake_fooevent)
ctx.should_return(fake_fooevent) ctx.should_return(fake_fooevent)
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: with pytest.raises(TypeMisMatch):
async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx: async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_event) ctx.pass_params(event=fake_event)
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
async with app.test_dependent(union_event, allow_types=[EventParam]) as ctx: async with app.test_dependent(union_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_fooevent) ctx.pass_params(event=fake_fooevent)
ctx.should_return(fake_fooevent) ctx.should_return(fake_fooevent)
@@ -310,28 +267,28 @@ async def test_event(app: App):
ctx.should_return(fake_event.is_tome()) ctx.should_return(fake_event.is_tome())
@pytest.mark.anyio @pytest.mark.asyncio
async def test_state(app: App): async def test_state(app: App):
from plugins.param.param_state import ( from plugins.param.param_state import (
state,
command, command,
command_arg, keyword,
command_start,
command_whitespace,
endswith, endswith,
fullmatch, fullmatch,
keyword,
legacy_state,
not_legacy_state,
postpone_state,
raw_command,
regex_dict,
regex_group,
regex_matched,
regex_str, regex_str,
regex_dict,
startswith,
command_arg,
raw_command,
regex_group,
legacy_state,
command_start,
regex_matched,
postpone_state,
not_legacy_state,
command_whitespace,
shell_command_args, shell_command_args,
shell_command_argv, shell_command_argv,
startswith,
state,
) )
fake_message = FakeMessage("text") fake_message = FakeMessage("text")
@@ -461,23 +418,21 @@ async def test_state(app: App):
ctx.should_return(fake_state[KEYWORD_KEY]) ctx.should_return(fake_state[KEYWORD_KEY])
@pytest.mark.anyio @pytest.mark.asyncio
async def test_matcher(app: App): async def test_matcher(app: App):
from plugins.param.param_matcher import ( from plugins.param.param_matcher import (
FooMatcher, FooMatcher,
generic_matcher,
generic_matcher_none,
last_receive,
legacy_matcher,
matcher, matcher,
not_legacy_matcher,
not_matcher,
pause_prompt_result,
postpone_matcher,
receive, receive,
receive_prompt_result, not_matcher,
sub_matcher, sub_matcher,
last_receive,
union_matcher, union_matcher,
legacy_matcher,
generic_matcher,
postpone_matcher,
not_legacy_matcher,
generic_matcher_none,
) )
fake_matcher = Matcher() fake_matcher = Matcher()
@@ -502,13 +457,10 @@ async def test_matcher(app: App):
ctx.pass_params(matcher=foo_matcher) ctx.pass_params(matcher=foo_matcher)
ctx.should_return(foo_matcher) ctx.should_return(foo_matcher)
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: with pytest.raises(TypeMisMatch):
async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx: async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=fake_matcher) ctx.pass_params(matcher=fake_matcher)
if isinstance(exc_info.value, BaseExceptionGroup):
assert exc_info.group_contains(TypeMisMatch)
async with app.test_dependent(union_matcher, allow_types=[MatcherParam]) as ctx: async with app.test_dependent(union_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=foo_matcher) ctx.pass_params(matcher=foo_matcher)
ctx.should_return(foo_matcher) ctx.should_return(foo_matcher)
@@ -543,52 +495,21 @@ async def test_matcher(app: App):
ctx.pass_params(matcher=fake_matcher) ctx.pass_params(matcher=fake_matcher)
ctx.should_return(event_next) ctx.should_return(event_next)
fake_matcher.set_target(RECEIVE_KEY.format(id="test"), cache=False)
async with app.test_api() as ctx: @pytest.mark.asyncio
bot = ctx.create_bot()
ctx.should_call_send(event, "test", result=True, bot=bot)
with fake_matcher.ensure_context(bot, event):
with suppress(RejectedException):
await fake_matcher.reject("test")
async with app.test_dependent(
receive_prompt_result, allow_types=[MatcherParam, DependParam]
) as ctx:
ctx.pass_params(matcher=fake_matcher)
ctx.should_return(True)
async with app.test_api() as ctx:
bot = ctx.create_bot()
ctx.should_call_send(event, "test", result=False, bot=bot)
with fake_matcher.ensure_context(bot, event):
fake_matcher.set_target("test")
with suppress(PausedException):
await fake_matcher.pause("test")
async with app.test_dependent(
pause_prompt_result, allow_types=[MatcherParam, DependParam]
) as ctx:
ctx.pass_params(matcher=fake_matcher)
ctx.should_return(False)
@pytest.mark.anyio
async def test_arg(app: App): async def test_arg(app: App):
from plugins.param.param_arg import ( from plugins.param.param_arg import (
arg,
arg_str,
annotated_arg, annotated_arg,
annotated_arg_plain_text, arg_plain_text,
annotated_arg_prompt_result,
annotated_arg_str, annotated_arg_str,
annotated_multi_arg, annotated_multi_arg,
annotated_prior_arg, annotated_prior_arg,
arg, annotated_arg_plain_text,
arg_plain_text,
arg_str,
) )
matcher = Matcher() matcher = Matcher()
event = make_fake_event()()
message = FakeMessage("text") message = FakeMessage("text")
matcher.set_arg("key", message) matcher.set_arg("key", message)
@@ -618,21 +539,6 @@ async def test_arg(app: App):
ctx.pass_params(matcher=matcher) ctx.pass_params(matcher=matcher)
ctx.should_return(message.extract_plain_text()) ctx.should_return(message.extract_plain_text())
matcher.set_target(ARG_KEY.format(key="key"), cache=False)
async with app.test_api() as ctx:
bot = ctx.create_bot()
ctx.should_call_send(event, "test", result="arg", bot=bot)
with matcher.ensure_context(bot, event):
with suppress(RejectedException):
await matcher.reject("test")
async with app.test_dependent(
annotated_arg_prompt_result, allow_types=[ArgParam]
) as ctx:
ctx.pass_params(matcher=matcher)
ctx.should_return("arg")
async with app.test_dependent(annotated_multi_arg, allow_types=[ArgParam]) as ctx: async with app.test_dependent(annotated_multi_arg, allow_types=[ArgParam]) as ctx:
ctx.pass_params(matcher=matcher) ctx.pass_params(matcher=matcher)
ctx.should_return(message.extract_plain_text()) ctx.should_return(message.extract_plain_text())
@@ -642,7 +548,7 @@ async def test_arg(app: App):
ctx.should_return(message.extract_plain_text()) ctx.should_return(message.extract_plain_text())
@pytest.mark.anyio @pytest.mark.asyncio
async def test_exception(app: App): async def test_exception(app: App):
from plugins.param.param_exception import exc, legacy_exc from plugins.param.param_exception import exc, legacy_exc
@@ -656,7 +562,7 @@ async def test_exception(app: App):
ctx.should_return(exception) ctx.should_return(exception)
@pytest.mark.anyio @pytest.mark.asyncio
async def test_default(app: App): async def test_default(app: App):
from plugins.param.param_default import default from plugins.param.param_default import default
@@ -664,7 +570,8 @@ async def test_default(app: App):
ctx.should_return(1) ctx.should_return(1)
def test_priority(): @pytest.mark.asyncio
async def test_priority():
from plugins.param.priority import complex_priority from plugins.param.priority import complex_priority
dependent = Dependent[None].parse( dependent = Dependent[None].parse(

Some files were not shown because too many files have changed in this diff Show More