Skip to content

Commit

Permalink
Improve BaseConverter mapping structuring (#496)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinche authored Feb 6, 2024
1 parent b58a45b commit d69cc96
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 9 deletions.
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ can now be used as decorators and have gained new features.
([#460](https://github.com/python-attrs/cattrs/issues/460) [#467](https://github.com/python-attrs/cattrs/pull/467))
- `typing_extensions.Any` is now supported and handled like `typing.Any`.
([#488](https://github.com/python-attrs/cattrs/issues/488) [#490](https://github.com/python-attrs/cattrs/pull/490))
- The BaseConverter now properly generates detailed validation errors for mappings.
- [PEP 695](https://peps.python.org/pep-0695/) generics are now tested.
([#452](https://github.com/python-attrs/cattrs/pull/452))
- Imports are now sorted using Ruff.
Expand Down
32 changes: 32 additions & 0 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,38 @@ def _structure_dict(self, obj: Mapping[T, V], cl: Any) -> dict[T, V]:
if is_bare(cl) or cl.__args__ == (Any, Any):
return dict(obj)
key_type, val_type = cl.__args__

if self.detailed_validation:
key_handler = self._structure_func.dispatch(key_type)
val_handler = self._structure_func.dispatch(val_type)
errors = []
res = {}

for k, v in obj.items():
try:
value = val_handler(v, val_type)
except Exception as exc:
msg = IterableValidationNote(
f"Structuring mapping value @ key {k!r}", k, val_type
)
exc.__notes__ = [*getattr(exc, "__notes__", []), msg]
errors.append(exc)
continue

try:
key = key_handler(k, key_type)
res[key] = value
except Exception as exc:
msg = IterableValidationNote(
f"Structuring mapping key @ key {k!r}", k, key_type
)
exc.__notes__ = [*getattr(exc, "__notes__", []), msg]
errors.append(exc)

if errors:
raise IterableValidationError(f"While structuring {cl!r}", errors, cl)
return res

if key_type in ANIES:
val_conv = self._structure_func.dispatch(val_type)
return {k: val_conv(v, val_type) for k, v in obj.items()}
Expand Down
6 changes: 3 additions & 3 deletions src/cattrs/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -879,12 +879,12 @@ def make_mapping_structure_fn(
globs["enumerate"] = enumerate

lines.append(" res = {}; errors = []")
lines.append(" for ix, (k, v) in enumerate(mapping.items()):")
lines.append(" for k, v in mapping.items():")
lines.append(" try:")
lines.append(f" value = {v_s}")
lines.append(" except Exception as e:")
lines.append(
" e.__notes__ = getattr(e, '__notes__', []) + [IterableValidationNote('Structuring mapping value @ key ' + repr(k), k, val_type)]"
" e.__notes__ = getattr(e, '__notes__', []) + [IterableValidationNote(f'Structuring mapping value @ key {k!r}', k, val_type)]"
)
lines.append(" errors.append(e)")
lines.append(" continue")
Expand All @@ -893,7 +893,7 @@ def make_mapping_structure_fn(
lines.append(" res[key] = value")
lines.append(" except Exception as e:")
lines.append(
" e.__notes__ = getattr(e, '__notes__', []) + [IterableValidationNote('Structuring mapping key @ key ' + repr(k), k, key_type)]"
" e.__notes__ = getattr(e, '__notes__', []) + [IterableValidationNote(f'Structuring mapping key @ key {k!r}', k, key_type)]"
)
lines.append(" errors.append(e)")
lines.append(" if errors:")
Expand Down
10 changes: 4 additions & 6 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,12 @@ def test_deque_validation():
]


@given(...)
def test_mapping_validation(detailed_validation: bool):
def test_mapping_validation(converter):
"""Proper validation errors are raised structuring mappings."""
c = Converter(detailed_validation=detailed_validation)

if detailed_validation:
if converter.detailed_validation:
with pytest.raises(IterableValidationError) as exc:
c.structure({"1": 1, "2": "b", "c": 3}, Dict[int, int])
converter.structure({"1": 1, "2": "b", "c": 3}, Dict[int, int])

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


@given(...)
Expand Down

0 comments on commit d69cc96

Please sign in to comment.