├── .coveragerc
├── .editorconfig
├── .env.prod
├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ ├── config.yml
│ └── feature_request.yml
├── dependabot.yml
├── release-drafter.yml
└── workflows
│ ├── codecov.yml
│ ├── release-draft.yml
│ └── release.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .python-version
├── .vscode
└── launch.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── nonebot_plugin_treehelp
├── __init__.py
├── config.py
└── data_source.py
├── pyproject.toml
├── requirements-dev.lock
├── requirements.lock
└── tests
├── __init__.py
├── conftest.py
├── plugins
├── adapters
│ ├── __init__.py
│ └── plugins
│ │ ├── sub1.py
│ │ ├── sub2.py
│ │ ├── sub3.py
│ │ ├── sub4.py
│ │ └── sub5.py
├── nested
│ ├── __init__.py
│ └── plugins
│ │ ├── sub1.py
│ │ └── sub2.py
└── tree
│ ├── __init__.py
│ └── plugins
│ ├── alc.py
│ ├── simple.py
│ └── sub
│ ├── __init__.py
│ └── subsub
│ └── sub2.py
├── test_adapters.py
├── test_help.py
├── test_ignored.py
├── test_nested.py
├── test_tree.py
└── utils.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | exclude_lines =
3 | pragma: no cover
4 | if TYPE_CHECKING:
5 | @(abc\.)?abstractmethod
6 | raise NotImplementedError
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_style = space
8 | indent_size = 2
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.py]
13 | indent_size = 4
14 |
15 | [*.md]
16 | max_line_length = off
17 | insert_final_newline = false
18 | trim_trailing_whitespace = false
19 |
--------------------------------------------------------------------------------
/.env.prod:
--------------------------------------------------------------------------------
1 | driver=~none
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: Bug 反馈
2 | title: "Bug: 出现异常"
3 | description: 提交 Bug 反馈以帮助我们改进代码
4 | labels: ["bug"]
5 | body:
6 | - type: textarea
7 | id: describe
8 | attributes:
9 | label: 描述问题
10 | description: 清晰简洁地说明问题是什么
11 | validations:
12 | required: true
13 |
14 | - type: textarea
15 | id: reproduction
16 | attributes:
17 | label: 复现步骤
18 | description: 提供能复现此问题的详细操作步骤
19 | placeholder: |
20 | 1. 首先……
21 | 2. 然后……
22 | 3. 发生……
23 | validations:
24 | required: true
25 |
26 | - type: textarea
27 | id: expected
28 | attributes:
29 | label: 期望的结果
30 | description: 清晰简洁地描述你期望发生的事情
31 |
32 | - type: textarea
33 | id: logs
34 | attributes:
35 | label: 截图或日志
36 | description: 提供有助于诊断问题的任何日志和截图
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 讨论区
4 | url: https://github.com/he0119/nonebot-plugin-treehelp/discussions
5 | about: 使用中若遇到问题,请先在这里求助。
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: 功能建议
2 | title: "Feature: 功能描述"
3 | description: 提出关于项目新功能的想法
4 | labels: ["enhancement"]
5 | body:
6 | - type: textarea
7 | id: problem
8 | attributes:
9 | label: 希望能解决的问题
10 | description: 在使用中遇到什么问题而需要新的功能?
11 | validations:
12 | required: true
13 |
14 | - type: textarea
15 | id: feature
16 | attributes:
17 | label: 描述所需要的功能
18 | description: 请说明需要的功能或解决方法
19 | validations:
20 | required: true
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "pip"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | groups:
8 | production-dependencies:
9 | dependency-type: "production"
10 | update-types:
11 | - "patch"
12 | - "minor"
13 | development-dependencies:
14 | dependency-type: "development"
15 | update-types:
16 | - "patch"
17 | - "minor"
18 | - "major"
19 | - package-ecosystem: "github-actions"
20 | directory: "/"
21 | schedule:
22 | interval: "daily"
23 | groups:
24 | actions:
25 | patterns:
26 | - "*"
27 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | template: $CHANGES
2 | name-template: "v$RESOLVED_VERSION"
3 | tag-template: "v$RESOLVED_VERSION"
4 | exclude-labels:
5 | - "dependencies"
6 | - "skip-changelog"
7 | autolabeler:
8 | - label: "bug"
9 | branch:
10 | - '/fix\/.+/'
11 | - label: "change"
12 | branch:
13 | - '/change\/.+/'
14 | - label: "enhancement"
15 | branch:
16 | - '/feature\/.+/'
17 | - '/feat\/.+/'
18 | - '/improve\/.+/'
19 | - label: "ci"
20 | files:
21 | - .github/**/*
22 | - label: "breaking-change"
23 | title:
24 | - "/.+!:.+/"
25 | categories:
26 | - title: 💥 破坏性变更
27 | labels:
28 | - breaking-change
29 | - title: 🚀 新功能
30 | labels:
31 | - enhancement
32 | - title: 🐛 Bug 修复
33 | labels:
34 | - bug
35 | - title: 💫 杂项
36 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
37 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
38 | version-resolver:
39 | major:
40 | labels:
41 | - "major"
42 | minor:
43 | labels:
44 | - "minor"
45 | patch:
46 | labels:
47 | - "patch"
48 | default: patch
49 |
--------------------------------------------------------------------------------
/.github/workflows/codecov.yml:
--------------------------------------------------------------------------------
1 | name: Code Coverage
2 |
3 | on:
4 | push:
5 | branches:
6 | - "main"
7 | pull_request:
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.ref }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | test:
15 | name: Test
16 | runs-on: ${{ matrix.os }}
17 | strategy:
18 | matrix:
19 | python-version: ["3.9", "3.10", "3.11", "3.12"]
20 | os: [ubuntu-latest, windows-latest, macos-latest]
21 | fail-fast: false
22 | env:
23 | OS: ${{ matrix.os }}
24 | PYTHON_VERSION: ${{ matrix.python-version }}
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v4
28 | - name: Setup rye
29 | uses: eifinger/setup-rye@d4c3ac7b15d8bf2e0b45e2d257c6b5cdbebc3643
30 | with:
31 | enable-cache: true
32 | cache-prefix: ${{ env.PYTHON_VERSION }}
33 | - name: Install prerequisites
34 | run: |
35 | rye pin ${{ env.PYTHON_VERSION }}
36 | rye sync
37 | - name: Run tests
38 | run: rye run test
39 | - name: Upload coverage to Codecov
40 | uses: codecov/codecov-action@v4
41 | with:
42 | token: ${{ secrets.CODECOV_TOKEN }}
43 | env_vars: OS,PYTHON_VERSION
44 |
45 | check:
46 | if: always()
47 | needs: test
48 | runs-on: ubuntu-latest
49 | steps:
50 | - name: Decide whether the needed jobs succeeded or failed
51 | uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe
52 | with:
53 | jobs: ${{ toJSON(needs) }}
54 |
--------------------------------------------------------------------------------
/.github/workflows/release-draft.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | types: [opened, reopened, synchronize]
9 |
10 | jobs:
11 | update_release_draft:
12 | name: Update Release Draft
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: release-drafter/release-drafter@v6
16 | env:
17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | release:
10 | name: Release
11 | runs-on: ubuntu-latest
12 | environment: release
13 | permissions:
14 | id-token: write
15 | contents: write
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v4
19 |
20 | - name: Setup rye
21 | uses: eifinger/setup-rye@d4c3ac7b15d8bf2e0b45e2d257c6b5cdbebc3643
22 | with:
23 | enable-cache: true
24 |
25 | - name: Install prerequisites
26 | run: rye sync
27 |
28 | - name: Get Version
29 | id: version
30 | run: |
31 | echo "VERSION=$(rye version)" >> $GITHUB_OUTPUT
32 | echo "TAG_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
33 | echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
34 |
35 | - name: Check Version
36 | if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION
37 | run: exit 1
38 |
39 | - name: Build
40 | run: rye build
41 |
42 | - name: Publish a Python distribution to PyPI
43 | uses: pypa/gh-action-pypi-publish@release/v1
44 |
45 | - name: Upload Release Asset
46 | run: gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl
47 | env:
48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/python
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python
3 |
4 | ### Python ###
5 | # Byte-compiled / optimized / DLL files
6 | __pycache__/
7 | *.py[cod]
8 | *$py.class
9 |
10 | # C extensions
11 | *.so
12 |
13 | # Distribution / packaging
14 | .Python
15 | build/
16 | develop-eggs/
17 | dist/
18 | downloads/
19 | eggs/
20 | .eggs/
21 | lib/
22 | lib64/
23 | parts/
24 | sdist/
25 | var/
26 | wheels/
27 | pip-wheel-metadata/
28 | share/python-wheels/
29 | *.egg-info/
30 | .installed.cfg
31 | *.egg
32 | MANIFEST
33 |
34 | # PyInstaller
35 | # Usually these files are written by a python script from a template
36 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
37 | *.manifest
38 | *.spec
39 |
40 | # Installer logs
41 | pip-log.txt
42 | pip-delete-this-directory.txt
43 |
44 | # Unit test / coverage reports
45 | htmlcov/
46 | .tox/
47 | .nox/
48 | .coverage
49 | .coverage.*
50 | .cache
51 | nosetests.xml
52 | coverage.xml
53 | *.cover
54 | *.py,cover
55 | .hypothesis/
56 | .pytest_cache/
57 | pytestdebug.log
58 |
59 | # Translations
60 | *.mo
61 | *.pot
62 |
63 | # Django stuff:
64 | *.log
65 | local_settings.py
66 | db.sqlite3
67 | db.sqlite3-journal
68 |
69 | # Flask stuff:
70 | instance/
71 | .webassets-cache
72 |
73 | # Scrapy stuff:
74 | .scrapy
75 |
76 | # Sphinx documentation
77 | docs/_build/
78 | doc/_build/
79 |
80 | # PyBuilder
81 | target/
82 |
83 | # Jupyter Notebook
84 | .ipynb_checkpoints
85 |
86 | # IPython
87 | profile_default/
88 | ipython_config.py
89 |
90 | # pyenv
91 | .python-version
92 |
93 | # pipenv
94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
97 | # install all needed dependencies.
98 | #Pipfile.lock
99 |
100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
101 | __pypackages__/
102 |
103 | # Celery stuff
104 | celerybeat-schedule
105 | celerybeat.pid
106 |
107 | # SageMath parsed files
108 | *.sage.py
109 |
110 | # Environments
111 | .env
112 | .venv
113 | env/
114 | venv/
115 | ENV/
116 | env.bak/
117 | venv.bak/
118 |
119 | # Spyder project settings
120 | .spyderproject
121 | .spyproject
122 |
123 | # Rope project settings
124 | .ropeproject
125 |
126 | # mkdocs documentation
127 | /site
128 |
129 | # mypy
130 | .mypy_cache/
131 | .dmypy.json
132 | dmypy.json
133 |
134 | # Pyre type checker
135 | .pyre/
136 |
137 | # pytype static type analyzer
138 | .pytype/
139 |
140 | # End of https://www.toptal.com/developers/gitignore/api/python
141 |
142 | # VSCode
143 | .vscode/*
144 | !.vscode/launch.json
145 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | ci:
2 | autofix_commit_msg: "style: auto fix by pre-commit hooks"
3 | autofix_prs: true
4 | autoupdate_branch: main
5 | autoupdate_schedule: quarterly
6 | autoupdate_commit_msg: "chore: auto update by pre-commit hooks"
7 | repos:
8 | - repo: https://github.com/astral-sh/ruff-pre-commit
9 | rev: v0.5.0
10 | hooks:
11 | - id: ruff
12 | args: [--fix, --exit-non-zero-on-fix]
13 | stages: [commit]
14 | - id: ruff-format
15 |
16 | - repo: https://github.com/pre-commit/mirrors-prettier
17 | rev: v4.0.0-alpha.8
18 | hooks:
19 | - id: prettier
20 | types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
21 | stages: [commit]
22 |
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.12.3
2 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // 使用 IntelliSense 了解相关属性。
3 | // 悬停以查看现有属性的描述。
4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Run",
9 | "type": "python",
10 | "request": "launch",
11 | "module": "nb_cli",
12 | "args": ["run", "--reload"],
13 | "justMyCode": false
14 | },
15 | {
16 | "name": "Pytest",
17 | "type": "python",
18 | "request": "launch",
19 | "module": "pytest"
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/lang/zh-CN/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ## [0.5.0] - 2024-07-13
11 |
12 | ### Fixed
13 |
14 | - 适配最新版的 Alconna
15 |
16 | ### Removed
17 |
18 | - 移除 Python 3.8 支持
19 |
20 | ## [0.4.1] - 2024-03-01
21 |
22 | ### Fixed
23 |
24 | - 修复命令没有 shortcut 时的报错
25 |
26 | ## [0.4.0] - 2024-02-29
27 |
28 | ### Added
29 |
30 | - 支持 Alconna
31 | - 适配 Pydantic V2
32 |
33 | ## [0.3.0] - 2023-06-17
34 |
35 | ### Added
36 |
37 | - 通过插件名称忽略指定插件
38 |
39 | ## [0.2.0] - 2023-06-06
40 |
41 | ### Added
42 |
43 | - 适配 NoneBot 2.0 正式版的插件元数据
44 |
45 | ## [0.1.0] - 2023-04-16
46 |
47 | ### Changed
48 |
49 | - 发送 /help 默认显示 /help -l 的内容
50 |
51 | ## [0.0.5] - 2023-03-06
52 |
53 | ### Fixed
54 |
55 | - 修复设置多个分隔符时无法匹配的问题
56 |
57 | ## [0.0.4] - 2023-03-06
58 |
59 | ### Added
60 |
61 | - 支持通过插件内命令来获取插件帮助
62 |
63 | ## [0.0.3] - 2023-02-18
64 |
65 | ### Changed
66 |
67 | - 美化插件树的显示
68 | - 使用 shell_command 处理命令
69 |
70 | ## [0.0.2] - 2023-02-06
71 |
72 | ### Added
73 |
74 | - 支持输出插件树
75 | - 支持通过设置 adapters 属性来指定支持的适配器
76 |
77 | ### Fixed
78 |
79 | - 修复子插件导入其他子插件时的错误判断
80 |
81 | ## [0.0.1] - 2022-07-02
82 |
83 | ### Added
84 |
85 | - 可以使用的版本。
86 |
87 | [unreleased]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.5.0...HEAD
88 | [0.5.0]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.4.1...v0.5.0
89 | [0.4.1]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.4.0...v0.4.1
90 | [0.4.0]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.3.0...v0.4.0
91 | [0.3.0]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.2.0...v0.3.0
92 | [0.2.0]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.1.0...v0.2.0
93 | [0.1.0]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.0.5...v0.1.0
94 | [0.0.5]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.0.4...v0.0.5
95 | [0.0.4]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.0.3...v0.0.4
96 | [0.0.3]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.0.2...v0.0.3
97 | [0.0.2]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.0.1...v0.0.2
98 | [0.0.1]: https://github.com/he0119/nonebot-plugin-treehelp/releases/tag/v0.0.1
99 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 hemengyang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | # NoneBot Plugin TreeHelp
10 |
11 | _✨ NoneBot 树形帮助插件 ✨_
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ## 简介
32 |
33 | 使用插件元数据获取插件信息,并通过插件与子插件的组织形式,来区分插件的多种功能。
34 |
35 | 树形帮助插件,最重要的功能当然是显示插件树!
36 |
37 | 发送 `/help --tree`,你将获得如下帮助:
38 |
39 | ```text
40 | 插件:
41 | 帮助 # 获取插件帮助信息
42 | 测试 # 一个测试插件
43 | ├── 复杂功能 # 测试插件复杂子插件
44 | │ └── 二级功能 # 测试插件二级插件
45 | └── 简单功能 # 测试插件简单子插件
46 | ```
47 |
48 | ## 使用方式
49 |
50 | 加载插件后发送 `/help help` 或 `/help --help` 获取具体用法。
51 |
52 | ## 插件适配
53 |
54 | 插件与子插件写法可参考 [示例插件](./tests/plugins/tree/)。
55 |
56 | ### 声明适配器
57 |
58 | 通过设置 adapters 属性来指定支持的适配器。如果不设置或留空则默认支持全部适配器。如果插件不支持该适配器,则不会在帮助列表上显示。
59 |
60 | ```python
61 | __plugin_meta__ = PluginMetadata(
62 | name="OneBot",
63 | description="测试 OneBot 适配器",
64 | usage="/onebot",
65 | type="application",
66 | supported_adapters={"~onebot.v11", "~onebot.v12"},
67 | )
68 | ```
69 |
70 | ## 配置项
71 |
72 | 配置方式:直接在 `NoneBot` 全局配置文件中添加以下配置项即可。
73 |
74 | ### treehelp_ignored_plugins
75 |
76 | - 类型: `list[str]`
77 | - 默认: `[]`
78 | - 说明: 需要忽略的插件名称列表
79 |
80 | ## 计划
81 |
82 | - [ ] 支持输出插件版本
83 | - [x] 支持输出插件树
84 | - [ ] 支持输出插件内的命令名称
85 |
--------------------------------------------------------------------------------
/nonebot_plugin_treehelp/__init__.py:
--------------------------------------------------------------------------------
1 | """帮助
2 |
3 | 通过读取插件元信息生成帮助信息
4 | """
5 |
6 | from nonebot import on_shell_command
7 | from nonebot.adapters import Bot
8 | from nonebot.exception import ParserExit
9 | from nonebot.params import ShellCommandArgs
10 | from nonebot.plugin import PluginMetadata
11 | from nonebot.rule import ArgumentParser, Namespace
12 |
13 | from .config import Config
14 | from .data_source import get_plugin_help, get_plugin_list
15 |
16 | __plugin_meta__ = PluginMetadata(
17 | name="帮助",
18 | description="获取插件帮助信息",
19 | usage="""获取插件列表
20 | /help
21 | 获取插件树
22 | /help -t
23 | /help --tree
24 | 获取某个插件的帮助
25 | /help 插件名
26 | 获取某个插件的树
27 | /help --tree 插件名
28 | """,
29 | type="application",
30 | homepage="https://github.com/he0119/nonebot-plugin-treehelp",
31 | config=Config,
32 | )
33 |
34 | parser = ArgumentParser("帮助", description="获取插件帮助信息")
35 | parser.add_argument("plugin_name", nargs="?", type=str, help="插件名", metavar="插件名")
36 | parser.add_argument("-t", "--tree", action="store_true", help="获取插件树")
37 | help_cmd = on_shell_command("help", aliases={"帮助"}, parser=parser)
38 |
39 |
40 | @help_cmd.handle()
41 | async def help_handle(bot: Bot, args: Namespace = ShellCommandArgs()):
42 | plugin_name = args.plugin_name
43 |
44 | if plugin_name is None:
45 | await help_cmd.finish(get_plugin_list(bot, args.tree))
46 |
47 | plugin_help = get_plugin_help(bot, plugin_name, args.tree)
48 | if plugin_help:
49 | await help_cmd.finish(plugin_help)
50 | else:
51 | await help_cmd.finish(f"未找到插件 {plugin_name}")
52 |
53 |
54 | @help_cmd.handle()
55 | async def _(foo: ParserExit = ShellCommandArgs()):
56 | await help_cmd.finish(foo.message)
57 |
--------------------------------------------------------------------------------
/nonebot_plugin_treehelp/config.py:
--------------------------------------------------------------------------------
1 | """配置"""
2 |
3 | from nonebot import get_plugin_config
4 | from pydantic import BaseModel
5 |
6 |
7 | class Config(BaseModel):
8 | treehelp_ignored_plugins: list[str] = []
9 | """需要忽略的插件"""
10 |
11 |
12 | plugin_config = get_plugin_config(Config)
13 |
--------------------------------------------------------------------------------
/nonebot_plugin_treehelp/data_source.py:
--------------------------------------------------------------------------------
1 | """帮助数据
2 |
3 | 获取插件的帮助信息,并通过子插件的形式获取次级菜单
4 | """
5 |
6 | from typing import TYPE_CHECKING, Optional, Union, cast
7 |
8 | from nonebot import get_driver, get_loaded_plugins, require
9 | from nonebot.rule import CommandRule, ShellCommandRule
10 |
11 | try:
12 | require("nonebot_plugin_alconna")
13 | from nonebot_plugin_alconna import command_manager
14 | from nonebot_plugin_alconna.rule import AlconnaRule
15 |
16 | COMMAND_RULES = (CommandRule, ShellCommandRule, AlconnaRule)
17 | except (ImportError, RuntimeError): # pragma: no cover
18 | command_manager = None
19 | AlconnaRule = None
20 | COMMAND_RULES = (CommandRule, ShellCommandRule)
21 |
22 | from .config import plugin_config
23 |
24 | if TYPE_CHECKING:
25 | from nonebot.adapters import Bot
26 | from nonebot.plugin import Plugin, PluginMetadata
27 |
28 | global_config = get_driver().config
29 |
30 | _plugins: Optional[dict[str, "Plugin"]] = None
31 | _commands: dict[tuple[str, ...], "Plugin"] = {}
32 |
33 |
34 | def map_command_to_plguin(plugin: "Plugin"):
35 | """建立命令与插件的映射"""
36 | matchers = plugin.matcher
37 | for matcher in matchers:
38 | checkers = matcher.rule.checkers
39 | command_handler = next(
40 | filter(lambda x: isinstance(x.call, COMMAND_RULES), checkers),
41 | None,
42 | )
43 | if not command_handler:
44 | continue
45 |
46 | if (
47 | AlconnaRule
48 | and isinstance(command_handler.call, AlconnaRule)
49 | and command_manager
50 | ):
51 | command = command_handler.call.command()
52 | if not command: # pragma: no cover
53 | continue
54 |
55 | cmds = [(str(command.command),)]
56 | shortcuts = command_manager.get_shortcut(command)
57 | cmds.extend([(shortcut,) for shortcut in shortcuts])
58 |
59 | else:
60 | command = cast(Union[CommandRule, ShellCommandRule], command_handler.call)
61 | cmds = command.cmds
62 |
63 | for cmd in cmds:
64 | _commands[cmd] = plugin
65 |
66 |
67 | def format_description(plugins: list["Plugin"]) -> str:
68 | """格式化描述"""
69 | return "\n".join(
70 | sorted(f"{x.metadata.name} # {x.metadata.description}" for x in plugins) # type: ignore
71 | )
72 |
73 |
74 | def is_supported_adapter(bot: "Bot", metadata: "PluginMetadata") -> bool:
75 | """是否是支持的适配器"""
76 | if metadata.supported_adapters is None:
77 | return True
78 |
79 | supported_adapters = metadata.get_supported_adapters()
80 | if not supported_adapters:
81 | return False
82 |
83 | for adapter in supported_adapters:
84 | if isinstance(bot.adapter, adapter):
85 | return True
86 |
87 | return False
88 |
89 |
90 | def is_supported_type(metadata: "PluginMetadata") -> bool:
91 | """是否是支持的插件类型
92 |
93 | 当前有 library 和 application 两种类型
94 | 仅支持 application 类型的插件
95 | """
96 | type_ = metadata.type
97 | # 如果没有指定类型,则默认支持
98 | if type_ is None:
99 | return True
100 | # 当前仅支持 application 类型
101 | if type_ == "application":
102 | return True
103 | return False
104 |
105 |
106 | def is_supported(bot: "Bot", plugin: "Plugin") -> bool:
107 | """是否是支持的插件"""
108 | if plugin.metadata is None:
109 | return False
110 |
111 | if plugin.metadata.name in plugin_config.treehelp_ignored_plugins:
112 | return False
113 |
114 | if not is_supported_type(plugin.metadata):
115 | return False
116 |
117 | if not is_supported_adapter(bot, plugin.metadata):
118 | return False
119 |
120 | return True
121 |
122 |
123 | def get_plugins() -> dict[str, "Plugin"]:
124 | """获取适配了元信息的插件"""
125 | global _plugins
126 |
127 | if _plugins is None:
128 | plugins = filter(lambda x: x.metadata is not None, get_loaded_plugins())
129 | _plugins = {x.metadata.name: x for x in plugins} # type: ignore
130 | for plugin in _plugins.values():
131 | map_command_to_plguin(plugin)
132 |
133 | return _plugins
134 |
135 |
136 | def get_plugin_list(bot: "Bot", tree: bool = False) -> str:
137 | """获取插件列表"""
138 | # 仅保留根插件
139 | plugins = [
140 | plugin
141 | for plugin in get_plugins().values()
142 | if plugin.parent_plugin is None and is_supported(bot, plugin)
143 | ]
144 | sorted_plugins = sorted(plugins, key=lambda x: x.metadata.name) # type: ignore
145 |
146 | docs = ["插件:"]
147 | if tree:
148 | for plugin in sorted_plugins:
149 | docs.append(f"{plugin.metadata.name} # {plugin.metadata.description}") # type: ignore
150 | get_tree_string(bot, docs, plugin.sub_plugins, "")
151 | else:
152 | docs.append(format_description(sorted_plugins))
153 | return "\n".join(docs)
154 |
155 |
156 | def get_plugin_help(bot: "Bot", name: str, tree: bool = False) -> Optional[str]:
157 | """通过插件获取命令的帮助"""
158 | plugins = get_plugins()
159 |
160 | plugin = plugins.get(name)
161 | if not plugin:
162 | # str.split 只支持单个分隔符
163 | # 用 re 又太麻烦了,直接遍历所有分隔符,找到就停止
164 | for sep in global_config.command_sep:
165 | plugin = _commands.get(tuple(name.split(sep)))
166 | if plugin:
167 | break
168 | if not plugin:
169 | return
170 |
171 | # 排除不支持的插件
172 | if not is_supported(bot, plugin):
173 | return
174 |
175 | metadata = cast("PluginMetadata", plugin.metadata)
176 |
177 | if tree:
178 | docs = [f"{metadata.name} # {metadata.description}"]
179 | get_tree_string(bot, docs, plugin.sub_plugins, "")
180 | return "\n".join(docs)
181 |
182 | sub_plugins = [plugin for plugin in plugin.sub_plugins if is_supported(bot, plugin)]
183 | sub_plugins_desc = format_description(sub_plugins)
184 | return "\n\n".join(
185 | [x for x in [metadata.name, metadata.usage, sub_plugins_desc] if x]
186 | )
187 |
188 |
189 | def get_tree_string(
190 | bot: "Bot",
191 | docs: list[str],
192 | plugins: set["Plugin"],
193 | previous_tree_bar: str,
194 | ) -> None:
195 | """通过递归获取树形结构的字符串"""
196 | previous_tree_bar = previous_tree_bar.replace("├", "│")
197 |
198 | filtered_plugins = filter(lambda x: is_supported(bot, x), plugins)
199 | sorted_plugins = sorted(filtered_plugins, key=lambda x: x.metadata.name) # type: ignore
200 |
201 | tree_bar = previous_tree_bar + "├"
202 | total = len(sorted_plugins)
203 | for i, plugin in enumerate(sorted_plugins, 1):
204 | if i == total:
205 | tree_bar = previous_tree_bar + "└"
206 | docs.append(
207 | f"{tree_bar}── {plugin.metadata.name} # {plugin.metadata.description}" # type: ignore
208 | )
209 | tree_bar = tree_bar.replace("└", " ")
210 | get_tree_string(bot, docs, plugin.sub_plugins, tree_bar + " ")
211 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "nonebot-plugin-treehelp"
3 | version = "0.5.0"
4 | description = "适用于 Nonebot2 的树形帮助插件"
5 | authors = [{ name = "uy_sun", email = "hmy0119@gmail.com" }]
6 | dependencies = ["nonebot2>=2.2.0"]
7 | readme = "README.md"
8 | license = { file = "LICENSE" }
9 | requires-python = ">= 3.9"
10 |
11 | [project.urls]
12 | Homepage = "https://github.com/he0119/nonebot-plugin-treehelp"
13 | Repository = "https://github.com/he0119/nonebot-plugin-treehelp.git"
14 | Issues = "https://github.com/he0119/nonebot-plugin-treehelp/issues"
15 | Changelog = "https://github.com/he0119/nonebot-plugin-treehelp/blob/main/CHANGELOG.md"
16 |
17 | [project.optional-dependencies]
18 | alconna = ["nonebot-plugin-alconna>=0.49.0"]
19 |
20 | [build-system]
21 | requires = ["hatchling"]
22 | build-backend = "hatchling.build"
23 |
24 | [tool.hatch.metadata]
25 | allow-direct-references = true
26 |
27 | [tool.hatch.build.targets.wheel]
28 | packages = ["nonebot_plugin_treehelp"]
29 |
30 | [tool.hatch.build.targets.sdist]
31 | only-include = ["nonebot_plugin_treehelp"]
32 |
33 | [tool.rye]
34 | managed = true
35 | universal = true
36 | dev-dependencies = [
37 | "nonebug>=0.3.7",
38 | "pytest-cov>=5.0.0",
39 | "pytest-xdist>=3.6.1",
40 | "pytest-mock>=3.14.0",
41 | "pytest-asyncio>=0.23.7",
42 | "nonebot2[fastapi]>=2.3.2",
43 | "nonebot-adapter-onebot>=2.4.4",
44 | "nonebot-adapter-console>=0.5.0",
45 | "nonebot-plugin-alconna>=0.49.0",
46 | ]
47 |
48 | [tool.rye.scripts]
49 | test = "pytest --cov=nonebot_plugin_treehelp --cov-report xml -n auto"
50 |
51 | [tool.pyright]
52 | pythonVersion = "3.9"
53 | pythonPlatform = "All"
54 | typeCheckingMode = "standard"
55 |
56 | [tool.ruff]
57 | line-length = 88
58 | target-version = "py39"
59 |
60 | [tool.ruff.lint]
61 | select = [
62 | "W", # pycodestyle warnings
63 | "E", # pycodestyle errors
64 | "F", # pyflakes
65 | "UP", # pyupgrade
66 | "C4", # flake8-comprehensions
67 | "T10", # flake8-debugger
68 | "T20", # flake8-print
69 | "PYI", # flake8-pyi
70 | "PT", # flake8-pytest-style
71 | "Q", # flake8-quotes
72 | ]
73 | ignore = [
74 | "E402", # module-import-not-at-top-of-file
75 | ]
76 |
77 | [tool.nonebot]
78 | adapters = [
79 | { name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" },
80 | ]
81 | plugins = ["nonebot_plugin_treehelp", "tests.plugins.tree"]
82 |
83 | [tool.coverage.report]
84 | exclude_lines = [
85 | "pragma: no cover",
86 | "raise NotImplementedError",
87 | "if __name__ == .__main__.:",
88 | "if TYPE_CHECKING:",
89 | "@overload",
90 | "except ImportError:",
91 | ]
92 |
93 | [tool.pytest.ini_options]
94 | addopts = ["--import-mode=importlib"]
95 | asyncio_mode = "auto"
96 |
--------------------------------------------------------------------------------
/requirements-dev.lock:
--------------------------------------------------------------------------------
1 | # generated by rye
2 | # use `rye lock` or `rye sync` to update this lockfile
3 | #
4 | # last locked with the following flags:
5 | # pre: false
6 | # features: []
7 | # all-features: false
8 | # with-sources: false
9 | # generate-hashes: false
10 | # universal: true
11 |
12 | -e file:.
13 | annotated-types==0.7.0
14 | # via pydantic
15 | anyio==4.4.0
16 | # via httpx
17 | # via starlette
18 | # via watchfiles
19 | arclet-alconna==1.8.18
20 | # via arclet-alconna-tools
21 | # via nonebot-plugin-alconna
22 | arclet-alconna-tools==0.7.6
23 | # via nonebot-plugin-alconna
24 | asgiref==3.8.1
25 | # via nonebug
26 | async-asgi-testclient==1.4.11
27 | # via nonebug
28 | certifi==2024.7.4
29 | # via httpcore
30 | # via httpx
31 | # via requests
32 | charset-normalizer==3.3.2
33 | # via requests
34 | click==8.1.7
35 | # via typer
36 | # via uvicorn
37 | colorama==0.4.6 ; platform_system == 'Windows' or sys_platform == 'win32'
38 | # via click
39 | # via loguru
40 | # via pytest
41 | # via uvicorn
42 | coverage==7.6.0
43 | # via pytest-cov
44 | dnspython==2.6.1
45 | # via email-validator
46 | email-validator==2.2.0
47 | # via fastapi
48 | execnet==2.1.1
49 | # via pytest-xdist
50 | fastapi==0.111.0
51 | # via nonebot2
52 | fastapi-cli==0.0.4
53 | # via fastapi
54 | h11==0.14.0
55 | # via httpcore
56 | # via uvicorn
57 | httpcore==1.0.5
58 | # via httpx
59 | httptools==0.6.1
60 | # via uvicorn
61 | httpx==0.27.0
62 | # via fastapi
63 | idna==3.7
64 | # via anyio
65 | # via email-validator
66 | # via httpx
67 | # via requests
68 | # via yarl
69 | importlib-metadata==8.0.0
70 | # via nonebot-plugin-alconna
71 | # via textual
72 | iniconfig==2.0.0
73 | # via pytest
74 | jinja2==3.1.4
75 | # via fastapi
76 | linkify-it-py==2.0.3
77 | # via markdown-it-py
78 | loguru==0.7.2
79 | # via nonebot2
80 | markdown-it-py==3.0.0
81 | # via mdit-py-plugins
82 | # via rich
83 | # via textual
84 | markupsafe==2.1.5
85 | # via jinja2
86 | mdit-py-plugins==0.4.1
87 | # via markdown-it-py
88 | mdurl==0.1.2
89 | # via markdown-it-py
90 | msgpack==1.0.8
91 | # via nonebot-adapter-onebot
92 | multidict==6.0.5
93 | # via async-asgi-testclient
94 | # via yarl
95 | nepattern==0.7.4
96 | # via arclet-alconna
97 | # via arclet-alconna-tools
98 | # via nonebot-plugin-alconna
99 | nonebot-adapter-console==0.6.0
100 | nonebot-adapter-onebot==2.4.4
101 | nonebot-plugin-alconna==0.49.0
102 | nonebot-plugin-waiter==0.6.2
103 | # via nonebot-plugin-alconna
104 | nonebot2==2.3.2
105 | # via nonebot-adapter-console
106 | # via nonebot-adapter-onebot
107 | # via nonebot-plugin-alconna
108 | # via nonebot-plugin-treehelp
109 | # via nonebot-plugin-waiter
110 | # via nonebug
111 | nonebug==0.3.7
112 | nonechat==0.2.1
113 | # via nonebot-adapter-console
114 | orjson==3.10.6
115 | # via fastapi
116 | packaging==24.1
117 | # via pytest
118 | pluggy==1.5.0
119 | # via pytest
120 | pydantic==2.8.2
121 | # via fastapi
122 | # via nonebot-adapter-console
123 | # via nonebot-adapter-onebot
124 | # via nonebot2
125 | pydantic-core==2.20.1
126 | # via pydantic
127 | pygments==2.18.0
128 | # via rich
129 | pygtrie==2.5.0
130 | # via nonebot2
131 | pytest==8.2.2
132 | # via nonebug
133 | # via pytest-asyncio
134 | # via pytest-cov
135 | # via pytest-mock
136 | # via pytest-xdist
137 | pytest-asyncio==0.23.7
138 | pytest-cov==5.0.0
139 | pytest-mock==3.14.0
140 | pytest-xdist==3.6.1
141 | python-dotenv==1.0.1
142 | # via nonebot2
143 | # via uvicorn
144 | python-multipart==0.0.9
145 | # via fastapi
146 | pyyaml==6.0.1
147 | # via uvicorn
148 | requests==2.32.3
149 | # via async-asgi-testclient
150 | rich==13.7.1
151 | # via textual
152 | # via typer
153 | shellingham==1.5.4
154 | # via typer
155 | sniffio==1.3.1
156 | # via anyio
157 | # via httpx
158 | starlette==0.37.2
159 | # via fastapi
160 | tarina==0.5.4
161 | # via arclet-alconna
162 | # via nepattern
163 | # via nonebot-plugin-alconna
164 | textual==0.29.0
165 | # via nonechat
166 | typer==0.12.3
167 | # via fastapi-cli
168 | typing-extensions==4.12.2
169 | # via arclet-alconna
170 | # via fastapi
171 | # via nepattern
172 | # via nonebot-adapter-console
173 | # via nonebot-adapter-onebot
174 | # via nonebot2
175 | # via nonebug
176 | # via pydantic
177 | # via pydantic-core
178 | # via tarina
179 | # via textual
180 | # via typer
181 | uc-micro-py==1.0.3
182 | # via linkify-it-py
183 | ujson==5.10.0
184 | # via fastapi
185 | urllib3==2.2.2
186 | # via requests
187 | uvicorn==0.30.1
188 | # via fastapi
189 | # via nonebot2
190 | uvloop==0.19.0 ; sys_platform != 'win32' and (platform_python_implementation != 'PyPy' and sys_platform != 'cygwin')
191 | # via uvicorn
192 | watchfiles==0.22.0
193 | # via uvicorn
194 | websockets==12.0
195 | # via uvicorn
196 | win32-setctime==1.1.0 ; sys_platform == 'win32'
197 | # via loguru
198 | yarl==1.9.4
199 | # via nonebot2
200 | zipp==3.19.2
201 | # via importlib-metadata
202 |
--------------------------------------------------------------------------------
/requirements.lock:
--------------------------------------------------------------------------------
1 | # generated by rye
2 | # use `rye lock` or `rye sync` to update this lockfile
3 | #
4 | # last locked with the following flags:
5 | # pre: false
6 | # features: []
7 | # all-features: false
8 | # with-sources: false
9 | # generate-hashes: false
10 | # universal: true
11 |
12 | -e file:.
13 | annotated-types==0.7.0
14 | # via pydantic
15 | colorama==0.4.6 ; sys_platform == 'win32'
16 | # via loguru
17 | idna==3.7
18 | # via yarl
19 | loguru==0.7.2
20 | # via nonebot2
21 | multidict==6.0.5
22 | # via yarl
23 | nonebot2==2.3.2
24 | # via nonebot-plugin-treehelp
25 | pydantic==2.8.2
26 | # via nonebot2
27 | pydantic-core==2.20.1
28 | # via pydantic
29 | pygtrie==2.5.0
30 | # via nonebot2
31 | python-dotenv==1.0.1
32 | # via nonebot2
33 | typing-extensions==4.12.2
34 | # via nonebot2
35 | # via pydantic
36 | # via pydantic-core
37 | win32-setctime==1.1.0 ; sys_platform == 'win32'
38 | # via loguru
39 | yarl==1.9.4
40 | # via nonebot2
41 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/he0119/nonebot-plugin-treehelp/0307a850d008269c12b3afeb720a86f7fee36380/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import nonebot
2 | import pytest
3 | from nonebug import NONEBOT_INIT_KWARGS, App
4 |
5 | from .utils import clear_plugins
6 |
7 |
8 | def pytest_configure(config: pytest.Config) -> None:
9 | config.stash[NONEBOT_INIT_KWARGS] = {"command_sep": [".", "。"]}
10 |
11 |
12 | @pytest.fixture(autouse=True)
13 | def _register_adapters(nonebug_init: None):
14 | from nonebot import get_driver
15 | from nonebot.adapters.console import Adapter as ConsoleAdapter
16 | from nonebot.adapters.onebot.v11 import Adapter as OnebotV11Adapter
17 | from nonebot.adapters.onebot.v12 import Adapter as OnebotV12Adapter
18 |
19 | driver = get_driver()
20 | driver.register_adapter(ConsoleAdapter)
21 | driver.register_adapter(OnebotV11Adapter)
22 | driver.register_adapter(OnebotV12Adapter)
23 |
24 |
25 | @pytest.fixture()
26 | def app(nonebug_init: None):
27 | clear_plugins()
28 | # 加载插件
29 | nonebot.load_plugin("nonebot_plugin_treehelp")
30 | return App()
31 |
--------------------------------------------------------------------------------
/tests/plugins/adapters/__init__.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import nonebot
4 | from nonebot.plugin import PluginMetadata
5 |
6 | __plugin_meta__ = PluginMetadata(
7 | name="适配器",
8 | description="测试不同适配器",
9 | usage="",
10 | type="application",
11 | )
12 |
13 | _sub_plugins = set()
14 | _sub_plugins |= nonebot.load_plugins(str((Path(__file__).parent / "plugins").resolve()))
15 |
--------------------------------------------------------------------------------
/tests/plugins/adapters/plugins/sub1.py:
--------------------------------------------------------------------------------
1 | from nonebot.plugin import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="OneBot",
5 | description="测试 OneBot 适配器",
6 | usage="/onebot",
7 | supported_adapters={"~onebot.v11", "~onebot.v12"},
8 | )
9 |
--------------------------------------------------------------------------------
/tests/plugins/adapters/plugins/sub2.py:
--------------------------------------------------------------------------------
1 | from nonebot.plugin import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="Console",
5 | description="测试 Console 适配器",
6 | usage="/console",
7 | supported_adapters={"~console"},
8 | )
9 |
--------------------------------------------------------------------------------
/tests/plugins/adapters/plugins/sub3.py:
--------------------------------------------------------------------------------
1 | from nonebot.plugin import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="library",
5 | description="测试库插件",
6 | usage="/library",
7 | supported_adapters={"~console"},
8 | type="library",
9 | )
10 |
--------------------------------------------------------------------------------
/tests/plugins/adapters/plugins/sub4.py:
--------------------------------------------------------------------------------
1 | """插件支持的适配器不存在"""
2 |
3 | from nonebot.plugin import PluginMetadata
4 |
5 | __plugin_meta__ = PluginMetadata(
6 | name="Console",
7 | description="测试 Console 适配器",
8 | usage="/console",
9 | supported_adapters={"~fake"},
10 | )
11 |
--------------------------------------------------------------------------------
/tests/plugins/adapters/plugins/sub5.py:
--------------------------------------------------------------------------------
1 | """没有插件元数据"""
2 |
--------------------------------------------------------------------------------
/tests/plugins/nested/__init__.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import nonebot
4 | from nonebot.plugin import PluginMetadata
5 |
6 | __plugin_meta__ = PluginMetadata(
7 | name="测试",
8 | description="一个测试插件",
9 | usage="",
10 | )
11 |
12 | _sub_plugins = set()
13 | _sub_plugins |= nonebot.load_plugins(str((Path(__file__).parent / "plugins").resolve()))
14 |
--------------------------------------------------------------------------------
/tests/plugins/nested/plugins/sub1.py:
--------------------------------------------------------------------------------
1 | from nonebot.plugin import PluginMetadata
2 |
3 | plugin_id = 1
4 |
5 | __plugin_meta__ = PluginMetadata(
6 | name="功能一",
7 | description="测试插件子插件一",
8 | usage="/功能一",
9 | )
10 |
--------------------------------------------------------------------------------
/tests/plugins/nested/plugins/sub2.py:
--------------------------------------------------------------------------------
1 | from nonebot.plugin import PluginMetadata
2 |
3 | from .sub1 import plugin_id # noqa: F401
4 |
5 | __plugin_meta__ = PluginMetadata(
6 | name="功能二",
7 | description="测试插件子插件二",
8 | usage="/功能二",
9 | )
10 |
--------------------------------------------------------------------------------
/tests/plugins/tree/__init__.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import nonebot
4 | from nonebot.plugin import PluginMetadata
5 |
6 | __plugin_meta__ = PluginMetadata(
7 | name="测试",
8 | description="一个测试插件",
9 | usage="",
10 | )
11 |
12 | _sub_plugins = set()
13 | _sub_plugins |= nonebot.load_plugins(str((Path(__file__).parent / "plugins").resolve()))
14 |
--------------------------------------------------------------------------------
/tests/plugins/tree/plugins/alc.py:
--------------------------------------------------------------------------------
1 | from nonebot import require
2 | from nonebot.plugin import PluginMetadata
3 |
4 | require("nonebot_plugin_alconna")
5 | from nonebot_plugin_alconna import Alconna, on_alconna
6 |
7 | __plugin_meta__ = PluginMetadata(
8 | name="Alconna",
9 | description="测试插件 alconna 子插件",
10 | usage="/alconna",
11 | )
12 |
13 | alconna = on_alconna(Alconna("alconna"))
14 | alconna.shortcut(
15 | "alc",
16 | {
17 | "prefix": True,
18 | "command": "alconna",
19 | },
20 | )
21 |
22 | no_shortcut = on_alconna(Alconna("no_shortcut"))
23 |
--------------------------------------------------------------------------------
/tests/plugins/tree/plugins/simple.py:
--------------------------------------------------------------------------------
1 | from nonebot import on_command, on_message
2 | from nonebot.plugin import PluginMetadata
3 |
4 | __plugin_meta__ = PluginMetadata(
5 | name="简单功能",
6 | description="测试插件简单子插件",
7 | usage="/简单功能",
8 | )
9 |
10 | simple = on_command("simple", aliases={("simple", "alias")}, priority=1, block=True)
11 |
12 | simple_message = on_message()
13 |
--------------------------------------------------------------------------------
/tests/plugins/tree/plugins/sub/__init__.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import nonebot
4 | from nonebot import on_shell_command
5 | from nonebot.plugin import PluginMetadata
6 |
7 | __plugin_meta__ = PluginMetadata(
8 | name="复杂功能",
9 | description="测试插件复杂子插件",
10 | usage="/复杂功能",
11 | )
12 |
13 | _sub_plugins = set()
14 | _sub_plugins |= nonebot.load_plugins(str((Path(__file__).parent / "subsub").resolve()))
15 |
16 | sub = on_shell_command("sub", aliases={("sub", "alias")})
17 |
--------------------------------------------------------------------------------
/tests/plugins/tree/plugins/sub/subsub/sub2.py:
--------------------------------------------------------------------------------
1 | from nonebot.plugin import PluginMetadata
2 |
3 | __plugin_meta__ = PluginMetadata(
4 | name="二级功能",
5 | description="测试插件二级插件",
6 | usage="/二级功能",
7 | )
8 |
--------------------------------------------------------------------------------
/tests/test_adapters.py:
--------------------------------------------------------------------------------
1 | from nonebug import App
2 |
3 | from .utils import make_fake_event, make_fake_message
4 |
5 |
6 | async def test_adapters(app: App):
7 | """当没有指定 adapters 时,默认显示"""
8 | from nonebot import require
9 |
10 | require("tests.plugins.adapters")
11 | from nonebot_plugin_treehelp import help_cmd
12 |
13 | async with app.test_matcher(help_cmd) as ctx:
14 | bot = ctx.create_bot()
15 | message = message = make_fake_message()("/help")
16 | event = make_fake_event(_message=message)()
17 |
18 | ctx.receive_event(bot, event)
19 | ctx.should_call_send(
20 | event, "插件:\n帮助 # 获取插件帮助信息\n适配器 # 测试不同适配器", True
21 | )
22 | ctx.should_finished()
23 |
24 |
25 | async def test_adapters_plugin(app: App):
26 | """仅支持 Console 适配器的插件"""
27 | from nonebot import get_adapter, require
28 | from nonebot.adapters.console import Adapter
29 |
30 | adapter = get_adapter(Adapter)
31 |
32 | require("tests.plugins.adapters")
33 | from nonebot_plugin_treehelp import help_cmd
34 |
35 | async with app.test_matcher(help_cmd) as ctx:
36 | bot = ctx.create_bot(adapter=adapter)
37 | message = message = make_fake_message()("/help 适配器")
38 | event = make_fake_event(_message=message)()
39 |
40 | ctx.receive_event(bot, event)
41 | ctx.should_call_send(event, "适配器\n\nConsole # 测试 Console 适配器", True)
42 | ctx.should_finished()
43 |
44 |
45 | async def test_adapters_supported_plugin(app: App):
46 | """支持的插件"""
47 | from nonebot import get_adapter, require
48 | from nonebot.adapters.console import Adapter
49 |
50 | adapter = get_adapter(Adapter)
51 |
52 | require("tests.plugins.adapters")
53 | from nonebot_plugin_treehelp import help_cmd
54 |
55 | async with app.test_matcher(help_cmd) as ctx:
56 | bot = ctx.create_bot(adapter=adapter)
57 | message = message = make_fake_message()("/help 适配器")
58 | event = make_fake_event(_message=message)()
59 |
60 | ctx.receive_event(bot, event)
61 | ctx.should_call_send(event, "适配器\n\nConsole # 测试 Console 适配器", True)
62 | ctx.should_finished()
63 |
64 |
65 | async def test_adapters_unsupported_plugin(app: App):
66 | """不支持的插件"""
67 | from nonebot import require
68 |
69 | require("tests.plugins.adapters")
70 | from nonebot_plugin_treehelp import help_cmd
71 |
72 | async with app.test_matcher(help_cmd) as ctx:
73 | bot = ctx.create_bot()
74 | message = message = make_fake_message()("/help OneBot")
75 | event = make_fake_event(_message=message)()
76 |
77 | ctx.receive_event(bot, event)
78 | ctx.should_call_send(event, "未找到插件 OneBot", True)
79 | ctx.should_finished()
80 |
--------------------------------------------------------------------------------
/tests/test_help.py:
--------------------------------------------------------------------------------
1 | from nonebug import App
2 |
3 | from .utils import make_fake_event, make_fake_message
4 |
5 |
6 | async def test_help(app: App):
7 | """测试帮助"""
8 | from nonebot_plugin_treehelp import help_cmd
9 |
10 | async with app.test_matcher(help_cmd) as ctx:
11 | bot = ctx.create_bot()
12 | message = message = make_fake_message()("/help")
13 | event = make_fake_event(_message=message)()
14 |
15 | ctx.receive_event(bot, event)
16 | ctx.should_call_send(event, "插件:\n帮助 # 获取插件帮助信息", True)
17 | ctx.should_finished()
18 |
19 |
20 | async def test_help_help(app: App):
21 | """测试获取帮助插件帮助"""
22 | from nonebot_plugin_treehelp import __plugin_meta__, help_cmd
23 |
24 | async with app.test_matcher(help_cmd) as ctx:
25 | bot = ctx.create_bot()
26 | message = message = make_fake_message()("/help 帮助")
27 | event = make_fake_event(_message=message)()
28 |
29 | ctx.receive_event(bot, event)
30 | ctx.should_call_send(
31 | event,
32 | f"帮助\n\n{__plugin_meta__.usage}",
33 | True,
34 | )
35 | ctx.should_finished()
36 |
37 |
38 | async def test_help_not_found(app: App):
39 | """测试插件不存在"""
40 | from nonebot_plugin_treehelp import help_cmd
41 |
42 | async with app.test_matcher(help_cmd) as ctx:
43 | bot = ctx.create_bot()
44 | message = message = make_fake_message()("/help test")
45 | event = make_fake_event(_message=message)()
46 |
47 | ctx.receive_event(bot, event)
48 | ctx.should_call_send(event, "未找到插件 test", True)
49 | ctx.should_finished()
50 |
51 |
52 | async def test_help_command_error(app: App):
53 | """测试命令错误"""
54 | from nonebot_plugin_treehelp import help_cmd
55 |
56 | async with app.test_matcher(help_cmd) as ctx:
57 | bot = ctx.create_bot()
58 | message = message = make_fake_message()("/help --test")
59 | event = make_fake_event(_message=message)()
60 |
61 | ctx.receive_event(bot, event)
62 | ctx.should_call_send(
63 | event,
64 | "usage: 帮助 [-h] [-t] [插件名]\n"
65 | "帮助: error: unrecognized arguments: --test\n",
66 | True,
67 | )
68 | ctx.should_finished()
69 |
70 |
71 | async def test_help_by_command(app: App):
72 | """测试通过命令获取帮助信息"""
73 | from nonebot import require
74 |
75 | require("tests.plugins.tree")
76 | from nonebot_plugin_treehelp import help_cmd
77 |
78 | async with app.test_matcher(help_cmd) as ctx:
79 | bot = ctx.create_bot()
80 | message = message = make_fake_message()("/help simple")
81 | event = make_fake_event(_message=message)()
82 |
83 | ctx.receive_event(bot, event)
84 | ctx.should_call_send(event, "简单功能\n\n/简单功能", True)
85 | ctx.should_finished()
86 |
87 | async with app.test_matcher(help_cmd) as ctx:
88 | bot = ctx.create_bot()
89 | message = message = make_fake_message()("/help simple.alias")
90 | event = make_fake_event(_message=message)()
91 |
92 | ctx.receive_event(bot, event)
93 | ctx.should_call_send(event, "简单功能\n\n/简单功能", True)
94 | ctx.should_finished()
95 |
96 | async with app.test_matcher(help_cmd) as ctx:
97 | bot = ctx.create_bot()
98 | message = message = make_fake_message()("/help sub")
99 | event = make_fake_event(_message=message)()
100 |
101 | ctx.receive_event(bot, event)
102 | ctx.should_call_send(
103 | event, "复杂功能\n\n/复杂功能\n\n二级功能 # 测试插件二级插件", True
104 | )
105 | ctx.should_finished()
106 |
107 | async with app.test_matcher(help_cmd) as ctx:
108 | bot = ctx.create_bot()
109 | message = message = make_fake_message()("/help sub.alias")
110 | event = make_fake_event(_message=message)()
111 |
112 | ctx.receive_event(bot, event)
113 | ctx.should_call_send(
114 | event, "复杂功能\n\n/复杂功能\n\n二级功能 # 测试插件二级插件", True
115 | )
116 | ctx.should_finished()
117 |
118 | async with app.test_matcher(help_cmd) as ctx:
119 | bot = ctx.create_bot()
120 | message = message = make_fake_message()("/help sub。alias")
121 | event = make_fake_event(_message=message)()
122 |
123 | ctx.receive_event(bot, event)
124 | ctx.should_call_send(
125 | event, "复杂功能\n\n/复杂功能\n\n二级功能 # 测试插件二级插件", True
126 | )
127 | ctx.should_finished()
128 |
129 | async with app.test_matcher(help_cmd) as ctx:
130 | bot = ctx.create_bot()
131 | message = message = make_fake_message()("/help alconna")
132 | event = make_fake_event(_message=message)()
133 |
134 | ctx.receive_event(bot, event)
135 | ctx.should_call_send(event, "Alconna\n\n/alconna", True)
136 | ctx.should_finished(help_cmd)
137 |
138 | async with app.test_matcher(help_cmd) as ctx:
139 | bot = ctx.create_bot()
140 | message = message = make_fake_message()("/help alc")
141 | event = make_fake_event(_message=message)()
142 |
143 | ctx.receive_event(bot, event)
144 | ctx.should_call_send(event, "Alconna\n\n/alconna", True)
145 | ctx.should_finished(help_cmd)
146 |
147 | async with app.test_matcher(help_cmd) as ctx:
148 | bot = ctx.create_bot()
149 | message = message = make_fake_message()("/help no_shortcut")
150 | event = make_fake_event(_message=message)()
151 |
152 | ctx.receive_event(bot, event)
153 | ctx.should_call_send(event, "Alconna\n\n/alconna", True)
154 | ctx.should_finished(help_cmd)
155 |
--------------------------------------------------------------------------------
/tests/test_ignored.py:
--------------------------------------------------------------------------------
1 | from nonebug import App
2 | from pytest_mock import MockerFixture
3 |
4 | from .utils import make_fake_event, make_fake_message
5 |
6 |
7 | async def test_ignored(app: App, mocker: MockerFixture):
8 | """通过插件名称忽略指定插件"""
9 | from nonebot import require
10 |
11 | from nonebot_plugin_treehelp.config import plugin_config
12 |
13 | mocker.patch.object(plugin_config, "treehelp_ignored_plugins", ["帮助"])
14 |
15 | require("tests.plugins.adapters")
16 | from nonebot_plugin_treehelp import help_cmd
17 |
18 | async with app.test_matcher(help_cmd) as ctx:
19 | bot = ctx.create_bot()
20 | message = message = make_fake_message()("/help")
21 | event = make_fake_event(_message=message)()
22 |
23 | ctx.receive_event(bot, event)
24 | ctx.should_call_send(event, "插件:\n适配器 # 测试不同适配器", True)
25 | ctx.should_finished()
26 |
--------------------------------------------------------------------------------
/tests/test_nested.py:
--------------------------------------------------------------------------------
1 | from nonebug import App
2 |
3 | from .utils import make_fake_event, make_fake_message
4 |
5 |
6 | async def test_parent(app: App):
7 | """测试父插件"""
8 | from nonebot import require
9 |
10 | require("tests.plugins.nested")
11 | from nonebot_plugin_treehelp import help_cmd
12 |
13 | async with app.test_matcher(help_cmd) as ctx:
14 | bot = ctx.create_bot()
15 | message = message = make_fake_message()("/help")
16 | event = make_fake_event(_message=message)()
17 |
18 | ctx.receive_event(bot, event)
19 | ctx.should_call_send(
20 | event, "插件:\n帮助 # 获取插件帮助信息\n测试 # 一个测试插件", True
21 | )
22 | ctx.should_finished()
23 |
24 |
25 | async def test_sub(app: App):
26 | """测试子插件"""
27 | from nonebot import require
28 |
29 | require("tests.plugins.nested")
30 | from nonebot_plugin_treehelp import help_cmd
31 |
32 | async with app.test_matcher(help_cmd) as ctx:
33 | bot = ctx.create_bot()
34 | message = message = make_fake_message()("/help 测试")
35 | event = make_fake_event(_message=message)()
36 |
37 | ctx.receive_event(bot, event)
38 | ctx.should_call_send(
39 | event, "测试\n\n功能一 # 测试插件子插件一\n功能二 # 测试插件子插件二", True
40 | )
41 | ctx.should_finished()
42 |
--------------------------------------------------------------------------------
/tests/test_tree.py:
--------------------------------------------------------------------------------
1 | from nonebug import App
2 |
3 | from .utils import make_fake_event, make_fake_message
4 |
5 |
6 | async def test_root(app: App):
7 | """测试帮助"""
8 | from nonebot import require
9 |
10 | require("tests.plugins.tree")
11 | from nonebot_plugin_treehelp import help_cmd
12 |
13 | async with app.test_matcher(help_cmd) as ctx:
14 | bot = ctx.create_bot()
15 | message = message = make_fake_message()("/help")
16 | event = make_fake_event(_message=message)()
17 |
18 | ctx.receive_event(bot, event)
19 | ctx.should_call_send(
20 | event, "插件:\n帮助 # 获取插件帮助信息\n测试 # 一个测试插件", True
21 | )
22 | ctx.should_finished()
23 |
24 |
25 | async def test_sub_plugins(app: App):
26 | """测试帮助"""
27 | from nonebot import require
28 |
29 | require("tests.plugins.tree")
30 | from nonebot_plugin_treehelp import help_cmd
31 |
32 | async with app.test_matcher(help_cmd) as ctx:
33 | bot = ctx.create_bot()
34 | message = message = make_fake_message()("/help 测试")
35 | event = make_fake_event(_message=message)()
36 |
37 | ctx.receive_event(bot, event)
38 | ctx.should_call_send(
39 | event,
40 | "测试\n\n"
41 | "Alconna # 测试插件 alconna 子插件\n"
42 | "复杂功能 # 测试插件复杂子插件\n"
43 | "简单功能 # 测试插件简单子插件",
44 | True,
45 | )
46 | ctx.should_finished()
47 |
48 |
49 | async def test_tree_view(app: App):
50 | """测试树形结构"""
51 | from nonebot import require
52 |
53 | require("tests.plugins.tree")
54 | from nonebot_plugin_treehelp import help_cmd
55 |
56 | async with app.test_matcher(help_cmd) as ctx:
57 | bot = ctx.create_bot()
58 | message = message = make_fake_message()("/help --tree")
59 | event = make_fake_event(_message=message)()
60 |
61 | ctx.receive_event(bot, event)
62 | ctx.should_call_send(
63 | event,
64 | "插件:\n帮助 # 获取插件帮助信息\n测试 # 一个测试插件\n├── Alconna # 测试插件 alconna 子插件\n├── 复杂功能 # 测试插件复杂子插件\n│ └── 二级功能 # 测试插件二级插件\n└── 简单功能 # 测试插件简单子插件", # noqa: E501
65 | True,
66 | )
67 | ctx.should_finished()
68 |
69 | async with app.test_matcher(help_cmd) as ctx:
70 | bot = ctx.create_bot()
71 | message = message = make_fake_message()("/help --tree 测试")
72 | event = make_fake_event(_message=message)()
73 |
74 | ctx.receive_event(bot, event)
75 | ctx.should_call_send(
76 | event,
77 | "测试 # 一个测试插件\n├── Alconna # 测试插件 alconna 子插件\n├── 复杂功能 # 测试插件复杂子插件\n│ └── 二级功能 # 测试插件二级插件\n└── 简单功能 # 测试插件简单子插件", # noqa: E501
78 | True,
79 | )
80 | ctx.should_finished()
81 |
--------------------------------------------------------------------------------
/tests/utils.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from typing import TYPE_CHECKING, Optional
3 |
4 | from pydantic import create_model
5 |
6 | if TYPE_CHECKING:
7 | from nonebot.adapters import Event, Message
8 |
9 |
10 | def make_fake_message() -> type["Message"]:
11 | from nonebot.adapters import Message, MessageSegment
12 |
13 | class FakeMessageSegment(MessageSegment):
14 | @classmethod
15 | def get_message_class(cls):
16 | return FakeMessage
17 |
18 | def __str__(self) -> str:
19 | return self.data["text"] if self.type == "text" else f"[fake:{self.type}]"
20 |
21 | @classmethod
22 | def text(cls, text: str):
23 | return cls("text", {"text": text})
24 |
25 | @classmethod
26 | def image(cls, url: str):
27 | return cls("image", {"url": url})
28 |
29 | def is_text(self) -> bool:
30 | return self.type == "text"
31 |
32 | class FakeMessage(Message):
33 | @classmethod
34 | def get_segment_class(cls):
35 | return FakeMessageSegment
36 |
37 | @staticmethod
38 | def _construct(msg: str):
39 | yield FakeMessageSegment.text(msg)
40 |
41 | return FakeMessage
42 |
43 |
44 | def make_fake_event(
45 | _type: str = "message",
46 | _name: str = "test",
47 | _description: str = "test",
48 | _user_id: str = "test",
49 | _session_id: str = "test",
50 | _message: Optional["Message"] = None,
51 | _to_me: bool = True,
52 | **fields,
53 | ) -> type["Event"]:
54 | from nonebot.adapters import Event
55 |
56 | _Fake = create_model("_Fake", __base__=Event, **fields)
57 |
58 | class FakeEvent(_Fake):
59 | def get_type(self) -> str:
60 | return _type
61 |
62 | def get_event_name(self) -> str:
63 | return _name
64 |
65 | def get_event_description(self) -> str:
66 | return _description
67 |
68 | def get_user_id(self) -> str:
69 | return _user_id
70 |
71 | def get_session_id(self) -> str:
72 | return _session_id
73 |
74 | def get_message(self) -> "Message":
75 | if _message is not None:
76 | return _message
77 | raise NotImplementedError
78 |
79 | def is_tome(self) -> bool:
80 | return _to_me
81 |
82 | class Config:
83 | extra = "forbid"
84 |
85 | return FakeEvent
86 |
87 |
88 | def clear_plugins() -> None:
89 | from nonebot.plugin import _managers, _plugins
90 |
91 | for plugin in _plugins.values():
92 | keys = [key for key in sys.modules if key.startswith(plugin.module_name)]
93 | for key in keys:
94 | del sys.modules[key]
95 | _plugins.clear()
96 | _managers.clear()
97 |
--------------------------------------------------------------------------------