mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-10-07 03:07:07 +00:00
Compare commits
1 Commits
ci/noneflo
...
publish/is
Author | SHA1 | Date | |
---|---|---|---|
|
d8c36e8eff |
@@ -2,18 +2,20 @@
|
||||
"name": "Default Linux Universal",
|
||||
"image": "mcr.microsoft.com/devcontainers/universal:2-linux",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-extra/features/poetry:2": {}
|
||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
|
||||
},
|
||||
"postCreateCommand": "./scripts/setup-envs.sh",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"python.analysis.diagnosticMode": "workspace",
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"ruff.organizeImports": false,
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.ruff": "explicit",
|
||||
"source.organizeImports": "explicit"
|
||||
"source.fixAll.ruff": true,
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
"[javascript]": {
|
||||
@@ -42,6 +44,8 @@
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.isort",
|
||||
"ms-python.black-formatter",
|
||||
"charliermarsh.ruff",
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode",
|
||||
|
394
.eslintrc.js
394
.eslintrc.js
@@ -1,10 +1,3 @@
|
||||
const OFF = 0;
|
||||
const WARNING = 1;
|
||||
const ERROR = 2;
|
||||
|
||||
// Prevent importing lodash, usually for browser bundle size reasons
|
||||
const LodashImportPatterns = ["lodash", "lodash.**", "lodash/**"];
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
@@ -13,391 +6,80 @@ module.exports = {
|
||||
node: true,
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {},
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ["./tsconfig.json", "./website/tsconfig.json"],
|
||||
},
|
||||
globals: {
|
||||
JSX: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"airbnb",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
// 'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
// 'plugin:@typescript-eslint/strict',
|
||||
"plugin:import/recommended",
|
||||
"plugin:regexp/recommended",
|
||||
"prettier",
|
||||
"plugin:@docusaurus/all",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
node: {
|
||||
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
||||
},
|
||||
typescript: true,
|
||||
},
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
reportUnusedDisableDirectives: true,
|
||||
plugins: ["react-hooks", "@typescript-eslint", "regexp", "@docusaurus"],
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.ts", "*.tsx"],
|
||||
rules: {
|
||||
"import/no-unresolved": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["*.js", "*.cjs"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
"react/jsx-uses-react": OFF, // JSX runtime: automatic
|
||||
"react/react-in-jsx-scope": OFF, // JSX runtime: automatic
|
||||
"array-callback-return": WARNING,
|
||||
camelcase: WARNING,
|
||||
"class-methods-use-this": OFF, // It's a way of allowing private variables.
|
||||
curly: [WARNING, "all"],
|
||||
"global-require": WARNING,
|
||||
"lines-between-class-members": OFF,
|
||||
"max-classes-per-file": OFF,
|
||||
"max-len": [
|
||||
WARNING,
|
||||
{
|
||||
code: Infinity, // Code width is already enforced by Prettier
|
||||
tabWidth: 2,
|
||||
comments: 80,
|
||||
ignoreUrls: true,
|
||||
ignorePattern: "(eslint-disable|@)",
|
||||
},
|
||||
],
|
||||
"arrow-body-style": OFF,
|
||||
"no-await-in-loop": OFF,
|
||||
"no-case-declarations": WARNING,
|
||||
"no-console": OFF,
|
||||
"no-constant-binary-expression": ERROR,
|
||||
"no-continue": OFF,
|
||||
"no-control-regex": WARNING,
|
||||
"no-else-return": OFF,
|
||||
"no-empty": [WARNING, { allowEmptyCatch: true }],
|
||||
"no-lonely-if": WARNING,
|
||||
"no-nested-ternary": WARNING,
|
||||
"no-param-reassign": [WARNING, { props: false }],
|
||||
"no-prototype-builtins": WARNING,
|
||||
"no-restricted-exports": OFF,
|
||||
"no-restricted-properties": [
|
||||
ERROR,
|
||||
.../** @type {[string, string][]} */ ([
|
||||
// TODO: TS doesn't make Boolean a narrowing function yet,
|
||||
// so filter(Boolean) is problematic type-wise
|
||||
// ['compact', 'Array#filter(Boolean)'],
|
||||
["concat", "Array#concat"],
|
||||
["drop", "Array#slice(n)"],
|
||||
["dropRight", "Array#slice(0, -n)"],
|
||||
["fill", "Array#fill"],
|
||||
["filter", "Array#filter"],
|
||||
["find", "Array#find"],
|
||||
["findIndex", "Array#findIndex"],
|
||||
["first", "foo[0]"],
|
||||
["flatten", "Array#flat"],
|
||||
["flattenDeep", "Array#flat(Infinity)"],
|
||||
["flatMap", "Array#flatMap"],
|
||||
["fromPairs", "Object.fromEntries"],
|
||||
["head", "foo[0]"],
|
||||
["indexOf", "Array#indexOf"],
|
||||
["initial", "Array#slice(0, -1)"],
|
||||
["join", "Array#join"],
|
||||
// Unfortunately there's no great alternative to _.last yet
|
||||
// Candidates: foo.slice(-1)[0]; foo[foo.length - 1]
|
||||
// Array#at is ES2022; could replace _.nth as well
|
||||
// ['last'],
|
||||
["map", "Array#map"],
|
||||
["reduce", "Array#reduce"],
|
||||
["reverse", "Array#reverse"],
|
||||
["slice", "Array#slice"],
|
||||
["take", "Array#slice(0, n)"],
|
||||
["takeRight", "Array#slice(-n)"],
|
||||
["tail", "Array#slice(1)"],
|
||||
]).map(([property, alternative]) => ({
|
||||
object: "_",
|
||||
property,
|
||||
message: `Use ${alternative} instead.`,
|
||||
})),
|
||||
...[
|
||||
"readdirSync",
|
||||
"readFileSync",
|
||||
"statSync",
|
||||
"lstatSync",
|
||||
"existsSync",
|
||||
"pathExistsSync",
|
||||
"realpathSync",
|
||||
"mkdirSync",
|
||||
"mkdirpSync",
|
||||
"mkdirsSync",
|
||||
"writeFileSync",
|
||||
"writeJsonSync",
|
||||
"outputFileSync",
|
||||
"outputJsonSync",
|
||||
"moveSync",
|
||||
"copySync",
|
||||
"copyFileSync",
|
||||
"ensureFileSync",
|
||||
"ensureDirSync",
|
||||
"ensureLinkSync",
|
||||
"ensureSymlinkSync",
|
||||
"unlinkSync",
|
||||
"removeSync",
|
||||
"emptyDirSync",
|
||||
].map((property) => ({
|
||||
object: "fs",
|
||||
property,
|
||||
message: "Do not use sync fs methods.",
|
||||
})),
|
||||
],
|
||||
"no-restricted-syntax": [
|
||||
WARNING,
|
||||
// Copied from airbnb, removed for...of statement, added export all
|
||||
{
|
||||
selector: "ForInStatement",
|
||||
message:
|
||||
"for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.",
|
||||
},
|
||||
{
|
||||
selector: "LabeledStatement",
|
||||
message:
|
||||
"Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.",
|
||||
},
|
||||
{
|
||||
selector: "WithStatement",
|
||||
message:
|
||||
"`with` is disallowed in strict mode because it makes code impossible to predict and optimize.",
|
||||
},
|
||||
{
|
||||
selector: "ExportAllDeclaration",
|
||||
message:
|
||||
"Export all does't work well if imported in ESM due to how they are transpiled, and they can also lead to unexpected exposure of internal methods.",
|
||||
},
|
||||
// TODO make an internal plugin to ensure this
|
||||
// {
|
||||
// selector:
|
||||
// @ 'ExportDefaultDeclaration > Identifier, ExportNamedDeclaration[source=null] > ExportSpecifier',
|
||||
// message: 'Export in one statement'
|
||||
// },
|
||||
...["path", "fs-extra", "webpack", "lodash"].map((m) => ({
|
||||
selector: `ImportDeclaration[importKind=value]:has(Literal[value=${m}]) > ImportSpecifier[importKind=value]`,
|
||||
message:
|
||||
"Default-import this, both for readability and interoperability with ESM",
|
||||
})),
|
||||
],
|
||||
"no-template-curly-in-string": WARNING,
|
||||
"no-unused-expressions": [
|
||||
WARNING,
|
||||
{ allowTaggedTemplates: true, allowShortCircuit: true },
|
||||
],
|
||||
"no-useless-escape": WARNING,
|
||||
"no-void": [ERROR, { allowAsStatement: true }],
|
||||
"prefer-destructuring": WARNING,
|
||||
"prefer-named-capture-group": WARNING,
|
||||
"prefer-template": WARNING,
|
||||
yoda: WARNING,
|
||||
|
||||
"import/extensions": OFF,
|
||||
// This rule doesn't yet support resolving .js imports when the actual file
|
||||
// is .ts. Plus it's not all that useful when our code is fully TS-covered.
|
||||
"import/no-unresolved": [
|
||||
OFF,
|
||||
{
|
||||
// Ignore certain webpack aliases because they can't be resolved
|
||||
ignore: [
|
||||
"^@theme",
|
||||
"^@docusaurus",
|
||||
"^@generated",
|
||||
"^@site",
|
||||
"^@testing-utils",
|
||||
],
|
||||
},
|
||||
],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
quotes: ["error", "double", { avoidEscape: true }],
|
||||
semi: ["error", "always"],
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"import/order": [
|
||||
WARNING,
|
||||
"error",
|
||||
{
|
||||
groups: [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
["parent", "sibling", "index"],
|
||||
"type",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
],
|
||||
"newlines-between": "always",
|
||||
pathGroups: [
|
||||
// always put css import to the last, ref:
|
||||
// https://github.com/import-js/eslint-plugin-import/issues/1239
|
||||
{
|
||||
pattern: "*.+(css|sass|less|scss|pcss|styl)",
|
||||
group: "unknown",
|
||||
patternOptions: { matchBase: true },
|
||||
position: "after",
|
||||
},
|
||||
{ pattern: "react", group: "builtin", position: "before" },
|
||||
{ pattern: "react-dom", group: "builtin", position: "before" },
|
||||
{ pattern: "react-dom/**", group: "builtin", position: "before" },
|
||||
{ pattern: "stream", group: "builtin", position: "before" },
|
||||
{ pattern: "fs-extra", group: "builtin" },
|
||||
{ pattern: "lodash", group: "external", position: "before" },
|
||||
{ pattern: "clsx", group: "external", position: "before" },
|
||||
// 'Bit weird to not use the `import/internal-regex` option, but this
|
||||
// way, we can make `import type { Props } from "@theme/*"` appear
|
||||
// before `import styles from "styles.module.css"`, which is what we
|
||||
// always did. This should be removable once we stop using ambient
|
||||
// module declarations for theme aliases.
|
||||
{ pattern: "@theme/**", group: "internal" },
|
||||
{ pattern: "@site/**", group: "internal" },
|
||||
{ pattern: "@theme-init/**", group: "internal" },
|
||||
{ pattern: "@theme-original/**", group: "internal" },
|
||||
{ pattern: "@/components/**", group: "internal" },
|
||||
{ pattern: "@/libs/**", group: "internal" },
|
||||
{ pattern: "@/types/**", group: "type" },
|
||||
],
|
||||
pathGroupsExcludedImportTypes: [],
|
||||
// example: let `import './nprogress.css';` after importing others
|
||||
// in `packages/docusaurus-theme-classic/src/nprogress.ts`
|
||||
// see more: https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md#warnonunassignedimports-truefalse
|
||||
warnOnUnassignedImports: true,
|
||||
},
|
||||
],
|
||||
"import/prefer-default-export": OFF,
|
||||
|
||||
"jsx-a11y/click-events-have-key-events": WARNING,
|
||||
"jsx-a11y/no-noninteractive-element-interactions": WARNING,
|
||||
"jsx-a11y/html-has-lang": OFF,
|
||||
|
||||
"react-hooks/rules-of-hooks": ERROR,
|
||||
"react-hooks/exhaustive-deps": ERROR,
|
||||
|
||||
// Sometimes we do need the props as a whole, e.g. when spreading
|
||||
"react/destructuring-assignment": OFF,
|
||||
"react/function-component-definition": [
|
||||
WARNING,
|
||||
{
|
||||
namedComponents: "function-declaration",
|
||||
unnamedComponents: "arrow-function",
|
||||
},
|
||||
],
|
||||
"react/jsx-filename-extension": OFF,
|
||||
"react/jsx-key": [ERROR, { checkFragmentShorthand: true }],
|
||||
"react/jsx-no-useless-fragment": [ERROR, { allowExpressions: true }],
|
||||
"react/jsx-props-no-spreading": OFF,
|
||||
"react/no-array-index-key": OFF, // We build a static site, and nearly all components don't change.
|
||||
"react/no-unstable-nested-components": [WARNING, { allowAsProps: true }],
|
||||
"react/prefer-stateless-function": WARNING,
|
||||
"react/prop-types": OFF,
|
||||
"react/require-default-props": [
|
||||
ERROR,
|
||||
{ ignoreFunctionalComponents: true },
|
||||
],
|
||||
|
||||
"@typescript-eslint/consistent-type-definitions": OFF,
|
||||
"@typescript-eslint/require-await": OFF,
|
||||
|
||||
"@typescript-eslint/ban-ts-comment": [
|
||||
ERROR,
|
||||
{ "ts-expect-error": "allow-with-description" },
|
||||
],
|
||||
"@typescript-eslint/consistent-indexed-object-style": OFF,
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
WARNING,
|
||||
{ disallowTypeAnnotations: false },
|
||||
],
|
||||
"@typescript-eslint/explicit-module-boundary-types": WARNING,
|
||||
"@typescript-eslint/method-signature-style": ERROR,
|
||||
"@typescript-eslint/no-empty-function": OFF,
|
||||
"@typescript-eslint/no-empty-interface": [
|
||||
ERROR,
|
||||
{
|
||||
allowSingleExtends: true,
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-inferrable-types": OFF,
|
||||
"@typescript-eslint/no-namespace": [WARNING, { allowDeclarations: true }],
|
||||
"no-use-before-define": OFF,
|
||||
"@typescript-eslint/no-use-before-define": [
|
||||
ERROR,
|
||||
{ functions: false, classes: false, variables: true },
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": OFF,
|
||||
"no-redeclare": OFF,
|
||||
"@typescript-eslint/no-redeclare": ERROR,
|
||||
"no-shadow": OFF,
|
||||
"@typescript-eslint/no-shadow": ERROR,
|
||||
"no-unused-vars": OFF,
|
||||
// We don't provide any escape hatches for this rule. Rest siblings and
|
||||
// function placeholder params are always ignored, and any other unused
|
||||
// locals must be justified with a disable comment.
|
||||
"@typescript-eslint/no-unused-vars": [ERROR, { ignoreRestSiblings: true }],
|
||||
"@typescript-eslint/prefer-optional-chain": ERROR,
|
||||
"@docusaurus/no-html-links": ERROR,
|
||||
"@docusaurus/prefer-docusaurus-heading": ERROR,
|
||||
"@docusaurus/no-untranslated-text": [
|
||||
WARNING,
|
||||
{
|
||||
ignoredStrings: [
|
||||
"·",
|
||||
"-",
|
||||
"—",
|
||||
"×",
|
||||
"", // zwj: ​
|
||||
"@",
|
||||
"WebContainers",
|
||||
"Twitter",
|
||||
"GitHub",
|
||||
"Dev.to",
|
||||
"1.x",
|
||||
],
|
||||
"newlines-between": "always",
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["packages/*/src/theme/**/*.{js,ts,tsx}"],
|
||||
excludedFiles: "*.test.{js,ts,tsx}",
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
patterns: LodashImportPatterns.concat(
|
||||
// Prevents relative imports between React theme components
|
||||
[
|
||||
"../**",
|
||||
"./**",
|
||||
// Allows relative styles module import with consistent filename
|
||||
"!./styles.module.css",
|
||||
// Allows relative tailwind css import with consistent filename
|
||||
"!./styles.css",
|
||||
]
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/*/src/theme/**/*.{js,ts,tsx}"],
|
||||
rules: {
|
||||
"import/no-named-export": ERROR,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["*.d.ts"],
|
||||
rules: {
|
||||
"import/no-duplicates": OFF,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["*.{ts,tsx}"],
|
||||
rules: {
|
||||
"no-undef": OFF,
|
||||
"import/no-import-module-exports": OFF,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["*.{js,mjs,cjs}"],
|
||||
rules: {
|
||||
// Make JS code directly runnable in Node.
|
||||
"@typescript-eslint/no-var-requires": OFF,
|
||||
"@typescript-eslint/explicit-module-boundary-types": OFF,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Internal files where extraneous deps don't matter much at long as
|
||||
// they run
|
||||
files: ["website/**"],
|
||||
rules: {
|
||||
"import/no-extraneous-dependencies": OFF,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
open_collective: nonebot
|
||||
custom: ["https://afdian.com/@nonebot"]
|
||||
custom: ["https://afdian.net/@nonebot"]
|
||||
|
2
.github/ISSUE_TEMPLATE/adapter_publish.yml
vendored
2
.github/ISSUE_TEMPLATE/adapter_publish.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: 发布适配器
|
||||
title: "Adapter: {name}"
|
||||
description: 发布适配器到 NoneBot 官方商店
|
||||
labels: ["Adapter", "Publish"]
|
||||
labels: ["Adapter"]
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
|
2
.github/ISSUE_TEMPLATE/bot_publish.yml
vendored
2
.github/ISSUE_TEMPLATE/bot_publish.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: 发布机器人
|
||||
title: "Bot: {name}"
|
||||
description: 发布机器人到 NoneBot 官方商店
|
||||
labels: ["Bot", "Publish"]
|
||||
labels: ["Bot"]
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
|
2
.github/ISSUE_TEMPLATE/plugin_publish.yml
vendored
2
.github/ISSUE_TEMPLATE/plugin_publish.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: 发布插件
|
||||
title: "Plugin: {name}"
|
||||
description: 发布插件到 NoneBot 官方商店
|
||||
labels: ["Plugin", "Publish"]
|
||||
labels: ["Plugin"]
|
||||
body:
|
||||
- type: input
|
||||
id: pypi
|
||||
|
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -35,12 +35,3 @@ updates:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: devcontainers
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
groups:
|
||||
devcontainers:
|
||||
patterns:
|
||||
- "*"
|
||||
|
12
.github/workflows/codecov.yml
vendored
12
.github/workflows/codecov.yml
vendored
@@ -48,21 +48,11 @@ jobs:
|
||||
cd ./envs/${{ matrix.env }}
|
||||
poetry run bash "../../scripts/run-tests.sh"
|
||||
|
||||
- name: Upload test results
|
||||
uses: codecov/test-results-action@v1
|
||||
with:
|
||||
env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION
|
||||
files: ./tests/junit.xml
|
||||
flags: unittests
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION
|
||||
files: ./tests/coverage.xml
|
||||
flags: unittests
|
||||
fail_ci_if_error: true
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
102
.github/workflows/noneflow.yml
vendored
102
.github/workflows/noneflow.yml
vendored
@@ -3,21 +3,21 @@ name: NoneFlow
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened, edited]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number && format('publish/issue{0}', github.event.issue.number) || github.head_ref || github.run_id }}
|
||||
cancel-in-progress: ${{ startsWith(github.head_ref, 'publish/issue')}}
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
noneflow:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
name: noneflow
|
||||
name: check
|
||||
# do not run on forked PRs, do not run on not related issues, do not run on pr comments
|
||||
if: |
|
||||
!(
|
||||
@@ -36,6 +36,70 @@ jobs:
|
||||
github.event_name == 'issue_comment' && github.event.issue.pull_request
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- run: echo "Check passed"
|
||||
reaction:
|
||||
runs-on: ubuntu-latest
|
||||
name: reaction
|
||||
needs: check
|
||||
if: |
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.action == 'created'
|
||||
) ||
|
||||
(
|
||||
github.event_name == 'issues' &&
|
||||
github.event.action == 'opened'
|
||||
)
|
||||
steps:
|
||||
- name: Generate token
|
||||
id: generate-token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_KEY }}
|
||||
|
||||
- name: Reaction on issue
|
||||
if: github.event_name == 'issues'
|
||||
run: |
|
||||
gh api --method POST /repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/reactions -f "content=rocket"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Reaction on issue comment
|
||||
if: github.event_name == 'issue_comment'
|
||||
run: |
|
||||
gh api --method POST /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions -f "content=rocket"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
plugin_test:
|
||||
runs-on: ubuntu-latest
|
||||
name: nonebot2 plugin test
|
||||
needs: check
|
||||
permissions:
|
||||
issues: read
|
||||
outputs:
|
||||
result: ${{ steps.plugin-test.outputs.RESULT }}
|
||||
output: ${{ steps.plugin-test.outputs.OUTPUT }}
|
||||
metadata: ${{ steps.plugin-test.outputs.METADATA }}
|
||||
steps:
|
||||
- name: Install Poetry
|
||||
if: ${{ !startsWith(github.event_name, 'pull_request') }}
|
||||
run: pipx install poetry
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Test Plugin
|
||||
id: plugin-test
|
||||
run: |
|
||||
curl -sSL https://github.com/nonebot/noneflow/releases/latest/download/plugin_test.py | python -
|
||||
noneflow:
|
||||
runs-on: ubuntu-latest
|
||||
name: noneflow
|
||||
needs: plugin_test
|
||||
steps:
|
||||
- name: Generate token
|
||||
id: generate-token
|
||||
@@ -49,25 +113,29 @@ jobs:
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Cache pre-commit hooks
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .cache/.pre-commit
|
||||
key: noneflow-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
|
||||
- name: NoneFlow
|
||||
uses: docker://ghcr.io/nonebot/noneflow:latest
|
||||
with:
|
||||
config: >
|
||||
{
|
||||
"base": "master",
|
||||
"plugin_path": "assets/plugins.json5",
|
||||
"bot_path": "assets/bots.json5",
|
||||
"adapter_path": "assets/adapters.json5",
|
||||
"registry_repository": "nonebot/registry",
|
||||
"artifact_path": "artifact"
|
||||
"plugin_path": "assets/plugins.json",
|
||||
"bot_path": "assets/bots.json",
|
||||
"adapter_path": "assets/adapters.json"
|
||||
}
|
||||
env:
|
||||
PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }}
|
||||
PLUGIN_TEST_OUTPUT: ${{ needs.plugin_test.outputs.output }}
|
||||
PLUGIN_TEST_METADATA: ${{ needs.plugin_test.outputs.metadata }}
|
||||
APP_ID: ${{ secrets.APP_ID }}
|
||||
PRIVATE_KEY: ${{ secrets.APP_KEY }}
|
||||
PRE_COMMIT_HOME: /github/workspace/.cache/.pre-commit
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: noneflow
|
||||
path: artifact/*
|
||||
if-no-files-found: ignore
|
||||
- name: Fix permission
|
||||
run: sudo chown -R $(whoami):$(id -ng) .cache/.pre-commit
|
||||
|
2
.github/workflows/pyright.yml
vendored
2
.github/workflows/pyright.yml
vendored
@@ -45,5 +45,3 @@ jobs:
|
||||
|
||||
- name: Run Pyright
|
||||
uses: jakebailey/pyright-action@v2
|
||||
with:
|
||||
pylance-version: latest-release
|
||||
|
6
.github/workflows/release-drafter.yml
vendored
6
.github/workflows/release-drafter.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Setup Node Environment
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- uses: release-drafter/release-drafter@v6.0.0
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
id: release-drafter
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Update Changelog
|
||||
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
||||
with:
|
||||
changelog_file: website/src/changelog/changelog.md
|
||||
changelog_file: website/src/pages/changelog.md
|
||||
latest_changes_position: '# 更新日志\n\n'
|
||||
latest_changes_title: "## 最近更新"
|
||||
replace_regex: '(?<=## 最近更新\n)[\s\S]*?(?=\n## )'
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION
|
||||
run: exit 1
|
||||
|
||||
- uses: release-drafter/release-drafter@v6.0.0
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
with:
|
||||
name: Release ${{ steps.version.outputs.TAG_NAME }} 🌈
|
||||
tag: ${{ steps.version.outputs.TAG_NAME }}
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Archive Changelog
|
||||
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
||||
with:
|
||||
changelog_file: website/src/changelog/changelog.md
|
||||
changelog_file: website/src/pages/changelog.md
|
||||
archive_regex: '(?<=## )最近更新(?=\n)'
|
||||
archive_title: ${{ env.TAG_NAME }}
|
||||
commit_and_push: false
|
||||
|
4
.github/workflows/ruff.yml
vendored
4
.github/workflows/ruff.yml
vendored
@@ -20,11 +20,11 @@ jobs:
|
||||
name: Ruff Lint
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ruff-${{ github.ref }}
|
||||
group: pyright-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run Ruff Lint
|
||||
uses: astral-sh/ruff-action@v3
|
||||
uses: chartboost/ruff-action@v1
|
||||
|
9
.github/workflows/website-preview-cd.yml
vendored
9
.github/workflows/website-preview-cd.yml
vendored
@@ -45,16 +45,11 @@ jobs:
|
||||
|
||||
- name: Restore Context
|
||||
run: |
|
||||
PR_NUMBER=$(cat ./pr-number)
|
||||
if ! [[ "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
echo "Invalid PR number: ${PR_NUMBER}"
|
||||
exit 1
|
||||
fi
|
||||
echo "PR_NUMBER=${PR_NUMBER}" >> "${GITHUB_ENV}"
|
||||
cat action.env >> $GITHUB_ENV
|
||||
|
||||
- name: Set Deploy Name
|
||||
run: |
|
||||
echo "DEPLOY_NAME=deploy-preview-${PR_NUMBER}" >> "${GITHUB_ENV}"
|
||||
echo "DEPLOY_NAME=deploy-preview-${{ env.PR_NUMBER }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
|
4
.github/workflows/website-preview-ci.yml
vendored
4
.github/workflows/website-preview-ci.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
- name: Export Context
|
||||
run: |
|
||||
echo "${{ github.event.pull_request.number }}" > ./pr-number
|
||||
echo "PR_NUMBER=${{ github.event.number }}" >> ./action.env
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -38,5 +38,5 @@ jobs:
|
||||
name: website-preview
|
||||
path: |
|
||||
./website/build
|
||||
./pr-number
|
||||
./action.env
|
||||
retention-days: 1
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,7 +7,6 @@ docs_build/_build
|
||||
!tests/.env
|
||||
.docusaurus
|
||||
website/docs/api/**/*.md
|
||||
website/src/pages/changelog/**/*
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python,node,visualstudiocode,jetbrains,macos,windows,linux
|
||||
|
@@ -7,13 +7,30 @@ ci:
|
||||
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.12
|
||||
rev: v0.5.6
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
stages: [pre-commit]
|
||||
- id: ruff-format
|
||||
stages: [pre-commit]
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.8.0
|
||||
hooks:
|
||||
- id: black
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v4.0.0-alpha.8
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/nonebot/nonemoji
|
||||
rev: v0.1.4
|
||||
|
@@ -1,3 +1,3 @@
|
||||
# Changelog
|
||||
|
||||
See [changelog.md](./website/src/changelog/changelog.md) or <https://nonebot.dev/changelog>
|
||||
See [changelog.md](./website/src/pages/changelog.md) or <https://nonebot.dev/changelog>
|
||||
|
10
README.md
10
README.md
@@ -118,15 +118,15 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
|
||||
| GitHub([仓库](https://github.com/nonebot/adapter-github),[协议](https://docs.github.com/en/apps)) | ✅ | GitHub APP & OAuth APP |
|
||||
| QQ([仓库](https://github.com/nonebot/adapter-qq),[协议](https://bot.q.qq.com/wiki/)) | ✅ | QQ 官方接口调整较多 |
|
||||
| Console([仓库](https://github.com/nonebot/adapter-console)) | ✅ | 控制台交互 |
|
||||
| Red([仓库](https://github.com/nonebot/adapter-red),[协议](https://chrononeko.github.io/QQNTRedProtocol/)) | ✅ | QQNT 协议 |
|
||||
| Red([仓库](https://github.com/nonebot/adapter-red),[协议](https://chrononeko.github.io/QQNTRedProtocol/)) | ✅ | QQ 协议 |
|
||||
| Satori([仓库](https://github.com/nonebot/adapter-satori),[协议](https://satori.js.org/zh-CN)) | ✅ | 支持 Onebot、TG、飞书、微信公众号、Koishi 等 |
|
||||
| Discord([仓库](https://github.com/nonebot/adapter-discord),[协议](https://discord.com/developers/docs/intro)) | ✅ | Discord Bot 协议 |
|
||||
| DoDo([仓库](https://github.com/nonebot/adapter-dodo),[协议](https://open.imdodo.com/)) | ✅ | DoDo Bot 协议 |
|
||||
| Kritor([仓库](https://github.com/nonebot/adapter-kritor),[协议](https://github.com/KarinJS/kritor)) | ✅ | Kritor (OnebotX) 协议,QQNT 机器人接口标准 |
|
||||
| Kritor([仓库](https://github.com/nonebot/adapter-kritor),[协议](https://github.com/KarinJS/kritor)) | ✅ | Kritor (OnebotX) 协议,QQ 机器人接口标准 |
|
||||
| Mirai([仓库](https://github.com/nonebot/adapter-mirai),[协议](https://docs.mirai.mamoe.net/mirai-api-http/)) | ✅ | QQ 协议 |
|
||||
| Milky([仓库](https://github.com/nonebot/adapter-milky),[协议](https://milky.ntqqrev.org/)) | ✅ | QQNT 机器人应用接口标准 |
|
||||
| 钉钉([仓库](https://github.com/nonebot/adapter-ding),[协议](https://open.dingtalk.com/document/)) | 🤗 | 寻找 Maintainer(暂不可用) |
|
||||
| 开黑啦([仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila),[协议](https://developer.kookapp.cn/)) | ↗️ | 由社区贡献 |
|
||||
| Mirai([仓库](https://github.com/ieew/nonebot_adapter_mirai2),[协议](https://docs.mirai.mamoe.net/mirai-api-http/)) | ↗️ | QQ 协议,由社区贡献 |
|
||||
| Ntchat([仓库](https://github.com/JustUndertaker/adapter-ntchat)) | ↗️ | 微信协议,由社区贡献 |
|
||||
| MineCraft([仓库](https://github.com/17TheWord/nonebot-adapter-minecraft)) | ↗️ | 由社区贡献 |
|
||||
| BiliBili Live([仓库](https://github.com/wwweww/adapter-bilibili)) | ↗️ | 由社区贡献 |
|
||||
@@ -134,10 +134,6 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
|
||||
| Villa([仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa)) | ❌ | 米游社大别野 Bot 协议,官方已下线 |
|
||||
| Rocket.Chat([仓库](https://github.com/IUnlimit/nonebot-adapter-rocketchat),[协议](https://developer.rocket.chat/)) | ↗️ | Rocket.Chat Bot 协议,由社区贡献 |
|
||||
| Tailchat([仓库](https://github.com/eya46/nonebot-adapter-tailchat),[协议](https://tailchat.msgbyte.com/)) | ↗️ | Tailchat 开放平台 Bot 协议,由社区贡献 |
|
||||
| Mail([仓库](https://github.com/mobyw/nonebot-adapter-mail)) | ↗️ | 邮件收发协议,由社区贡献 |
|
||||
| 黑盒语音([仓库](https://github.com/lclbm/adapter-heybox),[协议](https://github.com/QingFengOpen/HeychatDoc)) | ↗️ | 黑盒语音机器人协议,由社区贡献 |
|
||||
| 微信公众平台([仓库](https://github.com/YangRucheng/nonebot-adapter-wxmp),[协议](https://developers.weixin.qq.com/doc/))| ↗️ | 微信公众平台协议,由社区贡献 |
|
||||
| Gewechat([仓库](https://github.com/Shine-Light/nonebot-adapter-gewechat),[协议](https://github.com/Devo919/Gewechat))| ❌ | Gewechat 微信协议,Gewechat不再维护及可用 |
|
||||
|
||||
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
|
||||
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"project_link": "nonebot-adapter-onebot",
|
||||
"name": "OneBot V11",
|
||||
"desc": "OneBot V11 协议",
|
||||
"author_id": 42488585,
|
||||
"author": "yanyongyu",
|
||||
"homepage": "https://onebot.adapters.nonebot.dev/",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -14,7 +14,7 @@
|
||||
"project_link": "nonebot-adapter-ding",
|
||||
"name": "钉钉",
|
||||
"desc": "钉钉协议",
|
||||
"author_id": 1184028,
|
||||
"author": "Artin",
|
||||
"homepage": "https://github.com/nonebot/adapter-ding",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -24,7 +24,7 @@
|
||||
"project_link": "nonebot-adapter-feishu",
|
||||
"name": "飞书",
|
||||
"desc": "飞书协议",
|
||||
"author_id": 14922941,
|
||||
"author": "StarHeartHunt",
|
||||
"homepage": "https://github.com/nonebot/adapter-feishu",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -34,7 +34,7 @@
|
||||
"project_link": "nonebot-adapter-telegram",
|
||||
"name": "Telegram",
|
||||
"desc": "Telegram 协议",
|
||||
"author_id": 50312681,
|
||||
"author": "j1g5awi",
|
||||
"homepage": "https://github.com/nonebot/adapter-telegram",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -44,7 +44,7 @@
|
||||
"project_link": "nonebot-adapter-qq",
|
||||
"name": "QQ",
|
||||
"desc": "QQ 官方机器人",
|
||||
"author_id": 42488585,
|
||||
"author": "yanyongyu",
|
||||
"homepage": "https://github.com/nonebot/adapter-qq",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -54,7 +54,7 @@
|
||||
"project_link": "nonebot-adapter-kaiheila",
|
||||
"name": "开黑啦",
|
||||
"desc": "开黑啦协议适配",
|
||||
"author_id": 37477320,
|
||||
"author": "Tian-que",
|
||||
"homepage": "https://github.com/Tian-que/nonebot-adapter-kaiheila",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -64,17 +64,27 @@
|
||||
"project_link": "nonebot-adapter-mirai",
|
||||
"name": "Mirai",
|
||||
"desc": "mirai-api-http v2 协议适配",
|
||||
"author_id": 42648639,
|
||||
"author": "RF-Tar-Railt",
|
||||
"homepage": "https://github.com/nonebot/adapter-mirai",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot.adapters.mirai2",
|
||||
"project_link": "nonebot_adapter_mirai2",
|
||||
"name": "mirai2",
|
||||
"desc": "为 nonebot2 添加 mirai_api_http2.x的兼容适配器",
|
||||
"author": "ieew",
|
||||
"homepage": "https://github.com/ieew/nonebot_adapter_mirai2",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot.adapters.onebot.v12",
|
||||
"project_link": "nonebot-adapter-onebot",
|
||||
"name": "OneBot V12",
|
||||
"desc": "OneBot V12 协议",
|
||||
"author_id": 42488585,
|
||||
"author": "yanyongyu",
|
||||
"homepage": "https://onebot.adapters.nonebot.dev/",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -84,7 +94,7 @@
|
||||
"project_link": "nonebot-adapter-console",
|
||||
"name": "Console",
|
||||
"desc": "基于终端的交互式适配器",
|
||||
"author_id": 50488999,
|
||||
"author": "Melodyknit",
|
||||
"homepage": "https://github.com/nonebot/adapter-console",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -94,7 +104,7 @@
|
||||
"project_link": "nonebot-adapter-github",
|
||||
"name": "GitHub",
|
||||
"desc": "GitHub APP & OAuth APP integration",
|
||||
"author_id": 42488585,
|
||||
"author": "yanyongyu",
|
||||
"homepage": "https://github.com/nonebot/adapter-github",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -104,7 +114,7 @@
|
||||
"project_link": "nonebot-adapter-ntchat",
|
||||
"name": "Ntchat",
|
||||
"desc": "pc hook的微信客户端适配",
|
||||
"author_id": 37363867,
|
||||
"author": "JustUndertaker",
|
||||
"homepage": "https://github.com/JustUndertaker/adapter-ntchat",
|
||||
"tags": [
|
||||
{
|
||||
@@ -119,7 +129,7 @@
|
||||
"project_link": "nonebot-adapter-minecraft",
|
||||
"name": "Minecraft",
|
||||
"desc": "MineCraft通信适配,支持Rcon",
|
||||
"author_id": 54731914,
|
||||
"author": "17TheWord",
|
||||
"homepage": "https://github.com/17TheWord/nonebot-adapter-minecraft",
|
||||
"tags": [
|
||||
{
|
||||
@@ -134,7 +144,7 @@
|
||||
"project_link": "nonebot-adapter-bilibili",
|
||||
"name": "BilibiliLive",
|
||||
"desc": "b站直播间ws协议",
|
||||
"author_id": 39620657,
|
||||
"author": "wwweww",
|
||||
"homepage": "https://github.com/wwweww/adapter-bilibili",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -144,7 +154,7 @@
|
||||
"project_link": "nonebot-adapter-walleq",
|
||||
"name": "Walle-Q",
|
||||
"desc": "内置 QQ 协议实现",
|
||||
"author_id": 18395948,
|
||||
"author": "abrahum",
|
||||
"homepage": "https://github.com/onebot-walle/nonebot_adapter_walleq",
|
||||
"tags": [
|
||||
{
|
||||
@@ -159,7 +169,7 @@
|
||||
"project_link": "nonebot-adapter-villa",
|
||||
"name": "大别野",
|
||||
"desc": "米游社大别野官方Bot适配",
|
||||
"author_id": 63870437,
|
||||
"author": "CMHopeSunshine",
|
||||
"homepage": "https://github.com/CMHopeSunshine/nonebot-adapter-villa",
|
||||
"tags": [
|
||||
{
|
||||
@@ -174,7 +184,7 @@
|
||||
"project_link": "nonebot-adapter-red",
|
||||
"name": "RedProtocol",
|
||||
"desc": "QQNT RedProtocol 适配",
|
||||
"author_id": 55650833,
|
||||
"author": "zhaomaoniu",
|
||||
"homepage": "https://github.com/nonebot/adapter-red",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -184,7 +194,7 @@
|
||||
"project_link": "nonebot-adapter-discord",
|
||||
"name": "Discord",
|
||||
"desc": "Discord 官方 Bot 协议适配",
|
||||
"author_id": 63870437,
|
||||
"author": "CMHopeSunshine",
|
||||
"homepage": "https://github.com/nonebot/adapter-discord",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -194,7 +204,7 @@
|
||||
"project_link": "nonebot-adapter-satori",
|
||||
"name": "Satori",
|
||||
"desc": "Satori 协议适配器",
|
||||
"author_id": 42648639,
|
||||
"author": "RF-Tar-Railt",
|
||||
"homepage": "https://github.com/nonebot/adapter-satori",
|
||||
"tags": [
|
||||
{
|
||||
@@ -209,7 +219,7 @@
|
||||
"project_link": "nonebot-adapter-dodo",
|
||||
"name": "DoDo",
|
||||
"desc": "DoDo Bot 协议适配器",
|
||||
"author_id": 63870437,
|
||||
"author": "CMHopeSunshine",
|
||||
"homepage": "https://github.com/nonebot/adapter-dodo",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -219,7 +229,7 @@
|
||||
"project_link": "nonebot-adapter-rocketchat",
|
||||
"name": "RocketChat",
|
||||
"desc": "RocketChat adapter for nonebot2",
|
||||
"author_id": 78360471,
|
||||
"author": "IllTamer",
|
||||
"homepage": "https://github.com/IUnlimit/nonebot-adapter-rocketchat",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -229,7 +239,7 @@
|
||||
"project_link": "nonebot-adapter-kritor",
|
||||
"name": "Kritor",
|
||||
"desc": "Kritor 协议适配",
|
||||
"author_id": 42648639,
|
||||
"author": "RF-Tar-Railt",
|
||||
"homepage": "https://github.com/nonebot/adapter-kritor",
|
||||
"tags": [
|
||||
{
|
||||
@@ -244,54 +254,9 @@
|
||||
"project_link": "nonebot-adapter-tailchat",
|
||||
"name": "Tailchat",
|
||||
"desc": "Tailchat 适配器",
|
||||
"author_id": 61458340,
|
||||
"author": "eya46",
|
||||
"homepage": "https://github.com/eya46/nonebot-adapter-tailchat",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot.adapters.mail",
|
||||
"project_link": "nonebot-adapter-mail",
|
||||
"name": "Mail",
|
||||
"desc": "邮件收发协议",
|
||||
"author_id": 44370805,
|
||||
"homepage": "https://github.com/mobyw/nonebot-adapter-mail",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot.adapters.heybox",
|
||||
"project_link": "nonebot-adapter-heybox",
|
||||
"name": "黑盒语音",
|
||||
"desc": "黑盒语音机器人适配",
|
||||
"author_id": 54730982,
|
||||
"homepage": "https://github.com/lclbm/adapter-heybox",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot.adapters.wxmp",
|
||||
"project_link": "nonebot-adapter-wxmp",
|
||||
"name": "WXMP",
|
||||
"desc": "微信公众平台 客服适配器",
|
||||
"author_id": 60175467,
|
||||
"homepage": "https://github.com/YangRucheng/nonebot-adapter-wxmp",
|
||||
"tags": [
|
||||
{
|
||||
"label": "微信公众平台",
|
||||
"color": "#843dbc"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot.adapters.milky",
|
||||
"project_link": "nonebot-adapter-milky",
|
||||
"name": "nonebot-adapter-milky",
|
||||
"desc": "Milky 协议适配器",
|
||||
"author_id": 42648639,
|
||||
"homepage": "https://github.com/nonebot/adapter-milky",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
},
|
||||
}
|
||||
]
|
@@ -2,7 +2,7 @@
|
||||
{
|
||||
"name": "HarukaBot",
|
||||
"desc": "将B站UP主的动态和直播信息推送至QQ",
|
||||
"author_id": 36433929,
|
||||
"author": "SK-415",
|
||||
"homepage": "https://github.com/SK-415/HarukaBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -10,7 +10,7 @@
|
||||
{
|
||||
"name": "Omega Miya",
|
||||
"desc": "B站推送Pixiv搜图识番求签抽卡表情包还有其他杂七杂八的功能",
|
||||
"author_id": 41713304,
|
||||
"author": "Ailitonia",
|
||||
"homepage": "https://github.com/Ailitonia/omega-miya",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -18,7 +18,7 @@
|
||||
{
|
||||
"name": "Github Bot",
|
||||
"desc": "在QQ获取/处理Github repo/pr/issue",
|
||||
"author_id": 42488585,
|
||||
"author": "yanyongyu",
|
||||
"homepage": "https://github.com/cscs181/QQ-GitHub-Bot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -26,7 +26,7 @@
|
||||
{
|
||||
"name": "YanXiBot",
|
||||
"desc": "动漫资源查找与娱乐机器人",
|
||||
"author_id": 50488999,
|
||||
"author": "Melodyknit",
|
||||
"homepage": "https://github.com/Melodyknit/YanXiBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"name": "绪山真寻bot",
|
||||
"desc": "含有不少的娱乐功能同时稍稍有一些实用的功能 :P",
|
||||
"author_id": 45528451,
|
||||
"author": "HibiKier",
|
||||
"homepage": "https://github.com/HibiKier/zhenxun_bot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -42,7 +42,7 @@
|
||||
{
|
||||
"name": "ATRI",
|
||||
"desc": "高性能文爱萝卜子,糅杂了各类有趣小功能",
|
||||
"author_id": 37587870,
|
||||
"author": "Kyomotoi",
|
||||
"homepage": "https://github.com/Kyomotoi/ATRI",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -50,7 +50,7 @@
|
||||
{
|
||||
"name": "dumbot傻瓜机器人",
|
||||
"desc": "猜一猜游戏、新闻一览、英文每日一词一短语等等,含一键启动及docker容器部署就绪",
|
||||
"author_id": 52522252,
|
||||
"author": "ffreemt",
|
||||
"homepage": "https://github.com/ffreemt/koyeb-nb2",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -58,7 +58,7 @@
|
||||
{
|
||||
"name": "DicePP",
|
||||
"desc": "TRPG骰娘, 带先攻, 查询等功能, 主要面向DND5E. 面对骰主推出的船新版本, 内置Windows/Linux详细部署指南以及方便的自定义骰娘方法, 从回复文本到查询资料库都可轻松配置~",
|
||||
"author_id": 88259371,
|
||||
"author": "pear-studio",
|
||||
"homepage": "https://github.com/pear-studio/nonebot-dicepp",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -66,7 +66,7 @@
|
||||
{
|
||||
"name": "SetuBot",
|
||||
"desc": "每个群配置文件独立,可以控制频率,socks http代理,R18开关,支持多tag,自建API lolicon Pixiv热度榜",
|
||||
"author_id": 39484884,
|
||||
"author": "yuban10703",
|
||||
"homepage": "https://github.com/yuban10703/setu-nonebot2",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"name": "剑网三bot",
|
||||
"desc": "网络游戏《剑侠情缘三》的群聊机器人,数据使用:www.jx3api.com",
|
||||
"author_id": 37363867,
|
||||
"author": "JustUndertaker",
|
||||
"homepage": "https://github.com/JustUndertaker/mini_jx3_bot",
|
||||
"tags": [
|
||||
{
|
||||
@@ -87,7 +87,7 @@
|
||||
{
|
||||
"name": "PixivBot",
|
||||
"desc": "顾名思义是Pixiv的bot(随机推荐插画、随机指定关键词插画、随机书签、查看排行榜、查看指定id插画)",
|
||||
"author_id": 17331698,
|
||||
"author": "ssttkkl",
|
||||
"homepage": "https://github.com/ssttkkl/PixivBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -95,7 +95,7 @@
|
||||
{
|
||||
"name": "SeaBot_QQ",
|
||||
"desc": "一个能够获取新闻资讯并推送至QQ的群聊机器人。",
|
||||
"author_id": 31682561,
|
||||
"author": "B1ue1nWh1te",
|
||||
"homepage": "https://github.com/B1ue1nWh1te/SeaBot_QQ",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -103,7 +103,7 @@
|
||||
{
|
||||
"name": "琪露诺Bot",
|
||||
"desc": "用QQ机器人控制Minecraft服务器!服务器状态查询/服务器白名单/插件列表/玩家查询/转发服务器消息/执行指令... 其他实用娱乐功能,三步即可成功部署的QQ bot",
|
||||
"author_id": 56951617,
|
||||
"author": "summerkirakira",
|
||||
"homepage": "https://github.com/summerkirakira/CirnoBot",
|
||||
"tags": [
|
||||
{
|
||||
@@ -116,7 +116,7 @@
|
||||
{
|
||||
"name": "Inkar Suki",
|
||||
"desc": "一个十分方便的Bot,支持包括Webhook、群管、剑网3等一系列功能,持续更新中……",
|
||||
"author_id": 68726147,
|
||||
"author": "HornCopper",
|
||||
"homepage": "https://github.com/HornCopper/Inkar-Suki",
|
||||
"tags": [
|
||||
{
|
||||
@@ -137,7 +137,7 @@
|
||||
{
|
||||
"name": "屑岛风Bot",
|
||||
"desc": "自家用屑Bot",
|
||||
"author_id": 71873002,
|
||||
"author": "kexue-z",
|
||||
"homepage": "https://github.com/kexue-z/Dao-bot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -145,7 +145,7 @@
|
||||
{
|
||||
"name": "LiteyukiBot-轻雪机器人",
|
||||
"desc": "一个有各种琐事功能的bot,有AI接口,能陪聊",
|
||||
"author_id": 79104275,
|
||||
"author": "snowyfirefly",
|
||||
"homepage": "https://github.com/snowyfirefly/Liteyuki",
|
||||
"tags": [
|
||||
{
|
||||
@@ -162,7 +162,7 @@
|
||||
{
|
||||
"name": "nya_bot",
|
||||
"desc": "喵服——战魂铭人联机服务器兼机器人",
|
||||
"author_id": 31379266,
|
||||
"author": "nikissXI",
|
||||
"homepage": "https://github.com/nikissXI/nya_bot",
|
||||
"tags": [
|
||||
{
|
||||
@@ -175,7 +175,7 @@
|
||||
{
|
||||
"name": "真宵Bot",
|
||||
"desc": "专注群聊的QQ机器人",
|
||||
"author_id": 71173418,
|
||||
"author": "Shine-Light",
|
||||
"homepage": "https://github.com/Shine-Light/Nonebot_Bot_MayaFey",
|
||||
"tags": [
|
||||
{
|
||||
@@ -196,7 +196,7 @@
|
||||
{
|
||||
"name": "SkadiBot",
|
||||
"desc": "明日方舟主题机器人—斯卡蒂",
|
||||
"author_id": 101615359,
|
||||
"author": "yuyuziYYZ",
|
||||
"homepage": "https://github.com/yuyuziYYZ/skadi_bot",
|
||||
"tags": [
|
||||
{
|
||||
@@ -217,7 +217,7 @@
|
||||
{
|
||||
"name": "小白机器人",
|
||||
"desc": "一个高度依赖数据库的群管理机器人",
|
||||
"author_id": 69745333,
|
||||
"author": "SDIJF1521",
|
||||
"homepage": "https://github.com/SDIJF1521/qqai",
|
||||
"tags": [
|
||||
{
|
||||
@@ -234,7 +234,7 @@
|
||||
{
|
||||
"name": "LittlePaimon",
|
||||
"desc": "小派蒙,多功能原神机器人。",
|
||||
"author_id": 63870437,
|
||||
"author": "CMHopeSunshine",
|
||||
"homepage": "https://github.com/CMHopeSunshine/LittlePaimon",
|
||||
"tags": [
|
||||
{
|
||||
@@ -247,7 +247,7 @@
|
||||
{
|
||||
"name": "IdhagnBot",
|
||||
"desc": "🐱🤖 一个以娱乐功能为主的缝合怪(划掉)QQ机器人,包含一定Furry要素但是不会卖萌(就是逊啦!)",
|
||||
"author_id": 17371317,
|
||||
"author": "su226",
|
||||
"homepage": "https://github.com/su226/IdhagnBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -255,7 +255,7 @@
|
||||
{
|
||||
"name": "hsbot",
|
||||
"desc": "服务于《炉石传说》玩家的机器人,上线至今已有加入十余个个炉石相关群聊,上千名用户使用,响应请求数万次。 数据使用:HSreplay, Fbigame, Hearthstone API",
|
||||
"author_id": 67055520,
|
||||
"author": "gzy02",
|
||||
"homepage": "https://github.com/gzy02/hsbot",
|
||||
"tags": [
|
||||
{
|
||||
@@ -268,7 +268,7 @@
|
||||
{
|
||||
"name": "Bread Dog Bot",
|
||||
"desc": "Terraria TShock QQ 机器人",
|
||||
"author_id": 160252668,
|
||||
"author": "Qianyiovo",
|
||||
"homepage": "https://github.com/Qianyiovo/bread_dog_bot",
|
||||
"tags": [
|
||||
{
|
||||
@@ -289,7 +289,7 @@
|
||||
{
|
||||
"name": "RanBot",
|
||||
"desc": "不@会很安静的Bot",
|
||||
"author_id": 88923783,
|
||||
"author": "IAXRetailer",
|
||||
"homepage": "https://github.com/Hecatia-Hell-Workshop/RanBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -297,7 +297,7 @@
|
||||
{
|
||||
"name": "辞辞(cici)Bot",
|
||||
"desc": "一个集成娱乐和群管为一体的机器人",
|
||||
"author_id": 90902259,
|
||||
"author": "mengxinyuan638",
|
||||
"homepage": "https://github.com/mengxinyuan638/cici-bot",
|
||||
"tags": [
|
||||
{
|
||||
@@ -318,7 +318,7 @@
|
||||
{
|
||||
"name": "SuzunoBot",
|
||||
"desc": "多功能音游bot,主要服务maimaiDX、Arcaea",
|
||||
"author_id": 29980586,
|
||||
"author": "Rinfair-CSP-A016",
|
||||
"homepage": "https://github.com/Rinfair-CSP-A016/SuzunoBot-AGLAS",
|
||||
"tags": [
|
||||
{
|
||||
@@ -339,7 +339,7 @@
|
||||
{
|
||||
"name": "青岚",
|
||||
"desc": "基于NoneBot的与Minecraft Server互通消息的机器人",
|
||||
"author_id": 54731914,
|
||||
"author": "17TheWord",
|
||||
"homepage": "https://github.com/17TheWord/qinglan_bot",
|
||||
"tags": [
|
||||
{
|
||||
@@ -352,7 +352,7 @@
|
||||
{
|
||||
"name": "ChensQBOTv2",
|
||||
"desc": "多功能QQ群机器人,权限管理/联ban/社工等等等等,以及拥有一个强大的开发者",
|
||||
"author_id": 116929900,
|
||||
"author": "cnchens",
|
||||
"homepage": "https://github.com/cnchens/ChensQBOTv2",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -360,7 +360,7 @@
|
||||
{
|
||||
"name": "koishi",
|
||||
"desc": "支持爬取 codeforces, atcoder, 牛客上程序设计赛事的 bot。",
|
||||
"author_id": 71639222,
|
||||
"author": "CupidsBow",
|
||||
"homepage": "https://github.com/CupidsBow/koishi",
|
||||
"tags": [
|
||||
{
|
||||
@@ -381,7 +381,7 @@
|
||||
{
|
||||
"name": "脑积水",
|
||||
"desc": "一个超级缝合怪...",
|
||||
"author_id": 66541860,
|
||||
"author": "zhulinyv",
|
||||
"homepage": "https://github.com/zhulinyv/NJS",
|
||||
"tags": [
|
||||
{
|
||||
@@ -394,7 +394,7 @@
|
||||
{
|
||||
"name": "LOVE酱",
|
||||
"desc": "为铁锈战争游戏群服务的虚拟少女,内置了爬取铁锈房间列表功能,以及游戏内单位查询功能,并制作了教学系统以及铁锈相关游戏群的收集功能。",
|
||||
"author_id": 106828088,
|
||||
"author": "allureluoli",
|
||||
"homepage": "https://github.com/allureluoli/LOVE-",
|
||||
"tags": [
|
||||
{
|
||||
@@ -411,7 +411,7 @@
|
||||
{
|
||||
"name": "fubot",
|
||||
"desc": "基于nonebot与go-cqhttp的QQ娱乐bot,提供群日常娱乐功能与舞萌DX游戏相关的信息查询功能。",
|
||||
"author_id": 54059896,
|
||||
"author": "HCskia",
|
||||
"homepage": "https://github.com/HCskia/fu-Bot",
|
||||
"tags": [
|
||||
{
|
||||
@@ -424,7 +424,7 @@
|
||||
{
|
||||
"name": "桃桃酱",
|
||||
"desc": "一个会拆家的高性能缝合萝卜子",
|
||||
"author_id": 107618388,
|
||||
"author": "tkgs0",
|
||||
"homepage": "https://github.com/tkgs0/Momoko",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -432,7 +432,7 @@
|
||||
{
|
||||
"name": "CoolQBot",
|
||||
"desc": "基于 NoneBot2 的聊天机器人",
|
||||
"author_id": 5219550,
|
||||
"author": "he0119",
|
||||
"homepage": "https://github.com/he0119/CoolQBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -440,7 +440,7 @@
|
||||
{
|
||||
"name": "XDbot2",
|
||||
"desc": "简单的QQ功能型机器人",
|
||||
"author_id": 104149371,
|
||||
"author": "This-is-XiaoDeng",
|
||||
"homepage": "https://github.com/ITCraftDevelopmentTeam/XDbot2",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -448,7 +448,7 @@
|
||||
{
|
||||
"name": "March7th",
|
||||
"desc": "三月七 - 崩坏:星穹铁道机器人",
|
||||
"author_id": 44370805,
|
||||
"author": "mobyw",
|
||||
"homepage": "https://github.com/Mar-7th/March7th",
|
||||
"tags": [
|
||||
{
|
||||
@@ -465,7 +465,7 @@
|
||||
{
|
||||
"name": "ay机器人",
|
||||
"desc": "codeforces和洛谷卷王监视、股票监控、ai聊天",
|
||||
"author_id": 77315378,
|
||||
"author": "863109569",
|
||||
"homepage": "https://github.com/863109569/qqbot",
|
||||
"tags": [
|
||||
{
|
||||
@@ -486,7 +486,7 @@
|
||||
{
|
||||
"name": "狐尾",
|
||||
"desc": "一个整合了兽云祭api的机器人,支持账号令牌操作,以及上传兽图",
|
||||
"author_id": 99388013,
|
||||
"author": "bingqiu456",
|
||||
"homepage": "https://github.com/bingqiu456/shouyun",
|
||||
"tags": [
|
||||
{
|
||||
@@ -499,7 +499,7 @@
|
||||
{
|
||||
"name": "ReimeiBot-黎明机器人",
|
||||
"desc": "流星飞逝,黎明终将到来。",
|
||||
"author_id": 65395090,
|
||||
"author": "ThirdBlood",
|
||||
"homepage": "https://github.com/3rdBit/ReimeiBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -507,7 +507,7 @@
|
||||
{
|
||||
"name": "web_bot",
|
||||
"desc": "把机器人搬到网络上",
|
||||
"author_id": 63489103,
|
||||
"author": "wsdtl",
|
||||
"homepage": "https://github.com/wsdtl/web_bot",
|
||||
"tags": [
|
||||
{
|
||||
@@ -520,7 +520,7 @@
|
||||
{
|
||||
"name": "林汐",
|
||||
"desc": "多平台功能型Bot",
|
||||
"author_id": 110453675,
|
||||
"author": "mute23-code",
|
||||
"homepage": "https://github.com/netsora/SoraBot",
|
||||
"tags": [
|
||||
{
|
||||
@@ -537,7 +537,7 @@
|
||||
{
|
||||
"name": "米缸",
|
||||
"desc": "基于nonebot2的米缸Bot",
|
||||
"author_id": 13503375,
|
||||
"author": "LambdaYH",
|
||||
"homepage": "https://github.com/LambdaYH/MigangBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -545,7 +545,7 @@
|
||||
{
|
||||
"name": "不正经的妹妹",
|
||||
"desc": "一款功能丰富、简单易用、自定义性强、扩展性强的可爱的QQ娱乐机器人",
|
||||
"author_id": 104713034,
|
||||
"author": "itsevin",
|
||||
"homepage": "https://github.com/itsevin/sister_bot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -553,7 +553,7 @@
|
||||
{
|
||||
"name": "星见Kirami",
|
||||
"desc": "🌟 读作 Kirami,写作星见,简明轻快的聊天机器人应用。",
|
||||
"author_id": 66513481,
|
||||
"author": "A-kirami",
|
||||
"homepage": "https://kiramibot.dev/",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -561,7 +561,7 @@
|
||||
{
|
||||
"name": "OCNbot",
|
||||
"desc": "OI Contest Notifier bot,一个可以推送洛谷、cf、atcoder、牛客比赛通知的bot",
|
||||
"author_id": 91535478,
|
||||
"author": "ACnoway",
|
||||
"homepage": "https://github.com/ACnoway/OCNbot",
|
||||
"tags": [
|
||||
{
|
||||
@@ -578,7 +578,7 @@
|
||||
{
|
||||
"name": "妃爱",
|
||||
"desc": "超可爱的妃爱QQ群聊机器人",
|
||||
"author_id": 52267304,
|
||||
"author": "jiangyuxiaoxiao",
|
||||
"homepage": "https://github.com/jiangyuxiaoxiao/Hiyori",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -586,7 +586,7 @@
|
||||
{
|
||||
"name": "芙芙",
|
||||
"desc": "供 Mooncell Wiki 协作使用的跨平台机器人",
|
||||
"author_id": 14922941,
|
||||
"author": "StarHeartHunt",
|
||||
"homepage": "https://github.com/MooncellWiki/BotFooChan",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
@@ -594,7 +594,7 @@
|
||||
{
|
||||
"name": "Sakiko",
|
||||
"desc": "基于 LiteLoaderBDS 的 Minecraft 基岩版 Bot",
|
||||
"author_id": 55650833,
|
||||
"author": "zhaomaoniu",
|
||||
"homepage": "https://github.com/zhaomaoniu/Sakiko",
|
||||
"tags": [
|
||||
{
|
||||
@@ -608,10 +608,18 @@
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "星辰 Bot",
|
||||
"desc": "欢迎使用 星辰 Bot 项目!这是一款基于 NoneBot2 打造的智能 QQ 机器人,旨在为用户提供丰富的功能体验。无论是获取一言的灵感,探索历史上的今天,还是穿梭60s世界,星辰 Bot 为您打开了全新的交流之门。快来尝试吧!",
|
||||
"author": "StarXinXin",
|
||||
"homepage": "https://github.com/StarXinXin/StarsBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "Minecraft_QQBot",
|
||||
"desc": "基于 NoneBot2 的 Minecraft 群服互联 QQ 机器人,支持多服务器多种方式连接。",
|
||||
"author_id": 90964775,
|
||||
"author": "Lonely-Sails",
|
||||
"homepage": "https://github.com/Minecraft-QQBot/BotServer",
|
||||
"tags": [
|
||||
{
|
||||
@@ -624,115 +632,5 @@
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "小安提Bot",
|
||||
"desc": "服务于音游 舞萌DX 的多功能Bot",
|
||||
"author_id": 186144551,
|
||||
"homepage": "https://github.com/Ant1816/Ant1Bot",
|
||||
"tags": [
|
||||
{
|
||||
"label": "maimaiDX",
|
||||
"color": "#52ea9a"
|
||||
},
|
||||
{
|
||||
"label": "音游",
|
||||
"color": "#f74b18"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "CanrotBot",
|
||||
"desc": "有很多实用功能的bot,也有很多没什么用的娱乐功能;接入了大模型,并且有一部分功能可以被大模型调用。主打一个全都有(",
|
||||
"author_id": 18070676,
|
||||
"homepage": "https://github.com/wangyw15/CanrotBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "Mio澪",
|
||||
"desc": "超可爱多功能Qbot",
|
||||
"author_id": 50508678,
|
||||
"homepage": "https://github.com/EienSakura/mio",
|
||||
"tags": [
|
||||
{
|
||||
"label": "娱乐",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "AntiFraudBot",
|
||||
"desc": "反诈机器人",
|
||||
"author_id": 104713034,
|
||||
"homepage": "https://github.com/itsevin/AntiFraudBot",
|
||||
"tags": [
|
||||
{
|
||||
"label": "反诈",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "Nekro Agent Bot",
|
||||
"desc": "基于生成式人工智能与沙盒技术的 Nekro Agent 代理执行 AI 机器人,支持聊天、识图、通用文件处理等扩展能力,提供了 WebUI 维护界面、一键部署脚本",
|
||||
"author_id": 57167362,
|
||||
"homepage": "https://github.com/KroMiose/nekro-agent",
|
||||
"tags": [
|
||||
{
|
||||
"label": "聊天",
|
||||
"color": "#c65856"
|
||||
},
|
||||
{
|
||||
"label": "大模型",
|
||||
"color": "#46a34c"
|
||||
},
|
||||
{
|
||||
"label": "WebUI",
|
||||
"color": "#4a96c6"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "PickStarsBot",
|
||||
"desc": "欢迎使用PickStarsBot!这是一款基于NoneBot2构建的智能QQ机器人,提供丰富的功能,包括一言、历史上的今天、60秒早报等,快来试试吧!",
|
||||
"author_id": 183461085,
|
||||
"homepage": "https://github.com/PickStars308/PickStarsBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "LiteBot",
|
||||
"desc": "Web功能/MC/数据功能Bot",
|
||||
"author_id": 67693593,
|
||||
"homepage": "https://github.com/LiteSuggarDEV/LiteBot-NEO/",
|
||||
"tags": [
|
||||
{
|
||||
"label": "Minecraft",
|
||||
"color": "#ea5252"
|
||||
},
|
||||
{
|
||||
"label": "Web",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "Muicebot",
|
||||
"desc": "Muice-Chatbot 的 Nonebot2 实现,支持调用主流大模型,支持 Function Call 和内置 MCP Host 实现",
|
||||
"author_id": 72406624,
|
||||
"homepage": "https://github.com/Moemu/MuiceBot",
|
||||
"tags": [
|
||||
{
|
||||
"label": "LLM",
|
||||
"color": "#3e97ff"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
}
|
||||
]
|
@@ -4,7 +4,7 @@
|
||||
"project_link": "",
|
||||
"name": "None",
|
||||
"desc": "None 驱动器",
|
||||
"author_id": 42488585,
|
||||
"author": "yanyongyu",
|
||||
"homepage": "/docs/advanced/driver",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -14,7 +14,7 @@
|
||||
"project_link": "nonebot2[fastapi]",
|
||||
"name": "FastAPI",
|
||||
"desc": "FastAPI 驱动器",
|
||||
"author_id": 42488585,
|
||||
"author": "yanyongyu",
|
||||
"homepage": "/docs/advanced/driver",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -24,7 +24,7 @@
|
||||
"project_link": "nonebot2[quart]",
|
||||
"name": "Quart",
|
||||
"desc": "Quart 驱动器",
|
||||
"author_id": 42488585,
|
||||
"author": "yanyongyu",
|
||||
"homepage": "/docs/advanced/driver",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -34,7 +34,7 @@
|
||||
"project_link": "nonebot2[httpx]",
|
||||
"name": "HTTPX",
|
||||
"desc": "HTTPX 驱动器",
|
||||
"author_id": 42488585,
|
||||
"author": "yanyongyu",
|
||||
"homepage": "/docs/advanced/driver",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -44,7 +44,7 @@
|
||||
"project_link": "nonebot2[websockets]",
|
||||
"name": "websockets",
|
||||
"desc": "websockets 驱动器",
|
||||
"author_id": 42488585,
|
||||
"author": "yanyongyu",
|
||||
"homepage": "/docs/advanced/driver",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
@@ -54,9 +54,9 @@
|
||||
"project_link": "nonebot2[aiohttp]",
|
||||
"name": "AIOHTTP",
|
||||
"desc": "AIOHTTP 驱动器",
|
||||
"author_id": 42488585,
|
||||
"author": "yanyongyu",
|
||||
"homepage": "/docs/advanced/driver",
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
},
|
||||
}
|
||||
]
|
File diff suppressed because it is too large
Load Diff
3045
envs/pydantic-v1/poetry.lock
generated
3045
envs/pydantic-v1/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
3159
envs/pydantic-v2/poetry.lock
generated
3159
envs/pydantic-v2/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
1555
envs/test/poetry.lock
generated
1555
envs/test/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,11 @@ packages = [{ include = "nonebot-test.py" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
trio = "^0.27.0"
|
||||
nonebug = "^0.4.1"
|
||||
nonebug = "^0.3.7"
|
||||
wsproto = "^1.2.0"
|
||||
pytest-cov = "^6.0.0"
|
||||
pytest-cov = "^5.0.0"
|
||||
pytest-xdist = "^3.0.2"
|
||||
pytest-asyncio = "^0.23.2"
|
||||
werkzeug = ">=2.3.6,<4.0.0"
|
||||
coverage-conditional-plugin = "^0.9.0"
|
||||
|
||||
|
@@ -39,24 +39,22 @@
|
||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 0
|
||||
description: nonebot 模块
|
||||
"""
|
||||
|
||||
from importlib.metadata import version
|
||||
import os
|
||||
from typing import Any, Optional, TypeVar, Union, overload
|
||||
from importlib.metadata import version
|
||||
from typing import Any, Union, TypeVar, Optional, overload
|
||||
|
||||
import loguru
|
||||
|
||||
from nonebot.adapters import Adapter, Bot
|
||||
from nonebot.compat import model_dump
|
||||
from nonebot.config import DOTENV_TYPE, Config, Env
|
||||
from nonebot.drivers import ASGIMixin, Driver, combine_driver
|
||||
from nonebot.log import logger as logger
|
||||
from nonebot.adapters import Bot, Adapter
|
||||
from nonebot.config import DOTENV_TYPE, Env, Config
|
||||
from nonebot.utils import escape_tag, resolve_dot_notation
|
||||
from nonebot.drivers import Driver, ASGIMixin, combine_driver
|
||||
|
||||
try:
|
||||
__version__ = version("nonebot2")
|
||||
@@ -190,9 +188,9 @@ def get_asgi() -> Any:
|
||||
```
|
||||
"""
|
||||
driver = get_driver()
|
||||
assert isinstance(driver, ASGIMixin), (
|
||||
"asgi object is only available for asgi driver"
|
||||
)
|
||||
assert isinstance(
|
||||
driver, ASGIMixin
|
||||
), "asgi object is only available for asgi driver"
|
||||
return driver.asgi
|
||||
|
||||
|
||||
@@ -337,31 +335,31 @@ def run(*args: Any, **kwargs: Any) -> None:
|
||||
get_driver().run(*args, **kwargs)
|
||||
|
||||
|
||||
from nonebot.plugin import CommandGroup as CommandGroup
|
||||
from nonebot.plugin import MatcherGroup as MatcherGroup
|
||||
from nonebot.plugin import get_available_plugin_names as get_available_plugin_names
|
||||
from nonebot.plugin import get_loaded_plugins as get_loaded_plugins
|
||||
from nonebot.plugin import get_plugin as get_plugin
|
||||
from nonebot.plugin import get_plugin_by_module_name as get_plugin_by_module_name
|
||||
from nonebot.plugin import get_plugin_config as get_plugin_config
|
||||
from nonebot.plugin import load_all_plugins as load_all_plugins
|
||||
from nonebot.plugin import load_builtin_plugin as load_builtin_plugin
|
||||
from nonebot.plugin import load_builtin_plugins as load_builtin_plugins
|
||||
from nonebot.plugin import load_from_json as load_from_json
|
||||
from nonebot.plugin import load_from_toml as load_from_toml
|
||||
from nonebot.plugin import load_plugin as load_plugin
|
||||
from nonebot.plugin import load_plugins as load_plugins
|
||||
from nonebot.plugin import on as on
|
||||
from nonebot.plugin import on_command as on_command
|
||||
from nonebot.plugin import on_endswith as on_endswith
|
||||
from nonebot.plugin import on_fullmatch as on_fullmatch
|
||||
from nonebot.plugin import on_keyword as on_keyword
|
||||
from nonebot.plugin import on_message as on_message
|
||||
from nonebot.plugin import on_metaevent as on_metaevent
|
||||
from nonebot.plugin import on_notice as on_notice
|
||||
from nonebot.plugin import on_regex as on_regex
|
||||
from nonebot.plugin import on_request as on_request
|
||||
from nonebot.plugin import on_shell_command as on_shell_command
|
||||
from nonebot.plugin import on_startswith as on_startswith
|
||||
from nonebot.plugin import on_type as on_type
|
||||
from nonebot.plugin import require as require
|
||||
from nonebot.plugin import on_regex as on_regex
|
||||
from nonebot.plugin import on_notice as on_notice
|
||||
from nonebot.plugin import get_plugin as get_plugin
|
||||
from nonebot.plugin import on_command as on_command
|
||||
from nonebot.plugin import on_keyword as on_keyword
|
||||
from nonebot.plugin import on_message as on_message
|
||||
from nonebot.plugin import on_request as on_request
|
||||
from nonebot.plugin import load_plugin as load_plugin
|
||||
from nonebot.plugin import on_endswith as on_endswith
|
||||
from nonebot.plugin import CommandGroup as CommandGroup
|
||||
from nonebot.plugin import MatcherGroup as MatcherGroup
|
||||
from nonebot.plugin import load_plugins as load_plugins
|
||||
from nonebot.plugin import on_fullmatch as on_fullmatch
|
||||
from nonebot.plugin import on_metaevent as on_metaevent
|
||||
from nonebot.plugin import on_startswith as on_startswith
|
||||
from nonebot.plugin import load_from_json as load_from_json
|
||||
from nonebot.plugin import load_from_toml as load_from_toml
|
||||
from nonebot.plugin import load_all_plugins as load_all_plugins
|
||||
from nonebot.plugin import on_shell_command as on_shell_command
|
||||
from nonebot.plugin import get_plugin_config as get_plugin_config
|
||||
from nonebot.plugin import get_loaded_plugins as get_loaded_plugins
|
||||
from nonebot.plugin import load_builtin_plugin as load_builtin_plugin
|
||||
from nonebot.plugin import load_builtin_plugins as load_builtin_plugins
|
||||
from nonebot.plugin import get_plugin_by_module_name as get_plugin_by_module_name
|
||||
from nonebot.plugin import get_available_plugin_names as get_available_plugin_names
|
||||
|
@@ -3,15 +3,13 @@
|
||||
使用 {ref}`nonebot.drivers.Driver.register_adapter` 注册适配器。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 0
|
||||
description: nonebot.adapters 模块
|
||||
"""
|
||||
|
||||
from nonebot.internal.adapter import Adapter as Adapter
|
||||
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
|
||||
|
@@ -3,29 +3,26 @@
|
||||
为兼容 Pydantic V1 与 V2 版本,定义了一系列兼容函数与类供使用。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 16
|
||||
description: nonebot.compat 模块
|
||||
"""
|
||||
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass, is_dataclass
|
||||
from functools import cached_property
|
||||
from dataclasses import dataclass, is_dataclass
|
||||
from typing_extensions import Self, get_args, get_origin, is_typeddict
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
Any,
|
||||
Callable,
|
||||
Union,
|
||||
Generic,
|
||||
Literal,
|
||||
TypeVar,
|
||||
Callable,
|
||||
Optional,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
Union,
|
||||
Annotated,
|
||||
overload,
|
||||
)
|
||||
from typing_extensions import Self, get_args, get_origin, is_typeddict
|
||||
|
||||
from pydantic import VERSION, BaseModel
|
||||
|
||||
@@ -45,24 +42,21 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
__all__ = (
|
||||
"DEFAULT_CONFIG",
|
||||
"PYDANTIC_V2",
|
||||
"ConfigDict",
|
||||
"FieldInfo",
|
||||
"ModelField",
|
||||
"Required",
|
||||
"PydanticUndefined",
|
||||
"PydanticUndefinedType",
|
||||
"Required",
|
||||
"ConfigDict",
|
||||
"DEFAULT_CONFIG",
|
||||
"FieldInfo",
|
||||
"ModelField",
|
||||
"TypeAdapter",
|
||||
"custom_validation",
|
||||
"extract_field_info",
|
||||
"field_validator",
|
||||
"model_fields",
|
||||
"model_config",
|
||||
"model_dump",
|
||||
"model_fields",
|
||||
"model_validator",
|
||||
"type_validate_json",
|
||||
"type_validate_python",
|
||||
"type_validate_json",
|
||||
"custom_validation",
|
||||
)
|
||||
|
||||
__autodoc__ = {
|
||||
@@ -74,11 +68,9 @@ __autodoc__ = {
|
||||
if PYDANTIC_V2: # pragma: pydantic-v2
|
||||
from pydantic import GetCoreSchemaHandler
|
||||
from pydantic import TypeAdapter as TypeAdapter
|
||||
from pydantic import field_validator as field_validator
|
||||
from pydantic import model_validator as model_validator
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
from pydantic._internal._repr import display_as_type
|
||||
from pydantic.fields import FieldInfo as BaseFieldInfo
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
|
||||
Required = Ellipsis
|
||||
"""Alias of Ellipsis for compatibility with pydantic v1"""
|
||||
@@ -259,8 +251,9 @@ if PYDANTIC_V2: # pragma: pydantic-v2
|
||||
return class_
|
||||
|
||||
else: # pragma: pydantic-v1
|
||||
from pydantic import Extra
|
||||
from pydantic import parse_obj_as, parse_raw_as
|
||||
from pydantic import BaseConfig as PydanticConfig
|
||||
from pydantic import Extra, parse_obj_as, parse_raw_as, root_validator, validator
|
||||
from pydantic.fields import FieldInfo as BaseFieldInfo
|
||||
from pydantic.fields import ModelField as BaseModelField
|
||||
from pydantic.schema import get_annotation_from_field_info
|
||||
@@ -373,44 +366,6 @@ else: # pragma: pydantic-v1
|
||||
kwargs.update(field_info.extra)
|
||||
return kwargs
|
||||
|
||||
@overload
|
||||
def field_validator(
|
||||
field: str,
|
||||
/,
|
||||
*fields: str,
|
||||
mode: Literal["before"],
|
||||
check_fields: Optional[bool] = None,
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def field_validator(
|
||||
field: str,
|
||||
/,
|
||||
*fields: str,
|
||||
mode: Literal["after"] = ...,
|
||||
check_fields: Optional[bool] = None,
|
||||
): ...
|
||||
|
||||
def field_validator(
|
||||
field: str,
|
||||
/,
|
||||
*fields: str,
|
||||
mode: Literal["before", "after"] = "after",
|
||||
check_fields: Optional[bool] = None,
|
||||
):
|
||||
if mode == "before":
|
||||
return validator(
|
||||
field,
|
||||
*fields,
|
||||
pre=True,
|
||||
check_fields=check_fields or True,
|
||||
allow_reuse=True,
|
||||
)
|
||||
else:
|
||||
return validator(
|
||||
field, *fields, check_fields=check_fields or True, allow_reuse=True
|
||||
)
|
||||
|
||||
def model_fields(model: type[BaseModel]) -> list[ModelField]:
|
||||
"""Get field list of a model."""
|
||||
|
||||
@@ -448,18 +403,6 @@ else: # pragma: pydantic-v1
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
|
||||
@overload
|
||||
def model_validator(*, mode: Literal["before"]): ...
|
||||
|
||||
@overload
|
||||
def model_validator(*, mode: Literal["after"]): ...
|
||||
|
||||
def model_validator(*, mode: Literal["before", "after"]):
|
||||
if mode == "before":
|
||||
return root_validator(pre=True, allow_reuse=True)
|
||||
else:
|
||||
return root_validator(skip_on_failure=True, allow_reuse=True)
|
||||
|
||||
def type_validate_python(type_: type[T], data: Any) -> T:
|
||||
"""Validate data with given type."""
|
||||
return parse_obj_as(type_, data)
|
||||
|
@@ -7,26 +7,27 @@ NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及
|
||||
详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 1
|
||||
description: nonebot.config 模块
|
||||
"""
|
||||
|
||||
import os
|
||||
import abc
|
||||
from collections.abc import Mapping
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import timedelta
|
||||
from ipaddress import IPv4Address
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Optional, Union
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, Union, Optional
|
||||
from typing_extensions import TypeAlias, get_args, get_origin
|
||||
|
||||
from dotenv import dotenv_values
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import Field, BaseModel
|
||||
from pydantic.networks import IPvAnyAddress
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.typing import origin_is_union
|
||||
from nonebot.utils import deep_update, type_is_complex, lenient_issubclass
|
||||
from nonebot.compat import (
|
||||
PYDANTIC_V2,
|
||||
ConfigDict,
|
||||
@@ -36,9 +37,6 @@ from nonebot.compat import (
|
||||
model_config,
|
||||
model_fields,
|
||||
)
|
||||
from nonebot.log import logger
|
||||
from nonebot.typing import origin_is_union
|
||||
from nonebot.utils import deep_update, lenient_issubclass, type_is_complex
|
||||
|
||||
DOTENV_TYPE: TypeAlias = Union[
|
||||
Path, str, list[Union[Path, str]], tuple[Union[Path, str], ...]
|
||||
@@ -175,7 +173,8 @@ class DotEnvSettingsSource(BaseSettingsSource):
|
||||
continue
|
||||
|
||||
# delete from file vars when used
|
||||
env_file_vars.pop(env_name, None)
|
||||
if env_name in env_file_vars:
|
||||
del env_file_vars[env_name]
|
||||
|
||||
_, *keys, last_key = env_name.split(self.env_nested_delimiter)
|
||||
env_var = result
|
||||
|
@@ -1,8 +1,6 @@
|
||||
"""本模块包含了 NoneBot 事件处理过程中使用到的常量。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 9
|
||||
description: nonebot.consts 模块
|
||||
"""
|
||||
@@ -22,10 +20,6 @@ REJECT_TARGET: Literal["_current_target"] = "_current_target"
|
||||
"""当前 `reject` 目标存储 key"""
|
||||
REJECT_CACHE_TARGET: Literal["_next_target"] = "_next_target"
|
||||
"""下一个 `reject` 目标存储 key"""
|
||||
PAUSE_PROMPT_RESULT_KEY: Literal["_pause_result"] = "_pause_result"
|
||||
"""`pause` prompt 发送结果存储 key"""
|
||||
REJECT_PROMPT_RESULT_KEY: Literal["_reject_{key}_result"] = "_reject_{key}_result"
|
||||
"""`reject` prompt 发送结果存储 key"""
|
||||
|
||||
# used by Rule
|
||||
PREFIX_KEY: Literal["_prefix"] = "_prefix"
|
||||
|
@@ -1,32 +1,22 @@
|
||||
"""本模块模块实现了依赖注入的定义与处理。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 0
|
||||
description: nonebot.dependencies 模块
|
||||
"""
|
||||
|
||||
import abc
|
||||
from collections.abc import Awaitable, Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
import asyncio
|
||||
import inspect
|
||||
from typing import Any, Callable, Generic, Optional, TypeVar, cast
|
||||
from dataclasses import field, dataclass
|
||||
from collections.abc import Iterable, Awaitable
|
||||
from typing import Any, Generic, TypeVar, Callable, Optional, cast
|
||||
|
||||
import anyio
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
|
||||
from nonebot.compat import FieldInfo, ModelField, PydanticUndefined
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.log import logger
|
||||
from nonebot.typing import _DependentCallable
|
||||
from nonebot.utils import (
|
||||
flatten_exception_group,
|
||||
is_coroutine_callable,
|
||||
run_coro_with_shield,
|
||||
run_sync,
|
||||
)
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
from nonebot.compat import FieldInfo, ModelField, PydanticUndefined
|
||||
|
||||
from .utils import check_field_type, get_typed_signature
|
||||
|
||||
@@ -92,16 +82,7 @@ class Dependent(Generic[R]):
|
||||
)
|
||||
|
||||
async def __call__(self, **kwargs: Any) -> R:
|
||||
exception: Optional[BaseExceptionGroup[SkippedException]] = None
|
||||
|
||||
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
|
||||
nonlocal exception
|
||||
exception = exc_group
|
||||
# raise one of the exceptions instead
|
||||
excs = list(flatten_exception_group(exc_group))
|
||||
logger.trace(f"{self} skipped due to {excs}")
|
||||
|
||||
with catch({SkippedException: _handle_skipped}):
|
||||
try:
|
||||
# do pre-check
|
||||
await self.check(**kwargs)
|
||||
|
||||
@@ -113,8 +94,9 @@ class Dependent(Generic[R]):
|
||||
return await cast(Callable[..., Awaitable[R]], self.call)(**values)
|
||||
else:
|
||||
return await run_sync(cast(Callable[..., R], self.call))(**values)
|
||||
|
||||
raise exception
|
||||
except SkippedException as e:
|
||||
logger.trace(f"{self} skipped due to {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def parse_params(
|
||||
@@ -182,17 +164,10 @@ class Dependent(Generic[R]):
|
||||
return cls(call, params, parameterless_params)
|
||||
|
||||
async def check(self, **params: Any) -> None:
|
||||
if self.parameterless:
|
||||
async with anyio.create_task_group() as tg:
|
||||
for param in self.parameterless:
|
||||
tg.start_soon(partial(param._check, **params))
|
||||
|
||||
if self.params:
|
||||
async with anyio.create_task_group() as tg:
|
||||
for param in self.params:
|
||||
tg.start_soon(
|
||||
partial(cast(Param, param.field_info)._check, **params)
|
||||
)
|
||||
await asyncio.gather(*(param._check(**params) for param in self.parameterless))
|
||||
await asyncio.gather(
|
||||
*(cast(Param, param.field_info)._check(**params) for param in self.params)
|
||||
)
|
||||
|
||||
async def _solve_field(self, field: ModelField, params: dict[str, Any]) -> Any:
|
||||
param = cast(Param, field.field_info)
|
||||
@@ -208,22 +183,10 @@ class Dependent(Generic[R]):
|
||||
await param._solve(**params)
|
||||
|
||||
# solve param values
|
||||
result: dict[str, Any] = {}
|
||||
if not self.params:
|
||||
return result
|
||||
|
||||
async def _solve_field(field: ModelField, params: dict[str, Any]) -> None:
|
||||
value = await self._solve_field(field, params)
|
||||
result[field.name] = value
|
||||
|
||||
async with anyio.create_task_group() as tg:
|
||||
for field in self.params:
|
||||
# shield the task to prevent cancellation
|
||||
# when one of the tasks raises an exception
|
||||
# this will improve the dependency cache reusability
|
||||
tg.start_soon(run_coro_with_shield, _solve_field(field, params))
|
||||
|
||||
return result
|
||||
values = await asyncio.gather(
|
||||
*(self._solve_field(field, params) for field in self.params)
|
||||
)
|
||||
return {field.name: value for field, value in zip(self.params, values)}
|
||||
|
||||
|
||||
__autodoc__ = {"CustomConfig": False}
|
||||
|
@@ -1,7 +1,5 @@
|
||||
"""
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 1
|
||||
description: nonebot.dependencies.utils 模块
|
||||
"""
|
||||
|
@@ -3,31 +3,29 @@
|
||||
各驱动请继承以下基类。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 0
|
||||
description: nonebot.drivers 模块
|
||||
"""
|
||||
|
||||
from nonebot.internal.driver import URL as URL
|
||||
from nonebot.internal.driver import ASGIMixin as ASGIMixin
|
||||
from nonebot.internal.driver import Cookies as Cookies
|
||||
from nonebot.internal.driver import Driver as Driver
|
||||
from nonebot.internal.driver import ForwardDriver as ForwardDriver
|
||||
from nonebot.internal.driver import ForwardMixin as ForwardMixin
|
||||
from nonebot.internal.driver import HTTPClientMixin as HTTPClientMixin
|
||||
from nonebot.internal.driver import HTTPClientSession as HTTPClientSession
|
||||
from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup
|
||||
from nonebot.internal.driver import HTTPVersion as HTTPVersion
|
||||
from nonebot.internal.driver import Mixin as Mixin
|
||||
from nonebot.internal.driver import 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 ReverseDriver as ReverseDriver
|
||||
from nonebot.internal.driver import ReverseMixin as ReverseMixin
|
||||
from nonebot.internal.driver import ASGIMixin as ASGIMixin
|
||||
from nonebot.internal.driver import WebSocket as WebSocket
|
||||
from nonebot.internal.driver import HTTPVersion as HTTPVersion
|
||||
from nonebot.internal.driver import ForwardMixin as ForwardMixin
|
||||
from nonebot.internal.driver import ReverseMixin as ReverseMixin
|
||||
from nonebot.internal.driver import ForwardDriver as ForwardDriver
|
||||
from nonebot.internal.driver import ReverseDriver as ReverseDriver
|
||||
from nonebot.internal.driver import combine_driver as combine_driver
|
||||
from nonebot.internal.driver import HTTPClientMixin as HTTPClientMixin
|
||||
from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup
|
||||
from nonebot.internal.driver import HTTPClientSession as HTTPClientSession
|
||||
from nonebot.internal.driver import WebSocketClientMixin as WebSocketClientMixin
|
||||
from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup
|
||||
from nonebot.internal.driver import combine_driver as combine_driver
|
||||
|
||||
__autodoc__ = {
|
||||
"URL": True,
|
||||
|
@@ -11,33 +11,29 @@ pip install nonebot2[aiohttp]
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 2
|
||||
description: nonebot.drivers.aiohttp 模块
|
||||
"""
|
||||
|
||||
from typing_extensions import override
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from typing_extensions import override
|
||||
from typing import TYPE_CHECKING, Union, Optional
|
||||
|
||||
from multidict import CIMultiDict
|
||||
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers import URL, Request, Response
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.internal.driver import Cookies, QueryTypes, CookieTypes, HeaderTypes
|
||||
from nonebot.drivers import (
|
||||
URL,
|
||||
HTTPVersion,
|
||||
HTTPClientMixin,
|
||||
HTTPClientSession,
|
||||
HTTPVersion,
|
||||
Request,
|
||||
Response,
|
||||
WebSocketClientMixin,
|
||||
combine_driver,
|
||||
)
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.internal.driver import Cookies, CookieTypes, HeaderTypes, QueryTypes
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
@@ -90,7 +86,9 @@ class Session(HTTPClientSession):
|
||||
@override
|
||||
async def request(self, setup: Request) -> Response:
|
||||
if self._params:
|
||||
url = setup.url.with_query({**self._params, **setup.url.query})
|
||||
params = self._params.copy()
|
||||
params.update(setup.url.query)
|
||||
url = setup.url.with_query(params)
|
||||
else:
|
||||
url = setup.url
|
||||
|
||||
@@ -170,13 +168,11 @@ class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported HTTP version: {setup.version}")
|
||||
|
||||
timeout = aiohttp.ClientWSTimeout(ws_close=setup.timeout or 10.0) # type: ignore
|
||||
|
||||
async with aiohttp.ClientSession(version=version, trust_env=True) as session:
|
||||
async with session.ws_connect(
|
||||
setup.url,
|
||||
method=setup.method,
|
||||
timeout=timeout,
|
||||
timeout=setup.timeout or 10,
|
||||
headers=setup.headers,
|
||||
proxy=setup.proxy,
|
||||
) as ws:
|
||||
|
@@ -11,35 +11,34 @@ pip install nonebot2[fastapi]
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 1
|
||||
description: nonebot.drivers.fastapi 模块
|
||||
"""
|
||||
|
||||
import logging
|
||||
import contextlib
|
||||
from functools import wraps
|
||||
import logging
|
||||
from typing import Any, Optional, Union
|
||||
from typing_extensions import override
|
||||
from typing import Any, Union, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from nonebot.compat import model_dump, type_validate_python
|
||||
from nonebot.config import Config as NoneBotConfig
|
||||
from nonebot.config import Env
|
||||
from nonebot.drivers import ASGIMixin, HTTPServerSetup, WebSocketServerSetup
|
||||
from nonebot.drivers import Driver as BaseDriver
|
||||
from nonebot.drivers import Request as BaseRequest
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ASGIMixin
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.internal.driver import FileTypes
|
||||
from nonebot.drivers import Driver as BaseDriver
|
||||
from nonebot.config import Config as NoneBotConfig
|
||||
from nonebot.drivers import Request as BaseRequest
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.compat import model_dump, type_validate_python
|
||||
from nonebot.drivers import HTTPServerSetup, WebSocketServerSetup
|
||||
|
||||
try:
|
||||
from fastapi import FastAPI, Request, UploadFile, status
|
||||
from fastapi.responses import Response
|
||||
from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState
|
||||
import uvicorn
|
||||
from fastapi.responses import Response
|
||||
from fastapi import FastAPI, Request, UploadFile, status
|
||||
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install FastAPI first to use this driver. "
|
||||
|
@@ -11,28 +11,26 @@ pip install nonebot2[httpx]
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 3
|
||||
description: nonebot.drivers.httpx 模块
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from typing_extensions import override
|
||||
from typing import TYPE_CHECKING, Union, Optional
|
||||
|
||||
from multidict import CIMultiDict
|
||||
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.internal.driver import Cookies, QueryTypes, CookieTypes, HeaderTypes
|
||||
from nonebot.drivers import (
|
||||
URL,
|
||||
HTTPClientMixin,
|
||||
HTTPClientSession,
|
||||
HTTPVersion,
|
||||
Request,
|
||||
Response,
|
||||
HTTPVersion,
|
||||
HTTPClientMixin,
|
||||
HTTPClientSession,
|
||||
combine_driver,
|
||||
)
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.internal.driver import Cookies, CookieTypes, HeaderTypes, QueryTypes
|
||||
|
||||
try:
|
||||
import httpx
|
||||
@@ -82,8 +80,6 @@ class Session(HTTPClientSession):
|
||||
data=setup.data,
|
||||
files=setup.files,
|
||||
json=setup.json,
|
||||
# ensure the params priority
|
||||
params=setup.url.raw_query_string,
|
||||
headers=tuple(setup.headers.items()),
|
||||
cookies=setup.cookies.jar,
|
||||
timeout=setup.timeout,
|
||||
@@ -104,7 +100,7 @@ class Session(HTTPClientSession):
|
||||
headers=self._headers,
|
||||
cookies=self._cookies.jar,
|
||||
http2=self._version == HTTPVersion.H2,
|
||||
proxy=self._proxy,
|
||||
proxies=self._proxy,
|
||||
follow_redirects=True,
|
||||
)
|
||||
await self._client.__aenter__()
|
||||
|
@@ -5,25 +5,19 @@
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 6
|
||||
description: nonebot.drivers.none 模块
|
||||
"""
|
||||
|
||||
import signal
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
import threading
|
||||
from typing_extensions import override
|
||||
|
||||
import anyio
|
||||
from anyio.abc import TaskGroup
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
|
||||
from nonebot.config import Config, Env
|
||||
from nonebot.consts import WINDOWS
|
||||
from nonebot.drivers import Driver as BaseDriver
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import flatten_exception_group
|
||||
from nonebot.consts import WINDOWS
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.drivers import Driver as BaseDriver
|
||||
|
||||
HANDLED_SIGNALS = (
|
||||
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
||||
@@ -39,8 +33,8 @@ class Driver(BaseDriver):
|
||||
def __init__(self, env: Env, config: Config):
|
||||
super().__init__(env, config)
|
||||
|
||||
self.should_exit: anyio.Event = anyio.Event()
|
||||
self.force_exit: anyio.Event = anyio.Event()
|
||||
self.should_exit: asyncio.Event = asyncio.Event()
|
||||
self.force_exit: bool = False
|
||||
|
||||
@property
|
||||
@override
|
||||
@@ -58,95 +52,84 @@ class Driver(BaseDriver):
|
||||
def run(self, *args, **kwargs):
|
||||
"""启动 none driver"""
|
||||
super().run(*args, **kwargs)
|
||||
anyio.run(self._serve)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self._serve())
|
||||
|
||||
async def _serve(self):
|
||||
async with anyio.create_task_group() as driver_tg:
|
||||
driver_tg.start_soon(self._handle_signals)
|
||||
driver_tg.start_soon(self._listen_force_exit, driver_tg)
|
||||
driver_tg.start_soon(self._handle_lifespan, driver_tg)
|
||||
|
||||
async def _handle_signals(self):
|
||||
try:
|
||||
with anyio.open_signal_receiver(*HANDLED_SIGNALS) as signal_receiver:
|
||||
async for sig in signal_receiver:
|
||||
self.exit(force=self.should_exit.is_set())
|
||||
except NotImplementedError:
|
||||
# Windows
|
||||
for sig in HANDLED_SIGNALS:
|
||||
signal.signal(sig, self._handle_legacy_signal)
|
||||
|
||||
# backport for Windows signal handling
|
||||
def _handle_legacy_signal(self, sig, frame):
|
||||
self.exit(force=self.should_exit.is_set())
|
||||
|
||||
async def _handle_lifespan(self, tg: TaskGroup):
|
||||
try:
|
||||
await self._startup()
|
||||
|
||||
if self.should_exit.is_set():
|
||||
return
|
||||
|
||||
await self._listen_exit()
|
||||
|
||||
await self._shutdown()
|
||||
finally:
|
||||
tg.cancel_scope.cancel()
|
||||
self._install_signal_handlers()
|
||||
await self._startup()
|
||||
if self.should_exit.is_set():
|
||||
return
|
||||
await self._main_loop()
|
||||
await self._shutdown()
|
||||
|
||||
async def _startup(self):
|
||||
def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||
self.should_exit.set()
|
||||
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(
|
||||
"<r><bg #f8bbd0>Error occurred while running startup hook."
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
logger.error(
|
||||
"<r><bg #f8bbd0>Application startup failed. Exiting.</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
with catch({Exception: handle_exception}):
|
||||
try:
|
||||
await self._lifespan.startup()
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Application startup failed. "
|
||||
"Exiting.</bg #f8bbd0></r>"
|
||||
)
|
||||
self.should_exit.set()
|
||||
return
|
||||
|
||||
if not self.should_exit.is_set():
|
||||
logger.info("Application startup completed.")
|
||||
logger.info("Application startup completed.")
|
||||
|
||||
async def _listen_exit(self, tg: Optional[TaskGroup] = None):
|
||||
async def _main_loop(self):
|
||||
await self.should_exit.wait()
|
||||
|
||||
if tg is not None:
|
||||
tg.cancel_scope.cancel()
|
||||
|
||||
async def _shutdown(self):
|
||||
logger.info("Shutting down")
|
||||
logger.info("Waiting for application shutdown. (CTRL+C to force quit)")
|
||||
|
||||
error_occurred: bool = False
|
||||
logger.info("Waiting for application shutdown.")
|
||||
|
||||
def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||
nonlocal error_occurred
|
||||
|
||||
error_occurred = True
|
||||
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(
|
||||
"<r><bg #f8bbd0>Error occurred while running shutdown hook."
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
logger.error(
|
||||
"<r><bg #f8bbd0>Application shutdown failed. Exiting.</bg #f8bbd0></r>"
|
||||
try:
|
||||
await self._lifespan.shutdown()
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running shutdown function. "
|
||||
"Ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
with catch({Exception: handle_exception}):
|
||||
await self._lifespan.shutdown()
|
||||
for task in asyncio.all_tasks():
|
||||
if task is not asyncio.current_task() and not task.done():
|
||||
task.cancel()
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
if not error_occurred:
|
||||
logger.info("Application shutdown complete.")
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
if tasks and not self.force_exit:
|
||||
logger.info("Waiting for tasks to finish. (CTRL+C to force quit)")
|
||||
while tasks and not self.force_exit:
|
||||
await asyncio.sleep(0.1)
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
|
||||
async def _listen_force_exit(self, tg: TaskGroup):
|
||||
await self.force_exit.wait()
|
||||
tg.cancel_scope.cancel()
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
logger.info("Application shutdown complete.")
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.stop()
|
||||
|
||||
def _install_signal_handlers(self) -> None:
|
||||
if threading.current_thread() is not threading.main_thread():
|
||||
# Signals can only be listened to from the main thread.
|
||||
return
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
try:
|
||||
for sig in HANDLED_SIGNALS:
|
||||
loop.add_signal_handler(sig, self._handle_exit, sig, None)
|
||||
except NotImplementedError:
|
||||
# Windows
|
||||
for sig in HANDLED_SIGNALS:
|
||||
signal.signal(sig, self._handle_exit)
|
||||
|
||||
def _handle_exit(self, sig, frame):
|
||||
self.exit(force=self.should_exit.is_set())
|
||||
|
||||
def exit(self, force: bool = False):
|
||||
"""退出 none driver
|
||||
@@ -157,4 +140,4 @@ class Driver(BaseDriver):
|
||||
if not self.should_exit.is_set():
|
||||
self.should_exit.set()
|
||||
if force:
|
||||
self.force_exit.set()
|
||||
self.force_exit = True
|
||||
|
@@ -11,37 +11,36 @@ pip install nonebot2[quart]
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 5
|
||||
description: nonebot.drivers.quart 模块
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
from typing import Any, Optional, Union, cast
|
||||
from typing_extensions import override
|
||||
from typing import Any, Union, Optional, cast
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from nonebot.compat import model_dump, type_validate_python
|
||||
from nonebot.config import Config as NoneBotConfig
|
||||
from nonebot.config import Env
|
||||
from nonebot.drivers import ASGIMixin, HTTPServerSetup, WebSocketServerSetup
|
||||
from nonebot.drivers import Driver as BaseDriver
|
||||
from nonebot.drivers import Request as BaseRequest
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ASGIMixin
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.internal.driver import FileTypes
|
||||
from nonebot.drivers import Driver as BaseDriver
|
||||
from nonebot.config import Config as NoneBotConfig
|
||||
from nonebot.drivers import Request as BaseRequest
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.compat import model_dump, type_validate_python
|
||||
from nonebot.drivers import HTTPServerSetup, WebSocketServerSetup
|
||||
|
||||
try:
|
||||
from quart import Quart, Request, Response
|
||||
from quart import Websocket as QuartWebSocket
|
||||
import uvicorn
|
||||
from quart import request as _request
|
||||
from quart.ctx import WebsocketContext
|
||||
from quart.datastructures import FileStorage
|
||||
from quart.globals import websocket_ctx
|
||||
import uvicorn
|
||||
from quart import Quart, Request, Response
|
||||
from quart.datastructures import FileStorage
|
||||
from quart import Websocket as QuartWebSocket
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install Quart first to use this driver. "
|
||||
|
@@ -11,25 +11,23 @@ pip install nonebot2[websockets]
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 4
|
||||
description: nonebot.drivers.websockets 模块
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from functools import wraps
|
||||
import logging
|
||||
from types import CoroutineType
|
||||
from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
|
||||
from functools import wraps
|
||||
from contextlib import asynccontextmanager
|
||||
from typing_extensions import ParamSpec, override
|
||||
from collections.abc import Coroutine, AsyncGenerator
|
||||
from typing import TYPE_CHECKING, Any, Union, TypeVar, Callable
|
||||
|
||||
from nonebot.drivers import Request, WebSocketClientMixin, combine_driver
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers import Request
|
||||
from nonebot.log import LoguruHandler
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import WebSocketClientMixin, combine_driver
|
||||
|
||||
try:
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
@@ -48,8 +46,8 @@ logger.addHandler(LoguruHandler())
|
||||
|
||||
|
||||
def catch_closed(
|
||||
func: Callable[P, "CoroutineType[Any, Any, T]"],
|
||||
) -> Callable[P, "CoroutineType[Any, Any, T]"]:
|
||||
func: Callable[P, Coroutine[Any, Any, T]]
|
||||
) -> Callable[P, Coroutine[Any, Any, T]]:
|
||||
@wraps(func)
|
||||
async def decorator(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||
try:
|
||||
@@ -71,8 +69,6 @@ class Mixin(WebSocketClientMixin):
|
||||
@override
|
||||
@asynccontextmanager
|
||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||
if setup.proxy is not None:
|
||||
logger.warning("proxy is not supported by websockets driver")
|
||||
connection = Connect(
|
||||
str(setup.url),
|
||||
extra_headers={**setup.headers, **setup.cookies.as_header(setup)},
|
||||
|
@@ -25,8 +25,6 @@ NoneBotException
|
||||
```
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 10
|
||||
description: nonebot.exception 模块
|
||||
"""
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from .adapter import Adapter as Adapter
|
||||
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
|
||||
|
@@ -1,21 +1,21 @@
|
||||
import abc
|
||||
from typing import Any
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
from nonebot.config import Config
|
||||
from nonebot.internal.driver._lifespan import LIFESPAN_FUNC
|
||||
from nonebot.internal.driver import (
|
||||
ASGIMixin,
|
||||
Driver,
|
||||
HTTPClientMixin,
|
||||
HTTPServerSetup,
|
||||
Request,
|
||||
Response,
|
||||
ASGIMixin,
|
||||
WebSocket,
|
||||
HTTPClientMixin,
|
||||
HTTPServerSetup,
|
||||
WebSocketClientMixin,
|
||||
WebSocketServerSetup,
|
||||
)
|
||||
from nonebot.internal.driver._lifespan import LIFESPAN_FUNC
|
||||
|
||||
from .bot import Bot
|
||||
|
||||
|
@@ -1,19 +1,16 @@
|
||||
import abc
|
||||
import asyncio
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Protocol, Union
|
||||
|
||||
import anyio
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
from typing import TYPE_CHECKING, Any, Union, ClassVar, Optional, Protocol
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.config import Config
|
||||
from nonebot.exception import MockApiException
|
||||
from nonebot.log import logger
|
||||
from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook
|
||||
from nonebot.utils import flatten_exception_group
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .adapter import Adapter
|
||||
from .event import Event
|
||||
from .adapter import Adapter
|
||||
from .message import Message, MessageSegment
|
||||
|
||||
class _ApiCall(Protocol):
|
||||
@@ -79,49 +76,21 @@ class Bot(abc.ABC):
|
||||
skip_calling_api: bool = False
|
||||
exception: Optional[Exception] = None
|
||||
|
||||
if self._calling_api_hook:
|
||||
logger.debug("Running CallingAPI hooks...")
|
||||
|
||||
def _handle_mock_api_exception(
|
||||
exc_group: BaseExceptionGroup[MockApiException],
|
||||
) -> None:
|
||||
nonlocal skip_calling_api, result
|
||||
|
||||
excs = [
|
||||
exc
|
||||
for exc in flatten_exception_group(exc_group)
|
||||
if isinstance(exc, MockApiException)
|
||||
]
|
||||
if not excs:
|
||||
return
|
||||
elif len(excs) > 1:
|
||||
logger.warning(
|
||||
"Multiple hooks want to mock API result. Use the first one."
|
||||
)
|
||||
|
||||
if coros := [hook(self, api, data) for hook in self._calling_api_hook]:
|
||||
try:
|
||||
logger.debug("Running CallingAPI hooks...")
|
||||
await asyncio.gather(*coros)
|
||||
except MockApiException as e:
|
||||
skip_calling_api = True
|
||||
result = excs[0].result
|
||||
|
||||
result = e.result
|
||||
logger.debug(
|
||||
f"Calling API {api} is cancelled. Return {result!r} instead."
|
||||
f"Calling API {api} is cancelled. Return {result} instead."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running CallingAPI hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(
|
||||
"<r><bg #f8bbd0>Error when running CallingAPI hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
with catch(
|
||||
{
|
||||
MockApiException: _handle_mock_api_exception,
|
||||
Exception: _handle_exception,
|
||||
}
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for hook in self._calling_api_hook:
|
||||
tg.start_soon(hook, self, api, data)
|
||||
|
||||
if not skip_calling_api:
|
||||
try:
|
||||
@@ -129,48 +98,25 @@ class Bot(abc.ABC):
|
||||
except Exception as e:
|
||||
exception = e
|
||||
|
||||
if self._called_api_hook:
|
||||
logger.debug("Running CalledAPI hooks...")
|
||||
|
||||
def _handle_mock_api_exception(
|
||||
exc_group: BaseExceptionGroup[MockApiException],
|
||||
) -> None:
|
||||
nonlocal result, exception
|
||||
|
||||
excs = [
|
||||
exc
|
||||
for exc in flatten_exception_group(exc_group)
|
||||
if isinstance(exc, MockApiException)
|
||||
]
|
||||
if not excs:
|
||||
return
|
||||
elif len(excs) > 1:
|
||||
logger.warning(
|
||||
"Multiple hooks want to mock API result. Use the first one."
|
||||
)
|
||||
|
||||
result = excs[0].result
|
||||
if coros := [
|
||||
hook(self, exception, api, data, result) for hook in self._called_api_hook
|
||||
]:
|
||||
try:
|
||||
logger.debug("Running CalledAPI hooks...")
|
||||
await asyncio.gather(*coros)
|
||||
except MockApiException as e:
|
||||
# mock api result
|
||||
result = e.result
|
||||
# ignore exception
|
||||
exception = None
|
||||
logger.debug(
|
||||
f"Calling API {api} result is mocked. Return {result} instead."
|
||||
)
|
||||
|
||||
def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(
|
||||
"<r><bg #f8bbd0>Error when running CalledAPI hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
with catch(
|
||||
{
|
||||
MockApiException: _handle_mock_api_exception,
|
||||
Exception: _handle_exception,
|
||||
}
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for hook in self._called_api_hook:
|
||||
tg.start_soon(hook, self, exception, api, data, result)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running CalledAPI hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
if exception:
|
||||
raise exception
|
||||
|
@@ -3,8 +3,8 @@ from typing import Any, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from nonebot.compat import PYDANTIC_V2, ConfigDict
|
||||
from nonebot.utils import DataclassEncoder
|
||||
from nonebot.compat import PYDANTIC_V2, ConfigDict
|
||||
|
||||
from .message import Message
|
||||
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import abc
|
||||
from collections.abc import Iterable
|
||||
from copy import deepcopy
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing_extensions import Self
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import field, asdict, dataclass
|
||||
from typing import ( # noqa: UP035
|
||||
Any,
|
||||
Type,
|
||||
Union,
|
||||
Generic,
|
||||
TypeVar,
|
||||
Optional,
|
||||
SupportsIndex,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
|
||||
from nonebot.compat import custom_validation, type_validate_python
|
||||
|
||||
@@ -51,10 +51,10 @@ class MessageSegment(abc.ABC, Generic[TM]):
|
||||
) -> bool:
|
||||
return not self == other
|
||||
|
||||
def __add__(self, other: Union[str, Self, Iterable[Self]]) -> TM:
|
||||
def __add__(self: TMS, other: Union[str, TMS, Iterable[TMS]]) -> TM:
|
||||
return self.get_message_class()(self) + other
|
||||
|
||||
def __radd__(self, other: Union[str, Self, Iterable[Self]]) -> TM:
|
||||
def __radd__(self: TMS, other: Union[str, TMS, Iterable[TMS]]) -> TM:
|
||||
return self.get_message_class()(other) + self
|
||||
|
||||
@classmethod
|
||||
@@ -87,7 +87,7 @@ class MessageSegment(abc.ABC, Generic[TM]):
|
||||
def items(self):
|
||||
return asdict(self).items()
|
||||
|
||||
def join(self, iterable: Iterable[Union[Self, TM]]) -> TM:
|
||||
def join(self: TMS, iterable: Iterable[Union[TMS, TM]]) -> TM:
|
||||
return self.get_message_class()(self).join(iterable)
|
||||
|
||||
def copy(self) -> Self:
|
||||
@@ -329,9 +329,8 @@ class Message(list[TMS], abc.ABC):
|
||||
return self[type_]
|
||||
|
||||
iterator, filtered = (
|
||||
(seg for seg in self if seg.type == type_),
|
||||
self.__class__(),
|
||||
)
|
||||
seg for seg in self if seg.type == type_
|
||||
), self.__class__()
|
||||
for _ in range(count):
|
||||
seg = next(iterator, None)
|
||||
if seg is None:
|
||||
|
@@ -1,19 +1,20 @@
|
||||
from _string import formatter_field_name_split # type: ignore
|
||||
from collections.abc import Mapping, Sequence
|
||||
import functools
|
||||
from string import Formatter
|
||||
from typing_extensions import TypeAlias
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
Generic,
|
||||
TypeVar,
|
||||
Callable,
|
||||
Optional,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from _string import formatter_field_name_split # type: ignore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .message import Message, MessageSegment
|
||||
|
@@ -1,31 +1,31 @@
|
||||
from .abstract import ASGIMixin as ASGIMixin
|
||||
from .model import URL as URL
|
||||
from .model import RawURL as RawURL
|
||||
from .abstract import Mixin as Mixin
|
||||
from .model import Cookies as Cookies
|
||||
from .model import Request as Request
|
||||
from .abstract import Driver as Driver
|
||||
from .abstract import ForwardDriver as ForwardDriver
|
||||
from .model import FileType as FileType
|
||||
from .model import Response as Response
|
||||
from .model import DataTypes as DataTypes
|
||||
from .model import FileTypes as FileTypes
|
||||
from .model import WebSocket as WebSocket
|
||||
from .model import FilesTypes as FilesTypes
|
||||
from .model import QueryTypes as QueryTypes
|
||||
from .abstract import ASGIMixin as ASGIMixin
|
||||
from .model import CookieTypes as CookieTypes
|
||||
from .model import FileContent as FileContent
|
||||
from .model import HTTPVersion as HTTPVersion
|
||||
from .model import HeaderTypes as HeaderTypes
|
||||
from .model import SimpleQuery as SimpleQuery
|
||||
from .model import ContentTypes as ContentTypes
|
||||
from .model import QueryVariable as QueryVariable
|
||||
from .abstract import ForwardMixin as ForwardMixin
|
||||
from .abstract import ReverseMixin as ReverseMixin
|
||||
from .abstract import ForwardDriver as ForwardDriver
|
||||
from .abstract import ReverseDriver as ReverseDriver
|
||||
from .combine import combine_driver as combine_driver
|
||||
from .model import HTTPServerSetup as HTTPServerSetup
|
||||
from .abstract import HTTPClientMixin as HTTPClientMixin
|
||||
from .abstract import HTTPClientSession as HTTPClientSession
|
||||
from .abstract import Mixin as Mixin
|
||||
from .abstract import ReverseDriver as ReverseDriver
|
||||
from .abstract import ReverseMixin as ReverseMixin
|
||||
from .abstract import WebSocketClientMixin as WebSocketClientMixin
|
||||
from .combine import combine_driver as combine_driver
|
||||
from .model import URL as URL
|
||||
from .model import ContentTypes as ContentTypes
|
||||
from .model import Cookies as Cookies
|
||||
from .model import CookieTypes as CookieTypes
|
||||
from .model import DataTypes as DataTypes
|
||||
from .model import FileContent as FileContent
|
||||
from .model import FilesTypes as FilesTypes
|
||||
from .model import FileType as FileType
|
||||
from .model import FileTypes as FileTypes
|
||||
from .model import HeaderTypes as HeaderTypes
|
||||
from .model import HTTPServerSetup as HTTPServerSetup
|
||||
from .model import HTTPVersion as HTTPVersion
|
||||
from .model import QueryTypes as QueryTypes
|
||||
from .model import QueryVariable as QueryVariable
|
||||
from .model import RawURL as RawURL
|
||||
from .model import Request as Request
|
||||
from .model import Response as Response
|
||||
from .model import SimpleQuery as SimpleQuery
|
||||
from .model import WebSocket as WebSocket
|
||||
from .model import WebSocketServerSetup as WebSocketServerSetup
|
||||
from .abstract import WebSocketClientMixin as WebSocketClientMixin
|
||||
|
@@ -1,13 +1,8 @@
|
||||
from collections.abc import Awaitable, Iterable
|
||||
from types import TracebackType
|
||||
from typing import Any, Callable, Optional, Union, cast
|
||||
from collections.abc import Awaitable
|
||||
from typing_extensions import TypeAlias
|
||||
from typing import Any, Union, Callable, cast
|
||||
|
||||
import anyio
|
||||
from anyio.abc import TaskGroup
|
||||
from exceptiongroup import suppress
|
||||
|
||||
from nonebot.utils import is_coroutine_callable, run_sync
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
|
||||
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
|
||||
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
|
||||
@@ -16,24 +11,10 @@ LIFESPAN_FUNC: TypeAlias = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
|
||||
|
||||
class Lifespan:
|
||||
def __init__(self) -> None:
|
||||
self._task_group: Optional[TaskGroup] = None
|
||||
|
||||
self._startup_funcs: list[LIFESPAN_FUNC] = []
|
||||
self._ready_funcs: list[LIFESPAN_FUNC] = []
|
||||
self._shutdown_funcs: list[LIFESPAN_FUNC] = []
|
||||
|
||||
@property
|
||||
def task_group(self) -> TaskGroup:
|
||||
if self._task_group is None:
|
||||
raise RuntimeError("Lifespan not started")
|
||||
return self._task_group
|
||||
|
||||
@task_group.setter
|
||||
def task_group(self, task_group: TaskGroup) -> None:
|
||||
if self._task_group is not None:
|
||||
raise RuntimeError("Lifespan already started")
|
||||
self._task_group = task_group
|
||||
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
self._startup_funcs.append(func)
|
||||
return func
|
||||
@@ -48,7 +29,7 @@ class Lifespan:
|
||||
|
||||
@staticmethod
|
||||
async def _run_lifespan_func(
|
||||
funcs: Iterable[LIFESPAN_FUNC],
|
||||
funcs: list[LIFESPAN_FUNC],
|
||||
) -> None:
|
||||
for func in funcs:
|
||||
if is_coroutine_callable(func):
|
||||
@@ -57,44 +38,18 @@ class Lifespan:
|
||||
await run_sync(cast(SYNC_LIFESPAN_FUNC, func))()
|
||||
|
||||
async def startup(self) -> None:
|
||||
# create background task group
|
||||
self.task_group = anyio.create_task_group()
|
||||
await self.task_group.__aenter__()
|
||||
|
||||
# run startup funcs
|
||||
if self._startup_funcs:
|
||||
await self._run_lifespan_func(self._startup_funcs)
|
||||
|
||||
# run ready funcs
|
||||
if self._ready_funcs:
|
||||
await self._run_lifespan_func(self._ready_funcs)
|
||||
|
||||
async def shutdown(
|
||||
self,
|
||||
*,
|
||||
exc_type: Optional[type[BaseException]] = None,
|
||||
exc_val: Optional[BaseException] = None,
|
||||
exc_tb: Optional[TracebackType] = None,
|
||||
) -> None:
|
||||
async def shutdown(self) -> None:
|
||||
if self._shutdown_funcs:
|
||||
# reverse shutdown funcs to ensure stack order
|
||||
await self._run_lifespan_func(reversed(self._shutdown_funcs))
|
||||
|
||||
# shutdown background task group
|
||||
self.task_group.cancel_scope.cancel()
|
||||
|
||||
with suppress(Exception):
|
||||
await self.task_group.__aexit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
self._task_group = None
|
||||
await self._run_lifespan_func(self._shutdown_funcs)
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
await self.startup()
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: Optional[type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
await self.shutdown(exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb)
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
await self.shutdown()
|
||||
|
@@ -1,41 +1,38 @@
|
||||
import abc
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
import asyncio
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing_extensions import Self, TypeAlias
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Union, ClassVar, Optional
|
||||
|
||||
from anyio import CancelScope, create_task_group
|
||||
from anyio.abc import TaskGroup
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
|
||||
from nonebot.config import Config, Env
|
||||
from nonebot.log import logger
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.internal.params import BotParam, DefaultParam, DependParam
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import escape_tag, run_coro_with_catch
|
||||
from nonebot.internal.params import BotParam, DependParam, DefaultParam
|
||||
from nonebot.typing import (
|
||||
T_DependencyCache,
|
||||
T_BotConnectionHook,
|
||||
T_BotDisconnectionHook,
|
||||
T_DependencyCache,
|
||||
)
|
||||
from nonebot.utils import escape_tag, flatten_exception_group, run_coro_with_catch
|
||||
|
||||
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
||||
from .model import (
|
||||
CookieTypes,
|
||||
HeaderTypes,
|
||||
HTTPServerSetup,
|
||||
HTTPVersion,
|
||||
QueryTypes,
|
||||
Request,
|
||||
Response,
|
||||
WebSocket,
|
||||
QueryTypes,
|
||||
CookieTypes,
|
||||
HeaderTypes,
|
||||
HTTPVersion,
|
||||
HTTPServerSetup,
|
||||
WebSocketServerSetup,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.internal.adapter import Adapter, Bot
|
||||
from nonebot.internal.adapter import Bot, Adapter
|
||||
|
||||
|
||||
BOT_HOOK_PARAMS = [DependParam, BotParam, DefaultParam]
|
||||
@@ -64,6 +61,7 @@ class Driver(abc.ABC):
|
||||
self.config: Config = config
|
||||
"""全局配置对象"""
|
||||
self._bots: dict[str, "Bot"] = {}
|
||||
self._bot_tasks: set[asyncio.Task] = set()
|
||||
self._lifespan = Lifespan()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -77,10 +75,6 @@ class Driver(abc.ABC):
|
||||
"""获取当前所有已连接的 Bot"""
|
||||
return self._bots
|
||||
|
||||
@property
|
||||
def task_group(self) -> TaskGroup:
|
||||
return self._lifespan.task_group
|
||||
|
||||
def register_adapter(self, adapter: type["Adapter"], **kwargs) -> None:
|
||||
"""注册一个协议适配器
|
||||
|
||||
@@ -114,10 +108,12 @@ class Driver(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def run(self, *args, **kwargs):
|
||||
"""启动驱动框架"""
|
||||
logger.opt(colors=True).success(
|
||||
logger.opt(colors=True).debug(
|
||||
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
|
||||
)
|
||||
|
||||
self.on_shutdown(self._cleanup)
|
||||
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""注册一个启动时执行的函数"""
|
||||
return self._lifespan.on_startup(func)
|
||||
@@ -158,63 +154,66 @@ class Driver(abc.ABC):
|
||||
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
|
||||
self._bots[bot.self_id] = bot
|
||||
|
||||
if not self._bot_connection_hook:
|
||||
return
|
||||
|
||||
def handle_exception(exc_group: BaseExceptionGroup) -> None:
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(
|
||||
"<r><bg #f8bbd0>"
|
||||
"Error when running WebSocketConnection hook:"
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
with CancelScope(shield=True), catch({Exception: handle_exception}):
|
||||
async with AsyncExitStack() as stack, create_task_group() as tg:
|
||||
for hook in self._bot_connection_hook:
|
||||
tg.start_soon(
|
||||
run_coro_with_catch,
|
||||
hook(
|
||||
bot=bot, stack=stack, dependency_cache=dependency_cache
|
||||
),
|
||||
(SkippedException,),
|
||||
async with AsyncExitStack() as stack:
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
|
||||
(SkippedException,),
|
||||
)
|
||||
for hook in self._bot_connection_hook
|
||||
]:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>"
|
||||
"Error when running WebSocketConnection hook. "
|
||||
"Running cancelled!"
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
self.task_group.start_soon(_run_hook, bot)
|
||||
task = asyncio.create_task(_run_hook(bot))
|
||||
task.add_done_callback(self._bot_tasks.discard)
|
||||
self._bot_tasks.add(task)
|
||||
|
||||
def _bot_disconnect(self, bot: "Bot") -> None:
|
||||
"""在连接断开后,调用该函数来注销 bot 对象"""
|
||||
if bot.self_id in self._bots:
|
||||
del self._bots[bot.self_id]
|
||||
|
||||
if not self._bot_disconnection_hook:
|
||||
return
|
||||
|
||||
def handle_exception(exc_group: BaseExceptionGroup) -> None:
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(
|
||||
"<r><bg #f8bbd0>"
|
||||
"Error when running WebSocketDisConnection hook:"
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
# shield cancellation to ensure bot disconnect hooks are always run
|
||||
with CancelScope(shield=True), catch({Exception: handle_exception}):
|
||||
async with create_task_group() as tg, AsyncExitStack() as stack:
|
||||
for hook in self._bot_disconnection_hook:
|
||||
tg.start_soon(
|
||||
run_coro_with_catch,
|
||||
hook(
|
||||
bot=bot, stack=stack, dependency_cache=dependency_cache
|
||||
),
|
||||
(SkippedException,),
|
||||
async with AsyncExitStack() as stack:
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
|
||||
(SkippedException,),
|
||||
)
|
||||
for hook in self._bot_disconnection_hook
|
||||
]:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>"
|
||||
"Error when running WebSocketDisConnection hook. "
|
||||
"Running cancelled!"
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
self.task_group.start_soon(_run_hook, bot)
|
||||
task = asyncio.create_task(_run_hook(bot))
|
||||
task.add_done_callback(self._bot_tasks.discard)
|
||||
self._bot_tasks.add(task)
|
||||
|
||||
async def _cleanup(self) -> None:
|
||||
"""清理驱动器资源"""
|
||||
if self._bot_tasks:
|
||||
logger.opt(colors=True).debug(
|
||||
"<y>Waiting for running bot connection hooks...</y>"
|
||||
)
|
||||
await asyncio.gather(*self._bot_tasks, return_exceptions=True)
|
||||
|
||||
|
||||
class Mixin(abc.ABC):
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from typing import TYPE_CHECKING, TypeVar, Union, overload
|
||||
from typing import TYPE_CHECKING, Union, TypeVar, overload
|
||||
|
||||
from .abstract import Driver, Mixin
|
||||
from .abstract import Mixin, Driver
|
||||
|
||||
D = TypeVar("D", bound="Driver")
|
||||
|
||||
@@ -39,4 +39,6 @@ def combine_driver(
|
||||
+ "+".join(x.type.__get__(self) for x in mixins) # type: ignore
|
||||
)
|
||||
|
||||
return type("CombinedDriver", (*mixins, driver), {"type": property(type_)}) # type: ignore
|
||||
return type(
|
||||
"CombinedDriver", (*mixins, driver), {"type": property(type_)}
|
||||
) # type: ignore
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import abc
|
||||
from collections.abc import Awaitable, Iterator, Mapping, MutableMapping
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from http.cookiejar import Cookie, CookieJar
|
||||
from typing import IO, Any, Callable, Optional, Union
|
||||
from typing_extensions import TypeAlias
|
||||
import urllib.request
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from typing_extensions import TypeAlias
|
||||
from http.cookiejar import Cookie, CookieJar
|
||||
from typing import IO, Any, Union, Callable, Optional
|
||||
from collections.abc import Mapping, Iterator, Awaitable, MutableMapping
|
||||
|
||||
from multidict import CIMultiDict
|
||||
from yarl import URL as URL
|
||||
from multidict import CIMultiDict
|
||||
|
||||
RawURL: TypeAlias = tuple[bytes, bytes, Optional[int], bytes]
|
||||
|
||||
|
@@ -1,12 +1,12 @@
|
||||
from .manager import MatcherManager as MatcherManager
|
||||
from .provider import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
||||
from .provider import MatcherProvider as MatcherProvider
|
||||
from .provider import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
||||
|
||||
matchers = MatcherManager()
|
||||
|
||||
from .matcher import Matcher as Matcher
|
||||
from .matcher import MatcherSource as MatcherSource
|
||||
from .matcher import current_bot as current_bot
|
||||
from .matcher import MatcherSource as MatcherSource
|
||||
from .matcher import current_event as current_event
|
||||
from .matcher import current_handler as current_handler
|
||||
from .matcher import current_matcher as current_matcher
|
||||
|
@@ -1,5 +1,5 @@
|
||||
from collections.abc import ItemsView, Iterator, KeysView, MutableMapping, ValuesView
|
||||
from typing import TYPE_CHECKING, Optional, TypeVar, Union, overload
|
||||
from typing import TYPE_CHECKING, Union, TypeVar, Optional, overload
|
||||
from collections.abc import Iterator, KeysView, ItemsView, ValuesView, MutableMapping
|
||||
|
||||
from .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider
|
||||
|
||||
@@ -74,9 +74,9 @@ class MatcherManager(MutableMapping[int, list[type["Matcher"]]]):
|
||||
self.provider.clear()
|
||||
|
||||
def update( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
self, m: MutableMapping[int, list[type["Matcher"]]], /
|
||||
self, __m: MutableMapping[int, list[type["Matcher"]]]
|
||||
) -> None:
|
||||
self.provider.update(m)
|
||||
self.provider.update(__m)
|
||||
|
||||
def setdefault(
|
||||
self, key: int, default: list[type["Matcher"]]
|
||||
|
@@ -1,46 +1,32 @@
|
||||
from collections.abc import Iterable
|
||||
from contextlib import AsyncExitStack, contextmanager
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import inspect
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from dataclasses import dataclass
|
||||
from contextvars import ContextVar
|
||||
from typing_extensions import Self
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime, timedelta
|
||||
from contextlib import AsyncExitStack, contextmanager
|
||||
from typing import ( # noqa: UP035
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Type,
|
||||
Union,
|
||||
TypeVar,
|
||||
Callable,
|
||||
ClassVar,
|
||||
NoReturn,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
import warnings
|
||||
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
|
||||
from nonebot.consts import (
|
||||
ARG_KEY,
|
||||
LAST_RECEIVE_KEY,
|
||||
PAUSE_PROMPT_RESULT_KEY,
|
||||
RECEIVE_KEY,
|
||||
REJECT_CACHE_TARGET,
|
||||
REJECT_PROMPT_RESULT_KEY,
|
||||
REJECT_TARGET,
|
||||
)
|
||||
from nonebot.dependencies import Dependent, Param
|
||||
from nonebot.exception import (
|
||||
FinishedException,
|
||||
PausedException,
|
||||
RejectedException,
|
||||
SkippedException,
|
||||
StopPropagation,
|
||||
)
|
||||
from nonebot.log import logger
|
||||
from nonebot.internal.rule import Rule
|
||||
from nonebot.utils import classproperty
|
||||
from nonebot.dependencies import Param, Dependent
|
||||
from nonebot.internal.permission import User, Permission
|
||||
from nonebot.internal.adapter import (
|
||||
Bot,
|
||||
Event,
|
||||
@@ -48,27 +34,37 @@ from nonebot.internal.adapter import (
|
||||
MessageSegment,
|
||||
MessageTemplate,
|
||||
)
|
||||
from nonebot.typing import (
|
||||
T_State,
|
||||
T_Handler,
|
||||
T_TypeUpdater,
|
||||
T_DependencyCache,
|
||||
T_PermissionUpdater,
|
||||
)
|
||||
from nonebot.consts import (
|
||||
ARG_KEY,
|
||||
RECEIVE_KEY,
|
||||
REJECT_TARGET,
|
||||
LAST_RECEIVE_KEY,
|
||||
REJECT_CACHE_TARGET,
|
||||
)
|
||||
from nonebot.exception import (
|
||||
PausedException,
|
||||
StopPropagation,
|
||||
SkippedException,
|
||||
FinishedException,
|
||||
RejectedException,
|
||||
)
|
||||
from nonebot.internal.params import (
|
||||
Depends,
|
||||
ArgParam,
|
||||
BotParam,
|
||||
DefaultParam,
|
||||
DependParam,
|
||||
Depends,
|
||||
EventParam,
|
||||
MatcherParam,
|
||||
StateParam,
|
||||
DependParam,
|
||||
DefaultParam,
|
||||
MatcherParam,
|
||||
)
|
||||
from nonebot.internal.permission import Permission, User
|
||||
from nonebot.internal.rule import Rule
|
||||
from nonebot.log import logger
|
||||
from nonebot.typing import (
|
||||
T_DependencyCache,
|
||||
T_Handler,
|
||||
T_PermissionUpdater,
|
||||
T_State,
|
||||
T_TypeUpdater,
|
||||
)
|
||||
from nonebot.utils import classproperty, flatten_exception_group
|
||||
|
||||
from . import matchers
|
||||
|
||||
@@ -562,8 +558,8 @@ class Matcher(metaclass=MatcherMeta):
|
||||
"""
|
||||
bot = current_bot.get()
|
||||
event = current_event.get()
|
||||
state = current_matcher.get().state
|
||||
if isinstance(message, MessageTemplate):
|
||||
state = current_matcher.get().state
|
||||
_message = message.format(**state)
|
||||
else:
|
||||
_message = message
|
||||
@@ -599,15 +595,8 @@ class Matcher(metaclass=MatcherMeta):
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||
请参考对应 adapter 的 bot 对象 api
|
||||
"""
|
||||
try:
|
||||
matcher = current_matcher.get()
|
||||
except Exception:
|
||||
matcher = None
|
||||
|
||||
if prompt is not None:
|
||||
result = await cls.send(prompt, **kwargs)
|
||||
if matcher is not None:
|
||||
matcher.state[PAUSE_PROMPT_RESULT_KEY] = result
|
||||
await cls.send(prompt, **kwargs)
|
||||
raise PausedException
|
||||
|
||||
@classmethod
|
||||
@@ -624,19 +613,8 @@ class Matcher(metaclass=MatcherMeta):
|
||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||
请参考对应 adapter 的 bot 对象 api
|
||||
"""
|
||||
try:
|
||||
matcher = current_matcher.get()
|
||||
key = matcher.get_target()
|
||||
except Exception:
|
||||
matcher = None
|
||||
key = None
|
||||
|
||||
key = REJECT_PROMPT_RESULT_KEY.format(key=key) if key is not None else None
|
||||
|
||||
if prompt is not None:
|
||||
result = await cls.send(prompt, **kwargs)
|
||||
if key is not None and matcher:
|
||||
matcher.state[key] = result
|
||||
await cls.send(prompt, **kwargs)
|
||||
raise RejectedException
|
||||
|
||||
@classmethod
|
||||
@@ -656,12 +634,9 @@ class Matcher(metaclass=MatcherMeta):
|
||||
请参考对应 adapter 的 bot 对象 api
|
||||
"""
|
||||
matcher = current_matcher.get()
|
||||
arg_key = ARG_KEY.format(key=key)
|
||||
matcher.set_target(arg_key)
|
||||
|
||||
matcher.set_target(ARG_KEY.format(key=key))
|
||||
if prompt is not None:
|
||||
result = await cls.send(prompt, **kwargs)
|
||||
matcher.state[REJECT_PROMPT_RESULT_KEY.format(key=arg_key)] = result
|
||||
await cls.send(prompt, **kwargs)
|
||||
raise RejectedException
|
||||
|
||||
@classmethod
|
||||
@@ -681,12 +656,9 @@ class Matcher(metaclass=MatcherMeta):
|
||||
请参考对应 adapter 的 bot 对象 api
|
||||
"""
|
||||
matcher = current_matcher.get()
|
||||
receive_key = RECEIVE_KEY.format(id=id)
|
||||
matcher.set_target(receive_key)
|
||||
|
||||
matcher.set_target(RECEIVE_KEY.format(id=id))
|
||||
if prompt is not None:
|
||||
result = await cls.send(prompt, **kwargs)
|
||||
matcher.state[REJECT_PROMPT_RESULT_KEY.format(key=receive_key)] = result
|
||||
await cls.send(prompt, **kwargs)
|
||||
raise RejectedException
|
||||
|
||||
@classmethod
|
||||
@@ -840,34 +812,28 @@ class Matcher(metaclass=MatcherMeta):
|
||||
f"bot={bot}, event={event!r}, state={state!r}"
|
||||
)
|
||||
|
||||
def _handle_stop_propagation(exc_group: BaseExceptionGroup[StopPropagation]):
|
||||
self.block = True
|
||||
|
||||
with self.ensure_context(bot, event):
|
||||
try:
|
||||
with catch({StopPropagation: _handle_stop_propagation}):
|
||||
# Refresh preprocess state
|
||||
self.state.update(state)
|
||||
# Refresh preprocess state
|
||||
self.state.update(state)
|
||||
|
||||
while self.remain_handlers:
|
||||
handler = self.remain_handlers.pop(0)
|
||||
current_handler.set(handler)
|
||||
logger.debug(f"Running handler {handler}")
|
||||
|
||||
def _handle_skipped(
|
||||
exc_group: BaseExceptionGroup[SkippedException],
|
||||
):
|
||||
logger.debug(f"Handler {handler} skipped")
|
||||
|
||||
with catch({SkippedException: _handle_skipped}):
|
||||
await handler(
|
||||
matcher=self,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
while self.remain_handlers:
|
||||
handler = self.remain_handlers.pop(0)
|
||||
current_handler.set(handler)
|
||||
logger.debug(f"Running handler {handler}")
|
||||
try:
|
||||
await handler(
|
||||
matcher=self,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
except SkippedException:
|
||||
logger.debug(f"Handler {handler} skipped")
|
||||
except StopPropagation:
|
||||
self.block = True
|
||||
finally:
|
||||
logger.info(f"{self} running complete")
|
||||
|
||||
@@ -880,54 +846,10 @@ class Matcher(metaclass=MatcherMeta):
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
):
|
||||
exc: Optional[Union[FinishedException, RejectedException, PausedException]] = (
|
||||
None
|
||||
)
|
||||
|
||||
def _handle_special_exception(
|
||||
exc_group: BaseExceptionGroup[
|
||||
Union[FinishedException, RejectedException, PausedException]
|
||||
],
|
||||
):
|
||||
nonlocal exc
|
||||
excs = list(flatten_exception_group(exc_group))
|
||||
if len(excs) > 1:
|
||||
logger.warning(
|
||||
"Multiple session control exceptions occurred. "
|
||||
"NoneBot will choose the proper one."
|
||||
)
|
||||
finished_exc = next(
|
||||
(e for e in excs if isinstance(e, FinishedException)),
|
||||
None,
|
||||
)
|
||||
rejected_exc = next(
|
||||
(e for e in excs if isinstance(e, RejectedException)),
|
||||
None,
|
||||
)
|
||||
paused_exc = next(
|
||||
(e for e in excs if isinstance(e, PausedException)),
|
||||
None,
|
||||
)
|
||||
exc = finished_exc or rejected_exc or paused_exc
|
||||
elif isinstance(
|
||||
excs[0], (FinishedException, RejectedException, PausedException)
|
||||
):
|
||||
exc = excs[0]
|
||||
|
||||
with catch(
|
||||
{
|
||||
(
|
||||
FinishedException,
|
||||
RejectedException,
|
||||
PausedException,
|
||||
): _handle_special_exception
|
||||
}
|
||||
):
|
||||
try:
|
||||
await self.simple_run(bot, event, state, stack, dependency_cache)
|
||||
|
||||
if isinstance(exc, FinishedException):
|
||||
pass
|
||||
elif isinstance(exc, RejectedException):
|
||||
except RejectedException:
|
||||
await self.resolve_reject()
|
||||
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||
permission = await self.update_permission(
|
||||
@@ -948,7 +870,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
default_type_updater=self.__class__._default_type_updater,
|
||||
default_permission_updater=self.__class__._default_permission_updater,
|
||||
)
|
||||
elif isinstance(exc, PausedException):
|
||||
except PausedException:
|
||||
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||
permission = await self.update_permission(
|
||||
bot, event, stack, dependency_cache
|
||||
@@ -968,3 +890,5 @@ class Matcher(metaclass=MatcherMeta):
|
||||
default_type_updater=self.__class__._default_type_updater,
|
||||
default_permission_updater=self.__class__._default_permission_updater,
|
||||
)
|
||||
except FinishedException:
|
||||
pass
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import abc
|
||||
from typing import TYPE_CHECKING
|
||||
from collections import defaultdict
|
||||
from collections.abc import Mapping, MutableMapping
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .matcher import Matcher
|
||||
@@ -19,7 +19,7 @@ class MatcherProvider(abc.ABC, MutableMapping[int, list[type["Matcher"]]]):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _DictProvider(defaultdict[int, list[type["Matcher"]]], MatcherProvider): # type: ignore
|
||||
class _DictProvider(defaultdict, MatcherProvider):
|
||||
def __init__(self, matchers: Mapping[int, list[type["Matcher"]]]):
|
||||
super().__init__(list, matchers)
|
||||
|
||||
|
@@ -1,47 +1,43 @@
|
||||
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
|
||||
from enum import Enum
|
||||
import asyncio
|
||||
import inspect
|
||||
from typing_extensions import Self, get_args, override, get_origin
|
||||
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
Any,
|
||||
Callable,
|
||||
Literal,
|
||||
Optional,
|
||||
Union,
|
||||
Literal,
|
||||
Callable,
|
||||
Optional,
|
||||
Annotated,
|
||||
cast,
|
||||
)
|
||||
from typing_extensions import Self, get_args, get_origin, override
|
||||
|
||||
import anyio
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
from pydantic.fields import FieldInfo as PydanticFieldInfo
|
||||
|
||||
from nonebot.compat import FieldInfo, ModelField, PydanticUndefined, extract_field_info
|
||||
from nonebot.consts import ARG_KEY, REJECT_PROMPT_RESULT_KEY
|
||||
from nonebot.dependencies import Dependent, Param
|
||||
from nonebot.dependencies import Param, Dependent
|
||||
from nonebot.dependencies.utils import check_field_type
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.compat import FieldInfo, ModelField, PydanticUndefined, extract_field_info
|
||||
from nonebot.typing import (
|
||||
_STATE_FLAG,
|
||||
T_DependencyCache,
|
||||
T_Handler,
|
||||
T_State,
|
||||
T_Handler,
|
||||
T_DependencyCache,
|
||||
origin_is_annotated,
|
||||
)
|
||||
from nonebot.utils import (
|
||||
generic_check_issubclass,
|
||||
get_name,
|
||||
run_sync,
|
||||
is_gen_callable,
|
||||
run_sync_ctx_manager,
|
||||
is_async_gen_callable,
|
||||
is_coroutine_callable,
|
||||
is_gen_callable,
|
||||
run_sync,
|
||||
run_sync_ctx_manager,
|
||||
generic_check_issubclass,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.adapters import Bot, Event, Message
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.adapters import Bot, Event
|
||||
|
||||
|
||||
class DependsInner:
|
||||
@@ -97,78 +93,6 @@ def Depends(
|
||||
return DependsInner(dependency, use_cache=use_cache, validate=validate)
|
||||
|
||||
|
||||
class CacheState(str, Enum):
|
||||
"""子依赖缓存状态"""
|
||||
|
||||
PENDING = "PENDING"
|
||||
FINISHED = "FINISHED"
|
||||
|
||||
|
||||
class DependencyCache:
|
||||
"""子依赖结果缓存。
|
||||
|
||||
用于缓存子依赖的结果,以避免重复计算。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._state = CacheState.PENDING
|
||||
self._result: Any = None
|
||||
self._exception: Optional[BaseException] = None
|
||||
self._waiter = anyio.Event()
|
||||
|
||||
def done(self) -> bool:
|
||||
return self._state == CacheState.FINISHED
|
||||
|
||||
def result(self) -> Any:
|
||||
"""获取子依赖结果"""
|
||||
|
||||
if self._state != CacheState.FINISHED:
|
||||
raise RuntimeError("Result is not ready")
|
||||
|
||||
if self._exception is not None:
|
||||
raise self._exception
|
||||
return self._result
|
||||
|
||||
def exception(self) -> Optional[BaseException]:
|
||||
"""获取子依赖异常"""
|
||||
|
||||
if self._state != CacheState.FINISHED:
|
||||
raise RuntimeError("Result is not ready")
|
||||
|
||||
return self._exception
|
||||
|
||||
def set_result(self, result: Any) -> None:
|
||||
"""设置子依赖结果"""
|
||||
|
||||
if self._state != CacheState.PENDING:
|
||||
raise RuntimeError(f"Cache state invalid: {self._state}")
|
||||
|
||||
self._result = result
|
||||
self._state = CacheState.FINISHED
|
||||
self._waiter.set()
|
||||
|
||||
def set_exception(self, exception: BaseException) -> None:
|
||||
"""设置子依赖异常"""
|
||||
|
||||
if self._state != CacheState.PENDING:
|
||||
raise RuntimeError(f"Cache state invalid: {self._state}")
|
||||
|
||||
self._exception = exception
|
||||
self._state = CacheState.FINISHED
|
||||
self._waiter.set()
|
||||
|
||||
async def wait(self):
|
||||
"""等待子依赖结果"""
|
||||
await self._waiter.wait()
|
||||
if self._state != CacheState.FINISHED:
|
||||
raise RuntimeError("Invalid cache state")
|
||||
|
||||
if self._exception is not None:
|
||||
raise self._exception
|
||||
|
||||
return self._result
|
||||
|
||||
|
||||
class DependParam(Param):
|
||||
"""子依赖注入参数。
|
||||
|
||||
@@ -228,9 +152,9 @@ class DependParam(Param):
|
||||
dependency: T_Handler
|
||||
# sub dependency is not specified, use type annotation
|
||||
if depends_inner.dependency is None:
|
||||
assert type_annotation is not inspect.Signature.empty, (
|
||||
"Dependency cannot be empty"
|
||||
)
|
||||
assert (
|
||||
type_annotation is not inspect.Signature.empty
|
||||
), "Dependency cannot be empty"
|
||||
dependency = type_annotation
|
||||
else:
|
||||
dependency = depends_inner.dependency
|
||||
@@ -270,56 +194,35 @@ class DependParam(Param):
|
||||
call = cast(Callable[..., Any], sub_dependent.call)
|
||||
|
||||
# solve sub dependency with current cache
|
||||
exc: Optional[BaseExceptionGroup[SkippedException]] = None
|
||||
|
||||
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
|
||||
nonlocal exc
|
||||
exc = exc_group
|
||||
|
||||
with catch({SkippedException: _handle_skipped}):
|
||||
sub_values = await sub_dependent.solve(
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if exc is not None:
|
||||
raise exc
|
||||
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:
|
||||
return await dependency_cache[call].wait()
|
||||
|
||||
if is_gen_callable(call) or is_async_gen_callable(call):
|
||||
assert isinstance(stack, AsyncExitStack), (
|
||||
"Generator dependency should be called in context"
|
||||
)
|
||||
return 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)
|
||||
|
||||
target = stack.enter_async_context(cm)
|
||||
task = asyncio.create_task(stack.enter_async_context(cm))
|
||||
dependency_cache[call] = task
|
||||
return await task
|
||||
elif is_coroutine_callable(call):
|
||||
target = call(**sub_values)
|
||||
task = asyncio.create_task(call(**sub_values))
|
||||
dependency_cache[call] = task
|
||||
return await task
|
||||
else:
|
||||
target = run_sync(call)(**sub_values)
|
||||
|
||||
dependency_cache[call] = cache = DependencyCache()
|
||||
try:
|
||||
result = await target
|
||||
except Exception as e:
|
||||
cache.set_exception(e)
|
||||
raise
|
||||
except BaseException as e:
|
||||
cache.set_exception(e)
|
||||
# remove cache when base exception occurs
|
||||
# e.g. CancelledError
|
||||
dependency_cache.pop(call, None)
|
||||
raise
|
||||
else:
|
||||
cache.set_result(result)
|
||||
return result
|
||||
task = asyncio.create_task(run_sync(call)(**sub_values))
|
||||
dependency_cache[call] = task
|
||||
return await task
|
||||
|
||||
@override
|
||||
async def _check(self, **kwargs: Any) -> None:
|
||||
@@ -523,10 +426,10 @@ class MatcherParam(Param):
|
||||
|
||||
class ArgInner:
|
||||
def __init__(
|
||||
self, key: Optional[str], type: Literal["message", "str", "plaintext", "prompt"]
|
||||
self, key: Optional[str], type: Literal["message", "str", "plaintext"]
|
||||
) -> None:
|
||||
self.key: Optional[str] = key
|
||||
self.type: Literal["message", "str", "plaintext", "prompt"] = type
|
||||
self.type: Literal["message", "str", "plaintext"] = type
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ArgInner(key={self.key!r}, type={self.type!r})"
|
||||
@@ -547,11 +450,6 @@ def ArgPlainText(key: Optional[str] = None) -> str:
|
||||
return ArgInner(key, "plaintext") # type: ignore
|
||||
|
||||
|
||||
def ArgPromptResult(key: Optional[str] = None) -> Any:
|
||||
"""`arg` prompt 发送结果"""
|
||||
return ArgInner(key, "prompt")
|
||||
|
||||
|
||||
class ArgParam(Param):
|
||||
"""Arg 注入参数
|
||||
|
||||
@@ -565,7 +463,7 @@ class ArgParam(Param):
|
||||
self,
|
||||
*args,
|
||||
key: str,
|
||||
type: Literal["message", "str", "plaintext", "prompt"],
|
||||
type: Literal["message", "str", "plaintext"],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -590,32 +488,15 @@ class ArgParam(Param):
|
||||
async def _solve( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
self, matcher: "Matcher", **kwargs: Any
|
||||
) -> Any:
|
||||
message = matcher.get_arg(self.key)
|
||||
if message is None:
|
||||
return message
|
||||
if self.type == "message":
|
||||
return self._solve_message(matcher)
|
||||
return message
|
||||
elif self.type == "str":
|
||||
return self._solve_str(matcher)
|
||||
elif self.type == "plaintext":
|
||||
return self._solve_plaintext(matcher)
|
||||
elif self.type == "prompt":
|
||||
return self._solve_prompt(matcher)
|
||||
return str(message)
|
||||
else:
|
||||
raise ValueError(f"Unknown Arg type: {self.type}")
|
||||
|
||||
def _solve_message(self, matcher: "Matcher") -> Optional["Message"]:
|
||||
return matcher.get_arg(self.key)
|
||||
|
||||
def _solve_str(self, matcher: "Matcher") -> Optional[str]:
|
||||
message = matcher.get_arg(self.key)
|
||||
return str(message) if message is not None else None
|
||||
|
||||
def _solve_plaintext(self, matcher: "Matcher") -> Optional[str]:
|
||||
message = matcher.get_arg(self.key)
|
||||
return message.extract_plain_text() if message is not None else None
|
||||
|
||||
def _solve_prompt(self, matcher: "Matcher") -> Optional[Any]:
|
||||
return matcher.state.get(
|
||||
REJECT_PROMPT_RESULT_KEY.format(key=ARG_KEY.format(key=self.key))
|
||||
)
|
||||
return message.extract_plain_text()
|
||||
|
||||
|
||||
class ExceptionParam(Param):
|
||||
|
@@ -1,16 +1,15 @@
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import ClassVar, NoReturn, Optional, Union
|
||||
import asyncio
|
||||
from typing_extensions import Self
|
||||
|
||||
import anyio
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import Union, ClassVar, NoReturn, Optional
|
||||
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.utils import run_coro_with_catch
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.typing import T_DependencyCache, T_PermissionChecker
|
||||
from nonebot.utils import run_coro_with_catch
|
||||
|
||||
from .adapter import Bot, Event
|
||||
from .params import BotParam, DefaultParam, DependParam, EventParam, Param
|
||||
from .params import Param, BotParam, EventParam, DependParam, DefaultParam
|
||||
|
||||
|
||||
class Permission:
|
||||
@@ -71,26 +70,22 @@ class Permission:
|
||||
"""
|
||||
if not self.checkers:
|
||||
return True
|
||||
|
||||
result = False
|
||||
|
||||
async def _run_checker(checker: Dependent[bool]) -> None:
|
||||
nonlocal result
|
||||
# calculate the result first to avoid data racing
|
||||
is_passed = await run_coro_with_catch(
|
||||
checker(
|
||||
bot=bot, event=event, stack=stack, dependency_cache=dependency_cache
|
||||
),
|
||||
(SkippedException,),
|
||||
False,
|
||||
)
|
||||
result |= is_passed
|
||||
|
||||
async with anyio.create_task_group() as tg:
|
||||
for checker in self.checkers:
|
||||
tg.start_soon(_run_checker, checker)
|
||||
|
||||
return result
|
||||
results = await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
checker(
|
||||
bot=bot,
|
||||
event=event,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
False,
|
||||
)
|
||||
for checker in self.checkers
|
||||
),
|
||||
)
|
||||
return any(results)
|
||||
|
||||
def __and__(self, other: object) -> NoReturn:
|
||||
raise RuntimeError("And operation between Permissions is not allowed.")
|
||||
@@ -124,7 +119,7 @@ class User:
|
||||
perm: 需同时满足的权限
|
||||
"""
|
||||
|
||||
__slots__ = ("perm", "users")
|
||||
__slots__ = ("users", "perm")
|
||||
|
||||
def __init__(
|
||||
self, users: tuple[str, ...], perm: Optional[Permission] = None
|
||||
|
@@ -1,15 +1,13 @@
|
||||
import asyncio
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import ClassVar, NoReturn, Optional, Union
|
||||
|
||||
import anyio
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
from typing import Union, ClassVar, NoReturn, Optional
|
||||
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.typing import T_DependencyCache, T_RuleChecker, T_State
|
||||
from nonebot.typing import T_State, T_RuleChecker, T_DependencyCache
|
||||
|
||||
from .adapter import Bot, Event
|
||||
from .params import BotParam, DefaultParam, DependParam, EventParam, Param, StateParam
|
||||
from .params import Param, BotParam, EventParam, StateParam, DependParam, DefaultParam
|
||||
|
||||
|
||||
class Rule:
|
||||
@@ -73,33 +71,22 @@ class Rule:
|
||||
"""
|
||||
if not self.checkers:
|
||||
return True
|
||||
|
||||
result = True
|
||||
|
||||
def _handle_skipped_exception(
|
||||
exc_group: BaseExceptionGroup[SkippedException],
|
||||
) -> None:
|
||||
nonlocal result
|
||||
result = False
|
||||
|
||||
async def _run_checker(checker: Dependent[bool]) -> None:
|
||||
nonlocal result
|
||||
# calculate the result first to avoid data racing
|
||||
is_passed = await checker(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
try:
|
||||
results = await asyncio.gather(
|
||||
*(
|
||||
checker(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
for checker in self.checkers
|
||||
)
|
||||
)
|
||||
result &= is_passed
|
||||
|
||||
with catch({SkippedException: _handle_skipped_exception}):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for checker in self.checkers:
|
||||
tg.start_soon(_run_checker, checker)
|
||||
|
||||
return result
|
||||
except SkippedException:
|
||||
return False
|
||||
return all(results)
|
||||
|
||||
def __and__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
|
||||
if other is None:
|
||||
|
@@ -8,15 +8,13 @@ NoneBot 使用 [`loguru`][loguru] 来记录日志信息。
|
||||
[loguru]: https://github.com/Delgan/loguru
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 7
|
||||
description: nonebot.log 模块
|
||||
"""
|
||||
|
||||
import sys
|
||||
import inspect
|
||||
import logging
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import loguru
|
||||
|
@@ -1,22 +1,20 @@
|
||||
"""本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 3
|
||||
description: nonebot.matcher 模块
|
||||
"""
|
||||
|
||||
from nonebot.internal.matcher import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
||||
from nonebot.internal.matcher import Matcher as Matcher
|
||||
from nonebot.internal.matcher import matchers as matchers
|
||||
from nonebot.internal.matcher import current_bot as current_bot
|
||||
from nonebot.internal.matcher import MatcherSource as MatcherSource
|
||||
from nonebot.internal.matcher import current_event as current_event
|
||||
from nonebot.internal.matcher import MatcherManager as MatcherManager
|
||||
from nonebot.internal.matcher import MatcherProvider as MatcherProvider
|
||||
from nonebot.internal.matcher import MatcherSource as MatcherSource
|
||||
from nonebot.internal.matcher import current_bot as current_bot
|
||||
from nonebot.internal.matcher import current_event as current_event
|
||||
from nonebot.internal.matcher import current_handler as current_handler
|
||||
from nonebot.internal.matcher import current_matcher as current_matcher
|
||||
from nonebot.internal.matcher import matchers as matchers
|
||||
from nonebot.internal.matcher import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
||||
|
||||
__autodoc__ = {
|
||||
"Matcher": True,
|
||||
|
@@ -3,53 +3,44 @@
|
||||
NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 2
|
||||
description: nonebot.message 模块
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from contextlib import AsyncExitStack
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional
|
||||
|
||||
import anyio
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.rule import TrieRule
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
from nonebot.utils import escape_tag, run_coro_with_catch
|
||||
from nonebot.exception import (
|
||||
IgnoredException,
|
||||
NoLogException,
|
||||
SkippedException,
|
||||
StopPropagation,
|
||||
IgnoredException,
|
||||
SkippedException,
|
||||
)
|
||||
from nonebot.typing import (
|
||||
T_State,
|
||||
T_DependencyCache,
|
||||
T_RunPreProcessor,
|
||||
T_RunPostProcessor,
|
||||
T_EventPreProcessor,
|
||||
T_EventPostProcessor,
|
||||
)
|
||||
from nonebot.internal.params import (
|
||||
ArgParam,
|
||||
BotParam,
|
||||
DefaultParam,
|
||||
DependParam,
|
||||
EventParam,
|
||||
ExceptionParam,
|
||||
MatcherParam,
|
||||
StateParam,
|
||||
)
|
||||
from nonebot.log import logger
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
from nonebot.rule import TrieRule
|
||||
from nonebot.typing import (
|
||||
T_DependencyCache,
|
||||
T_EventPostProcessor,
|
||||
T_EventPreProcessor,
|
||||
T_RunPostProcessor,
|
||||
T_RunPreProcessor,
|
||||
T_State,
|
||||
)
|
||||
from nonebot.utils import (
|
||||
escape_tag,
|
||||
flatten_exception_group,
|
||||
run_coro_with_catch,
|
||||
run_coro_with_shield,
|
||||
DependParam,
|
||||
DefaultParam,
|
||||
MatcherParam,
|
||||
ExceptionParam,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -132,21 +123,6 @@ def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
|
||||
return func
|
||||
|
||||
|
||||
def _handle_ignored_exception(msg: str) -> Callable[[BaseExceptionGroup], None]:
|
||||
def _handle(exc_group: BaseExceptionGroup[IgnoredException]) -> None:
|
||||
logger.opt(colors=True).info(msg)
|
||||
|
||||
return _handle
|
||||
|
||||
|
||||
def _handle_exception(msg: str) -> Callable[[BaseExceptionGroup], None]:
|
||||
def _handle(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(msg)
|
||||
|
||||
return _handle
|
||||
|
||||
|
||||
async def _apply_event_preprocessors(
|
||||
bot: "Bot",
|
||||
event: "Event",
|
||||
@@ -174,21 +150,10 @@ async def _apply_event_preprocessors(
|
||||
if show_log:
|
||||
logger.debug("Running PreProcessors...")
|
||||
|
||||
with catch(
|
||||
{
|
||||
IgnoredException: _handle_ignored_exception(
|
||||
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
|
||||
),
|
||||
Exception: _handle_exception(
|
||||
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
|
||||
"Event ignored!</bg #f8bbd0></r>"
|
||||
),
|
||||
}
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for proc in _event_preprocessors:
|
||||
tg.start_soon(
|
||||
run_coro_with_catch,
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
bot=bot,
|
||||
event=event,
|
||||
@@ -198,10 +163,22 @@ async def _apply_event_preprocessors(
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
for proc in _event_preprocessors
|
||||
)
|
||||
)
|
||||
except IgnoredException:
|
||||
logger.opt(colors=True).info(
|
||||
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
|
||||
"Event ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def _apply_event_postprocessors(
|
||||
@@ -228,17 +205,10 @@ async def _apply_event_postprocessors(
|
||||
if show_log:
|
||||
logger.debug("Running PostProcessors...")
|
||||
|
||||
with catch(
|
||||
{
|
||||
Exception: _handle_exception(
|
||||
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
|
||||
)
|
||||
}
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for proc in _event_postprocessors:
|
||||
tg.start_soon(
|
||||
run_coro_with_catch,
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
bot=bot,
|
||||
event=event,
|
||||
@@ -248,6 +218,13 @@ async def _apply_event_postprocessors(
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
for proc in _event_postprocessors
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
|
||||
async def _apply_run_preprocessors(
|
||||
@@ -275,38 +252,35 @@ async def _apply_run_preprocessors(
|
||||
return True
|
||||
|
||||
# ensure matcher function can be correctly called
|
||||
with (
|
||||
matcher.ensure_context(bot, event),
|
||||
catch(
|
||||
{
|
||||
IgnoredException: _handle_ignored_exception(
|
||||
f"{matcher} running is <b>cancelled</b>"
|
||||
),
|
||||
Exception: _handle_exception(
|
||||
"<r><bg #f8bbd0>Error when running RunPreProcessors. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
),
|
||||
}
|
||||
),
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for proc in _run_preprocessors:
|
||||
tg.start_soon(
|
||||
run_coro_with_catch,
|
||||
proc(
|
||||
matcher=matcher,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
with matcher.ensure_context(bot, event):
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
matcher=matcher,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
for proc in _run_preprocessors
|
||||
)
|
||||
)
|
||||
except IgnoredException:
|
||||
logger.opt(colors=True).info(f"{matcher} running is <b>cancelled</b>")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPreProcessors. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def _apply_run_postprocessors(
|
||||
@@ -330,32 +304,29 @@ async def _apply_run_postprocessors(
|
||||
if not _run_postprocessors:
|
||||
return
|
||||
|
||||
with (
|
||||
matcher.ensure_context(bot, event),
|
||||
catch(
|
||||
{
|
||||
Exception: _handle_exception(
|
||||
"<r><bg #f8bbd0>Error when running RunPostProcessors"
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
}
|
||||
),
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for proc in _run_postprocessors:
|
||||
tg.start_soon(
|
||||
run_coro_with_catch,
|
||||
proc(
|
||||
matcher=matcher,
|
||||
exception=exception,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=matcher.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
with matcher.ensure_context(bot, event):
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
matcher=matcher,
|
||||
exception=exception,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=matcher.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
for proc in _run_postprocessors
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPostProcessors</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
|
||||
async def _check_matcher(
|
||||
@@ -452,9 +423,8 @@ async def _run_matcher(
|
||||
|
||||
exception = None
|
||||
|
||||
logger.debug(f"Running {matcher}")
|
||||
|
||||
try:
|
||||
logger.debug(f"Running {matcher}")
|
||||
await matcher.run(bot, event, state, stack, dependency_cache)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
@@ -522,7 +492,8 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
|
||||
用法:
|
||||
```python
|
||||
driver.task_group.start_soon(handle_event, bot, event)
|
||||
import asyncio
|
||||
asyncio.create_task(handle_event(bot, event))
|
||||
```
|
||||
"""
|
||||
show_log = True
|
||||
@@ -557,13 +528,6 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
)
|
||||
|
||||
break_flag = False
|
||||
|
||||
def _handle_stop_propagation(exc_group: BaseExceptionGroup) -> None:
|
||||
nonlocal break_flag
|
||||
|
||||
break_flag = True
|
||||
logger.debug("Stop event propagation")
|
||||
|
||||
# iterate through all priority until stop propagation
|
||||
for priority in sorted(matchers.keys()):
|
||||
if break_flag:
|
||||
@@ -572,30 +536,23 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
if show_log:
|
||||
logger.debug(f"Checking for matchers in priority {priority}...")
|
||||
|
||||
if not (priority_matchers := matchers[priority]):
|
||||
continue
|
||||
|
||||
with catch(
|
||||
{
|
||||
StopPropagation: _handle_stop_propagation,
|
||||
Exception: _handle_exception(
|
||||
pending_tasks = [
|
||||
check_and_run_matcher(
|
||||
matcher, bot, event, state.copy(), stack, dependency_cache
|
||||
)
|
||||
for matcher in matchers[priority]
|
||||
]
|
||||
results = await asyncio.gather(*pending_tasks, return_exceptions=True)
|
||||
for result in results:
|
||||
if not isinstance(result, Exception):
|
||||
continue
|
||||
if isinstance(result, StopPropagation):
|
||||
break_flag = True
|
||||
logger.debug("Stop event propagation")
|
||||
else:
|
||||
logger.opt(colors=True, exception=result).error(
|
||||
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
|
||||
),
|
||||
}
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for matcher in priority_matchers:
|
||||
tg.start_soon(
|
||||
run_coro_with_shield,
|
||||
check_and_run_matcher(
|
||||
matcher,
|
||||
bot,
|
||||
event,
|
||||
state.copy(),
|
||||
stack,
|
||||
dependency_cache,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if show_log:
|
||||
logger.debug("Checking for matchers completed")
|
||||
|
@@ -1,49 +1,43 @@
|
||||
"""本模块定义了依赖注入的各类参数。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 4
|
||||
description: nonebot.params 模块
|
||||
"""
|
||||
|
||||
from re import Match
|
||||
from typing import Any, Callable, Literal, Optional, Union, overload
|
||||
from typing import Any, Union, Literal, Callable, Optional, overload
|
||||
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.internal.params import Arg as Arg
|
||||
from nonebot.internal.params import ArgStr as ArgStr
|
||||
from nonebot.internal.params import Depends as Depends
|
||||
from nonebot.internal.params import ArgParam as ArgParam
|
||||
from nonebot.internal.params import BotParam as BotParam
|
||||
from nonebot.adapters import Event, Message, MessageSegment
|
||||
from nonebot.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_ARG_KEY,
|
||||
CMD_KEY,
|
||||
CMD_START_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
ENDSWITH_KEY,
|
||||
FULLMATCH_KEY,
|
||||
KEYWORD_KEY,
|
||||
PAUSE_PROMPT_RESULT_KEY,
|
||||
PREFIX_KEY,
|
||||
RAW_CMD_KEY,
|
||||
RECEIVE_KEY,
|
||||
REGEX_MATCHED,
|
||||
REJECT_PROMPT_RESULT_KEY,
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
RAW_CMD_KEY,
|
||||
ENDSWITH_KEY,
|
||||
CMD_START_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
)
|
||||
from nonebot.internal.params import Arg as Arg
|
||||
from nonebot.internal.params import ArgParam as ArgParam
|
||||
from nonebot.internal.params import ArgPlainText as ArgPlainText
|
||||
from nonebot.internal.params import ArgPromptResult as ArgPromptResult
|
||||
from nonebot.internal.params import ArgStr as ArgStr
|
||||
from nonebot.internal.params import BotParam as BotParam
|
||||
from nonebot.internal.params import DefaultParam as DefaultParam
|
||||
from nonebot.internal.params import DependParam as DependParam
|
||||
from nonebot.internal.params import Depends as Depends
|
||||
from nonebot.internal.params import EventParam as EventParam
|
||||
from nonebot.internal.params import ExceptionParam as ExceptionParam
|
||||
from nonebot.internal.params import MatcherParam as MatcherParam
|
||||
from nonebot.internal.params import StateParam as StateParam
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.typing import T_State
|
||||
|
||||
|
||||
async def _event_type(event: Event) -> str:
|
||||
@@ -155,7 +149,7 @@ def RegexMatched() -> Match[str]:
|
||||
|
||||
|
||||
def _regex_str(
|
||||
groups: tuple[Union[str, int], ...],
|
||||
groups: tuple[Union[str, int], ...]
|
||||
) -> Callable[[T_State], Union[str, tuple[Union[str, Any], ...], Any]]:
|
||||
def _regex_str_dependency(
|
||||
state: T_State,
|
||||
@@ -166,16 +160,16 @@ def _regex_str(
|
||||
|
||||
|
||||
@overload
|
||||
def RegexStr(group: Literal[0] = 0, /) -> str: ...
|
||||
def RegexStr(__group: Literal[0] = 0) -> str: ...
|
||||
|
||||
|
||||
@overload
|
||||
def RegexStr(group: Union[str, int], /) -> Union[str, Any]: ...
|
||||
def RegexStr(__group: Union[str, int]) -> Union[str, Any]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def RegexStr(
|
||||
group1: Union[str, int], group2: Union[str, int], /, *groups: Union[str, int]
|
||||
__group1: Union[str, int], __group2: Union[str, int], *groups: Union[str, int]
|
||||
) -> tuple[Union[str, Any], ...]: ...
|
||||
|
||||
|
||||
@@ -256,26 +250,6 @@ def LastReceived(default: Any = None) -> Any:
|
||||
return Depends(_last_received, use_cache=False)
|
||||
|
||||
|
||||
def ReceivePromptResult(id: Optional[str] = None) -> Any:
|
||||
"""`receive` prompt 发送结果"""
|
||||
|
||||
def _receive_prompt_result(matcher: "Matcher") -> Any:
|
||||
return matcher.state.get(
|
||||
REJECT_PROMPT_RESULT_KEY.format(key=RECEIVE_KEY.format(id=id))
|
||||
)
|
||||
|
||||
return Depends(_receive_prompt_result, use_cache=False)
|
||||
|
||||
|
||||
def PausePromptResult() -> Any:
|
||||
"""`pause` prompt 发送结果"""
|
||||
|
||||
def _pause_prompt_result(matcher: "Matcher") -> Any:
|
||||
return matcher.state.get(PAUSE_PROMPT_RESULT_KEY)
|
||||
|
||||
return Depends(_pause_prompt_result, use_cache=False)
|
||||
|
||||
|
||||
__autodoc__ = {
|
||||
"Arg": True,
|
||||
"ArgStr": True,
|
||||
@@ -289,5 +263,4 @@ __autodoc__ = {
|
||||
"DefaultParam": True,
|
||||
"MatcherParam": True,
|
||||
"ExceptionParam": True,
|
||||
"ArgPromptResult": True,
|
||||
}
|
||||
|
@@ -5,17 +5,15 @@
|
||||
只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 6
|
||||
description: nonebot.permission 模块
|
||||
"""
|
||||
|
||||
from nonebot.params import EventType
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.internal.permission import USER as USER
|
||||
from nonebot.internal.permission import Permission as Permission
|
||||
from nonebot.internal.permission import User as User
|
||||
from nonebot.params import EventType
|
||||
from nonebot.internal.permission import Permission as Permission
|
||||
|
||||
|
||||
class Message:
|
||||
|
@@ -32,16 +32,14 @@
|
||||
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.model.PluginMetadata>`
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 0
|
||||
description: nonebot.plugin 模块
|
||||
"""
|
||||
|
||||
from contextvars import ContextVar
|
||||
from itertools import chain
|
||||
from types import ModuleType
|
||||
from typing import Optional, TypeVar
|
||||
from contextvars import ContextVar
|
||||
from typing import TypeVar, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -175,30 +173,30 @@ def get_plugin_config(config: type[C]) -> C:
|
||||
return type_validate_python(config, model_dump(get_driver().config))
|
||||
|
||||
|
||||
from .load import inherit_supported_adapters as inherit_supported_adapters
|
||||
from .on import on as on
|
||||
from .manager import PluginManager
|
||||
from .on import on_type as on_type
|
||||
from .model import Plugin as Plugin
|
||||
from .load import require as require
|
||||
from .on import on_regex as on_regex
|
||||
from .on import on_notice as on_notice
|
||||
from .on import on_command as on_command
|
||||
from .on import on_keyword as on_keyword
|
||||
from .on import on_message as on_message
|
||||
from .on import on_request as on_request
|
||||
from .on import on_endswith as on_endswith
|
||||
from .load import load_plugin as load_plugin
|
||||
from .on import CommandGroup as CommandGroup
|
||||
from .on import MatcherGroup as MatcherGroup
|
||||
from .on import on_fullmatch as on_fullmatch
|
||||
from .on import on_metaevent as on_metaevent
|
||||
from .load import load_plugins as load_plugins
|
||||
from .on import on_startswith as on_startswith
|
||||
from .load import load_from_json as load_from_json
|
||||
from .load import load_from_toml as load_from_toml
|
||||
from .model import PluginMetadata as PluginMetadata
|
||||
from .on import on_shell_command as on_shell_command
|
||||
from .load import load_all_plugins as load_all_plugins
|
||||
from .load import load_builtin_plugin as load_builtin_plugin
|
||||
from .load import load_builtin_plugins as load_builtin_plugins
|
||||
from .load import load_from_json as load_from_json
|
||||
from .load import load_from_toml as load_from_toml
|
||||
from .load import load_plugin as load_plugin
|
||||
from .load import load_plugins as load_plugins
|
||||
from .load import require as require
|
||||
from .manager import PluginManager
|
||||
from .model import Plugin as Plugin
|
||||
from .model import PluginMetadata as PluginMetadata
|
||||
from .on import CommandGroup as CommandGroup
|
||||
from .on import MatcherGroup as MatcherGroup
|
||||
from .on import on as on
|
||||
from .on import on_command as on_command
|
||||
from .on import on_endswith as on_endswith
|
||||
from .on import on_fullmatch as on_fullmatch
|
||||
from .on import on_keyword as on_keyword
|
||||
from .on import on_message as on_message
|
||||
from .on import on_metaevent as on_metaevent
|
||||
from .on import on_notice as on_notice
|
||||
from .on import on_regex as on_regex
|
||||
from .on import on_request as on_request
|
||||
from .on import on_shell_command as on_shell_command
|
||||
from .on import on_startswith as on_startswith
|
||||
from .on import on_type as on_type
|
||||
from .load import inherit_supported_adapters as inherit_supported_adapters
|
||||
|
@@ -1,28 +1,26 @@
|
||||
"""本模块定义插件加载接口。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 1
|
||||
description: nonebot.plugin.load 模块
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Optional, Union
|
||||
from typing import Union, Optional
|
||||
from collections.abc import Iterable
|
||||
|
||||
from nonebot.utils import path_to_module_name
|
||||
|
||||
from . import _managers, _module_name_to_plugin_id, get_plugin
|
||||
from .manager import PluginManager
|
||||
from .model import Plugin
|
||||
from .manager import PluginManager
|
||||
from . import _managers, get_plugin, _module_name_to_plugin_id
|
||||
|
||||
try: # pragma: py-gte-311
|
||||
import tomllib # pyright: ignore[reportMissingImports]
|
||||
except ModuleNotFoundError: # pragma: py-lt-311
|
||||
import tomli as tomllib # pyright: ignore[reportMissingImports]
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
|
||||
|
@@ -3,34 +3,32 @@
|
||||
参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/)
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 5
|
||||
description: nonebot.plugin.manager 模块
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable, Sequence
|
||||
import importlib
|
||||
from importlib.abc import MetaPathFinder
|
||||
from importlib.machinery import PathFinder, SourceFileLoader
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
import pkgutil
|
||||
import sys
|
||||
from types import ModuleType
|
||||
import pkgutil
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
from itertools import chain
|
||||
from typing import Optional
|
||||
from types import ModuleType
|
||||
from importlib.abc import MetaPathFinder
|
||||
from collections.abc import Iterable, Sequence
|
||||
from importlib.machinery import PathFinder, SourceFileLoader
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import escape_tag, path_to_module_name
|
||||
|
||||
from .model import Plugin, PluginMetadata
|
||||
from . import (
|
||||
_current_plugin,
|
||||
_managers,
|
||||
_module_name_to_plugin_id,
|
||||
_new_plugin,
|
||||
_revert_plugin,
|
||||
_current_plugin,
|
||||
_module_name_to_plugin_id,
|
||||
)
|
||||
from .model import Plugin, PluginMetadata
|
||||
|
||||
|
||||
class PluginManager:
|
||||
|
@@ -1,16 +1,14 @@
|
||||
"""本模块定义插件相关信息。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 3
|
||||
description: nonebot.plugin.model 模块
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
from dataclasses import dataclass, field
|
||||
from types import ModuleType
|
||||
from typing import TYPE_CHECKING, Any, Optional, Type # noqa: UP035
|
||||
from dataclasses import field, dataclass
|
||||
from typing import TYPE_CHECKING, Any, Type, Optional # noqa: UP035
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
@@ -1,40 +1,38 @@
|
||||
"""本模块定义事件响应器便携定义函数。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 2
|
||||
description: nonebot.plugin.on 模块
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import inspect
|
||||
import re
|
||||
from types import ModuleType
|
||||
from typing import Any, Optional, Union
|
||||
import inspect
|
||||
import warnings
|
||||
from types import ModuleType
|
||||
from typing import Any, Union, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.permission import Permission
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.matcher import Matcher, MatcherSource
|
||||
from nonebot.permission import Permission
|
||||
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
|
||||
from nonebot.rule import (
|
||||
ArgumentParser,
|
||||
Rule,
|
||||
ArgumentParser,
|
||||
regex,
|
||||
command,
|
||||
endswith,
|
||||
fullmatch,
|
||||
is_type,
|
||||
keyword,
|
||||
regex,
|
||||
shell_command,
|
||||
endswith,
|
||||
fullmatch,
|
||||
startswith,
|
||||
shell_command,
|
||||
)
|
||||
from nonebot.typing import T_Handler, T_PermissionChecker, T_RuleChecker, T_State
|
||||
|
||||
from . import get_plugin_by_module_name
|
||||
from .manager import _current_plugin
|
||||
from .model import Plugin
|
||||
from .manager import _current_plugin
|
||||
from . import get_plugin_by_module_name
|
||||
|
||||
|
||||
def store_matcher(matcher: type[Matcher]) -> None:
|
||||
|
@@ -1,14 +1,14 @@
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from types import ModuleType
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.matcher import Matcher, MatcherSource
|
||||
from nonebot.permission import Permission
|
||||
from nonebot.rule import ArgumentParser, Rule
|
||||
from nonebot.typing import T_Handler, T_PermissionChecker, T_RuleChecker, T_State
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.rule import Rule, ArgumentParser
|
||||
from nonebot.matcher import Matcher, MatcherSource
|
||||
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
|
||||
|
||||
from .model import Plugin
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
from nonebot import on_command
|
||||
from nonebot.rule import to_me
|
||||
from nonebot.adapters import Message
|
||||
from nonebot.params import CommandArg
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="echo",
|
||||
|
@@ -1,9 +1,9 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.message import IgnoredException, event_preprocessor
|
||||
from nonebot.params import Depends
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.message import IgnoredException, event_preprocessor
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="唯一会话",
|
||||
|
@@ -5,29 +5,28 @@
|
||||
只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 5
|
||||
description: nonebot.rule 模块
|
||||
"""
|
||||
|
||||
from argparse import Action, ArgumentError
|
||||
from argparse import ArgumentParser as ArgParser
|
||||
from argparse import Namespace as Namespace
|
||||
from collections.abc import Sequence
|
||||
from contextvars import ContextVar
|
||||
from gettext import gettext
|
||||
from itertools import chain, product
|
||||
import re
|
||||
import shlex
|
||||
from argparse import Action
|
||||
from gettext import gettext
|
||||
from argparse import ArgumentError
|
||||
from contextvars import ContextVar
|
||||
from collections.abc import Sequence
|
||||
from itertools import chain, product
|
||||
from argparse import Namespace as Namespace
|
||||
from argparse import ArgumentParser as ArgParser
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
NamedTuple,
|
||||
Union,
|
||||
TypeVar,
|
||||
Optional,
|
||||
TypedDict,
|
||||
TypeVar,
|
||||
Union,
|
||||
NamedTuple,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
@@ -35,27 +34,27 @@ from typing import (
|
||||
from pygtrie import CharTrie
|
||||
|
||||
from nonebot import get_driver
|
||||
from nonebot.adapters import Bot, Event, Message, MessageSegment
|
||||
from nonebot.consts import (
|
||||
CMD_ARG_KEY,
|
||||
CMD_KEY,
|
||||
CMD_START_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
ENDSWITH_KEY,
|
||||
FULLMATCH_KEY,
|
||||
KEYWORD_KEY,
|
||||
PREFIX_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_MATCHED,
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
STARTSWITH_KEY,
|
||||
)
|
||||
from nonebot.log import logger
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.exception import ParserExit
|
||||
from nonebot.internal.rule import Rule as Rule
|
||||
from nonebot.log import logger
|
||||
from nonebot.params import Command, CommandArg, CommandWhitespace, EventToMe
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.adapters import Bot, Event, Message, MessageSegment
|
||||
from nonebot.params import Command, EventToMe, CommandArg, CommandWhitespace
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
PREFIX_KEY,
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
RAW_CMD_KEY,
|
||||
ENDSWITH_KEY,
|
||||
CMD_START_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -145,7 +144,7 @@ class StartswithRule:
|
||||
ignorecase: 是否忽略大小写
|
||||
"""
|
||||
|
||||
__slots__ = ("ignorecase", "msg")
|
||||
__slots__ = ("msg", "ignorecase")
|
||||
|
||||
def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):
|
||||
self.msg = msg
|
||||
@@ -200,7 +199,7 @@ class EndswithRule:
|
||||
ignorecase: 是否忽略大小写
|
||||
"""
|
||||
|
||||
__slots__ = ("ignorecase", "msg")
|
||||
__slots__ = ("msg", "ignorecase")
|
||||
|
||||
def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):
|
||||
self.msg = msg
|
||||
@@ -255,7 +254,7 @@ class FullmatchRule:
|
||||
ignorecase: 是否忽略大小写
|
||||
"""
|
||||
|
||||
__slots__ = ("ignorecase", "msg")
|
||||
__slots__ = ("msg", "ignorecase")
|
||||
|
||||
def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):
|
||||
self.msg = tuple(map(str.casefold, msg) if ignorecase else msg)
|
||||
@@ -509,7 +508,7 @@ class ArgumentParser(ArgParser):
|
||||
super()._parse_optional(arg_string) if isinstance(arg_string, str) else None
|
||||
)
|
||||
|
||||
def _print_message(self, message: str, file: Optional[IO[str]] = None): # type: ignore
|
||||
def _print_message(self, message: str, file: Optional[IO[str]] = None):
|
||||
if (msg := parser_message.get(None)) is not None:
|
||||
parser_message.set(msg + message)
|
||||
else:
|
||||
@@ -557,22 +556,12 @@ class ShellCommandRule:
|
||||
if cmd not in self.cmds or msg is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
state[SHELL_ARGV] = list(
|
||||
chain.from_iterable(
|
||||
shlex.split(str(seg))
|
||||
if cast(MessageSegment, seg).is_text()
|
||||
else (seg,)
|
||||
for seg in msg
|
||||
)
|
||||
state[SHELL_ARGV] = list(
|
||||
chain.from_iterable(
|
||||
shlex.split(str(seg)) if cast(MessageSegment, seg).is_text() else (seg,)
|
||||
for seg in msg
|
||||
)
|
||||
except Exception as e:
|
||||
# set SHELL_ARGV to none indicating shlex error
|
||||
state[SHELL_ARGV] = None
|
||||
# ensure SHELL_ARGS is set to ParserExit if parser is provided
|
||||
if self.parser:
|
||||
state[SHELL_ARGS] = ParserExit(status=2, message=str(e))
|
||||
return True
|
||||
)
|
||||
|
||||
if self.parser:
|
||||
t = parser_message.set("")
|
||||
@@ -664,7 +653,7 @@ class RegexRule:
|
||||
flags: 正则表达式标记
|
||||
"""
|
||||
|
||||
__slots__ = ("flags", "regex")
|
||||
__slots__ = ("regex", "flags")
|
||||
|
||||
def __init__(self, regex: str, flags: int = 0):
|
||||
self.regex = regex
|
||||
|
@@ -6,23 +6,22 @@
|
||||
[`typing`](https://docs.python.org/3/library/typing.html)。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 11
|
||||
description: nonebot.typing 模块
|
||||
"""
|
||||
|
||||
import sys
|
||||
import types
|
||||
import typing as t
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
import typing_extensions as t_ext
|
||||
from typing_extensions import ParamSpec, TypeAlias, get_args, get_origin, override
|
||||
import warnings
|
||||
import typing as t
|
||||
import typing_extensions as t_ext
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
from typing_extensions import ParamSpec, TypeAlias, get_args, override, get_origin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from asyncio import Task
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.internal.params import DependencyCache
|
||||
from nonebot.permission import Permission
|
||||
|
||||
T = TypeVar("T")
|
||||
@@ -257,5 +256,5 @@ T_PermissionUpdater: TypeAlias = _DependentCallable["Permission"]
|
||||
- MatcherParam: Matcher 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_DependencyCache: TypeAlias = dict[_DependentCallable[t.Any], "DependencyCache"]
|
||||
T_DependencyCache: TypeAlias = dict[_DependentCallable[t.Any], "Task[t.Any]"]
|
||||
"""依赖缓存, 用于存储依赖函数的返回值"""
|
||||
|
@@ -1,38 +1,35 @@
|
||||
"""本模块包含了 NoneBot 的一些工具函数
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 8
|
||||
description: nonebot.utils 模块
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
from collections.abc import AsyncGenerator, Coroutine, Generator, Mapping, Sequence
|
||||
import contextlib
|
||||
from contextlib import AbstractContextManager, asynccontextmanager
|
||||
import dataclasses
|
||||
from functools import partial, wraps
|
||||
import importlib
|
||||
import inspect
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any, Callable, Generic, Optional, TypeVar, Union, overload
|
||||
from typing_extensions import ParamSpec, get_args, get_origin, override
|
||||
import json
|
||||
import asyncio
|
||||
import inspect
|
||||
import importlib
|
||||
import contextlib
|
||||
import dataclasses
|
||||
from pathlib import Path
|
||||
from collections import deque
|
||||
from contextvars import copy_context
|
||||
from functools import wraps, partial
|
||||
from contextlib import AbstractContextManager, asynccontextmanager
|
||||
from typing_extensions import ParamSpec, get_args, override, get_origin
|
||||
from collections.abc import Mapping, Sequence, Coroutine, AsyncGenerator
|
||||
from typing import Any, Union, Generic, TypeVar, Callable, Optional, overload
|
||||
|
||||
import anyio
|
||||
import anyio.to_thread
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
from pydantic import BaseModel
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.typing import (
|
||||
all_literal_values,
|
||||
is_none_type,
|
||||
origin_is_literal,
|
||||
origin_is_union,
|
||||
type_has_args,
|
||||
origin_is_union,
|
||||
origin_is_literal,
|
||||
all_literal_values,
|
||||
)
|
||||
|
||||
P = ParamSpec("P")
|
||||
@@ -40,7 +37,6 @@ R = TypeVar("R")
|
||||
T = TypeVar("T")
|
||||
K = TypeVar("K")
|
||||
V = TypeVar("V")
|
||||
E = TypeVar("E", bound=BaseException)
|
||||
|
||||
|
||||
def escape_tag(s: str) -> str:
|
||||
@@ -180,9 +176,11 @@ def run_sync(call: Callable[P, R]) -> Callable[P, Coroutine[None, None, R]]:
|
||||
|
||||
@wraps(call)
|
||||
async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
return await anyio.to_thread.run_sync(
|
||||
partial(call, *args, **kwargs), abandon_on_cancel=True
|
||||
)
|
||||
loop = asyncio.get_running_loop()
|
||||
pfunc = partial(call, *args, **kwargs)
|
||||
context = copy_context()
|
||||
result = await loop.run_in_executor(None, partial(context.run, pfunc))
|
||||
return result
|
||||
|
||||
return _wrapper
|
||||
|
||||
@@ -234,36 +232,10 @@ async def run_coro_with_catch(
|
||||
协程的返回值或发生异常时的指定值
|
||||
"""
|
||||
|
||||
with catch({exc: lambda exc_group: None}):
|
||||
try:
|
||||
return await coro
|
||||
|
||||
return return_on_err
|
||||
|
||||
|
||||
async def run_coro_with_shield(coro: Coroutine[Any, Any, T]) -> T:
|
||||
"""运行协程并在取消时屏蔽取消异常。
|
||||
|
||||
参数:
|
||||
coro: 要运行的协程
|
||||
|
||||
返回:
|
||||
协程的返回值
|
||||
"""
|
||||
|
||||
with anyio.CancelScope(shield=True):
|
||||
return await coro
|
||||
|
||||
raise RuntimeError("This should not happen")
|
||||
|
||||
|
||||
def flatten_exception_group(
|
||||
exc_group: BaseExceptionGroup[E],
|
||||
) -> Generator[E, None, None]:
|
||||
for exc in exc_group.exceptions:
|
||||
if isinstance(exc, BaseExceptionGroup):
|
||||
yield from flatten_exception_group(exc)
|
||||
else:
|
||||
yield exc
|
||||
except exc:
|
||||
return return_on_err
|
||||
|
||||
|
||||
def get_name(obj: Any) -> str:
|
||||
|
16
package.json
16
package.json
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "root",
|
||||
"private": true,
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"workspaces": [
|
||||
"website"
|
||||
],
|
||||
@@ -21,19 +20,20 @@
|
||||
"pyright": "pyright"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||
"@typescript-eslint/parser": "^6.6.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-regexp": "^1.15.0",
|
||||
"prettier": "^3.0.3",
|
||||
"pyright": "1.1.393",
|
||||
"pyright": "^1.1.317",
|
||||
"stylelint": "^15.10.3",
|
||||
"stylelint-config-standard": "^34.0.0",
|
||||
"stylelint-prettier": "^4.0.2"
|
||||
|
@@ -31,7 +31,8 @@ def init():
|
||||
if host in {"0.0.0.0", "127.0.0.1"}:
|
||||
host = "localhost"
|
||||
logger.opt(colors=True).info(
|
||||
f"Nonebot docs will be running at: <b><u>http://{host}:{port}/website/</u></b>"
|
||||
f"Nonebot docs will be running at: "
|
||||
f"<b><u>http://{host}:{port}/website/</u></b>"
|
||||
)
|
||||
|
||||
|
||||
|
3398
poetry.lock
generated
3398
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "nonebot2"
|
||||
version = "2.4.2"
|
||||
version = "2.3.2"
|
||||
description = "An asynchronous python bot framework."
|
||||
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||
license = "MIT"
|
||||
@@ -22,33 +22,33 @@ include = ["nonebot/py.typed"]
|
||||
[tool.poetry.urls]
|
||||
"Bug Tracker" = "https://github.com/nonebot/nonebot2/issues"
|
||||
"Changelog" = "https://nonebot.dev/changelog"
|
||||
"Funding" = "https://afdian.com/@nonebot"
|
||||
"Funding" = "https://afdian.net/@nonebot"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
yarl = "^1.7.2"
|
||||
anyio = "^4.4.0"
|
||||
pygtrie = "^2.4.1"
|
||||
exceptiongroup = "^1.2.2"
|
||||
loguru = ">=0.6.0,<1.0.0"
|
||||
python-dotenv = ">=0.21.0,<2.0.0"
|
||||
typing-extensions = ">=4.4.0,<5.0.0"
|
||||
pydantic = ">=1.10.0,<3.0.0,!=2.5.0,!=2.5.1"
|
||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||
pydantic = ">=1.10.0,<3.0.0,!=2.5.0,!=2.5.1,!=2.10.0,!=2.10.1"
|
||||
|
||||
websockets = { version = ">=10.0", optional = true }
|
||||
Quart = { version = ">=0.18.0,<1.0.0", optional = true }
|
||||
fastapi = { version = ">=0.93.0,<1.0.0", optional = true }
|
||||
aiohttp = { version = "^3.11.0", extras = ["speedups"], optional = true }
|
||||
httpx = { version = ">=0.26.0,<1.0.0", extras = ["http2"], optional = true }
|
||||
aiohttp = { version = "^3.9.0b0", extras = ["speedups"], optional = true }
|
||||
httpx = { version = ">=0.20.0,<1.0.0", extras = ["http2"], optional = true }
|
||||
uvicorn = { version = ">=0.20.0,<1.0.0", extras = [
|
||||
"standard",
|
||||
], optional = true }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.9.0"
|
||||
ruff = "^0.4.0"
|
||||
isort = "^5.10.1"
|
||||
black = "^24.0.0"
|
||||
nonemoji = "^0.1.2"
|
||||
pre-commit = "^4.0.0"
|
||||
pre-commit = "^3.0.0"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
nonebot-test = { path = "./envs/test/", develop = false }
|
||||
@@ -65,22 +65,35 @@ fastapi = ["fastapi", "uvicorn"]
|
||||
all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "strict"
|
||||
addopts = "--cov=nonebot --cov-report=term-missing"
|
||||
filterwarnings = ["error", "ignore::DeprecationWarning"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ["py39", "py310", "py311", "py312"]
|
||||
include = '\.pyi?$'
|
||||
extend-exclude = '''
|
||||
'''
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 88
|
||||
length_sort = true
|
||||
skip_gitignore = true
|
||||
force_sort_within_sections = true
|
||||
src_paths = ["nonebot", "tests"]
|
||||
extra_standard_library = ["typing_extensions"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py39"
|
||||
|
||||
[tool.ruff.format]
|
||||
line-ending = "lf"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"F", # Pyflakes
|
||||
"W", # pycodestyle warnings
|
||||
"E", # pycodestyle errors
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
"ASYNC", # flake8-async
|
||||
"C4", # flake8-comprehensions
|
||||
@@ -89,7 +102,6 @@ select = [
|
||||
"PYI", # flake8-pyi
|
||||
"PT", # flake8-pytest-style
|
||||
"Q", # flake8-quotes
|
||||
"TID", # flake8-tidy-imports
|
||||
"RUF", # Ruff-specific rules
|
||||
]
|
||||
ignore = [
|
||||
@@ -100,18 +112,10 @@ ignore = [
|
||||
"RUF003", # ambiguous-unicode-character-comment
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
force-sort-within-sections = true
|
||||
known-first-party = ["nonebot", "tests/*"]
|
||||
extra-standard-library = ["typing_extensions"]
|
||||
|
||||
[tool.ruff.lint.flake8-pytest-style]
|
||||
fixture-parentheses = false
|
||||
mark-parentheses = false
|
||||
|
||||
[tool.ruff.lint.pyupgrade]
|
||||
keep-runtime-typing = true
|
||||
|
||||
[tool.pyright]
|
||||
pythonVersion = "3.9"
|
||||
pythonPlatform = "All"
|
||||
|
@@ -4,4 +4,4 @@
|
||||
cd "$(dirname "$0")/../tests"
|
||||
|
||||
# Run the tests
|
||||
pytest -n auto --cov-append --cov-report xml --junitxml=./junit.xml $@
|
||||
pytest -n auto --cov-append --cov-report xml $@
|
||||
|
@@ -1,20 +1,18 @@
|
||||
from collections.abc import Generator
|
||||
from functools import wraps
|
||||
import os
|
||||
from pathlib import Path
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Callable, TypeVar
|
||||
from typing_extensions import ParamSpec
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import Generator
|
||||
|
||||
from nonebug import NONEBOT_INIT_KWARGS
|
||||
import pytest
|
||||
from nonebug import NONEBOT_INIT_KWARGS
|
||||
from werkzeug.serving import BaseWSGIServer, make_server
|
||||
|
||||
from fake_server import request_handler
|
||||
import nonebot
|
||||
from nonebot import _resolve_combine_expr
|
||||
from nonebot.config import Env
|
||||
from fake_server import request_handler
|
||||
from nonebot.drivers import URL, Driver
|
||||
from nonebot import _resolve_combine_expr
|
||||
|
||||
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
|
||||
os.environ["CONFIG_OVERRIDE"] = "new"
|
||||
@@ -22,9 +20,6 @@ os.environ["CONFIG_OVERRIDE"] = "new"
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
collect_ignore = ["plugins/", "dynamic/", "bad_plugins/"]
|
||||
|
||||
|
||||
@@ -43,36 +38,14 @@ def load_driver(request: pytest.FixtureRequest) -> Driver:
|
||||
return DriverClass(Env(environment=global_driver.env), global_driver.config)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", params=[pytest.param("asyncio"), pytest.param("trio")])
|
||||
def anyio_backend(request: pytest.FixtureRequest):
|
||||
return request.param
|
||||
|
||||
|
||||
def run_once(func: Callable[P, R]) -> Callable[P, R]:
|
||||
result = ...
|
||||
|
||||
@wraps(func)
|
||||
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
nonlocal result
|
||||
if result is not Ellipsis:
|
||||
return result
|
||||
|
||||
result = func(*args, **kwargs)
|
||||
return result
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
@run_once
|
||||
def load_plugin(anyio_backend, nonebug_init: None) -> set["Plugin"]:
|
||||
def load_plugin(nonebug_init: None) -> set["Plugin"]:
|
||||
# preload global plugins
|
||||
return nonebot.load_plugins(str(Path(__file__).parent / "plugins"))
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
@run_once
|
||||
def load_builtin_plugin(anyio_backend, nonebug_init: None) -> set["Plugin"]:
|
||||
def load_builtin_plugin(nonebug_init: None) -> set["Plugin"]:
|
||||
# preload builtin plugins
|
||||
return nonebot.load_builtin_plugins("echo", "single_session")
|
||||
|
||||
|
@@ -1,20 +1,15 @@
|
||||
import base64
|
||||
import json
|
||||
import base64
|
||||
import socket
|
||||
from typing import TypeVar, Union
|
||||
from typing import Union, TypeVar
|
||||
|
||||
from wsproto.events import Ping
|
||||
from werkzeug import Request, Response
|
||||
from werkzeug.datastructures import MultiDict
|
||||
from wsproto import ConnectionType, WSConnection
|
||||
from wsproto.events import (
|
||||
AcceptConnection,
|
||||
BytesMessage,
|
||||
CloseConnection,
|
||||
Ping,
|
||||
TextMessage,
|
||||
)
|
||||
from wsproto.events import Request as WSRequest
|
||||
from wsproto.frame_protocol import CloseReason
|
||||
from wsproto.events import Request as WSRequest
|
||||
from wsproto import WSConnection, ConnectionType
|
||||
from wsproto.events import TextMessage, BytesMessage, CloseConnection, AcceptConnection
|
||||
|
||||
K = TypeVar("K")
|
||||
V = TypeVar("V")
|
||||
|
@@ -1,7 +1,7 @@
|
||||
from nonebot import on_message
|
||||
from nonebot.adapters import Event, Message
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.params import ArgStr, EventMessage, LastReceived, Received
|
||||
from nonebot.adapters import Event, Message
|
||||
from nonebot.params import ArgStr, Received, EventMessage, LastReceived
|
||||
|
||||
test_handle = on_message()
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
from typing import Annotated, Any
|
||||
from typing import Annotated
|
||||
|
||||
from nonebot.adapters import Message
|
||||
from nonebot.params import Arg, ArgPlainText, ArgPromptResult, ArgStr
|
||||
from nonebot.params import Arg, ArgStr, ArgPlainText
|
||||
|
||||
|
||||
async def arg(key: Message = Arg()) -> Message:
|
||||
@@ -28,16 +28,12 @@ async def annotated_arg_plain_text(key: Annotated[str, ArgPlainText()]) -> str:
|
||||
return key
|
||||
|
||||
|
||||
async def annotated_arg_prompt_result(key: Annotated[Any, ArgPromptResult()]) -> Any:
|
||||
return key
|
||||
|
||||
|
||||
# test dependency priority
|
||||
async def annotated_prior_arg(key: Annotated[str, ArgStr("foo")] = ArgPlainText()):
|
||||
return key
|
||||
|
||||
|
||||
async def annotated_multi_arg(
|
||||
key: Annotated[Annotated[str, ArgStr("foo")], ArgPlainText()],
|
||||
key: Annotated[Annotated[str, ArgStr("foo")], ArgPlainText()]
|
||||
):
|
||||
return key
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from typing import TypeVar, Union
|
||||
from typing import Union, TypeVar
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Annotated
|
||||
from dataclasses import dataclass
|
||||
|
||||
import anyio
|
||||
from pydantic import Field
|
||||
|
||||
from nonebot import on_message
|
||||
@@ -33,8 +32,8 @@ async def gen_async():
|
||||
|
||||
@dataclass
|
||||
class ClassDependency:
|
||||
x: int = Depends(gen_sync)
|
||||
y: int = Depends(gen_async)
|
||||
x: int = Depends(gen_sync) # noqa: RUF009
|
||||
y: int = Depends(gen_async) # noqa: RUF009
|
||||
|
||||
|
||||
class FooBot(Bot): ...
|
||||
@@ -74,13 +73,13 @@ async def annotated_class_depend(c: Annotated[ClassDependency, Depends()]):
|
||||
|
||||
# test dependency priority
|
||||
async def annotated_prior_depend(
|
||||
x: Annotated[int, Depends(lambda: 2)] = Depends(dependency),
|
||||
x: Annotated[int, Depends(lambda: 2)] = Depends(dependency)
|
||||
):
|
||||
return x
|
||||
|
||||
|
||||
async def annotated_multi_depend(
|
||||
x: Annotated[Annotated[int, Depends(lambda: 2)], Depends(dependency)],
|
||||
x: Annotated[Annotated[int, Depends(lambda: 2)], Depends(dependency)]
|
||||
):
|
||||
return x
|
||||
|
||||
@@ -106,26 +105,3 @@ async def validate_field(x: int = Depends(lambda: "1", validate=Field(gt=0))):
|
||||
|
||||
async def validate_field_fail(x: int = Depends(lambda: "0", validate=Field(gt=0))):
|
||||
return x
|
||||
|
||||
|
||||
async def _dep():
|
||||
await anyio.sleep(1)
|
||||
return 1
|
||||
|
||||
|
||||
def _dep_mismatch():
|
||||
return 1
|
||||
|
||||
|
||||
async def cache_exception_func1(
|
||||
dep: int = Depends(_dep),
|
||||
mismatch: dict = Depends(_dep_mismatch),
|
||||
):
|
||||
raise RuntimeError("Never reach here")
|
||||
|
||||
|
||||
async def cache_exception_func2(
|
||||
dep: int = Depends(_dep),
|
||||
match: int = Depends(_dep_mismatch),
|
||||
):
|
||||
return dep
|
||||
|
@@ -1,7 +1,7 @@
|
||||
from typing import TypeVar, Union
|
||||
from typing import Union, TypeVar
|
||||
|
||||
from nonebot.adapters import Event, Message
|
||||
from nonebot.params import EventMessage, EventPlainText, EventToMe, EventType
|
||||
from nonebot.params import EventToMe, EventType, EventMessage, EventPlainText
|
||||
|
||||
|
||||
async def event(e: Event) -> Event:
|
||||
|
@@ -1,13 +1,8 @@
|
||||
from typing import Any, TypeVar, Union
|
||||
from typing import Union, TypeVar
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.params import (
|
||||
LastReceived,
|
||||
PausePromptResult,
|
||||
Received,
|
||||
ReceivePromptResult,
|
||||
)
|
||||
from nonebot.params import Received, LastReceived
|
||||
|
||||
|
||||
async def matcher(m: Matcher) -> Matcher:
|
||||
@@ -64,11 +59,3 @@ async def receive(e: Event = Received("test")) -> Event:
|
||||
|
||||
async def last_receive(e: Event = LastReceived()) -> Event:
|
||||
return e
|
||||
|
||||
|
||||
async def receive_prompt_result(result: Any = ReceivePromptResult("test")) -> Any:
|
||||
return result
|
||||
|
||||
|
||||
async def pause_prompt_result(result: Any = PausePromptResult()) -> Any:
|
||||
return result
|
||||
|
@@ -1,24 +1,24 @@
|
||||
from re import Match
|
||||
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.adapters import Message
|
||||
from nonebot.params import (
|
||||
Command,
|
||||
CommandArg,
|
||||
CommandStart,
|
||||
CommandWhitespace,
|
||||
Endswith,
|
||||
Fullmatch,
|
||||
Keyword,
|
||||
RawCommand,
|
||||
RegexDict,
|
||||
RegexGroup,
|
||||
RegexMatched,
|
||||
Endswith,
|
||||
RegexStr,
|
||||
Fullmatch,
|
||||
RegexDict,
|
||||
CommandArg,
|
||||
RawCommand,
|
||||
RegexGroup,
|
||||
Startswith,
|
||||
CommandStart,
|
||||
RegexMatched,
|
||||
ShellCommandArgs,
|
||||
ShellCommandArgv,
|
||||
Startswith,
|
||||
CommandWhitespace,
|
||||
)
|
||||
from nonebot.typing import T_State
|
||||
|
||||
|
||||
async def state(x: T_State) -> T_State:
|
||||
|
@@ -1,9 +1,9 @@
|
||||
from typing import Optional
|
||||
|
||||
from nonebot.adapters import Bot, Event, Message
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.params import Arg, Depends
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.adapters import Bot, Event, Message
|
||||
|
||||
|
||||
def dependency():
|
||||
|
@@ -1,24 +1,24 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot import (
|
||||
CommandGroup,
|
||||
MatcherGroup,
|
||||
on,
|
||||
on_type,
|
||||
on_regex,
|
||||
on_notice,
|
||||
on_command,
|
||||
on_endswith,
|
||||
on_fullmatch,
|
||||
on_keyword,
|
||||
on_message,
|
||||
on_metaevent,
|
||||
on_notice,
|
||||
on_regex,
|
||||
on_request,
|
||||
on_shell_command,
|
||||
on_endswith,
|
||||
on_fullmatch,
|
||||
on_metaevent,
|
||||
on_startswith,
|
||||
on_type,
|
||||
on_shell_command,
|
||||
)
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
|
||||
async def rule() -> bool:
|
||||
|
@@ -1,5 +0,0 @@
|
||||
[tool.ruff]
|
||||
extend = "../pyproject.toml"
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["nonebot", "fake_server", "utils"]
|
@@ -1,23 +1,23 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from nonebug import App
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from utils import FakeAdapter
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.drivers import (
|
||||
URL,
|
||||
Driver,
|
||||
HTTPServerSetup,
|
||||
Request,
|
||||
Response,
|
||||
WebSocket,
|
||||
HTTPServerSetup,
|
||||
WebSocketServerSetup,
|
||||
)
|
||||
from utils import FakeAdapter
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_adapter_connect(app: App, driver: Driver):
|
||||
last_connect_bot: Optional[Bot] = None
|
||||
last_disconnect_bot: Optional[Bot] = None
|
||||
@@ -45,6 +45,7 @@ async def test_adapter_connect(app: App, driver: Driver):
|
||||
assert bot.self_id not in adapter.bots
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -74,7 +75,7 @@ async def test_adapter_connect(app: App, driver: Driver):
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
def test_adapter_server(driver: Driver):
|
||||
async def test_adapter_server(driver: Driver):
|
||||
last_http_setup: Optional[HTTPServerSetup] = None
|
||||
last_ws_setup: Optional[WebSocketServerSetup] = None
|
||||
|
||||
@@ -111,7 +112,7 @@ def test_adapter_server(driver: Driver):
|
||||
assert last_ws_setup is setup
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -158,7 +159,7 @@ async def test_adapter_http_client(driver: Driver):
|
||||
assert last_request is request
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
|
@@ -1,14 +1,13 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
import anyio
|
||||
from nonebug import App
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.exception import MockApiException
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot_call_api(app: App):
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
@@ -24,7 +23,7 @@ async def test_bot_call_api(app: App):
|
||||
await bot.call_api("test")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot_calling_api_hook_simple(app: App):
|
||||
runned: bool = False
|
||||
|
||||
@@ -50,7 +49,7 @@ async def test_bot_calling_api_hook_simple(app: App):
|
||||
assert result is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot_calling_api_hook_mock(app: App):
|
||||
runned: bool = False
|
||||
|
||||
@@ -77,47 +76,7 @@ async def test_bot_calling_api_hook_mock(app: App):
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_bot_calling_api_hook_multi_mock(app: App):
|
||||
runned1: bool = False
|
||||
runned2: bool = False
|
||||
event = anyio.Event()
|
||||
|
||||
async def calling_api_hook1(bot: Bot, api: str, data: dict[str, Any]):
|
||||
nonlocal runned1
|
||||
runned1 = True
|
||||
event.set()
|
||||
|
||||
raise MockApiException(1)
|
||||
|
||||
async def calling_api_hook2(bot: Bot, api: str, data: dict[str, Any]):
|
||||
nonlocal runned2
|
||||
runned2 = True
|
||||
with anyio.fail_after(1):
|
||||
await event.wait()
|
||||
|
||||
raise MockApiException(2)
|
||||
|
||||
hooks = set()
|
||||
|
||||
with pytest.MonkeyPatch.context() as m:
|
||||
m.setattr(Bot, "_calling_api_hook", hooks)
|
||||
|
||||
Bot.on_calling_api(calling_api_hook1)
|
||||
Bot.on_calling_api(calling_api_hook2)
|
||||
|
||||
assert hooks == {calling_api_hook1, calling_api_hook2}
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
result = await bot.call_api("test")
|
||||
|
||||
assert runned1 is True
|
||||
assert runned2 is True
|
||||
assert result == 1
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot_called_api_hook_simple(app: App):
|
||||
runned: bool = False
|
||||
|
||||
@@ -149,7 +108,7 @@ async def test_bot_called_api_hook_simple(app: App):
|
||||
assert result is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot_called_api_hook_mock(app: App):
|
||||
runned: bool = False
|
||||
|
||||
@@ -191,56 +150,3 @@ async def test_bot_called_api_hook_mock(app: App):
|
||||
|
||||
assert runned is True
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_bot_called_api_hook_multi_mock(app: App):
|
||||
runned1: bool = False
|
||||
runned2: bool = False
|
||||
event = anyio.Event()
|
||||
|
||||
async def called_api_hook1(
|
||||
bot: Bot,
|
||||
exception: Optional[Exception],
|
||||
api: str,
|
||||
data: dict[str, Any],
|
||||
result: Any,
|
||||
):
|
||||
nonlocal runned1
|
||||
runned1 = True
|
||||
event.set()
|
||||
|
||||
raise MockApiException(1)
|
||||
|
||||
async def called_api_hook2(
|
||||
bot: Bot,
|
||||
exception: Optional[Exception],
|
||||
api: str,
|
||||
data: dict[str, Any],
|
||||
result: Any,
|
||||
):
|
||||
nonlocal runned2
|
||||
runned2 = True
|
||||
with anyio.fail_after(1):
|
||||
await event.wait()
|
||||
|
||||
raise MockApiException(2)
|
||||
|
||||
hooks = set()
|
||||
|
||||
with pytest.MonkeyPatch.context() as m:
|
||||
m.setattr(Bot, "_called_api_hook", hooks)
|
||||
|
||||
Bot.on_called_api(called_api_hook1)
|
||||
Bot.on_called_api(called_api_hook2)
|
||||
|
||||
assert hooks == {called_api_hook1, called_api_hook2}
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
ctx.should_call_api("test", {}, True)
|
||||
result = await bot.call_api("test")
|
||||
|
||||
assert runned1 is True
|
||||
assert runned2 is True
|
||||
assert result == 1
|
||||
|
@@ -1,9 +1,9 @@
|
||||
from pydantic import ValidationError
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from nonebot.adapters import Message, MessageSegment
|
||||
from nonebot.compat import type_validate_python
|
||||
from utils import FakeMessage, FakeMessageSegment
|
||||
from nonebot.adapters import Message, MessageSegment
|
||||
|
||||
|
||||
def test_segment_data():
|
||||
|
@@ -1,31 +1,31 @@
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from nonebug import App
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot import on_message
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.exception import IgnoredException
|
||||
from nonebot.log import default_filter, default_format, logger
|
||||
from nonebot.matcher import Matcher
|
||||
import nonebot.message as message
|
||||
from nonebot.message import (
|
||||
event_postprocessor,
|
||||
event_preprocessor,
|
||||
run_postprocessor,
|
||||
run_preprocessor,
|
||||
)
|
||||
from utils import make_fake_event
|
||||
from nonebot.params import Depends
|
||||
from nonebot.typing import T_State
|
||||
from utils import make_fake_event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.exception import IgnoredException
|
||||
from nonebot.log import logger, default_filter, default_format
|
||||
from nonebot.message import (
|
||||
run_preprocessor,
|
||||
run_postprocessor,
|
||||
event_preprocessor,
|
||||
event_postprocessor,
|
||||
)
|
||||
|
||||
|
||||
async def _dependency() -> int:
|
||||
return 1
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_event_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(message, "_event_preprocessors", set())
|
||||
@@ -58,7 +58,7 @@ async def test_event_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
assert runned, "event_preprocessor should runned"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_event_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(message, "_event_preprocessors", set())
|
||||
@@ -88,7 +88,7 @@ async def test_event_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPat
|
||||
assert not runned, "matcher should not runned"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_event_preprocessor_exception(
|
||||
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
):
|
||||
@@ -132,7 +132,7 @@ async def test_event_preprocessor_exception(
|
||||
assert "RuntimeError: test" in capsys.readouterr().out
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_event_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(message, "_event_postprocessors", set())
|
||||
@@ -165,7 +165,7 @@ async def test_event_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
assert runned, "event_postprocessor should runned"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_event_postprocessor_exception(
|
||||
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
):
|
||||
@@ -202,7 +202,7 @@ async def test_event_postprocessor_exception(
|
||||
assert "RuntimeError: test" in capsys.readouterr().out
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(message, "_run_preprocessors", set())
|
||||
@@ -239,7 +239,7 @@ async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
assert runned, "run_preprocessor should runned"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(message, "_run_preprocessors", set())
|
||||
@@ -269,7 +269,7 @@ async def test_run_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch
|
||||
assert not runned, "matcher should not runned"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_preprocessor_exception(
|
||||
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
):
|
||||
@@ -313,7 +313,7 @@ async def test_run_preprocessor_exception(
|
||||
assert "RuntimeError: test" in capsys.readouterr().out
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(message, "_run_postprocessors", set())
|
||||
@@ -351,7 +351,7 @@ async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
assert runned, "run_postprocessor should runned"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_postprocessor_exception(
|
||||
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
):
|
||||
|
@@ -1,30 +1,30 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Annotated, Any, Optional
|
||||
from typing import Any, Optional, Annotated
|
||||
|
||||
from pydantic import BaseModel, ValidationError
|
||||
import pytest
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from nonebot.compat import (
|
||||
DEFAULT_CONFIG,
|
||||
FieldInfo,
|
||||
PydanticUndefined,
|
||||
Required,
|
||||
FieldInfo,
|
||||
TypeAdapter,
|
||||
custom_validation,
|
||||
field_validator,
|
||||
PydanticUndefined,
|
||||
model_dump,
|
||||
model_validator,
|
||||
custom_validation,
|
||||
type_validate_json,
|
||||
type_validate_python,
|
||||
)
|
||||
|
||||
|
||||
def test_default_config():
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_config():
|
||||
assert DEFAULT_CONFIG.get("extra") == "allow"
|
||||
assert DEFAULT_CONFIG.get("arbitrary_types_allowed") is True
|
||||
|
||||
|
||||
def test_field_info():
|
||||
@pytest.mark.asyncio
|
||||
async def test_field_info():
|
||||
# required should be convert to PydanticUndefined
|
||||
assert FieldInfo(Required).default is PydanticUndefined
|
||||
|
||||
@@ -32,33 +32,8 @@ def test_field_info():
|
||||
assert FieldInfo(test="test").extra["test"] == "test"
|
||||
|
||||
|
||||
def test_field_validator():
|
||||
class TestModel(BaseModel):
|
||||
foo: int
|
||||
bar: str
|
||||
|
||||
@field_validator("foo")
|
||||
@classmethod
|
||||
def test_validator(cls, v: Any) -> Any:
|
||||
if v > 0:
|
||||
return v
|
||||
raise ValueError("test must be greater than 0")
|
||||
|
||||
@field_validator("bar", mode="before")
|
||||
@classmethod
|
||||
def test_validator_before(cls, v: Any) -> Any:
|
||||
if not isinstance(v, str):
|
||||
v = str(v)
|
||||
return v
|
||||
|
||||
assert type_validate_python(TestModel, {"foo": 1, "bar": "test"}).foo == 1
|
||||
assert type_validate_python(TestModel, {"foo": 1, "bar": 123}).bar == "123"
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
TestModel(foo=0, bar="test")
|
||||
|
||||
|
||||
def test_type_adapter():
|
||||
@pytest.mark.asyncio
|
||||
async def test_type_adapter():
|
||||
t = TypeAdapter(Annotated[int, FieldInfo(ge=1)])
|
||||
|
||||
assert t.validate_python(2) == 2
|
||||
@@ -72,7 +47,8 @@ def test_type_adapter():
|
||||
t.validate_json("0")
|
||||
|
||||
|
||||
def test_model_dump():
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_dump():
|
||||
class TestModel(BaseModel):
|
||||
test1: int
|
||||
test2: int
|
||||
@@ -81,36 +57,8 @@ def test_model_dump():
|
||||
assert model_dump(TestModel(test1=1, test2=2), exclude={"test1"}) == {"test2": 2}
|
||||
|
||||
|
||||
def test_model_validator():
|
||||
class TestModel(BaseModel):
|
||||
foo: int
|
||||
bar: str
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def test_validator_before(cls, data: Any) -> Any:
|
||||
if isinstance(data, dict):
|
||||
if "foo" not in data:
|
||||
data["foo"] = 1
|
||||
return data
|
||||
|
||||
@model_validator(mode="after")
|
||||
@classmethod
|
||||
def test_validator_after(cls, data: Any) -> Any:
|
||||
if isinstance(data, dict):
|
||||
if data["bar"] == "test":
|
||||
raise ValueError("bar should not be test")
|
||||
elif data.bar == "test":
|
||||
raise ValueError("bar should not be test")
|
||||
return data
|
||||
|
||||
assert type_validate_python(TestModel, {"bar": "aaa"}).foo == 1
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
type_validate_python(TestModel, {"foo": 1, "bar": "test"})
|
||||
|
||||
|
||||
def test_custom_validation():
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_validation():
|
||||
called = []
|
||||
|
||||
@custom_validation
|
||||
@@ -137,7 +85,8 @@ def test_custom_validation():
|
||||
assert called == [1, 2]
|
||||
|
||||
|
||||
def test_validate_json():
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_json():
|
||||
class TestModel(BaseModel):
|
||||
test1: int
|
||||
test2: str
|
||||
|
@@ -1,10 +1,10 @@
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from typing import TYPE_CHECKING, Union, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
import pytest
|
||||
from pydantic import Field, BaseModel
|
||||
|
||||
from nonebot.compat import PYDANTIC_V2
|
||||
from nonebot.config import DOTENV_TYPE, BaseSettings, SettingsConfig, SettingsError
|
||||
from nonebot.config import DOTENV_TYPE, BaseSettings, SettingsError, SettingsConfig
|
||||
|
||||
|
||||
class Simple(BaseModel):
|
||||
@@ -50,14 +50,16 @@ class ExampleWithoutDelimiter(Example):
|
||||
env_nested_delimiter = None
|
||||
|
||||
|
||||
def test_config_no_env():
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_no_env():
|
||||
config = Example(_env_file=None)
|
||||
assert config.simple == ""
|
||||
with pytest.raises(AttributeError):
|
||||
config.common_config
|
||||
|
||||
|
||||
def test_config_with_env():
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_with_env():
|
||||
config = Example(_env_file=(".env", ".env.example"))
|
||||
assert config.simple == "simple"
|
||||
|
||||
@@ -100,7 +102,8 @@ def test_config_with_env():
|
||||
config.other_nested_inner__b
|
||||
|
||||
|
||||
def test_config_error_env():
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_error_env():
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setenv("COMPLEX", "not json")
|
||||
|
||||
@@ -108,7 +111,8 @@ def test_config_error_env():
|
||||
Example(_env_file=(".env", ".env.example"))
|
||||
|
||||
|
||||
def test_config_without_delimiter():
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_without_delimiter():
|
||||
config = ExampleWithoutDelimiter()
|
||||
assert config.nested.a == 1
|
||||
assert config.nested.b == 0
|
||||
|
@@ -1,31 +1,31 @@
|
||||
from http.cookies import SimpleCookie
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Any, Optional
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
import anyio
|
||||
from nonebug import App
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from utils import FakeAdapter
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.params import Depends
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers import (
|
||||
URL,
|
||||
ASGIMixin,
|
||||
Driver,
|
||||
HTTPClientMixin,
|
||||
HTTPServerSetup,
|
||||
Request,
|
||||
Response,
|
||||
ASGIMixin,
|
||||
WebSocket,
|
||||
HTTPClientMixin,
|
||||
HTTPServerSetup,
|
||||
WebSocketClientMixin,
|
||||
WebSocketServerSetup,
|
||||
)
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.params import Depends
|
||||
from utils import FakeAdapter
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver", [pytest.param("nonebot.drivers.none:Driver", id="none")], indirect=True
|
||||
)
|
||||
@@ -59,22 +59,22 @@ async def test_lifespan(driver: Driver):
|
||||
|
||||
@driver.on_shutdown
|
||||
async def _shutdown1():
|
||||
assert shutdown_log == [2]
|
||||
assert shutdown_log == []
|
||||
shutdown_log.append(1)
|
||||
|
||||
@driver.on_shutdown
|
||||
async def _shutdown2():
|
||||
assert shutdown_log == []
|
||||
assert shutdown_log == [1]
|
||||
shutdown_log.append(2)
|
||||
|
||||
async with driver._lifespan:
|
||||
assert start_log == [1, 2]
|
||||
assert ready_log == [1, 2]
|
||||
|
||||
assert shutdown_log == [2, 1]
|
||||
assert shutdown_log == [1, 2]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -99,10 +99,10 @@ async def test_http_server(app: App, driver: Driver):
|
||||
assert response.status_code == 200
|
||||
assert response.text == "test"
|
||||
|
||||
await anyio.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -155,10 +155,10 @@ async def test_websocket_server(app: App, driver: Driver):
|
||||
|
||||
await ws.close(code=1000)
|
||||
|
||||
await anyio.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -171,10 +171,9 @@ async def test_cross_context(app: App, driver: Driver):
|
||||
assert isinstance(driver, ASGIMixin)
|
||||
|
||||
ws: Optional[WebSocket] = None
|
||||
ws_ready = anyio.Event()
|
||||
ws_should_close = anyio.Event()
|
||||
ws_ready = asyncio.Event()
|
||||
ws_should_close = asyncio.Event()
|
||||
|
||||
# create a background task before the ws connection established
|
||||
async def background_task():
|
||||
try:
|
||||
await ws_ready.wait()
|
||||
@@ -186,6 +185,8 @@ async def test_cross_context(app: App, driver: Driver):
|
||||
finally:
|
||||
ws_should_close.set()
|
||||
|
||||
task = asyncio.create_task(background_task())
|
||||
|
||||
async def _handle_ws(websocket: WebSocket) -> None:
|
||||
nonlocal ws
|
||||
await websocket.accept()
|
||||
@@ -198,9 +199,7 @@ async def test_cross_context(app: App, driver: Driver):
|
||||
ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws)
|
||||
driver.setup_websocket_server(ws_setup)
|
||||
|
||||
async with anyio.create_task_group() as tg, app.test_server(driver.asgi) as ctx:
|
||||
tg.start_soon(background_task)
|
||||
|
||||
async with app.test_server(driver.asgi) as ctx:
|
||||
client = ctx.get_client()
|
||||
|
||||
async with client.websocket_connect("/ws_test") as websocket:
|
||||
@@ -212,10 +211,11 @@ async def test_cross_context(app: App, driver: Driver):
|
||||
if not e.args or "websocket.close" not in str(e.args[0]):
|
||||
raise
|
||||
|
||||
await anyio.sleep(1)
|
||||
await task
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -251,9 +251,9 @@ async def test_http_client(driver: Driver, server_url: URL):
|
||||
cookies={"session": "test"},
|
||||
content="test",
|
||||
)
|
||||
assert request.url == request_raw_url.url, (
|
||||
"request.url should be equal to request_raw_url.url"
|
||||
)
|
||||
assert (
|
||||
request.url == request_raw_url.url
|
||||
), "request.url should be equal to request_raw_url.url"
|
||||
assert response.status_code == 200
|
||||
assert response.content
|
||||
data = json.loads(response.content)
|
||||
@@ -304,10 +304,10 @@ async def test_http_client(driver: Driver, server_url: URL):
|
||||
"test3": "test",
|
||||
}, "file parsing error"
|
||||
|
||||
await anyio.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -419,10 +419,10 @@ async def test_http_client_session(driver: Driver, server_url: URL):
|
||||
"test3": "test",
|
||||
}, "file parsing error"
|
||||
|
||||
await anyio.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -452,9 +452,10 @@ async def test_websocket_client(driver: Driver, server_url: URL):
|
||||
with pytest.raises(WebSocketClosed, match=r"code=1000"):
|
||||
await ws.receive()
|
||||
|
||||
await anyio.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("driver", "driver_type"),
|
||||
[
|
||||
@@ -471,11 +472,11 @@ async def test_websocket_client(driver: Driver, server_url: URL):
|
||||
],
|
||||
indirect=["driver"],
|
||||
)
|
||||
def test_combine_driver(driver: Driver, driver_type: str):
|
||||
async def test_combine_driver(driver: Driver, driver_type: str):
|
||||
assert driver.type == driver_type
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot_connect_hook(app: App, driver: Driver):
|
||||
with pytest.MonkeyPatch.context() as m:
|
||||
conn_hooks: set[Dependent[Any]] = set()
|
||||
@@ -532,7 +533,7 @@ async def test_bot_connect_hook(app: App, driver: Driver):
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
|
||||
await anyio.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if not conn_should_be_called:
|
||||
pytest.fail("on_bot_connect hook not called")
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user