Skip to content

Commit

Permalink
fix: use __notes__ instead of __note__ (#303)
Browse files Browse the repository at this point in the history
* fix: use __notes__

Signed-off-by: Henry Schreiner <[email protected]>

* docs: add changelog snippet

Signed-off-by: Henry Schreiner <[email protected]>

* Tweak escaping class names

Signed-off-by: Henry Schreiner <[email protected]>
Co-authored-by: Tin Tvrtković <[email protected]>
  • Loading branch information
henryiii and Tinche authored Sep 18, 2022
1 parent 5b3fb4d commit 410a267
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 33 deletions.
11 changes: 8 additions & 3 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/python-attrs/cattrs/issues/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 <https://github.com/python-attrs/cattrs/issues/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 <https://github.com/python-attrs/cattrs/issues/275>`_)
* Fix `Converter.register_structure_hook_factory` and `cattrs.gen.make_dict_unstructure_fn` type annotations.
(`#281 <https://github.com/python-attrs/cattrs/issues/281>`_)
* Expose all error classes in the `cattr.errors` namespace. Note that it is deprecated, just use `cattrs.errors`. (`#252 <https://github.com/python-attrs/cattrs/issues/252>`_)
* Expose all error classes in the `cattr.errors` namespace. Note that it is deprecated, just use `cattrs.errors`.
(`#252 <https://github.com/python-attrs/cattrs/issues/252>`_)
* ``cattrs.Converter`` and ``cattrs.BaseConverter`` can now copy themselves using the ``copy`` method.
(`#284 <https://github.com/python-attrs/cattrs/pull/284>`_)
* Fix generating structuring functions for types with quotes in the name. (`#291 <https://github.com/python-attrs/cattrs/issues/291>`_ `#277 <https://github.com/python-attrs/cattrs/issues/277>`_)
* Fix generating structuring functions for types with quotes in the name.
(`#291 <https://github.com/python-attrs/cattrs/issues/291>`_ `#277 <https://github.com/python-attrs/cattrs/issues/277>`_)
* Fix usage of notes for the final version of `PEP 678 <https://peps.python.org/pep-0678/>`_, supported since ``exceptiongroup>=1.0.0rc4``.
(`#303 <303 <https://github.com/python-attrs/cattrs/pull/303>`_)

22.1.0 (2022-04-03)
-------------------
Expand Down
2 changes: 1 addition & 1 deletion docs/validation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
17 changes: 10 additions & 7 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
6 changes: 3 additions & 3 deletions src/cattrs/gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)")

Expand Down Expand Up @@ -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")
Expand All @@ -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:")
Expand Down
44 changes: 25 additions & 19 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.<locals>.Test @ attribute a"
assert exc.value.exceptions[0].__notes__ == (
"Structuring class test_class_validation.<locals>.Test @ attribute a",
)

assert repr(exc.value.exceptions[1]) == repr(KeyError("c"))
assert (
exc.value.exceptions[1].__note__
== "Structuring class test_class_validation.<locals>.Test @ attribute c"
assert exc.value.exceptions[1].__notes__ == (
"Structuring class test_class_validation.<locals>.Test @ attribute c",
)


Expand Down Expand Up @@ -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(...)
Expand All @@ -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])
Expand All @@ -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):
Expand All @@ -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():
Expand All @@ -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():
Expand All @@ -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",
)


Expand All @@ -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",
)

0 comments on commit 410a267

Please sign in to comment.