Feature: 添加插件 Pydantic 相关使用方法 (#2563)

This commit is contained in:
Ju4tCode 2024-02-05 14:00:49 +08:00 committed by GitHub
parent 2ebf956599
commit dace63d9d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 93 additions and 19 deletions

View File

@ -35,6 +35,7 @@
{ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>`
- `get_available_plugin_names` =>
{ref}``get_available_plugin_names` <nonebot.plugin.get_available_plugin_names>`
- `get_plugin_config` => {ref}``get_plugin_config` <nonebot.plugin.get_plugin_config>`
- `require` => {ref}``require` <nonebot.plugin.load.require>`
FrontMatter:
@ -352,6 +353,7 @@ 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

View File

@ -12,6 +12,7 @@ from typing_extensions import Self, Annotated, is_typeddict
from typing import (
TYPE_CHECKING,
Any,
Set,
Dict,
List,
Type,
@ -50,6 +51,7 @@ __all__ = (
"model_field_validate",
"model_fields",
"model_config",
"model_dump",
"type_validate_python",
"custom_validation",
)
@ -183,6 +185,13 @@ if PYDANTIC_V2: # pragma: pydantic-v2
"""Get config of a model."""
return model.model_config
def model_dump(
model: BaseModel,
include: Optional[Set[str]] = None,
exclude: Optional[Set[str]] = None,
) -> Dict[str, Any]:
return model.model_dump(include=include, exclude=exclude)
def type_validate_python(type_: Type[T], data: Any) -> T:
"""Validate data with given type."""
return TypeAdapter(type_).validate_python(data)
@ -321,6 +330,13 @@ else: # pragma: pydantic-v1
"""Get config of a model."""
return model.__config__
def model_dump(
model: BaseModel,
include: Optional[Set[str]] = None,
exclude: Optional[Set[str]] = None,
) -> Dict[str, Any]:
return model.dict(include=include, exclude=exclude)
def type_validate_python(type_: Type[T], data: Any) -> T:
"""Validate data with given type."""
return parse_obj_as(type_, data)

View File

@ -39,7 +39,14 @@ FrontMatter:
from itertools import chain
from types import ModuleType
from contextvars import ContextVar
from typing import Set, Dict, List, Tuple, Optional
from typing import Set, Dict, List, Type, Tuple, TypeVar, Optional
from pydantic import BaseModel
from nonebot import get_driver
from nonebot.compat import model_dump, type_validate_python
C = TypeVar("C", bound=BaseModel)
_plugins: Dict[str, "Plugin"] = {}
_managers: List["PluginManager"] = []
@ -108,6 +115,11 @@ def get_available_plugin_names() -> Set[str]:
return {*chain.from_iterable(manager.available_plugins for manager in _managers)}
def get_plugin_config(config: Type[C]) -> C:
"""从全局配置获取当前插件需要的配置项。"""
return type_validate_python(config, model_dump(get_driver().config))
from .on import on as on
from .manager import PluginManager
from .on import on_type as on_type

View File

@ -13,3 +13,4 @@ NESTED_MISSING_DICT__A=1
NESTED_MISSING_DICT__B__C=2
NOT_NESTED=some string
NOT_NESTED__A=1
PLUGIN_CONFIG=1

View File

@ -2,12 +2,14 @@ from typing import Any
from dataclasses import dataclass
import pytest
from pydantic import BaseModel
from nonebot.compat import (
DEFAULT_CONFIG,
Required,
FieldInfo,
PydanticUndefined,
model_dump,
custom_validation,
type_validate_python,
)
@ -28,6 +30,16 @@ async def test_field_info():
assert FieldInfo(test="test").extra["test"] == "test"
@pytest.mark.asyncio
async def test_model_dump():
class TestModel(BaseModel):
test1: int
test2: int
assert model_dump(TestModel(test1=1, test2=2), include={"test1"}) == {"test1": 1}
assert model_dump(TestModel(test1=1, test2=2), exclude={"test1"}) == {"test2": 2}
@pytest.mark.asyncio
async def test_custom_validation():
called = []

View File

@ -1,4 +1,5 @@
import pytest
from pydantic import BaseModel
import nonebot
from nonebot.plugin import PluginManager, _managers
@ -35,3 +36,14 @@ async def test_get_available_plugin():
finally:
_managers.clear()
_managers.extend(old_managers)
@pytest.mark.asyncio
async def test_get_plugin_config():
class Config(BaseModel):
plugin_config: int
# check get plugin config
config = nonebot.get_plugin_config(Config)
assert isinstance(config, Config)
assert config.plugin_config == 1

View File

@ -19,6 +19,25 @@ NoneBot 使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`]
NoneBot 内置的配置项列表及含义可以在[内置配置项](#内置配置项)中查看。
:::caution 注意
NoneBot 自 2.2.0 起兼容了 Pydantic v1 与 v2 版本,以下文档中 Pydantic 相关示例均采用 v2 版本用法。
如果在使用商店或其他第三方插件的过程中遇到 Pydantic 相关警告或报错,例如:
```python
pydantic_core._pydantic_core.ValidationError: 1 validation error for Config
Input should be a valid dictionary or instance of Config [type=model_type, input_value=Config(...), input_type=Config]
```
请考虑降级 Pydantic 至 v1 版本:
```bash
pip install --force-reinstall 'pydantic~=1.10'
```
:::
## 配置项的加载
在 NoneBot 中,我们可以把配置途径分为 **直接传入**、**系统环境变量**、**dotenv 配置文件** 三种,其加载优先级依次由高到低。
@ -182,18 +201,19 @@ superusers = config.superusers
在 NoneBot 中,我们使用强大高效的 `pydantic` 来定义配置模型,这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型:
```python title=weather/config.py
from pydantic import BaseModel, validator
from pydantic import BaseModel, field_validator
class Config(BaseModel):
weather_api_key: str
weather_command_priority: int = 10
weather_plugin_enabled: bool = True
@validator("weather_command_priority")
def check_priority(cls, v):
if isinstance(v, int) and v >= 1:
@field_validator("weather_command_priority")
@classmethod
def check_priority(cls, v: int) -> int:
if v >= 1:
return v
raise ValueError("weather command priority must be an integer and greater than 1")
raise ValueError("weather command priority must greater than 1")
```
在 `config.py` 中,我们定义了一个 `Config` 类,它继承自 `pydantic.BaseModel`,并定义了一些配置项。在 `Config` 类中,我们还定义了一个 `check_priority` 方法,它用于检查 `weather_command_priority` 配置项的合法性。更多关于 `pydantic` 的编写方式,可以参考 [pydantic 官方文档](https://docs.pydantic.dev/)。
@ -201,11 +221,11 @@ class Config(BaseModel):
在定义好配置模型后,我们可以在插件加载时获取全局配置,导入插件自身的配置模型并使用:
```python {5,11} title=weather/__init__.py
from nonebot import get_driver
from nonebot import get_plugin_config
from .config import Config
plugin_config = Config.parse_obj(get_driver().config)
plugin_config = get_plugin_config(Config)
weather = on_command(
"天气",
@ -239,11 +259,11 @@ class Config(BaseModel):
```
```python title=weather/__init__.py
from nonebot import get_driver
from nonebot import get_plugin_config
from .config import Config
plugin_config = Config.parse_obj(get_driver().config).weather
plugin_config = get_plugin_config(Config).weather
```
这样我们就可以省略插件配置项名称中的前缀 `weather_` 了。但需要注意的是,如果我们使用了 scope 配置,那么在配置文件中也需要使用 [`env_nested_delimiter` 格式](#配置项解析),例如:

View File

@ -79,9 +79,9 @@ except Exception as e:
通常适配器需要一些配置项,例如平台连接密钥等。适配器的配置方法与[插件配置](../appendices/config#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE)类似,例如:
```python title=config.py
from pydantic import BaseModel, Extra
from pydantic import BaseModel
class Config(BaseModel, extra=Extra.ignore):
class Config(BaseModel):
xxx_id: str
xxx_token: str
```

View File

@ -56,7 +56,7 @@ options:
## 创建配置文件
配置文件用于存放 NoneBot 运行所需要的配置项,使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。配置项需符合 dotenv 格式,复杂类型数据需使用 JSON 格式填写。具体可选配置方式以及配置项详情参考[配置](../appendices/config.mdx)。
配置文件用于存放 NoneBot 运行所需要的配置项,使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。配置项需符合 dotenv 格式,复杂类型数据需使用 JSON 格式填写。具体可选配置方式以及配置项详情参考[配置](../appendices/config.mdx)。
在**项目文件夹**中创建一个 `.env` 文本文件,并写入以下内容:

View File

@ -82,20 +82,19 @@ Message([MessageSegment.text("Hello, world!")])
#### 从字典数组构造
`Message` 对象支持 Pydantic 自定义类型构造,可以使用 Pydantic 的 `parse_obj_as` 方法进行构造。
`Message` 对象支持 Pydantic 自定义类型构造,可以使用 Pydantic 的 `TypeAdapter` 方法进行构造。
```python
from pydantic import parse_obj_as
from pydantic import TypeAdapter
from nonebot.adapters.console import Message, MessageSegment
# 由字典构造消息段
parse_obj_as(
MessageSegment, {"type": "text", "data": {"text": "text"}}
TypeAdapter(MessageSegment).validate_python(
{"type": "text", "data": {"text": "text"}}
) == MessageSegment.text("text")
# 由字典数组构造消息序列
parse_obj_as(
Message,
TypeAdapter(Message).validate_python(
[MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}],
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
```