Skip to content

Commit 4d8d1f1

Browse files
committed
Error handling coverage
1 parent 662fc75 commit 4d8d1f1

File tree

4 files changed

+43
-23
lines changed

4 files changed

+43
-23
lines changed

HISTORY.md

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## 24.1.0 (UNRELEASED)
44

5+
- **Potentially breaking**: `IterableValidationError`s now require their subexceptions to have appropriate notes attached.
6+
This was always the case internally in _cattrs_, but is now required of errors produced outside too.
7+
58
- Add support for [PEP 695](https://peps.python.org/pep-0695/) type aliases.
69
([#452](https://github.com/python-attrs/cattrs/pull/452))
710
- More robust support for `Annotated` and `NotRequired` in TypedDicts.

src/cattrs/errors.py

+13-15
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

+4-7
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

+23-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from attrs import Factory, define
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
@@ -321,3 +321,25 @@ class E(TypedDict):
321321
assert transform_error(exc.value) == [
322322
f"invalid value for type, expected {tn} (invalid literal for int() with base 10: 'str') @ $.a"
323323
]
324+
325+
326+
def test_iterable_val_no_note():
327+
"""`IterableValidationErrors` require subexceptions with notes."""
328+
with raises(AttributeError):
329+
IterableValidationError("Test", [RuntimeError()], List[str]).group_exceptions()
330+
331+
r = RuntimeError()
332+
r.__notes__ = ["test"]
333+
with raises(AttributeError):
334+
IterableValidationError("Test", [r], List[str]).group_exceptions()
335+
336+
337+
def test_typeerror_formatting():
338+
"""`format_exception` works with non-iteration TypeErrors."""
339+
exc = TypeError("exception")
340+
assert format_exception(exc, None) == "invalid type (exception)"
341+
342+
343+
def test_other_formatting():
344+
"""`format_exception` handles unsupported errors."""
345+
assert format_exception(RuntimeError("test"), None) == "unknown error (test)"

0 commit comments

Comments
 (0)