diff --git a/HISTORY.md b/HISTORY.md index f81acc7a..f2decefa 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -13,6 +13,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python ## NEXT (UNRELEASED) +- Fix `ForbiddenExtraKeysError` and {func}`transform_error ` crashing with `TypeError` when an extra key isn't a string, like an int key from YAML. + ([#784](https://github.com/python-attrs/cattrs/pull/784)) - Fix heterogeneous tuples and `NamedTuple`s with a member type containing a quote in its `repr`, like `tuple[Literal["a"], int]`, crashing structuring code generation with `SyntaxError`; the index note is now embedded with `repr`. ([#777](https://github.com/python-attrs/cattrs/pull/777)) - Fix {func}`transform_error ` listing the extra keys of a `ForbiddenExtraKeysError` in set iteration order, which made the message differ between runs; the keys are now sorted, like the error's own `__str__` already sorts them. diff --git a/src/cattrs/errors.py b/src/cattrs/errors.py index 13df1009..f8db816f 100644 --- a/src/cattrs/errors.py +++ b/src/cattrs/errors.py @@ -138,5 +138,5 @@ def __str__(self) -> str: return ( self.message or f"Extra fields in constructor for {self.cl.__name__}: " - f"{', '.join(sorted(self.extra_fields))}" + f"{', '.join(sorted(map(str, self.extra_fields)))}" ) diff --git a/src/cattrs/v.py b/src/cattrs/v.py index 78361aab..e089bdf7 100644 --- a/src/cattrs/v.py +++ b/src/cattrs/v.py @@ -41,7 +41,7 @@ def format_exception(exc: BaseException, type: Union[type, None]) -> str: tn = type.__name__ if hasattr(type, "__name__") else repr(type) res = f"invalid value for type, expected {tn}" elif isinstance(exc, ForbiddenExtraKeysError): - res = f"extra fields found ({', '.join(sorted(exc.extra_fields))})" + res = f"extra fields found ({', '.join(sorted(map(str, exc.extra_fields)))})" elif isinstance(exc, AttributeError) and exc.args[0].endswith( "object has no attribute 'items'" ): diff --git a/tests/test_v.py b/tests/test_v.py index 4456b60e..7f7021cb 100644 --- a/tests/test_v.py +++ b/tests/test_v.py @@ -127,6 +127,26 @@ class C: assert transform_error(exc_info.value) == ["extra fields found (b, c, d, e) @ $"] +def test_extra_keys_not_strings(c: Converter) -> None: + """Extra keys that aren't strings, like ints from YAML, are formatted.""" + + @define + class C: + a: int + + c.register_structure_hook( + C, make_dict_structure_fn(C, c, _cattrs_forbid_extra_keys=True) + ) + + with raises(Exception) as exc_info: + c.structure({"a": 1, 2: 2, "b": 3}, C) + + assert transform_error(exc_info.value) == ["extra fields found (2, b) @ $"] + assert ( + str(exc_info.value.exceptions[0]) == "Extra fields in constructor for C: 2, b" + ) + + def test_untyped_class_errors(c: Converter) -> None: """Errors on untyped attrs classes transform correctly."""