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>` {ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>`
- `get_available_plugin_names` => - `get_available_plugin_names` =>
{ref}``get_available_plugin_names` <nonebot.plugin.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>` - `require` => {ref}``require` <nonebot.plugin.load.require>`
FrontMatter: 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_from_toml as load_from_toml
from nonebot.plugin import load_all_plugins as load_all_plugins 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 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 get_loaded_plugins as get_loaded_plugins
from nonebot.plugin import load_builtin_plugin as load_builtin_plugin 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_builtin_plugins as load_builtin_plugins

View File

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

View File

@ -39,7 +39,14 @@ FrontMatter:
from itertools import chain from itertools import chain
from types import ModuleType from types import ModuleType
from contextvars import ContextVar 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"] = {} _plugins: Dict[str, "Plugin"] = {}
_managers: List["PluginManager"] = [] _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)} 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 .on import on as on
from .manager import PluginManager from .manager import PluginManager
from .on import on_type as on_type 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 NESTED_MISSING_DICT__B__C=2
NOT_NESTED=some string NOT_NESTED=some string
NOT_NESTED__A=1 NOT_NESTED__A=1
PLUGIN_CONFIG=1

View File

@ -2,12 +2,14 @@ from typing import Any
from dataclasses import dataclass from dataclasses import dataclass
import pytest import pytest
from pydantic import BaseModel
from nonebot.compat import ( from nonebot.compat import (
DEFAULT_CONFIG, DEFAULT_CONFIG,
Required, Required,
FieldInfo, FieldInfo,
PydanticUndefined, PydanticUndefined,
model_dump,
custom_validation, custom_validation,
type_validate_python, type_validate_python,
) )
@ -28,6 +30,16 @@ async def test_field_info():
assert FieldInfo(test="test").extra["test"] == "test" 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 @pytest.mark.asyncio
async def test_custom_validation(): async def test_custom_validation():
called = [] called = []

View File

@ -1,4 +1,5 @@
import pytest import pytest
from pydantic import BaseModel
import nonebot import nonebot
from nonebot.plugin import PluginManager, _managers from nonebot.plugin import PluginManager, _managers
@ -35,3 +36,14 @@ async def test_get_available_plugin():
finally: finally:
_managers.clear() _managers.clear()
_managers.extend(old_managers) _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 内置的配置项列表及含义可以在[内置配置项](#内置配置项)中查看。 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 配置文件** 三种,其加载优先级依次由高到低。 在 NoneBot 中,我们可以把配置途径分为 **直接传入**、**系统环境变量**、**dotenv 配置文件** 三种,其加载优先级依次由高到低。
@ -182,18 +201,19 @@ superusers = config.superusers
在 NoneBot 中,我们使用强大高效的 `pydantic` 来定义配置模型,这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型: 在 NoneBot 中,我们使用强大高效的 `pydantic` 来定义配置模型,这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型:
```python title=weather/config.py ```python title=weather/config.py
from pydantic import BaseModel, validator from pydantic import BaseModel, field_validator
class Config(BaseModel): class Config(BaseModel):
weather_api_key: str weather_api_key: str
weather_command_priority: int = 10 weather_command_priority: int = 10
weather_plugin_enabled: bool = True weather_plugin_enabled: bool = True
@validator("weather_command_priority") @field_validator("weather_command_priority")
def check_priority(cls, v): @classmethod
if isinstance(v, int) and v >= 1: def check_priority(cls, v: int) -> int:
if v >= 1:
return v 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/)。 在 `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 ```python {5,11} title=weather/__init__.py
from nonebot import get_driver from nonebot import get_plugin_config
from .config import Config from .config import Config
plugin_config = Config.parse_obj(get_driver().config) plugin_config = get_plugin_config(Config)
weather = on_command( weather = on_command(
"天气", "天气",
@ -239,11 +259,11 @@ class Config(BaseModel):
``` ```
```python title=weather/__init__.py ```python title=weather/__init__.py
from nonebot import get_driver from nonebot import get_plugin_config
from .config import 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` 格式](#配置项解析),例如: 这样我们就可以省略插件配置项名称中的前缀 `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)类似,例如: 通常适配器需要一些配置项,例如平台连接密钥等。适配器的配置方法与[插件配置](../appendices/config#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE)类似,例如:
```python title=config.py ```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_id: str
xxx_token: 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` 文本文件,并写入以下内容: 在**项目文件夹**中创建一个 `.env` 文本文件,并写入以下内容:

View File

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