Skip to content

Commit 94d7985

Browse files
Skip reusing after validators in prebuilt schemas (#1663)
1 parent c3be32d commit 94d7985

File tree

2 files changed

+140
-17
lines changed

2 files changed

+140
-17
lines changed

src/validators/prebuilt.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ 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| {
1919
let schema_validator = py_any.extract::<Py<SchemaValidator>>()?;
20-
if matches!(schema_validator.get().validator, CombinedValidator::FunctionWrap(_)) {
20+
if matches!(
21+
schema_validator.get().validator,
22+
CombinedValidator::FunctionWrap(_) | CombinedValidator::FunctionAfter(_)
23+
) {
2124
return Ok(None);
2225
}
2326
Ok(Some(Self { schema_validator }.into()))

tests/test_prebuilt.py

Lines changed: 136 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ class InnerModel:
1414
),
1515
)
1616

17-
inner_schema_validator = SchemaValidator(inner_schema)
18-
inner_schema_serializer = SchemaSerializer(inner_schema)
17+
inner_validator = SchemaValidator(inner_schema)
18+
inner_serializer = SchemaSerializer(inner_schema)
1919
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
20-
InnerModel.__pydantic_validator__ = inner_schema_validator # pyright: ignore[reportAttributeAccessIssue]
21-
InnerModel.__pydantic_serializer__ = inner_schema_serializer # pyright: ignore[reportAttributeAccessIssue]
20+
InnerModel.__pydantic_validator__ = inner_validator # pyright: ignore[reportAttributeAccessIssue]
21+
InnerModel.__pydantic_serializer__ = inner_serializer # pyright: ignore[reportAttributeAccessIssue]
2222

2323
class OuterModel:
2424
inner: InnerModel
@@ -69,9 +69,9 @@ def serialize_inner(v: InnerModel, serializer) -> Union[dict[str, str], str]:
6969
serialization=core_schema.wrap_serializer_function_ser_schema(serialize_inner),
7070
)
7171

72-
inner_schema_serializer = SchemaSerializer(inner_schema)
72+
inner_serializer = SchemaSerializer(inner_schema)
7373
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
74-
InnerModel.__pydantic_serializer__ = inner_schema_serializer # pyright: ignore[reportAttributeAccessIssue]
74+
InnerModel.__pydantic_serializer__ = inner_serializer # pyright: ignore[reportAttributeAccessIssue]
7575

7676
class OuterModel:
7777
inner: InnerModel
@@ -97,7 +97,6 @@ def __init__(self, inner: InnerModel) -> None:
9797
),
9898
)
9999

100-
inner_serializer = SchemaSerializer(inner_schema)
101100
outer_serializer = SchemaSerializer(outer_schema)
102101

103102
# the custom serialization function does apply for the inner model
@@ -130,9 +129,9 @@ def validate_inner(data, validator) -> InnerModel:
130129
),
131130
)
132131

133-
inner_schema_validator = SchemaValidator(inner_schema)
132+
inner_validator = SchemaValidator(inner_schema)
134133
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
135-
InnerModel.__pydantic_validator__ = inner_schema_validator # pyright: ignore[reportAttributeAccessIssue]
134+
InnerModel.__pydantic_validator__ = inner_validator # pyright: ignore[reportAttributeAccessIssue]
136135

137136
class OuterModel:
138137
inner: InnerModel
@@ -158,7 +157,6 @@ def __init__(self, inner: InnerModel) -> None:
158157
),
159158
)
160159

161-
inner_validator = SchemaValidator(inner_schema)
162160
outer_validator = SchemaValidator(outer_schema)
163161

164162
# the custom validation function does apply for the inner model
@@ -170,6 +168,66 @@ def __init__(self, inner: InnerModel) -> None:
170168
assert result_outer.inner.x == 'hello'
171169

172170

171+
def test_prebuilt_not_used_for_after_validator_functions() -> None:
172+
class InnerModel:
173+
x: str
174+
175+
def __init__(self, x: str) -> None:
176+
self.x = x
177+
178+
def validate_after(self) -> InnerModel:
179+
self.x = self.x + ' modified'
180+
return self
181+
182+
inner_schema = core_schema.no_info_after_validator_function(
183+
validate_after,
184+
core_schema.model_schema(
185+
InnerModel,
186+
schema=core_schema.model_fields_schema(
187+
{'x': core_schema.model_field(schema=core_schema.str_schema())},
188+
),
189+
),
190+
)
191+
192+
inner_validator = SchemaValidator(inner_schema)
193+
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
194+
InnerModel.__pydantic_validator__ = inner_validator # pyright: ignore[reportAttributeAccessIssue]
195+
196+
class OuterModel:
197+
inner: InnerModel
198+
199+
def __init__(self, inner: InnerModel) -> None:
200+
self.inner = inner
201+
202+
outer_schema = core_schema.model_schema(
203+
OuterModel,
204+
schema=core_schema.model_fields_schema(
205+
{
206+
'inner': core_schema.model_field(
207+
schema=core_schema.model_schema(
208+
InnerModel,
209+
schema=core_schema.model_fields_schema(
210+
# note, we use a simple str schema (with no custom validation)
211+
# in order to verify that the prebuilt validator from InnerModel is not used
212+
{'x': core_schema.model_field(schema=core_schema.str_schema())},
213+
),
214+
)
215+
)
216+
}
217+
),
218+
)
219+
220+
outer_validator = SchemaValidator(outer_schema)
221+
222+
# the custom validation function does apply for the inner model
223+
result_inner = inner_validator.validate_python({'x': 'hello'})
224+
assert result_inner.x == 'hello modified'
225+
226+
# but the outer model doesn't reuse the custom after validator function, so we see simple str val
227+
result_outer = outer_validator.validate_python({'inner': {'x': 'hello'}})
228+
assert result_outer.inner.x == 'hello'
229+
230+
173231
def test_reuse_plain_serializer_ok() -> None:
174232
class InnerModel:
175233
x: str
@@ -188,9 +246,9 @@ def serialize_inner(v: InnerModel) -> str:
188246
serialization=core_schema.plain_serializer_function_ser_schema(serialize_inner),
189247
)
190248

