Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve BaseConverter mapping structuring #496

Merged
merged 1 commit into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading