Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ The third number is for emergencies when we need to start branches for older rel

Our backwards-compatibility policy can be found [here](https://github.com/python-attrs/cattrs/blob/main/.github/SECURITY.md).

## NEXT (UNRELEASED)

- Fix {func}`transform_error <cattrs.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.
([#776](https://github.com/python-attrs/cattrs/pull/776))

## 26.2.0 (2026-09-08)

- Fix the `msgpack` and `cbor2` converters unstructuring naive datetimes as local time, which made the serialized value depend on the timezone of the machine doing the unstructuring; naive datetimes are now assumed to be UTC, matching what the structure hooks already read back.
Expand Down
2 changes: 1 addition & 1 deletion src/cattrs/v.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(exc.extra_fields)})"
res = f"extra fields found ({', '.join(sorted(exc.extra_fields))})"
elif isinstance(exc, AttributeError) and exc.args[0].endswith(
"object has no attribute 'items'"
):
Expand Down
21 changes: 21 additions & 0 deletions tests/test_v.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,27 @@ class C:
]


def test_extra_keys_are_sorted(c: Converter) -> None:
"""Extra keys are reported in a stable order.

`ForbiddenExtraKeysError.extra_fields` is a set, so the message used to
depend on the iteration order of that set, which differs between processes.
"""

@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, "e": 2, "c": 3, "b": 4, "d": 5}, C)

assert transform_error(exc_info.value) == ["extra fields found (b, c, d, e) @ $"]


def test_untyped_class_errors(c: Converter) -> None:
"""Errors on untyped attrs classes transform correctly."""

Expand Down
Loading