Skip to content

Commit d69cc96

Browse files
authored
Improve BaseConverter mapping structuring (#496)
1 parent b58a45b commit d69cc96

File tree

4 files changed

+40
-9
lines changed

4 files changed

+40
-9
lines changed

HISTORY.md

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ can now be used as decorators and have gained new features.
4949
([#460](https://github.com/python-attrs/cattrs/issues/460) [#467](https://github.com/python-attrs/cattrs/pull/467))
5050
- `typing_extensions.Any` is now supported and handled like `typing.Any`.
5151
([#488](https://github.com/python-attrs/cattrs/issues/488) [#490](https://github.com/python-attrs/cattrs/pull/490))
52+
- The BaseConverter now properly generates detailed validation errors for mappings.
5253
- [PEP 695](https://peps.python.org/pep-0695/) generics are now tested.
5354
([#452](https://github.com/python-attrs/cattrs/pull/452))
5455
- Imports are now sorted using Ruff.

src/cattrs/converters.py

+32
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,38 @@ def _structure_dict(self, obj: Mapping[T, V], cl: Any) -> dict[T, V]:
853853
if is_bare(cl) or cl.__args__ == (Any, Any):
854854
return dict(obj)
855855
key_type, val_type = cl.__args__
856+
857+
if self.detailed_validation:
858+
key_handler = self._structure_func.dispatch(key_type)
859+
val_handler = self._structure_func.dispatch(val_type)
860+
errors = []
861+
res = {}
862+
863+
for k, v in obj.items():
864+
try:
865+
value = val_handler(v, val_type)
866+
except Exception as exc:
867+
msg = IterableValidationNote(
868+
f"Structuring mapping value @ key {k!r}", k, val_type
869+
)
870+
exc.__notes__ = [*getattr(exc, "__notes__", []), msg]
871+
errors.append(exc)
872+
continue
873+
874+
try:
875+
key = key_handler(k, key_type)
876+
res[key] = value
877+
except Exception as exc:
878+
msg = IterableValidationNote(
879+
f"Structuring mapping key @ key {k!r}", k, key_type
880+
)
881+
exc.__notes__ = [*getattr(exc, "__notes__", []), msg]
882+
errors.append(exc)
883+
884+
if errors:
885+
raise IterableValidationError(f"While structuring {cl!r}", errors, cl)
886+
return res
887+
856888
if key_type in ANIES:
857889
val_conv = self._structure_func.dispatch(val_type)
858890
return {k: val_conv(v, val_type) for k, v in obj.items()}

src/cattrs/gen/__init__.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -879,12 +879,12 @@ def make_mapping_structure_fn(
879879
globs["enumerate"] = enumerate
880880

881881
lines.append(" res = {}; errors = []")
882-
lines.append(" for ix, (k, v) in enumerate(mapping.items()):")
882+
lines.append(" for k, v in mapping.items():")
883883
lines.append(" try:")
884884
lines.append(f" value = {v_s}")
885885
lines.append(" except Exception as e:")
886886
lines.append(
887-
" e.__notes__ = getattr(e, '__notes__', []) + [IterableValidationNote('Structuring mapping value @ key ' + repr(k), k, val_type)]"
887+
" e.__notes__ = getattr(e, '__notes__', []) + [IterableValidationNote(f'Structuring mapping value @ key {k!r}', k, val_type)]"
888888
)
889889
lines.append(" errors.append(e)")
890890
lines.append(" continue")
@@ -893,7 +893,7 @@ def make_mapping_structure_fn(
893893
lines.append(" res[key] = value")
894894
lines.append(" except Exception as e:")
895895
lines.append(
896-
" e.__notes__ = getattr(e, '__notes__', []) + [IterableValidationNote('Structuring mapping key @ key ' + repr(k), k, key_type)]"
896+
" e.__notes__ = getattr(e, '__notes__', []) + [IterableValidationNote(f'Structuring mapping key @ key {k!r}', k, key_type)]"
897897
)
898898
lines.append(" errors.append(e)")
899899
lines.append(" if errors:")

tests/test_validation.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,12 @@ def test_deque_validation():
104104
]
105105

106106

107-
@given(...)
108-
def test_mapping_validation(detailed_validation: bool):
107+
def test_mapping_validation(converter):
109108
"""Proper validation errors are raised structuring mappings."""
110-
c = Converter(detailed_validation=detailed_validation)
111109

112-
if detailed_validation:
110+
if converter.detailed_validation:
113111
with pytest.raises(IterableValidationError) as exc:
114-
c.structure({"1": 1, "2": "b", "c": 3}, Dict[int, int])
112+
converter.structure({"1": 1, "2": "b", "c": 3}, Dict[int, int])
115113

116114
assert repr(exc.value.exceptions[0]) == repr(
117115
ValueError("invalid literal for int() with base 10: 'b'")
@@ -128,7 +126,7 @@ def test_mapping_validation(detailed_validation: bool):
128126
]
129127
else:
130128
with pytest.raises(ValueError):
131-
c.structure({"1": 1, "2": "b", "c": 3}, Dict[int, int])
129+
converter.structure({"1": 1, "2": "b", "c": 3}, Dict[int, int])
132130

133131

134132
@given(...)

0 commit comments

Comments
 (0)