Skip to content

Commit 19dde6e

Browse files
committed
Error handling coverage
1 parent 673f8a6 commit 19dde6e

4 files changed

Lines changed: 37 additions & 23 deletions

File tree

HISTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ can now be used as decorators and have gained new features.
3131
([#472](https://github.com/python-attrs/cattrs/pull/472))
3232
- The default union handler now also handles dataclasses.
3333
([#426](https://github.com/python-attrs/cattrs/issues/426) [#477](https://github.com/python-attrs/cattrs/pull/477))
34+
- **Potentially breaking**: `IterableValidationError`s now require their subexceptions to have appropriate notes attached.
35+
This was always the case internally in _cattrs_, but is now required of errors produced outside too.
3436
- Add support for [PEP 695](https://peps.python.org/pep-0695/) type aliases.
3537
([#452](https://github.com/python-attrs/cattrs/pull/452))
3638
- Add support for named tuples with type metadata ([`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)).

src/cattrs/errors.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,24 @@ def __getnewargs__(self) -> Tuple[str, Union[int, str], Any]:
4545

4646

4747
class IterableValidationError(BaseValidationError):
48-
"""Raised when structuring an iterable."""
48+
"""Raised when structuring an iterable.
4949
50-
def group_exceptions(
51-
self,
52-
) -> Tuple[List[Tuple[Exception, IterableValidationNote]], List[Exception]]:
53-
"""Split the exceptions into two groups: with and without validation notes."""
50+
If instantiating this error manually (outside of cattrs), ensure every
51+
subexception has an appropriate IterableValidationNote note in its notes.
52+
"""
53+
54+
def group_exceptions(self) -> List[Tuple[Exception, IterableValidationNote]]:
55+
"""Group up the subexceptions alongside their IV notes."""
5456
excs_with_notes = []
55-
other_excs = []
5657
for subexc in self.exceptions:
57-
if hasattr(subexc, "__notes__"):
58-
for note in subexc.__notes__:
59-
if note.__class__ is IterableValidationNote:
60-
excs_with_notes.append((subexc, note))
61-
break
62-
else:
63-
other_excs.append(subexc)
58+
for note in subexc.__notes__:
59+
if note.__class__ is IterableValidationNote:
60+
excs_with_notes.append((subexc, note))
61+
break
6462
else:
65-
other_excs.append(subexc)
63+
raise AttributeError("Subexceptions require notes")
6664

67-
return excs_with_notes, other_excs
65+
return excs_with_notes
6866

6967

7068
class AttributeValidationNote(str):

src/cattrs/v/__init__.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,12 @@ def transform_error(
9898
"""
9999
errors: List[str] = []
100100
if isinstance(exc, IterableValidationError):
101-
with_notes, without = exc.group_exceptions()
102-
for exc, note in with_notes:
101+
for e, note in exc.group_exceptions():
103102
p = f"{path}[{note.index!r}]"
104-
if isinstance(exc, (ClassValidationError, IterableValidationError)):
105-
errors.extend(transform_error(exc, p, format_exception))
103+
if isinstance(e, (ClassValidationError, IterableValidationError)):
104+
errors.extend(transform_error(e, p, format_exception))
106105
else:
107-
errors.append(f"{format_exception(exc, note.type)} @ {p}")
108-
for exc in without:
109-
errors.append(f"{format_exception(exc, None)} @ {path}")
106+
errors.append(f"{format_exception(e, note.type)} @ {p}")
110107
elif isinstance(exc, ClassValidationError):
111108
with_notes, without = exc.group_exceptions()
112109
for exc, note in with_notes:

tests/v/test_v.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from attrs import Factory, define, field
1313
from pytest import raises
1414

15-
from cattrs import Converter, transform_error
15+
from cattrs import Converter, IterableValidationError, transform_error
1616
from cattrs._compat import Mapping, TypedDict
1717
from cattrs.gen import make_dict_structure_fn
1818
from cattrs.v import format_exception
@@ -351,3 +351,20 @@ class E(TypedDict):
351351
def test_other_errors():
352352
"""Errors without explicit support transform predictably."""
353353
assert format_exception(IndexError("Test"), List[int]) == "unknown error (Test)"
354+
355+
356+
def test_iterable_val_no_note():
357+
"""`IterableValidationErrors` require subexceptions with notes."""
358+
with raises(AttributeError):
359+
IterableValidationError("Test", [RuntimeError()], List[str]).group_exceptions()
360+
361+
r = RuntimeError()
362+
r.__notes__ = ["test"]
363+
with raises(AttributeError):
364+
IterableValidationError("Test", [r], List[str]).group_exceptions()
365+
366+
367+
def test_typeerror_formatting():
368+
"""`format_exception` works with non-iteration TypeErrors."""
369+
exc = TypeError("exception")
370+
assert format_exception(exc, None) == "invalid type (exception)"

0 commit comments

Comments
 (0)