mirror of
https://github.com/nonebot/nonebot2.git
synced 2024-11-24 00:55:07 +08:00
✨ Feature: 添加插件 Pydantic 相关使用方法 (#2563)
This commit is contained in:
parent
2ebf956599
commit
dace63d9d2
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 = []
|
||||
|
@ -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
|
||||
|
@ -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` 格式](#配置项解析),例如:
|
||||
|
@ -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
|
||||
```
|
||||
|
@ -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` 文本文件,并写入以下内容:
|
||||
|
||||
|
@ -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")])
|
||||
```
|
||||
|
Loading…
Reference in New Issue
Block a user