diff --git a/HISTORY.rst b/HISTORY.rst index 4c00d1aa..194a1fc2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,13 +15,18 @@ History * Fix propagating the `detailed_validation` flag to mapping and counter structuring generators. * Fix ``typing.Set`` applying too broadly when used with the ``GenConverter.unstruct_collection_overrides`` parameter on Python versions below 3.9. Switch to ``typing.AbstractSet`` on those versions to restore the old behavior. (`#264 `_) -* Uncap the required Python version, to avoid problems detailed in https://iscinumpy.dev/post/bound-version-constraints/#pinning-the-python-version-is-special (`#275 `_) +* Uncap the required Python version, to avoid problems detailed in https://iscinumpy.dev/post/bound-version-constraints/#pinning-the-python-version-is-special + (`#275 `_) * Fix `Converter.register_structure_hook_factory` and `cattrs.gen.make_dict_unstructure_fn` type annotations. (`#281 `_) -* Expose all error classes in the `cattr.errors` namespace. Note that it is deprecated, just use `cattrs.errors`. (`#252 `_) +* 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 generating structuring functions for types with quotes in the name. (`#291 `_ `#277 `_) +* Fix generating structuring functions for types with quotes in the name. + (`#291 `_ `#277 `_) +* 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) ------------------- 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 088bade0..4d031cdf 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -495,7 +495,8 @@ def _structure_list(self, obj: Iterable[T], cl: Any) -> List[T]: 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 @@ -522,9 +523,8 @@ def _structure_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) @@ -593,7 +593,8 @@ def _structure_tuple(self, obj: Any, 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( @@ -620,14 +621,16 @@ def _structure_tuple(self, obj: Any, 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 597a672a..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.__note__ = '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)") @@ -736,7 +736,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") @@ -745,7 +745,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", )