Skip to content

Commit 53dbdce

Browse files
authored
add support for "init mode" in SchemaValidator (#443)
* add support for "init mode" in SchemaValidator * support pass through on functions * fix benchmarks * switching to init_self * logic conflicts * tweaks to validate assignment WIP * fix logic conflicts * model assignment * more tests and fix benchmarks * skip root validators on assignment * dataclasses init * validate assignment on a dataclass * more tests for dc assignment * add NoSuchAttribute * update signature of validate_assignment * invalid input to validate assignment * fix safe_repr * support older python * info.data on validate_assignment * allow KeyError when removing field from info.data * add equivilant dataclass function test
1 parent b9bb303 commit 53dbdce

21 files changed

+1144
-248
lines changed

benches/main.rs

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ fn ints_json(bench: &mut Bencher) {
2323
Python::with_gil(|py| {
2424
let validator = build_schema_validator(py, "{'type': 'int'}");
2525

26-
let result = validator.validate_json(py, json(py, "123"), None, None).unwrap();
26+
let result = validator.validate_json(py, json(py, "123"), None, None, None).unwrap();
2727
let result_int: i64 = result.extract(py).unwrap();
2828
assert_eq!(result_int, 123);
2929

30-
bench.iter(|| black_box(validator.validate_json(py, json(py, "123"), None, None).unwrap()))
30+
bench.iter(|| black_box(validator.validate_json(py, json(py, "123"), None, None, None).unwrap()))
3131
})
3232
}
3333

@@ -38,12 +38,12 @@ fn ints_python(bench: &mut Bencher) {
3838

3939
let input = 123_i64.into_py(py);
4040
let input = input.as_ref(py);
41-
let result = validator.validate_python(py, input, None, None).unwrap();
41+
let result = validator.validate_python(py, input, None, None, None).unwrap();
4242
let result_int: i64 = result.extract(py).unwrap();
4343
assert_eq!(result_int, 123);
4444

4545
let input = black_box(input);
46-
bench.iter(|| black_box(validator.validate_python(py, input, None, None).unwrap()))
46+
bench.iter(|| black_box(validator.validate_python(py, input, None, None, None).unwrap()))
4747
})
4848
}
4949

@@ -56,7 +56,7 @@ fn list_int_json(bench: &mut Bencher) {
5656
(0..100).map(|x| x.to_string()).collect::<Vec<String>>().join(",")
5757
);
5858

59-
bench.iter(|| black_box(validator.validate_json(py, json(py, &code), None, None).unwrap()))
59+
bench.iter(|| black_box(validator.validate_json(py, json(py, &code), None, None, None).unwrap()))
6060
})
6161
}
6262

@@ -77,7 +77,7 @@ fn list_int_python(bench: &mut Bencher) {
7777
let (validator, input) = list_int_input(py);
7878
let input = black_box(input.as_ref(py));
7979
bench.iter(|| {
80-
let v = validator.validate_python(py, input, None, None).unwrap();
80+
let v = validator.validate_python(py, input, None, None, None).unwrap();
8181
black_box(v)
8282
})
8383
})
@@ -88,11 +88,11 @@ fn list_int_python_isinstance(bench: &mut Bencher) {
8888
Python::with_gil(|py| {
8989
let (validator, input) = list_int_input(py);
9090
let input = black_box(input.as_ref(py));
91-
let v = validator.isinstance_python(py, input, None, None).unwrap();
91+
let v = validator.isinstance_python(py, input, None, None, None).unwrap();
9292
assert!(v);
9393

9494
bench.iter(|| {
95-
let v = validator.isinstance_python(py, input, None, None).unwrap();
95+
let v = validator.isinstance_python(py, input, None, None, None).unwrap();
9696
black_box(v)
9797
})
9898
})
@@ -110,7 +110,7 @@ fn list_error_json(bench: &mut Bencher) {
110110
.join(", ")
111111
);
112112

113-
match validator.validate_json(py, json(py, &code), None, None) {
113+
match validator.validate_json(py, json(py, &code), None, None, None) {
114114
Ok(_) => panic!("unexpectedly valid"),
115115
Err(e) => {
116116
let v = e.value(py);
@@ -121,10 +121,12 @@ fn list_error_json(bench: &mut Bencher) {
121121
}
122122
};
123123

124-
bench.iter(|| match validator.validate_json(py, json(py, &code), None, None) {
125-
Ok(_) => panic!("unexpectedly valid"),
126-
Err(e) => black_box(e),
127-
})
124+
bench.iter(
125+
|| match validator.validate_json(py, json(py, &code), None, None, None) {
126+
Ok(_) => panic!("unexpectedly valid"),
127+
Err(e) => black_box(e),
128+
},
129+
)
128130
})
129131
}
130132

