Skip to content

Commit

Permalink
✨ Feature: 添加插件 Pydantic 相关使用方法 (#2563)
Browse files Browse the repository at this point in the history
  • Loading branch information
yanyongyu authored Feb 5, 2024
1 parent 2ebf956 commit dace63d
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 19 deletions.
2 changes: 2 additions & 0 deletions nonebot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -352,6 +353,7 @@ def run(*args: Any, **kwargs: Any) -> None:
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
Expand Down
16 changes: 16 additions & 0 deletions nonebot/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from typing import (
TYPE_CHECKING,
Any,
Set,
Dict,
List,
Type,
Expand Down Expand Up @@ -50,6 +51,7 @@ def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]:
"model_field_validate",
"model_fields",
"model_config",
"model_dump",
"type_validate_python",
"custom_validation",
)
Expand Down Expand Up @@ -183,6 +185,13 @@ def model_config(model: Type[BaseModel]) -> Any:
"""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)
Expand Down Expand Up @@ -321,6 +330,13 @@ def model_config(model: Type[BaseModel]) -> Any:
"""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)
Expand Down
14 changes: 13 additions & 1 deletion nonebot/plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@
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"] = []
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 12 additions & 0 deletions tests/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
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,
)
Expand All @@ -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 = []
Expand Down
12 changes: 12 additions & 0 deletions tests/test_plugin/test_get.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from pydantic import BaseModel

import nonebot
from nonebot.plugin import PluginManager, _managers
Expand Down Expand Up @@ -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
38 changes: 29 additions & 9 deletions website/docs/appendices/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 配置文件** 三种,其加载优先级依次由高到低。
Expand Down Expand Up @@ -182,30 +201,31 @@ 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/)

在定义好配置模型后,我们可以在插件加载时获取全局配置,导入插件自身的配置模型并使用:

```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(
"天气",
Expand Down Expand Up @@ -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` 格式](#配置项解析),例如:
Expand Down
4 changes: 2 additions & 2 deletions website/docs/developer/adapter-writing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
2 changes: 1 addition & 1 deletion website/docs/tutorial/application.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 文本文件,并写入以下内容:

Expand Down
11 changes: 5 additions & 6 deletions website/docs/tutorial/message.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")])
```
Expand Down

0 comments on commit dace63d

Please sign in to comment.