Skip to content

Commit 72350a1

Browse files
Skip reusing wrap validators / serializers for prebuilt variants (#1660)
1 parent 66c8c58 commit 72350a1

File tree

4 files changed

+249
-8
lines changed

4 files changed

+249
-8
lines changed

src/common/prebuilt.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ pub fn get_prebuilt<T>(
88
type_: &str,
99
schema: &Bound<'_, PyDict>,
1010
prebuilt_attr_name: &str,
11-
extractor: impl FnOnce(Bound<'_, PyAny>) -> PyResult<T>,
11+
extractor: impl FnOnce(Bound<'_, PyAny>) -> PyResult<Option<T>>,
1212
) -> PyResult<Option<T>> {
1313
let py = schema.py();
1414

@@ -40,5 +40,5 @@ pub fn get_prebuilt<T>(
4040

4141
// Retrieve the prebuilt validator / serializer if available
4242
let prebuilt: Bound<'_, PyAny> = class_dict.get_item(prebuilt_attr_name)?;
43-
extractor(prebuilt).map(Some)
43+
extractor(prebuilt)
4444
}

src/serializers/prebuilt.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ pub struct PrebuiltSerializer {
1717
impl PrebuiltSerializer {
1818
pub fn try_get_from_schema(type_: &str, schema: &Bound<'_, PyDict>) -> PyResult<Option<CombinedSerializer>> {
1919
get_prebuilt(type_, schema, "__pydantic_serializer__", |py_any| {
20-
py_any
21-
.extract::<Py<SchemaSerializer>>()
22-
.map(|schema_serializer| Self { schema_serializer }.into())
20+
let schema_serializer = py_any.extract::<Py<SchemaSerializer>>()?;
21+
if matches!(schema_serializer.get().serializer, CombinedSerializer::FunctionWrap(_)) {
22+
return Ok(None);
23+
}
24+
Ok(Some(Self { schema_serializer }.into()))
2325
})
2426
}
2527
}

src/validators/prebuilt.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ pub struct PrebuiltValidator {
1616
impl PrebuiltValidator {
1717
pub fn try_get_from_schema(type_: &str, schema: &Bound<'_, PyDict>) -> PyResult<Option<CombinedValidator>> {
1818
get_prebuilt(type_, schema, "__pydantic_validator__", |py_any| {
19-
py_any
20-
.extract::<Py<SchemaValidator>>()
21-
.map(|schema_validator| Self { schema_validator }.into())
19+
let schema_validator = py_any.extract::<Py<SchemaValidator>>()?;
20+
if matches!(schema_validator.get().validator, CombinedValidator::FunctionWrap(_)) {
21+
return Ok(None);
22+
}
23+
Ok(Some(Self { schema_validator }.into()))
2224
})
2325
}
2426
}

tests/test_prebuilt.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Union
2+
13
from pydantic_core import SchemaSerializer, SchemaValidator, core_schema
24

35

@@ -46,3 +48,238 @@ class OuterModel:
4648
result = outer_validator.validate_python({'inner': {'x': 1}})
4749
assert result.inner.x == 1
4850
assert outer_serializer.to_python(result) == {'inner': {'x': 1}}
51+
52+
53+
def test_prebuilt_not_used_for_wrap_serializer_functions() -> None:
54+
class InnerModel:
55+
x: str
56+
57+
def __init__(self, x: str) -> None:
58+
self.x = x
59+
60+
def serialize_inner(v: InnerModel, serializer) -> Union[dict[str, str], str]:
61+
v.x = v.x + ' modified'
62+
return serializer(v)
63+
64+
inner_schema = core_schema.model_schema(
65+
InnerModel,
66+
schema=core_schema.model_fields_schema(
67+
{'x': core_schema.model_field(schema=core_schema.str_schema())},
68+
),
69+
serialization=core_schema.wrap_serializer_function_ser_schema(serialize_inner),
70+
)
71+
72+
inner_schema_serializer = SchemaSerializer(inner_schema)
73+
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
74+
InnerModel.__pydantic_serializer__ = inner_schema_serializer # pyright: ignore[reportAttributeAccessIssue]
75+
76+
class OuterModel:
77+
inner: InnerModel
78+
79+
def __init__(self, inner: InnerModel) -> None:
80+
self.inner = inner
81+
82+
outer_schema = core_schema.model_schema(
83+
OuterModel,
84+
schema=core_schema.model_fields_schema(
85+
{
86+
'inner': core_schema.model_field(
87+
schema=core_schema.model_schema(
88+
InnerModel,
89+
schema=core_schema.model_fields_schema(
90+
# note, we use a simple str schema (with no custom serialization)
91+
# in order to verify that the prebuilt serializer from InnerModel is not used
92+
{'x': core_schema.model_field(schema=core_schema.str_schema())},
93+
),
94+
)
95+
)
96+
}
97+
),
98+
)
99+
100+
inner_serializer = SchemaSerializer(inner_schema)
101+
outer_serializer = SchemaSerializer(outer_schema)
102+
103+
# the custom serialization function does apply for the inner model
104+
inner_instance = InnerModel(x='hello')
105+
assert inner_serializer.to_python(inner_instance) == {'x': 'hello modified'}
106+
107+
# but the outer model doesn't reuse the custom wrap serializer function, so we see simple str ser
108+
outer_instance = OuterModel(inner=InnerModel(x='hello'))
109+
assert outer_serializer.to_python(outer_instance) == {'inner': {'x': 'hello'}}
110+
111+
112+
def test_prebuilt_not_used_for_wrap_validator_functions() -> None:
113+
class InnerModel:
114+
x: str
115+
116+
def __init__(self, x: str) -> None:
117+
self.x = x
118+
119+
def validate_inner(data, validator) -> InnerModel:
120+
data['x'] = data['x'] + ' modified'
121+
return validator(data)
122+
123+
inner_schema = core_schema.no_info_wrap_validator_function(
124+
validate_inner,
125+
core_schema.model_schema(
126+
InnerModel,
127+
schema=core_schema.model_fields_schema(
128+
{'x': core_schema.model_field(schema=core_schema.str_schema())},
129+
),
130+
),
131+
)
132+
133+
inner_schema_validator = SchemaValidator(inner_schema)
134+
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
135+
InnerModel.__pydantic_validator__ = inner_schema_validator # pyright: ignore[reportAttributeAccessIssue]
136+
137+
class OuterModel:
138+
inner: InnerModel
139+
140+
def __init__(self, inner: InnerModel) -> None:
141+
self.inner = inner
142+
143+
outer_schema = core_schema.model_schema(
144+
OuterModel,
145+
schema=core_schema.model_fields_schema(
146+
{
147+
'inner': core_schema.model_field(
148+
schema=core_schema.model_schema(
149+
InnerModel,
150+
schema=core_schema.model_fields_schema(
151+
# note, we use a simple str schema (with no custom validation)
152+
# in order to verify that the prebuilt validator from InnerModel is not used
153+
{'x': core_schema.model_field(schema=core_schema.str_schema())},
154+
),
155+
)
156+
)
157+
}
158+
),
159+
)
160+
161+
inner_validator = SchemaValidator(inner_schema)
162+
outer_validator = SchemaValidator(outer_schema)
163+
164+
# the custom validation function does apply for the inner model
165+
result_inner = inner_validator.validate_python({'x': 'hello'})
166+
assert result_inner.x == 'hello modified'
167+
168+
# but the outer model doesn't reuse the custom wrap validator function, so we see simple str val
169+
result_outer = outer_validator.validate_python({'inner': {'x': 'hello'}})
170+
assert result_outer.inner.x == 'hello'
171+
172+
173+
def test_reuse_plain_serializer_ok() -> None:
174+
class InnerModel:
175+
x: str
176+
177+
def __init__(self, x: str) -> None:
178+
self.x = x
179+
180+
def serialize_inner(v: InnerModel) -> str:
181+
return v.x + ' modified'
182+
183+
inner_schema = core_schema.model_schema(
184+
InnerModel,
185+
schema=core_schema.model_fields_schema(
186+
{'x': core_schema.model_field(schema=core_schema.str_schema())},
187+
),
188+
serialization=core_schema.plain_serializer_function_ser_schema(serialize_inner),
189+
)
190+
191+
inner_schema_serializer = SchemaSerializer(inner_schema)
192+
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
193+
InnerModel.__pydantic_serializer__ = inner_schema_serializer # pyright: ignore[reportAttributeAccessIssue]
194+
195+
class OuterModel:
196+
inner: InnerModel
197+
198+
def __init__(self, inner: InnerModel) -> None:
199+
self.inner = inner
200+
201+
outer_schema = core_schema.model_schema(
202+
OuterModel,
203+
schema=core_schema.model_fields_schema(
204+
{
205+
'inner': core_schema.model_field(
206+
schema=core_schema.model_schema(
207+
InnerModel,
208+
schema=core_schema.model_fields_schema(
209+
# note, we use a simple str schema (with no custom serialization)
210+
# in order to verify that the prebuilt serializer from InnerModel is used instead
211+
{'x': core_schema.model_field(schema=core_schema.str_schema())},
212+
),
213+
)
214+
)
215+
}
216+
),
217+
)
218+
219+
inner_serializer = SchemaSerializer(inner_schema)
220+
outer_serializer = SchemaSerializer(outer_schema)
221+
222+
# the custom serialization function does apply for the inner model
223+
inner_instance = InnerModel(x='hello')
224+
assert inner_serializer.to_python(inner_instance) == 'hello modified'
225+
assert 'FunctionPlainSerializer' in repr(inner_serializer)
226+
227+
# the custom ser function applies for the outer model as well, a plain serializer is permitted as a prebuilt candidate
228+
outer_instance = OuterModel(inner=InnerModel(x='hello'))
229+
assert outer_serializer.to_python(outer_instance) == {'inner': 'hello modified'}
230+
assert 'PrebuiltSerializer' in repr(outer_serializer)
231+
232+
233+
def test_reuse_plain_validator_ok() -> None:
234+
class InnerModel:
235+
x: str
236+
237+
def __init__(self, x: str) -> None:
238+
self.x = x
239+
240+
def validate_inner(data) -> InnerModel:
241+
data['x'] = data['x'] + ' modified'
242+
return InnerModel(**data)
243+
244+
inner_schema = core_schema.no_info_plain_validator_function(validate_inner)
245+
246+
inner_schema_validator = SchemaValidator(inner_schema)
247+
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
248+
InnerModel.__pydantic_validator__ = inner_schema_validator # pyright: ignore[reportAttributeAccessIssue]
249+
250+
class OuterModel:
251+
inner: InnerModel
252+
253+
def __init__(self, inner: InnerModel) -> None:
254+
self.inner = inner
255+
256+
outer_schema = core_schema.model_schema(
257+
OuterModel,
258+
schema=core_schema.model_fields_schema(
259+
{
260+
'inner': core_schema.model_field(
261+
schema=core_schema.model_schema(
262+
InnerModel,
263+
schema=core_schema.model_fields_schema(
264+
# note, we use a simple str schema (with no custom validation)
265+
# in order to verify that the prebuilt validator from InnerModel is used instead
266+
{'x': core_schema.model_field(schema=core_schema.str_schema())},
267+
),
268+
)
269+
)
270+
}
271+
),
272+
)
273+
274+
inner_validator = SchemaValidator(inner_schema)
275+
outer_validator = SchemaValidator(outer_schema)
276+
277+
# the custom validation function does apply for the inner model
278+
result_inner = inner_validator.validate_python({'x': 'hello'})
279+
assert result_inner.x == 'hello modified'
280+
assert 'FunctionPlainValidator' in repr(inner_validator)
281+
282+
# the custom validation function does apply for the outer model as well, a plain validator is permitted as a prebuilt candidate
283+
result_outer = outer_validator.validate_python({'inner': {'x': 'hello'}})
284+
assert result_outer.inner.x == 'hello modified'
285+
assert 'PrebuiltValidator' in repr(outer_validator)

0 commit comments

Comments
 (0)