@@ -140,7 +142,7 @@ fn list_error_python_input(py: Python<'_>) -> (SchemaValidator, PyObject) {
140142

141143
let input = py.eval(&code, None, None).unwrap();
142144

143-
match validator.validate_python(py, input, None, None) {
145+
match validator.validate_python(py, input, None, None, None) {
144146
Ok(_) => panic!("unexpectedly valid"),
145147
Err(e) => {
146148
let v = e.value(py);
@@ -160,7 +162,7 @@ fn list_error_python(bench: &mut Bencher) {
160162

161163
let input = black_box(input.as_ref(py));
162164
bench.iter(|| {
163-
let result = validator.validate_python(py, input, None, None);
165+
let result = validator.validate_python(py, input, None, None, None);
164166

165167
match result {
166168
Ok(_) => panic!("unexpectedly valid"),
@@ -175,13 +177,13 @@ fn list_error_python_isinstance(bench: &mut Bencher) {
175177
Python::with_gil(|py| {
176178
let (validator, input) = list_error_python_input(py);
177179
let r = validator
178-
.isinstance_python(py, black_box(input.as_ref(py)), None, None)
180+
.isinstance_python(py, black_box(input.as_ref(py)), None, None, None)
179181
.unwrap();
180182
assert!(!r);
181183

182184
let input = black_box(input.as_ref(py));
183185
bench.iter(|| {
184-
black_box(validator.isinstance_python(py, input, None, None).unwrap());
186+
black_box(validator.isinstance_python(py, input, None, None, None).unwrap());
185187
})
186188
})
187189
}
@@ -195,7 +197,7 @@ fn list_any_json(bench: &mut Bencher) {
195197
(0..100).map(|x| x.to_string()).collect::<Vec<String>>().join(",")
196198
);
197199

198-
bench.iter(|| black_box(validator.validate_json(py, json(py, &code), None, None).unwrap()))
200+
bench.iter(|| black_box(validator.validate_json(py, json(py, &code), None, None, None).unwrap()))
199201
})
200202
}
201203

@@ -210,7 +212,7 @@ fn list_any_python(bench: &mut Bencher) {
210212
let input = py.eval(&code, None, None).unwrap();
211213
let input = black_box(input);
212214
bench.iter(|| {
213-
let v = validator.validate_python(py, input, None, None).unwrap();
215+
let v = validator.validate_python(py, input, None, None, None).unwrap();
214216
black_box(v)
215217
})
216218
})
@@ -240,7 +242,7 @@ fn dict_json(bench: &mut Bencher) {
240242
.join(", ")
241243
);
242244

243-
bench.iter(|| black_box(validator.validate_json(py, json(py, &code), None, None).unwrap()))
245+
bench.iter(|| black_box(validator.validate_json(py, json(py, &code), None, None, None).unwrap()))
244246
})
245247
}
246248

@@ -262,7 +264,7 @@ fn dict_python(bench: &mut Bencher) {
262264
let input = py.eval(&code, None, None).unwrap();
263265
let input = black_box(input);
264266
bench.iter(|| {
265-
let v = validator.validate_python(py, input, None, None).unwrap();
267+
let v = validator.validate_python(py, input, None, None, None).unwrap();
266268
black_box(v)
267269
})
268270
})
@@ -290,7 +292,7 @@ fn dict_value_error(bench: &mut Bencher) {
290292

291293
let input = py.eval(&code, None, None).unwrap();
292294

293-
match validator.validate_python(py, input, None, None) {
295+
match validator.validate_python(py, input, None, None, None) {
294296
Ok(_) => panic!("unexpectedly valid"),
295297
Err(e) => {
296298
let v = e.value(py);
@@ -303,7 +305,7 @@ fn dict_value_error(bench: &mut Bencher) {
303305

304306
let input = black_box(input);
305307
bench.iter(|| {
306-
let result = validator.validate_python(py, input, None, None);
308+
let result = validator.validate_python(py, input, None, None, None);
307309

308310
match result {
309311
Ok(_) => panic!("unexpectedly valid"),
@@ -338,7 +340,7 @@ fn typed_dict_json(bench: &mut Bencher) {
338340

339341
let code = r#"{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6, "g": 7, "h": 8, "i": 9, "j": 0}"#.to_string();
340342

341-
bench.iter(|| black_box(validator.validate_json(py, json(py, &code), None, None).unwrap()))
343+
bench.iter(|| black_box(validator.validate_json(py, json(py, &code), None, None, None).unwrap()))
342344
})
343345
}
344346

@@ -369,7 +371,7 @@ fn typed_dict_python(bench: &mut Bencher) {
369371
let input = py.eval(&code, None, None).unwrap();
370372
let input = black_box(input);
371373
bench.iter(|| {
372-
let v = validator.validate_python(py, input, None, None).unwrap();
374+
let v = validator.validate_python(py, input, None, None, None).unwrap();
373375
black_box(v)
374376
})
375377
})
@@ -407,7 +409,7 @@ fn typed_dict_deep_error(bench: &mut Bencher) {
407409
let input = py.eval(code, None, None).unwrap();
408410
let input = black_box(input);
409411

410-
match validator.validate_python(py, input, None, None) {
412+
match validator.validate_python(py, input, None, None, None) {
411413
Ok(_) => panic!("unexpectedly valid"),
412414
Err(e) => {
413415
let v = e.value(py);
@@ -419,7 +421,7 @@ fn typed_dict_deep_error(bench: &mut Bencher) {
419421
};
420422

421423
bench.iter(|| {
422-
let result = validator.validate_python(py, input, None, None);
424+
let result = validator.validate_python(py, input, None, None, None);
423425

424426
match result {
425427
Ok(_) => panic!("unexpectedly valid"),
@@ -443,7 +445,7 @@ fn complete_model(bench: &mut Bencher) {
443445
let input = black_box(input);
444446

445447
bench.iter(|| {
446-
black_box(validator.validate_python(py, input, None, None).unwrap());
448+
black_box(validator.validate_python(py, input, None, None, None).unwrap());
447449
})
448450
})
449451
}

pydantic_core/_pydantic_core.pyi

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,30 @@ class SchemaValidator:
3737
@property
3838
def title(self) -> str: ...
3939
def __init__(self, schema: CoreSchema, config: 'CoreConfig | None' = None) -> None: ...
40-
def validate_python(self, input: Any, strict: 'bool | None' = None, context: Any = None) -> Any: ...
41-
def isinstance_python(self, input: Any, strict: 'bool | None' = None, context: Any = None) -> bool: ...
40+
def validate_python(
41+
self, input: Any, *, strict: 'bool | None' = None, context: Any = None, self_instance: 'Any | None' = None
42+
) -> Any: ...
43+
def isinstance_python(
44+
self, input: Any, *, strict: 'bool | None' = None, context: Any = None, self_instance: 'Any | None' = None
45+
) -> bool: ...
4246
def validate_json(
43-
self, input: 'str | bytes | bytearray', strict: 'bool | None' = None, context: Any = None
47+
self,
48+
input: 'str | bytes | bytearray',
49+
*,
50+
strict: 'bool | None' = None,
51+
context: Any = None,
52+
self_instance: 'Any | None' = None,
4453
) -> Any: ...
4554
def isinstance_json(
46-
self, input: 'str | bytes | bytearray', strict: 'bool | None' = None, context: Any = None
55+
self,
56+
input: 'str | bytes | bytearray',
57+
*,
58+
strict: 'bool | None' = None,
59+
context: Any = None,
60+
self_instance: 'Any | None' = None,
4761
) -> bool: ...
4862
def validate_assignment(
49-
self, field: str, input: Any, data: 'dict[str, Any]', strict: 'bool | None' = None, context: Any = None
63+
self, obj: Any, field: str, input: Any, *, strict: 'bool | None' = None, context: Any = None
5064
) -> 'dict[str, Any]': ...
5165

5266
IncEx: TypeAlias = 'set[int] | set[str] | dict[int, IncEx] | dict[str, IncEx] | None'

pydantic_core/core_schema.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2485,7 +2485,7 @@ class ModelSchema(TypedDict, total=False):
24852485
type: Required[Literal['model']]
24862486
cls: Required[Type[Any]]
24872487
schema: Required[CoreSchema]
2488-
call_after_init: str
2488+
post_init: str
24892489
strict: bool
24902490
config: CoreConfig
24912491
ref: str
@@ -2497,7 +2497,7 @@ def model_schema(
24972497
cls: Type[Any],
24982498
schema: CoreSchema,
24992499
*,
2500-
call_after_init: str | None = None,
2500+
post_init: str | None = None,
25012501
strict: bool | None = None,
25022502
config: CoreConfig | None = None,
25032503
ref: str | None = None,
@@ -2533,7 +2533,7 @@ class MyModel:
25332533
Args:
25342534
cls: The class to use for the model
25352535
schema: The schema to use for the model
2536-
call_after_init: The call after init to use for the model
2536+
post_init: The call after init to use for the model
25372537
strict: Whether the model is strict
25382538
config: The config to use for the model
25392539
ref: See [TODO] for details
@@ -2544,7 +2544,7 @@ class MyModel:
25442544
type='model',
25452545
cls=cls,
25462546
schema=schema,
2547-
call_after_init=call_after_init,
2547+
post_init=post_init,
25482548
strict=strict,
25492549
config=config,
25502550
ref=ref,
@@ -3237,6 +3237,7 @@ def definition_reference_schema(
32373237
# used in _pydantic_core.pyi::PydanticKnownError
32383238
# to update this, call `pytest -k test_all_errors` and copy the output
32393239
ErrorType = Literal[
3240+
'no_such_attribute',
32403241
'json_invalid',
32413242
'json_type',
32423243
'recursion_loop',

src/build_tools.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,11 @@ pub fn function_name(f: &PyAny) -> PyResult<String> {
204204
}
205205

206206
pub fn safe_repr(v: &PyAny) -> Cow<str> {
207-
match v.repr() {
208-
Ok(r) => r.to_string_lossy(),
209-
Err(_) => v.to_string().into(),
207+
if let Ok(s) = v.repr() {
208+
s.to_string_lossy()
209+
} else if let Ok(name) = v.get_type().name() {
210+
format!("<unprintable {name} object>").into()
211+
} else {
212+
"<unprintable object>".into()
210213
}
211214
}

src/errors/types.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ pub fn list_all_errors(py: Python) -> PyResult<&PyList> {
3737
#[derive(Clone, Debug, Display, EnumMessage, EnumIter)]
3838
#[strum(serialize_all = "snake_case")]
3939
pub enum ErrorType {
40+
// ---------------------
41+
// Assignment errors
42+
#[strum(message = "Object has no attribute '{attribute}'")]
43+
NoSuchAttribute {
44+
attribute: String,
45+
},
46+
// ---------------------
47+
// JSON errors
4048
#[strum(message = "Invalid JSON: {error}")]
4149
JsonInvalid {
4250
error: String,
@@ -432,6 +440,7 @@ impl ErrorType {
432440
None => return py_err!(PyKeyError; "Invalid error type: '{}'", value),
433441
};
434442
match error_type {
443+
Self::NoSuchAttribute { .. } => extract_context!(NoSuchAttribute, ctx, attribute: String),
435444
Self::JsonInvalid { .. } => extract_context!(JsonInvalid, ctx, error: String),
436445
Self::GetAttributeError { .. } => extract_context!(GetAttributeError, ctx, error: String),
437446
Self::ModelClassType { .. } => extract_context!(ModelClassType, ctx, class_name: String),
@@ -523,6 +532,7 @@ impl ErrorType {
523532

524533
pub fn render_message(&self, py: Python) -> PyResult<String> {
525534
match self {
535+
Self::NoSuchAttribute { attribute } => render!(self, attribute),
526536
Self::JsonInvalid { error } => render!(self, error),
527537
Self::GetAttributeError { error } => render!(self, error),
528538
Self::ModelClassType { class_name } => render!(self, class_name),
@@ -583,6 +593,7 @@ impl ErrorType {
583593

584594
pub fn py_dict(&self, py: Python) -> PyResult<Option<Py<PyDict>>> {
585595
match self {
596+
Self::NoSuchAttribute { attribute } => py_dict!(py, attribute),
586597
Self::JsonInvalid { error } => py_dict!(py, error),
587598
Self::GetAttributeError { error } => py_dict!(py, error),
588599
Self::ModelClassType { class_name } => py_dict!(py, class_name),

src/url.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ impl PyUrl {
3737
pub fn py_new(py: Python, url: &PyAny) -> PyResult<Self> {
3838
let schema_obj = SCHEMA_DEFINITION_URL
3939
.get_or_init(py, || build_schema_validator(py, "url"))
40-
.validate_python(py, url, None, None)?;
40+
.validate_python(py, url, None, None, None)?;
4141
schema_obj.extract(py)
4242
}
4343

@@ -147,7 +147,7 @@ impl PyMultiHostUrl {
147147
pub fn py_new(py: Python, url: &PyAny) -> PyResult<Self> {
148148
let schema_obj = SCHEMA_DEFINITION_MULTI_HOST_URL
149149
.get_or_init(py, || build_schema_validator(py, "multi-host-url"))
150-
.validate_python(py, url, None, None)?;
150+
.validate_python(py, url, None, None, None)?;
151151
schema_obj.extract(py)
152152
}
153153

0 commit comments

Comments
 (0)