Skip to content

Commit

Permalink
Error benchmarks and validator names (#114)
Browse files Browse the repository at this point in the history
* improve error benchmarks, tweak validator names

* improve validator names
  • Loading branch information
samuelcolvin authored Jun 22, 2022
1 parent d87f66d commit f486dd5
Show file tree
Hide file tree
Showing 15 changed files with 103 additions and 46 deletions.
65 changes: 54 additions & 11 deletions benches/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,24 @@ fn list_error_json(bench: &mut Bencher) {
let gil = Python::acquire_gil();
let py = gil.python();
let validator = build_schema_validator(py, "{'type': 'list', 'items': 'int'}");
let mut code = format!("[{}", (0..90).map(|x| x.to_string()).collect::<Vec<String>>().join(","));
code.push_str(r#","a","b","c","d","e","f","g","h","i","j"]"#);
let code = format!(
"[{}]",
(0..100)
.map(|v| format!(r#""{}""#, as_str(v)))
.collect::<Vec<String>>()
.join(", ")
);

let input_json = black_box(code.clone());
match validator.validate_json(py, input_json) {
Ok(_) => panic!("unexpectedly valid"),
Err(e) => e,
Err(e) => {
let v = e.value(py);
// println!("error: {}", v.to_string());
assert_eq!(v.getattr("title").unwrap().to_string(), "list[int]");
let error_count: i64 = v.call_method0("error_count").unwrap().extract().unwrap();
assert_eq!(error_count, 100);
}
};

bench.iter(|| {
Expand All @@ -105,11 +116,28 @@ fn list_error_python(bench: &mut Bencher) {
let gil = Python::acquire_gil();
let py = gil.python();
let validator = build_schema_validator(py, "{'type': 'list', 'items': 'int'}");
let mut code = format!("[{}", (0..90).map(|x| x.to_string()).collect::<Vec<String>>().join(","));
code.push_str(r#","a","b","c","d","e","f","g","h","i","j"]"#);
let code = format!(
"[{}]",
(0..100)
.map(|v| format!(r#""{}""#, as_str(v)))
.collect::<Vec<String>>()
.join(", ")
);

let input_python = py.eval(&code, None, None).unwrap();
let input_python = black_box(input_python.to_object(py));

match validator.validate_python(py, input_python.as_ref(py)) {
Ok(_) => panic!("unexpectedly valid"),
Err(e) => {
let v = e.value(py);
// println!("error: {}", v.to_string());
assert_eq!(v.getattr("title").unwrap().to_string(), "list[int]");
let error_count: i64 = v.call_method0("error_count").unwrap().extract().unwrap();
assert_eq!(error_count, 100);
}
};

bench.iter(|| {
let result = validator.validate_python(py, black_box(input_python.as_ref(py)));

Expand Down Expand Up @@ -159,6 +187,10 @@ fn as_char(i: u8) -> char {
(i % 26 + 97) as char
}

fn as_str(i: u8) -> String {
format!("{}{}", as_char(i / 26), as_char(i))
}

#[bench]
fn dict_json(bench: &mut Bencher) {
let gil = Python::acquire_gil();
Expand All @@ -168,7 +200,7 @@ fn dict_json(bench: &mut Bencher) {
let code = format!(
"{{{}}}",
(0..100_u8)
.map(|i| format!(r#""{}{}": {}"#, as_char(i / 26), as_char(i), i))
.map(|i| format!(r#""{}": {}"#, as_str(i), i))
.collect::<Vec<String>>()
.join(", ")
);
Expand Down Expand Up @@ -203,32 +235,43 @@ fn dict_python(bench: &mut Bencher) {
}

#[bench]
fn dict_key_error(bench: &mut Bencher) {
fn dict_value_error(bench: &mut Bencher) {
let gil = Python::acquire_gil();
let py = gil.python();
let validator = build_schema_validator(
py,
r#"{
'type': 'dict',
'keys': {
'keys': 'str',
'values': {
'type': 'int',
'lt': 10,
'lt': 0,
},
'values': 'int'
}"#,
);

let code = format!(
"{{{}}}",
(0..100_u8)
.map(|i| format!(r#"{}: {}"#, i, i))
.map(|i| format!(r#""{}": {}"#, as_str(i), i))
.collect::<Vec<String>>()
.join(", ")
);

let input_python = py.eval(&code, None, None).unwrap();
let input_python = black_box(input_python.to_object(py));

match validator.validate_python(py, input_python.as_ref(py)) {
Ok(_) => panic!("unexpectedly valid"),
Err(e) => {
let v = e.value(py);
// println!("error: {}", v.to_string());
assert_eq!(v.getattr("title").unwrap().to_string(), "dict[str, constrained-int]");
let error_count: i64 = v.call_method0("error_count").unwrap().extract().unwrap();
assert_eq!(error_count, 100);
}
};

bench.iter(|| {
let result = validator.validate_python(py, black_box(input_python.as_ref(py)));

Expand Down
9 changes: 7 additions & 2 deletions src/validators/dict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,13 @@ impl Validator for DictValidator {
self._validation_logic(py, input, input.strict_dict()?, extra, slots)
}

fn get_name(&self, _py: Python) -> String {
Self::EXPECTED_TYPE.to_string()
fn get_name(&self, py: Python) -> String {
format!(
"{}[{}, {}]",
Self::EXPECTED_TYPE,
self.key_validator.get_name(py),
self.value_validator.get_name(py)
)
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/validators/frozenset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl Validator for FrozenSetValidator {
}

fn get_name(&self, py: Python) -> String {
format!("{}-{}", Self::EXPECTED_TYPE, self.item_validator.get_name(py))
format!("{}[{}]", Self::EXPECTED_TYPE, self.item_validator.get_name(py))
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/validators/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl Validator for ListValidator {
}

fn get_name(&self, py: Python) -> String {
format!("{}-{}", Self::EXPECTED_TYPE, self.item_validator.get_name(py))
format!("{}[{}]", Self::EXPECTED_TYPE, self.item_validator.get_name(py))
}
}

Expand Down
10 changes: 5 additions & 5 deletions src/validators/literal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ impl Validator for LiteralSingleStringValidator {
}

fn get_name(&self, _py: Python) -> String {
"literal-single-string".to_string()
format!("literal[{}]", self.repr)
}
}

Expand Down Expand Up @@ -113,7 +113,7 @@ impl Validator for LiteralSingleIntValidator {
}

fn get_name(&self, _py: Python) -> String {
"literal-single-int".to_string()
format!("literal[{}]", self.expected)
}
}

Expand Down Expand Up @@ -164,7 +164,7 @@ impl Validator for LiteralMultipleStringsValidator {
}

fn get_name(&self, _py: Python) -> String {
"literal-multiple-strings".to_string()
format!("literal[{}]", self.repr)
}
}

Expand Down Expand Up @@ -215,7 +215,7 @@ impl Validator for LiteralMultipleIntsValidator {
}

fn get_name(&self, _py: Python) -> String {
"literal-multiple-ints".to_string()
format!("literal[{}]", self.repr)
}
}

Expand Down Expand Up @@ -292,6 +292,6 @@ impl Validator for LiteralGeneralValidator {
}

fn get_name(&self, _py: Python) -> String {
"literal-general".to_string()
format!("literal[{}]", self.repr)
}
}
5 changes: 1 addition & 4 deletions src/validators/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ struct ModelField {

#[derive(Debug, Clone)]
pub struct ModelValidator {
name: String,
fields: Vec<ModelField>,
extra_behavior: ExtraBehavior,
extra_validator: Option<Box<CombinedValidator>>,
Expand Down Expand Up @@ -48,7 +47,6 @@ impl BuildValidator for ModelValidator {
_ => None,
};

let name: String = schema.get_as("name")?.unwrap_or_else(|| "Model".to_string());
let fields_dict: &PyDict = schema.get_as_req("fields")?;
let mut fields: Vec<ModelField> = Vec::with_capacity(fields_dict.len());
let allow_by_name: bool = config.get_as("allow_population_by_field_name")?.unwrap_or(false);
Expand Down Expand Up @@ -81,7 +79,6 @@ impl BuildValidator for ModelValidator {
});
}
Ok(Self {
name,
fields,
extra_behavior,
extra_validator,
Expand Down Expand Up @@ -206,7 +203,7 @@ impl Validator for ModelValidator {
}

fn get_name(&self, _py: Python) -> String {
self.name.clone()
Self::EXPECTED_TYPE.to_string()
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/validators/nullable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl Validator for NullableValidator {
}
}

fn get_name(&self, _py: Python) -> String {
Self::EXPECTED_TYPE.to_string()
fn get_name(&self, py: Python) -> String {
format!("{}[{}]", Self::EXPECTED_TYPE, self.validator.get_name(py))
}
}
2 changes: 1 addition & 1 deletion src/validators/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl Validator for SetValidator {
}

fn get_name(&self, py: Python) -> String {
format!("{}-{}", Self::EXPECTED_TYPE, self.item_validator.get_name(py))
format!("{}[{}]", Self::EXPECTED_TYPE, self.item_validator.get_name(py))
}
}

Expand Down
12 changes: 9 additions & 3 deletions src/validators/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl Validator for TupleVarLenValidator {
}

fn get_name(&self, py: Python) -> String {
format!("{}-{}", Self::EXPECTED_TYPE, self.item_validator.get_name(py))
format!("{}[{}]", Self::EXPECTED_TYPE, self.item_validator.get_name(py))
}
}

Expand Down Expand Up @@ -142,8 +142,14 @@ impl Validator for TupleFixLenValidator {
self._validation_logic(py, input, input.strict_tuple()?, extra, slots)
}

fn get_name(&self, _py: Python) -> String {
format!("{}-{}-items", Self::EXPECTED_TYPE, self.items_validators.len())
fn get_name(&self, py: Python) -> String {
let descr = self
.items_validators
.iter()
.map(|v| v.get_name(py))
.collect::<Vec<_>>()
.join(", ");
format!("{}[{}]", Self::EXPECTED_TYPE, descr)
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/validators/union.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,13 @@ impl Validator for UnionValidator {
Err(ValError::LineErrors(errors))
}

fn get_name(&self, _py: Python) -> String {
Self::EXPECTED_TYPE.to_string()
fn get_name(&self, py: Python) -> String {
let descr = self
.choices
.iter()
.map(|v| v.get_name(py))
.collect::<Vec<_>>()
.join(", ");
format!("{}[{}]", Self::EXPECTED_TYPE, descr)
}
}
8 changes: 4 additions & 4 deletions tests/validators/test_frozenset.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_frozenset_no_validators_both(py_or_json, input_value, expected):
(frozenset([1, 2, 3, 2, 3]), {1, 2, 3}),
({'abc'}, Err('0\n Value must be a valid integer')),
({1, 2, 'wrong'}, Err('Value must be a valid integer')),
({1: 2}, Err('1 validation error for frozenset-int\n Value must be a valid frozenset')),
({1: 2}, Err('1 validation error for frozenset[int]\n Value must be a valid frozenset')),
('abc', Err('Value must be a valid frozenset')),
# Technically correct, but does anyone actually need this? I think needs a new type in pyo3
pytest.param({1: 10, 2: 20, 3: 30}.keys(), {1, 2, 3}, marks=pytest.mark.xfail(raises=ValidationError)),
Expand Down Expand Up @@ -143,14 +143,14 @@ def test_union_frozenset_list(input_value, expected):
errors=[
{
'kind': 'int_type',
'loc': ['frozenset-strict-int', 1],
'loc': ['frozenset[strict-int]', 1],
'message': 'Value must be a valid integer',
'input_value': 'a',
},
# second because validation on the string choice comes second
{
'kind': 'str_type',
'loc': ['frozenset-strict-str', 0],
'loc': ['frozenset[strict-str]', 0],
'message': 'Value must be a valid string',
'input_value': 1,
},
Expand Down Expand Up @@ -189,7 +189,7 @@ def test_frozenset_as_dict_keys(py_or_json):
def test_repr():
v = SchemaValidator({'type': 'frozenset', 'strict': True, 'min_items': 42})
assert repr(v) == (
'SchemaValidator(name="frozenset-any", validator=FrozenSet(\n'
'SchemaValidator(name="frozenset[any]", validator=FrozenSet(\n'
' FrozenSetValidator {\n'
' strict: true,\n'
' item_validator: Any(\n'
Expand Down
2 changes: 1 addition & 1 deletion tests/validators/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def test_missing_error():
assert (
str(exc_info.value)
== """\
1 validation error for Model
1 validation error for model
field_b
Field required [kind=missing, input_value={'field_a': 123}, input_type=dict]"""
)
Expand Down
6 changes: 3 additions & 3 deletions tests/validators/test_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def test_frozenset_no_validators_both(py_or_json, input_value, expected):
((), set()),
(frozenset([1, 2, 3, 2, 3]), {1, 2, 3}),
({'abc'}, Err('0\n Value must be a valid integer')),
({1: 2}, Err('1 validation error for set-int\n Value must be a valid set')),
({1: 2}, Err('1 validation error for set[int]\n Value must be a valid set')),
('abc', Err('Value must be a valid set')),
# Technically correct, but does anyone actually need this? I think needs a new type in pyo3
pytest.param({1: 10, 2: 20, 3: 30}.keys(), {1, 2, 3}, marks=pytest.mark.xfail(raises=ValidationError)),
Expand Down Expand Up @@ -147,14 +147,14 @@ def test_union_set_list(input_value, expected):
errors=[
{
'kind': 'int_type',
'loc': ['set-strict-int', 1],
'loc': ['set[strict-int]', 1],
'message': 'Value must be a valid integer',
'input_value': 'a',
},
# second because validation on the string choice comes second
{
'kind': 'str_type',
'loc': ['set-strict-str', 0],
'loc': ['set[strict-str]', 0],
'message': 'Value must be a valid string',
'input_value': 1,
},
Expand Down
Loading

0 comments on commit f486dd5

Please sign in to comment.