Skip to content

Commit 6721395

Browse files
committed
MappingValidationError and tests
1 parent a6ab503 commit 6721395

File tree

4 files changed

+80
-8
lines changed

4 files changed

+80
-8
lines changed

src/cattr/gen.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import attr
2323
from attr import NOTHING, resolve_types
2424

25-
from cattrs.errors import ClassValidationError
25+
from cattrs.errors import ClassValidationError, MappingValidationError
2626

2727
from ._compat import (
2828
adapted_fields,
@@ -527,7 +527,12 @@ def make_mapping_unstructure_fn(
527527

528528

529529
def make_mapping_structure_fn(
530-
cl: Any, converter, structure_to=dict, key_type=NOTHING, val_type=NOTHING
530+
cl: Any,
531+
converter,
532+
structure_to=dict,
533+
key_type=NOTHING,
534+
val_type=NOTHING,
535+
extended_validation: bool = True,
531536
):
532537
"""Generate a specialized unstructure function for a mapping."""
533538
fn_name = "structure_mapping"
@@ -586,13 +591,35 @@ def make_mapping_structure_fn(
586591

587592
if is_bare_dict:
588593
# No args, it's a bare dict.
589-
lines.append(" res = dict(mapping)")
594+
lines.append(" res = dict(mapping)")
590595
else:
591-
lines.append(f" res = {{{k_s}: {v_s} for k, v in mapping.items()}}")
596+
if extended_validation:
597+
globs["enumerate"] = enumerate
598+
globs["MappingValidationError"] = MappingValidationError
599+
lines.append(" res = {}; key_errors = {}; value_errors = {}")
600+
lines.append(" for ix, (k, v) in enumerate(mapping.items()):")
601+
lines.append(" try:")
602+
lines.append(f" value = {v_s}")
603+
lines.append(" except Exception as e:")
604+
lines.append(" value_errors[key] = e")
605+
lines.append(" continue")
606+
lines.append(" try:")
607+
lines.append(f" key = {k_s}")
608+
lines.append(" res[key] = value")
609+
lines.append(" except Exception as e:")
610+
lines.append(" key_errors[ix] = e")
611+
lines.append(" if key_errors or value_errors:")
612+
lines.append(
613+
" raise MappingValidationError(key_errors, value_errors)"
614+
)
615+
else:
616+
lines.append(
617+
f" res = {{{k_s}: {v_s} for k, v in mapping.items()}}"
618+
)
592619
if structure_to is not dict:
593-
lines.append(" res = __cattr_mapping_cl(res)")
620+
lines.append(" res = __cattr_mapping_cl(res)")
594621

595-
total_lines = lines + [" return res"]
622+
total_lines = lines + [" return res"]
596623

597624
eval(compile("\n".join(total_lines), "", "exec"), globs)
598625

src/cattrs/errors.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ class ValidationError(Exception):
1717

1818
@define
1919
class IterableValidationError(ValidationError):
20+
"""Iterable validation errors.
21+
22+
A dictionary of indices to errors for the element at that index.
23+
"""
24+
2025
errors_by_index: Dict[int, Exception]
2126

2227

@@ -25,3 +30,15 @@ class ClassValidationError(ValidationError):
2530
"""Raised when validating a class if any attributes are invalid."""
2631

2732
errors_by_attribute: Dict[str, Exception]
33+
34+
35+
@define
36+
class MappingValidationError(ValidationError):
37+
"""Mapping validation errors.
38+
39+
A dictionary of element indices to key validation errors, and
40+
a dictionary of keys to value validation errors.
41+
"""
42+
43+
key_errors: Dict[int, Exception]
44+
value_errors: Dict[str, Exception]

tests/test_generics.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,11 @@ def test_able_to_structure_generics(converter: Converter, t, t2, result):
6363
(str, GenericCols("1", ["2"], {"3": "3"})),
6464
),
6565
)
66-
def test_structure_generics_with_cols(t, result):
67-
res = GenConverter().structure(asdict(result), GenericCols[t])
66+
@pytest.mark.parametrize("extended_validation", [True, False])
67+
def test_structure_generics_with_cols(t, result, extended_validation):
68+
res = GenConverter(extended_validation=extended_validation).structure(
69+
asdict(result), GenericCols[t]
70+
)
6871

6972
assert res == result
7073

tests/test_validation.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Tests for the extended validation mode."""
2+
import pytest
3+
from attrs import define, field
4+
from attrs.validators import in_
5+
6+
from cattrs import GenConverter
7+
from cattrs.errors import ClassValidationError
8+
9+
10+
def test_class_validation():
11+
c = GenConverter(extended_validation=True)
12+
13+
@define
14+
class Test:
15+
a: int
16+
b: str = field(validator=in_(["a", "b"]))
17+
c: str
18+
19+
with pytest.raises(ClassValidationError) as exc:
20+
c.structure({"a": "a", "b": "c"}, Test)
21+
22+
assert repr(exc.value.errors_by_attribute["a"]) == repr(
23+
ValueError("invalid literal for int() with base 10: 'a'")
24+
)
25+
assert repr(exc.value.errors_by_attribute["c"]) == repr(KeyError("c"))

0 commit comments

Comments
 (0)