191-
inner_schema_serializer = SchemaSerializer(inner_schema)
249+
inner_serializer = SchemaSerializer(inner_schema)
192250
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
193-
InnerModel.__pydantic_serializer__ = inner_schema_serializer # pyright: ignore[reportAttributeAccessIssue]
251+
InnerModel.__pydantic_serializer__ = inner_serializer # pyright: ignore[reportAttributeAccessIssue]
194252

195253
class OuterModel:
196254
inner: InnerModel
@@ -216,7 +274,6 @@ def __init__(self, inner: InnerModel) -> None:
216274
),
217275
)
218276

219-
inner_serializer = SchemaSerializer(inner_schema)
220277
outer_serializer = SchemaSerializer(outer_schema)
221278

222279
# the custom serialization function does apply for the inner model
@@ -243,9 +300,9 @@ def validate_inner(data) -> InnerModel:
243300

244301
inner_schema = core_schema.no_info_plain_validator_function(validate_inner)
245302

246-
inner_schema_validator = SchemaValidator(inner_schema)
303+
inner_validator = SchemaValidator(inner_schema)
247304
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
248-
InnerModel.__pydantic_validator__ = inner_schema_validator # pyright: ignore[reportAttributeAccessIssue]
305+
InnerModel.__pydantic_validator__ = inner_validator # pyright: ignore[reportAttributeAccessIssue]
249306

250307
class OuterModel:
251308
inner: InnerModel
@@ -271,7 +328,6 @@ def __init__(self, inner: InnerModel) -> None:
271328
),
272329
)
273330

274-
inner_validator = SchemaValidator(inner_schema)
275331
outer_validator = SchemaValidator(outer_schema)
276332

277333
# the custom validation function does apply for the inner model
@@ -283,3 +339,67 @@ def __init__(self, inner: InnerModel) -> None:
283339
result_outer = outer_validator.validate_python({'inner': {'x': 'hello'}})
284340
assert result_outer.inner.x == 'hello modified'
285341
assert 'PrebuiltValidator' in repr(outer_validator)
342+
343+
344+
def test_reuse_before_validator_ok() -> None:
345+
class InnerModel:
346+
x: str
347+
348+
def __init__(self, x: str) -> None:
349+
self.x = x
350+
351+
def validate_before(data) -> dict:
352+
data['x'] = data['x'] + ' modified'
353+
return data
354+
355+
inner_schema = core_schema.no_info_before_validator_function(
356+
validate_before,
357+
core_schema.model_schema(
358+
InnerModel,
359+
schema=core_schema.model_fields_schema(
360+
{'x': core_schema.model_field(schema=core_schema.str_schema())},
361+
),
362+
),
363+
)
364+
365+
inner_validator = SchemaValidator(inner_schema)
366+
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
367+
InnerModel.__pydantic_validator__ = inner_validator # pyright: ignore[reportAttributeAccessIssue]
368+
369+
class OuterModel:
370+
inner: InnerModel
371+
372+
def __init__(self, inner: InnerModel) -> None:
373+
self.inner = inner
374+
375+
outer_schema = core_schema.model_schema(
376+
OuterModel,
377+
schema=core_schema.model_fields_schema(
378+
{
379+
'inner': core_schema.model_field(
380+
schema=core_schema.model_schema(
381+
InnerModel,
382+
schema=core_schema.model_fields_schema(
383+
# note, we use a simple str schema (with no custom validation)
384+
# in order to verify that the prebuilt validator from InnerModel is used instead
385+
{'x': core_schema.model_field(schema=core_schema.str_schema())},
386+
),
387+
)
388+
)
389+
}
390+
),
391+
)
392+
393+
outer_validator = SchemaValidator(outer_schema)
394+
print(inner_validator)
395+
print(outer_validator)
396+
397+
# the custom validation function does apply for the inner model
398+
result_inner = inner_validator.validate_python({'x': 'hello'})
399+
assert result_inner.x == 'hello modified'
400+
assert 'FunctionBeforeValidator' in repr(inner_validator)
401+
402+
# the custom validation function does apply for the outer model as well, a before validator is permitted as a prebuilt candidate
403+
result_outer = outer_validator.validate_python({'inner': {'x': 'hello'}})
404+
assert result_outer.inner.x == 'hello modified'
405+
assert 'PrebuiltValidator' in repr(outer_validator)

0 commit comments

Comments
 (0)