From 8f7aaf2b21e28fb763b4c3f35b21e15c608478d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=B5=E5=91=B5=E3=81=A7=E3=81=99?= <51957264+shoucandanghehe@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:50:00 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20Feature:=20=E6=94=BE=E5=AE=BD=20py?= =?UTF-8?q?dantic=20compat=20model=20dump=20=E7=B1=BB=E5=9E=8B=20(#3898)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nonebot/compat.py | 29 +++++++++++++++++++++-------- pyproject.toml | 2 +- tests/test_compat.py | 33 +++++++++++++++++++++++++++++++-- uv.lock | 10 +++++----- 4 files changed, 58 insertions(+), 16 deletions(-) diff --git a/nonebot/compat.py b/nonebot/compat.py index 63b096f1..7875d299 100644 --- a/nonebot/compat.py +++ b/nonebot/compat.py @@ -19,7 +19,9 @@ from typing import ( Generic, Literal, Protocol, + TypeAlias, TypeVar, + cast, get_args, get_origin, overload, @@ -44,6 +46,16 @@ if TYPE_CHECKING: CVC = TypeVar("CVC", bound=_CustomValidationClass) +ModelDumpIncEx: TypeAlias = ( + set[int] + | set[str] + | dict[int, "ModelDumpIncEx"] + | dict[str, "ModelDumpIncEx"] + | None +) +"""Common include/exclude shape accepted by all supported pydantic versions.""" + + __all__ = ( "DEFAULT_CONFIG", "PYDANTIC_V2", @@ -230,16 +242,17 @@ if PYDANTIC_V2: # pragma: pydantic-v2 def model_dump( model: BaseModel, - include: set[str] | None = None, - exclude: set[str] | None = None, + include: ModelDumpIncEx = None, + exclude: ModelDumpIncEx = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, ) -> dict[str, Any]: return model.model_dump( - include=include, - exclude=exclude, + # Nested types cannot be inferred correctly + include=cast(Any, include), + exclude=cast(Any, exclude), by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, @@ -457,16 +470,16 @@ else: # pragma: pydantic-v1 def model_dump( model: BaseModel, - include: set[str] | None = None, - exclude: set[str] | None = None, + include: ModelDumpIncEx = None, + exclude: ModelDumpIncEx = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, ) -> dict[str, Any]: return model.dict( - include=include, - exclude=exclude, + include=cast(Any, include), + exclude=cast(Any, exclude), by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, diff --git a/pyproject.toml b/pyproject.toml index b039baf4..ee4e2d08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ test = [ "pytest-xdist >=3.0.2, <4.0.0", "coverage-conditional-plugin >=0.9.0, <0.10.0", ] -docs = ["nb-autodoc >=1.0.3, <2.0.0"] +docs = ["nb-autodoc >=1.0.4, <2.0.0"] pydantic-v1 = ["pydantic >=1.10.0, <2.0.0"] pydantic-v2 = ["pydantic >=2.0.0, <3.0.0"] diff --git a/tests/test_compat.py b/tests/test_compat.py index bdcb63b2..85c90362 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -73,12 +73,41 @@ def test_type_adapter(): def test_model_dump(): + class NestedModel(BaseModel): + hidden: int + shown: int + class TestModel(BaseModel): test1: int test2: int + nested: NestedModel + items: list[NestedModel] - assert model_dump(TestModel(test1=1, test2=2), include={"test1"}) == {"test1": 1} - assert model_dump(TestModel(test1=1, test2=2), exclude={"test1"}) == {"test2": 2} + model = TestModel( + test1=1, + test2=2, + nested=NestedModel(hidden=3, shown=4), + items=[NestedModel(hidden=5, shown=6)], + ) + + assert model_dump(model, include={"test1"}) == {"test1": 1} + assert model_dump(model, exclude={"test1"}) == { + "test2": 2, + "nested": {"hidden": 3, "shown": 4}, + "items": [{"hidden": 5, "shown": 6}], + } + assert model_dump(model, exclude={"nested": {"hidden"}}) == { + "test1": 1, + "test2": 2, + "nested": {"shown": 4}, + "items": [{"hidden": 5, "shown": 6}], + } + assert model_dump(model, exclude={"items": {"__all__": {"hidden"}}}) == { + "test1": 1, + "test2": 2, + "nested": {"hidden": 3, "shown": 4}, + "items": [{"shown": 6}], + } def test_model_validator(): diff --git a/uv.lock b/uv.lock index 41e580bd..d8977876 100644 --- a/uv.lock +++ b/uv.lock @@ -1391,15 +1391,15 @@ wheels = [ [[package]] name = "nb-autodoc" -version = "1.0.3" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/b7/46978e3423b8311e1599967c4b91a6c5233b9e2d853aa99a8aef4de4b8fa/nb_autodoc-1.0.3.tar.gz", hash = "sha256:8bb90a20820280adf12a0aad1b94603e9f0c65750d81af8378c6dd6cfb0be400", size = 62703, upload-time = "2025-12-01T08:52:16.891Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/36/07ffb6434e5a8554f89a5e1f405da67df8399b2f18a221199a3012e10010/nb_autodoc-1.0.4.tar.gz", hash = "sha256:57566cc074aa46de5d367bc6406364ade87956f7d0e6e30ce5fc2af64af912a0", size = 62982, upload-time = "2026-03-15T11:18:07.647Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/70/ce9c943da109599649efed7ca1d8e549515f60de9c8a046e93fe17b10ad4/nb_autodoc-1.0.3-py3-none-any.whl", hash = "sha256:6b5d1d244ee5c5ae896e0c6683193df64e47497986beceaeed2a76ae7235c06b", size = 53029, upload-time = "2025-12-01T08:52:15.301Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/8c46be71fab80e7fee5b474eccf2efd234d5c72022a834807d54766199ba/nb_autodoc-1.0.4-py3-none-any.whl", hash = "sha256:68a75ee461dfc895efd5c31ced653dc7de1983605f65e701541d0eb172290032", size = 53035, upload-time = "2026-03-15T11:18:06.26Z" }, ] [[package]] @@ -1516,7 +1516,7 @@ provides-extras = ["websockets", "httpx", "aiohttp", "quart", "fastapi", "all"] [package.metadata.requires-dev] dev = [ { name = "coverage-conditional-plugin", specifier = ">=0.9.0,<0.10.0" }, - { name = "nb-autodoc", specifier = ">=1.0.3,<2.0.0" }, + { name = "nb-autodoc", specifier = ">=1.0.4,<2.0.0" }, { name = "nonebug", specifier = ">=0.4.1,<0.5.0" }, { name = "nonemoji", specifier = ">=0.1.2,<0.2.0" }, { name = "pre-commit", specifier = ">=4.0.0,<5.0.0" }, @@ -1527,7 +1527,7 @@ dev = [ { name = "werkzeug", specifier = ">=2.3.6,<4.0.0" }, { name = "wsproto", specifier = ">=1.2.0,<2.0.0" }, ] -docs = [{ name = "nb-autodoc", specifier = ">=1.0.3,<2.0.0" }] +docs = [{ name = "nb-autodoc", specifier = ">=1.0.4,<2.0.0" }] pydantic-v1 = [{ name = "pydantic", specifier = ">=1.10.0,<2.0.0" }] pydantic-v2 = [{ name = "pydantic", specifier = ">=2.0.0,<3.0.0" }] test = [