From 84d43f15e163d5217efc8526029e0108852bb6ab Mon Sep 17 00:00:00 2001 From: sydney-runkle Date: Tue, 4 Mar 2025 11:32:01 -0500 Subject: [PATCH 1/4] skip reusing wrap validators --- src/common/prebuilt.rs | 4 ++-- src/serializers/prebuilt.rs | 8 +++++--- src/validators/prebuilt.rs | 8 +++++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/common/prebuilt.rs b/src/common/prebuilt.rs index f4307064f..e66e2c35b 100644 --- a/src/common/prebuilt.rs +++ b/src/common/prebuilt.rs @@ -8,7 +8,7 @@ pub fn get_prebuilt( type_: &str, schema: &Bound<'_, PyDict>, prebuilt_attr_name: &str, - extractor: impl FnOnce(Bound<'_, PyAny>) -> PyResult, + extractor: impl FnOnce(Bound<'_, PyAny>) -> PyResult>, ) -> PyResult> { let py = schema.py(); @@ -40,5 +40,5 @@ pub fn get_prebuilt( // Retrieve the prebuilt validator / serializer if available let prebuilt: Bound<'_, PyAny> = class_dict.get_item(prebuilt_attr_name)?; - extractor(prebuilt).map(Some) + extractor(prebuilt) } diff --git a/src/serializers/prebuilt.rs b/src/serializers/prebuilt.rs index 33d197d9b..3f1cf1c68 100644 --- a/src/serializers/prebuilt.rs +++ b/src/serializers/prebuilt.rs @@ -17,9 +17,11 @@ pub struct PrebuiltSerializer { impl PrebuiltSerializer { pub fn try_get_from_schema(type_: &str, schema: &Bound<'_, PyDict>) -> PyResult> { get_prebuilt(type_, schema, "__pydantic_serializer__", |py_any| { - py_any - .extract::>() - .map(|schema_serializer| Self { schema_serializer }.into()) + let schema_serializer = py_any.extract::>()?; + if matches!(schema_serializer.get().serializer, CombinedSerializer::FunctionWrap(_)) { + return Ok(None); + } + Ok(Some(Self { schema_serializer }.into())) }) } } diff --git a/src/validators/prebuilt.rs b/src/validators/prebuilt.rs index c17acb9f9..54c40f197 100644 --- a/src/validators/prebuilt.rs +++ b/src/validators/prebuilt.rs @@ -16,9 +16,11 @@ pub struct PrebuiltValidator { impl PrebuiltValidator { pub fn try_get_from_schema(type_: &str, schema: &Bound<'_, PyDict>) -> PyResult> { get_prebuilt(type_, schema, "__pydantic_validator__", |py_any| { - py_any - .extract::>() - .map(|schema_validator| Self { schema_validator }.into()) + let schema_validator = py_any.extract::>()?; + if matches!(schema_validator.get().validator, CombinedValidator::FunctionWrap(_)) { + return Ok(None); + } + Ok(Some(Self { schema_validator }.into())) }) } } From 2faf807c7cc6d8ea3b0dbb47f6944f7c09ebc816 Mon Sep 17 00:00:00 2001 From: sydney-runkle Date: Tue, 4 Mar 2025 12:00:27 -0500 Subject: [PATCH 2/4] writing tests, ofc --- tests/test_prebuilt.py | 120 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/tests/test_prebuilt.py b/tests/test_prebuilt.py index 9cd5aa325..1f075ecda 100644 --- a/tests/test_prebuilt.py +++ b/tests/test_prebuilt.py @@ -46,3 +46,123 @@ class OuterModel: result = outer_validator.validate_python({'inner': {'x': 1}}) assert result.inner.x == 1 assert outer_serializer.to_python(result) == {'inner': {'x': 1}} + + +def test_prebuilt_not_used_for_wrap_serializer_functions() -> None: + class InnerModel: + x: str + + def __init__(self, x: str) -> None: + self.x = x + + def serialize_inner(v: InnerModel, serializer) -> str: + v.x = v.x + ' modified' + return serializer(v) + + inner_schema = core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + serialization=core_schema.wrap_serializer_function_ser_schema(serialize_inner), + ) + + inner_schema_serializer = SchemaSerializer(inner_schema) + InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue] + InnerModel.__pydantic_serializer__ = inner_schema_serializer # pyright: ignore[reportAttributeAccessIssue] + + class OuterModel: + inner: InnerModel + + def __init__(self, inner: InnerModel) -> None: + self.inner = inner + + outer_schema = core_schema.model_schema( + OuterModel, + schema=core_schema.model_fields_schema( + { + 'inner': core_schema.model_field( + schema=core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + # note, we use a simple str schema (with no custom serialization) + # in order to verify that the prebuilt serializer from InnerModel is not used + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + ) + ) + } + ), + ) + + inner_serializer = SchemaSerializer(inner_schema) + outer_serializer = SchemaSerializer(outer_schema) + + # the custom serialization function does apply for the inner model + inner_instance = InnerModel(x='hello') + assert inner_serializer.to_python(inner_instance) == {'x': 'hello modified'} + + # but the outer model doesn't reuse the custom wrap serializer function, so we see simple str ser + outer_instance = OuterModel(inner=InnerModel(x='hello')) + assert outer_serializer.to_python(outer_instance) == {'inner': {'x': 'hello'}} + + +def test_prebuilt_not_used_for_wrap_validator_functions() -> None: + class InnerModel: + x: str + + def __init__(self, x: str) -> None: + self.x = x + + def validate_inner(data, validator) -> str: + data['x'] = data['x'] + ' modified' + return validator(data) + + inner_schema = core_schema.no_info_wrap_validator_function( + validate_inner, + core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + ), + ) + + inner_schema_validator = SchemaValidator(inner_schema) + InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue] + InnerModel.__pydantic_validator__ = inner_schema_validator # pyright: ignore[reportAttributeAccessIssue] + + class OuterModel: + inner: InnerModel + + def __init__(self, inner: InnerModel) -> None: + self.inner = inner + + outer_schema = core_schema.model_schema( + OuterModel, + schema=core_schema.model_fields_schema( + { + 'inner': core_schema.model_field( + schema=core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + # note, we use a simple str schema (with no custom validation) + # in order to verify that the prebuilt validator from InnerModel is not used + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + ) + ) + } + ), + ) + + inner_validator = SchemaValidator(inner_schema) + outer_validator = SchemaValidator(outer_schema) + + # the custom validation function does apply for the inner model + result_inner = inner_validator.validate_python({'x': 'hello'}) + assert result_inner.x == 'hello modified' + + # but the outer model doesn't reuse the custom wrap validator function, so we see simple str val + result_outer = outer_validator.validate_python({'inner': {'x': 'hello'}}) + assert result_outer.inner.x == 'hello' From 6efdfc932b2c7984555085074c9b630df9be79ff Mon Sep 17 00:00:00 2001 From: sydney-runkle Date: Tue, 4 Mar 2025 14:16:28 -0500 Subject: [PATCH 3/4] add plain validator ok tests --- tests/test_prebuilt.py | 119 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 2 deletions(-) diff --git a/tests/test_prebuilt.py b/tests/test_prebuilt.py index 1f075ecda..d8340dac3 100644 --- a/tests/test_prebuilt.py +++ b/tests/test_prebuilt.py @@ -55,7 +55,7 @@ class InnerModel: def __init__(self, x: str) -> None: self.x = x - def serialize_inner(v: InnerModel, serializer) -> str: + def serialize_inner(v: InnerModel, serializer) -> dict[str, str] | str: v.x = v.x + ' modified' return serializer(v) @@ -114,7 +114,7 @@ class InnerModel: def __init__(self, x: str) -> None: self.x = x - def validate_inner(data, validator) -> str: + def validate_inner(data, validator) -> InnerModel: data['x'] = data['x'] + ' modified' return validator(data) @@ -166,3 +166,118 @@ def __init__(self, inner: InnerModel) -> None: # but the outer model doesn't reuse the custom wrap validator function, so we see simple str val result_outer = outer_validator.validate_python({'inner': {'x': 'hello'}}) assert result_outer.inner.x == 'hello' + + +def test_reuse_plain_serializer_ok() -> None: + class InnerModel: + x: str + + def __init__(self, x: str) -> None: + self.x = x + + def serialize_inner(v: InnerModel) -> str: + return v.x + ' modified' + + inner_schema = core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + serialization=core_schema.plain_serializer_function_ser_schema(serialize_inner), + ) + + inner_schema_serializer = SchemaSerializer(inner_schema) + InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue] + InnerModel.__pydantic_serializer__ = inner_schema_serializer # pyright: ignore[reportAttributeAccessIssue] + + class OuterModel: + inner: InnerModel + + def __init__(self, inner: InnerModel) -> None: + self.inner = inner + + outer_schema = core_schema.model_schema( + OuterModel, + schema=core_schema.model_fields_schema( + { + 'inner': core_schema.model_field( + schema=core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + # note, we use a simple str schema (with no custom serialization) + # in order to verify that the prebuilt serializer from InnerModel is used instead + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + ) + ) + } + ), + ) + + inner_serializer = SchemaSerializer(inner_schema) + outer_serializer = SchemaSerializer(outer_schema) + + # the custom serialization function does apply for the inner model + inner_instance = InnerModel(x='hello') + assert inner_serializer.to_python(inner_instance) == 'hello modified' + assert 'FunctionPlainSerializer' in repr(inner_serializer) + + # the custom ser function applies for the outer model as well, a plain serializer is permitted as a prebuilt candidate + outer_instance = OuterModel(inner=InnerModel(x='hello')) + assert outer_serializer.to_python(outer_instance) == {'inner': 'hello modified'} + assert 'PrebuiltSerializer' in repr(outer_serializer) + + +def test_reuse_plain_validator_ok() -> None: + class InnerModel: + x: str + + def __init__(self, x: str) -> None: + self.x = x + + def validate_inner(data) -> InnerModel: + data['x'] = data['x'] + ' modified' + return InnerModel(**data) + + inner_schema = core_schema.no_info_plain_validator_function(validate_inner) + + inner_schema_validator = SchemaValidator(inner_schema) + InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue] + InnerModel.__pydantic_validator__ = inner_schema_validator # pyright: ignore[reportAttributeAccessIssue] + + class OuterModel: + inner: InnerModel + + def __init__(self, inner: InnerModel) -> None: + self.inner = inner + + outer_schema = core_schema.model_schema( + OuterModel, + schema=core_schema.model_fields_schema( + { + 'inner': core_schema.model_field( + schema=core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + # note, we use a simple str schema (with no custom validation) + # in order to verify that the prebuilt validator from InnerModel is used instead + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + ) + ) + } + ), + ) + + inner_validator = SchemaValidator(inner_schema) + outer_validator = SchemaValidator(outer_schema) + + # the custom validation function does apply for the inner model + result_inner = inner_validator.validate_python({'x': 'hello'}) + assert result_inner.x == 'hello modified' + assert 'FunctionPlainValidator' in repr(inner_validator) + + # the custom validation function does apply for the outer model as well, a plain validator is permitted as a prebuilt candidate + result_outer = outer_validator.validate_python({'inner': {'x': 'hello'}}) + assert result_outer.inner.x == 'hello modified' + assert 'PrebuiltValidator' in repr(outer_validator) From ff93f1a3cd98f05a28c7e04ba0e413cb928ce057 Mon Sep 17 00:00:00 2001 From: sydney-runkle Date: Tue, 4 Mar 2025 14:38:54 -0500 Subject: [PATCH 4/4] fix 3.9 test with union syntax --- tests/test_prebuilt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_prebuilt.py b/tests/test_prebuilt.py index d8340dac3..14a9f288b 100644 --- a/tests/test_prebuilt.py +++ b/tests/test_prebuilt.py @@ -1,3 +1,5 @@ +from typing import Union + from pydantic_core import SchemaSerializer, SchemaValidator, core_schema @@ -55,7 +57,7 @@ class InnerModel: def __init__(self, x: str) -> None: self.x = x - def serialize_inner(v: InnerModel, serializer) -> dict[str, str] | str: + def serialize_inner(v: InnerModel, serializer) -> Union[dict[str, str], str]: v.x = v.x + ' modified' return serializer(v)