Feature: 兼容 Pydantic v2 (#2544)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Ju4tCode
2024-01-26 11:12:57 +08:00
committed by GitHub
parent 82e4ccb227
commit bbd13c04cc
36 changed files with 6535 additions and 414 deletions

View File

@ -22,5 +22,8 @@ rules =
"sys_platform != 'linux'": py-linux
"sys_platform != 'darwin'": py-darwin
"sys_version_info < (3, 9)": py-gte-39
"sys_version_info >= (3, 9)": py-lt-39
"sys_version_info < (3, 11)": py-gte-311
"sys_version_info >= (3, 11)": py-lt-311
"package_version('pydantic') < (2,)": pydantic-v2
"package_version('pydantic') >= (2,)": pydantic-v1

View File

@ -4,3 +4,7 @@ from typing import Union
async def exc(e: Exception, x: Union[ValueError, TypeError]) -> Exception:
assert e == x
return e
async def legacy_exc(exception) -> Exception:
return exception

View File

@ -1,8 +1,9 @@
import pytest
from pydantic import ValidationError, parse_obj_as
from pydantic import ValidationError
from nonebot.adapters import Message
from nonebot.compat import type_validate_python
from utils import FakeMessage, FakeMessageSegment
from nonebot.adapters import Message, MessageSegment
def test_segment_data():
@ -47,16 +48,21 @@ def test_segment_add():
def test_segment_validate():
assert parse_obj_as(
assert type_validate_python(
FakeMessageSegment,
{"type": "text", "data": {"text": "text"}, "extra": "should be ignored"},
) == FakeMessageSegment.text("text")
with pytest.raises(ValidationError):
type_validate_python(
type("FakeMessageSegment2", (MessageSegment,), {}),
FakeMessageSegment.text("text"),
)
with pytest.raises(ValidationError):
parse_obj_as(FakeMessageSegment, "some str")
type_validate_python(FakeMessageSegment, "some str")
with pytest.raises(ValidationError):
parse_obj_as(FakeMessageSegment, {"data": {}})
type_validate_python(FakeMessageSegment, {"data": {}})
def test_segment_join():
@ -144,26 +150,26 @@ def test_message_getitem():
def test_message_validate():
assert parse_obj_as(FakeMessage, FakeMessage([])) == FakeMessage([])
assert type_validate_python(FakeMessage, FakeMessage([])) == FakeMessage([])
with pytest.raises(ValidationError):
parse_obj_as(type("FakeMessage2", (Message,), {}), FakeMessage([]))
type_validate_python(type("FakeMessage2", (Message,), {}), FakeMessage([]))
assert parse_obj_as(FakeMessage, "text") == FakeMessage(
assert type_validate_python(FakeMessage, "text") == FakeMessage(
[FakeMessageSegment.text("text")]
)
assert parse_obj_as(
assert type_validate_python(
FakeMessage, {"type": "text", "data": {"text": "text"}}
) == FakeMessage([FakeMessageSegment.text("text")])
assert parse_obj_as(
assert type_validate_python(
FakeMessage,
[FakeMessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}],
) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")])
with pytest.raises(ValidationError):
parse_obj_as(FakeMessage, object())
type_validate_python(FakeMessage, object())
def test_message_contains():

56
tests/test_compat.py Normal file
View File

@ -0,0 +1,56 @@
from typing import Any
from dataclasses import dataclass
import pytest
from nonebot.compat import (
DEFAULT_CONFIG,
Required,
FieldInfo,
PydanticUndefined,
custom_validation,
type_validate_python,
)
@pytest.mark.asyncio
async def test_default_config():
assert DEFAULT_CONFIG.get("extra") == "allow"
assert DEFAULT_CONFIG.get("arbitrary_types_allowed") is True
@pytest.mark.asyncio
async def test_field_info():
# required should be convert to PydanticUndefined
assert FieldInfo(Required).default is PydanticUndefined
# field info should allow extra attributes
assert FieldInfo(test="test").extra["test"] == "test"
@pytest.mark.asyncio
async def test_custom_validation():
called = []
@custom_validation
@dataclass
class TestModel:
test: int
@classmethod
def __get_validators__(cls):
yield cls._validate_1
yield cls._validate_2
@classmethod
def _validate_1(cls, v: Any) -> Any:
called.append(1)
return v
@classmethod
def _validate_2(cls, v: Any) -> Any:
called.append(2)
return cls(test=v["test"])
assert type_validate_python(TestModel, {"test": 1}) == TestModel(test=1)
assert called == [1, 2]

View File

@ -218,7 +218,7 @@ async def test_event(app: App):
async with app.test_dependent(union_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_fooevent)
ctx.should_return(fake_event)
ctx.should_return(fake_fooevent)
async with app.test_dependent(generic_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_event)
@ -529,13 +529,17 @@ async def test_arg(app: App):
@pytest.mark.asyncio
async def test_exception(app: App):
from plugins.param.param_exception import exc
from plugins.param.param_exception import exc, legacy_exc
exception = ValueError("test")
async with app.test_dependent(exc, allow_types=[ExceptionParam]) as ctx:
ctx.pass_params(exception=exception)
ctx.should_return(exception)
async with app.test_dependent(legacy_exc, allow_types=[ExceptionParam]) as ctx:
ctx.pass_params(exception=exception)
ctx.should_return(exception)
@pytest.mark.asyncio
async def test_default(app: App):