Skip to content

Commit 8435153

Browse files
authored
improve quality of too short / too long error messages (#990)
1 parent e610984 commit 8435153

File tree

10 files changed

+56
-76
lines changed

10 files changed

+56
-76
lines changed

src/errors/types.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ error_types! {
227227
TooLong {
228228
field_type: {ctx_type: String, ctx_fn: field_from_context},
229229
max_length: {ctx_type: usize, ctx_fn: field_from_context},
230-
actual_length: {ctx_type: usize, ctx_fn: field_from_context},
230+
actual_length: {ctx_type: Option<usize>, ctx_fn: field_from_context},
231231
},
232232
// ---------------------
233233
// generic collection and iteration errors
@@ -630,7 +630,7 @@ impl ErrorType {
630630
..
631631
} => {
632632
let expected_plural = plural_s(*min_length);
633-
to_string_render!(tmpl, field_type, min_length, actual_length, expected_plural)
633+
to_string_render!(tmpl, field_type, min_length, actual_length, expected_plural,)
634634
}
635635
Self::TooLong {
636636
field_type,
@@ -639,7 +639,8 @@ impl ErrorType {
639639
..
640640
} => {
641641
let expected_plural = plural_s(*max_length);
642-
to_string_render!(tmpl, field_type, max_length, actual_length, expected_plural)
642+
let actual_length = actual_length.map_or(Cow::Borrowed("more"), |v| Cow::Owned(v.to_string()));
643+
to_string_render!(tmpl, field_type, max_length, actual_length, expected_plural,)
643644
}
644645
Self::IterationError { error, .. } => render!(tmpl, error),
645646
Self::StringTooShort { min_length, .. } => to_string_render!(tmpl, min_length),

src/input/return_enums.rs

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -106,54 +106,33 @@ struct MaxLengthCheck<'a, INPUT> {
106106
max_length: Option<usize>,
107107
field_type: &'a str,
108108
input: &'a INPUT,
109-
known_input_length: usize,
109+
actual_length: Option<usize>,
110110
}
111111

112112
impl<'a, INPUT: Input<'a>> MaxLengthCheck<'a, INPUT> {
113-
fn new(max_length: Option<usize>, field_type: &'a str, input: &'a INPUT, known_input_length: usize) -> Self {
113+
fn new(max_length: Option<usize>, field_type: &'a str, input: &'a INPUT, actual_length: Option<usize>) -> Self {
114114
Self {
115115
current_length: 0,
116116
max_length,
117117
field_type,
118118
input,
119-
known_input_length,
119+
actual_length,
120120
}
121121
}
122122

123123
fn incr(&mut self) -> ValResult<'a, ()> {
124-
match self.max_length {
125-
Some(max_length) => {
126-
self.current_length += 1;
127-
if self.current_length > max_length {
128-
let biggest_length = if self.known_input_length > self.current_length {
129-
self.known_input_length
130-
} else {
131-
self.current_length
132-
};
133-
return Err(ValError::new(
134-
ErrorType::TooLong {
135-
field_type: self.field_type.to_string(),
136-
max_length,
137-
actual_length: biggest_length,
138-
context: None,
139-
},
140-
self.input,
141-
));
142-
}
143-
}
144-
None => {
145-
self.current_length += 1;
146-
if self.current_length > self.known_input_length {
147-
return Err(ValError::new(
148-
ErrorType::TooLong {
149-
field_type: self.field_type.to_string(),
150-
max_length: self.known_input_length,
151-
actual_length: self.current_length,
152-
context: None,
153-
},
154-
self.input,
155-
));
156-
}
124+
if let Some(max_length) = self.max_length {
125+
self.current_length += 1;
126+
if self.current_length > max_length {
127+
return Err(ValError::new(
128+
ErrorType::TooLong {
129+
field_type: self.field_type.to_string(),
130+
max_length,
131+
actual_length: self.actual_length,
132+
context: None,
133+
},
134+
self.input,
135+
));
157136
}
158137
}
159138
Ok(())
@@ -255,13 +234,15 @@ fn validate_iter_to_set<'a, 's>(
255234
Ok(item) => {
256235
set.build_add(item)?;
257236
if let Some(max_length) = max_length {
258-
let actual_length = set.build_len();
259-
if actual_length > max_length {
237+
if set.build_len() > max_length {
260238
return Err(ValError::new(
261239
ErrorType::TooLong {
262240
field_type: field_type.to_string(),
263241
max_length,
264-
actual_length,
242+
// The logic here is that it doesn't matter how many elements the
243+
// input actually had; all we know is it had more than the allowed
244+
// number of deduplicated elements.
245+
actual_length: None,
265246
context: None,
266247
},
267248
input,
@@ -335,10 +316,9 @@ impl<'a> GenericIterable<'a> {
335316
validator: &'s CombinedValidator,
336317
state: &mut ValidationState,
337318
) -> ValResult<'a, Vec<PyObject>> {
338-
let capacity = self
339-
.generic_len()
340-
.unwrap_or_else(|| max_length.unwrap_or(DEFAULT_CAPACITY));
341-
let max_length_check = MaxLengthCheck::new(max_length, field_type, input, capacity);
319+
let actual_length = self.generic_len();
320+
let capacity = actual_length.unwrap_or(DEFAULT_CAPACITY);
321+
let max_length_check = MaxLengthCheck::new(max_length, field_type, input, actual_length);
342322

343323
macro_rules! validate {
344324
($iter:expr) => {
@@ -394,10 +374,8 @@ impl<'a> GenericIterable<'a> {
394374
field_type: &'static str,
395375
max_length: Option<usize>,
396376
) -> ValResult<'a, Vec<PyObject>> {
397-
let capacity = self
398-
.generic_len()
399-
.unwrap_or_else(|| max_length.unwrap_or(DEFAULT_CAPACITY));
400-
let max_length_check = MaxLengthCheck::new(max_length, field_type, input, capacity);
377+
let actual_length = self.generic_len();
378+
let max_length_check = MaxLengthCheck::new(max_length, field_type, input, actual_length);
401379

402380
match self {
403381
GenericIterable::List(collection) => {

src/validators/generator.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ impl ValidatorIterator {
145145
ErrorType::TooLong {
146146
field_type: "Generator".to_string(),
147147
max_length,
148-
actual_length: index + 1,
148+
actual_length: None,
149149
context: None,
150150
},
151151
$iter.input_as_error_value(py),

src/validators/list.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ macro_rules! length_check {
5858
crate::errors::ErrorType::TooLong {
5959
field_type: $field_type.to_string(),
6060
max_length,
61-
actual_length,
61+
actual_length: Some(actual_length),
6262
context: None,
6363
},
6464
$input,

src/validators/tuple.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,7 @@ fn validate_tuple_positional<'s, 'data, T: Iterator<Item = PyResult<&'data I>>,
137137
extras_validator: &Option<Box<CombinedValidator>>,
138138
items_validators: &[CombinedValidator],
139139
collection_iter: &mut T,
140-
collection_len: Option<usize>,
141-
expected_length: usize,
140+
actual_length: Option<usize>,
142141
) -> ValResult<'data, ()> {
143142
for (index, validator) in items_validators.iter().enumerate() {
144143
match collection_iter.next() {
@@ -167,7 +166,7 @@ fn validate_tuple_positional<'s, 'data, T: Iterator<Item = PyResult<&'data I>>,
167166
errors.extend(
168167
line_errors
169168
.into_iter()
170-
.map(|err| err.with_outer_location((index + expected_length).into())),
169+
.map(|err| err.with_outer_location((index + items_validators.len()).into())),
171170
);
172171
}
173172
Err(ValError::Omit) => (),
@@ -177,8 +176,8 @@ fn validate_tuple_positional<'s, 'data, T: Iterator<Item = PyResult<&'data I>>,
177176
errors.push(ValLineError::new(
178177
ErrorType::TooLong {
179178
field_type: "Tuple".to_string(),
180-
max_length: expected_length,
181-
actual_length: collection_len.unwrap_or(index),
179+
max_length: items_validators.len(),
180+
actual_length,
182181
context: None,
183182
},
184183
input,
@@ -204,8 +203,12 @@ impl Validator for TuplePositionalValidator {
204203
state: &mut ValidationState,
205204
) -> ValResult<'data, PyObject> {
206205
let collection = input.validate_tuple(state.strict_or(self.strict))?;
207-
let expected_length = self.items_validators.len();
208-
let collection_len = collection.generic_len();
206+
let actual_length = collection.generic_len();
207+
let expected_length = if self.extras_validator.is_some() {
208+
actual_length.unwrap_or(self.items_validators.len())
209+
} else {
210+
self.items_validators.len()
211+
};
209212

210213
let mut output: Vec<PyObject> = Vec::with_capacity(expected_length);
211214
let mut errors: Vec<ValLineError> = Vec::new();
@@ -221,8 +224,7 @@ impl Validator for TuplePositionalValidator {
221224
&self.extras_validator,
222225
&self.items_validators,
223226
&mut $collection_iter,
224-
collection_len,
225-
expected_length,
227+
actual_length,
226228
)?
227229
}};
228230
}

tests/validators/test_frozenset.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,20 +148,20 @@ def generate_repeats():
148148
(
149149
{'max_length': 3},
150150
{1, 2, 3, 4},
151-
Err('Frozenset should have at most 3 items after validation, not 4 [type=too_long,'),
151+
Err('Frozenset should have at most 3 items after validation, not more [type=too_long,'),
152152
),
153153
(
154154
{'items_schema': {'type': 'int'}, 'max_length': 3},
155155
{1, 2, 3, 4},
156-
Err('Frozenset should have at most 3 items after validation, not 4 [type=too_long,'),
156+
Err('Frozenset should have at most 3 items after validation, not more [type=too_long,'),
157157
),
158158
# length check after set creation
159159
({'max_length': 3}, [1, 1, 2, 2, 3, 3], {1, 2, 3}),
160160
({'max_length': 3}, generate_repeats(), {1, 2, 3}),
161161
(
162162
{'max_length': 3},
163163
infinite_generator(),
164-
Err('Frozenset should have at most 3 items after validation, not 4 [type=too_long,'),
164+
Err('Frozenset should have at most 3 items after validation, not more [type=too_long,'),
165165
),
166166
],
167167
)

tests/validators/test_generator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,9 @@ def test_too_long(py_and_json: PyAndJson):
118118
{
119119
'type': 'too_long',
120120
'loc': (),
121-
'msg': 'Generator should have at most 2 items after validation, not 3',
121+
'msg': 'Generator should have at most 2 items after validation, not more',
122122
'input': [1, 2, 3],
123-
'ctx': {'field_type': 'Generator', 'max_length': 2, 'actual_length': 3},
123+
'ctx': {'field_type': 'Generator', 'max_length': 2, 'actual_length': None},
124124
}
125125
]
126126

@@ -167,8 +167,8 @@ def test_generator_too_long():
167167
'type': 'too_long',
168168
'loc': (),
169169
'input': HasRepr(IsStr(regex='<generator object gen at .+>')),
170-
'msg': 'Generator should have at most 2 items after validation, not 3',
171-
'ctx': {'field_type': 'Generator', 'max_length': 2, 'actual_length': 3},
170+
'msg': 'Generator should have at most 2 items after validation, not more',
171+
'ctx': {'field_type': 'Generator', 'max_length': 2, 'actual_length': None},
172172
}
173173
]
174174

tests/validators/test_list.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,13 @@ def test_list_error(input_value, index):
160160
(
161161
{'max_length': 44},
162162
infinite_generator(),
163-
Err('List should have at most 44 items after validation, not 45 [type=too_long,'),
163+
Err('List should have at most 44 items after validation, not more [type=too_long,'),
164164
),
165165
(
166166
{'max_length': 4, 'items_schema': {'type': 'int'}},
167167
[0, 1, 2, 3, 4, 5, 6, 7, 8],
168168
Err('List should have at most 4 items after validation, not 9 [type=too_long,'),
169169
),
170-
({}, infinite_generator(), Err('List should have at most 10 items after validation, not 11 [type=too_long,')),
171170
],
172171
)
173172
def test_list_length_constraints(kwargs: Dict[str, Any], input_value, expected):

tests/validators/test_set.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,12 @@ def generate_repeats():
126126
(
127127
{'max_length': 3},
128128
{1, 2, 3, 4},
129-
Err('Set should have at most 3 items after validation, not 4 [type=too_long,'),
129+
Err('Set should have at most 3 items after validation, not more [type=too_long,'),
130130
),
131131
(
132132
{'max_length': 3},
133133
[1, 2, 3, 4],
134-
Err('Set should have at most 3 items after validation, not 4 [type=too_long,'),
134+
Err('Set should have at most 3 items after validation, not more [type=too_long,'),
135135
),
136136
({'max_length': 3, 'items_schema': {'type': 'int'}}, {1, 2, 3, 4}, Err('type=too_long,')),
137137
({'max_length': 3, 'items_schema': {'type': 'int'}}, [1, 2, 3, 4], Err('type=too_long,')),
@@ -141,7 +141,7 @@ def generate_repeats():
141141
(
142142
{'max_length': 3},
143143
infinite_generator(),
144-
Err('Set should have at most 3 items after validation, not 4 [type=too_long,'),
144+
Err('Set should have at most 3 items after validation, not more [type=too_long,'),
145145
),
146146
],
147147
ids=repr,

tests/validators/test_tuple.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,13 @@ def test_tuple_strict_fails_without_tuple(wrong_coll_type: Type[Any], mode, item
107107
),
108108
(
109109
{'max_length': 3},
110-
[1, 2, 3, 4],
111-
Err('Tuple should have at most 3 items after validation, not 4 [type=too_long,'),
110+
[1, 2, 3, 4, 5],
111+
Err('Tuple should have at most 3 items after validation, not 5 [type=too_long,'),
112112
),
113113
(
114114
{'max_length': 3},
115115
infinite_generator(),
116-
Err('Tuple should have at most 3 items after validation, not 4 [type=too_long,'),
116+
Err('Tuple should have at most 3 items after validation, not more [type=too_long,'),
117117
),
118118
],
119119
ids=repr,

0 commit comments

Comments
 (0)