Skip to content

Commit 51b6ee1

Browse files
committed
add support for "init mode" in SchemaValidator
1 parent 6a200c9 commit 51b6ee1

File tree

9 files changed

+250
-48
lines changed

9 files changed

+250
-48
lines changed

pydantic_core/_pydantic_core.pyi

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,27 @@ 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, in_init: bool = False
42+
) -> Any: ...
43+
def isinstance_python(
44+
self, input: Any, *, strict: 'bool | None' = None, context: Any = None, in_init: bool = False
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+
in_init: bool = False,
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+
in_init: bool = False,
4761
) -> bool: ...
4862
def validate_assignment(
4963
self, field: str, input: Any, data: 'dict[str, Any]', strict: 'bool | None' = None, context: Any = None

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, false)?;
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, false)?;
151151
schema_obj.extract(py)
152152
}
153153

src/validators/mod.rs

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -97,88 +97,70 @@ impl SchemaValidator {
9797
Ok((cls, args).into_py(py))
9898
}
9999

100+
#[pyo3(signature = (input, *, strict=None, context=None, in_init=false))]
100101
pub fn validate_python(
101102
&self,
102103
py: Python,
103104
input: &PyAny,
104105
strict: Option<bool>,
105106
context: Option<&PyAny>,
107+
in_init: bool,
106108
) -> PyResult<PyObject> {
107-
let r = self.validator.validate(
108-
py,
109-
input,
110-
&Extra::new(strict, context),
111-
&self.slots,
112-
&mut RecursionGuard::default(),
113-
);
109+
let r = self._validate(py, input, strict, context, in_init);
114110
r.map_err(|e| self.prepare_validation_err(py, e))
115111
}
116112

