From 318fe54832aef75da3089a61d0d9a3467d22e597 Mon Sep 17 00:00:00 2001 From: Camillo Date: Mon, 10 Jun 2024 01:43:56 -0700 Subject: [PATCH] Fix add_field_serializer_for_reverse_relations clearing validators (#1302) * Fix add_field_serializer_for_reverse_relations clearing validators * add test to check that validators are not removed * compatibility with old python * fix test default values * fix coverage and cleanup --------- Co-authored-by: collerek --- ormar/models/helpers/relations.py | 21 +++++--- .../test_relations_with_nested_defaults.py | 20 +++++++- ...st_reverse_relation_preserves_validator.py | 48 +++++++++++++++++++ 3 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 tests/test_relations/test_reverse_relation_preserves_validator.py diff --git a/ormar/models/helpers/relations.py b/ormar/models/helpers/relations.py index 823efc6..1525580 100644 --- a/ormar/models/helpers/relations.py +++ b/ormar/models/helpers/relations.py @@ -5,9 +5,7 @@ from typing import TYPE_CHECKING, Any, List, Optional, Type, Union, cast from pydantic import BaseModel, create_model, field_serializer from pydantic._internal._decorators import DecoratorInfos from pydantic.fields import FieldInfo -from pydantic_core.core_schema import ( - SerializerFunctionWrapHandler, -) +from pydantic_core.core_schema import SerializerFunctionWrapHandler import ormar from ormar import ForeignKey, ManyToMany @@ -174,8 +172,8 @@ def add_field_serializer_for_reverse_relations( "ignore", message="Pydantic serializer warnings" ) return handler(children) - except ValueError as exc: - if not str(exc).startswith("Circular reference"): # pragma: no cover + except ValueError as exc: # pragma: no cover + if not str(exc).startswith("Circular reference"): raise exc result = [] @@ -188,7 +186,18 @@ def add_field_serializer_for_reverse_relations( serialize ) setattr(to_model, f"serialize_{related_name}", decorator) - DecoratorInfos.build(to_model) + # DecoratorInfos.build will overwrite __pydantic_decorators__ on to_model, + # deleting the previous decorators. We need to save them and then merge them. + prev_decorators = getattr(to_model, "__pydantic_decorators__", DecoratorInfos()) + new_decorators = DecoratorInfos.build(to_model) + prev_decorators.validators.update(new_decorators.validators) + prev_decorators.field_validators.update(new_decorators.field_validators) + prev_decorators.root_validators.update(new_decorators.root_validators) + prev_decorators.field_serializers.update(new_decorators.field_serializers) + prev_decorators.model_serializers.update(new_decorators.model_serializers) + prev_decorators.model_validators.update(new_decorators.model_validators) + prev_decorators.computed_fields.update(new_decorators.computed_fields) + setattr(to_model, "__pydantic_decorators__", prev_decorators) def replace_models_with_copy( diff --git a/tests/test_fastapi/test_relations_with_nested_defaults.py b/tests/test_fastapi/test_relations_with_nested_defaults.py index ebbb6c7..ae0eedf 100644 --- a/tests/test_fastapi/test_relations_with_nested_defaults.py +++ b/tests/test_fastapi/test_relations_with_nested_defaults.py @@ -97,7 +97,25 @@ async def test_related_with_defaults(sample_data): "year": 2021, } ], - "country": {"authors": [{"id": 1}], "id": 1}, + "country": { + "authors": [ + { + "books": [ + { + "author": {"id": 1}, + "id": 1, + "title": "Bug caused by " "default value", + "year": 2021, + } + ], + "country": {"id": 1}, + "id": 1, + "name": "bug", + "rating": 5, + } + ], + "id": 1, + }, "id": 1, "name": "bug", "rating": 5, diff --git a/tests/test_relations/test_reverse_relation_preserves_validator.py b/tests/test_relations/test_reverse_relation_preserves_validator.py new file mode 100644 index 0000000..64d181b --- /dev/null +++ b/tests/test_relations/test_reverse_relation_preserves_validator.py @@ -0,0 +1,48 @@ +from typing import List, Optional, Union + +import ormar +import pytest_asyncio +from pydantic import field_validator + +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() + + +class Author(ormar.Model): + ormar_config = base_ormar_config.copy() + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=80) + + @field_validator("name", mode="before") + @classmethod + def validate_name(cls, v: Union[str, List[str]]) -> str: + if isinstance(v, list): + v = " ".join(v) + return v + + +class Post(ormar.Model): + ormar_config = base_ormar_config.copy() + + id: int = ormar.Integer(primary_key=True) + title: str = ormar.String(max_length=200) + author: Optional[Author] = ormar.ForeignKey(Author) + + +create_test_database = init_tests(base_ormar_config) + + +@pytest_asyncio.fixture(scope="function", autouse=True) +async def cleanup(): + yield + async with base_ormar_config.database: + await Post.objects.delete(each=True) + await Author.objects.delete(each=True) + + +def test_validator(): + author = Author(name=["Test", "Author"]) + assert author.name == "Test Author"