From 279235199086913401d0e2ebbd38b025a016ab3d Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 12 Sep 2022 10:32:59 -0400 Subject: [PATCH 1/3] fix: use __notes__ Signed-off-by: Henry Schreiner --- docs/validation.rst | 2 +- src/cattrs/converters.py | 17 +++++++++------- src/cattrs/gen.py | 6 +++--- tests/test_validation.py | 44 +++++++++++++++++++++++----------------- 4 files changed, 39 insertions(+), 30 deletions(-) diff --git a/docs/validation.rst b/docs/validation.rst index a0043418..721dca8a 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -14,7 +14,7 @@ In essence, ExceptionGroups are trees of exceptions. When un/structuring a class, `cattrs` will gather any exceptions on a field-by-field basis and raise them as a ``cattrs.ClassValidationError``, which is a subclass of ``BaseValidationError``. When structuring sequences and mappings, `cattrs` will gather any exceptions on a key- or index-basis and raise them as a ``cattrs.IterableValidationError``, which is a subclass of ``BaseValidationError``. -The exceptions will also have their ``__note__`` attributes set, as per `PEP 678`_, showing the field, key or index for each inner exception. +The exceptions will also have their ``__notes__`` attributes set, as per `PEP 678`_, showing the field, key or index for each inner exception. A simple example involving a class containing a list and a dictionary: diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 0a6b065f..0c61d51f 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -470,7 +470,8 @@ def _structure_list(self, obj, cl): try: res.append(handler(e, elem_type)) except Exception as e: - e.__note__ = f"Structuring {cl} @ index {ix}" + msg = f"Structuring {cl} @ index {ix}" + e.__notes__ = getattr(e, "__notes__", ()) + (msg,) errors.append(e) finally: ix += 1 @@ -495,9 +496,8 @@ def _structure_set(self, obj, cl, structure_to=set): try: res.add(handler(e, elem_type)) except Exception as exc: - exc.__note__ = ( - f"Structuring {structure_to.__name__} @ element {e!r}" - ) + msg = f"Structuring {structure_to.__name__} @ element {e!r}" + exc.__notes__ = getattr(e, "__notes__", ()) + (msg,) errors.append(exc) if errors: raise IterableValidationError(f"While structuring {cl!r}", errors, cl) @@ -564,7 +564,8 @@ def _structure_tuple(self, obj, tup: Type[T]) -> T: try: res.append(conv(e, tup_type)) except Exception as exc: - exc.__note__ = f"Structuring {tup} @ index {ix}" + msg = f"Structuring {tup} @ index {ix}" + exc.__notes__ = getattr(e, "__notes__", ()) + (msg,) errors.append(exc) if errors: raise IterableValidationError( @@ -591,14 +592,16 @@ def _structure_tuple(self, obj, tup: Type[T]) -> T: conv = self._structure_func.dispatch(t) res.append(conv(e, t)) except Exception as exc: - exc.__note__ = f"Structuring {tup} @ index {ix}" + msg = f"Structuring {tup} @ index {ix}" + exc.__notes__ = getattr(e, "__notes__", ()) + (msg,) errors.append(exc) if len(res) < exp_len: problem = "Not enough" if len(res) < len(tup_params) else "Too many" exc = ValueError( f"{problem} values in {obj!r} to structure as {tup!r}" ) - exc.__note__ = f"Structuring {tup}" + msg = f"Structuring {tup}" + exc.__notes__ = getattr(e, "__notes__", ()) + (msg,) errors.append(exc) if errors: raise IterableValidationError( diff --git a/src/cattrs/gen.py b/src/cattrs/gen.py index 95cc02ed..a882c36d 100644 --- a/src/cattrs/gen.py +++ b/src/cattrs/gen.py @@ -340,7 +340,7 @@ def make_dict_structure_fn( lines.append(f"{i}except Exception as e:") i = f"{i} " lines.append( - f"{i}e.__note__ = 'Structuring class {cl.__qualname__} @ attribute {an}'" + f"{i}e.__notes__ = getattr(e, '__notes__', ()) + ('Structuring class {cl.__qualname__} @ attribute {an}',)" ) lines.append(f"{i}errors.append(e)") @@ -703,7 +703,7 @@ def make_mapping_structure_fn( lines.append(f" value = {v_s}") lines.append(" except Exception as e:") lines.append( - " e.__note__ = 'Structuring mapping value @ key ' + repr(k)" + " e.__notes__ = getattr(e, '__notes__', ()) + ('Structuring mapping value @ key ' + repr(k),)" ) lines.append(" errors.append(e)") lines.append(" continue") @@ -712,7 +712,7 @@ def make_mapping_structure_fn( lines.append(" res[key] = value") lines.append(" except Exception as e:") lines.append( - " e.__note__ = 'Structuring mapping key @ key ' + repr(k)" + " e.__notes__ = getattr(e, '__notes__', ()) + ('Structuring mapping key @ key ' + repr(k),)" ) lines.append(" errors.append(e)") lines.append(" if errors:") diff --git a/tests/test_validation.py b/tests/test_validation.py index 5fceab20..d0d691d9 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -27,15 +27,13 @@ class Test: assert repr(exc.value.exceptions[0]) == repr( ValueError("invalid literal for int() with base 10: 'a'") ) - assert ( - exc.value.exceptions[0].__note__ - == "Structuring class test_class_validation..Test @ attribute a" + assert exc.value.exceptions[0].__notes__ == ( + "Structuring class test_class_validation..Test @ attribute a", ) assert repr(exc.value.exceptions[1]) == repr(KeyError("c")) - assert ( - exc.value.exceptions[1].__note__ - == "Structuring class test_class_validation..Test @ attribute c" + assert exc.value.exceptions[1].__notes__ == ( + "Structuring class test_class_validation..Test @ attribute c", ) @@ -66,12 +64,16 @@ def test_list_validation(): assert repr(exc.value.exceptions[0]) == repr( ValueError("invalid literal for int() with base 10: 'a'") ) - assert exc.value.exceptions[0].__note__ == "Structuring typing.List[int] @ index 2" + assert exc.value.exceptions[0].__notes__ == ( + "Structuring typing.List[int] @ index 2", + ) assert repr(exc.value.exceptions[1]) == repr( ValueError("invalid literal for int() with base 10: 'c'") ) - assert exc.value.exceptions[1].__note__ == "Structuring typing.List[int] @ index 4" + assert exc.value.exceptions[1].__notes__ == ( + "Structuring typing.List[int] @ index 4", + ) @given(...) @@ -86,12 +88,16 @@ def test_mapping_validation(detailed_validation: bool): assert repr(exc.value.exceptions[0]) == repr( ValueError("invalid literal for int() with base 10: 'b'") ) - assert exc.value.exceptions[0].__note__ == "Structuring mapping value @ key '2'" + assert exc.value.exceptions[0].__notes__ == ( + "Structuring mapping value @ key '2'", + ) assert repr(exc.value.exceptions[1]) == repr( ValueError("invalid literal for int() with base 10: 'c'") ) - assert exc.value.exceptions[1].__note__ == "Structuring mapping key @ key 'c'" + assert exc.value.exceptions[1].__notes__ == ( + "Structuring mapping key @ key 'c'", + ) else: with pytest.raises(ValueError): c.structure({"1": 1, "2": "b", "c": 3}, Dict[int, int]) @@ -109,7 +115,9 @@ def test_counter_validation(detailed_validation: bool): assert repr(exc.value.exceptions[0]) == repr( ValueError("invalid literal for int() with base 10: 'b'") ) - assert exc.value.exceptions[0].__note__ == "Structuring mapping value @ key 'b'" + assert exc.value.exceptions[0].__notes__ == ( + "Structuring mapping value @ key 'b'", + ) else: with pytest.raises(ValueError): @@ -126,7 +134,7 @@ def test_set_validation(): assert repr(exc.value.exceptions[0]) == repr( ValueError("invalid literal for int() with base 10: 'a'") ) - assert exc.value.exceptions[0].__note__ == "Structuring set @ element 'a'" + assert exc.value.exceptions[0].__notes__ == ("Structuring set @ element 'a'",) def test_frozenset_validation(): @@ -139,7 +147,7 @@ def test_frozenset_validation(): assert repr(exc.value.exceptions[0]) == repr( ValueError("invalid literal for int() with base 10: 'a'") ) - assert exc.value.exceptions[0].__note__ == "Structuring frozenset @ element 'a'" + assert exc.value.exceptions[0].__notes__ == ("Structuring frozenset @ element 'a'",) def test_homo_tuple_validation(): @@ -152,9 +160,8 @@ def test_homo_tuple_validation(): assert repr(exc.value.exceptions[0]) == repr( ValueError("invalid literal for int() with base 10: 'a'") ) - assert ( - exc.value.exceptions[0].__note__ - == "Structuring typing.Tuple[int, ...] @ index 2" + assert exc.value.exceptions[0].__notes__ == ( + "Structuring typing.Tuple[int, ...] @ index 2", ) @@ -168,7 +175,6 @@ def test_hetero_tuple_validation(): assert repr(exc.value.exceptions[0]) == repr( ValueError("invalid literal for int() with base 10: 'a'") ) - assert ( - exc.value.exceptions[0].__note__ - == "Structuring typing.Tuple[int, int, int] @ index 2" + assert exc.value.exceptions[0].__notes__ == ( + "Structuring typing.Tuple[int, int, int] @ index 2", ) From 6b531ad41dd05c7e9e9e4cd1b5f0ccd9162a22bb Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 12 Sep 2022 11:51:59 -0400 Subject: [PATCH 2/3] docs: add changelog snippet Signed-off-by: Henry Schreiner --- HISTORY.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 93de9e65..590a4131 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,6 +21,8 @@ History * Expose all error classes in the `cattr.errors` namespace. Note that it is deprecated, just use `cattrs.errors`. (`#252 `_) * ``cattrs.Converter`` and ``cattrs.BaseConverter`` can now copy themselves using the ``copy`` method. (`#284 `_) +* Fix usage of notes for the final version of `PEP 678`_, supported since ``exceptiongroup>=1.0.0rc4``. + (`#303 <303 `_) 22.1.0 (2022-04-03) ------------------- From c01027ab9c69af7e9701a2b657997e32b3fe70fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Sun, 18 Sep 2022 20:17:38 +0200 Subject: [PATCH 3/3] Tweak escaping class names --- src/cattrs/gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cattrs/gen.py b/src/cattrs/gen.py index 2c212c35..a999a8dc 100644 --- a/src/cattrs/gen.py +++ b/src/cattrs/gen.py @@ -354,7 +354,7 @@ def make_dict_structure_fn( lines.append(f"{i}except Exception as e:") i = f"{i} " lines.append( - f"{i}e.__notes__ = getattr(e, '__notes__', ()) + ('Structuring class {cl.__qualname__!r} @ attribute {an}',)" + f"{i}e.__notes__ = getattr(e, '__notes__', ()) + (\"Structuring class {cl.__qualname__} @ attribute {an}\",)" ) lines.append(f"{i}errors.append(e)")