113+
#[pyo3(signature = (input, *, strict=None, context=None, in_init=false))]
117114
pub fn isinstance_python(
118115
&self,
119116
py: Python,
120117
input: &PyAny,
121118
strict: Option<bool>,
122119
context: Option<&PyAny>,
120+
in_init: bool,
123121
) -> PyResult<bool> {
124-
match self.validator.validate(
125-
py,
126-
input,
127-
&Extra::new(strict, context),
128-
&self.slots,
129-
&mut RecursionGuard::default(),
130-
) {
122+
match self._validate(py, input, strict, context, in_init) {
131123
Ok(_) => Ok(true),
132124
Err(ValError::InternalErr(err)) => Err(err),
133125
Err(ValError::Omit) => Err(ValidationError::omit_error()),
134126
Err(ValError::LineErrors(_)) => Ok(false),
135127
}
136128
}
137129

130+
#[pyo3(signature = (input, *, strict=None, context=None, in_init=false))]
138131
pub fn validate_json(
139132
&self,
140133
py: Python,
141134
input: &PyAny,
142135
strict: Option<bool>,
143136
context: Option<&PyAny>,
137+
in_init: bool,
144138
) -> PyResult<PyObject> {
145139
match input.parse_json() {
146140
Ok(input) => {
147-
let r = self.validator.validate(
148-
py,
149-
&input,
150-
&Extra::new(strict, context),
151-
&self.slots,
152-
&mut RecursionGuard::default(),
153-
);
141+
let r = self._validate(py, &input, strict, context, in_init);
154142
r.map_err(|e| self.prepare_validation_err(py, e))
155143
}
156144
Err(err) => Err(self.prepare_validation_err(py, err)),
157145
}
158146
}
159147

148+
#[pyo3(signature = (input, *, strict=None, context=None, in_init=false))]
160149
pub fn isinstance_json(
161150
&self,
162151
py: Python,
163152
input: &PyAny,
164153
strict: Option<bool>,
165154
context: Option<&PyAny>,
155+
in_init: bool,
166156
) -> PyResult<bool> {
167157
match input.parse_json() {
168-
Ok(input) => {
169-
match self.validator.validate(
170-
py,
171-
&input,
172-
&Extra::new(strict, context),
173-
&self.slots,
174-
&mut RecursionGuard::default(),
175-
) {
176-
Ok(_) => Ok(true),
177-
Err(ValError::InternalErr(err)) => Err(err),
178-
Err(ValError::Omit) => Err(ValidationError::omit_error()),
179-
Err(ValError::LineErrors(_)) => Ok(false),
180-
}
181-
}
158+
Ok(input) => match self._validate(py, &input, strict, context, in_init) {
159+
Ok(_) => Ok(true),
160+
Err(ValError::InternalErr(err)) => Err(err),
161+
Err(ValError::Omit) => Err(ValidationError::omit_error()),
162+
Err(ValError::LineErrors(_)) => Ok(false),
163+
},
182164
Err(_) => Ok(false),
183165
}
184166
}
@@ -232,6 +214,36 @@ impl SchemaValidator {
232214
}
233215

234216
impl SchemaValidator {
217+
fn _validate<'s, 'data>(
218+
&'data self,
219+
py: Python<'data>,
220+
input: &'data impl Input<'data>,
221+
strict: Option<bool>,
222+
context: Option<&'data PyAny>,
223+
in_init: bool,
224+
) -> ValResult<'data, PyObject>
225+
where
226+
's: 'data,
227+
{
228+
if in_init {
229+
self.validator.validate_init(
230+
py,
231+
input,
232+
&Extra::new(strict, context),
233+
&self.slots,
234+
&mut RecursionGuard::default(),
235+
)
236+
} else {
237+
self.validator.validate(
238+
py,
239+
input,
240+
&Extra::new(strict, context),
241+
&self.slots,
242+
&mut RecursionGuard::default(),
243+
)
244+
}
245+
}
246+
235247
fn prepare_validation_err(&self, py: Python, error: ValError) -> PyErr {
236248
ValidationError::from_val_error(py, self.title.clone_ref(py), error, None)
237249
}
@@ -590,6 +602,18 @@ pub trait Validator: Send + Sync + Clone + Debug {
590602
recursion_guard: &'s mut RecursionGuard,
591603
) -> ValResult<'data, PyObject>;
592604

605+
/// Do validation but skip the root model or dataclass validator
606+
fn validate_init<'s, 'data>(
607+
&'s self,
608+
py: Python<'data>,
609+
input: &'data impl Input<'data>,
610+
extra: &Extra,
611+
slots: &'data [CombinedValidator],
612+
recursion_guard: &'s mut RecursionGuard,
613+
) -> ValResult<'data, PyObject> {
614+
self.validate(py, input, extra, slots, recursion_guard)
615+
}
616+
593617
/// `get_name` generally returns `Self::EXPECTED_TYPE` or some other clear identifier of the validator
594618
/// this is used in the error location in unions, and in the top level message in `ValidationError`
595619
fn get_name(&self) -> &str;

src/validators/model.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ impl Validator for ModelValidator {
7070
self.validator.py_gc_traverse(visit)?;
7171
Ok(())
7272
}
73+
7374
fn validate<'s, 'data>(
7475
&'s self,
7576
py: Python<'data>,
@@ -120,6 +121,18 @@ impl Validator for ModelValidator {
120121
}
121122
}
122123

124+
/// here we just call the inner validator and return the result of that
125+
fn validate_init<'s, 'data>(
126+
&'s self,
127+
py: Python<'data>,
128+
input: &'data impl Input<'data>,
129+
extra: &Extra,
130+
slots: &'data [CombinedValidator],
131+
recursion_guard: &'s mut RecursionGuard,
132+
) -> ValResult<'data, PyObject> {
133+
self.validator.validate(py, input, extra, slots, recursion_guard)
134+
}
135+
123136
fn get_name(&self) -> &str {
124137
&self.name
125138
}

