Skip to content

Commit 3c35ab0

Browse files
committed
Get everything working
1 parent 31cdebc commit 3c35ab0

File tree

11 files changed

+119
-61
lines changed

11 files changed

+119
-61
lines changed

src/input/generic_iterable.rs

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
use crate::errors::ValError;
1+
use crate::errors::{py_err_string, ErrorType, ValError, ValResult};
22

33
use super::parse_json::{JsonInput, JsonObject};
44
use pyo3::{
55
exceptions::PyTypeError,
66
types::{
77
PyByteArray, PyBytes, PyDict, PyFrozenSet, PyIterator, PyList, PyMapping, PySequence, PySet, PyString, PyTuple,
88
},
9-
PyAny, PyResult, Python, ToPyObject,
9+
PyAny, PyErr, PyResult, Python, ToPyObject,
1010
};
1111

1212
#[derive(Debug)]
@@ -41,6 +41,16 @@ fn extract_items(item: PyResult<&PyAny>) -> PyResult<PyMappingItems<'_>> {
4141
}
4242
}
4343

44+
#[inline(always)]
45+
fn map_err<'data>(py: Python<'data>, err: PyErr, input: &'data PyAny) -> ValError<'data> {
46+
ValError::new(
47+
ErrorType::IterationError {
48+
error: py_err_string(py, err),
49+
},
50+
input,
51+
)
52+
}
53+
4454
impl<'a, 'py: 'a> GenericIterable<'a> {
4555
pub fn len(&self) -> Option<usize> {
4656
match &self {
@@ -96,21 +106,52 @@ impl<'a, 'py: 'a> GenericIterable<'a> {
96106

97107
pub fn into_mapping_items_iterator(
98108
self,
99-
py: Python<'py>,
100-
) -> PyResult<Box<dyn Iterator<Item = PyResult<PyMappingItems<'a>>> + 'a>> {
109+
py: Python<'a>,
110+
) -> PyResult<Box<dyn Iterator<Item = ValResult<'a, PyMappingItems<'a>>> + 'a>> {
111+
let py2 = py;
101112
match self {
102-
GenericIterable::List(iter) => Ok(Box::new(iter.iter().map(|v| extract_items(Ok(v))))),
103-
GenericIterable::Tuple(iter) => Ok(Box::new(iter.iter().map(|v| extract_items(Ok(v))))),
104-
GenericIterable::Set(iter) => Ok(Box::new(iter.iter().map(|v| extract_items(Ok(v))))),
105-
GenericIterable::FrozenSet(iter) => Ok(Box::new(iter.iter().map(|v| extract_items(Ok(v))))),
113+
GenericIterable::List(iter) => {
114+
Ok(Box::new(iter.iter().map(move |v| {
115+
extract_items(Ok(v)).map_err(|e| map_err(py2, e, iter.as_ref()))
116+
})))
117+
}
118+
GenericIterable::Tuple(iter) => {
119+
Ok(Box::new(iter.iter().map(move |v| {
120+
extract_items(Ok(v)).map_err(|e| map_err(py2, e, iter.as_ref()))
121+
})))
122+
}
123+
GenericIterable::Set(iter) => {
124+
Ok(Box::new(iter.iter().map(move |v| {
125+
extract_items(Ok(v)).map_err(|e| map_err(py2, e, iter.as_ref()))
126+
})))
127+
}
128+
GenericIterable::FrozenSet(iter) => {
129+
Ok(Box::new(iter.iter().map(move |v| {
130+
extract_items(Ok(v)).map_err(|e| map_err(py2, e, iter.as_ref()))
131+
})))
132+
}
106133
// Note that we iterate over (key, value), unlike doing iter({}) in Python
107134
GenericIterable::Dict(iter) => Ok(Box::new(iter.iter().map(Ok))),
108135
// Keys or values can be tuples
109-
GenericIterable::DictKeys(iter) => Ok(Box::new(iter.map(extract_items))),
110-
GenericIterable::DictValues(iter) => Ok(Box::new(iter.map(extract_items))),
111-
GenericIterable::DictItems(iter) => Ok(Box::new(iter.map(extract_items))),
136+
GenericIterable::DictKeys(iter) => Ok(Box::new(
137+
iter.map(extract_items)
138+
.map(move |r| r.map_err(|e| map_err(py2, e, iter.as_ref()))),
139+
)),
140+
GenericIterable::DictValues(iter) => Ok(Box::new(
141+
iter.map(extract_items)
142+
.map(move |r| r.map_err(|e| map_err(py2, e, iter.as_ref()))),
143+
)),
144+
GenericIterable::DictItems(iter) => Ok(Box::new(
145+
iter.map(extract_items)
146+
.map(move |r| r.map_err(|e| map_err(py2, e, iter.as_ref()))),
147+
)),
112148
// Note that we iterate over (key, value), unlike doing iter({}) in Python
113-
GenericIterable::Mapping(iter) => Ok(Box::new(iter.items()?.iter()?.map(extract_items))),
149+
GenericIterable::Mapping(iter) => Ok(Box::new(
150+
iter.items()?
151+
.iter()?
152+
.map(extract_items)
153+
.map(move |r| r.map_err(|e| map_err(py2, e, iter.as_ref()))),
154+
)),
114155
// In Python if you do dict("foobar") you get "dictionary update sequence element #0 has length 1; 2 is required"
115156
// This is similar but arguably a better error message
116157
GenericIterable::String(_) => Err(PyTypeError::new_err(
@@ -124,11 +165,20 @@ impl<'a, 'py: 'a> GenericIterable<'a> {
124165
)),
125166
// Obviously these may be things that are not convertible to a tuple of (Hashable, Any)
126167
// Python fails with a similar error message to above, ours will be slightly different (PyO3 will fail to extract) but similar enough
127-
GenericIterable::Sequence(iter) => Ok(Box::new(iter.iter()?.map(extract_items))),
128-
GenericIterable::Iterator(iter) => Ok(Box::new(iter.iter()?.map(extract_items))),
168+
GenericIterable::Sequence(iter) => Ok(Box::new(
169+
iter.iter()?
170+
.map(extract_items)
171+
.map(move |r| r.map_err(|e| map_err(py2, e, iter.as_ref()))),
172+
)),
173+
GenericIterable::Iterator(iter) => Ok(Box::new(
174+
iter.iter()?
175+
.map(extract_items)
176+
.map(move |r| r.map_err(|e| map_err(py2, e, iter.as_ref()))),
177+
)),
129178
GenericIterable::JsonArray(iter) => Ok(Box::new(
130179
iter.iter()
131-
.map(move |v| extract_items(Ok(v.to_object(py).into_ref(py)))),
180+
.map(move |v| extract_items(Ok(v.to_object(py).into_ref(py))))
181+
.map(move |r| r.map_err(|e| map_err(py2, e, iter.to_object(py).into_ref(py)))),
132182
)),
133183
// Note that we iterate over (key, value), unlike doing iter({}) in Python
134184
GenericIterable::JsonObject(iter) => Ok(Box::new(iter.iter().map(move |(k, v)| {

src/input/iterator.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ impl<'data> IterableValidationChecks<'data> {
4040
errors: vec![],
4141
}
4242
}
43+
pub fn add_error(&mut self, error: ValLineError<'data>) {
44+
self.errors.push(error)
45+
}
4346
pub fn filter_validation_result<R, I: Input<'data>>(
4447
&mut self,
4548
result: ValResult<'data, R>,

src/validators/dict.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ impl BuildValidator for DictValidator {
6060

6161
const FIELD_TYPE: &str = "Dictionary";
6262

63+
#[allow(clippy::too_many_arguments)]
6364
fn validation_function<'s, 'data, K, V>(
6465
py: Python<'data>,
6566
extra: &'s Extra<'s>,
@@ -86,14 +87,15 @@ where
8687
Ok((v_key, v_value))
8788
}
8889

90+
#[allow(clippy::too_many_arguments)]
8991
fn validate_mapping<'s, 'data, K, V>(
9092
py: Python<'data>,
9193
input: &'data impl Input<'data>,
9294
extra: &'s Extra<'s>,
9395
definitions: &'data Definitions<CombinedValidator>,
9496
recursion_guard: &'s mut RecursionGuard,
9597
checks: &mut IterableValidationChecks<'data>,
96-
iter: impl Iterator<Item = PyResult<(&'data K, &'data V)>>,
98+
iter: impl Iterator<Item = ValResult<'data, (&'data K, &'data V)>>,
9799
key_validator: &'s CombinedValidator,
98100
value_validator: &'s CombinedValidator,
99101
output: &'data PyDict,

src/validators/list.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ impl Validator for ListValidator {
170170
}
171171
}
172172

173+
#[allow(clippy::too_many_arguments)]
173174
fn validate_iterator<'s, 'data, V>(
174175
py: Python<'data>,
175176
input: &'data impl Input<'data>,

src/validators/sets.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,7 @@ impl IntoSetValidator {
8080
SetType::Set => "Set",
8181
};
8282

83-
let generic_iterable = input
84-
.extract_iterable()
85-
.map_err(|_| ValError::new(ErrorType::ListType, input))?;
83+
let generic_iterable = input.extract_iterable().map_err(|_| create_err(input))?;
8684

8785
let strict = extra.strict.unwrap_or(self.strict);
8886

@@ -286,6 +284,7 @@ impl Validator for SetValidator {
286284
}
287285
}
288286

287+
#[allow(clippy::too_many_arguments)]
289288
fn validate_iterator<'s, 'data, V>(
290289
py: Python<'data>,
291290
input: &'data impl Input<'data>,

src/validators/tuple.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use pyo3::prelude::*;
33
use pyo3::types::{PyDict, PyList, PyTuple};
44

55
use crate::build_tools::{is_strict, SchemaDict};
6+
use crate::errors::ValLineError;
67
use crate::errors::{ErrorType, ValError, ValResult};
78
use crate::input::iterator::calculate_output_init_capacity;
89
use crate::input::iterator::map_iter_error;
@@ -48,6 +49,7 @@ impl BuildValidator for TupleVariableValidator {
4849
}
4950
}
5051

52+
#[allow(clippy::too_many_arguments)]
5153
fn validate_iterator<'s, 'data, V>(
5254
py: Python<'data>,
5355
input: &'data impl Input<'data>,
@@ -214,6 +216,7 @@ impl BuildValidator for TuplePositionalValidator {
214216
}
215217
}
216218

219+
#[allow(clippy::too_many_arguments)]
217220
fn validate_iterator_tuple_positional<'s, 'data, V>(
218221
py: Python<'data>,
219222
input: &'data impl Input<'data>,
@@ -266,17 +269,14 @@ where
266269
}
267270
checks.check_output_length(output.len(), input)?;
268271
}
269-
if output.len() < items_validators.len() {
270-
let remaining_item_validators = &items_validators[output.len()..];
271-
for validator in remaining_item_validators {
272-
let default = validator.default_value(py, Some(output.len()), extra, definitions, recursion_guard)?;
273-
match default {
274-
Some(v) => {
275-
output.push(v);
276-
checks.check_output_length(output.len(), input)?;
277-
}
278-
None => return Err(ValError::new_with_loc(ErrorType::Missing, input, output.len())),
272+
for (idx, validator) in items_validators.iter().enumerate().skip(output.len()) {
273+
let default = validator.default_value(py, Some(output.len()), extra, definitions, recursion_guard)?;
274+
match default {
275+
Some(v) => {
276+
output.push(v);
277+
checks.check_output_length(output.len(), input)?;
279278
}
279+
None => checks.add_error(ValLineError::new_with_loc(ErrorType::Missing, input, idx)),
280280
}
281281
}
282282
checks.finish(input)?;

tests/test_errors.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from decimal import Decimal
33

44
import pytest
5-
from dirty_equals import HasRepr, IsInstance, IsJson, IsStr
5+
from dirty_equals import Contains, HasRepr, IsInstance, IsJson, IsStr
66

77
from pydantic_core import (
88
PydanticCustomError,
@@ -615,21 +615,25 @@ def test_loc_with_dots():
615615
with pytest.raises(ValidationError) as exc_info:
616616
v.validate_python({'foo.bar': ('x', 42)})
617617
# insert_assert(exc_info.value.errors(include_url=False))
618-
assert exc_info.value.errors(include_url=False) == [
618+
assert exc_info.value.errors(include_url=False) == Contains(
619619
{
620620
'type': 'int_parsing',
621621
'loc': ('foo.bar', 0),
622622
'msg': 'Input should be a valid integer, unable to parse string as an integer',
623623
'input': 'x',
624624
}
625-
]
625+
)
626626
# insert_assert(str(exc_info.value))
627-
assert str(exc_info.value) == (
628-
"1 validation error for typed-dict\n"
629-
"`foo.bar`.0\n"
630-
" Input should be a valid integer, unable to parse string as an integer "
631-
"[type=int_parsing, input_value='x', input_type=str]\n"
632-
f' For further information visit https://errors.pydantic.dev/{__version__}/v/int_parsing'
627+
assert (
628+
str(exc_info.value)
629+
== f"""\
630+
2 validation errors for typed-dict
631+
`foo.bar`.0
632+
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='x', input_type=str]
633+
For further information visit https://errors.pydantic.dev/{__version__}/v/int_parsing
634+
`foo.bar`.1
635+
Field required [type=missing, input_value=('x', 42), input_type=tuple]
636+
For further information visit https://errors.pydantic.dev/{__version__}/v/missing""" # noqa: E501
633637
)
634638

635639

tests/validators/test_definitions_recursive.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import List, Optional
33

44
import pytest
5-
from dirty_equals import AnyThing, HasAttributes, IsList, IsPartialDict, IsStr, IsTuple
5+
from dirty_equals import AnyThing, Contains, HasAttributes, IsList, IsPartialDict, IsStr, IsTuple
66

77
from pydantic_core import SchemaError, SchemaValidator, ValidationError, __version__, core_schema
88

@@ -509,7 +509,7 @@ def test_multiple_tuple_recursion(multiple_tuple_schema: SchemaValidator):
509509
with pytest.raises(ValidationError) as exc_info:
510510
multiple_tuple_schema.validate_python({'f1': data, 'f2': data})
511511

512-
assert exc_info.value.errors(include_url=False) == [
512+
assert exc_info.value.errors(include_url=False) == Contains(
513513
{
514514
'type': 'recursion_loop',
515515
'loc': ('f1', 1),
@@ -522,7 +522,7 @@ def test_multiple_tuple_recursion(multiple_tuple_schema: SchemaValidator):
522522
'msg': 'Recursion error - cyclic reference detected',
523523
'input': [1, IsList(length=2)],
524524
},
525-
]
525+
)
526526

527527

528528
def test_multiple_tuple_recursion_once(multiple_tuple_schema: SchemaValidator):
@@ -531,7 +531,7 @@ def test_multiple_tuple_recursion_once(multiple_tuple_schema: SchemaValidator):
531531
with pytest.raises(ValidationError) as exc_info:
532532
multiple_tuple_schema.validate_python({'f1': data, 'f2': data})
533533

534-
assert exc_info.value.errors(include_url=False) == [
534+
assert exc_info.value.errors(include_url=False) == Contains(
535535
{
536536
'type': 'recursion_loop',
537537
'loc': ('f1', 1),
@@ -544,7 +544,7 @@ def test_multiple_tuple_recursion_once(multiple_tuple_schema: SchemaValidator):
544544
'msg': 'Recursion error - cyclic reference detected',
545545
'input': [1, IsList(length=2)],
546546
},
547-
]
547+
)
548548

549549

550550
def test_definition_wrap():
@@ -571,14 +571,14 @@ def wrap_func(input_value, validator, info):
571571
t.append(t)
572572
with pytest.raises(ValidationError) as exc_info:
573573
v.validate_python(t)
574-
assert exc_info.value.errors(include_url=False) == [
574+
assert exc_info.value.errors(include_url=False) == Contains(
575575
{
576576
'type': 'recursion_loop',
577577
'loc': (1,),
578578
'msg': 'Recursion error - cyclic reference detected',
579579
'input': IsList(positions={0: 1}, length=2),
580580
}
581-
]
581+
)
582582

583583

584584
def test_union_ref_strictness():

tests/validators/test_dict.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def __len__(self):
179179
[
180180
{
181181
'type': 'iteration_error',
182-
'loc': (0,),
182+
'loc': (),
183183
'msg': 'Error iterating over object, error: ValueError: expected tuple of length 2, but got tuple of length 1', # noqa: E501
184184
'input': HasRepr(IsStr(regex='.+BadMapping object at.+')),
185185
'ctx': {'error': 'ValueError: expected tuple of length 2, but got tuple of length 1'},
@@ -191,7 +191,7 @@ def __len__(self):
191191
[
192192
{
193193
'type': 'iteration_error',
194-
'loc': (0,),
194+
'loc': (),
195195
'msg': "Error iterating over object, error: TypeError: 'str' object cannot be converted to 'PyTuple'", # noqa: E501
196196
'input': HasRepr(IsStr(regex='.+BadMapping object at.+')),
197197
'ctx': {'error': "TypeError: 'str' object cannot be converted to 'PyTuple'"},
@@ -203,7 +203,7 @@ def __len__(self):
203203
[
204204
{
205205
'type': 'iteration_error',
206-
'loc': (0,),
206+
'loc': (),
207207
'msg': 'Error iterating over object, error: ValueError: expected tuple of length 2, but got tuple of length 3', # noqa: E501
208208
'input': HasRepr(IsStr(regex='.+BadMapping object at.+')),
209209
'ctx': {'error': 'ValueError: expected tuple of length 2, but got tuple of length 3'},
@@ -215,7 +215,7 @@ def __len__(self):
215215
[
216216
{
217217
'type': 'iteration_error',
218-
'loc': (0,),
218+
'loc': (),
219219
'msg': "Error iterating over object, error: TypeError: 'str' object cannot be converted to 'PyTuple'", # noqa: E501
220220
'input': HasRepr(IsStr(regex='.+BadMapping object at.+')),
221221
'ctx': {'error': "TypeError: 'str' object cannot be converted to 'PyTuple'"},

tests/validators/test_frozenset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def test_repr():
248248
'SchemaValidator('
249249
'title="frozenset[any]",'
250250
'validator=FrozenSet(FrozenSetValidator{'
251-
'inner:IntoSetValidator{strict:true,item_validator:None,min_length:Some(42),max_length:None,generator_max_length:None,'
251+
'inner:IntoSetValidator{strict:true,item_validator:Any(AnyValidator),min_length:42,max_length:None,generator_max_length:None,'
252252
'name:"frozenset[any]"'
253253
r'}}),definitions=[])'
254254
)

0 commit comments

Comments
 (0)