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>`
|
{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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 = []
|
||||||
|
@ -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
|
||||||
|
@ -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` 格式](#配置项解析),例如:
|
||||||
|
@ -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
|
||||||
```
|
```
|
||||||
|
@ -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` 文本文件,并写入以下内容:
|
||||||
|
|
||||||
|
@ -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")])
|
||||||
```
|
```
|
||||||
|
Loading…
Reference in New Issue
Block a user