tests/conftest.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,23 @@ def __init__(self, schema, validator_type: Literal['json', 'python'] | None = No
5454
self.validator_type = validator_type
5555

5656
def validate_python(self, py_input, strict: bool | None = None, context: Any = None):
57-
return self.validator.validate_python(py_input, strict, context)
57+
return self.validator.validate_python(py_input, strict=strict, context=context)
5858

5959
def validate_test(self, py_input, strict: bool | None = None, context: Any = None):
6060
if self.validator_type == 'json':
61-
return self.validator.validate_json(json.dumps(py_input, default=json_default), strict, context)
61+
return self.validator.validate_json(
62+
json.dumps(py_input, default=json_default), strict=strict, context=context
63+
)
6264
else:
6365
assert self.validator_type == 'python', self.validator_type
64-
return self.validator.validate_python(py_input, strict, context)
66+
return self.validator.validate_python(py_input, strict=strict, context=context)
6567

6668
def isinstance_test(self, py_input, strict: bool | None = None, context: Any = None):
6769
if self.validator_type == 'json':
68-
return self.validator.isinstance_json(json.dumps(py_input), strict, context)
70+
return self.validator.isinstance_json(json.dumps(py_input), strict=strict, context=context)
6971
else:
7072
assert self.validator_type == 'python', self.validator_type
71-
return self.validator.isinstance_python(py_input, strict, context)
73+
return self.validator.isinstance_python(py_input, strict=strict, context=context)
7274

7375

7476
PyAndJson = Type[PyAndJsonValidator]

tests/test_benchmark.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import dataclasses
2+
3+
import pytest
4+
5+
import pydantic
6+
from pydantic_core import core_schema, SchemaValidator
7+
8+
9+
@dataclasses.dataclass
10+
class StdLibDc:
11+
x: int
12+
y: int
13+
z: int
14+
15+
16+
@pytest.mark.benchmark(group='dataclass')
17+
def test_std_lib(benchmark):
18+
dc = StdLibDc(1, 2, z=3)
19+
assert dataclasses.asdict(dc) == {'x': 1, 'y': 2, 'z': 3}
20+
21+
@benchmark
22+
def t():
23+
StdLibDc(1, 2, z=3)
24+
25+
26+
@pydantic.dataclasses.dataclass
27+
class PydanticDc:
28+
x: int
29+
y: int
30+
z: int
31+
32+
33+
@pytest.mark.benchmark(group='dataclass')
34+
def test_pydantic(benchmark):
35+
dc = PydanticDc(1, 2, z=3)
36+
assert dataclasses.asdict(dc) == {'x': 1, 'y': 2, 'z': 3}
37+
38+
@benchmark
39+
def t():
40+
PydanticDc(1, 2, z=3)
41+
42+
43+
list_data = [{'x': i, 'y': 2, 'z': 3} for i in range(1000)]
44+
45+
46+
@pytest.mark.benchmark(group='dataclass-list')
47+
def test_list_std_lib(benchmark):
48+
49+
@benchmark
50+
def t():
51+
return [StdLibDc(**d) for d in list_data]
52+
53+
54+
55+
# @pytest.mark.benchmark(group='dataclass-list')
56+
# def test_list_pydantic(benchmark):
57+
# v = SchemaValidator(core_schema.list_schema(PydanticDc.__pydantic_core_schema__))
58+
# dcs = v.validate_python([{'x': 1, 'y': 2, 'z': 3}, {'x': 4, 'y': 5, 'z': 6}])
59+
# assert dataclasses.asdict(dcs[0]) == {'x': 1, 'y': 2, 'z': 3}
60+
# assert dataclasses.asdict(dcs[1]) == {'x': 4, 'y': 5, 'z': 6}
61+
#
62+
# @benchmark
63+
# def t():
64+
# return v.validate_python(list_data)
65+
66+
@pytest.mark.benchmark(group='dataclass-list')
67+
def test_list_pydantic(benchmark):
68+
class MyModel(pydantic.BaseModel):
69+
dcs: list[PydanticDc]
70+
dcs = MyModel(dcs=[{'x': 1, 'y': 2, 'z': 3}, {'x': 4, 'y': 5, 'z': 6}]).dcs
71+
assert dataclasses.asdict(dcs[0]) == {'x': 1, 'y': 2, 'z': 3}
72+
assert dataclasses.asdict(dcs[1]) == {'x': 4, 'y': 5, 'z': 6}
73+
74+
@benchmark
75+
def t():
76+
return MyModel(dcs=list_data).dcs

tests/test_isinstance.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ def test_isinstance():
1111
assert v.isinstance_python(123) is True
1212
assert v.validate_python('123') == 123
1313
assert v.isinstance_python('123') is True
14+
assert v.validate_python('123', in_init=True) == 123
15+
assert v.isinstance_python('123', in_init=True) is True
1416

1517
with pytest.raises(ValidationError, match='Input should be a valid integer'):
1618
v.validate_python('foo')

tests/test_validation_context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def f2(input_value, info):
131131
}
132132
)
133133

134-
m1 = v.validate_python({'f1': '1', 'f2': '2'}, None, {'x': 'y'})
134+
m1 = v.validate_python({'f1': '1', 'f2': '2'}, strict=None, context={'x': 'y'})
135135
assert m1 == {'f1': "1| context: {'x': 'y', 'f1': '1'}", 'f2': "2| context: {'x': 'y', 'f1': '1', 'f2': '2'}"}
136136

137137
m2 = v.validate_assignment('f1', '3', m1, None, {'x': 'y'})

0 commit comments

Comments
 (0)