🔀 Merge pull request #790

Release: 2.0.0-beta.2
This commit is contained in:
Ju4tCode 2022-02-14 23:55:05 +08:00 committed by GitHub
commit 803223f31c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
213 changed files with 9246 additions and 14264 deletions

View File

@ -0,0 +1,11 @@
name: Build API Doc
description: Build API Doc
runs:
using: "composite"
steps:
- run: |
poetry run nb-autodoc nonebot
cp -r ./build/nonebot/* ./website/docs/api/
yarn prettier
shell: bash

21
.github/actions/setup-node/action.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: Setup Node
description: Setup Node
runs:
using: "composite"
steps:
- uses: actions/setup-node@v2
with:
node-version: "16"
- id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
shell: bash
- uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- run: yarn install
shell: bash

26
.github/actions/setup-python/action.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Setup Python
description: Setup Python
inputs:
python-version:
description: Python version
required: false
default: "3.9"
runs:
using: "composite"
steps:
- uses: actions/setup-python@v2
with:
python-version: ${{ inputs.python-version }}
architecture: "x64"
- uses: Gr1N/setup-poetry@v7
- uses: actions/cache@v2
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-${{ inputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
- run: poetry install -E all
shell: bash

View File

@ -1,42 +1,46 @@
name-template: 'Release v$RESOLVED_VERSION 🌈'
tag-template: 'v$RESOLVED_VERSION'
categories:
- title: '💥 Breaking Changes'
labels:
- 'Breaking'
- title: '🚀 Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🍻 Plugin Publish'
label: 'Plugin'
- title: '🍻 Bot Publish'
label: 'Bot'
- title: '🍻 Adapter Publish'
label: 'Adapter'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
header: |
### Documentation
See: https://v2.nonebot.dev
template: |
### 💫 Changes
$CHANGES
category-template: "### $TITLE"
name-template: "Release v$RESOLVED_VERSION 🌈"
tag-template: "v$RESOLVED_VERSION"
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
categories:
- title: "💥 Breaking Changes"
labels:
- "Breaking"
- title: "🚀 Features"
labels:
- "feature"
- "enhancement"
- title: "🐛 Bug Fixes"
labels:
- "fix"
- "bugfix"
- "bug"
- title: "📝 Documentation"
labels:
- "documentation"
- title: "🍻 Plugin Publish"
label: "Plugin"
- title: "🍻 Bot Publish"
label: "Bot"
- title: "🍻 Adapter Publish"
label: "Adapter"
version-resolver:
major:
labels:
- 'major'
- "major"
minor:
labels:
- 'minor'
- "minor"
patch:
labels:
- 'patch'
- "patch"
default: patch
template: |
## Documentation
See: https://v2.nonebot.dev
## 💫 Changes
$CHANGES

View File

@ -2,38 +2,40 @@ name: Code Coverage
on:
push:
branches:
- master
- dev
pull_request:
jobs:
test:
runs-on: ubuntu-latest
name: Run Pytest and Upload Coverage
name: Test Coverage
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: false
env:
OS: ${{ matrix.os }}
PYTHON_VERSION: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v2
- name: Set up Python environment
uses: actions/setup-python@v2
- name: Setup Python environment
uses: ./.github/actions/setup-python
with:
python-version: "3.9"
architecture: "x64"
- uses: Gr1N/setup-poetry@v7
- uses: actions/cache@v2
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
- name: Install dependencies
run: poetry install -E all
python-version: ${{ matrix.python-version }}
- name: Run Pytest
run: |
cd tests/
poetry run pytest --cov-report xml
poetry run pytest -n auto --cov-report xml
- name: Upload coverage report
uses: codecov/codecov-action@v2
with:
env_vars: OS,PYTHON_VERSION
files: ./tests/coverage.xml
flags: unittests

74
.github/workflows/release-drafter.yml vendored Normal file
View File

@ -0,0 +1,74 @@
name: Release Drafter
on:
push:
tags:
- v*
pull_request_target:
branches:
- master
types:
- closed
jobs:
update-release-draft:
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: release-drafter/release-drafter@v5
id: release-drafter
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# TODO
- name: Update Changelog
run: |
echo ${{ steps.release-drafter.outputs.body }}
release:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Python Environment
uses: ./.github/actions/setup-python
- name: Setup Node Environment
uses: ./.github/actions/setup-node
- name: Build API Doc
uses: ./.github/actions/build-api-doc
- run: |
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- uses: release-drafter/release-drafter@v5
with:
name: Release ${{ env.TAG_NAME }} 🌈
tag: ${{ env.TAG_NAME }}
publish: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Publish Package
run: |
poetry build
poetry publish -u ${{secrets.PYPI_USERNAME}} -p ${{secrets.PYPI_PASSWORD}}
gh release upload --clobber ${{ env.TAG_NAME }} dist/*.tar.gz dist/*.whl
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Publish Doc Package
run: |
yarn build:plugin --out-dir ../packages/nonebot-plugin-docs/nonebot_plugin_docs/dist
export NONEBOT_VERSION=`poetry version -s`
cd packages/nonebot-plugin-docs/
poetry version $NONEBOT_VERSION
poetry build
poetry publish -u ${{secrets.PYPI_USERNAME}} -p ${{secrets.PYPI_PASSWORD}}
gh release upload --clobber ${{ env.TAG_NAME }} dist/*.tar.gz dist/*.whl
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,36 +0,0 @@
name: Release Drafter
on:
push:
branches:
- master
tags:
- v*
jobs:
update-release-draft:
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: |
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- uses: release-drafter/release-drafter@v5
with:
name: Release ${{ env.TAG_NAME }} 🌈
tag: ${{ env.TAG_NAME }}
publish: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,58 +0,0 @@
name: Release Nonebot Plugin Docs
on:
push:
tags:
- v*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.9"
architecture: "x64"
- uses: Gr1N/setup-poetry@v7
- uses: actions/cache@v2
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: "16"
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache Packages
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- name: Install and build
run: |
poetry install -E all
poetry run sphinx-build -M markdown ./docs_build ./build
cp -r ./build/markdown/* ./website/docs/api/
yarn install
yarn prettier
yarn build:plugin --out-dir ../packages/nonebot-plugin-docs/nonebot_plugin_docs/dist
- name: Publish Package
run: |
export NONEBOT_VERSION=`poetry version -s`
cd packages/nonebot-plugin-docs/
poetry version $NONEBOT_VERSION
poetry build
poetry publish -u ${{secrets.PYPI_USERNAME}} -p ${{secrets.PYPI_PASSWORD}}

View File

@ -13,51 +13,23 @@ jobs:
ref: master
token: ${{ secrets.GH_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.9"
architecture: "x64"
- name: Setup Python Environment
uses: ./.github/actions/setup-python
- uses: Gr1N/setup-poetry@v7
- name: Setup Node Environment
uses: ./.github/actions/setup-node
- uses: actions/cache@v2
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: "16"
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache Packages
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- name: Set up dependencies
run: |
poetry install -E all
- name: Build Doc
run: poetry run sphinx-build -M markdown ./docs_build ./build
- name: Copy Files
run: cp -r ./build/markdown/* ./website/docs/api/
- name: Build API Doc
uses: ./.github/actions/build-api-doc
- name: Archive Files
run: |
yarn install
yarn prettier
yarn archive $(poetry version -s)
run: yarn archive $(poetry version -s)
- name: Push Tag and Release to PyPI
# TODO
- name: Archive Changelog
run: cat CHANGELOG.md
- name: Push Tag
run: |
git config user.name github-actions
git config user.email github-actions@github.com
@ -65,5 +37,3 @@ jobs:
git commit -m ":bookmark: Release $(poetry version -s)"
git tag v$(poetry version -s)
git push && git push --tags
poetry build
poetry publish -u ${{secrets.PYPI_USERNAME}} -p ${{secrets.PYPI_PASSWORD}}

View File

@ -1,7 +1,10 @@
name: Build Upload Site
name: Site Deploy
on:
push:
branches:
- master
- dev
pull_request_target:
jobs:
@ -17,42 +20,17 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
architecture: "x64"
- name: Setup Python Environment
uses: ./.github/actions/setup-python
- uses: Gr1N/setup-poetry@v7
- name: Setup Node Environment
uses: ./.github/actions/setup-node
- uses: actions/cache@v2
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
- name: Build API Doc
uses: ./.github/actions/build-api-doc
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: "16"
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache Packages
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- name: Install and build
run: |
poetry install -E all
poetry run sphinx-build -M markdown ./docs_build ./build
cp -r ./build/markdown/* ./website/docs/api/
yarn install
yarn prettier
yarn build
- name: Build Doc
run: yarn build
- name: Get Branch Name
run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV
@ -72,10 +50,10 @@ jobs:
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v1
with:
publish-dir: './website/build'
publish-dir: "./website/build"
production-deploy: ${{ env.PRODUCTION }}
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: 'Deploy ${{ env.DEPLOY_NAME }}@${{ github.sha }}'
deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.sha }}"
enable-commit-comment: false
alias: ${{ env.DEPLOY_NAME }}
env:

22
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,22 @@
ci:
autofix_commit_msg: ":rotating_light: auto fix by pre-commit hooks"
autofix_prs: true
autoupdate_branch: dev
autoupdate_schedule: weekly
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
repos:
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 22.1.0
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.5.1
hooks:
- id: prettier
types_or: [javascript, jsx, ts, tsx, markdown]

86
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,86 @@
# NoneBot2 贡献者公约
## 我们的承诺
身为项目成员、贡献者、负责人,我们保证参与此社区的每个人都不受骚扰,不论其年龄、体型、身体条件、民族、性征、性别认同与表现、经验水平、教育程度、社会地位、国籍、相貌、种族、宗教信仰及性取向如何。
我们承诺致力于建设开放、友善、多元、包容、健康的社区环境。
## 我们的准则
有助于促进本社区积极环境的行为包括但不限于:
- 与人为善、推己及人
- 尊重不同的主张、观点和经历
- 积极提出、耐心接受有益批评
- 面对过失,承担责任、认真道歉、从中学习
- 关注社区共同诉求,而非一己私利
不当行为包括但不限于:
- 发布与性有关的言论或图像,以及任何形式的献殷勤或勾引
- 挑衅行为、侮辱或贬损的言论、人身及政治攻击
- 公开或私下骚扰
- 未获明确授权擅自发布他人的资料,如地址、电子邮箱等
- 其他有理由认定为违反职业操守的不当行为
## 落实之义务
社区负责人有责任诠释什么是“妥当行为”,并据此准则,妥善公正地认定与处置不当、威胁、冒犯及有害的行为。
社区负责人有权利和义务删除、编辑、拒绝违背本公约的评论comment、提交commit、代码、维基wiki编辑、问题issue等贡献。如有必要需告知采取措施的理由。
## 适用范围
此行为标准适用于本社区全部场合,以及在其他场合代表本社区的个人。
代表本社区的情形包括但不限于:使用官方电子邮件与社交平台、作为指定代表参与在线或线下活动。
## 贯彻落实
如遇滥用、骚扰等不当行为,请通过 contact@nonebot.dev 向我们举报。我们将迅速审议并调查全部投诉。
社区全体负责人有义务保密举报者信息。
## 指导方针
社区负责人将依据下列方案判断并处置违纪行为:
### 一、督促
**社区影响**:用语不当、举止不符合道德或不受社区欢迎。
**处理意见**:由社区负责人予以非公开的书面警告,阐明违纪事由、解释举止如何不妥。或要求公开道歉。
### 二、警告
**社区影响**:一起或多起事件中的违纪行为。
**处理意见**:警告继续违纪的后果、违纪者在特定时间内禁止与当事人往来、不得擅自与社区执法者往来,禁令涵盖社区内外、社交网络在内的一切联络。如有违反,可致封禁乃至开除。
### 三、封禁
**社区影响**:严重违纪行为,包括屡教不改。
**处理意见**:违纪者在特定时间内禁止与社区的任何往来或公开联络,禁止任何与当事人公开或私下往来,不得擅自与社区管理者往来。如有违反,可导致开除。
### 四、开除
**社区影响**:典型违纪行为,例如屡教不改、骚扰某个人、敌对或贬低某个群体。
**处理意见**:无限期禁止违纪者与项目社区的一切公开往来。
## 来源
本行为标准改编自[参与者公约][homepage]2.0 版,可在此查阅:[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0]
指导方针借鉴自[Mozilla 纪检分级][mozilla coc]。
此行为标准常见问题请洽:[https://www.contributor-covenant.org/faq][faq]。
另有诸译本:[https://www.contributor-covenant.org/translations][translations]。
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[mozilla coc]: https://github.com/mozilla/diversity
[faq]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

71
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,71 @@
# NoneBot2 贡献指南
首先,感谢你愿意为 NoneBot2 贡献自己的一份力量!
本指南旨在引导你更规范地向 NoneBot2 提交贡献,请务必认真阅读。
## 提交 Issue
在提交 Issue 前,我们建议你先查看 [FAQ](https://github.com/nonebot/discussions/discussions/13) 与 [已有的 Issues](https://github.com/nonebot/nonebot2/issues),以防重复提交。
### 报告问题、故障与漏洞
NoneBot2 仍然是一个不够稳定的开发中项目,如果你在使用过程中发现问题并确信是由 NoneBot2 引起的,欢迎提交 Issue。
### 建议功能
NoneBot2 还未进入正式版,欢迎在 Issue 中提议要加入哪些新功能。
为了让开发者更好地理解你的意图,请认真描述你所需要的特性,可能的话可以提出你认为可行的解决方案。
## Pull Request
NoneBot 使用 [poetry](https://python-poetry.org/) 管理项目依赖,由于 pre-commit 也经其管理,所以在此一并说明。
下面的命令能在已安装 poetry 和 npm 的情况下帮你快速配置开发环境。
```sh
poetry install
pre-commit install
npm -g i gitmoji-cli
gitmoji -i
```
### Commit 规范
请确保你的每一个 commit 都能清晰地描述其意图,一个 commit 尽量只有一个意图。
NoneBot 的 commit message 格式遵循 [gitmoji](https://gitmoji.dev/) 规范,在创建 commit 时请牢记这一点。
### 撰写文档
NoneBot2 的文档使用 [docusaurus](https://docusaurus.io/),它有一些 [Markdown 特性](https://docusaurus.io/zh-CN/docs/markdown-features) 可能会帮助到你。
NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量写得简单易懂。
如果你需要在本地预览修改后的文档,可以使用 yarn 安装文档依赖后启动 dev server如下所示
```sh
yarn install
yarn start
```
### 参与开发
NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 与 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 规范,请确保你的代码风格和项目已有的代码保持一致,变量命名清晰,有适当的注释与测试代码。
## 为社区做贡献
你可以在 NoneBot 商店上架自己的适配器、插件、机器人,具体步骤可参考 [发布插件](https://v2.nonebot.dev/docs/advanced/publish-plugin) 一节。
我们仅对插件的兼容性进行简单测试,并会在下一个版本发布前对与该版本不兼容的插件作出处理。
虽然对插件的内容没有严格限制,但我们还是建议在上架插件之前先查看商店有无功能一致的插件。如果你想要上架商店的插件功能与现有插件不完全重合,请在插件说明中补充其与现有插件的区别。
同时,如果你参考或基于他人发行的代码进行开发,请注意遵守各代码所使用的开源许可协议。
## Git 工作流
`dev` 分支为 NoneBot 的开发分支,如无特殊情况请将更改提交到该分支。
如果你不是 NoneBot 团队的成员,可在 fork 本仓库后,向本仓库的 `dev` 分支发起 Pull Request注意遵循先前提到的 commit message 规范创建 commit。

View File

@ -23,7 +23,13 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
</a>
<img src="https://img.shields.io/badge/python-3.7.3+-blue" alt="python">
<a href="https://codecov.io/gh/nonebot/nonebot2">
<img src="https://codecov.io/gh/nonebot/nonebot2/branch/master/graph/badge.svg?token=2P0G0VS7N4"/>
<img src="https://codecov.io/gh/nonebot/nonebot2/branch/master/graph/badge.svg?token=2P0G0VS7N4" alt="codecov"/>
</a>
<a href="https://github.com/nonebot/nonebot2/actions/workflows/website-deploy.yml">
<img src="https://github.com/nonebot/nonebot2/actions/workflows/website-deploy.yml/badge.svg?branch=master&event=push" alt="site"/>
</a>
<a href="https://results.pre-commit.ci/latest/github/nonebot/nonebot2/master">
<img src="https://results.pre-commit.ci/badge/github/nonebot/nonebot2/master.svg" />
</a>
<br />
<a href="https://onebot.dev/">
@ -56,14 +62,18 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
<p align="center">
<a href="https://v2.nonebot.dev/">文档</a>
·
<a href="https://v2.nonebot.dev/guide/installation.html">安装</a>
<a href="https://v2.nonebot.dev/docs/start/installation">安装</a>
·
<a href="https://v2.nonebot.dev/guide/getting-started.html">开始使用</a>
<a href="https://v2.nonebot.dev/docs/tutorial/create-project">开始使用</a>
·
<a href="#插件">文档打不开?</a>
</p>
<!-- TODO: asciinema for install -->
<p align="center">
<a href="https://asciinema.org/a/464654">
<img src="https://v2.nonebot.dev/img/setup.svg">
</a>
</p>
## 简介
@ -152,20 +162,18 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
`NoneBot` 采用 `MIT` 许可证进行开源
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```text
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
## 贡献
如果你在使用过程中发现任何问题,可以 [提交 Issue](https://github.com/nonebot/nonebot2/issues/new) 或自行 Fork 修改后提交 Pull Request。
如果你要提交 Pull Request请确保你的代码风格和项目已有的代码保持一致遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 与 [PEP 484](https://www.python.org/dev/peps/pep-0484/),变量命名清晰,有适当的注释与测试代码,**并且请以 `dev` 分支作为 Pull Request 目标分支**。
<!--TODO: Add a CONTRIBUTING.md-->
请参考 [贡献指南](./CONTRIBUTING.md)
### 鸣谢

View File

@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@ -1,12 +0,0 @@
\-\-\-
sidebar_position: 1
id: index
slug: /api
\-\-\-
NoneBot 模块
===============
.. automodule:: nonebot
:members:
:show-inheritance:

View File

@ -1,43 +0,0 @@
\-\-\-
id: index
slug: /api/adapters/
\-\-\-
NoneBot.adapters 模块
=====================
.. automodule:: nonebot.adapters
:members:
:private-members:
:special-members: __init__
:show-inheritance:
.. automodule:: nonebot.adapters._adapter
:members:
:private-members:
:special-members: __init__
:show-inheritance:
.. automodule:: nonebot.adapters._bot
:members:
:private-members:
:special-members: __init__
:show-inheritance:
.. automodule:: nonebot.adapters._message
:members:
:private-members:
:special-members: __init__
:show-inheritance:
.. automodule:: nonebot.adapters._event
:members:
:private-members:
:special-members: __init__
:show-inheritance:
.. automodule:: nonebot.adapters._template
:members:
:private-members:
:special-members: __init__
:show-inheritance:

View File

@ -1,82 +0,0 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
# -- Project information -----------------------------------------------------
project = 'nonebot'
copyright = '2020, richardchien'
author = 'richardchien'
# The short X.Y version
version = '2.0.0'
# The full version, including alpha/beta/rc tags
release = '2.0.0'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinx.ext.todo',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'zh_CN'
master_doc = "README"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# html_baseurl = '/api/'
# -- Extension configuration -------------------------------------------------
# -- Options for autodoc extension ----------------------------------------------
autodoc_default_options = {'member-order': 'bysource'}
autodoc_inherit_docstrings = False
autodoc_typehints = 'none'
# -- Options for todo extension ----------------------------------------------
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True

View File

@ -1,10 +0,0 @@
\-\-\-
sidebar_position: 2
\-\-\-
NoneBot.config 模块
===================
.. automodule:: nonebot.config
:members: Env, Config
:show-inheritance:

View File

@ -1,11 +0,0 @@
\-\-\-
sidebar_position: 8
\-\-\-
NoneBot.dependencies 模块
====================
.. automodule:: nonebot.dependencies
:members:
:private-members:
:show-inheritance:

View File

@ -1,13 +0,0 @@
\-\-\-
id: index
slug: /api/drivers/
\-\-\-
NoneBot.drivers 模块
=====================
.. automodule:: nonebot.drivers
:members:
:private-members:
:special-members: __init__
:show-inheritance:

View File

@ -1,9 +0,0 @@
NoneBot.drivers.aiohttp 模块
=============================
.. automodule:: nonebot.drivers.aiohttp
:members:
:private-members:
:show-inheritance:

View File

@ -1,9 +0,0 @@
NoneBot.drivers.fastapi 模块
=============================
.. automodule:: nonebot.drivers.fastapi
:members:
:private-members:
:show-inheritance:

View File

@ -1,9 +0,0 @@
NoneBot.drivers.httpx 模块
=============================
.. automodule:: nonebot.drivers.httpx
:members:
:private-members:
:show-inheritance:

View File

@ -1,9 +0,0 @@
NoneBot.drivers.quart 模块
==========================
.. automodule:: nonebot.drivers.quart
:members:
:private-members:
:show-inheritance:

View File

@ -1,9 +0,0 @@
NoneBot.drivers.websockets 模块
=============================
.. automodule:: nonebot.drivers.websockets
:members:
:private-members:
:show-inheritance:

View File

@ -1,10 +0,0 @@
\-\-\-
sidebar_position: 12
\-\-\-
NoneBot.exception 模块
======================
.. automodule:: nonebot.exception
:members:
:show-inheritance:

View File

@ -1,10 +0,0 @@
\-\-\-
sidebar_position: 9
\-\-\-
NoneBot.log 模块
=================
.. automodule:: nonebot.log
:members:
:show-inheritance:

View File

@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@ -1,12 +0,0 @@
\-\-\-
sidebar_position: 5
\-\-\-
NoneBot.matcher 模块
====================
.. automodule:: nonebot.matcher
:members:
:private-members:
:special-members: __init__
:show-inheritance:

View File

@ -1,10 +0,0 @@
\-\-\-
sidebar_position: 4
\-\-\-
NoneBot.message 模块
======================
.. automodule:: nonebot.message
:members:
:show-inheritance:

View File

@ -1,11 +0,0 @@
\-\-\-
sidebar_position: 7
\-\-\-
NoneBot.permission 模块
=======================
.. automodule:: nonebot.permission
:members:
:show-inheritance:
:special-members:

View File

@ -1,31 +0,0 @@
\-\-\-
sidebar_position: 3
\-\-\-
NoneBot.plugin 模块
====================
.. automodule:: nonebot.plugin
:members:
:show-inheritance:
:special-members: __init__
.. automodule:: nonebot.plugin.plugin
:members:
:show-inheritance:
:special-members: __init__
.. automodule:: nonebot.plugin.on
:members:
:show-inheritance:
:special-members: __init__
.. automodule:: nonebot.plugin.load
:members:
:show-inheritance:
:special-members: __init__
.. automodule:: nonebot.plugin.export
:members:
:show-inheritance:
:special-members: __init__

View File

@ -1,11 +0,0 @@
\-\-\-
sidebar_position: 6
\-\-\-
NoneBot.rule 模块
====================
.. automodule:: nonebot.rule
:members:
:special-members:
:show-inheritance:

View File

@ -1,10 +0,0 @@
\-\-\-
sidebar_position: 11
\-\-\-
NoneBot.typing 模块
===================
.. automodule:: nonebot.typing
:members:
:show-inheritance:

View File

@ -1,10 +0,0 @@
\-\-\-
sidebar_position: 10
\-\-\-
NoneBot.utils 模块
==================
.. automodule:: nonebot.utils
:members:
:show-inheritance:

View File

@ -1,31 +1,37 @@
"""
快捷导入
========
"""本模块主要定义了 NoneBot 启动所需函数,供 bot 入口文件调用。
为方便使用``nonebot`` 模块从子模块导入了部分内容
## 快捷导入
- ``on_message`` => ``nonebot.plugin.on_message``
- ``on_notice`` => ``nonebot.plugin.on_notice``
- ``on_request`` => ``nonebot.plugin.on_request``
- ``on_metaevent`` => ``nonebot.plugin.on_metaevent``
- ``on_startswith`` => ``nonebot.plugin.on_startswith``
- ``on_endswith`` => ``nonebot.plugin.on_endswith``
- ``on_keyword`` => ``nonebot.plugin.on_keyword``
- ``on_command`` => ``nonebot.plugin.on_command``
- ``on_shell_command`` => ``nonebot.plugin.on_shell_command``
- ``on_regex`` => ``nonebot.plugin.on_regex``
- ``CommandGroup`` => ``nonebot.plugin.CommandGroup``
- ``Matchergroup`` => ``nonebot.plugin.MatcherGroup``
- ``load_plugin`` => ``nonebot.plugin.load_plugin``
- ``load_plugins`` => ``nonebot.plugin.load_plugins``
- ``load_all_plugins`` => ``nonebot.plugin.load_all_plugins``
- ``load_from_json`` => ``nonebot.plugin.load_from_json``
- ``load_from_toml`` => ``nonebot.plugin.load_from_toml``
- ``load_builtin_plugins`` => ``nonebot.plugin.load_builtin_plugins``
- ``get_plugin`` => ``nonebot.plugin.get_plugin``
- ``get_loaded_plugins`` => ``nonebot.plugin.get_loaded_plugins``
- ``export`` => ``nonebot.plugin.export``
- ``require`` => ``nonebot.plugin.require``
为方便使用本模块从子模块导入了部分内容以下内容可以直接通过本模块导入:
- `on` => {ref}``on` <nonebot.plugin.on.on>`
- `on_metaevent` => {ref}``on_metaevent` <nonebot.plugin.on.on_metaevent>`
- `on_message` => {ref}``on_message` <nonebot.plugin.on.on_message>`
- `on_notice` => {ref}``on_notice` <nonebot.plugin.on.on_notice>`
- `on_request` => {ref}``on_request` <nonebot.plugin.on.on_request>`
- `on_startswith` => {ref}``on_startswith` <nonebot.plugin.on.on_startswith>`
- `on_endswith` => {ref}``on_endswith` <nonebot.plugin.on.on_endswith>`
- `on_keyword` => {ref}``on_keyword` <nonebot.plugin.on.on_keyword>`
- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`
- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`
- `on_regex` => {ref}``on_regex` <nonebot.plugin.on.on_regex>`
- `CommandGroup` => {ref}``CommandGroup` <nonebot.plugin.on.CommandGroup>`
- `Matchergroup` => {ref}``MatcherGroup` <nonebot.plugin.on.MatcherGroup>`
- `load_plugin` => {ref}``load_plugin` <nonebot.plugin.load.load_plugin>`
- `load_plugins` => {ref}``load_plugins` <nonebot.plugin.load.load_plugins>`
- `load_all_plugins` => {ref}``load_all_plugins` <nonebot.plugin.load.load_all_plugins>`
- `load_from_json` => {ref}``load_from_json` <nonebot.plugin.load.load_from_json>`
- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`
- `load_builtin_plugin` => {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`
- `load_builtin_plugins` => {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
- `get_plugin` => {ref}``get_plugin` <nonebot.plugin.plugin.get_plugin>`
- `get_loaded_plugins` => {ref}``get_loaded_plugins` <nonebot.plugin.plugin.get_loaded_plugins>`
- `export` => {ref}``export` <nonebot.plugin.export.export>`
- `require` => {ref}``require` <nonebot.plugin.load.require>`
FrontMatter:
sidebar_position: 0
description: nonebot 模块
"""
import importlib
@ -51,25 +57,20 @@ _driver: Optional[Driver] = None
def get_driver() -> Driver:
"""
:说明:
"""获取全局 {ref}`nonebot.drivers.Driver` 实例。
获取全局 Driver 对象可用于在计划任务的回调中获取当前 Driver 对象
可用于在计划任务的回调等情形中获取当前 {ref}`nonebot.drivers.Driver` 实例
:返回:
返回:
全局 {ref}`nonebot.drivers.Driver` 对象
* ``Driver``: 全局 Driver 对象
:异常:
* ``ValueError``: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
:用法:
.. code-block:: python
异常:
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
用法:
```python
driver = nonebot.get_driver()
```
"""
if _driver is None:
raise ValueError("NoneBot has not been initialized.")
@ -77,25 +78,19 @@ def get_driver() -> Driver:
def get_app() -> Any:
"""
:说明:
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应的 Server App 对象。
获取全局 Driver 对应 Server App 对象
返回:
Server App 对象
:返回:
* ``Any``: Server App 对象
:异常:
* ``ValueError``: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
:用法:
.. code-block:: python
异常:
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ReverseDriver` 类型
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
用法:
```python
app = nonebot.get_app()
```
"""
driver = get_driver()
assert isinstance(
@ -105,25 +100,19 @@ def get_app() -> Any:
def get_asgi() -> Any:
"""
:说明:
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应 [ASGI](https://asgi.readthedocs.io/) 对象。
获取全局 Driver 对应 Asgi 对象
返回:
ASGI 对象
:返回:
* ``Any``: Asgi 对象
:异常:
* ``ValueError``: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
:用法:
.. code-block:: python
异常:
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ReverseDriver` 类型
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
用法:
```python
asgi = nonebot.get_asgi()
```
"""
driver = get_driver()
assert isinstance(
@ -133,32 +122,28 @@ def get_asgi() -> Any:
def get_bot(self_id: Optional[str] = None) -> Bot:
"""
:说明:
"""获取一个连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。
当提供 self_id 此函数是 get_bots()[self_id] 的简写当不提供时返回一个 Bot
当提供 `self_id` 此函数是 `get_bots()[self_id]` 的简写
当不提供时返回一个 {ref}`nonebot.adapters.Bot`
:参数:
参数:
self_id: 用来识别 {ref}`nonebot.adapters.Bot` {ref}`nonebot.adapters.Bot.self_id` 属性
* ``self_id: Optional[str]``: 用来识别 Bot ID
返回:
{ref}`nonebot.adapters.Bot` 对象
:返回:
异常:
KeyError: 对应 self_id Bot 不存在
ValueError: 没有传入 self_id 且没有 Bot 可用
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
* ``Bot``: Bot 对象
:异常:
* ``KeyError``: 对应 ID Bot 不存在
* ``ValueError``: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
* ``ValueError``: 没有传入 ID 且没有 Bot 可用
:用法:
.. code-block:: python
assert nonebot.get_bot('12345') == nonebot.get_bots()['12345']
用法:
```python
assert nonebot.get_bot("12345") == nonebot.get_bots()["12345"]
another_unspecified_bot = nonebot.get_bot()
```
"""
bots = get_bots()
if self_id is not None:
@ -171,25 +156,18 @@ def get_bot(self_id: Optional[str] = None) -> Bot:
def get_bots() -> Dict[str, Bot]:
"""
:说明:
"""获取所有连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。
获取所有通过 ws 连接 NoneBot Bot 对象
返回:
一个以 {ref}`nonebot.adapters.Bot.self_id` 为键{ref}`nonebot.adapters.Bot` 对象为值的字典
:返回:
* ``Dict[str, Bot]``: 一个以字符串 ID 为键Bot 对象为值的字典
:异常:
* ``ValueError``: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
:用法:
.. code-block:: python
异常:
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
用法:
```python
bots = nonebot.get_bots()
```
"""
driver = get_driver()
return driver.bots
@ -226,31 +204,21 @@ def _resolve_combine_expr(obj_str: str) -> Type[Driver]:
return combine_driver(DriverClass, *mixins)
def init(*, _env_file: Optional[str] = None, **kwargs):
"""
:说明:
def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
"""初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。
初始化 NoneBot 以及 全局 Driver 对象
NoneBot 将会从 .env 文件中读取环境信息并使用相应的 env 文件配置
NoneBot 将会从 .env 文件中读取环境信息并使用相应的 env 文件配置
也可以传入自定义的 `_env_file` 来指定 NoneBot 从该文件读取配置
你也可以传入自定义的 _env_file 来指定 NoneBot 从该文件读取配置
:参数:
* ``_env_file: Optional[str]``: 配置文件名默认从 .env.{env_name} 中读取配置
* ``**kwargs``: 任意变量将会存储到 Config 对象里
:返回:
- ``None``
:用法:
.. code-block:: python
参数:
_env_file: 配置文件名默认从 `.env.{env_name}` 中读取配置
kwargs: 任意变量将会存储到 {ref}`nonebot.drivers.Driver.config` 对象里
用法:
```python
nonebot.init(database=Database(...))
```
"""
global _driver
if not _driver:
@ -275,32 +243,22 @@ def init(*, _env_file: Optional[str] = None, **kwargs):
def run(*args: Any, **kwargs: Any) -> None:
"""
:说明:
"""启动 NoneBot即运行全局 {ref}`nonebot.drivers.Driver` 对象。
启动 NoneBot即运行全局 Driver 对象
:参数:
* ``*args``: 传入 Driver.run 的位置参数
* ``**kwargs``: 传入 Driver.run 的命名参数
:返回:
- ``None``
:用法:
.. code-block:: python
参数:
args: 传入 {ref}`nonebot.drivers.Driver.run` 的位置参数
kwargs: 传入 {ref}`nonebot.drivers.Driver.run` 的命名参数
用法:
```python
nonebot.run(host="127.0.0.1", port=8080)
```
"""
logger.success("Running NoneBot...")
get_driver().run(*args, **kwargs)
import nonebot.params as params
from nonebot.plugin import on as on
from nonebot.plugin import export as export
from nonebot.plugin import require as require
from nonebot.plugin import on_regex as on_regex
@ -322,4 +280,7 @@ 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_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
__autodoc__ = {"internal": False}

View File

@ -1,8 +1,10 @@
"""
协议适配基类
============
"""本模块定义了协议适配基类,各协议请继承以下基类。
各协议请继承以下基类并使用 ``driver.register_adapter`` 注册适配器
使用 {ref}`nonebot.drivers.Driver.register_adapter` 注册适配器
FrontMatter:
sidebar_position: 0
description: nonebot.adapters 模块
"""
from typing import Iterable
@ -20,9 +22,18 @@ except ImportError:
except Exception:
pass
from ._bot import Bot as Bot
from ._event import Event as Event
from ._adapter import Adapter as Adapter
from ._message import Message as Message
from ._message import MessageSegment as MessageSegment
from ._template import MessageTemplate as MessageTemplate
from nonebot.internal.adapter import Bot as Bot
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 MessageSegment as MessageSegment
from nonebot.internal.adapter import MessageTemplate as MessageTemplate
__autodoc__ = {
"Bot": True,
"Event": True,
"Adapter": True,
"Message": True,
"MessageSegment": True,
"MessageTemplate": True,
}

View File

@ -1,137 +0,0 @@
import abc
from pydantic import BaseModel
from ._message import Message
from nonebot.utils import DataclassEncoder
class Event(abc.ABC, BaseModel):
"""Event 基类。提供获取关键信息的方法,其余信息可直接获取。"""
class Config:
extra = "allow"
json_encoders = {Message: DataclassEncoder}
@abc.abstractmethod
def get_type(self) -> str:
"""
:说明:
获取事件类型的方法类型通常为 NoneBot 内置的四种类型
:返回:
* ``Literal["message", "notice", "request", "meta_event"]``
* 其他自定义 ``str``
"""
raise NotImplementedError
@abc.abstractmethod
def get_event_name(self) -> str:
"""
:说明:
获取事件名称的方法
:返回:
* ``str``
"""
raise NotImplementedError
@abc.abstractmethod
def get_event_description(self) -> str:
"""
:说明:
获取事件描述的方法通常为事件具体内容
:返回:
* ``str``
"""
raise NotImplementedError
def __str__(self) -> str:
return f"[{self.get_event_name()}]: {self.get_event_description()}"
def get_log_string(self) -> str:
"""
:说明:
获取事件日志信息的方法通常你不需要修改这个方法只有当希望 NoneBot 隐藏该事件日志时可以抛出 ``NoLogException`` 异常
:返回:
* ``str``
:异常:
- ``NoLogException``
"""
return f"[{self.get_event_name()}]: {self.get_event_description()}"
@abc.abstractmethod
def get_user_id(self) -> str:
"""
:说明:
获取事件主体 id 的方法通常是用户 id
:返回:
* ``str``
"""
raise NotImplementedError
@abc.abstractmethod
def get_session_id(self) -> str:
"""
:说明:
获取会话 id 的方法用于判断当前事件属于哪一个会话通常是用户 id群组 id 组合
:返回:
* ``str``
"""
raise NotImplementedError
@abc.abstractmethod
def get_message(self) -> "Message":
"""
:说明:
获取事件消息内容的方法
:返回:
* ``Message``
"""
raise NotImplementedError
def get_plaintext(self) -> str:
"""
:说明:
获取消息纯文本的方法通常不需要修改默认通过 ``get_message().extract_plain_text`` 获取
:返回:
* ``str``
"""
return self.get_message().extract_plain_text()
@abc.abstractmethod
def is_tome(self) -> bool:
"""
:说明:
获取事件是否与机器人有关的方法
:返回:
* ``bool``
"""
raise NotImplementedError

View File

@ -1,232 +0,0 @@
import abc
from copy import deepcopy
from dataclasses import field, asdict, dataclass
from typing import (
Any,
Dict,
List,
Type,
Union,
Generic,
Mapping,
TypeVar,
Iterable,
)
from ._template import MessageTemplate
T = TypeVar("T")
TMS = TypeVar("TMS", covariant=True)
TM = TypeVar("TM", bound="Message")
@dataclass
class MessageSegment(Mapping, abc.ABC, Generic[TM]):
"""消息段基类"""
type: str
"""
- 类型: ``str``
- 说明: 消息段类型
"""
data: Dict[str, Any] = field(default_factory=lambda: {})
"""
- 类型: ``Dict[str, Union[str, list]]``
- 说明: 消息段数据
"""
@classmethod
@abc.abstractmethod
def get_message_class(cls) -> Type[TM]:
raise NotImplementedError
@abc.abstractmethod
def __str__(self) -> str:
"""该消息段所代表的 str在命令匹配部分使用"""
raise NotImplementedError
def __len__(self) -> int:
return len(str(self))
def __ne__(self: T, other: T) -> bool:
return not self == other
def __add__(self, other: Union[str, Mapping, Iterable[Mapping]]) -> TM:
return self.get_message_class()(self) + other # type: ignore
def __radd__(self, other: Union[str, Mapping, Iterable[Mapping]]) -> TM:
return self.get_message_class()(other) + self # type: ignore
def __getitem__(self, key: str):
return getattr(self, key)
def __setitem__(self, key: str, value: Any):
return setattr(self, key, value)
def __iter__(self):
yield from asdict(self).keys()
def __contains__(self, key: Any) -> bool:
return key in asdict(self).keys()
def get(self, key: str, default: Any = None):
return getattr(self, key, default)
def keys(self):
return asdict(self).keys()
def values(self):
return asdict(self).values()
def items(self):
return asdict(self).items()
def copy(self: T) -> T:
return deepcopy(self)
@abc.abstractmethod
def is_text(self) -> bool:
raise NotImplementedError
class Message(List[TMS], abc.ABC):
"""消息数组"""
def __init__(
self: TM,
message: Union[str, None, Mapping, Iterable[Mapping], TMS, TM, Any] = None,
*args,
**kwargs,
):
"""
:参数:
* ``message: Union[str, list, dict, MessageSegment, Message, Any]``: 消息内容
"""
super().__init__(*args, **kwargs)
if message is None:
return
elif isinstance(message, Message):
self.extend(message)
elif isinstance(message, MessageSegment):
self.append(message)
else:
self.extend(self._construct(message))
@classmethod
def template(cls: Type[TM], format_string: Union[str, TM]) -> MessageTemplate[TM]:
"""
:说明:
根据创建消息模板, 用法和 ``str.format`` 大致相同, 但是可以输出消息对象, 并且支持以 ``Message`` 对象作为消息模板
并且提供了拓展的格式化控制符, 可以用适用于该消息类型的 ``MessageSegment`` 的工厂方法创建消息
:示例:
.. code-block:: python
>>> Message.template("{} {}").format("hello", "world") # 基础演示
Message(MessageSegment(type='text', data={'text': 'hello world'}))
>>> Message.template("{} {}").format(MessageSegment.image("file///..."), "world") # 支持消息段等对象
Message(MessageSegment(type='image', data={'file': 'file///...'}), MessageSegment(type='text', data={'text': 'world'}))
>>> Message.template( # 支持以Message对象作为消息模板
... MessageSegment.text('test {event.user_id}') + MessageSegment.face(233) +
... MessageSegment.text('test {event.message}')).format(event={'user_id':123456, 'message':'hello world'})
Message(MessageSegment(type='text', data={'text': 'test 123456'}),
MessageSegment(type='face', data={'face': 233}),
MessageSegment(type='text', data={'text': 'test hello world'}))
>>> Message.template("{link:image}").format(link='https://...') # 支持拓展格式化控制符
Message(MessageSegment(type='image', data={'file': 'https://...'}))
:参数:
* ``format_string: str``: 格式化字符串
:返回:
- ``MessageFormatter[TM]``: 消息格式化器
"""
return MessageTemplate(format_string, cls)
@classmethod
@abc.abstractmethod
def get_segment_class(cls) -> Type[TMS]:
raise NotImplementedError
def __str__(self):
return "".join(str(seg) for seg in self)
@classmethod
def __get_validators__(cls):
yield cls._validate
@classmethod
def _validate(cls, value):
return cls(value)
@staticmethod
@abc.abstractmethod
def _construct(msg: Union[str, Mapping, Iterable[Mapping], Any]) -> Iterable[TMS]:
raise NotImplementedError
def __add__(self: TM, other: Union[str, Mapping, Iterable[Mapping]]) -> TM:
result = self.copy()
result += other
return result
def __radd__(self: TM, other: Union[str, Mapping, Iterable[Mapping]]) -> TM:
result = self.__class__(other) # type: ignore
return result + self
def __iadd__(self: TM, other: Union[str, Mapping, Iterable[Mapping]]) -> TM:
if isinstance(other, MessageSegment):
self.append(other)
elif isinstance(other, Message):
self.extend(other)
else:
self.extend(self._construct(other))
return self
def append(self: TM, obj: Union[str, TMS]) -> TM:
"""
:说明:
添加一个消息段到消息数组末尾
:参数:
* ``obj: Union[str, MessageSegment]``: 要添加的消息段
"""
if isinstance(obj, MessageSegment):
super(Message, self).append(obj)
elif isinstance(obj, str):
self.extend(self._construct(obj))
else:
raise ValueError(f"Unexpected type: {type(obj)} {obj}")
return self
def extend(self: TM, obj: Union[TM, Iterable[TMS]]) -> TM:
"""
:说明:
拼接一个消息数组或多个消息段到消息数组末尾
:参数:
* ``obj: Union[Message, Iterable[MessageSegment]]``: 要添加的消息数组
"""
for segment in obj:
self.append(segment)
return self
def copy(self: TM) -> TM:
return deepcopy(self)
def extract_plain_text(self: "Message[MessageSegment]") -> str:
"""
:说明:
提取消息内纯文本消息
"""
return "".join(str(seg) for seg in self if seg.is_text())

View File

@ -1,23 +1,18 @@
"""
配置
====
"""本模块定义了 NoneBot 本身运行所需的配置项。
NoneBot 使用 `pydantic`_ 以及 `python-dotenv`_ 来读取配置
NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置
配置项需符合特殊格式或 json 序列化格式详情见 `pydantic Field Type`_ 文档
配置项需符合特殊格式或 json 序列化格式详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档
.. _pydantic:
https://pydantic-docs.helpmanual.io/
.. _python-dotenv:
https://saurabh-kumar.com/python-dotenv/
.. _pydantic Field Type:
https://pydantic-docs.helpmanual.io/usage/types/
FrontMatter:
sidebar_position: 1
description: nonebot.config 模块
"""
import os
from pathlib import Path
from datetime import timedelta
from ipaddress import IPv4Address
from typing import Any, Set, Dict, Tuple, Union, Mapping, Optional
from typing import TYPE_CHECKING, Any, Set, Dict, Tuple, Union, Mapping, Optional
from pydantic import BaseSettings, IPvAnyAddress
from pydantic.env_settings import (
@ -105,9 +100,10 @@ class CustomEnvSettings(EnvSettingsSource):
class BaseConfig(BaseSettings):
# dummy getattr for pylance checking, actually not used
def __getattr__(self, name: str) -> Any: # pragma: no cover
return self.__dict__.get(name)
if TYPE_CHECKING:
# dummy getattr for pylance checking, actually not used
def __getattr__(self, name: str) -> Any: # pragma: no cover
return self.__dict__.get(name)
class Config:
@classmethod
@ -129,19 +125,15 @@ class BaseConfig(BaseSettings):
class Env(BaseConfig):
"""
运行环境配置大小写不敏感
"""运行环境配置。大小写不敏感。
将会从 ``nonebot.init 参数`` > ``环境变量`` > ``.env 环境配置文件`` 的优先级读取配置
将会从 `环境变量` > `.env 环境配置文件` 的优先级读取环境信息
"""
environment: str = "prod"
"""
- **类型**: ``str``
- **默认值**: ``"prod"``
"""当前环境名。
:说明:
当前环境名 NoneBot 将从 ``.env.{environment}`` 文件中加载配置
NoneBot 将从 `.env.{environment}` 文件中加载配置
"""
class Config:
@ -150,138 +142,85 @@ class Env(BaseConfig):
class Config(BaseConfig):
"""
NoneBot 主要配置大小写不敏感
"""NoneBot 主要配置。大小写不敏感。
除了 NoneBot 的配置项外还可以自行添加配置项到 ``.env.{environment}`` 文件中
这些配置将会在 json 反序列化后一起带入 ``Config`` 类中
除了 NoneBot 的配置项外还可以自行添加配置项到 `.env.{environment}` 文件中
这些配置将会在 json 反序列化后一起带入 `Config` 类中
配置方法参考: [配置](https://v2.nonebot.dev/docs/tutorial/configuration)
"""
_common_config: dict
_env_file: str
_env_file: str = ".env"
_common_config: Dict[str, Any] = {}
# nonebot configs
driver: str = "~fastapi"
"""
- **类型**: ``str``
- **默认值**: ``"~fastapi"``
"""NoneBot 运行所使用的 `Driver` 。继承自 {ref}`nonebot.drivers.Driver` 。
:说明:
配置格式为 `<module>[:<Driver>][+<module>[:<Mixin>]]*`
NoneBot 运行所使用的 ``Driver`` 继承自 ``nonebot.drivers.Driver``
配置格式为 ``<module>[:<Driver>][+<module>[:<Mixin>]]*``
``~`` ``nonebot.drivers.`` 的缩写
`~` `nonebot.drivers.` 的缩写
"""
host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore
"""
- **类型**: ``IPvAnyAddress``
- **默认值**: ``127.0.0.1``
:说明:
NoneBot HTTP WebSocket 服务端监听的 IP/主机名
"""
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。"""
port: int = 8080
"""
- **类型**: ``int``
- **默认值**: ``8080``
:说明:
NoneBot HTTP WebSocket 服务端监听的端口
"""
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。"""
log_level: Union[int, str] = "INFO"
"""
- **类型**: ``Union[int, str]``
- **默认值**: ``INFO``
"""NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称
:说明:
参考 [`loguru 日志等级`](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。
配置 NoneBot 日志输出等级可以为 ``int`` 类型等级或等级名称参考 `loguru 日志等级`_
:示例:
.. code-block:: default
:::tip 提示
日志等级名称应为大写 `INFO`
:::
用法:
```conf
LOG_LEVEL=25
LOG_LEVEL=INFO
.. _loguru 日志等级:
https://loguru.readthedocs.io/en/stable/api/logger.html#levels
```
"""
# bot connection configs
api_timeout: Optional[float] = 30.0
"""
- **类型**: ``Optional[float]``
- **默认值**: ``30.``
:说明:
API 请求超时时间单位:
"""
"""API 请求超时时间,单位: 秒。"""
# bot runtime configs
superusers: Set[str] = set()
"""
- **类型**: ``Set[str]``
- **默认值**: ``set()``
:说明:
机器人超级用户
:示例:
.. code-block:: default
"""机器人超级用户。
用法:
```conf
SUPERUSERS=["12345789"]
```
"""
nickname: Set[str] = set()
"""
- **类型**: ``Set[str]``
- **默认值**: ``set()``
:说明:
机器人昵称
"""
"""机器人昵称。"""
command_start: Set[str] = {"/"}
"""
- **类型**: ``Set[str]``
- **默认值**: ``{"/"}``
"""命令的起始标记,用于判断一条消息是不是命令。
:说明:
命令的起始标记用于判断一条消息是不是命令
用法:
```conf
COMMAND_START=["/", ""]
```
"""
command_sep: Set[str] = {"."}
"""
- **类型**: ``Set[str]``
- **默认值**: ``{"."}``
"""命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。
:说明:
命令的分隔标记用于将文本形式的命令切分为元组实际的命令名
用法:
```conf
COMMAND_SEP=["."]
```
"""
session_expire_timeout: timedelta = timedelta(minutes=2)
"""
- **类型**: ``timedelta``
- **默认值**: ``timedelta(minutes=2)``
:说明:
等待用户回复的超时时间
:示例:
.. code-block:: default
"""等待用户回复的超时时间。
用法:
```conf
SESSION_EXPIRE_TIMEOUT=120 # 单位: 秒
SESSION_EXPIRE_TIMEOUT=[DD ][HH:MM]SS[.ffffff]
SESSION_EXPIRE_TIMEOUT=P[DD]DT[HH]H[MM]M[SS]S # ISO 8601
```
"""
# adapter configs
@ -294,3 +233,9 @@ class Config(BaseConfig):
class Config:
extra = "allow"
env_file = ".env.prod"
__autodoc__ = {
"CustomEnvSettings": False,
"BaseConfig": False,
}

View File

@ -1,20 +1,42 @@
"""本模块包含了 NoneBot 事件处理过程中使用到的常量。
FrontMatter:
sidebar_position: 9
description: nonebot.consts 模块
"""
from typing_extensions import Literal
# used by Matcher
RECEIVE_KEY = "_receive_{id}"
LAST_RECEIVE_KEY = "_last_receive"
ARG_KEY = "{key}"
REJECT_TARGET = "_current_target"
REJECT_CACHE_TARGET = "_next_target"
RECEIVE_KEY: Literal["_receive_{id}"] = "_receive_{id}"
"""`receive` 存储 key"""
LAST_RECEIVE_KEY: Literal["_last_receive"] = "_last_receive"
"""`last_receive` 存储 key"""
ARG_KEY: Literal["{key}"] = "{key}"
"""`arg` 存储 key"""
REJECT_TARGET: Literal["_current_target"] = "_current_target"
"""当前 `reject` 目标存储 key"""
REJECT_CACHE_TARGET: Literal["_next_target"] = "_next_target"
"""下一个 `reject` 目标存储 key"""
# used by Rule
PREFIX_KEY = "_prefix"
PREFIX_KEY: Literal["_prefix"] = "_prefix"
"""命令前缀存储 key"""
CMD_KEY = "command"
RAW_CMD_KEY = "raw_command"
CMD_ARG_KEY = "command_arg"
CMD_KEY: Literal["command"] = "command"
"""命令元组存储 key"""
RAW_CMD_KEY: Literal["raw_command"] = "raw_command"
"""命令文本存储 key"""
CMD_ARG_KEY: Literal["command_arg"] = "command_arg"
"""命令参数存储 key"""
SHELL_ARGS = "_args"
SHELL_ARGV = "_argv"
SHELL_ARGS: Literal["_args"] = "_args"
"""shell 命令 parse 后参数字典存储 key"""
SHELL_ARGV: Literal["_argv"] = "_argv"
"""shell 命令原始参数列表存储 key"""
REGEX_MATCHED = "_matched"
REGEX_GROUP = "_matched_groups"
REGEX_DICT = "_matched_dict"
REGEX_MATCHED: Literal["_matched"] = "_matched"
"""正则匹配结果存储 key"""
REGEX_GROUP: Literal["_matched_groups"] = "_matched_groups"
"""正则匹配 group 元组存储 key"""
REGEX_DICT: Literal["_matched_dict"] = "_matched_dict"
"""正则匹配 group 字典存储 key"""

View File

@ -1,8 +1,8 @@
"""
依赖注入处理模块
================
"""本模块模块实现了依赖注入的定义与处理。
该模块实现了依赖注入的定义与处理
FrontMatter:
sidebar_position: 0
description: nonebot.dependencies 模块
"""
import abc
@ -14,15 +14,21 @@ from pydantic.schema import get_annotation_from_field_info
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
from nonebot.log import logger
from .utils import get_typed_signature
from nonebot.exception import TypeMisMatch
from nonebot.utils import run_sync, is_coroutine_callable
T = TypeVar("T", bound="Dependent")
from .utils import check_field_type, get_typed_signature
R = TypeVar("R")
T = TypeVar("T", bound="Dependent")
class Param(abc.ABC, FieldInfo):
"""依赖注入的基本单元 —— 参数。
继承自 `pydantic.fields.FieldInfo`用于描述参数信息不包括参数名
"""
@classmethod
def _check_param(
cls, dependent: "Dependent", name: str, param: inspect.Parameter
@ -45,6 +51,16 @@ class CustomConfig(BaseConfig):
class Dependent(Generic[R]):
"""依赖注入容器
参数:
call: 依赖注入的可调用对象可以是任何 Callable 对象
pre_checkers: 依赖注入解析前的参数检查
params: 具名参数列表
parameterless: 匿名参数列表
allow_types: 允许的参数类型
"""
def __init__(
self,
*,
@ -180,15 +196,18 @@ class Dependent(Generic[R]):
value = await field_info._solve(**params)
if value == Undefined:
value = field.get_default()
_, errs_ = field.validate(value, values, loc=(str(field_info), field.alias))
if errs_:
try:
values[field.name] = check_field_type(field, value)
except TypeMisMatch:
logger.debug(
f"{field_info} "
f"type {type(value)} not match depends {self.call} "
f"annotation {field._type_display()}, ignored"
)
raise TypeMisMatch(field, value)
else:
values[field.name] = value
raise
return values
__autodoc__ = {"CustomConfig": False}

View File

@ -1,11 +1,22 @@
"""
FrontMatter:
sidebar_position: 1
description: nonebot.dependencies.utils 模块
"""
import inspect
from typing import Any, Dict, Callable
from typing import Any, Dict, TypeVar, Callable
from loguru import logger
from pydantic.fields import ModelField
from pydantic.typing import ForwardRef, evaluate_forwardref
from nonebot.exception import TypeMisMatch
V = TypeVar("V")
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
"""获取可调用对象签名"""
signature = inspect.signature(call)
globalns = getattr(call, "__globals__", {})
typed_params = [
@ -22,6 +33,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
"""获取参数的类型注解"""
annotation = param.annotation
if isinstance(annotation, str):
annotation = ForwardRef(annotation)
@ -33,3 +45,10 @@ def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) ->
)
return inspect.Parameter.empty
return annotation
def check_field_type(field: ModelField, value: V) -> V:
_, errs_ = field.validate(value, {}, loc=())
if errs_:
raise TypeMisMatch(field, value)
return value

View File

@ -1,297 +1,38 @@
"""
后端驱动适配基类
=================
"""本模块定义了驱动适配器基类。
各驱动请继承以下基类
各驱动请继承以下基类
FrontMatter:
sidebar_position: 0
description: nonebot.drivers 模块
"""
import abc
import asyncio
from dataclasses import dataclass
from contextlib import asynccontextmanager
from typing import (
TYPE_CHECKING,
Any,
Set,
Dict,
Type,
Callable,
Awaitable,
AsyncGenerator,
)
from nonebot.internal.driver import URL as URL
from nonebot.internal.driver import Driver as Driver
from nonebot.internal.driver import Cookies as Cookies
from nonebot.internal.driver import Request as Request
from nonebot.internal.driver import Response as Response
from nonebot.internal.driver import WebSocket as WebSocket
from nonebot.internal.driver import HTTPVersion as HTTPVersion
from nonebot.internal.driver import ForwardMixin as ForwardMixin
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 HTTPServerSetup as HTTPServerSetup
from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup
from ._model import URL as URL
from nonebot.log import logger
from nonebot.utils import escape_tag
from ._model import Request as Request
from nonebot.config import Env, Config
from ._model import Response as Response
from ._model import WebSocket as WebSocket
from ._model import HTTPVersion as HTTPVersion
from nonebot.typing import T_BotConnectionHook, T_BotDisconnectionHook
if TYPE_CHECKING:
from nonebot.adapters import Bot, Adapter
class Driver(abc.ABC):
"""
Driver 基类
"""
_adapters: Dict[str, "Adapter"] = {}
"""
:类型: ``Dict[str, Adapter]``
:说明: 已注册的适配器列表
"""
_bot_connection_hook: Set[T_BotConnectionHook] = set()
"""
:类型: ``Set[T_BotConnectionHook]``
:说明: Bot 连接建立时执行的函数
"""
_bot_disconnection_hook: Set[T_BotDisconnectionHook] = set()
"""
:类型: ``Set[T_BotDisconnectionHook]``
:说明: Bot 连接断开时执行的函数
"""
def __init__(self, env: Env, config: Config):
"""
:参数:
* ``env: Env``: 包含环境信息的 Env 对象
* ``config: Config``: 包含配置信息的 Config 对象
"""
self.env: str = env.environment
"""
:类型: ``str``
:说明: 环境名称
"""
self.config: Config = config
"""
:类型: ``Config``
:说明: 配置对象
"""
self._clients: Dict[str, "Bot"] = {}
"""
:类型: ``Dict[str, Bot]``
:说明: 已连接的 Bot
"""
@property
def bots(self) -> Dict[str, "Bot"]:
"""
:类型:
``Dict[str, Bot]``
:说明:
获取当前所有已连接的 Bot
"""
return self._clients
def register_adapter(self, adapter: Type["Adapter"], **kwargs) -> None:
"""
:说明:
注册一个协议适配器
:参数:
* ``name: str``: 适配器名称用于在连接时进行识别
* ``adapter: Type[Bot]``: 适配器 Class
* ``**kwargs``: 其他传递给适配器的参数
"""
name = adapter.get_name()
if name in self._adapters:
logger.opt(colors=True).debug(
f'Adapter "<y>{escape_tag(name)}</y>" already exists'
)
return
self._adapters[name] = adapter(self, **kwargs)
logger.opt(colors=True).debug(
f'Succeeded to load adapter "<y>{escape_tag(name)}</y>"'
)
@property
@abc.abstractmethod
def type(self) -> str:
"""驱动类型名称"""
raise NotImplementedError
@property
@abc.abstractmethod
def logger(self):
"""驱动专属 logger 日志记录器"""
raise NotImplementedError
@abc.abstractmethod
def run(self, *args, **kwargs):
"""
:说明:
启动驱动框架
:参数:
* ``*args``
* ``**kwargs``
"""
logger.opt(colors=True).debug(
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
)
@abc.abstractmethod
def on_startup(self, func: Callable) -> Callable:
"""注册一个在驱动启动时运行的函数"""
raise NotImplementedError
@abc.abstractmethod
def on_shutdown(self, func: Callable) -> Callable:
"""注册一个在驱动停止时运行的函数"""
raise NotImplementedError
def on_bot_connect(self, func: T_BotConnectionHook) -> T_BotConnectionHook:
"""
:说明:
装饰一个函数使他在 bot 通过 WebSocket 连接成功时执行
:函数参数:
* ``bot: Bot``: 当前连接上的 Bot 对象
"""
self._bot_connection_hook.add(func)
return func
def on_bot_disconnect(self, func: T_BotDisconnectionHook) -> T_BotDisconnectionHook:
"""
:说明:
装饰一个函数使他在 bot 通过 WebSocket 连接断开时执行
:函数参数:
* ``bot: Bot``: 当前连接上的 Bot 对象
"""
self._bot_disconnection_hook.add(func)
return func
def _bot_connect(self, bot: "Bot") -> None:
"""在 WebSocket 连接成功后,调用该函数来注册 bot 对象"""
if bot.self_id in self._clients:
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
self._clients[bot.self_id] = bot
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot), self._bot_connection_hook))
if coros:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketConnection hook. "
"Running cancelled!</bg #f8bbd0></r>"
)
asyncio.create_task(_run_hook(bot))
def _bot_disconnect(self, bot: "Bot") -> None:
"""在 WebSocket 连接断开后,调用该函数来注销 bot 对象"""
if bot.self_id in self._clients:
del self._clients[bot.self_id]
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot), self._bot_disconnection_hook))
if coros:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
"Running cancelled!</bg #f8bbd0></r>"
)
asyncio.create_task(_run_hook(bot))
class ForwardMixin(abc.ABC):
@property
@abc.abstractmethod
def type(self) -> str:
raise NotImplementedError
@abc.abstractmethod
async def request(self, setup: Request) -> Response:
raise NotImplementedError
@abc.abstractmethod
@asynccontextmanager
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
raise NotImplementedError
yield # used for static type checking's generator detection
class ForwardDriver(Driver, ForwardMixin):
"""
Forward Driver 基类将客户端框架封装以满足适配器使用
"""
class ReverseDriver(Driver):
"""
Reverse Driver 基类将后端框架封装以满足适配器使用
"""
@property
@abc.abstractmethod
def server_app(self) -> Any:
"""驱动 APP 对象"""
raise NotImplementedError
@property
@abc.abstractmethod
def asgi(self) -> Any:
"""驱动 ASGI 对象"""
raise NotImplementedError
@abc.abstractmethod
def setup_http_server(self, setup: "HTTPServerSetup") -> None:
raise NotImplementedError
@abc.abstractmethod
def setup_websocket_server(self, setup: "WebSocketServerSetup") -> None:
raise NotImplementedError
def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Driver]:
# check first
assert issubclass(driver, Driver), "`driver` must be subclass of Driver"
assert all(
map(lambda m: issubclass(m, ForwardMixin), mixins)
), "`mixins` must be subclass of ForwardMixin"
class CombinedDriver(*mixins, driver, ForwardDriver): # type: ignore
@property
def type(self) -> str:
return (
driver.type.__get__(self)
+ "+"
+ "+".join(map(lambda x: x.type.__get__(self), mixins))
)
return CombinedDriver
@dataclass
class HTTPServerSetup:
path: URL # path should not be absolute, check it by URL.is_absolute() == False
method: str
name: str
handle_func: Callable[[Request], Awaitable[Response]]
@dataclass
class WebSocketServerSetup:
path: URL # path should not be absolute, check it by URL.is_absolute() == False
name: str
handle_func: Callable[[WebSocket], Awaitable[Any]]
__autodoc__ = {
"URL": True,
"Driver": True,
"Cookies": True,
"Request": True,
"Response": True,
"WebSocket": True,
"HTTPVersion": True,
"ForwardMixin": True,
"ForwardDriver": True,
"ReverseDriver": True,
"combine_driver": True,
"HTTPServerSetup": True,
"WebSocketServerSetup": True,
}

View File

@ -27,7 +27,7 @@ class BlockDriver(Driver):
@property
@overrides(Driver)
def type(self) -> str:
"""驱动名称: ``block_driver``"""
"""驱动名称: `block_driver`"""
return "block_driver"
@property
@ -39,13 +39,7 @@ class BlockDriver(Driver):
@overrides(Driver)
def on_startup(self, func: STARTUP_FUNC) -> STARTUP_FUNC:
"""
:说明:
注册一个启动时执行的函数
:参数:
* ``func: Callable[[], Awaitable[None]]``
注册一个启动时执行的函数
"""
self.startup_funcs.add(func)
return func
@ -53,13 +47,7 @@ class BlockDriver(Driver):
@overrides(Driver)
def on_shutdown(self, func: SHUTDOWN_FUNC) -> SHUTDOWN_FUNC:
"""
:说明:
注册一个停止时执行的函数
:参数:
* ``func: Callable[[], Awaitable[None]]``
注册一个停止时执行的函数
"""
self.shutdown_funcs.add(func)
return func

View File

@ -1,11 +1,21 @@
"""
AIOHTTP 驱动适配
================
"""[AIOHTTP](https://aiohttp.readthedocs.io/en/stable/) 驱动适配器。
```bash
nb driver install aiohttp
# 或者
pip install nonebot2[aiohttp]
```
:::tip 提示
本驱动仅支持客户端连接
:::
FrontMatter:
sidebar_position: 2
description: nonebot.drivers.aiohttp 模块
"""
from typing import AsyncGenerator
from typing import Type, AsyncGenerator
from contextlib import asynccontextmanager
from nonebot.typing import overrides
@ -13,7 +23,7 @@ from nonebot.drivers import Request, Response
from nonebot.exception import WebSocketClosed
from nonebot.drivers._block_driver import BlockDriver
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.drivers import HTTPVersion, ForwardMixin, combine_driver
from nonebot.drivers import HTTPVersion, ForwardMixin, ForwardDriver, combine_driver
try:
import aiohttp
@ -24,6 +34,8 @@ except ImportError:
class Mixin(ForwardMixin):
"""AIOHTTP Mixin"""
@property
@overrides(ForwardMixin)
def type(self) -> str:
@ -85,6 +97,8 @@ class Mixin(ForwardMixin):
class WebSocket(BaseWebSocket):
"""AIOHTTP Websocket Wrapper"""
def __init__(
self,
*,
@ -139,4 +153,5 @@ class WebSocket(BaseWebSocket):
await self.websocket.send_bytes(data)
Driver = combine_driver(BlockDriver, Mixin)
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore
"""AIOHTTP Driver"""

View File

@ -1,13 +1,12 @@
"""
FastAPI 驱动适配
================
"""[FastAPI](https://fastapi.tiangolo.com/) 驱动适配
本驱动同时支持服务端以及客户端连接
:::tip 提示
本驱动仅支持服务端连接
:::
后端使用方法请参考: `FastAPI 文档`_
.. _FastAPI 文档:
https://fastapi.tiangolo.com/
FrontMatter:
sidebar_position: 1
description: nonebot.drivers.fastapi 模块
"""
import logging
@ -20,10 +19,10 @@ from fastapi.responses import Response
from fastapi import FastAPI, Request, UploadFile, status
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
from ._model import FileTypes
from nonebot.config import Env
from nonebot.typing import overrides
from nonebot.exception import WebSocketClosed
from nonebot.internal.driver import FileTypes
from nonebot.config import Config as NoneBotConfig
from nonebot.drivers import Request as BaseRequest
from nonebot.drivers import WebSocket as BaseWebSocket
@ -42,97 +41,33 @@ def catch_closed(func):
class Config(BaseSettings):
"""
FastAPI 驱动框架设置详情参考 FastAPI 文档
"""
"""FastAPI 驱动框架设置,详情参考 FastAPI 文档"""
fastapi_openapi_url: Optional[str] = None
"""
:类型:
``Optional[str]``
:说明:
``openapi.json`` 地址默认为 ``None`` 即关闭
"""
"""`openapi.json` 地址,默认为 `None` 即关闭"""
fastapi_docs_url: Optional[str] = None
"""
:类型:
``Optional[str]``
:说明:
``swagger`` 地址默认为 ``None`` 即关闭
"""
"""`swagger` 地址,默认为 `None` 即关闭"""
fastapi_redoc_url: Optional[str] = None
"""
:类型:
``Optional[str]``
:说明:
``redoc`` 地址默认为 ``None`` 即关闭
"""
"""`redoc` 地址,默认为 `None` 即关闭"""
fastapi_include_adapter_schema: bool = True
"""是否包含适配器路由的 schema默认为 `True`"""
fastapi_reload: bool = False
"""
:类型:
``bool``
:说明:
开启/关闭冷重载
"""
"""开启/关闭冷重载"""
fastapi_reload_dirs: Optional[List[str]] = None
"""
:类型:
``Optional[List[str]]``
:说明:
重载监控文件夹列表默认为 uvicorn 默认值
"""
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
fastapi_reload_delay: Optional[float] = None
"""
:类型:
``Optional[float]``
:说明:
重载延迟默认为 uvicorn 默认值
"""
"""重载延迟,默认为 uvicorn 默认值"""
fastapi_reload_includes: Optional[List[str]] = None
"""
:类型:
``Optional[List[str]]``
:说明:
要监听的文件列表支持 glob pattern默认为 uvicorn 默认值
"""
"""要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
fastapi_reload_excludes: Optional[List[str]] = None
"""
:类型:
``Optional[List[str]]``
:说明:
不要监听的文件列表支持 glob pattern默认为 uvicorn 默认值
"""
"""不要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
class Config:
extra = "ignore"
class Driver(ReverseDriver):
"""FastAPI 驱动框架。包含反向 Server 功能。"""
"""FastAPI 驱动框架。"""
def __init__(self, env: Env, config: NoneBotConfig):
super(Driver, self).__init__(env, config)
@ -148,19 +83,19 @@ class Driver(ReverseDriver):
@property
@overrides(ReverseDriver)
def type(self) -> str:
"""驱动名称: ``fastapi``"""
"""驱动名称: `fastapi`"""
return "fastapi"
@property
@overrides(ReverseDriver)
def server_app(self) -> FastAPI:
"""``FastAPI APP`` 对象"""
"""`FastAPI APP` 对象"""
return self._server_app
@property
@overrides(ReverseDriver)
def asgi(self) -> FastAPI:
"""``FastAPI APP`` 对象"""
"""`FastAPI APP` 对象"""
return self._server_app
@property
@ -179,6 +114,7 @@ class Driver(ReverseDriver):
_handle,
name=setup.name,
methods=[setup.method],
include_in_schema=self.fastapi_config.fastapi_include_adapter_schema,
)
@overrides(ReverseDriver)
@ -199,7 +135,7 @@ class Driver(ReverseDriver):
@overrides(ReverseDriver)
def on_shutdown(self, func: Callable) -> Callable:
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#startup-event>`_"""
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#shutdown-event>`_"""
return self.server_app.on_event("shutdown")(func)
@overrides(ReverseDriver)
@ -211,7 +147,7 @@ class Driver(ReverseDriver):
app: Optional[str] = None,
**kwargs,
):
"""使用 ``uvicorn`` 启动 FastAPI"""
"""使用 `uvicorn` 启动 FastAPI"""
super().run(host, port, app, **kwargs)
LOGGING_CONFIG = {
"version": 1,
@ -300,6 +236,8 @@ class Driver(ReverseDriver):
class FastAPIWebSocket(BaseWebSocket):
"""FastAPI WebSocket Wrapper"""
@overrides(BaseWebSocket)
def __init__(self, *, request: BaseRequest, websocket: WebSocket):
super().__init__(request=request)
@ -340,3 +278,6 @@ class FastAPIWebSocket(BaseWebSocket):
@overrides(BaseWebSocket)
async def send_bytes(self, data: bytes) -> None:
await self.websocket.send({"type": "websocket.send", "bytes": data})
__autodoc__ = {"catch_closed": False}

View File

@ -1,4 +1,20 @@
from typing import AsyncGenerator
"""[HTTPX](https://www.python-httpx.org/) 驱动适配
```bash
nb driver install httpx
# 或者
pip install nonebot2[httpx]
```
:::tip 提示
本驱动仅支持客户端 HTTP 连接
:::
FrontMatter:
sidebar_position: 3
description: nonebot.drivers.httpx 模块
"""
from typing import Type, AsyncGenerator
from contextlib import asynccontextmanager
from nonebot.typing import overrides
@ -9,6 +25,7 @@ from nonebot.drivers import (
WebSocket,
HTTPVersion,
ForwardMixin,
ForwardDriver,
combine_driver,
)
@ -21,6 +38,8 @@ except ImportError:
class Mixin(ForwardMixin):
"""HTTPX Mixin"""
@property
@overrides(ForwardMixin)
def type(self) -> str:
@ -57,4 +76,5 @@ class Mixin(ForwardMixin):
yield ws
Driver = combine_driver(BlockDriver, Mixin)
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore
"""HTTPX Driver"""

View File

@ -1,11 +1,18 @@
"""
Quart 驱动适配
================
"""[Quart](https://pgjones.gitlab.io/quart/index.html) 驱动适配
后端使用方法请参考: `Quart 文档`_
```bash
nb driver install quart
# 或者
pip install nonebot2[quart]
```
.. _Quart 文档:
https://pgjones.gitlab.io/quart/index.html
:::tip 提示
本驱动仅支持服务端连接
:::
FrontMatter:
sidebar_position: 5
description: nonebot.drivers.quart 模块
"""
import asyncio
@ -15,10 +22,10 @@ from typing import List, Tuple, TypeVar, Callable, Optional, Coroutine
import uvicorn
from pydantic import BaseSettings
from ._model import FileTypes
from nonebot.config import Env
from nonebot.typing import overrides
from nonebot.exception import WebSocketClosed
from nonebot.internal.driver import FileTypes
from nonebot.config import Config as NoneBotConfig
from nonebot.drivers import Request as BaseRequest
from nonebot.drivers import WebSocket as BaseWebSocket
@ -50,69 +57,25 @@ def catch_closed(func):
class Config(BaseSettings):
"""
Quart 驱动框架设置
"""
"""Quart 驱动框架设置"""
quart_reload: bool = False
"""
:类型:
``bool``
:说明:
开启/关闭冷重载
"""
"""开启/关闭冷重载"""
quart_reload_dirs: Optional[List[str]] = None
"""
:类型:
``Optional[List[str]]``
:说明:
重载监控文件夹列表默认为 uvicorn 默认值
"""
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
quart_reload_delay: Optional[float] = None
"""
:类型:
``Optional[float]``
:说明:
重载延迟默认为 uvicorn 默认值
"""
"""重载延迟,默认为 uvicorn 默认值"""
quart_reload_includes: Optional[List[str]] = None
"""
:类型:
``Optional[List[str]]``
:说明:
要监听的文件列表支持 glob pattern默认为 uvicorn 默认值
"""
"""要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
quart_reload_excludes: Optional[List[str]] = None
"""
:类型:
``Optional[List[str]]``
:说明:
不要监听的文件列表支持 glob pattern默认为 uvicorn 默认值
"""
"""不要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
class Config:
extra = "ignore"
class Driver(ReverseDriver):
"""
Quart 驱动框架
"""
"""Quart 驱动框架"""
def __init__(self, env: Env, config: NoneBotConfig):
super().__init__(env, config)
@ -124,19 +87,19 @@ class Driver(ReverseDriver):
@property
@overrides(ReverseDriver)
def type(self) -> str:
"""驱动名称: ``quart``"""
"""驱动名称: `quart`"""
return "quart"
@property
@overrides(ReverseDriver)
def server_app(self) -> Quart:
"""``Quart`` 对象"""
"""`Quart` 对象"""
return self._server_app
@property
@overrides(ReverseDriver)
def asgi(self):
"""``Quart`` 对象"""
"""`Quart` 对象"""
return self._server_app
@property
@ -170,16 +133,12 @@ class Driver(ReverseDriver):
@overrides(ReverseDriver)
def on_startup(self, func: _AsyncCallable) -> _AsyncCallable:
"""参考文档: `Startup and Shutdown`_
.. _Startup and Shutdown:
https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html
"""
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
return self.server_app.before_serving(func) # type: ignore
@overrides(ReverseDriver)
def on_shutdown(self, func: _AsyncCallable) -> _AsyncCallable:
"""参考文档: `Startup and Shutdown`_"""
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
return self.server_app.after_serving(func) # type: ignore
@overrides(ReverseDriver)
@ -191,7 +150,7 @@ class Driver(ReverseDriver):
app: Optional[str] = None,
**kwargs,
):
"""使用 ``uvicorn`` 启动 Quart"""
"""使用 `uvicorn` 启动 Quart"""
super().run(host, port, app, **kwargs)
LOGGING_CONFIG = {
"version": 1,
@ -276,6 +235,8 @@ class Driver(ReverseDriver):
class WebSocket(BaseWebSocket):
"""Quart WebSocket Wrapper"""
def __init__(self, *, request: BaseRequest, websocket: QuartWebSocket):
super().__init__(request=request)
self.websocket = websocket
@ -317,3 +278,6 @@ class WebSocket(BaseWebSocket):
@overrides(BaseWebSocket)
async def send_bytes(self, data: bytes):
await self.websocket.send(data)
__autodoc__ = {"catch_closed": False}

View File

@ -1,6 +1,22 @@
"""[websockets](https://websockets.readthedocs.io/) 驱动适配
```bash
nb driver install websockets
# 或者
pip install nonebot2[websockets]
```
:::tip 提示
本驱动仅支持客户端 WebSocket 连接
:::
FrontMatter:
sidebar_position: 4
description: nonebot.drivers.websockets 模块
"""
import logging
from functools import wraps
from typing import AsyncGenerator
from typing import Type, AsyncGenerator
from contextlib import asynccontextmanager
from nonebot.typing import overrides
@ -9,7 +25,7 @@ from nonebot.drivers import Request, Response
from nonebot.exception import WebSocketClosed
from nonebot.drivers._block_driver import BlockDriver
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.drivers import ForwardMixin, combine_driver
from nonebot.drivers import ForwardMixin, ForwardDriver, combine_driver
try:
from websockets.exceptions import ConnectionClosed
@ -38,6 +54,8 @@ def catch_closed(func):
class Mixin(ForwardMixin):
"""Websockets Mixin"""
@property
@overrides(ForwardMixin)
def type(self) -> str:
@ -60,6 +78,8 @@ class Mixin(ForwardMixin):
class WebSocket(BaseWebSocket):
"""Websockets WebSocket Wrapper"""
@overrides(BaseWebSocket)
def __init__(self, *, request: Request, websocket: WebSocketClientProtocol):
super().__init__(request=request)
@ -103,4 +123,5 @@ class WebSocket(BaseWebSocket):
await self.websocket.send(data)
Driver = combine_driver(BlockDriver, Mixin)
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore
"""Websockets Driver"""

View File

@ -1,9 +1,32 @@
"""
异常
====
"""本模块包含了所有 NoneBot 运行时可能会抛出的异常。
下列文档中的异常是所有 NoneBot 运行时可能会抛出的
这些异常并非所有需要用户处理 NoneBot 内部运行时被捕获并进行对应操作
```bash
NoneBotException
ParserExit
ProcessException
| IgnoredException
| MockApiException
| StopPropagation
MatcherException
| SkippedException
| | TypeMisMatch
| PausedException
| RejectedException
| FinishedException
AdapterException
| NoLogException
| ApiNotAvailable
| NetworkError
| ActionFailed
DriverException
WebSocketClosed
```
FrontMatter:
sidebar_position: 10
description: nonebot.exception 模块
"""
from typing import Any, Optional
@ -12,25 +35,12 @@ from pydantic.fields import ModelField
class NoneBotException(Exception):
"""
:说明:
所有 NoneBot 发生的异常基类
"""
"""所有 NoneBot 发生的异常基类。"""
# Rule Exception
class ParserExit(NoneBotException):
"""
:说明:
``shell command`` 处理消息失败时返回的异常
:参数:
* ``status``
* ``message``
"""
"""{ref}`nonebot.rule.shell_command` 处理消息失败时返回的异常"""
def __init__(self, status: int = 0, message: Optional[str] = None):
self.status = status
@ -45,26 +55,18 @@ class ParserExit(NoneBotException):
# Processor Exception
class ProcessException(NoneBotException):
"""
:说明:
事件处理过程中发生的异常基类
"""
"""事件处理过程中发生的异常基类。"""
class IgnoredException(ProcessException):
"""
:说明:
"""指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。
指示 NoneBot 应该忽略该事件可由 PreProcessor 抛出
:参数:
* ``reason``: 忽略事件的原因
参数:
reason: 忽略事件的原因
"""
def __init__(self, reason):
self.reason = reason
def __init__(self, reason: Any):
self.reason: Any = reason
def __repr__(self):
return f"<IgnoredException, reason={self.reason}>"
@ -74,14 +76,10 @@ class IgnoredException(ProcessException):
class MockApiException(ProcessException):
"""
:说明:
"""指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。可由 api hook 抛出。
指示 NoneBot 阻止本次 API 调用或修改本次调用返回值并返回自定义内容可由 api hook 抛出
:参数:
* ``result``: 返回的内容
参数:
result: 返回的内容
"""
def __init__(self, result: Any):
@ -95,44 +93,46 @@ class MockApiException(ProcessException):
class StopPropagation(ProcessException):
"""
:说明:
"""指示 NoneBot 终止事件向下层传播。
指示 NoneBot 终止事件向下层传播
{ref}`nonebot.matcher.Matcher.block` `True`
或使用 {ref}`nonebot.matcher.Matcher.stop_propagation` 方法时抛出
:用法:
``Matcher.block == True`` 时抛出
用法:
```python
matcher = on_notice(block=True)
# 或者
@matcher.handle()
async def handler(matcher: Matcher):
matcher.stop_propagation()
```
"""
# Matcher Exceptions
class MatcherException(NoneBotException):
"""
:说明:
所有 Matcher 发生的异常基类
"""
"""所有 Matcher 发生的异常基类。"""
class SkippedException(MatcherException):
"""
:说明:
"""指示 NoneBot 立即结束当前 `Handler` 的处理,继续处理下一个 `Handler`。
指示 NoneBot 立即结束当前 ``Handler`` 的处理继续处理下一个 ``Handler``
可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.skip` 抛出
:用法:
用法:
```python
def always_skip():
Matcher.skip()
可以在 ``Handler`` 中通过 ``Matcher.skip()`` 抛出
@matcher.handle()
async def handler(dependency = Depends(always_skip)):
...
```
"""
class TypeMisMatch(SkippedException):
"""
:说明:
当前 ``Handler`` 的参数类型不匹配
"""
"""当前 `Handler` 的参数类型不匹配。"""
def __init__(self, param: ModelField, value: Any):
self.param: ModelField = param
@ -146,115 +146,85 @@ class TypeMisMatch(SkippedException):
class PausedException(MatcherException):
"""
:说明:
"""指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。可用于用户输入新信息。
指示 NoneBot 结束当前 ``Handler`` 并等待下一条消息后继续下一个 ``Handler``
可用于用户输入新信息
可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.pause` 抛出
:用法:
可以在 ``Handler`` 中通过 ``Matcher.pause()`` 抛出
用法:
```python
@matcher.handle()
async def handler():
await matcher.pause("some message")
```
"""
class RejectedException(MatcherException):
"""
:说明:
"""指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。可用于用户重新输入。
指示 NoneBot 结束当前 ``Handler`` 并等待下一条消息后重新运行当前 ``Handler``
可用于用户重新输入
可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.reject` 抛出
:用法:
可以在 ``Handler`` 中通过 ``Matcher.reject()`` 抛出
用法:
```python
@matcher.handle()
async def handler():
await matcher.reject("some message")
```
"""
class FinishedException(MatcherException):
"""
:说明:
"""指示 NoneBot 结束当前 `Handler` 且后续 `Handler` 不再被运行。可用于结束用户会话。
指示 NoneBot 结束当前 ``Handler`` 且后续 ``Handler`` 不再被运行
可用于结束用户会话
可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.finish` 抛出
:用法:
可以在 ``Handler`` 中通过 ``Matcher.finish()`` 抛出
用法:
```python
@matcher.handle()
async def handler():
await matcher.finish("some message")
```
"""
# Adapter Exceptions
class AdapterException(NoneBotException):
"""
:说明:
"""代表 `Adapter` 抛出的异常,所有的 `Adapter` 都要在内部继承自这个 `Exception`
代表 ``Adapter`` 抛出的异常所有的 ``Adapter`` 都要在内部继承自这个 ``Exception``
:参数:
* ``adapter_name: str``: 标识 adapter
参数:
adapter_name: 标识 adapter
"""
def __init__(self, adapter_name: str) -> None:
self.adapter_name = adapter_name
self.adapter_name: str = adapter_name
class NoLogException(AdapterException):
"""
:说明:
"""指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。
指示 NoneBot 对当前 ``Event`` 进行处理但不显示 Log 信息可在 ``get_log_string`` 时抛出
可在 {ref}`nonebot.adapters.Event.get_log_string` 时抛出
"""
pass
class ApiNotAvailable(AdapterException):
"""
:说明:
API 连接不可用时抛出
"""
pass
"""在 API 连接不可用时抛出。"""
class NetworkError(AdapterException):
"""
:说明:
在网络出现问题时抛出: API 请求地址不正确, API 请求无返回或返回状态非正常等
"""
pass
"""在网络出现问题时抛出,如: API 请求地址不正确, API 请求无返回或返回状态非正常等。"""
class ActionFailed(AdapterException):
"""
:说明:
API 请求成功返回数据 API 操作失败
"""
pass
"""API 请求成功返回数据,但 API 操作失败。"""
# Driver Exceptions
class DriverException(NoneBotException):
"""
:说明:
``Driver`` 抛出的异常基类
"""
"""`Driver` 抛出的异常基类"""
class WebSocketClosed(DriverException):
"""
:说明:
WebSocket 连接已关闭
"""
"""WebSocket 连接已关闭"""
def __init__(self, code: int, reason: Optional[str] = None):
self.code = code

View File

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

View File

@ -2,9 +2,8 @@ import abc
from contextlib import asynccontextmanager
from typing import Any, Dict, AsyncGenerator
from ._bot import Bot
from nonebot.config import Config
from nonebot.drivers import (
from nonebot.internal.driver import (
Driver,
Request,
Response,
@ -15,61 +14,93 @@ from nonebot.drivers import (
WebSocketServerSetup,
)
from .bot import Bot
class Adapter(abc.ABC):
"""协议适配器基类。
通常 Adapter 中编写协议通信相关代码: 建立通信连接处理接收与发送 data
参数:
driver: {ref}`nonebot.drivers.Driver` 实例
kwargs: 其他由 {ref}`nonebot.drivers.Driver.register_adapter` 传入的额外参数
"""
def __init__(self, driver: Driver, **kwargs: Any):
self.driver: Driver = driver
"""{ref}`nonebot.drivers.Driver` 实例"""
self.bots: Dict[str, Bot] = {}
"""本协议适配器已建立连接的 {ref}`nonebot.adapters.Bot` 实例"""
@classmethod
@abc.abstractmethod
def get_name(cls) -> str:
"""当前协议适配器的名称"""
raise NotImplementedError
@property
def config(self) -> Config:
"""全局 NoneBot 配置"""
return self.driver.config
def bot_connect(self, bot: Bot) -> None:
"""告知 NoneBot 建立了一个新的 {ref}`nonebot.adapters.Bot` 连接。
当有新的 {ref}`nonebot.adapters.Bot` 实例连接建立成功时调用
参数:
bot: {ref}`nonebot.adapters.Bot` 实例
"""
self.driver._bot_connect(bot)
self.bots[bot.self_id] = bot
def bot_disconnect(self, bot: Bot) -> None:
"""告知 NoneBot {ref}`nonebot.adapters.Bot` 连接已断开。
当有 {ref}`nonebot.adapters.Bot` 实例连接断开时调用
参数:
bot: {ref}`nonebot.adapters.Bot` 实例
"""
self.driver._bot_disconnect(bot)
self.bots.pop(bot.self_id, None)
def setup_http_server(self, setup: HTTPServerSetup):
"""设置一个 HTTP 服务器路由配置"""
if not isinstance(self.driver, ReverseDriver):
raise TypeError("Current driver does not support http server")
self.driver.setup_http_server(setup)
def setup_websocket_server(self, setup: WebSocketServerSetup):
"""设置一个 WebSocket 服务器路由配置"""
if not isinstance(self.driver, ReverseDriver):
raise TypeError("Current driver does not support websocket server")
self.driver.setup_websocket_server(setup)
async def request(self, setup: Request) -> Response:
"""进行一个 HTTP 客户端请求"""
if not isinstance(self.driver, ForwardDriver):
raise TypeError("Current driver does not support http client")
return await self.driver.request(setup)
@asynccontextmanager
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
"""建立一个 WebSocket 客户端连接请求"""
if not isinstance(self.driver, ForwardDriver):
raise TypeError("Current driver does not support websocket client")
async with self.driver.websocket(setup) as ws:
yield ws
@abc.abstractmethod
async def _call_api(self, bot: Bot, api: str, **data) -> Any:
"""
:说明:
async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:
"""`Adapter` 实际调用 api 的逻辑实现函数,实现该方法以调用 api。
``adapter`` 实际调用 api 的逻辑实现函数实现该方法以调用 api
:参数:
* ``api: str``: API 名称
* ``**data``: API 数据
参数:
api: API 名称
data: API 数据
"""
raise NotImplementedError
__autodoc__ = {"Adapter._call_api": True}

View File

@ -10,9 +10,9 @@ from nonebot.exception import MockApiException
from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook
if TYPE_CHECKING:
from ._event import Event
from ._adapter import Adapter
from ._message import Message, MessageSegment
from .event import Event
from .adapter import Adapter
from .message import Message, MessageSegment
class _ApiCall(Protocol):
@ -21,29 +21,23 @@ class _ApiCall(Protocol):
class Bot(abc.ABC):
"""
Bot 基类用于处理上报消息并提供 API 调用接口
"""Bot 基类。
用于处理上报消息并提供 API 调用接口
参数:
adapter: 协议适配器实例
self_id: 机器人 ID
"""
_calling_api_hook: Set[T_CallingAPIHook] = set()
"""
:类型: ``Set[T_CallingAPIHook]``
:说明: call_api 时执行的函数
"""
"""call_api 时执行的函数"""
_called_api_hook: Set[T_CalledAPIHook] = set()
"""
:类型: ``Set[T_CalledAPIHook]``
:说明: call_api 后执行的函数
"""
"""call_api 后执行的函数"""
def __init__(self, adapter: "Adapter", self_id: str):
"""
:参数:
* ``self_id: str``: 机器人 ID
* ``request: HTTPConnection``: request 连接对象
"""
self.adapter: "Adapter" = adapter
"""协议适配器实例"""
self.self_id: str = self_id
"""机器人 ID"""
@ -52,29 +46,26 @@ class Bot(abc.ABC):
@property
def type(self) -> str:
"""协议适配器名称"""
return self.adapter.get_name()
@property
def config(self) -> Config:
"""全局 NoneBot 配置"""
return self.adapter.config
async def call_api(self, api: str, **data: Any) -> Any:
"""
:说明:
"""调用机器人 API 接口,可以通过该函数或直接通过 bot 属性进行调用
调用机器人 API 接口可以通过该函数或直接通过 bot 属性进行调用
:参数:
* ``api: str``: API 名称
* ``**data``: API 数据
:示例:
.. code-block:: python
参数:
api: API 名称
data: API 数据
用法:
```python
await bot.call_api("send_msg", message="hello world")
await bot.send_msg(message="hello world")
```
"""
result: Any = None
@ -128,51 +119,44 @@ class Bot(abc.ABC):
@abc.abstractmethod
async def send(
self, event: "Event", message: Union[str, "Message", "MessageSegment"], **kwargs
self,
event: "Event",
message: Union[str, "Message", "MessageSegment"],
**kwargs: Any,
) -> Any:
"""
:说明:
"""调用机器人基础发送消息接口
调用机器人基础发送消息接口
:参数:
* ``event: Event``: 上报事件
* ``message: Union[str, Message, MessageSegment]``: 要发送的消息
* ``**kwargs``
参数:
event: 上报事件
message: 要发送的消息
kwargs: 任意额外参数
"""
raise NotImplementedError
@classmethod
def on_calling_api(cls, func: T_CallingAPIHook) -> T_CallingAPIHook:
"""
:说明:
"""调用 api 预处理。
调用 api 预处理
钩子函数参数:
:参数:
* ``bot: Bot``: 当前 bot 对象
* ``api: str``: 调用的 api 名称
* ``data: Dict[str, Any]``: api 调用的参数字典
- bot: 当前 bot 对象
- api: 调用的 api 名称
- data: api 调用的参数字典
"""
cls._calling_api_hook.add(func)
return func
@classmethod
def on_called_api(cls, func: T_CalledAPIHook) -> T_CalledAPIHook:
"""
:说明:
"""调用 api 后处理。
调用 api 后处理
钩子函数参数:
:参数:
* ``bot: Bot``: 当前 bot 对象
* ``exception: Optional[Exception]``: 调用 api 时发生的错误
* ``api: str``: 调用的 api 名称
* ``data: Dict[str, Any]``: api 调用的参数字典
* ``result: Any``: api 调用的返回
- bot: 当前 bot 对象
- exception: 调用 api 时发生的错误
- api: 调用的 api 名称
- data: api 调用的参数字典
- result: api 调用的返回
"""
cls._called_api_hook.add(func)
return func

View File

@ -0,0 +1,70 @@
import abc
from pydantic import BaseModel
from nonebot.utils import DataclassEncoder
from .message import Message
class Event(abc.ABC, BaseModel):
"""Event 基类。提供获取关键信息的方法,其余信息可直接获取。"""
class Config:
extra = "allow"
json_encoders = {Message: DataclassEncoder}
@abc.abstractmethod
def get_type(self) -> str:
"""获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。"""
raise NotImplementedError
@abc.abstractmethod
def get_event_name(self) -> str:
"""获取事件名称的方法。"""
raise NotImplementedError
@abc.abstractmethod
def get_event_description(self) -> str:
"""获取事件描述的方法,通常为事件具体内容。"""
raise NotImplementedError
def __str__(self) -> str:
return f"[{self.get_event_name()}]: {self.get_event_description()}"
def get_log_string(self) -> str:
"""获取事件日志信息的方法。
通常你不需要修改这个方法只有当希望 NoneBot 隐藏该事件日志时可以抛出 `NoLogException` 异常
异常:
NoLogException
"""
return f"[{self.get_event_name()}]: {self.get_event_description()}"
@abc.abstractmethod
def get_user_id(self) -> str:
"""获取事件主体 id 的方法,通常是用户 id 。"""
raise NotImplementedError
@abc.abstractmethod
def get_session_id(self) -> str:
"""获取会话 id 的方法,用于判断当前事件属于哪一个会话,通常是用户 id、群组 id 组合。"""
raise NotImplementedError
@abc.abstractmethod
def get_message(self) -> "Message":
"""获取事件消息内容的方法。"""
raise NotImplementedError
def get_plaintext(self) -> str:
"""获取消息纯文本的方法。
通常不需要修改默认通过 `get_message().extract_plain_text` 获取
"""
return self.get_message().extract_plain_text()
@abc.abstractmethod
def is_tome(self) -> bool:
"""获取事件是否与机器人有关的方法。"""
raise NotImplementedError

View File

@ -0,0 +1,325 @@
import abc
from copy import deepcopy
from dataclasses import field, asdict, dataclass
from typing import (
Any,
Dict,
List,
Type,
Tuple,
Union,
Generic,
TypeVar,
Iterable,
Optional,
overload,
)
from pydantic import parse_obj_as
from .template import MessageTemplate
T = TypeVar("T")
TMS = TypeVar("TMS", bound="MessageSegment")
TM = TypeVar("TM", bound="Message")
@dataclass
class MessageSegment(abc.ABC, Generic[TM]):
"""消息段基类"""
type: str
"""消息段类型"""
data: Dict[str, Any] = field(default_factory=dict)
"""消息段数据"""
@classmethod
@abc.abstractmethod
def get_message_class(cls) -> Type[TM]:
"""获取消息数组类型"""
raise NotImplementedError
@abc.abstractmethod
def __str__(self) -> str:
"""该消息段所代表的 str在命令匹配部分使用"""
raise NotImplementedError
def __len__(self) -> int:
return len(str(self))
def __ne__(self: T, other: T) -> bool:
return not self == other
def __add__(self: TMS, other: Union[str, TMS, Iterable[TMS]]) -> TM:
return self.get_message_class()(self) + other
def __radd__(self: TMS, other: Union[str, TMS, Iterable[TMS]]) -> TM:
return self.get_message_class()(other) + self
@classmethod
def __get_validators__(cls):
yield cls._validate
@classmethod
def _validate(cls, value):
if isinstance(value, cls):
return value
if not isinstance(value, dict):
raise ValueError(f"Expected dict for MessageSegment, got {type(value)}")
return cls(**value)
def get(self, key: str, default: Any = None):
return asdict(self).get(key, default)
def keys(self):
return asdict(self).keys()
def values(self):
return asdict(self).values()
def items(self):
return asdict(self).items()
def copy(self: T) -> T:
return deepcopy(self)
@abc.abstractmethod
def is_text(self) -> bool:
"""当前消息段是否为纯文本"""
raise NotImplementedError
class Message(List[TMS], abc.ABC):
"""消息数组
参数:
message: 消息内容
"""
def __init__(
self,
message: Union[str, None, Iterable[TMS], TMS] = None,
):
super().__init__()
if message is None:
return
elif isinstance(message, str):
self.extend(self._construct(message))
elif isinstance(message, MessageSegment):
self.append(message)
elif isinstance(message, Iterable):
self.extend(message)
else:
self.extend(self._construct(message)) # pragma: no cover
@classmethod
def template(cls: Type[TM], format_string: Union[str, TM]) -> MessageTemplate[TM]:
"""创建消息模板。
用法和 `str.format` 大致相同, 但是可以输出消息对象, 并且支持以 `Message` 对象作为消息模板
并且提供了拓展的格式化控制符, 可以用适用于该消息类型的 `MessageSegment` 的工厂方法创建消息
参数:
format_string: 格式化模板
返回:
消息格式化器
"""
return MessageTemplate(format_string, cls)
@classmethod
@abc.abstractmethod
def get_segment_class(cls) -> Type[TMS]:
"""获取消息段类型"""
raise NotImplementedError
def __str__(self) -> str:
return "".join(str(seg) for seg in self)
@classmethod
def __get_validators__(cls):
yield cls._validate
@classmethod
def _validate(cls, value):
if isinstance(value, cls):
return value
elif isinstance(value, Message):
raise ValueError(f"Type {type(value)} can not be converted to {cls}")
elif isinstance(value, str):
pass
elif isinstance(value, dict):
value = parse_obj_as(cls.get_segment_class(), value)
elif isinstance(value, Iterable):
value = [parse_obj_as(cls.get_segment_class(), v) for v in value]
else:
raise ValueError(
f"Expected str, dict or iterable for Message, got {type(value)}"
)
return cls(value)
@staticmethod
@abc.abstractmethod
def _construct(msg: str) -> Iterable[TMS]:
"""构造消息数组"""
raise NotImplementedError
def __add__(self: TM, other: Union[str, TMS, Iterable[TMS]]) -> TM:
result = self.copy()
result += other
return result
def __radd__(self: TM, other: Union[str, TMS, Iterable[TMS]]) -> TM:
result = self.__class__(other)
return result + self
def __iadd__(self: TM, other: Union[str, TMS, Iterable[TMS]]) -> TM:
if isinstance(other, str):
self.extend(self._construct(other))
elif isinstance(other, MessageSegment):
self.append(other)
elif isinstance(other, Iterable):
self.extend(other)
else:
raise ValueError(f"Unsupported type: {type(other)}") # pragma: no cover
return self
@overload
def __getitem__(self: TM, __args: str) -> TM:
"""
参数:
__args: 消息段类型
返回:
所有类型为 `__args` 的消息段
"""
@overload
def __getitem__(self, __args: Tuple[str, int]) -> TMS:
"""
参数:
__args: 消息段类型和索引
返回:
类型为 `__args[0]` 的消息段第 `__args[1]`
"""
@overload
def __getitem__(self: TM, __args: Tuple[str, slice]) -> TM:
"""
参数:
__args: 消息段类型和切片
返回:
类型为 `__args[0]` 的消息段切片 `__args[1]`
"""
@overload
def __getitem__(self, __args: int) -> TMS:
"""
参数:
__args: 索引
返回:
`__args` 个消息段
"""
@overload
def __getitem__(self: TM, __args: slice) -> TM:
"""
参数:
__args: 切片
返回:
消息切片 `__args`
"""
def __getitem__(
self: TM,
args: Union[
str,
Tuple[str, int],
Tuple[str, slice],
int,
slice,
],
) -> Union[TMS, TM]:
arg1, arg2 = args if isinstance(args, tuple) else (args, None)
if isinstance(arg1, int) and arg2 is None:
return super().__getitem__(arg1)
elif isinstance(arg1, slice) and arg2 is None:
return self.__class__(super().__getitem__(arg1))
elif isinstance(arg1, str) and arg2 is None:
return self.__class__(seg for seg in self if seg.type == arg1)
elif isinstance(arg1, str) and isinstance(arg2, int):
return [seg for seg in self if seg.type == arg1][arg2]
elif isinstance(arg1, str) and isinstance(arg2, slice):
return self.__class__([seg for seg in self if seg.type == arg1][arg2])
else:
raise ValueError("Incorrect arguments to slice") # pragma: no cover
def index(self, value: Union[TMS, str], *args) -> int:
if isinstance(value, str):
first_segment = next((seg for seg in self if seg.type == value), None)
if first_segment is None:
raise ValueError(f"Segment with type {value} is not in message")
return super().index(first_segment, *args)
return super().index(value, *args)
def get(self: TM, type_: str, count: Optional[int] = None) -> TM:
if count is None:
return self[type_]
iterator, filtered = (
seg for seg in self if seg.type == type_
), self.__class__()
for _ in range(count):
seg = next(iterator, None)
if seg is None:
break
filtered.append(seg)
return filtered
def count(self, value: Union[TMS, str]) -> int:
return len(self[value]) if isinstance(value, str) else super().count(value)
def append(self: TM, obj: Union[str, TMS]) -> TM:
"""添加一个消息段到消息数组末尾。
参数:
obj: 要添加的消息段
"""
if isinstance(obj, MessageSegment):
super().append(obj)
elif isinstance(obj, str):
self.extend(self._construct(obj))
else:
raise ValueError(f"Unexpected type: {type(obj)} {obj}") # pragma: no cover
return self
def extend(self: TM, obj: Union[TM, Iterable[TMS]]) -> TM:
"""拼接一个消息数组或多个消息段到消息数组末尾。
参数:
obj: 要添加的消息数组
"""
for segment in obj:
self.append(segment)
return self
def copy(self: TM) -> TM:
return deepcopy(self)
def extract_plain_text(self) -> str:
"""提取消息内纯文本消息"""
return "".join(str(seg) for seg in self if seg.is_text())
__autodoc__ = {
"MessageSegment.__str__": True,
"MessageSegment.__add__": True,
"Message.__getitem__": True,
"Message._construct": True,
}

View File

@ -1,4 +1,3 @@
import inspect
import functools
from string import Formatter
from typing import (
@ -21,7 +20,7 @@ from typing import (
)
if TYPE_CHECKING:
from . import Message, MessageSegment
from .message import Message, MessageSegment
TM = TypeVar("TM", bound="Message")
TF = TypeVar("TF", str, "Message")
@ -31,7 +30,12 @@ FormatSpecFunc_T = TypeVar("FormatSpecFunc_T", bound=FormatSpecFunc)
class MessageTemplate(Formatter, Generic[TF]):
"""消息模板格式化实现类"""
"""消息模板格式化实现类。
参数:
template: 模板
factory: 消息类型工厂默认为 `str`
"""
@overload
def __init__(
@ -46,16 +50,6 @@ class MessageTemplate(Formatter, Generic[TF]):
...
def __init__(self, template, factory=str) -> None:
"""
:说明:
创建一个模板
:参数:
* ``template: Union[str, Message]``: 模板
* ``factory: Union[str, Message]``: 消息构造类型默认为 `str`
"""
self.template: TF = template
self.factory: Type[TF] = factory
self.format_specs: Dict[str, FormatSpecFunc] = {}
@ -69,12 +63,15 @@ class MessageTemplate(Formatter, Generic[TF]):
self.format_specs[name] = spec
return spec
def format(self, *args: Any, **kwargs: Any) -> TF:
"""
:说明:
def format(self, *args, **kwargs):
"""根据传入参数和模板生成消息对象"""
return self._format(args, kwargs)
根据模板和参数生成消息对象
"""
def format_map(self, mapping: Mapping[str, Any]) -> TF:
"""根据传入字典和模板生成消息对象, 在传入字段名不是有效标识符时有用"""
return self._format([], mapping)
def _format(self, args: Sequence[Any], kwargs: Mapping[str, Any]) -> TF:
msg = self.factory()
if isinstance(self.template, str):
msg += self.vformat(self.template, args, kwargs)
@ -107,7 +104,7 @@ class MessageTemplate(Formatter, Generic[TF]):
if recursion_depth < 0:
raise ValueError("Max string recursion exceeded")
results: List[Any] = []
results: List[Any] = [self.factory()]
for (literal_text, field_name, format_spec, conversion) in self.parse(
format_string
@ -165,17 +162,14 @@ class MessageTemplate(Formatter, Generic[TF]):
formatted_text = self.format_field(obj, str(format_control))
results.append(formatted_text)
return (
self.factory(functools.reduce(self._add, results or [""])),
auto_arg_index,
)
return functools.reduce(self._add, results), auto_arg_index
def format_field(self, value: Any, format_spec: str) -> Any:
formatter: Optional[FormatSpecFunc] = self.format_specs.get(format_spec)
if formatter is None and not issubclass(self.factory, str):
segment_class: Type["MessageSegment"] = self.factory.get_segment_class()
method = getattr(segment_class, format_spec, None)
if inspect.ismethod(method):
if callable(method) and not cast(str, method.__name__).startswith("_"):
formatter = getattr(segment_class, format_spec)
return (
super().format_field(value, format_spec)

View File

@ -0,0 +1,25 @@
from .model import URL as URL
from .model import RawURL as RawURL
from .driver import Driver as Driver
from .model import Cookies as Cookies
from .model import Request as Request
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 .model import CookieTypes as CookieTypes
from .model import FileContent as FileContent
from .model import HTTPVersion as HTTPVersion
from .model import HeaderTypes as HeaderTypes
from .model import SimpleQuery as SimpleQuery
from .model import ContentTypes as ContentTypes
from .driver import ForwardMixin as ForwardMixin
from .model import QueryVariable as QueryVariable
from .driver import ForwardDriver as ForwardDriver
from .driver import ReverseDriver as ReverseDriver
from .driver import combine_driver as combine_driver
from .model import HTTPServerSetup as HTTPServerSetup
from .model import WebSocketServerSetup as WebSocketServerSetup

View File

@ -0,0 +1,234 @@
import abc
import asyncio
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
from nonebot.log import logger
from nonebot.utils import escape_tag
from nonebot.config import Env, Config
from nonebot.dependencies import Dependent
from nonebot.typing import T_BotConnectionHook, T_BotDisconnectionHook
from nonebot.internal.params import BotParam, DependParam, DefaultParam
from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup
if TYPE_CHECKING:
from nonebot.internal.adapter import Bot, Adapter
BOT_HOOK_PARAMS = [DependParam, BotParam, DefaultParam]
class Driver(abc.ABC):
"""Driver 基类。
参数:
env: 包含环境信息的 Env 对象
config: 包含配置信息的 Config 对象
"""
_adapters: Dict[str, "Adapter"] = {}
"""已注册的适配器列表"""
_bot_connection_hook: Set[Dependent[Any]] = set()
"""Bot 连接建立时执行的函数"""
_bot_disconnection_hook: Set[Dependent[Any]] = set()
"""Bot 连接断开时执行的函数"""
def __init__(self, env: Env, config: Config):
self.env: str = env.environment
"""环境名称"""
self.config: Config = config
"""全局配置对象"""
self._clients: Dict[str, "Bot"] = {}
@property
def bots(self) -> Dict[str, "Bot"]:
"""获取当前所有已连接的 Bot"""
return self._clients
def register_adapter(self, adapter: Type["Adapter"], **kwargs) -> None:
"""注册一个协议适配器
参数:
adapter: 适配器类
kwargs: 其他传递给适配器的参数
"""
name = adapter.get_name()
if name in self._adapters:
logger.opt(colors=True).debug(
f'Adapter "<y>{escape_tag(name)}</y>" already exists'
)
return
self._adapters[name] = adapter(self, **kwargs)
logger.opt(colors=True).debug(
f'Succeeded to load adapter "<y>{escape_tag(name)}</y>"'
)
@property
@abc.abstractmethod
def type(self) -> str:
"""驱动类型名称"""
raise NotImplementedError
@property
@abc.abstractmethod
def logger(self):
"""驱动专属 logger 日志记录器"""
raise NotImplementedError
@abc.abstractmethod
def run(self, *args, **kwargs):
"""
启动驱动框架
"""
logger.opt(colors=True).debug(
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
)
@abc.abstractmethod
def on_startup(self, func: Callable) -> Callable:
"""注册一个在驱动器启动时执行的函数"""
raise NotImplementedError
@abc.abstractmethod
def on_shutdown(self, func: Callable) -> Callable:
"""注册一个在驱动器停止时执行的函数"""
raise NotImplementedError
@classmethod
def on_bot_connect(cls, func: T_BotConnectionHook) -> T_BotConnectionHook:
"""装饰一个函数使他在 bot 连接成功时执行。
钩子函数参数:
- bot: 当前连接上的 Bot 对象
"""
cls._bot_connection_hook.add(
Dependent[Any].parse(call=func, allow_types=BOT_HOOK_PARAMS)
)
return func
@classmethod
def on_bot_disconnect(cls, func: T_BotDisconnectionHook) -> T_BotDisconnectionHook:
"""装饰一个函数使他在 bot 连接断开时执行。
钩子函数参数:
- bot: 当前连接上的 Bot 对象
"""
cls._bot_disconnection_hook.add(
Dependent[Any].parse(call=func, allow_types=BOT_HOOK_PARAMS)
)
return func
def _bot_connect(self, bot: "Bot") -> None:
"""在连接成功后,调用该函数来注册 bot 对象"""
if bot.self_id in self._clients:
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
self._clients[bot.self_id] = bot
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot=bot), self._bot_connection_hook))
if coros:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketConnection hook. "
"Running cancelled!</bg #f8bbd0></r>"
)
asyncio.create_task(_run_hook(bot))
def _bot_disconnect(self, bot: "Bot") -> None:
"""在连接断开后,调用该函数来注销 bot 对象"""
if bot.self_id in self._clients:
del self._clients[bot.self_id]
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot=bot), self._bot_disconnection_hook))
if coros:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
"Running cancelled!</bg #f8bbd0></r>"
)
asyncio.create_task(_run_hook(bot))
class ForwardMixin(abc.ABC):
"""客户端混入基类。"""
@property
@abc.abstractmethod
def type(self) -> str:
"""客户端驱动类型名称"""
raise NotImplementedError
@abc.abstractmethod
async def request(self, setup: Request) -> Response:
"""发送一个 HTTP 请求"""
raise NotImplementedError
@abc.abstractmethod
@asynccontextmanager
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
"""发起一个 WebSocket 连接"""
raise NotImplementedError
yield # used for static type checking's generator detection
class ForwardDriver(Driver, ForwardMixin):
"""客户端基类。将客户端框架封装,以满足适配器使用。"""
class ReverseDriver(Driver):
"""服务端基类。将后端框架封装,以满足适配器使用。"""
@property
@abc.abstractmethod
def server_app(self) -> Any:
"""驱动 APP 对象"""
raise NotImplementedError
@property
@abc.abstractmethod
def asgi(self) -> Any:
"""驱动 ASGI 对象"""
raise NotImplementedError
@abc.abstractmethod
def setup_http_server(self, setup: "HTTPServerSetup") -> None:
"""设置一个 HTTP 服务器路由配置"""
raise NotImplementedError
@abc.abstractmethod
def setup_websocket_server(self, setup: "WebSocketServerSetup") -> None:
"""设置一个 WebSocket 服务器路由配置"""
raise NotImplementedError
def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Driver]:
"""将一个驱动器和多个混入类合并。"""
# check first
assert issubclass(driver, Driver), "`driver` must be subclass of Driver"
assert all(
map(lambda m: issubclass(m, ForwardMixin), mixins)
), "`mixins` must be subclass of ForwardMixin"
if not mixins:
return driver
class CombinedDriver(*mixins, driver, ForwardDriver): # type: ignore
@property
def type(self) -> str:
return (
driver.type.__get__(self)
+ "+"
+ "+".join(map(lambda x: x.type.__get__(self), mixins))
)
return CombinedDriver

View File

@ -1,5 +1,6 @@
import abc
from enum import Enum
from dataclasses import dataclass
from http.cookiejar import Cookie, CookieJar
from typing import (
IO,
@ -9,8 +10,10 @@ from typing import (
Tuple,
Union,
Mapping,
Callable,
Iterator,
Optional,
Awaitable,
MutableMapping,
)
@ -168,8 +171,7 @@ class WebSocket(abc.ABC):
@abc.abstractmethod
def closed(self) -> bool:
"""
:类型: ``bool``
:说明: 连接是否已经关闭
连接是否已经关闭
"""
raise NotImplementedError
@ -315,3 +317,22 @@ class Cookies(MutableMapping):
)
return f"<Cookies [{cookies_repr}]>"
@dataclass
class HTTPServerSetup:
"""HTTP 服务器路由配置。"""
path: URL # path should not be absolute, check it by URL.is_absolute() == False
method: str
name: str
handle_func: Callable[[Request], Awaitable[Response]]
@dataclass
class WebSocketServerSetup:
"""WebSocket 服务器路由配置。"""
path: URL # path should not be absolute, check it by URL.is_absolute() == False
name: str
handle_func: Callable[[WebSocket], Awaitable[Any]]

721
nonebot/internal/matcher.py Normal file
View File

@ -0,0 +1,721 @@
from types import ModuleType
from datetime import datetime
from contextvars import ContextVar
from collections import defaultdict
from contextlib import AsyncExitStack
from typing import (
TYPE_CHECKING,
Any,
Dict,
List,
Type,
Union,
TypeVar,
Callable,
NoReturn,
Optional,
)
from nonebot.log import logger
from nonebot.dependencies import Dependent
from nonebot.consts import (
ARG_KEY,
RECEIVE_KEY,
REJECT_TARGET,
LAST_RECEIVE_KEY,
REJECT_CACHE_TARGET,
)
from nonebot.typing import (
Any,
T_State,
T_Handler,
T_TypeUpdater,
T_DependencyCache,
T_PermissionUpdater,
)
from nonebot.exception import (
TypeMisMatch,
PausedException,
StopPropagation,
SkippedException,
FinishedException,
RejectedException,
)
from .rule import Rule
from .permission import USER, Permission
from .adapter import Bot, Event, Message, MessageSegment, MessageTemplate
from .params import (
Depends,
ArgParam,
BotParam,
EventParam,
StateParam,
DependParam,
DefaultParam,
MatcherParam,
)
if TYPE_CHECKING:
from nonebot.plugin import Plugin
T = TypeVar("T")
matchers: Dict[int, List[Type["Matcher"]]] = defaultdict(list)
"""用于存储当前所有的事件响应器"""
current_bot: ContextVar[Bot] = ContextVar("current_bot")
current_event: ContextVar[Event] = ContextVar("current_event")
current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
current_handler: ContextVar[Dependent] = ContextVar("current_handler")
class MatcherMeta(type):
if TYPE_CHECKING:
module: Optional[str]
plugin_name: Optional[str]
module_name: Optional[str]
module_prefix: Optional[str]
type: str
rule: Rule
permission: Permission
handlers: List[T_Handler]
priority: int
block: bool
temp: bool
expire_time: Optional[datetime]
def __repr__(self) -> str:
return (
f"<Matcher from {self.module_name or 'unknown'}, "
f"type={self.type}, priority={self.priority}, "
f"temp={self.temp}>"
)
def __str__(self) -> str:
return repr(self)
class Matcher(metaclass=MatcherMeta):
"""事件响应器类"""
plugin: Optional["Plugin"] = None
"""事件响应器所在插件"""
module: Optional[ModuleType] = None
"""事件响应器所在插件模块"""
plugin_name: Optional[str] = None
"""事件响应器所在插件名"""
module_name: Optional[str] = None
"""事件响应器所在点分割插件模块路径"""
type: str = ""
"""事件响应器类型"""
rule: Rule = Rule()
"""事件响应器匹配规则"""
permission: Permission = Permission()
"""事件响应器触发权限"""
handlers: List[Dependent[Any]] = []
"""事件响应器拥有的事件处理函数列表"""
priority: int = 1
"""事件响应器优先级"""
block: bool = False
"""事件响应器是否阻止事件传播"""
temp: bool = False
"""事件响应器是否为临时"""
expire_time: Optional[datetime] = None
"""事件响应器过期时间点"""
_default_state: T_State = {}
"""事件响应器默认状态"""
_default_type_updater: Optional[Dependent[str]] = None
"""事件响应器类型更新函数"""
_default_permission_updater: Optional[Dependent[Permission]] = None
"""事件响应器权限更新函数"""
HANDLER_PARAM_TYPES = [
DependParam,
BotParam,
EventParam,
StateParam,
ArgParam,
MatcherParam,
DefaultParam,
]
def __init__(self):
self.handlers = self.handlers.copy()
self.state = self._default_state.copy()
def __repr__(self) -> str:
return (
f"<Matcher from {self.module_name or 'unknown'}, type={self.type}, "
f"priority={self.priority}, temp={self.temp}>"
)
def __str__(self) -> str:
return repr(self)
@classmethod
def new(
cls,
type_: str = "",
rule: Optional[Rule] = None,
permission: Optional[Permission] = None,
handlers: Optional[List[Union[T_Handler, Dependent[Any]]]] = None,
temp: bool = False,
priority: int = 1,
block: bool = False,
*,
plugin: Optional["Plugin"] = None,
module: Optional[ModuleType] = None,
expire_time: Optional[datetime] = None,
default_state: Optional[T_State] = None,
default_type_updater: Optional[Union[T_TypeUpdater, Dependent[str]]] = None,
default_permission_updater: Optional[
Union[T_PermissionUpdater, Dependent[Permission]]
] = None,
) -> Type["Matcher"]:
"""
创建一个新的事件响应器并存储至 `matchers <#matchers>`_
参数:
type_: 事件响应器类型 `event.get_type()` 一致时触发空字符串表示任意
rule: 匹配规则
permission: 权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器即触发一次后删除
priority: 响应优先级
block: 是否阻止事件向更低优先级的响应器传播
plugin: 事件响应器所在插件
module: 事件响应器所在模块
default_state: 默认状态 `state`
expire_time: 事件响应器最终有效时间点过时即被删除
返回:
Type[Matcher]: 新的事件响应器类
"""
NewMatcher = type(
"Matcher",
(Matcher,),
{
"plugin": plugin,
"module": module,
"plugin_name": plugin and plugin.name,
"module_name": module and module.__name__,
"type": type_,
"rule": rule or Rule(),
"permission": permission or Permission(),
"handlers": [
handler
if isinstance(handler, Dependent)
else Dependent[Any].parse(
call=handler, allow_types=cls.HANDLER_PARAM_TYPES
)
for handler in handlers
]
if handlers
else [],
"temp": temp,
"expire_time": expire_time,
"priority": priority,
"block": block,
"_default_state": default_state or {},
"_default_type_updater": (
default_type_updater
if isinstance(default_type_updater, Dependent)
else default_type_updater
and Dependent[str].parse(
call=default_type_updater, allow_types=cls.HANDLER_PARAM_TYPES
)
),
"_default_permission_updater": (
default_permission_updater
if isinstance(default_permission_updater, Dependent)
else default_permission_updater
and Dependent[Permission].parse(
call=default_permission_updater,
allow_types=cls.HANDLER_PARAM_TYPES,
)
),
},
)
logger.trace(f"Define new matcher {NewMatcher}")
matchers[priority].append(NewMatcher)
return NewMatcher
@classmethod
async def check_perm(
cls,
bot: Bot,
event: Event,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查是否满足触发权限
参数:
bot: Bot 对象
event: 上报事件
stack: 异步上下文栈
dependency_cache: 依赖缓存
返回:
是否满足权限
"""
event_type = event.get_type()
return event_type == (cls.type or event_type) and await cls.permission(
bot, event, stack, dependency_cache
)
@classmethod
async def check_rule(
cls,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查是否满足匹配规则
参数:
bot: Bot 对象
event: 上报事件
state: 当前状态
stack: 异步上下文栈
dependency_cache: 依赖缓存
返回:
是否满足匹配规则
"""
event_type = event.get_type()
return event_type == (cls.type or event_type) and await cls.rule(
bot, event, state, stack, dependency_cache
)
@classmethod
def type_updater(cls, func: T_TypeUpdater) -> T_TypeUpdater:
"""装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数
参数:
func: 响应事件类型更新函数
"""
cls._default_type_updater = Dependent[str].parse(
call=func, allow_types=cls.HANDLER_PARAM_TYPES
)
return func
@classmethod
def permission_updater(cls, func: T_PermissionUpdater) -> T_PermissionUpdater:
"""装饰一个函数来更改当前事件响应器的默认会话权限更新函数
参数:
func: 会话权限更新函数
"""
cls._default_permission_updater = Dependent[Permission].parse(
call=func, allow_types=cls.HANDLER_PARAM_TYPES
)
return func
@classmethod
def append_handler(
cls, handler: T_Handler, parameterless: Optional[List[Any]] = None
) -> Dependent[Any]:
handler_ = Dependent[Any].parse(
call=handler,
parameterless=parameterless,
allow_types=cls.HANDLER_PARAM_TYPES,
)
cls.handlers.append(handler_)
return handler_
@classmethod
def handle(
cls, parameterless: Optional[List[Any]] = None
) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来向事件响应器直接添加一个处理函数
参数:
parameterless: 非参数类型依赖列表
"""
def _decorator(func: T_Handler) -> T_Handler:
cls.append_handler(func, parameterless=parameterless)
return func
return _decorator
@classmethod
def receive(
cls, id: str = "", parameterless: Optional[List[Any]] = None
) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数
参数:
id: 消息 ID
parameterless: 非参数类型依赖列表
"""
async def _receive(event: Event, matcher: "Matcher") -> Union[None, NoReturn]:
matcher.set_target(RECEIVE_KEY.format(id=id))
if matcher.get_target() == RECEIVE_KEY.format(id=id):
matcher.set_receive(id, event)
return
if matcher.get_receive(id, ...) is not ...:
return
await matcher.reject()
_parameterless = [Depends(_receive), *(parameterless or [])]
def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func:
func_handler = cls.handlers[-1]
for depend in reversed(_parameterless):
func_handler.prepend_parameterless(depend)
else:
cls.append_handler(func, parameterless=_parameterless)
return func
return _decorator
@classmethod
def got(
cls,
key: str,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
parameterless: Optional[List[Any]] = None,
) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来指示 NoneBot 获取一个参数 `key`
当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数如果 `key` 已存在则直接继续运行
参数:
key: 参数名
prompt: 在参数不存在时向用户发送的消息
parameterless: 非参数类型依赖列表
"""
async def _key_getter(event: Event, matcher: "Matcher"):
matcher.set_target(ARG_KEY.format(key=key))
if matcher.get_target() == ARG_KEY.format(key=key):
matcher.set_arg(key, event.get_message())
return
if matcher.get_arg(key, ...) is not ...:
return
await matcher.reject(prompt)
_parameterless = [
Depends(_key_getter),
*(parameterless or []),
]
def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func:
func_handler = cls.handlers[-1]
for depend in reversed(_parameterless):
func_handler.prepend_parameterless(depend)
else:
cls.append_handler(func, parameterless=_parameterless)
return func
return _decorator
@classmethod
async def send(
cls,
message: Union[str, Message, MessageSegment, MessageTemplate],
**kwargs: Any,
) -> Any:
"""发送一条消息给当前交互用户
参数:
message: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
bot = current_bot.get()
event = current_event.get()
state = current_matcher.get().state
if isinstance(message, MessageTemplate):
_message = message.format(**state)
else:
_message = message
return await bot.send(event=event, message=_message, **kwargs)
@classmethod
async def finish(
cls,
message: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""发送一条消息给当前交互用户并结束当前事件响应器
参数:
message: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
if message is not None:
await cls.send(message, **kwargs)
raise FinishedException
@classmethod
async def pause(
cls,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""发送一条消息给当前交互用户并暂停事件响应器,在接收用户新的一条消息后继续下一个处理函数
参数:
prompt: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
if prompt is not None:
await cls.send(prompt, **kwargs)
raise PausedException
@classmethod
async def reject(
cls,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""最近使用 `got` / `receive` 接收的消息不符合预期,
发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置在接收用户新的一个事件后从头开始执行当前处理函数
参数:
prompt: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
if prompt is not None:
await cls.send(prompt, **kwargs)
raise RejectedException
@classmethod
async def reject_arg(
cls,
key: str,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""最近使用 `got` 接收的消息不符合预期,
发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置在接收用户新的一条消息后从头开始执行当前处理函数
参数:
key: 参数名
prompt: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
matcher = current_matcher.get()
matcher.set_target(ARG_KEY.format(key=key))
if prompt is not None:
await cls.send(prompt, **kwargs)
raise RejectedException
@classmethod
async def reject_receive(
cls,
id: str = "",
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""最近使用 `receive` 接收的消息不符合预期,
发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置在接收用户新的一个事件后从头开始执行当前处理函数
参数:
id: 消息 id
prompt: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
matcher = current_matcher.get()
matcher.set_target(RECEIVE_KEY.format(id=id))
if prompt is not None:
await cls.send(prompt, **kwargs)
raise RejectedException
@classmethod
def skip(cls) -> NoReturn:
"""跳过当前事件处理函数,继续下一个处理函数
通常在事件处理函数的依赖中使用
"""
raise SkippedException
def get_receive(self, id: str, default: T = None) -> Union[Event, T]:
"""获取一个 `receive` 事件
如果没有找到对应的事件返回 `default`
"""
return self.state.get(RECEIVE_KEY.format(id=id), default)
def set_receive(self, id: str, event: Event) -> None:
"""设置一个 `receive` 事件"""
self.state[RECEIVE_KEY.format(id=id)] = event
self.state[LAST_RECEIVE_KEY] = event
def get_last_receive(self, default: T = None) -> Union[Event, T]:
"""获取最近一次 `receive` 事件
如果没有事件返回 `default`
"""
return self.state.get(LAST_RECEIVE_KEY, default)
def get_arg(self, key: str, default: T = None) -> Union[Message, T]:
"""获取一个 `got` 消息
如果没有找到对应的消息返回 `default`
"""
return self.state.get(ARG_KEY.format(key=key), default)
def set_arg(self, key: str, message: Message) -> None:
"""设置一个 `got` 消息"""
self.state[ARG_KEY.format(key=key)] = message
def set_target(self, target: str, cache: bool = True) -> None:
if cache:
self.state[REJECT_CACHE_TARGET] = target
else:
self.state[REJECT_TARGET] = target
def get_target(self, default: T = None) -> Union[str, T]:
return self.state.get(REJECT_TARGET, default)
def stop_propagation(self):
"""阻止事件传播"""
self.block = True
async def update_type(self, bot: Bot, event: Event) -> str:
updater = self.__class__._default_type_updater
if not updater:
return "message"
return await updater(bot=bot, event=event, state=self.state, matcher=self)
async def update_permission(self, bot: Bot, event: Event) -> Permission:
updater = self.__class__._default_permission_updater
if not updater:
return USER(event.get_session_id(), perm=self.permission)
return await updater(bot=bot, event=event, state=self.state, matcher=self)
async def resolve_reject(self):
handler = current_handler.get()
self.handlers.insert(0, handler)
if REJECT_CACHE_TARGET in self.state:
self.state[REJECT_TARGET] = self.state[REJECT_CACHE_TARGET]
async def simple_run(
self,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
):
logger.trace(
f"Matcher {self} run with incoming args: "
f"bot={bot}, event={event}, state={state}"
)
b_t = current_bot.set(bot)
e_t = current_event.set(event)
m_t = current_matcher.set(self)
try:
# Refresh preprocess state
self.state.update(state)
while self.handlers:
handler = self.handlers.pop(0)
current_handler.set(handler)
logger.debug(f"Running handler {handler}")
try:
await handler(
matcher=self,
bot=bot,
event=event,
state=self.state,
stack=stack,
dependency_cache=dependency_cache,
)
except TypeMisMatch as e:
logger.debug(
f"Handler {handler} param {e.param.name} value {e.value} "
f"mismatch type {e.param._type_display()}, skipped"
)
except SkippedException as e:
logger.debug(f"Handler {handler} skipped")
except StopPropagation:
self.block = True
finally:
logger.info(f"Matcher {self} running complete")
current_bot.reset(b_t)
current_event.reset(e_t)
current_matcher.reset(m_t)
# 运行handlers
async def run(
self,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
):
try:
await self.simple_run(bot, event, state, stack, dependency_cache)
except RejectedException:
await self.resolve_reject()
type_ = await self.update_type(bot, event)
permission = await self.update_permission(bot, event)
Matcher.new(
type_,
Rule(),
permission,
self.handlers,
temp=True,
priority=0,
block=True,
plugin=self.plugin,
module=self.module,
expire_time=datetime.now() + bot.config.session_expire_timeout,
default_state=self.state,
default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater,
)
except PausedException:
type_ = await self.update_type(bot, event)
permission = await self.update_permission(bot, event)
Matcher.new(
type_,
Rule(),
permission,
self.handlers,
temp=True,
priority=0,
block=True,
plugin=self.plugin,
module=self.module,
expire_time=datetime.now() + bot.config.session_expire_timeout,
default_state=self.state,
default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater,
)
except FinishedException:
pass
__autodoc__ = {
"MatcherMeta": False,
"Matcher.get_target": False,
"Matcher.set_target": False,
"Matcher.update_type": False,
"Matcher.update_permission": False,
"Matcher.resolve_reject": False,
"Matcher.simple_run": False,
}

378
nonebot/internal/params.py Normal file
View File

@ -0,0 +1,378 @@
import asyncio
import inspect
import warnings
from typing_extensions import Literal
from typing import TYPE_CHECKING, Any, Callable, Optional, cast
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
from pydantic.fields import Required, Undefined, ModelField
from nonebot.log import logger
from nonebot.exception import TypeMisMatch
from nonebot.dependencies.utils import check_field_type
from nonebot.dependencies import Param, Dependent, CustomConfig
from nonebot.typing import T_State, T_Handler, T_DependencyCache
from nonebot.utils import (
get_name,
run_sync,
is_gen_callable,
run_sync_ctx_manager,
is_async_gen_callable,
is_coroutine_callable,
generic_check_issubclass,
)
if TYPE_CHECKING:
from nonebot.matcher import Matcher
from nonebot.adapters import Bot, Event
class DependsInner:
def __init__(
self,
dependency: Optional[T_Handler] = None,
*,
use_cache: bool = True,
) -> None:
self.dependency = dependency
self.use_cache = use_cache
def __repr__(self) -> str:
dep = get_name(self.dependency)
cache = "" if self.use_cache else ", use_cache=False"
return f"{self.__class__.__name__}({dep}{cache})"
def Depends(
dependency: Optional[T_Handler] = None,
*,
use_cache: bool = True,
) -> Any:
"""子依赖装饰器
参数:
dependency: 依赖函数默认为参数的类型注释
use_cache: 是否使用缓存默认为 `True`
用法:
```python
def depend_func() -> Any:
return ...
def depend_gen_func():
try:
yield ...
finally:
...
async def handler(param_name: Any = Depends(depend_func), gen: Any = Depends(depend_gen_func)):
...
```
"""
return DependsInner(dependency, use_cache=use_cache)
class DependParam(Param):
"""子依赖参数"""
@classmethod
def _check_param(
cls,
dependent: Dependent,
name: str,
param: inspect.Parameter,
) -> Optional["DependParam"]:
if isinstance(param.default, DependsInner):
dependency: T_Handler
if param.default.dependency is None:
assert param.annotation is not param.empty, "Dependency cannot be empty"
dependency = param.annotation
else:
dependency = param.default.dependency
sub_dependent = Dependent[Any].parse(
call=dependency,
allow_types=dependent.allow_types,
)
dependent.pre_checkers.extend(sub_dependent.pre_checkers)
sub_dependent.pre_checkers.clear()
return cls(
Required, use_cache=param.default.use_cache, dependent=sub_dependent
)
@classmethod
def _check_parameterless(
cls, dependent: "Dependent", value: Any
) -> Optional["Param"]:
if isinstance(value, DependsInner):
assert value.dependency, "Dependency cannot be empty"
dependent = Dependent[Any].parse(
call=value.dependency, allow_types=dependent.allow_types
)
return cls(Required, use_cache=value.use_cache, dependent=dependent)
async def _solve(
self,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
**kwargs: Any,
) -> Any:
use_cache: bool = self.extra["use_cache"]
dependency_cache = {} if dependency_cache is None else dependency_cache
sub_dependent: Dependent = self.extra["dependent"]
sub_dependent.call = cast(Callable[..., Any], sub_dependent.call)
call = sub_dependent.call
# solve sub dependency with current cache
sub_values = await sub_dependent.solve(
stack=stack,
dependency_cache=dependency_cache,
**kwargs,
)
# run dependency function
task: asyncio.Task[Any]
if use_cache and call in dependency_cache:
solved = await dependency_cache[call]
elif is_gen_callable(call) or is_async_gen_callable(call):
assert isinstance(
stack, AsyncExitStack
), "Generator dependency should be called in context"
if is_gen_callable(call):
cm = run_sync_ctx_manager(contextmanager(call)(**sub_values))
else:
cm = asynccontextmanager(call)(**sub_values)
task = asyncio.create_task(stack.enter_async_context(cm))
dependency_cache[call] = task
solved = await task
elif is_coroutine_callable(call):
task = asyncio.create_task(call(**sub_values))
dependency_cache[call] = task
solved = await task
else:
task = asyncio.create_task(run_sync(call)(**sub_values))
dependency_cache[call] = task
solved = await task
return solved
class _BotChecker(Param):
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
field: ModelField = self.extra["field"]
try:
return check_field_type(field, bot)
except TypeMisMatch:
logger.debug(
f"Bot type {type(bot)} not match "
f"annotation {field._type_display()}, ignored"
)
raise
class BotParam(Param):
"""{ref}`nonebot.adapters.Bot` 参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["BotParam"]:
from nonebot.adapters import Bot
if param.default == param.empty:
if generic_check_issubclass(param.annotation, Bot):
if param.annotation is not Bot:
dependent.pre_checkers.append(
_BotChecker(
Required,
field=ModelField(
name=name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
),
)
)
return cls(Required)
elif param.annotation == param.empty and name == "bot":
return cls(Required)
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
return bot
class _EventChecker(Param):
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
field: ModelField = self.extra["field"]
try:
return check_field_type(field, event)
except TypeMisMatch:
logger.debug(
f"Event type {type(event)} not match "
f"annotation {field._type_display()}, ignored"
)
raise
class EventParam(Param):
"""{ref}`nonebot.adapters.Event` 参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["EventParam"]:
from nonebot.adapters import Event
if param.default == param.empty:
if generic_check_issubclass(param.annotation, Event):
if param.annotation is not Event:
dependent.pre_checkers.append(
_EventChecker(
Required,
field=ModelField(
name=name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
),
)
)
return cls(Required)
elif param.annotation == param.empty and name == "event":
return cls(Required)
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
return event
class StateInner(T_State):
...
def State() -> T_State:
"""**Deprecated**: 事件处理状态参数,请直接使用 {ref}`nonebot.typing.T_State`"""
warnings.warn("State() is deprecated, use `T_State` instead", DeprecationWarning)
return StateInner()
class StateParam(Param):
"""事件处理状态参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["StateParam"]:
if isinstance(param.default, StateInner):
return cls(Required)
elif param.default == param.empty:
if param.annotation is T_State:
return cls(Required)
elif param.annotation == param.empty and name == "state":
return cls(Required)
async def _solve(self, state: T_State, **kwargs: Any) -> Any:
return state
class MatcherParam(Param):
"""事件响应器实例参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["MatcherParam"]:
from nonebot.matcher import Matcher
if generic_check_issubclass(param.annotation, Matcher) or (
param.annotation == param.empty and name == "matcher"
):
return cls(Required)
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
return matcher
class ArgInner:
def __init__(
self, key: Optional[str], type: Literal["message", "str", "plaintext"]
) -> None:
self.key = key
self.type = type
def Arg(key: Optional[str] = None) -> Any:
"""`got` 的 Arg 参数消息"""
return ArgInner(key, "message")
def ArgStr(key: Optional[str] = None) -> str:
"""`got` 的 Arg 参数消息文本"""
return ArgInner(key, "str") # type: ignore
def ArgPlainText(key: Optional[str] = None) -> str:
"""`got` 的 Arg 参数消息纯文本"""
return ArgInner(key, "plaintext") # type: ignore
class ArgParam(Param):
"""`got` 的 Arg 参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["ArgParam"]:
if isinstance(param.default, ArgInner):
return cls(Required, key=param.default.key or name, type=param.default.type)
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
message = matcher.get_arg(self.extra["key"])
if message is None:
return message
if self.extra["type"] == "message":
return message
elif self.extra["type"] == "str":
return str(message)
else:
return message.extract_plain_text()
class ExceptionParam(Param):
"""`run_postprocessor` 的异常参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["ExceptionParam"]:
if generic_check_issubclass(param.annotation, Exception) or (
param.annotation == param.empty and name == "exception"
):
return cls(Required)
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
return exception
class DefaultParam(Param):
"""默认值参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["DefaultParam"]:
if param.default != param.empty:
return cls(param.default)
async def _solve(self, **kwargs: Any) -> Any:
return Undefined
__autodoc__ = {
"DependsInner": False,
"StateInner": False,
"ArgInner": False,
}

View File

@ -0,0 +1,133 @@
import asyncio
from contextlib import AsyncExitStack
from typing import Any, Set, Tuple, Union, NoReturn, Optional, Coroutine
from nonebot.dependencies import Dependent
from nonebot.exception import SkippedException
from nonebot.typing import T_DependencyCache, T_PermissionChecker
from .adapter import Bot, Event
from .params import BotParam, EventParam, DependParam, DefaultParam
async def _run_coro_with_catch(coro: Coroutine[Any, Any, Any]):
try:
return await coro
except SkippedException:
return False
class Permission:
"""{ref}`nonebot.matcher.Matcher` 权限类。
当事件传递时 {ref}`nonebot.matcher.Matcher` 运行前进行检查
参数:
checkers: PermissionChecker
用法:
```python
Permission(async_function) | sync_function
# 等价于
Permission(async_function, sync_function)
```
"""
__slots__ = ("checkers",)
HANDLER_PARAM_TYPES = [
DependParam,
BotParam,
EventParam,
DefaultParam,
]
def __init__(self, *checkers: Union[T_PermissionChecker, Dependent[bool]]) -> None:
self.checkers: Set[Dependent[bool]] = set(
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
)
for checker in checkers
)
"""存储 `PermissionChecker`"""
async def __call__(
self,
bot: Bot,
event: Event,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查是否满足某个权限
参数:
bot: Bot 对象
event: Event 对象
stack: 异步上下文栈
dependency_cache: 依赖缓存
"""
if not self.checkers:
return True
results = await asyncio.gather(
*(
_run_coro_with_catch(
checker(
bot=bot,
event=event,
stack=stack,
dependency_cache=dependency_cache,
)
)
for checker in self.checkers
),
)
return any(results)
def __and__(self, other) -> NoReturn:
raise RuntimeError("And operation between Permissions is not allowed.")
def __or__(
self, other: Optional[Union["Permission", T_PermissionChecker]]
) -> "Permission":
if other is None:
return self
elif isinstance(other, Permission):
return Permission(*self.checkers, *other.checkers)
else:
return Permission(*self.checkers, other)
class User:
"""检查当前事件是否属于指定会话
参数:
users: 会话 ID 元组
perm: 需同时满足的权限
"""
__slots__ = ("users", "perm")
def __init__(
self, users: Tuple[str, ...], perm: Optional[Permission] = None
) -> None:
self.users = users
self.perm = perm
async def __call__(self, bot: Bot, event: Event) -> bool:
return bool(
event.get_session_id() in self.users
and (self.perm is None or await self.perm(bot, event))
)
def USER(*users: str, perm: Optional[Permission] = None):
"""匹配当前事件属于指定会话
参数:
user: 会话白名单
perm: 需要同时满足的权限
"""
return Permission(User(users, perm))

95
nonebot/internal/rule.py Normal file
View File

@ -0,0 +1,95 @@
import asyncio
from contextlib import AsyncExitStack
from typing import Set, Union, NoReturn, Optional
from nonebot.dependencies import Dependent
from nonebot.exception import SkippedException
from nonebot.typing import T_State, T_RuleChecker, T_DependencyCache
from .adapter import Bot, Event
from .params import BotParam, EventParam, StateParam, DependParam, DefaultParam
class Rule:
"""{ref}`nonebot.matcher.Matcher` 规则类。
当事件传递时 {ref}`nonebot.matcher.Matcher` 运行前进行检查
参数:
*checkers: RuleChecker
用法:
```python
Rule(async_function) & sync_function
# 等价于
Rule(async_function, sync_function)
```
"""
__slots__ = ("checkers",)
HANDLER_PARAM_TYPES = [
DependParam,
BotParam,
EventParam,
StateParam,
DefaultParam,
]
def __init__(self, *checkers: Union[T_RuleChecker, Dependent[bool]]) -> None:
self.checkers: Set[Dependent[bool]] = set(
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
)
for checker in checkers
)
"""存储 `RuleChecker`"""
async def __call__(
self,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查是否符合所有规则
参数:
bot: Bot 对象
event: Event 对象
state: 当前 State
stack: 异步上下文栈
dependency_cache: 依赖缓存
"""
if not self.checkers:
return True
try:
results = await asyncio.gather(
*(
checker(
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
)
for checker in self.checkers
)
)
except SkippedException:
return False
return all(results)
def __and__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
if other is None:
return self
elif isinstance(other, Rule):
return Rule(*self.checkers, *other.checkers)
else:
return Rule(*self.checkers, other)
def __or__(self, other) -> NoReturn:
raise RuntimeError("Or operation between rules is not allowed.")

View File

@ -1,13 +1,15 @@
"""
日志
====
"""本模块定义了 NoneBot 的日志记录 Logger。
NoneBot 使用 `loguru`_ 来记录日志信息
NoneBot 使用 [`loguru`][loguru] 来记录日志信息
自定义 logger 请参考 `loguru`_ 文档
自定义 logger 请参考 [自定义日志](https://v2.nonebot.dev/docs/tutorial/custom-logger)
以及 [`loguru`][loguru] 文档
.. _loguru:
https://github.com/Delgan/loguru
[loguru]: https://github.com/Delgan/loguru
FrontMatter:
sidebar_position: 7
description: nonebot.log 模块
"""
import sys
@ -21,24 +23,22 @@ if TYPE_CHECKING:
# because loguru module do not have `Logger` class actually
from loguru import Logger
from nonebot.plugin import Plugin
# logger = logging.getLogger("nonebot")
logger: "Logger" = loguru.logger
"""
:说明:
"""NoneBot 日志记录器对象。
NoneBot 日志记录器对象
默认信息:
:默认信息:
* 格式: ``[%(asctime)s %(name)s] %(levelname)s: %(message)s``
* 等级: ``INFO`` 根据 ``config.log_level`` 配置改变
* 输出: 输出至 stdout
:用法:
.. code-block:: python
- 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s`
- 等级: `INFO` 根据 `config.log_level` 配置改变
- 输出: 输出至 stdout
用法:
```python
from nonebot.log import logger
```
"""
# default_handler = logging.StreamHandler(sys.stdout)
@ -53,9 +53,11 @@ class Filter:
def __call__(self, record):
module_name: str = record["name"]
module = sys.modules.get(module_name)
if module:
module_name = getattr(module, "__module_name__", module_name)
# TODO: get plugin name instead of module name
# module = sys.modules.get(module_name)
# if module and hasattr(module, "__plugin__"):
# plugin: "Plugin" = getattr(module, "__plugin__")
# module_name = plugin.module_name
record["name"] = module_name.split(".")[0]
levelno = (
logger.level(self.level).no if isinstance(self.level, str) else self.level
@ -81,14 +83,16 @@ class LoguruHandler(logging.Handler): # pragma: no cover
logger.remove()
default_filter = Filter()
default_format = (
default_filter: Filter = Filter()
"""默认日志等级过滤器"""
default_format: str = (
"<g>{time:MM-DD HH:mm:ss}</g> "
"[<lvl>{level}</lvl>] "
"<c><u>{name}</u></c> | "
# "<c>{function}:{line}</c>| "
"{message}"
)
"""默认日志格式"""
logger_id = logger.add(
sys.stdout,
level=0,
@ -97,3 +101,5 @@ logger_id = logger.add(
filter=default_filter,
format=default_format,
)
__autodoc__ = {"Filter": False, "LoguruHandler": False}

View File

@ -1,784 +1,18 @@
"""
事件响应器
==========
"""本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。
该模块实现事件响应器的创建与运行并提供一些快捷方法来帮助用户更好的与机器人进行对话
FrontMatter:
sidebar_position: 3
description: nonebot.matcher 模块
"""
from types import ModuleType
from datetime import datetime
from contextvars import ContextVar
from collections import defaultdict
from contextlib import AsyncExitStack
from typing import (
TYPE_CHECKING,
Any,
Dict,
List,
Type,
Union,
TypeVar,
Callable,
NoReturn,
Optional,
)
from nonebot import params
from nonebot.rule import Rule
from nonebot.log import logger
from nonebot.dependencies import Dependent
from nonebot.permission import USER, Permission
from nonebot.adapters import (
Bot,
Event,
Message,
MessageSegment,
MessageTemplate,
)
from nonebot.consts import (
ARG_KEY,
RECEIVE_KEY,
REJECT_TARGET,
LAST_RECEIVE_KEY,
REJECT_CACHE_TARGET,
)
from nonebot.typing import (
Any,
T_State,
T_Handler,
T_TypeUpdater,
T_DependencyCache,
T_PermissionUpdater,
)
from nonebot.exception import (
TypeMisMatch,
PausedException,
StopPropagation,
SkippedException,
FinishedException,
RejectedException,
)
if TYPE_CHECKING:
from nonebot.plugin import Plugin
T = TypeVar("T")
matchers: Dict[int, List[Type["Matcher"]]] = defaultdict(list)
"""
:类型: ``Dict[int, List[Type[Matcher]]]``
:说明: 用于存储当前所有的事件响应器
"""
current_bot: ContextVar[Bot] = ContextVar("current_bot")
current_event: ContextVar[Event] = ContextVar("current_event")
current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
current_handler: ContextVar[Dependent] = ContextVar("current_handler")
class MatcherMeta(type):
if TYPE_CHECKING:
module: Optional[str]
plugin_name: Optional[str]
module_name: Optional[str]
module_prefix: Optional[str]
type: str
rule: Rule
permission: Permission
handlers: List[T_Handler]
priority: int
block: bool
temp: bool
expire_time: Optional[datetime]
def __repr__(self) -> str:
return (
f"<Matcher from {self.module_name or 'unknown'}, "
f"type={self.type}, priority={self.priority}, "
f"temp={self.temp}>"
)
def __str__(self) -> str:
return repr(self)
class Matcher(metaclass=MatcherMeta):
"""事件响应器类"""
plugin: Optional["Plugin"] = None
"""
:类型: ``Optional[Plugin]``
:说明: 事件响应器所在插件
"""
module: Optional[ModuleType] = None
"""
:类型: ``Optional[ModuleType]``
:说明: 事件响应器所在插件模块
"""
plugin_name: Optional[str] = None
"""
:类型: ``Optional[str]``
:说明: 事件响应器所在插件名
"""
module_name: Optional[str] = None
"""
:类型: ``Optional[str]``
:说明: 事件响应器所在点分割插件模块路径
"""
type: str = ""
"""
:类型: ``str``
:说明: 事件响应器类型
"""
rule: Rule = Rule()
"""
:类型: ``Rule``
:说明: 事件响应器匹配规则
"""
permission: Permission = Permission()
"""
:类型: ``Permission``
:说明: 事件响应器触发权限
"""
handlers: List[Dependent[Any]] = []
"""
:类型: ``List[Handler]``
:说明: 事件响应器拥有的事件处理函数列表
"""
priority: int = 1
"""
:类型: ``int``
:说明: 事件响应器优先级
"""
block: bool = False
"""
:类型: ``bool``
:说明: 事件响应器是否阻止事件传播
"""
temp: bool = False
"""
:类型: ``bool``
:说明: 事件响应器是否为临时
"""
expire_time: Optional[datetime] = None
"""
:类型: ``Optional[datetime]``
:说明: 事件响应器过期时间点
"""
_default_state: T_State = {}
"""
:类型: ``T_State``
:说明: 事件响应器默认状态
"""
_default_type_updater: Optional[Dependent[str]] = None
"""
:类型: ``Optional[Dependent]``
:说明: 事件响应器类型更新函数
"""
_default_permission_updater: Optional[Dependent[Permission]] = None
"""
:类型: ``Optional[Dependent]``
:说明: 事件响应器权限更新函数
"""
HANDLER_PARAM_TYPES = [
params.DependParam,
params.BotParam,
params.EventParam,
params.StateParam,
params.ArgParam,
params.MatcherParam,
params.DefaultParam,
]
def __init__(self):
"""实例化 Matcher 以便运行"""
self.handlers = self.handlers.copy()
self.state = self._default_state.copy()
def __repr__(self) -> str:
return (
f"<Matcher from {self.module_name or 'unknown'}, type={self.type}, "
f"priority={self.priority}, temp={self.temp}>"
)
def __str__(self) -> str:
return repr(self)
@classmethod
def new(
cls,
type_: str = "",
rule: Optional[Rule] = None,
permission: Optional[Permission] = None,
handlers: Optional[List[Union[T_Handler, Dependent[Any]]]] = None,
temp: bool = False,
priority: int = 1,
block: bool = False,
*,
plugin: Optional["Plugin"] = None,
module: Optional[ModuleType] = None,
expire_time: Optional[datetime] = None,
default_state: Optional[T_State] = None,
default_type_updater: Optional[T_TypeUpdater] = None,
default_permission_updater: Optional[T_PermissionUpdater] = None,
) -> Type["Matcher"]:
"""
:说明:
创建一个新的事件响应器并存储至 `matchers <#matchers>`_
:参数:
* ``type_: str``: 事件响应器类型 ``event.get_type()`` 一致时触发空字符串表示任意
* ``rule: Optional[Rule]``: 匹配规则
* ``permission: Optional[Permission]``: 权限
* ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器即触发一次后删除
* ``priority: int``: 响应优先级
* ``block: bool``: 是否阻止事件向更低优先级的响应器传播
* ``plugin: Optional[Plugin]``: 事件响应器所在插件
* ``module: Optional[ModuleType]``: 事件响应器所在模块
* ``default_state: Optional[T_State]``: 默认状态 ``state``
* ``expire_time: Optional[datetime]``: 事件响应器最终有效时间点过时即被删除
:返回:
- ``Type[Matcher]``: 新的事件响应器类
"""
NewMatcher = type(
"Matcher",
(Matcher,),
{
"plugin": plugin,
"module": module,
"plugin_name": plugin and plugin.name,
"module_name": module and module.__name__,
"type": type_,
"rule": rule or Rule(),
"permission": permission or Permission(),
"handlers": [
handler
if isinstance(handler, Dependent)
else Dependent[Any].parse(
call=handler, allow_types=cls.HANDLER_PARAM_TYPES
)
for handler in handlers
]
if handlers
else [],
"temp": temp,
"expire_time": expire_time,
"priority": priority,
"block": block,
"_default_state": default_state or {},
"_default_type_updater": default_type_updater,
"_default_permission_updater": default_permission_updater,
},
)
logger.trace(f"Define new matcher {NewMatcher}")
matchers[priority].append(NewMatcher)
return NewMatcher
@classmethod
async def check_perm(
cls,
bot: Bot,
event: Event,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""
:说明:
检查是否满足触发权限
:参数:
* ``bot: Bot``: Bot 对象
* ``event: Event``: 上报事件
:返回:
- ``bool``: 是否满足权限
"""
event_type = event.get_type()
return event_type == (cls.type or event_type) and await cls.permission(
bot, event, stack, dependency_cache
)
@classmethod
async def check_rule(
cls,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""
:说明:
检查是否满足匹配规则
:参数:
* ``bot: Bot``: Bot 对象
* ``event: Event``: 上报事件
* ``state: T_State``: 当前状态
:返回:
- ``bool``: 是否满足匹配规则
"""
event_type = event.get_type()
return event_type == (cls.type or event_type) and await cls.rule(
bot, event, state, stack, dependency_cache
)
@classmethod
def type_updater(cls, func: T_TypeUpdater) -> T_TypeUpdater:
"""
:说明:
装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数
:参数:
* ``func: T_TypeUpdater``: 响应事件类型更新函数
"""
cls._default_type_updater = Dependent[str].parse(
call=func, allow_types=cls.HANDLER_PARAM_TYPES
)
return func
@classmethod
def permission_updater(cls, func: T_PermissionUpdater) -> T_PermissionUpdater:
"""
:说明:
装饰一个函数来更改当前事件响应器的默认会话权限更新函数
:参数:
* ``func: T_PermissionUpdater``: 会话权限更新函数
"""
cls._default_permission_updater = Dependent[Permission].parse(
call=func, allow_types=cls.HANDLER_PARAM_TYPES
)
return func
@classmethod
def append_handler(
cls, handler: T_Handler, parameterless: Optional[List[Any]] = None
) -> Dependent[Any]:
handler_ = Dependent[Any].parse(
call=handler,
parameterless=parameterless,
allow_types=cls.HANDLER_PARAM_TYPES,
)
cls.handlers.append(handler_)
return handler_
@classmethod
def handle(
cls, parameterless: Optional[List[Any]] = None
) -> Callable[[T_Handler], T_Handler]:
"""
:说明:
装饰一个函数来向事件响应器直接添加一个处理函数
:参数:
* ``parameterless: Optional[List[Any]]``: 非参数类型依赖列表
"""
def _decorator(func: T_Handler) -> T_Handler:
cls.append_handler(func, parameterless=parameterless)
return func
return _decorator
@classmethod
def receive(
cls, id: str = "", parameterless: Optional[List[Any]] = None
) -> Callable[[T_Handler], T_Handler]:
"""
:说明:
装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数
:参数:
* ``id: str``: 消息 ID
* ``parameterless: Optional[List[Any]]``: 非参数类型依赖列表
"""
async def _receive(event: Event, matcher: "Matcher") -> Union[None, NoReturn]:
matcher.set_target(RECEIVE_KEY.format(id=id))
if matcher.get_target() == RECEIVE_KEY.format(id=id):
matcher.set_receive(id, event)
return
if matcher.get_receive(id):
return
await matcher.reject()
_parameterless = [params.Depends(_receive), *(parameterless or [])]
def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func:
func_handler = cls.handlers[-1]
for depend in reversed(_parameterless):
func_handler.prepend_parameterless(depend)
else:
cls.append_handler(func, parameterless=_parameterless)
return func
return _decorator
@classmethod
def got(
cls,
key: str,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
parameterless: Optional[List[Any]] = None,
) -> Callable[[T_Handler], T_Handler]:
"""
:说明:
装饰一个函数来指示 NoneBot 当要获取的 ``key`` 不存在时接收用户新的一条消息并经过 ``ArgsParser`` 处理后再运行该函数如果 ``key`` 已存在则直接继续运行
:参数:
* ``key: str``: 参数名
* ``prompt: Optional[Union[str, Message, MessageSegment, MessageFormatter]]``: 在参数不存在时向用户发送的消息
* ``args_parser: Optional[T_ArgsParser]``: 可选参数解析函数空则使用默认解析函数
* ``parameterless: Optional[List[Any]]``: 非参数类型依赖列表
"""
async def _key_getter(event: Event, matcher: "Matcher"):
matcher.set_target(ARG_KEY.format(key=key))
if matcher.get_target() == ARG_KEY.format(key=key):
matcher.set_arg(key, event.get_message())
return
if matcher.get_arg(key):
return
await matcher.reject(prompt)
_parameterless = [
params.Depends(_key_getter),
*(parameterless or []),
]
def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func:
func_handler = cls.handlers[-1]
for depend in reversed(_parameterless):
func_handler.prepend_parameterless(depend)
else:
cls.append_handler(func, parameterless=_parameterless)
return func
return _decorator
@classmethod
async def send(
cls,
message: Union[str, Message, MessageSegment, MessageTemplate],
**kwargs: Any,
) -> Any:
"""
:说明:
发送一条消息给当前交互用户
:参数:
* ``message: Union[str, Message, MessageSegment]``: 消息内容
* ``**kwargs``: 其他传递给 ``bot.send`` 的参数请参考对应 adapter bot 对象 api
"""
bot = current_bot.get()
event = current_event.get()
state = current_matcher.get().state
if isinstance(message, MessageTemplate):
_message = message.format(**state)
else:
_message = message
return await bot.send(event=event, message=_message, **kwargs)
@classmethod
async def finish(
cls,
message: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""
:说明:
发送一条消息给当前交互用户并结束当前事件响应器
:参数:
* ``message: Union[str, Message, MessageSegment, MessageTemplate]``: 消息内容
* ``**kwargs``: 其他传递给 ``bot.send`` 的参数请参考对应 adapter bot 对象 api
"""
if message is not None:
await cls.send(message, **kwargs)
raise FinishedException
@classmethod
async def pause(
cls,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""
:说明:
发送一条消息给当前交互用户并暂停事件响应器在接收用户新的一条消息后继续下一个处理函数
:参数:
* ``prompt: Union[str, Message, MessageSegment, MessageTemplate]``: 消息内容
* ``**kwargs``: 其他传递给 ``bot.send`` 的参数请参考对应 adapter bot 对象 api
"""
if prompt is not None:
await cls.send(prompt, **kwargs)
raise PausedException
@classmethod
async def reject(
cls,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""
:说明:
最近使用 ``got`` / ``receive`` 接收的消息不符合预期发送一条消息给当前交互用户并暂停事件响应器
在接收用户新的一条消息后继续当前处理函数
:参数:
* ``prompt: Union[str, Message, MessageSegment, MessageTemplate]``: 消息内容
* ``**kwargs``: 其他传递给 ``bot.send`` 的参数请参考对应 adapter bot 对象 api
"""
if prompt is not None:
await cls.send(prompt, **kwargs)
raise RejectedException
@classmethod
async def reject_arg(
cls,
key: str,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""
:说明:
最近使用 ``got`` 接收的消息不符合预期发送一条消息给当前交互用户并暂停事件响应器
在接收用户新的一条消息后继续当前处理函数
:参数:
* ``key: str``: 参数名
* ``prompt: Union[str, Message, MessageSegment, MessageTemplate]``: 消息内容
* ``**kwargs``: 其他传递给 ``bot.send`` 的参数请参考对应 adapter bot 对象 api
"""
matcher = current_matcher.get()
matcher.set_target(ARG_KEY.format(key=key))
if prompt is not None:
await cls.send(prompt, **kwargs)
raise RejectedException
@classmethod
async def reject_receive(
cls,
id: str = "",
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""
:说明:
最近使用 ``got`` 接收的消息不符合预期发送一条消息给当前交互用户并暂停事件响应器
在接收用户新的一条消息后继续当前处理函数
:参数:
* ``id: str``: 消息 id
* ``prompt: Union[str, Message, MessageSegment, MessageTemplate]``: 消息内容
* ``**kwargs``: 其他传递给 ``bot.send`` 的参数请参考对应 adapter bot 对象 api
"""
matcher = current_matcher.get()
matcher.set_target(RECEIVE_KEY.format(id=id))
if prompt is not None:
await cls.send(prompt, **kwargs)
raise RejectedException
@classmethod
def skip(cls) -> NoReturn:
raise SkippedException
def get_receive(self, id: str, default: T = None) -> Union[Event, T]:
return self.state.get(RECEIVE_KEY.format(id=id), default)
def set_receive(self, id: str, event: Event) -> None:
self.state[RECEIVE_KEY.format(id=id)] = event
self.state[LAST_RECEIVE_KEY] = event
def get_last_receive(self, default: T = None) -> Union[Event, T]:
return self.state.get(LAST_RECEIVE_KEY, default)
def get_arg(self, key: str, default: T = None) -> Union[Message, T]:
return self.state.get(ARG_KEY.format(key=key), default)
def set_arg(self, key: str, message: Message) -> None:
self.state[ARG_KEY.format(key=key)] = message
def set_target(self, target: str, cache: bool = True) -> None:
if cache:
self.state[REJECT_CACHE_TARGET] = target
else:
self.state[REJECT_TARGET] = target
def get_target(self, default: T = None) -> Union[str, T]:
return self.state.get(REJECT_TARGET, default)
def stop_propagation(self):
"""
:说明:
阻止事件传播
"""
self.block = True
async def update_type(self, bot: Bot, event: Event) -> str:
updater = self.__class__._default_type_updater
if not updater:
return "message"
return await updater(bot=bot, event=event, state=self.state, matcher=self)
async def update_permission(self, bot: Bot, event: Event) -> Permission:
updater = self.__class__._default_permission_updater
if not updater:
return USER(event.get_session_id(), perm=self.permission)
return await updater(bot=bot, event=event, state=self.state, matcher=self)
async def resolve_reject(self):
handler = current_handler.get()
self.handlers.insert(0, handler)
if REJECT_CACHE_TARGET in self.state:
self.state[REJECT_TARGET] = self.state[REJECT_CACHE_TARGET]
async def simple_run(
self,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
):
logger.trace(
f"Matcher {self} run with incoming args: "
f"bot={bot}, event={event}, state={state}"
)
b_t = current_bot.set(bot)
e_t = current_event.set(event)
m_t = current_matcher.set(self)
try:
# Refresh preprocess state
self.state.update(state)
while self.handlers:
handler = self.handlers.pop(0)
current_handler.set(handler)
logger.debug(f"Running handler {handler}")
try:
await handler(
matcher=self,
bot=bot,
event=event,
state=self.state,
stack=stack,
dependency_cache=dependency_cache,
)
except TypeMisMatch as e:
logger.debug(
f"Handler {handler} param {e.param.name} value {e.value} "
f"mismatch type {e.param._type_display()}, skipped"
)
except SkippedException as e:
logger.debug(f"Handler {handler} skipped")
except StopPropagation:
self.block = True
finally:
logger.info(f"Matcher {self} running complete")
current_bot.reset(b_t)
current_event.reset(e_t)
current_matcher.reset(m_t)
# 运行handlers
async def run(
self,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
):
try:
await self.simple_run(bot, event, state, stack, dependency_cache)
except RejectedException:
await self.resolve_reject()
type_ = await self.update_type(bot, event)
permission = await self.update_permission(bot, event)
Matcher.new(
type_,
Rule(),
permission,
self.handlers,
temp=True,
priority=0,
block=True,
plugin=self.plugin,
module=self.module,
expire_time=datetime.now() + bot.config.session_expire_timeout,
default_state=self.state,
default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater,
)
except PausedException:
type_ = await self.update_type(bot, event)
permission = await self.update_permission(bot, event)
Matcher.new(
type_,
Rule(),
permission,
self.handlers,
temp=True,
priority=0,
block=True,
plugin=self.plugin,
module=self.module,
expire_time=datetime.now() + bot.config.session_expire_timeout,
default_state=self.state,
default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater,
)
except FinishedException:
pass
from nonebot.internal.matcher import Matcher as Matcher
from nonebot.internal.matcher import matchers as matchers
from nonebot.internal.matcher import current_bot as current_bot
from nonebot.internal.matcher import current_event as current_event
from nonebot.internal.matcher import current_handler as current_handler
from nonebot.internal.matcher import current_matcher as current_matcher
__autodoc__ = {
"Matcher": True,
"matchers": True,
}

View File

@ -1,8 +1,10 @@
"""
事件处理
========
"""本模块定义了事件处理主要流程。
NoneBot 内部处理并按优先级分发事件给所有事件响应器提供了多个插槽以进行事件的预处理等
FrontMatter:
sidebar_position: 2
description: nonebot.message 模块
"""
import asyncio
@ -10,7 +12,6 @@ from datetime import datetime
from contextlib import AsyncExitStack
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional, Coroutine
from nonebot import params
from nonebot.log import logger
from nonebot.rule import TrieRule
from nonebot.utils import escape_tag
@ -30,87 +31,81 @@ from nonebot.typing import (
T_EventPreProcessor,
T_EventPostProcessor,
)
from nonebot.internal.params import (
ArgParam,
BotParam,
EventParam,
StateParam,
DependParam,
DefaultParam,
MatcherParam,
ExceptionParam,
)
if TYPE_CHECKING:
from nonebot.adapters import Bot, Event
_event_preprocessors: Set[Dependent[None]] = set()
_event_postprocessors: Set[Dependent[None]] = set()
_run_preprocessors: Set[Dependent[None]] = set()
_run_postprocessors: Set[Dependent[None]] = set()
_event_preprocessors: Set[Dependent[Any]] = set()
_event_postprocessors: Set[Dependent[Any]] = set()
_run_preprocessors: Set[Dependent[Any]] = set()
_run_postprocessors: Set[Dependent[Any]] = set()
EVENT_PCS_PARAMS = [
params.DependParam,
params.BotParam,
params.EventParam,
params.StateParam,
params.DefaultParam,
DependParam,
BotParam,
EventParam,
StateParam,
DefaultParam,
]
RUN_PREPCS_PARAMS = [
params.DependParam,
params.BotParam,
params.EventParam,
params.StateParam,
params.ArgParam,
params.MatcherParam,
params.DefaultParam,
DependParam,
BotParam,
EventParam,
StateParam,
ArgParam,
MatcherParam,
DefaultParam,
]
RUN_POSTPCS_PARAMS = [
params.DependParam,
params.ExceptionParam,
params.BotParam,
params.EventParam,
params.StateParam,
params.ArgParam,
params.MatcherParam,
params.DefaultParam,
DependParam,
ExceptionParam,
BotParam,
EventParam,
StateParam,
ArgParam,
MatcherParam,
DefaultParam,
]
def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor:
"""
:说明:
事件预处理装饰一个函数使它在每次接收到事件并分发给各响应器之前执行
"""
"""事件预处理。装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。"""
_event_preprocessors.add(
Dependent[None].parse(call=func, allow_types=EVENT_PCS_PARAMS)
Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS)
)
return func
def event_postprocessor(func: T_EventPostProcessor) -> T_EventPostProcessor:
"""
:说明:
事件后处理装饰一个函数使它在每次接收到事件并分发给各响应器之后执行
"""
"""事件后处理。装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。"""
_event_postprocessors.add(
Dependent[None].parse(call=func, allow_types=EVENT_PCS_PARAMS)
Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS)
)
return func
def run_preprocessor(func: T_RunPreProcessor) -> T_RunPreProcessor:
"""
:说明:
运行预处理装饰一个函数使它在每次事件响应器运行前执行
"""
"""运行预处理。装饰一个函数,使它在每次事件响应器运行前执行。"""
_run_preprocessors.add(
Dependent[None].parse(call=func, allow_types=RUN_PREPCS_PARAMS)
Dependent[Any].parse(call=func, allow_types=RUN_PREPCS_PARAMS)
)
return func
def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
"""
:说明:
运行后处理装饰一个函数使它在每次事件响应器运行后执行
"""
"""运行后处理。装饰一个函数,使它在每次事件响应器运行后执行。"""
_run_postprocessors.add(
Dependent[None].parse(call=func, allow_types=RUN_POSTPCS_PARAMS)
Dependent[Any].parse(call=func, allow_types=RUN_POSTPCS_PARAMS)
)
return func
@ -241,22 +236,17 @@ async def _run_matcher(
async def handle_event(bot: "Bot", event: "Event") -> None:
"""
:说明:
"""处理一个事件。调用该函数以实现分发事件。
处理一个事件调用该函数以实现分发事件
:参数:
* ``bot: Bot``: Bot 对象
* ``event: Event``: Event 对象
:示例:
.. code-block:: python
参数:
bot: Bot 对象
event: Event 对象
用法:
```python
import asyncio
asyncio.create_task(handle_event(bot, event))
```
"""
show_log = True
log_msg = f"<m>{escape_tag(bot.type.upper())} {escape_tag(bot.self_id)}</m> | "

View File

@ -1,16 +1,28 @@
import asyncio
import inspect
from typing_extensions import Literal
from typing import Any, Dict, List, Tuple, Callable, Optional, cast
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
"""本模块定义了依赖注入的各类参数。
from pydantic.fields import Required, Undefined, ModelField
FrontMatter:
sidebar_position: 4
description: nonebot.params 模块
"""
from nonebot.log import logger
from nonebot.exception import TypeMisMatch
from nonebot.adapters import Bot, Event, Message
from nonebot.dependencies import Param, Dependent, CustomConfig
from nonebot.typing import T_State, T_Handler, T_DependencyCache
from typing import Any, Dict, List, Tuple, Optional
from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.adapters import Event, Message
from nonebot.internal.params import Arg as Arg
from nonebot.internal.params import State as State
from nonebot.internal.params import ArgStr as ArgStr
from nonebot.internal.params import Depends as Depends
from nonebot.internal.params import ArgParam as ArgParam
from nonebot.internal.params import BotParam as BotParam
from nonebot.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 (
CMD_KEY,
PREFIX_KEY,
@ -22,228 +34,6 @@ from nonebot.consts import (
REGEX_GROUP,
REGEX_MATCHED,
)
from nonebot.utils import (
get_name,
run_sync,
is_gen_callable,
run_sync_ctx_manager,
is_async_gen_callable,
is_coroutine_callable,
generic_check_issubclass,
)
class DependsInner:
def __init__(
self,
dependency: Optional[T_Handler] = None,
*,
use_cache: bool = True,
) -> None:
self.dependency = dependency
self.use_cache = use_cache
def __repr__(self) -> str:
dep = get_name(self.dependency)
cache = "" if self.use_cache else ", use_cache=False"
return f"{self.__class__.__name__}({dep}{cache})"
def Depends(
dependency: Optional[T_Handler] = None,
*,
use_cache: bool = True,
) -> Any:
"""
:说明:
参数依赖注入装饰器
:参数:
* ``dependency: Optional[Callable[..., Any]] = None``: 依赖函数默认为参数的类型注释
* ``use_cache: bool = True``: 是否使用缓存默认为 ``True``
.. code-block:: python
def depend_func() -> Any:
return ...
def depend_gen_func():
try:
yield ...
finally:
...
async def handler(param_name: Any = Depends(depend_func), gen: Any = Depends(depend_gen_func)):
...
"""
return DependsInner(dependency, use_cache=use_cache)
class DependParam(Param):
@classmethod
def _check_param(
cls,
dependent: Dependent,
name: str,
param: inspect.Parameter,
) -> Optional["DependParam"]:
if isinstance(param.default, DependsInner):
dependency: T_Handler
if param.default.dependency is None:
assert param.annotation is not param.empty, "Dependency cannot be empty"
dependency = param.annotation
else:
dependency = param.default.dependency
sub_dependent = Dependent[Any].parse(
call=dependency,
allow_types=dependent.allow_types,
)
dependent.pre_checkers.extend(sub_dependent.pre_checkers)
sub_dependent.pre_checkers.clear()
return cls(
Required, use_cache=param.default.use_cache, dependent=sub_dependent
)
@classmethod
def _check_parameterless(
cls, dependent: "Dependent", value: Any
) -> Optional["Param"]:
if isinstance(value, DependsInner):
assert value.dependency, "Dependency cannot be empty"
dependent = Dependent[Any].parse(
call=value.dependency, allow_types=dependent.allow_types
)
return cls(Required, use_cache=value.use_cache, dependent=dependent)
async def _solve(
self,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
**kwargs: Any,
) -> Any:
use_cache: bool = self.extra["use_cache"]
dependency_cache = {} if dependency_cache is None else dependency_cache
sub_dependent: Dependent = self.extra["dependent"]
sub_dependent.call = cast(Callable[..., Any], sub_dependent.call)
call = sub_dependent.call
# solve sub dependency with current cache
sub_values = await sub_dependent.solve(
stack=stack,
dependency_cache=dependency_cache,
**kwargs,
)
# run dependency function
task: asyncio.Task[Any]
if use_cache and call in dependency_cache:
solved = await dependency_cache[call]
elif is_gen_callable(call) or is_async_gen_callable(call):
assert isinstance(
stack, AsyncExitStack
), "Generator dependency should be called in context"
if is_gen_callable(call):
cm = run_sync_ctx_manager(contextmanager(call)(**sub_values))
else:
cm = asynccontextmanager(call)(**sub_values)
task = asyncio.create_task(stack.enter_async_context(cm))
dependency_cache[call] = task
solved = await task
elif is_coroutine_callable(call):
task = asyncio.create_task(call(**sub_values))
dependency_cache[call] = task
solved = await task
else:
task = asyncio.create_task(run_sync(call)(**sub_values))
dependency_cache[call] = task
solved = await task
return solved
class _BotChecker(Param):
async def _solve(self, bot: Bot, **kwargs: Any) -> Any:
field: ModelField = self.extra["field"]
_, errs_ = field.validate(bot, {}, loc=("bot",))
if errs_:
logger.debug(
f"Bot type {type(bot)} not match "
f"annotation {field._type_display()}, ignored"
)
raise TypeMisMatch(field, bot)
class BotParam(Param):
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["BotParam"]:
if param.default == param.empty:
if generic_check_issubclass(param.annotation, Bot):
if param.annotation is not Bot:
dependent.pre_checkers.append(
_BotChecker(
Required,
field=ModelField(
name=name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
),
)
)
return cls(Required)
elif param.annotation == param.empty and name == "bot":
return cls(Required)
async def _solve(self, bot: Bot, **kwargs: Any) -> Any:
return bot
class _EventChecker(Param):
async def _solve(self, event: Event, **kwargs: Any) -> Any:
field: ModelField = self.extra["field"]
_, errs_ = field.validate(event, {}, loc=("event",))
if errs_:
logger.debug(
f"Event type {type(event)} not match "
f"annotation {field._type_display()}, ignored"
)
raise TypeMisMatch(field, event)
class EventParam(Param):
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["EventParam"]:
if param.default == param.empty:
if generic_check_issubclass(param.annotation, Event):
if param.annotation is not Event:
dependent.pre_checkers.append(
_EventChecker(
Required,
field=ModelField(
name=name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
),
)
)
return cls(Required)
elif param.annotation == param.empty and name == "event":
return cls(Required)
async def _solve(self, event: Event, **kwargs: Any) -> Any:
return event
async def _event_type(event: Event) -> str:
@ -251,6 +41,7 @@ async def _event_type(event: Event) -> str:
def EventType() -> str:
"""{ref}`nonebot.adapters.Event` 类型参数"""
return Depends(_event_type)
@ -259,6 +50,7 @@ async def _event_message(event: Event) -> Message:
def EventMessage() -> Any:
"""{ref}`nonebot.adapters.Event` 消息参数"""
return Depends(_event_message)
@ -267,6 +59,7 @@ async def _event_plain_text(event: Event) -> str:
def EventPlainText() -> str:
"""{ref}`nonebot.adapters.Event` 纯文本消息参数"""
return Depends(_event_plain_text)
@ -275,108 +68,85 @@ async def _event_to_me(event: Event) -> bool:
def EventToMe() -> bool:
"""{ref}`nonebot.adapters.Event` `to_me` 参数"""
return Depends(_event_to_me)
class StateInner:
...
def State() -> T_State:
return StateInner() # type: ignore
class StateParam(Param):
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["StateParam"]:
if isinstance(param.default, StateInner):
return cls(Required)
async def _solve(self, state: T_State, **kwargs: Any) -> Any:
return state
def _command(state=State()) -> Message:
def _command(state: T_State) -> Message:
return state[PREFIX_KEY][CMD_KEY]
def Command() -> Tuple[str, ...]:
return Depends(_command, use_cache=False)
"""消息命令元组"""
return Depends(_command)
def _raw_command(state=State()) -> Message:
def _raw_command(state: T_State) -> Message:
return state[PREFIX_KEY][RAW_CMD_KEY]
def RawCommand() -> str:
return Depends(_raw_command, use_cache=False)
"""消息命令文本"""
return Depends(_raw_command)
def _command_arg(state=State()) -> Message:
def _command_arg(state: T_State) -> Message:
return state[PREFIX_KEY][CMD_ARG_KEY]
def CommandArg() -> Any:
return Depends(_command_arg, use_cache=False)
"""消息命令参数"""
return Depends(_command_arg)
def _shell_command_args(state=State()) -> Any:
def _shell_command_args(state: T_State) -> Any:
return state[SHELL_ARGS]
def ShellCommandArgs():
"""shell 命令解析后的参数字典"""
return Depends(_shell_command_args, use_cache=False)
def _shell_command_argv(state=State()) -> List[str]:
def _shell_command_argv(state: T_State) -> List[str]:
return state[SHELL_ARGV]
def ShellCommandArgv() -> Any:
"""shell 命令原始参数列表"""
return Depends(_shell_command_argv, use_cache=False)
def _regex_matched(state=State()) -> str:
def _regex_matched(state: T_State) -> str:
return state[REGEX_MATCHED]
def RegexMatched() -> str:
"""正则匹配结果"""
return Depends(_regex_matched, use_cache=False)
def _regex_group(state=State()):
def _regex_group(state: T_State):
return state[REGEX_GROUP]
def RegexGroup() -> Tuple[Any, ...]:
"""正则匹配结果 group 元组"""
return Depends(_regex_group, use_cache=False)
def _regex_dict(state=State()):
def _regex_dict(state: T_State):
return state[REGEX_DICT]
def RegexDict() -> Dict[str, Any]:
"""正则匹配结果 group 字典"""
return Depends(_regex_dict, use_cache=False)
class MatcherParam(Param):
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["MatcherParam"]:
if generic_check_issubclass(param.annotation, Matcher) or (
param.annotation == param.empty and name == "matcher"
):
return cls(Required)
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
return matcher
def Received(id: Optional[str] = None, default: Any = None) -> Any:
"""`receive` 事件参数"""
def _received(matcher: "Matcher"):
return matcher.get_receive(id or "", default)
@ -384,76 +154,26 @@ def Received(id: Optional[str] = None, default: Any = None) -> Any:
def LastReceived(default: Any = None) -> Any:
"""`last_receive` 事件参数"""
def _last_received(matcher: "Matcher") -> Any:
return matcher.get_last_receive(default)
return Depends(_last_received, use_cache=False)
class ArgInner:
def __init__(
self, key: Optional[str], type: Literal["message", "str", "plaintext"]
) -> None:
self.key = key
self.type = type
def Arg(key: Optional[str] = None) -> Any:
return ArgInner(key, "message")
def ArgStr(key: Optional[str] = None) -> str:
return ArgInner(key, "str") # type: ignore
def ArgPlainText(key: Optional[str] = None) -> str:
return ArgInner(key, "plaintext") # type: ignore
class ArgParam(Param):
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["ArgParam"]:
if isinstance(param.default, ArgInner):
return cls(Required, key=param.default.key or name, type=param.default.type)
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
message = matcher.get_arg(self.extra["key"])
if message is None:
return message
if self.extra["type"] == "message":
return message
elif self.extra["type"] == "str":
return str(message)
else:
return message.extract_plain_text()
class ExceptionParam(Param):
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["ExceptionParam"]:
if generic_check_issubclass(param.annotation, Exception) or (
param.annotation == param.empty and name == "exception"
):
return cls(Required)
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
return exception
class DefaultParam(Param):
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["DefaultParam"]:
if param.default != param.empty:
return cls(param.default)
async def _solve(self, **kwargs: Any) -> Any:
return Undefined
from nonebot.matcher import Matcher
__autodoc__ = {
"Arg": True,
"State": True,
"ArgStr": True,
"Depends": True,
"ArgParam": True,
"BotParam": True,
"EventParam": True,
"StateParam": True,
"DependParam": True,
"ArgPlainText": True,
"DefaultParam": True,
"MatcherParam": True,
"ExceptionParam": True,
}

View File

@ -1,210 +1,83 @@
r"""
权限
====
"""本模块是 {ref}`nonebot.matcher.Matcher.permission` 的类型定义。
每个 ``Matcher`` 拥有一个 ``Permission`` 其中是 ``PermissionChecker`` 的集合只要有一个 ``PermissionChecker`` 检查结果为 ``True`` 时就会继续运行
每个 {ref}`nonebot.matcher.Matcher` 拥有一个 {ref}`nonebot.permission.Permission`
其中是 `PermissionChecker` 的集合只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行
\:\:\:tip 提示
``PermissionChecker`` 既可以是 async function 也可以是 sync function
\:\:\:
FrontMatter:
sidebar_position: 6
description: nonebot.permission 模块
"""
import asyncio
from contextlib import AsyncExitStack
from typing import Any, Set, Tuple, Union, NoReturn, Optional, Coroutine
from nonebot.params import EventType
from nonebot.adapters import Bot, Event
from nonebot.dependencies import Dependent
from nonebot.exception import SkippedException
from nonebot.typing import T_Handler, T_DependencyCache, T_PermissionChecker
from nonebot.params import (
BotParam,
EventType,
EventParam,
DependParam,
DefaultParam,
)
async def _run_coro_with_catch(coro: Coroutine[Any, Any, Any]):
try:
return await coro
except SkippedException:
return False
class Permission:
"""
:说明:
``Matcher`` 规则类当事件传递时 ``Matcher`` 运行前进行检查
:示例:
.. code-block:: python
Permission(async_function) | sync_function
# 等价于
from nonebot.utils import run_sync
Permission(async_function, run_sync(sync_function))
"""
__slots__ = ("checkers",)
HANDLER_PARAM_TYPES = [
DependParam,
BotParam,
EventParam,
DefaultParam,
]
def __init__(self, *checkers: Union[T_PermissionChecker, Dependent[bool]]) -> None:
"""
:参数:
* ``*checkers: Union[T_PermissionChecker, Dependent[bool]``: PermissionChecker
"""
self.checkers: Set[Dependent[bool]] = set(
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
)
for checker in checkers
)
"""
:说明:
存储 ``PermissionChecker``
:类型:
* ``Set[Dependent[bool]]``
"""
async def __call__(
self,
bot: Bot,
event: Event,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""
:说明:
检查是否满足某个权限
:参数:
* ``bot: Bot``: Bot 对象
* ``event: Event``: Event 对象
* ``stack: Optional[AsyncExitStack]``: 异步上下文栈
* ``dependency_cache: Optional[CacheDict[T_Handler, Any]]``: 依赖缓存
:返回:
- ``bool``
"""
if not self.checkers:
return True
results = await asyncio.gather(
*(
_run_coro_with_catch(
checker(
bot=bot,
event=event,
stack=stack,
dependency_cache=dependency_cache,
)
)
for checker in self.checkers
),
)
return any(results)
def __and__(self, other) -> NoReturn:
raise RuntimeError("And operation between Permissions is not allowed.")
def __or__(
self, other: Optional[Union["Permission", T_PermissionChecker]]
) -> "Permission":
if other is None:
return self
elif isinstance(other, Permission):
return Permission(*self.checkers, *other.checkers)
else:
return Permission(*self.checkers, other)
from nonebot.internal.permission import USER as USER
from nonebot.internal.permission import User as User
from nonebot.internal.permission import Permission as Permission
class Message:
"""检查是否为消息事件"""
__slots__ = ()
async def __call__(self, type: str = EventType()) -> bool:
return type == "message"
class Notice:
"""检查是否为通知事件"""
__slots__ = ()
async def __call__(self, type: str = EventType()) -> bool:
return type == "notice"
class Request:
"""检查是否为请求事件"""
__slots__ = ()
async def __call__(self, type: str = EventType()) -> bool:
return type == "request"
class MetaEvent:
"""检查是否为元事件"""
__slots__ = ()
async def __call__(self, type: str = EventType()) -> bool:
return type == "meta_event"
MESSAGE = Permission(Message())
MESSAGE: Permission = Permission(Message())
"""匹配任意 `message` 类型事件
仅在需要同时捕获不同类型事件时使用优先使用 message type Matcher
"""
- **说明**: 匹配任意 ``message`` 类型事件仅在需要同时捕获不同类型事件时使用优先使用 message type Matcher
NOTICE: Permission = Permission(Notice())
"""匹配任意 `notice` 类型事件
仅在需要同时捕获不同类型事件时使用优先使用 notice type Matcher
"""
NOTICE = Permission(Notice())
REQUEST: Permission = Permission(Request())
"""匹配任意 `request` 类型事件
仅在需要同时捕获不同类型事件时使用优先使用 request type Matcher
"""
- **说明**: 匹配任意 ``notice`` 类型事件仅在需要同时捕获不同类型事件时使用优先使用 notice type Matcher
METAEVENT: Permission = Permission(MetaEvent())
"""匹配任意 `meta_event` 类型事件
仅在需要同时捕获不同类型事件时使用优先使用 meta_event type Matcher
"""
REQUEST = Permission(Request())
"""
- **说明**: 匹配任意 ``request`` 类型事件仅在需要同时捕获不同类型事件时使用优先使用 request type Matcher
"""
METAEVENT = Permission(MetaEvent())
"""
- **说明**: 匹配任意 ``meta_event`` 类型事件仅在需要同时捕获不同类型事件时使用优先使用 meta_event type Matcher
"""
class User:
def __init__(
self, users: Tuple[str, ...], perm: Optional[Permission] = None
) -> None:
self.users = users
self.perm = perm
async def __call__(self, bot: Bot, event: Event) -> bool:
return bool(
event.get_session_id() in self.users
and (self.perm is None or await self.perm(bot, event))
)
def USER(*users: str, perm: Optional[Permission] = None):
"""
:说明:
``event`` ``session_id`` 在白名单内且满足 perm
:参数:
* ``*user: str``: 白名单
* ``perm: Optional[Permission]``: 需要同时满足的权限
"""
return Permission(User(users, perm))
class SuperUser:
"""检查当前事件是否是消息事件且属于超级管理员"""
__slots__ = ()
async def __call__(self, bot: Bot, event: Event) -> bool:
return event.get_type() == "message" and (
f"{bot.adapter.get_name().split(maxsplit=1)[0].lower()}:{event.get_user_id()}"
@ -213,7 +86,12 @@ class SuperUser:
)
SUPERUSER = Permission(SuperUser())
"""
- **说明**: 匹配任意超级用户消息类型事件
"""
SUPERUSER: Permission = Permission(SuperUser())
"""匹配任意超级用户消息类型事件"""
__autodoc__ = {
"Permission": True,
"Permission.__call__": True,
"User": True,
"USER": True,
}

View File

@ -1,8 +1,37 @@
"""
插件
====
"""本模块为 NoneBot 插件开发提供便携的定义函数。
NoneBot 插件开发提供便携的定义函数
## 快捷导入
为方便使用本模块从子模块导入了部分内容以下内容可以直接通过本模块导入:
- `on` => {ref}``on` <nonebot.plugin.on.on>`
- `on_metaevent` => {ref}``on_metaevent` <nonebot.plugin.on.on_metaevent>`
- `on_message` => {ref}``on_message` <nonebot.plugin.on.on_message>`
- `on_notice` => {ref}``on_notice` <nonebot.plugin.on.on_notice>`
- `on_request` => {ref}``on_request` <nonebot.plugin.on.on_request>`
- `on_startswith` => {ref}``on_startswith` <nonebot.plugin.on.on_startswith>`
- `on_endswith` => {ref}``on_endswith` <nonebot.plugin.on.on_endswith>`
- `on_keyword` => {ref}``on_keyword` <nonebot.plugin.on.on_keyword>`
- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`
- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`
- `on_regex` => {ref}``on_regex` <nonebot.plugin.on.on_regex>`
- `CommandGroup` => {ref}``CommandGroup` <nonebot.plugin.on.CommandGroup>`
- `Matchergroup` => {ref}``MatcherGroup` <nonebot.plugin.on.MatcherGroup>`
- `load_plugin` => {ref}``load_plugin` <nonebot.plugin.load.load_plugin>`
- `load_plugins` => {ref}``load_plugins` <nonebot.plugin.load.load_plugins>`
- `load_all_plugins` => {ref}``load_all_plugins` <nonebot.plugin.load.load_all_plugins>`
- `load_from_json` => {ref}``load_from_json` <nonebot.plugin.load.load_from_json>`
- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`
- `load_builtin_plugin` => {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`
- `load_builtin_plugins` => {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
- `get_plugin` => {ref}``get_plugin` <nonebot.plugin.plugin.get_plugin>`
- `get_loaded_plugins` => {ref}``get_loaded_plugins` <nonebot.plugin.plugin.get_loaded_plugins>`
- `export` => {ref}``export` <nonebot.plugin.export.export>`
- `require` => {ref}``require` <nonebot.plugin.load.require>`
FrontMatter:
sidebar_position: 0
description: nonebot.plugin 模块
"""
from typing import List, Optional
@ -37,5 +66,6 @@ from .load import load_from_json as load_from_json
from .load import load_from_toml as load_from_toml
from .on import on_shell_command as on_shell_command
from .load import load_all_plugins as load_all_plugins
from .load import load_builtin_plugin as load_builtin_plugin
from .plugin import get_loaded_plugins as get_loaded_plugins
from .load import load_builtin_plugins as load_builtin_plugins

View File

@ -1,16 +1,20 @@
"""本模块定义了插件导出的内容对象。
在新版插件系统中推荐优先使用直接 import 所需要的插件内容
FrontMatter:
sidebar_position: 4
description: nonebot.plugin.export 模块
"""
from . import _current_plugin
class Export(dict):
"""
:说明:
插件导出内容以使得其他插件可以获得
:示例:
.. code-block:: python
"""插件导出内容以使得其他插件可以获得。
用法:
```python
nonebot.export().default = "bar"
@nonebot.export()
@ -25,6 +29,7 @@ class Export(dict):
@nonebot.export().sub
def something_else():
pass
```
"""
def __call__(self, func, **kwargs):
@ -45,15 +50,7 @@ class Export(dict):
def export() -> Export:
"""
:说明:
获取插件的导出内容对象
:返回:
- ``Export``
"""
"""获取当前插件的导出内容对象"""
plugin = _current_plugin.get()
if not plugin:
raise RuntimeError("Export outside of the plugin!")

View File

@ -1,3 +1,9 @@
"""本模块定义插件加载接口。
FrontMatter:
sidebar_position: 1
description: nonebot.plugin.load 模块
"""
import json
import warnings
from typing import Set, Iterable, Optional
@ -11,18 +17,10 @@ from .plugin import Plugin, get_plugin
def load_plugin(module_path: str) -> Optional[Plugin]:
"""
:说明:
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
使用 ``PluginManager`` 加载单个插件可以是本地插件或是通过 ``pip`` 安装的插件
:参数:
* ``module_path: str``: 插件名称 ``path.to.your.plugin``
:返回:
- ``Optional[Plugin]``
参数:
module_path: 插件名称 `path.to.your.plugin`
"""
manager = PluginManager([module_path])
@ -31,18 +29,10 @@ def load_plugin(module_path: str) -> Optional[Plugin]:
def load_plugins(*plugin_dir: str) -> Set[Plugin]:
"""
:说明:
"""导入文件夹下多个插件,以 `_` 开头的插件不会被导入!
导入目录下多个插件 ``_`` 开头的插件不会被导入
:参数:
- ``*plugin_dir: str``: 插件路径
:返回:
- ``Set[Plugin]``
参数:
plugin_dir: 文件夹路径
"""
manager = PluginManager(search_path=plugin_dir)
_managers.append(manager)
@ -52,19 +42,11 @@ def load_plugins(*plugin_dir: str) -> Set[Plugin]:
def load_all_plugins(
module_path: Iterable[str], plugin_dir: Iterable[str]
) -> Set[Plugin]:
"""
:说明:
"""导入指定列表中的插件以及指定目录下多个插件,以 `_` 开头的插件不会被导入!
导入指定列表中的插件以及指定目录下多个插件 ``_`` 开头的插件不会被导入
:参数:
- ``module_path: Iterable[str]``: 指定插件集合
- ``plugin_dir: Iterable[str]``: 指定插件路径集合
:返回:
- ``Set[Plugin]``
参数:
module_path: 指定插件集合
plugin_dir: 指定文件夹路径集合
"""
manager = PluginManager(module_path, plugin_dir)
_managers.append(manager)
@ -72,19 +54,23 @@ def load_all_plugins(
def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
"""
:说明:
"""导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件,以 `_` 开头的插件不会被导入!
导入指定 json 文件中的 ``plugins`` 以及 ``plugin_dirs`` 下多个插件 ``_`` 开头的插件不会被导入
参数:
file_path: 指定 json 文件路径
encoding: 指定 json 文件编码
:参数:
用法:
```json title=plugins.json
{
"plugins": ["some_plugin"],
"plugin_dirs": ["some_dir"]
}
```
- ``file_path: str``: 指定 json 文件路径
- ``encoding: str``: 指定 json 文件编码
:返回:
- ``Set[Plugin]``
```python
nonebot.load_from_json("plugins.json")
```
"""
with open(file_path, "r", encoding=encoding) as f:
data = json.load(f)
@ -96,20 +82,22 @@ def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
"""
:说明:
"""导入指定 toml 文件 `[tool.nonebot]` 中的 `plugins` 以及 `plugin_dirs` 下多个插件,以 `_` 开头的插件不会被导入!
导入指定 toml 文件 ``[tool.nonebot]`` 中的 ``plugins`` 以及 ``plugin_dirs`` 下多个插件
``_`` 开头的插件不会被导入
参数:
file_path: 指定 toml 文件路径
encoding: 指定 toml 文件编码
:参数:
用法:
```toml title=pyproject.toml
[tool.nonebot]
plugins = ["some_plugin"]
plugin_dirs = ["some_dir"]
```
- ``file_path: str``: 指定 toml 文件路径
- ``encoding: str``: 指定 toml 文件编码
:返回:
- ``Set[Plugin]``
```python
nonebot.load_from_toml("pyproject.toml")
```
"""
with open(file_path, "r", encoding=encoding) as f:
data = tomlkit.parse(f.read()) # type: ignore
@ -131,37 +119,48 @@ def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
return load_all_plugins(plugins, plugin_dirs)
def load_builtin_plugins(name: str) -> Optional[Plugin]:
"""
:说明:
def load_builtin_plugin(name: str) -> Optional[Plugin]:
"""导入 NoneBot 内置插件。
导入 NoneBot 内置插件
:返回:
- ``Plugin``
参数:
name: 插件名称
"""
return load_plugin(f"nonebot.plugins.{name}")
def load_builtin_plugins(*plugins) -> Set[Plugin]:
"""导入多个 NoneBot 内置插件。
参数:
plugins: 插件名称列表
"""
return load_all_plugins([f"nonebot.plugins.{p}" for p in plugins], [])
def _find_manager_by_name(name: str) -> Optional[PluginManager]:
for manager in reversed(_managers):
if name in manager.plugins or name in manager.searched_plugins:
return manager
def require(name: str) -> Export:
"""获取一个插件的导出内容。
如果为 `load_plugins` 文件夹导入的插件则为文件()
参数:
name: 插件名 {ref}`nonebot.plugin.plugin.Plugin.name`
异常:
RuntimeError: 插件无法加载
"""
:说明:
获取一个插件的导出内容
:参数:
* ``name: str``: 插件名 ``load_plugin`` 参数一致如果为 ``load_plugins`` 导入的插件则为文件()
:返回:
- ``Export``
:异常:
- ``RuntimeError``: 插件无法加载
"""
plugin = get_plugin(name) or load_plugin(name)
plugin = get_plugin(name.rsplit(".", 1)[-1])
if not plugin:
raise RuntimeError(f'Cannot load plugin "{name}"!')
manager = _find_manager_by_name(name)
if manager:
plugin = manager.load_plugin(name)
else:
plugin = load_plugin(name)
if not plugin:
raise RuntimeError(f'Cannot load plugin "{name}"!')
return plugin.export

View File

@ -1,3 +1,11 @@
"""本模块实现插件加载流程。
参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/)
FrontMatter:
sidebar_position: 5
description: nonebot.plugin.manager 模块
"""
import sys
import pkgutil
import importlib
@ -10,8 +18,9 @@ from typing import Set, Dict, List, Union, Iterable, Optional, Sequence
from nonebot.log import logger
from nonebot.utils import escape_tag
from .plugin import Plugin, _new_plugin
from . import _managers, _current_plugin
from .plugin import Plugin, _new_plugin, _confirm_plugin
class PluginManager:
@ -44,7 +53,10 @@ class PluginManager:
return [
*chain.from_iterable(
[*manager.plugins, *manager.searched_plugins.keys()]
[
*map(lambda x: x.rsplit(".", 1)[-1], manager.plugins),
*manager.searched_plugins.keys(),
]
for manager in _pre_managers
)
]
@ -56,7 +68,7 @@ class PluginManager:
third_party_plugins: Set[str] = set()
for plugin in self.plugins:
name = plugin.rsplit(".", 1)[-1] if "." in plugin else plugin
name = plugin.rsplit(".", 1)[-1]
if name in third_party_plugins or name in previous_plugins:
raise RuntimeError(
f"Plugin already exists: {name}! Check your plugin name"
@ -86,21 +98,27 @@ class PluginManager:
return third_party_plugins | set(self.searched_plugins.keys())
def load_plugin(self, name) -> Optional[Plugin]:
def load_plugin(self, name: str) -> Optional[Plugin]:
try:
if name in self.plugins:
module = importlib.import_module(name)
elif name not in self.searched_plugins:
raise RuntimeError(f"Plugin not found: {name}! Check your plugin name")
else:
elif name in self.searched_plugins:
module = importlib.import_module(
self._path_to_module_name(self.searched_plugins[name])
)
else:
raise RuntimeError(f"Plugin not found: {name}! Check your plugin name")
logger.opt(colors=True).success(
f'Succeeded to import "<y>{escape_tag(name)}</y>"'
)
return getattr(module, "__plugin__", None)
plugin = getattr(module, "__plugin__", None)
if plugin is None:
raise RuntimeError(
f"Module {module.__name__} is not loaded as a plugin! "
"Make sure not to import it before loading."
)
return plugin
except Exception as e:
logger.opt(colors=True, exception=e).error(
f'<r><bg #f8bbd0>Failed to import "{escape_tag(name)}"</bg #f8bbd0></r>'
@ -120,7 +138,6 @@ class PluginFinder(MetaPathFinder):
target: Optional[ModuleType] = None,
):
if _managers:
index = -1
module_spec = PathFinder.find_spec(fullname, path, target)
if not module_spec:
return
@ -129,17 +146,13 @@ class PluginFinder(MetaPathFinder):
return
module_path = Path(module_origin).resolve()
while -index <= len(_managers):
manager = _managers[index]
for manager in reversed(_managers):
if (
fullname in manager.plugins
or module_path in manager.searched_plugins.values()
):
module_spec.loader = PluginLoader(manager, fullname, module_origin)
return module_spec
index -= 1
return
@ -180,6 +193,8 @@ class PluginLoader(SourceFileLoader):
# ) from e
super().exec_module(module)
_confirm_plugin(plugin)
_current_plugin.reset(_plugin_token)
return

View File

@ -1,3 +1,9 @@
"""本模块定义事件响应器便携定义函数。
FrontMatter:
sidebar_position: 2
description: nonebot.plugin.on 模块
"""
import re
import sys
import inspect
@ -5,15 +11,9 @@ from types import ModuleType
from typing import Any, Set, Dict, List, Type, Tuple, Union, Optional
from nonebot.matcher import Matcher
from .manager import _current_plugin
from nonebot.permission import Permission
from nonebot.dependencies import Dependent
from nonebot.typing import (
T_State,
T_Handler,
T_RuleChecker,
T_PermissionChecker,
)
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
from nonebot.rule import (
Rule,
ArgumentParser,
@ -25,6 +25,8 @@ from nonebot.rule import (
shell_command,
)
from .manager import _current_plugin
def _store_matcher(matcher: Type[Matcher]) -> None:
plugin = _current_plugin.get()
@ -55,24 +57,17 @@ def on(
_depth: int = 0,
) -> Type[Matcher]:
"""
:说明:
注册一个基础事件响应器可自定义类型
注册一个基础事件响应器可自定义类型
:参数:
* ``type: str``: 事件响应器类型
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
type: 事件响应器类型
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
matcher = Matcher.new(
type,
@ -101,22 +96,15 @@ def on_metaevent(
_depth: int = 0,
) -> Type[Matcher]:
"""
:说明:
注册一个元事件响应器
注册一个元事件响应器
:参数:
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
matcher = Matcher.new(
"meta_event",
@ -146,23 +134,16 @@ def on_message(
_depth: int = 0,
) -> Type[Matcher]:
"""
:说明:
注册一个消息事件响应器
注册一个消息事件响应器
:参数:
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
matcher = Matcher.new(
"message",
@ -191,22 +172,15 @@ def on_notice(
_depth: int = 0,
) -> Type[Matcher]:
"""
:说明:
注册一个通知事件响应器
注册一个通知事件响应器
:参数:
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
matcher = Matcher.new(
"notice",
@ -235,22 +209,15 @@ def on_request(
_depth: int = 0,
) -> Type[Matcher]:
"""
:说明:
注册一个请求事件响应器
注册一个请求事件响应器
:参数:
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
matcher = Matcher.new(
"request",
@ -276,25 +243,18 @@ def on_startswith(
**kwargs,
) -> Type[Matcher]:
"""
:说明:
注册一个消息事件响应器并且当消息的**文本部分**以指定内容开头时响应
注册一个消息事件响应器并且当消息的**文本部分**以指定内容开头时响应
:参数:
* ``msg: Union[str, Tuple[str, ...]]``: 指定消息开头内容
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``ignorecase: bool``: 是否忽略大小写
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
msg: 指定消息开头内容
rule: 事件响应规则
ignorecase: 是否忽略大小写
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
return on_message(startswith(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1)
@ -307,25 +267,18 @@ def on_endswith(
**kwargs,
) -> Type[Matcher]:
"""
:说明:
注册一个消息事件响应器并且当消息的**文本部分**以指定内容结尾时响应
注册一个消息事件响应器并且当消息的**文本部分**以指定内容结尾时响应
:参数:
* ``msg: Union[str, Tuple[str, ...]]``: 指定消息结尾内容
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``ignorecase: bool``: 是否忽略大小写
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
msg: 指定消息结尾内容
rule: 事件响应规则
ignorecase: 是否忽略大小写
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
return on_message(endswith(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1)
@ -337,24 +290,17 @@ def on_keyword(
**kwargs,
) -> Type[Matcher]:
"""
:说明:
注册一个消息事件响应器并且当消息纯文本部分包含关键词时响应
注册一个消息事件响应器并且当消息纯文本部分包含关键词时响应
:参数:
* ``keywords: Set[str]``: 关键词列表
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
keywords: 关键词列表
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
return on_message(keyword(*keywords) & rule, **kwargs, _depth=_depth + 1)
@ -367,27 +313,20 @@ def on_command(
**kwargs,
) -> Type[Matcher]:
"""
:说明:
注册一个消息事件响应器并且当消息以指定命令开头时响应
注册一个消息事件响应器并且当消息以指定命令开头时响应
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
:参数:
* ``cmd: Union[str, Tuple[str, ...]]``: 指定命令内容
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``aliases: Optional[Set[Union[str, Tuple[str, ...]]]]``: 命令别名
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
cmd: 指定命令内容
rule: 事件响应规则
aliases: 命令别名
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
commands = set([cmd]) | (aliases or set())
@ -406,30 +345,23 @@ def on_shell_command(
**kwargs,
) -> Type[Matcher]:
"""
:说明:
注册一个支持 `shell_like` 解析参数的命令消息事件响应器
注册一个支持 ``shell_like`` 解析参数的命令消息事件响应器
与普通的 `on_command` 不同的是在添加 `parser` 参数时, 响应器会自动处理消息
与普通的 ``on_command`` 不同的是在添加 ``parser`` 参数时, 响应器会自动处理消息
并将用户输入的原始参数列表保存在 `state["argv"]`, `parser` 处理的参数保存在 `state["args"]`
并将用户输入的原始参数列表保存在 ``state["argv"]``, ``parser`` 处理的参数保存在 ``state["args"]``
:参数:
* ``cmd: Union[str, Tuple[str, ...]]``: 指定命令内容
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``aliases: Optional[Set[Union[str, Tuple[str, ...]]]]``: 命令别名
* ``parser: Optional[ArgumentParser]``: ``nonebot.rule.ArgumentParser`` 对象
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
cmd: 指定命令内容
rule: 事件响应规则
aliases: 命令别名
parser: `nonebot.rule.ArgumentParser` 对象
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
commands = set([cmd]) | (aliases or set())
@ -448,27 +380,20 @@ def on_regex(
**kwargs,
) -> Type[Matcher]:
"""
:说明:
注册一个消息事件响应器并且当消息匹配正则表达式时响应
注册一个消息事件响应器并且当消息匹配正则表达式时响应
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
:参数:
* ``pattern: str``: 正则表达式
* ``flags: Union[int, re.RegexFlag]``: 正则匹配标志
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
pattern: 正则表达式
flags: 正则匹配标志
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
return on_message(regex(pattern, flags) & rule, **kwargs, _depth=_depth + 1)
@ -478,38 +403,28 @@ class CommandGroup:
def __init__(self, cmd: Union[str, Tuple[str, ...]], **kwargs):
"""
:参数:
* ``cmd: Union[str, Tuple[str, ...]]``: 命令前缀
* ``**kwargs``: 其他传递给 ``on_command`` 的参数默认值参考 `on_command <#on-command-cmd-rule-none-aliases-none-kwargs>`_
参数:
cmd: 命令前缀
**kwargs: `on_command` 的参数默认值参考 `on_command <#on-command-cmd-rule-none-aliases-none-kwargs>`_
"""
self.basecmd: Tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd
"""
- **类型**: ``Tuple[str, ...]``
- **说明**: 命令前缀
命令前缀
"""
if "aliases" in kwargs:
del kwargs["aliases"]
self.base_kwargs: Dict[str, Any] = kwargs
"""
- **类型**: ``Dict[str, Any]``
- **说明**: 其他传递给 ``on_command`` 的参数默认值
其他传递给 `on_command` 的参数默认值
"""
def command(self, cmd: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
"""
:说明:
注册一个新的命令
注册一个新的命令
:参数:
* ``cmd: Union[str, Tuple[str, ...]]``: 命令前缀
* ``**kwargs``: 其他传递给 ``on_command`` 的参数将会覆盖命令组默认值
:返回:
- ``Type[Matcher]``
参数:
cmd: 命令前缀
**kwargs: `on_command` 的参数将会覆盖命令组默认值
"""
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
cmd = self.basecmd + sub_cmd
@ -522,18 +437,11 @@ class CommandGroup:
self, cmd: Union[str, Tuple[str, ...]], **kwargs
) -> Type[Matcher]:
"""
:说明:
注册一个新的命令
注册一个新的命令
:参数:
* ``cmd: Union[str, Tuple[str, ...]]``: 命令前缀
* ``**kwargs``: 其他传递给 ``on_shell_command`` 的参数将会覆盖命令组默认值
:返回:
- ``Type[Matcher]``
参数:
cmd: 命令前缀
**kwargs: `on_shell_command` 的参数将会覆盖命令组默认值
"""
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
cmd = self.basecmd + sub_cmd
@ -544,45 +452,34 @@ class CommandGroup:
class MatcherGroup:
"""事件响应器组合,统一管理。为 ``Matcher`` 创建提供默认属性。"""
"""事件响应器组合,统一管理。为 `Matcher` 创建提供默认属性。"""
def __init__(self, **kwargs):
"""
:说明:
创建一个事件响应器组合参数为默认值 ``on`` 一致
创建一个事件响应器组合参数为默认值 `on` 一致
"""
self.matchers: List[Type[Matcher]] = []
"""
:类型: ``List[Type[Matcher]]``
:说明: 组内事件响应器列表
组内事件响应器列表
"""
self.base_kwargs: Dict[str, Any] = kwargs
"""
- **类型**: ``Dict[str, Any]``
- **说明**: 其他传递给 ``on`` 的参数默认值
其他传递给 `on` 的参数默认值
"""
def on(self, **kwargs) -> Type[Matcher]:
"""
:说明:
注册一个基础事件响应器可自定义类型
注册一个基础事件响应器可自定义类型
:参数:
* ``type: str``: 事件响应器类型
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
type: 事件响应器类型
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
@ -592,22 +489,15 @@ class MatcherGroup:
def on_metaevent(self, **kwargs) -> Type[Matcher]:
"""
:说明:
注册一个元事件响应器
注册一个元事件响应器
:参数:
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
@ -619,23 +509,16 @@ class MatcherGroup:
def on_message(self, **kwargs) -> Type[Matcher]:
"""
:说明:
注册一个消息事件响应器
注册一个消息事件响应器
:参数:
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
@ -646,22 +529,15 @@ class MatcherGroup:
def on_notice(self, **kwargs) -> Type[Matcher]:
"""
:说明:
注册一个通知事件响应器
注册一个通知事件响应器
:参数:
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
@ -672,22 +548,15 @@ class MatcherGroup:
def on_request(self, **kwargs) -> Type[Matcher]:
"""
:说明:
注册一个请求事件响应器
注册一个请求事件响应器
:参数:
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
@ -700,25 +569,18 @@ class MatcherGroup:
self, msg: Union[str, Tuple[str, ...]], **kwargs
) -> Type[Matcher]:
"""
:说明:
注册一个消息事件响应器并且当消息的**文本部分**以指定内容开头时响应
注册一个消息事件响应器并且当消息的**文本部分**以指定内容开头时响应
:参数:
* ``msg: Union[str, Tuple[str, ...]]``: 指定消息开头内容
* ``ignorecase: bool``: 是否忽略大小写
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
msg: 指定消息开头内容
ignorecase: 是否忽略大小写
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
@ -729,25 +591,18 @@ class MatcherGroup:
def on_endswith(self, msg: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
"""
:说明:
注册一个消息事件响应器并且当消息的**文本部分**以指定内容结尾时响应
注册一个消息事件响应器并且当消息的**文本部分**以指定内容结尾时响应
:参数:
* ``msg: Union[str, Tuple[str, ...]]``: 指定消息结尾内容
* ``ignorecase: bool``: 是否忽略大小写
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
msg: 指定消息结尾内容
ignorecase: 是否忽略大小写
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
@ -758,24 +613,17 @@ class MatcherGroup:
def on_keyword(self, keywords: Set[str], **kwargs) -> Type[Matcher]:
"""
:说明:
注册一个消息事件响应器并且当消息纯文本部分包含关键词时响应
注册一个消息事件响应器并且当消息纯文本部分包含关键词时响应
:参数:
* ``keywords: Set[str]``: 关键词列表
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
keywords: 关键词列表
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
@ -791,27 +639,20 @@ class MatcherGroup:
**kwargs,
) -> Type[Matcher]:
"""
:说明:
注册一个消息事件响应器并且当消息以指定命令开头时响应
注册一个消息事件响应器并且当消息以指定命令开头时响应
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
:参数:
* ``cmd: Union[str, Tuple[str, ...]]``: 指定命令内容
* ``aliases: Optional[Set[Union[str, Tuple[str, ...]]]]``: 命令别名
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
cmd: 指定命令内容
aliases: 命令别名
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
@ -828,30 +669,23 @@ class MatcherGroup:
**kwargs,
) -> Type[Matcher]:
"""
:说明:
注册一个支持 `shell_like` 解析参数的命令消息事件响应器
注册一个支持 ``shell_like`` 解析参数的命令消息事件响应器
与普通的 `on_command` 不同的是在添加 `parser` 参数时, 响应器会自动处理消息
与普通的 ``on_command`` 不同的是在添加 ``parser`` 参数时, 响应器会自动处理消息
并将用户输入的原始参数列表保存在 `state["argv"]`, `parser` 处理的参数保存在 `state["args"]`
并将用户输入的原始参数列表保存在 ``state["argv"]``, ``parser`` 处理的参数保存在 ``state["args"]``
:参数:
* ``cmd: Union[str, Tuple[str, ...]]``: 指定命令内容
* ``aliases: Optional[Set[Union[str, Tuple[str, ...]]]]``: 命令别名
* ``parser: Optional[ArgumentParser]``: ``nonebot.rule.ArgumentParser`` 对象
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
cmd: 指定命令内容
aliases: 命令别名
parser: `nonebot.rule.ArgumentParser` 对象
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
@ -866,27 +700,20 @@ class MatcherGroup:
self, pattern: str, flags: Union[int, re.RegexFlag] = 0, **kwargs
) -> Type[Matcher]:
"""
:说明:
注册一个消息事件响应器并且当消息匹配正则表达式时响应
注册一个消息事件响应器并且当消息匹配正则表达式时响应
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
:参数:
* ``pattern: str``: 正则表达式
* ``flags: Union[int, re.RegexFlag]``: 正则匹配标志
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Union[Permission, T_PermissionChecker]] =]]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Dependent]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
* ``priority: int``: 事件响应器优先级
* ``block: bool``: 是否阻止事件向更低优先级传递
* ``state: Optional[T_State]``: 默认 state
:返回:
- ``Type[Matcher]``
参数:
pattern: 正则表达式
flags: 正则匹配标志
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器仅执行一次
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)

View File

@ -5,12 +5,7 @@ from nonebot.matcher import Matcher
from nonebot.permission import Permission
from nonebot.dependencies import Dependent
from nonebot.rule import Rule, ArgumentParser
from nonebot.typing import (
T_State,
T_Handler,
T_RuleChecker,
T_PermissionChecker,
)
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
def on(
type: str = "",

View File

@ -1,18 +1,22 @@
"""本模块定义插件对象。
FrontMatter:
sidebar_position: 3
description: nonebot.plugin.plugin 模块
"""
from types import ModuleType
from dataclasses import field, dataclass
from typing import TYPE_CHECKING, Set, Dict, Type, Optional
from .export import Export
from nonebot.matcher import Matcher
from .export import Export
if TYPE_CHECKING:
from .manager import PluginManager
plugins: Dict[str, "Plugin"] = {}
"""
:类型: ``Dict[str, Plugin]``
:说明: 已加载的插件
"""
"""已加载的插件"""
@dataclass(eq=False)
@ -20,74 +24,36 @@ class Plugin(object):
"""存储插件信息"""
name: str
"""
- **类型**: ``str``
- **说明**: 插件名称使用 文件/文件夹 名称作为插件名
"""
"""插件名称,使用 文件/文件夹 名称作为插件名"""
module: ModuleType
"""
- **类型**: ``ModuleType``
- **说明**: 插件模块对象
"""
"""插件模块对象"""
module_name: str
"""
- **类型**: ``str``
- **说明**: 点分割模块路径
"""
"""点分割模块路径"""
manager: "PluginManager"
"""
- **类型**: ``PluginManager``
- **说明**: 导入该插件的插件管理器
"""
"""导入该插件的插件管理器"""
export: Export = field(default_factory=Export)
"""
- **类型**: ``Export``
- **说明**: 插件内定义的导出内容
"""
"""插件内定义的导出内容"""
matcher: Set[Type[Matcher]] = field(default_factory=set)
"""
- **类型**: ``Set[Type[Matcher]]``
- **说明**: 插件内定义的 ``Matcher``
"""
"""插件内定义的 `Matcher`"""
parent_plugin: Optional["Plugin"] = None
"""
- **类型**: ``Optional[Plugin]``
- **说明**: 父插件
"""
"""父插件"""
sub_plugins: Set["Plugin"] = field(default_factory=set)
"""
- **类型**: ``Set[Plugin]``
- **说明**: 子插件集合
"""
"""子插件集合"""
def get_plugin(name: str) -> Optional[Plugin]:
"""
:说明:
"""获取已经导入的某个插件。
获取当前导入的某个插件
如果为 `load_plugins` 文件夹导入的插件则为文件()
:参数:
* ``name: str``: 插件名 ``load_plugin`` 参数一致如果为 ``load_plugins`` 导入的插件则为文件()
:返回:
- ``Optional[Plugin]``
参数:
name: 插件名 {ref}`nonebot.plugin.plugin.Plugin.name`
"""
return plugins.get(name)
def get_loaded_plugins() -> Set[Plugin]:
"""
:说明:
获取当前已导入的所有插件
:返回:
- ``Set[Plugin]``
"""
"""获取当前已导入的所有插件。"""
return set(plugins.values())
@ -96,5 +62,10 @@ def _new_plugin(fullname: str, module: ModuleType, manager: "PluginManager") ->
if name in plugins:
raise RuntimeError("Plugin already exists! Check your plugin name.")
plugin = Plugin(name, module, fullname, manager)
plugins[name] = plugin
return plugin
def _confirm_plugin(plugin: Plugin) -> None:
if plugin.name in plugins:
raise RuntimeError("Plugin already exists! Check your plugin name.")
plugins[plugin.name] = plugin

View File

@ -1,34 +1,31 @@
from typing import Dict
from typing import Dict, AsyncGenerator
from nonebot.adapters import Event
from nonebot.message import (
IgnoredException,
run_preprocessor,
run_postprocessor,
)
from nonebot.params import Depends
from nonebot.message import IgnoredException, event_preprocessor
_running_matcher: Dict[str, int] = {}
@run_preprocessor
async def preprocess(event: Event):
async def matcher_mutex(event: Event) -> AsyncGenerator[bool, None]:
result = False
try:
session_id = event.get_session_id()
except Exception:
return
current_event_id = id(event)
event_id = _running_matcher.get(session_id, None)
if event_id and event_id != current_event_id:
yield result
else:
current_event_id = id(event)
event_id = _running_matcher.get(session_id, None)
if event_id:
result = event_id != current_event_id
else:
_running_matcher[session_id] = current_event_id
yield result
if not result:
del _running_matcher[session_id]
@event_preprocessor
async def preprocess(mutex: bool = Depends(matcher_mutex)):
if mutex:
raise IgnoredException("Another matcher running")
_running_matcher[session_id] = current_event_id
@run_postprocessor
async def postprocess(event: Event):
try:
session_id = event.get_session_id()
except Exception:
return
if session_id in _running_matcher:
del _running_matcher[session_id]

View File

@ -1,32 +1,37 @@
r"""
规则
====
"""本模块是 {ref}`nonebot.matcher.Matcher.rule` 的类型定义。
每个事件响应器 ``Matcher`` 拥有一个匹配规则 ``Rule`` 其中是 ``RuleChecker`` 的集合只有当所有 ``RuleChecker`` 检查结果为 ``True`` 时继续运行
每个事件响应器 {ref}`nonebot.matcher.Matcher` 拥有一个匹配规则 {ref}`nonebot.rule.Rule`
其中是 `RuleChecker` 的集合只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行
\:\:\:tip 提示
``RuleChecker`` 既可以是 async function 也可以是 sync function
\:\:\:
FrontMatter:
sidebar_position: 5
description: nonebot.rule 模块
"""
import re
import shlex
import asyncio
from itertools import product
from argparse import Namespace
from contextlib import AsyncExitStack
from typing_extensions import TypedDict
from argparse import ArgumentParser as ArgParser
from typing import Any, Set, List, Tuple, Union, NoReturn, Optional, Sequence
from typing import Any, List, Tuple, Union, Optional, Sequence
from pygtrie import CharTrie
from nonebot import get_driver
from nonebot.log import logger
from nonebot.dependencies import Dependent
from nonebot.exception import ParserExit, SkippedException
from nonebot.typing import T_State
from nonebot.exception import ParserExit
from nonebot.internal.rule import Rule as Rule
from nonebot.adapters import Bot, Event, Message, MessageSegment
from nonebot.typing import T_State, T_RuleChecker, T_DependencyCache
from nonebot.params import (
Command,
EventToMe,
EventType,
CommandArg,
EventMessage,
EventPlainText,
)
from nonebot.consts import (
CMD_KEY,
PREFIX_KEY,
@ -38,19 +43,6 @@ from nonebot.consts import (
REGEX_GROUP,
REGEX_MATCHED,
)
from nonebot.params import (
State,
Command,
BotParam,
EventToMe,
EventType,
EventParam,
StateParam,
DependParam,
DefaultParam,
EventMessage,
EventPlainText,
)
CMD_RESULT = TypedDict(
"CMD_RESULT",
@ -62,113 +54,6 @@ CMD_RESULT = TypedDict(
)
class Rule:
"""
:说明:
``Matcher`` 规则类当事件传递时 ``Matcher`` 运行前进行检查
:示例:
.. code-block:: python
Rule(async_function) & sync_function
# 等价于
from nonebot.utils import run_sync
Rule(async_function, run_sync(sync_function))
"""
__slots__ = ("checkers",)
HANDLER_PARAM_TYPES = [
DependParam,
BotParam,
EventParam,
StateParam,
DefaultParam,
]
def __init__(self, *checkers: Union[T_RuleChecker, Dependent[bool]]) -> None:
"""
:参数:
* ``*checkers: Union[T_RuleChecker, Dependent[bool]]``: RuleChecker
"""
self.checkers: Set[Dependent[bool]] = set(
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
)
for checker in checkers
)
"""
:说明:
存储 ``RuleChecker``
:类型:
* ``Set[Dependent[bool]]``
"""
async def __call__(
self,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""
:说明:
检查是否符合所有规则
:参数:
* ``bot: Bot``: Bot 对象
* ``event: Event``: Event 对象
* ``state: T_State``: 当前 State
* ``stack: Optional[AsyncExitStack]``: 异步上下文栈
* ``dependency_cache: Optional[CacheDict[T_Handler, Any]]``: 依赖缓存
:返回:
- ``bool``
"""
if not self.checkers:
return True
try:
results = await asyncio.gather(
*(
checker(
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
)
for checker in self.checkers
)
)
except SkippedException:
return False
return all(results)
def __and__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
if other is None:
return self
elif isinstance(other, Rule):
return Rule(*self.checkers, *other.checkers)
else:
return Rule(*self.checkers, other)
def __or__(self, other) -> NoReturn:
raise RuntimeError("Or operation between rules is not allowed.")
class TrieRule:
prefix: CharTrie = CharTrie()
@ -205,6 +90,15 @@ class TrieRule:
class StartswithRule:
"""检查消息纯文本是否以指定字符串开头。
参数:
msg: 指定消息开头字符串元组
ignorecase: 是否忽略大小写
"""
__slots__ = ("msg", "ignorecase")
def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False):
self.msg = msg
self.ignorecase = ignorecase
@ -224,14 +118,11 @@ class StartswithRule:
def startswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
"""
:说明:
"""匹配消息纯文本开头。
匹配消息开头
:参数:
* ``msg: str``: 消息开头字符串
参数:
msg: 指定消息开头字符串元组
ignorecase: 是否忽略大小写
"""
if isinstance(msg, str):
msg = (msg,)
@ -240,6 +131,15 @@ def startswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Ru
class EndswithRule:
"""检查消息纯文本是否以指定字符串结尾。
参数:
msg: 指定消息结尾字符串元组
ignorecase: 是否忽略大小写
"""
__slots__ = ("msg", "ignorecase")
def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False):
self.msg = msg
self.ignorecase = ignorecase
@ -259,14 +159,11 @@ class EndswithRule:
def endswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
"""
:说明:
"""匹配消息纯文本结尾。
匹配消息结尾
:参数:
* ``msg: str``: 消息结尾字符串
参数:
msg: 指定消息开头字符串元组
ignorecase: 是否忽略大小写
"""
if isinstance(msg, str):
msg = (msg,)
@ -275,6 +172,14 @@ def endswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule
class KeywordsRule:
"""检查消息纯文本是否包含指定关键字。
参数:
keywords: 指定关键字元组
"""
__slots__ = ("keywords",)
def __init__(self, *keywords: str):
self.keywords = keywords
@ -287,20 +192,24 @@ class KeywordsRule:
def keyword(*keywords: str) -> Rule:
"""
:说明:
"""匹配消息纯文本关键词。
匹配消息关键词
:参数:
* ``*keywords: str``: 关键词
参数:
keywords: 指定关键字元组
"""
return Rule(KeywordsRule(*keywords))
class CommandRule:
"""检查消息是否为指定命令。
参数:
cmds: 指定命令元组列表
"""
__slots__ = ("cmds",)
def __init__(self, cmds: List[Tuple[str, ...]]):
self.cmds = cmds
@ -312,27 +221,27 @@ class CommandRule:
def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
r"""
:说明:
"""匹配消息命令。
命令形式匹配根据配置里提供的 ``command_start``, ``command_sep`` 判断消息是否为命令
根据配置里提供的 {ref}``command_start` <nonebot.config.Config.command_start>`,
{ref}``command_sep` <nonebot.config.Config.command_sep>` 判断消息是否为命令
可以通过 ``state["_prefix"]["command"]`` 获取匹配成功的命令``("test",)``通过 ``state["_prefix"]["raw_command"]`` 获取匹配成功的原始命令文本``"/test"``
可以通过 {ref}`nonebot.params.Command` 获取匹配成功的命令: `("test",)`
通过 {ref}`nonebot.params.RawCommand` 获取匹配成功的原始命令文本: `"/test"`
通过 {ref}`nonebot.params.CommandArg` 获取匹配成功的命令参数
:参数:
参数:
cmds: 命令文本或命令元组
* ``*cmds: Union[str, Tuple[str, ...]]``: 命令内容
用法:
使用默认 `command_start`, `command_sep` 配置
:示例:
命令 `("test",)` 可以匹配: `/test` 开头的消息
命令 `("test", "sub")` 可以匹配: `/test.sub` 开头的消息
使用默认 ``command_start``, ``command_sep`` 配置
命令 ``("test",)`` 可以匹配``/test`` 开头的消息
命令 ``("test", "sub")`` 可以匹配``/test.sub`` 开头的消息
\:\:\:tip 提示
命令内容与后续消息间无需空格
\:\:\:
:::tip 提示
命令内容与后续消息间无需空格!
:::
"""
config = get_driver().config
@ -356,10 +265,11 @@ def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
class ArgumentParser(ArgParser):
"""
:说明:
"""`shell_like` 命令参数解析器,解析出错时不会退出程序。
``shell_like`` 命令参数解析器解析出错时不会退出程序
用法:
用法与 `argparse.ArgumentParser` 相同
参考文档: [argparse](https://docs.python.org/3/library/argparse.html)
"""
def _print_message(self, message, file=None):
@ -384,20 +294,28 @@ class ArgumentParser(ArgParser):
class ShellCommandRule:
"""检查消息是否为指定 shell 命令。
参数:
cmds: 指定命令元组列表
parser: 可选参数解析器
"""
__slots__ = ("cmds", "parser")
def __init__(self, cmds: List[Tuple[str, ...]], parser: Optional[ArgumentParser]):
self.cmds = cmds
self.parser = parser
async def __call__(
self,
state: T_State,
cmd: Optional[Tuple[str, ...]] = Command(),
msg: Message = EventMessage(),
state: T_State = State(),
msg: Optional[Message] = CommandArg(),
) -> bool:
if cmd in self.cmds:
if cmd in self.cmds and msg is not None:
message = str(msg)
strip_message = message[len(state[PREFIX_KEY][RAW_CMD_KEY]) :].lstrip()
state[SHELL_ARGV] = shlex.split(strip_message)
state[SHELL_ARGV] = shlex.split(message)
if self.parser:
try:
args = self.parser.parse_args(state[SHELL_ARGV])
@ -412,38 +330,40 @@ class ShellCommandRule:
def shell_command(
*cmds: Union[str, Tuple[str, ...]], parser: Optional[ArgumentParser] = None
) -> Rule:
r"""
:说明:
"""匹配 `shell_like` 形式的消息命令。
支持 ``shell_like`` 解析参数的命令形式匹配根据配置里提供的 ``command_start``, ``command_sep`` 判断消息是否为命令
根据配置里提供的 {ref}``command_start` <nonebot.config.Config.command_start>`,
{ref}``command_sep` <nonebot.config.Config.command_sep>` 判断消息是否为命令
可以通过 ``state["_prefix"]["command"]`` 获取匹配成功的命令``("test",)``通过 ``state["_prefix"]["raw_command"]`` 获取匹配成功的原始命令文本``"/test"``
可以通过 {ref}`nonebot.params.Command` 获取匹配成功的命令: `("test",)`
通过 {ref}`nonebot.params.RawCommand` 获取匹配成功的原始命令文本: `"/test"`
通过 {ref}`nonebot.params.ShellCommandArgv` 获取解析前的参数列表: `["arg", "-h"]`
通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典: `{"arg": "arg", "h": True}`
可以通过 ``state["argv"]`` 获取用户输入的原始参数列表
:::warning 警告
如果参数解析失败则通过 {ref}`nonebot.params.ShellCommandArgs`
获取的将是 {ref}`nonebot.exception.ParserExit` 异常
:::
添加 ``parser`` 参数后, 可以自动处理消息并将结果保存在 ``state["args"]``
参数:
cmds: 命令文本或命令元组
parser: {ref}`nonebot.rule.ArgumentParser` 对象
:参数:
* ``*cmds: Union[str, Tuple[str, ...]]``: 命令内容
* ``parser: Optional[ArgumentParser]``: ``nonebot.rule.ArgumentParser`` 对象
:示例:
使用默认 ``command_start``, ``command_sep`` 配置更多示例参考 ``argparse`` 标准库文档
.. code-block:: python
用法:
使用默认 `command_start`, `command_sep` 配置更多示例参考 `argparse` 标准库文档
```python
from nonebot.rule import ArgumentParser
parser = ArgumentParser()
parser.add_argument("-a", action="store_true")
rule = shell_command("ls", parser=parser)
```
\:\:\:tip 提示
命令内容与后续消息间无需空格
\:\:\:
:::tip 提示
命令内容与后续消息间无需空格!
:::
"""
if parser is not None and not isinstance(parser, ArgumentParser):
raise TypeError("`parser` must be an instance of nonebot.rule.ArgumentParser")
@ -469,15 +389,24 @@ def shell_command(
class RegexRule:
"""检查消息字符串是否符合指定正则表达式。
参数:
regex: 正则表达式
flags: 正则表达式标记
"""
__slots__ = ("regex", "flags")
def __init__(self, regex: str, flags: int = 0):
self.regex = regex
self.flags = flags
async def __call__(
self,
state: T_State,
type: str = EventType(),
msg: Message = EventMessage(),
state: T_State = State(),
) -> bool:
if type != "message":
return False
@ -492,41 +421,47 @@ class RegexRule:
def regex(regex: str, flags: Union[int, re.RegexFlag] = 0) -> Rule:
r"""
:说明:
"""匹配符合正则表达式的消息字符串。
根据正则表达式进行匹配
可以通过 {ref}`nonebot.params.RegexMatched` 获取匹配成功的字符串
通过 {ref}`nonebot.params.RegexGroup` 获取匹配成功的 group 元组
通过 {ref}`nonebot.params.RegexDict` 获取匹配成功的 group 字典
可以通过 ``state["_matched"]`` ``state["_matched_groups"]`` ``state["_matched_dict"]``
获取正则表达式匹配成功的文本
参数:
regex: 正则表达式
flags: 正则表达式标记
:参数:
:::tip 提示
正则表达式匹配使用 search 而非 match如需从头匹配请使用 `r"^xxx"` 来确保匹配开头
:::
* ``regex: str``: 正则表达式
* ``flags: Union[int, re.RegexFlag]``: 正则标志
\:\:\:tip 提示
正则表达式匹配使用 search 而非 match如需从头匹配请使用 ``r"^xxx"`` 来确保匹配开头
\:\:\:
:::tip 提示
正则表达式匹配使用 `EventMessage` `str` 字符串而非 `EventMessage` `PlainText` 纯文本字符串
:::
"""
return Rule(RegexRule(regex, flags))
class ToMeRule:
"""检查事件是否与机器人有关。"""
__slots__ = ()
async def __call__(self, to_me: bool = EventToMe()) -> bool:
return to_me
def to_me() -> Rule:
"""
:说明:
通过 ``event.is_tome()`` 判断事件是否与机器人有关
:参数:
*
"""
"""匹配与机器人有关的事件。"""
return Rule(ToMeRule())
__autodoc__ = {
"Rule": True,
"Rule.__call__": True,
"TrieRule": False,
"ArgumentParser.exit": False,
"ArgumentParser.parse_args": False,
}

View File

@ -1,21 +1,15 @@
"""
类型
====
"""本模块定义了 NoneBot 模块中共享的一些类型。
下面的文档中类型部分使用 Python Type Hint 语法 `PEP 484`_`PEP 526`_ `typing`_
下面的文档中类型部分使用 Python Type Hint 语法
参考 [`PEP 484`](https://www.python.org/dev/peps/pep-0484/),
[`PEP 526`](https://www.python.org/dev/peps/pep-0526/)
[`typing`](https://docs.python.org/3/library/typing.html)
除了 Python 内置的类型下面还出现了如下 NoneBot 自定类型实际上它们是 Python 内置类型的别名
以下类型均可从 nonebot.typing 模块导入
.. _PEP 484:
https://www.python.org/dev/peps/pep-0484/
.. _PEP 526:
https://www.python.org/dev/peps/pep-0526/
.. _typing:
https://docs.python.org/3/library/typing.html
FrontMatter:
sidebar_position: 11
description: nonebot.typing 模块
"""
from typing import (
TYPE_CHECKING,
@ -31,13 +25,15 @@ from typing import (
if TYPE_CHECKING:
from asyncio import Task
from nonebot.adapters import Bot, Event
from nonebot.adapters import Bot
from nonebot.permission import Permission
T_Wrapped = TypeVar("T_Wrapped", bound=Callable)
def overrides(InterfaceClass: object):
def overrides(InterfaceClass: object) -> Callable[[T_Wrapped], T_Wrapped]:
"""标记一个方法为父类 interface 的 implement"""
def overrider(func: T_Wrapped) -> T_Wrapped:
assert func.__name__ in dir(InterfaceClass), f"Error method: {func.__name__}"
return func
@ -46,194 +42,114 @@ def overrides(InterfaceClass: object):
T_State = Dict[Any, Any]
"""
:类型: ``Dict[Any, Any]``
"""事件处理状态 State 类型"""
:说明:
事件处理状态 State 类型
"""
T_BotConnectionHook = Callable[["Bot"], Awaitable[None]]
"""
:类型: ``Callable[[Bot], Awaitable[None]]``
:说明:
Bot 连接建立时执行的函数
"""
T_BotDisconnectionHook = Callable[["Bot"], Awaitable[None]]
"""
:类型: ``Callable[[Bot], Awaitable[None]]``
:说明:
Bot 连接断开时执行的函数
"""
T_CallingAPIHook = Callable[["Bot", str, Dict[str, Any]], Awaitable[None]]
"""
:类型: ``Callable[[Bot, str, Dict[str, Any]], Awaitable[None]]``
:说明:
``bot.call_api`` 时执行的函数
"""
T_BotConnectionHook = Callable[..., Awaitable[Any]]
"""Bot 连接建立时钩子函数"""
T_BotDisconnectionHook = Callable[..., Awaitable[Any]]
"""Bot 连接断开时钩子函数"""
T_CallingAPIHook = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]]
"""`bot.call_api` 钩子函数"""
T_CalledAPIHook = Callable[
["Bot", Optional[Exception], str, Dict[str, Any], Any], Awaitable[None]
["Bot", Optional[Exception], str, Dict[str, Any], Any], Awaitable[Any]
]
"""`bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result"""
T_EventPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
"""事件预处理函数 EventPreProcessor 类型
依赖参数:
- DependParam: 子依赖参数
- BotParam: Bot 对象
- EventParam: Event 对象
- StateParam: State 对象
- DefaultParam: 带有默认值的参数
"""
:类型: ``Callable[[Bot, Optional[Exception], str, Dict[str, Any], Any], Awaitable[None]]``
T_EventPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
"""事件预处理函数 EventPostProcessor 类型
:说明:
依赖参数:
``bot.call_api`` 后执行的函数参数分别为 bot, exception, api, data, result
- DependParam: 子依赖参数
- BotParam: Bot 对象
- EventParam: Event 对象
- StateParam: State 对象
- DefaultParam: 带有默认值的参数
"""
T_RunPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
"""事件响应器运行前预处理函数 RunPreProcessor 类型
T_EventPreProcessor = Callable[..., Union[None, Awaitable[None]]]
依赖参数:
- DependParam: 子依赖参数
- BotParam: Bot 对象
- EventParam: Event 对象
- StateParam: State 对象
- MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数
"""
:类型: ``Callable[..., Union[None, Awaitable[None]]]``
T_RunPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
"""事件响应器运行前预处理函数 RunPostProcessor 类型
:依赖参数:
依赖参数:
* ``DependParam``: 子依赖参数
* ``BotParam``: Bot 对象
* ``EventParam``: Event 对象
* ``StateParam``: State 对象
* ``DefaultParam``: 带有默认值的参数
:说明:
事件预处理函数 EventPreProcessor 类型
"""
T_EventPostProcessor = Callable[..., Union[None, Awaitable[None]]]
"""
:类型: ``Callable[..., Union[None, Awaitable[None]]]``
:依赖参数:
* ``DependParam``: 子依赖参数
* ``BotParam``: Bot 对象
* ``EventParam``: Event 对象
* ``StateParam``: State 对象
* ``DefaultParam``: 带有默认值的参数
:说明:
事件预处理函数 EventPostProcessor 类型
"""
T_RunPreProcessor = Callable[..., Union[None, Awaitable[None]]]
"""
:类型: ``Callable[..., Union[None, Awaitable[None]]]``
:依赖参数:
* ``DependParam``: 子依赖参数
* ``BotParam``: Bot 对象
* ``EventParam``: Event 对象
* ``StateParam``: State 对象
* ``MatcherParam``: Matcher 对象
* ``DefaultParam``: 带有默认值的参数
:说明:
事件响应器运行前预处理函数 RunPreProcessor 类型
"""
T_RunPostProcessor = Callable[..., Union[None, Awaitable[None]]]
"""
:类型: ``Callable[..., Union[None, Awaitable[None]]]``
:依赖参数:
* ``DependParam``: 子依赖参数
* ``BotParam``: Bot 对象
* ``EventParam``: Event 对象
* ``StateParam``: State 对象
* ``MatcherParam``: Matcher 对象
* ``ExceptionParam``: 异常对象可能为 None
* ``DefaultParam``: 带有默认值的参数
:说明:
事件响应器运行前预处理函数 RunPostProcessor 类型第二个参数为运行时产生的错误如果存在
- DependParam: 子依赖参数
- BotParam: Bot 对象
- EventParam: Event 对象
- StateParam: State 对象
- MatcherParam: Matcher 对象
- ExceptionParam: 异常对象可能为 None
- DefaultParam: 带有默认值的参数
"""
T_RuleChecker = Callable[..., Union[bool, Awaitable[bool]]]
"""
:类型: ``Callable[..., Union[bool, Awaitable[bool]]]``
"""RuleChecker 即判断是否响应事件的处理函数。
:依赖参数:
依赖参数:
* ``DependParam``: 子依赖参数
* ``BotParam``: Bot 对象
* ``EventParam``: Event 对象
* ``StateParam``: State 对象
* ``DefaultParam``: 带有默认值的参数
:说明:
RuleChecker 即判断是否响应事件的处理函数
- DependParam: 子依赖参数
- BotParam: Bot 对象
- EventParam: Event 对象
- StateParam: State 对象
- DefaultParam: 带有默认值的参数
"""
T_PermissionChecker = Callable[..., Union[bool, Awaitable[bool]]]
"""
:类型: ``Callable[..., Union[bool, Awaitable[bool]]]``
"""PermissionChecker 即判断事件是否满足权限的处理函数。
:依赖参数:
依赖参数:
* ``DependParam``: 子依赖参数
* ``BotParam``: Bot 对象
* ``EventParam``: Event 对象
* ``DefaultParam``: 带有默认值的参数
:说明:
RuleChecker 即判断是否响应消息的处理函数
- DependParam: 子依赖参数
- BotParam: Bot 对象
- EventParam: Event 对象
- DefaultParam: 带有默认值的参数
"""
T_Handler = Callable[..., Any]
"""
:类型: ``Callable[..., Any]``
:说明:
Handler 处理函数
"""
"""Handler 处理函数。"""
T_TypeUpdater = Callable[..., Union[str, Awaitable[str]]]
"""
:类型: ``Callable[..., Union[None, Awaitable[None]]]``
"""TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。默认会更新为 `message`。
:依赖参数:
依赖参数:
* ``DependParam``: 子依赖参数
* ``BotParam``: Bot 对象
* ``EventParam``: Event 对象
* ``StateParam``: State 对象
* ``MatcherParam``: Matcher 对象
* ``DefaultParam``: 带有默认值的参数
:说明:
TypeUpdater Matcher.pause, Matcher.reject 时被运行用于更新响应的事件类型默认会更新为 ``message``
- DependParam: 子依赖参数
- BotParam: Bot 对象
- EventParam: Event 对象
- StateParam: State 对象
- MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数
"""
T_PermissionUpdater = Callable[..., Union["Permission", Awaitable["Permission"]]]
"""
:类型: ``Callable[..., Union[Permission, Awaitable[Permission]]]``
"""PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新会话对象权限。默认会更新为当前事件的触发对象。
:依赖参数:
依赖参数:
* ``DependParam``: 子依赖参数
* ``BotParam``: Bot 对象
* ``EventParam``: Event 对象
* ``StateParam``: State 对象
* ``MatcherParam``: Matcher 对象
* ``DefaultParam``: 带有默认值的参数
:说明:
PermissionUpdater Matcher.pause, Matcher.reject 时被运行用于更新会话对象权限默认会更新为当前事件的触发对象
- DependParam: 子依赖参数
- BotParam: Bot 对象
- EventParam: Event 对象
- StateParam: State 对象
- MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数
"""
T_DependencyCache = Dict[Callable[..., Any], "Task[Any]"]
"""
:类型: ``Dict[Callable[..., Any], Task[Any]]``
:说明:
依赖缓存, 用于存储依赖函数的返回值
"""
"""依赖缓存, 用于存储依赖函数的返回值"""

View File

@ -1,3 +1,10 @@
"""本模块包含了 NoneBot 的一些工具函数
FrontMatter:
sidebar_position: 8
description: nonebot.utils 模块
"""
import re
import json
import asyncio
@ -15,10 +22,13 @@ from typing import (
Callable,
Optional,
Awaitable,
Coroutine,
AsyncGenerator,
ContextManager,
)
from pydantic.typing import is_union, is_none_type
from nonebot.log import logger
from nonebot.typing import overrides
@ -30,18 +40,12 @@ V = TypeVar("V")
def escape_tag(s: str) -> str:
"""
:说明:
"""用于记录带颜色日志时转义 `<tag>` 类型特殊标签
用于记录带颜色日志时转义 ``<tag>`` 类型特殊标签
参考: [loguru color 标签](https://loguru.readthedocs.io/en/stable/api/logger.html#color)
:参数:
* ``s: str``: 需要转义的字符串
:返回:
- ``str``
参数:
s: 需要转义的字符串
"""
return re.sub(r"</?((?:[fb]g\s)?[^<>\s]*)>", r"\\\g<0>", s)
@ -49,13 +53,18 @@ def escape_tag(s: str) -> str:
def generic_check_issubclass(
cls: Any, class_or_tuple: Union[Type[Any], Tuple[Type[Any], ...]]
) -> bool:
"""检查 cls 是否是 class_or_tuple 中的一个类型子类。
特别的如果 cls `typing.Union` `types.UnionType` 类型
则会检查其中的类型是否是 class_or_tuple 中的一个类型子类None 会被忽略
"""
try:
return issubclass(cls, class_or_tuple)
except TypeError:
origin = get_origin(cls)
if origin is Union:
if is_union(origin):
for type_ in get_args(cls):
if type_ is not type(None) and not generic_check_issubclass(
if not is_none_type(type_) and not generic_check_issubclass(
type_, class_or_tuple
):
return False
@ -66,6 +75,7 @@ def generic_check_issubclass(
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
"""检查 call 是否是一个 callable 协程函数"""
if inspect.isroutine(call):
return inspect.iscoroutinefunction(call)
if inspect.isclass(call):
@ -75,6 +85,7 @@ def is_coroutine_callable(call: Callable[..., Any]) -> bool:
def is_gen_callable(call: Callable[..., Any]) -> bool:
"""检查 call 是否是一个生成器函数"""
if inspect.isgeneratorfunction(call):
return True
func_ = getattr(call, "__call__", None)
@ -82,25 +93,18 @@ def is_gen_callable(call: Callable[..., Any]) -> bool:
def is_async_gen_callable(call: Callable[..., Any]) -> bool:
"""检查 call 是否是一个异步生成器函数"""
if inspect.isasyncgenfunction(call):
return True
func_ = getattr(call, "__call__", None)
return inspect.isasyncgenfunction(func_)
def run_sync(call: Callable[P, R]) -> Callable[P, Awaitable[R]]:
"""
:说明:
def run_sync(call: Callable[P, R]) -> Callable[P, Coroutine[None, None, R]]:
"""一个用于包装 sync function 为 async function 的装饰器
一个用于包装 sync function async function 的装饰器
:参数:
* ``call: Callable[P, R]``: 被装饰的同步函数
:返回:
- ``Callable[P, Awaitable[R]]``
参数:
call: 被装饰的同步函数
"""
@wraps(call)
@ -117,6 +121,7 @@ def run_sync(call: Callable[P, R]) -> Callable[P, Awaitable[R]]:
async def run_sync_ctx_manager(
cm: ContextManager[T],
) -> AsyncGenerator[T, None]:
"""一个用于包装 sync context manager 为 async context manager 的执行函数"""
try:
yield await run_sync(cm.__enter__)()
except Exception as e:
@ -128,17 +133,14 @@ async def run_sync_ctx_manager(
def get_name(obj: Any) -> str:
"""获取对象的名称"""
if inspect.isfunction(obj) or inspect.isclass(obj):
return obj.__name__
return obj.__class__.__name__
class DataclassEncoder(json.JSONEncoder):
"""
:说明:
在JSON序列化 ``Message`` (List[Dataclass]) 时使用的 ``JSONEncoder``
"""
"""在JSON序列化 {re}`nonebot.adapters._message.Message` (List[Dataclass]) 时使用的 `JSONEncoder`"""
@overrides(json.JSONEncoder)
def default(self, o):
@ -148,16 +150,17 @@ class DataclassEncoder(json.JSONEncoder):
def logger_wrapper(logger_name: str):
"""
:说明:
"""用于打印 adapter 的日志。
用于打印 adapter 的日志
参数:
logger_name: adapter 的名称
:log 参数:
返回:
日志记录函数
* ``level: Literal["CRITICAL", "WARNING", "INFO", "DEBUG", "TRACE"]``: 日志等级
* ``message: str``: 日志信息
* ``exception: Optional[Exception]``: 异常信息
- level: 日志等级
- message: 日志信息
- exception: 异常信息
"""
def log(level: str, message: str, exception: Optional[Exception] = None):

View File

@ -1,8 +1,9 @@
from pathlib import Path
from nonebot.drivers.fastapi import Driver
from fastapi.staticfiles import StaticFiles
from nonebot.drivers.fastapi import Driver
def register_route(driver: Driver):
app = driver.server_app

View File

@ -17,21 +17,6 @@ nonebot2 = "^2.0.0-beta.1"
[tool.poetry.dev-dependencies]
[tool.black]
line-length = 88
target-version = ["py37", "py38", "py39"]
include = '\.pyi?$'
extend-exclude = '''
'''
[tool.isort]
profile = "black"
line_length = 80
length_sort = true
skip_gitignore = true
force_sort_within_sections = true
extra_standard_library = ["typing_extensions"]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

1027
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -24,25 +24,27 @@ include = ["nonebot/py.typed"]
[tool.poetry.dependencies]
python = "^3.7.3"
yarl = "^1.7.2"
loguru = "^0.5.1"
loguru = "^0.6.0"
pygtrie = "^2.4.1"
tomlkit = "^0.7.0"
fastapi = "^0.70.0"
tomlkit = "^0.9.0"
fastapi = "^0.73.0"
typing-extensions = ">=3.10.0,<5.0.0"
Quart = { version = "^0.16.0", optional = true }
websockets = { version=">=9.1", optional = true }
websockets = { version="^10.0", optional = true }
pydantic = { version = "~1.9.0", extras = ["dotenv"] }
uvicorn = { version = "^0.15.0", extras = ["standard"] }
uvicorn = { version = "^0.17.0", extras = ["standard"] }
aiohttp = { version = "^3.7.4", extras = ["speedups"], optional = true }
httpx = { version = ">=0.20.0, <1.0.0", extras = ["http2"], optional = true }
[tool.poetry.dev-dependencies]
sphinx = "^4.1.1"
isort = "^5.10.1"
black = "^21.11b1"
black = "^22.1.0"
pytest-cov = "^3.0.0"
pre-commit = "^2.16.0"
pytest-xdist = "^2.5.0"
pytest-asyncio = "^0.18.1"
nonebug = { git = "https://github.com/nonebot/nonebug.git" }
sphinx-markdown-builder = { git = "https://github.com/nonebot/sphinx-markdown-builder.git" }
nb-autodoc = { git = "https://github.com/nonebot/nb-autodoc.git" }
[tool.poetry.extras]
quart = ["quart"]
@ -57,6 +59,7 @@ all = ["quart", "aiohttp", "httpx", "websockets"]
# default = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
addopts = "--cov=nonebot --cov-report=term-missing"
[tool.black]
@ -68,11 +71,11 @@ extend-exclude = '''
[tool.isort]
profile = "black"
line_length = 80
line_length = 88
length_sort = true
skip_gitignore = true
force_sort_within_sections = true
known_local_folder = ["nonebot"]
src_paths = ["nonebot", "tests"]
extra_standard_library = ["typing_extensions"]
[build-system]

View File

@ -1,8 +0,0 @@
[settings]
profile=black
line_length=80
length_sort=true
skip_gitignore=true
force_sort_within_sections=true
known_local_folder=plugins
extra_standard_library=typing_extensions

View File

@ -3,7 +3,6 @@ from typing import List, Tuple
from nonebot.typing import T_State
from nonebot.adapters import Message
from nonebot.params import (
State,
Command,
RegexDict,
CommandArg,
@ -15,7 +14,7 @@ from nonebot.params import (
)
async def state(x: T_State = State()) -> T_State:
async def state(x: T_State) -> T_State:
return x

View File

@ -1,6 +1,6 @@
from nonebot import require
from plugins.export import test
from .export import test as test_related
test_require = require("export").test

View File

@ -0,0 +1,153 @@
from pydantic import ValidationError, parse_obj_as
from utils import make_fake_message
def test_segment_add():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
assert MessageSegment.text("text") + MessageSegment.text("text") == Message(
[MessageSegment.text("text"), MessageSegment.text("text")]
)
assert MessageSegment.text("text") + "text" == Message(
[MessageSegment.text("text"), MessageSegment.text("text")]
)
assert (
MessageSegment.text("text") + Message([MessageSegment.text("text")])
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
assert "text" + MessageSegment.text("text") == Message(
[MessageSegment.text("text"), MessageSegment.text("text")]
)
def test_segment_validate():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
assert parse_obj_as(
MessageSegment, {"type": "text", "data": {"text": "text"}}
) == MessageSegment.text("text")
try:
parse_obj_as(MessageSegment, "some str")
assert False
except ValidationError:
assert True
def test_segment():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
assert len(MessageSegment.text("text")) == 4
assert MessageSegment.text("text") != MessageSegment.text("other")
assert MessageSegment.text("text").get("data") == {"text": "text"}
assert list(MessageSegment.text("text").keys()) == ["type", "data"]
assert list(MessageSegment.text("text").values()) == ["text", {"text": "text"}]
assert list(MessageSegment.text("text").items()) == [
("type", "text"),
("data", {"text": "text"}),
]
origin = MessageSegment.text("text")
copy = origin.copy()
assert origin is not copy
assert origin == copy
def test_message_add():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
assert (
Message([MessageSegment.text("text")]) + MessageSegment.text("text")
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
assert Message([MessageSegment.text("text")]) + "text" == Message(
[MessageSegment.text("text"), MessageSegment.text("text")]
)
assert (
Message([MessageSegment.text("text")]) + Message([MessageSegment.text("text")])
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
assert "text" + Message([MessageSegment.text("text")]) == Message(
[MessageSegment.text("text"), MessageSegment.text("text")]
)
msg = Message([MessageSegment.text("text")])
msg += MessageSegment.text("text")
assert msg == Message([MessageSegment.text("text"), MessageSegment.text("text")])
def test_message_getitem():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
message = Message(
[
MessageSegment.text("test"),
MessageSegment.image("test2"),
MessageSegment.image("test3"),
MessageSegment.text("test4"),
]
)
assert message[0] == MessageSegment.text("test")
assert message[0:2] == Message(
[MessageSegment.text("test"), MessageSegment.image("test2")]
)
assert message["image"] == Message(
[MessageSegment.image("test2"), MessageSegment.image("test3")]
)
assert message["image", 0] == MessageSegment.image("test2")
assert message["image", 0:2] == message["image"]
assert message.index(message[0]) == 0
assert message.index("image") == 1
assert message.get("image") == message["image"]
assert message.get("image", 114514) == message["image"]
assert message.get("image", 1) == Message([message["image", 0]])
assert message.count("image") == 2
def test_message_validate():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
Message_ = make_fake_message()
assert parse_obj_as(Message, Message([])) == Message([])
try:
parse_obj_as(Message, Message_([]))
assert False
except ValidationError:
assert True
assert parse_obj_as(Message, "text") == Message([MessageSegment.text("text")])
assert parse_obj_as(Message, {"type": "text", "data": {"text": "text"}}) == Message(
[MessageSegment.text("text")]
)
assert parse_obj_as(
Message,
[MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}],
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
try:
parse_obj_as(Message, object())
assert False
except ValidationError:
assert True

View File

@ -1,17 +1,41 @@
from utils import make_fake_message
from utils import escape_text, make_fake_message
def test_message_template():
def test_template_basis():
from nonebot.adapters import MessageTemplate
Message = make_fake_message()
template = MessageTemplate("{key:.3%}")
formatted = template.format(key=0.123456789)
assert formatted == "12.346%"
template = MessageTemplate("{a:custom}{b:text}{c:image}", Message)
def test_template_message():
Message = make_fake_message()
template = Message.template("{a:custom}{b:text}{c:image}")
@template.add_format_spec
def custom(input: str) -> str:
return input + "-custom!"
formatted = template.format(a="test", b="test", c="https://example.com/test")
assert formatted.extract_plain_text() == "test-custom!test"
assert str(formatted) == "test-custom!test[fake:image]"
try:
template.add_format_spec(custom)
except ValueError:
pass
else:
raise AssertionError("Should raise ValueError")
format_args = {"a": "custom", "b": "text", "c": "https://example.com/test"}
formatted = template.format(**format_args)
assert template.format_map(format_args) == formatted
assert formatted.extract_plain_text() == "custom-custom!text"
assert str(formatted) == "custom-custom!text[fake:image]"
def test_message_injection():
Message = make_fake_message()
template = Message.template("{name}Is Bad")
message = template.format(name="[fake:image]")
assert message.extract_plain_text() == escape_text("[fake:image]Is Bad")

View File

@ -29,7 +29,11 @@ async def test_weather(app: App):
event = make_fake_event(_message=msg, _to_me=True)()
ctx.receive_event(bot, event)
ctx.should_call_send(event, Message("你想查询的城市 南京 暂不支持,请重新输入!"), True)
ctx.should_call_send(
event,
Message.template("你想查询的城市 {} 暂不支持,请重新输入!").format("南京"),
True,
)
ctx.should_rejected()
msg = Message("北京")
@ -53,7 +57,11 @@ async def test_weather(app: App):
event = make_fake_event(_message=msg)()
ctx.receive_event(bot, event)
ctx.should_call_send(event, Message("你想查询的城市 杭州 暂不支持,请重新输入!"), True)
ctx.should_call_send(
event,
Message.template("你想查询的城市 {} 暂不支持,请重新输入!").format("杭州"),
True,
)
ctx.should_rejected()
msg = Message("北京")

View File

@ -1,12 +1,7 @@
import os
import sys
from typing import TYPE_CHECKING, Set
import pytest
if TYPE_CHECKING:
from nonebot.plugin import Plugin
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
@ -74,23 +69,3 @@ async def test_get(monkeypatch: pytest.MonkeyPatch, nonebug_clear):
assert get_bot() == "test"
assert get_bot("test") == "test"
assert get_bots() == {"test": "test"}
@pytest.mark.asyncio
async def test_load_plugin(load_plugin: Set["Plugin"]):
import nonebot
loaded_plugins = set(
plugin for plugin in nonebot.get_loaded_plugins() if not plugin.parent_plugin
)
assert loaded_plugins == load_plugin
plugin = nonebot.get_plugin("export")
assert plugin
assert plugin.module_name == "plugins.export"
assert "plugins.export" in sys.modules
try:
nonebot.load_plugin("plugins.export")
assert False
except RuntimeError:
assert True

View File

@ -78,10 +78,7 @@ async def test_matcher(app: App, load_plugin):
@pytest.mark.asyncio
async def test_type_updater(app: App, load_plugin):
from plugins.matcher.matcher_type import (
test_type_updater,
test_custom_updater,
)
from plugins.matcher.matcher_type import test_type_updater, test_custom_updater
event = make_fake_event()()
@ -103,7 +100,6 @@ async def test_type_updater(app: App, load_plugin):
@pytest.mark.asyncio
async def test_permission_updater(app: App, load_plugin):
from nonebot.permission import User
from plugins.matcher.matcher_permission import (
default_permission,
test_custom_updater,

View File

@ -7,7 +7,6 @@ from utils import make_fake_event, make_fake_message
@pytest.mark.asyncio
async def test_depend(app: App, load_plugin):
from nonebot.params import DependParam
from plugins.param.param_depend import (
ClassDependency,
runned,
@ -37,7 +36,6 @@ async def test_depend(app: App, load_plugin):
@pytest.mark.asyncio
async def test_bot(app: App, load_plugin):
from nonebot.params import BotParam
from plugins.param.param_bot import get_bot
async with app.test_dependent(get_bot, allow_types=[BotParam]) as ctx:
@ -49,7 +47,6 @@ async def test_bot(app: App, load_plugin):
@pytest.mark.asyncio
async def test_event(app: App, load_plugin):
from nonebot.params import EventParam, DependParam
from plugins.param.param_event import (
event,
event_type,
@ -104,7 +101,6 @@ async def test_state(app: App, load_plugin):
REGEX_GROUP,
REGEX_MATCHED,
)
from plugins.param.param_state import (
state,
command,
@ -184,7 +180,6 @@ async def test_state(app: App, load_plugin):
async def test_matcher(app: App, load_plugin):
from nonebot.matcher import Matcher
from nonebot.params import DependParam, MatcherParam
from plugins.param.param_matcher import matcher, receive, last_receive
fake_matcher = Matcher()
@ -215,7 +210,6 @@ async def test_matcher(app: App, load_plugin):
async def test_arg(app: App, load_plugin):
from nonebot.matcher import Matcher
from nonebot.params import ArgParam
from plugins.param.param_arg import arg, arg_str, arg_plain_text
matcher = Matcher()
@ -238,7 +232,6 @@ async def test_arg(app: App, load_plugin):
@pytest.mark.asyncio
async def test_exception(app: App, load_plugin):
from nonebot.params import ExceptionParam
from plugins.param.param_exception import exc
exception = ValueError("test")
@ -250,7 +243,6 @@ async def test_exception(app: App, load_plugin):
@pytest.mark.asyncio
async def test_default(app: App, load_plugin):
from nonebot.params import DefaultParam
from plugins.param.param_default import default
async with app.test_dependent(default, allow_types=[DefaultParam]) as ctx:

View File

@ -0,0 +1,89 @@
import sys
from typing import TYPE_CHECKING, Set
import pytest
from nonebug import App
if TYPE_CHECKING:
from nonebot.plugin import Plugin
@pytest.mark.asyncio
async def test_load_plugin(load_plugin: Set["Plugin"]):
import nonebot
loaded_plugins = set(
plugin for plugin in nonebot.get_loaded_plugins() if not plugin.parent_plugin
)
assert loaded_plugins == load_plugin
plugin = nonebot.get_plugin("export")
assert plugin
assert plugin.module_name == "plugins.export"
assert "plugins.export" in sys.modules
try:
nonebot.load_plugin("plugins.export")
assert False
except RuntimeError:
assert True
assert nonebot.load_plugin("some_plugin_not_exist") is None
@pytest.mark.asyncio
async def test_require_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
import nonebot
def _patched_find(name: str):
assert False
monkeypatch.setattr("nonebot.plugin.load._find_manager_by_name", _patched_find)
nonebot.load_plugin("plugins.export")
nonebot.require("plugins.export")
@pytest.mark.asyncio
async def test_require_not_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
import nonebot
from nonebot.plugin import _managers
from nonebot.plugin.manager import PluginManager
m = PluginManager(["plugins.export"])
_managers.append(m)
origin_load = PluginManager.load_plugin
def _patched_load(self: PluginManager, name: str):
assert self is m
return origin_load(self, name)
monkeypatch.setattr(PluginManager, "load_plugin", _patched_load)
nonebot.require("plugins.export")
assert len(_managers) == 1
@pytest.mark.asyncio
async def test_require_not_declared(app: App):
import nonebot
from nonebot.plugin import _managers
nonebot.require("plugins.export")
assert len(_managers) == 1
assert _managers[-1].plugins == {"plugins.export"}
@pytest.mark.asyncio
async def test_require_not_found(app: App):
import nonebot
from nonebot.plugin import _managers
try:
nonebot.require("some_plugin_not_exist")
assert False
except RuntimeError:
assert True

View File

@ -0,0 +1,36 @@
from contextlib import asynccontextmanager
import pytest
from utils import make_fake_event
@pytest.mark.asyncio
async def test_matcher_mutex():
from nonebot.plugins.single_session import matcher_mutex, _running_matcher
am = asynccontextmanager(matcher_mutex)
event = make_fake_event()()
event_1 = make_fake_event()()
event_2 = make_fake_event(_session_id="test1")()
event_3 = make_fake_event(_session_id=None)()
async with am(event) as ctx:
assert ctx == False
assert not _running_matcher
async with am(event) as ctx:
async with am(event_1) as ctx_1:
assert ctx == False
assert ctx_1 == True
assert not _running_matcher
async with am(event) as ctx:
async with am(event_2) as ctx_2:
assert ctx == False
assert ctx_2 == False
assert not _running_matcher
async with am(event_3) as ctx_3:
assert ctx_3 == False
assert not _running_matcher

View File

@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Type, Optional
from typing import TYPE_CHECKING, Type, Union, Mapping, Iterable, Optional
from pydantic import create_model
@ -6,7 +6,14 @@ if TYPE_CHECKING:
from nonebot.adapters import Event, Message
def make_fake_message() -> Type["Message"]:
def escape_text(s: str, *, escape_comma: bool = True) -> str:
s = s.replace("&", "&amp;").replace("[", "&#91;").replace("]", "&#93;")
if escape_comma:
s = s.replace(",", "&#44;")
return s
def make_fake_message():
from nonebot.adapters import Message, MessageSegment
class FakeMessageSegment(MessageSegment):
@ -21,9 +28,9 @@ def make_fake_message() -> Type["Message"]:
def text(cls, text: str):
return cls("text", {"text": text})
@classmethod
def image(cls, url: str):
return cls("image", {"url": url})
@staticmethod
def image(url: str):
return FakeMessageSegment("image", {"url": url})
def is_text(self) -> bool:
return self.type == "text"
@ -34,8 +41,17 @@ def make_fake_message() -> Type["Message"]:
return FakeMessageSegment
@staticmethod
def _construct(msg: str):
yield FakeMessageSegment.text(msg)
def _construct(msg: Union[str, Iterable[Mapping]]):
if isinstance(msg, str):
yield FakeMessageSegment.text(msg)
else:
for seg in msg:
yield FakeMessageSegment(**seg)
return
def __add__(self, other):
other = escape_text(other) if isinstance(other, str) else other
return super().__add__(other)
return FakeMessage
@ -45,7 +61,7 @@ def make_fake_event(
_name: str = "test",
_description: str = "test",
_user_id: str = "test",
_session_id: str = "test",
_session_id: Optional[str] = "test",
_message: Optional["Message"] = None,
_to_me: bool = True,
**fields,
@ -68,7 +84,9 @@ def make_fake_event(
return _user_id
def get_session_id(self) -> str:
return _session_id
if _session_id is not None:
return _session_id
raise NotImplementedError
def get_message(self) -> "Message":
if _message is not None:

View File

@ -1,3 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
};

View File

@ -8,7 +8,7 @@ slug: /
NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架,它基于 Python 的类型注解和异步特性,能够为你的需求实现提供便捷灵活的支持。
需要注意的是NoneBot 仅支持 **Python 3.7.3 以上版本**
需要注意的是NoneBot2 仅支持 **Python 3.7.3 以上版本**
## 特色
@ -18,15 +18,15 @@ NoneBot2 基于 Python [asyncio](https://docs.python.org/3/library/asyncio.html)
### 完整的类型注解
NoneBot2 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 `pyright`/`pylance` 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](./start/editor-support))
NoneBot2 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 `pyright`/`pylance` 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中[编辑器支持](./start/editor-support)
### 开箱即用
NoneBot2 提供了使用便捷、具有交互式功能的命令行工具 -- `nb-cli`,使得初次接触 NoneBot 时更容易上手。详细使用方法请参考各文档章节以及 [使用脚手架](./start/nb-cli)。
NoneBot2 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得初次接触 NoneBot2 时更容易上手。详细使用方法请参考各文档章节以及[使用脚手架](./start/nb-cli)。
### 插件系统
插件系统是 NoneBot 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。
插件系统是 NoneBot2 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。
### 依赖注入系统
@ -34,15 +34,15 @@ NoneBot2 采用了一套自行定义的依赖注入系统,可以让事件的
#### 什么是依赖注入
[**"依赖注入"**](https://zh.m.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西, 即"**依赖**"
[**“依赖注入”**](https://zh.m.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**“依赖”**
系统 (在这里是指 `NoneBot` ) 将负责做任何需要的事情,为你的代码提供这些必要依赖 (即"**注入**"依赖性)
系统(在这里是指 NoneBot2将负责做任何需要的事情为你的代码提供这些必要依赖即**“注入”**依赖性)
这在你有以下情形的需求时非常有用:
这在你有以下情形的需求时非常有用
- 这部分代码拥有共享的逻辑(同样的代码逻辑多次重复)
- 这部分代码拥有共享的逻辑(同样的代码逻辑多次重复)
- 共享数据库以及网络请求连接会话
- 比如 `httpx.AsyncClient`, `aiohttp.ClientSession``sqlalchemy.Session`
- 比如 `httpx.AsyncClient`、`aiohttp.ClientSession``sqlalchemy.Session`
- 用户权限检查以及认证
- 还有更多...

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