Skip to content
Open
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
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
([#698](https://github.com/python-attrs/cattrs/pull/698))
- Apply the attrs converter to the default value before checking if it is equal to the attribute's value, when `omit_if_default` is true and an attrs converter is specified.
([#696](https://github.com/python-attrs/cattrs/pull/696))
- Unstructure enum values and structure enum values if they have a type hinted `_value_` attribute.
([##699](https://github.com/python-attrs/cattrs/issues/699))

## 25.3.0 (2025-10-07)

Expand Down
25 changes: 24 additions & 1 deletion docs/defaulthooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ When unstructuring, these types are passed through unchanged.
### Enums

Enums are structured by their values, and unstructured to their values.
This works even for complex values, like tuples.

```{doctest}

Expand All @@ -70,6 +69,30 @@ This works even for complex values, like tuples.
'siamese'
```

Enum structuring and unstructuring even works for complex values, like tuples, but if you have anything but simple literal types in those tuples (`str`, `bool`, `int`, `float`) you should consider defining the Enum value's type via the `_value_` attribute's type hint so that cattrs can properly structure it.

```{doctest}

>>> @unique
... class VideoStandard(Enum):
... NTSC = "ntsc"
... PAL = "pal"

>>> @unique
... class Resolution(Enum):
... _value_: tuple[VideoStandard, int]
... NTSC_0 = (VideoStandard.NTSC, 0)
... PAL_0 = (VideoStandard.PAL, 0)
... NTSC_1 = (VideoStandard.NTSC, 1)
... PAL_1 = (VideoStandard.PAL, 1)

>>> cattrs.structure(("ntsc", 1), Resolution)
<Resolution.NTSC_1: (<VideoStandard.NTSC: 'ntsc'>, 1)>

>>> cattrs.unstructure(Resolution.PAL_0)
['pal', 0]
```

Again, in case of errors, the expected exceptions are raised.

### `pathlib.Path`
Expand Down
18 changes: 14 additions & 4 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from inspect import Signature
from inspect import signature as inspect_signature
from pathlib import Path
from typing import Any, Optional, Tuple, TypeVar, overload
from typing import Any, Optional, Tuple, TypeVar, get_type_hints, overload

from attrs import Attribute, resolve_types
from attrs import has as attrs_has
Expand Down Expand Up @@ -308,7 +308,7 @@ def __init__(
(bytes, self._structure_call),
(int, self._structure_call),
(float, self._structure_call),
(Enum, self._structure_call),
(Enum, self._structure_enum),
(Path, self._structure_call),
]
)
Expand Down Expand Up @@ -631,8 +631,8 @@ def unstructure_attrs_astuple(self, obj: Any) -> tuple[Any, ...]:
return tuple(res)

def _unstructure_enum(self, obj: Enum) -> Any:
"""Convert an enum to its value."""
return obj.value
"""Convert an enum to its unstructured value."""
return self._unstructure_func.dispatch(obj.value.__class__)(obj.value)

def _unstructure_seq(self, seq: Sequence[T]) -> Sequence[T]:
"""Convert a sequence to primitive equivalents."""
Expand Down Expand Up @@ -713,6 +713,16 @@ def _structure_simple_literal(val, type):
raise Exception(f"{val} not in literal {type}")
return val

def _structure_enum(self, val: Any, cl: type[Enum]) -> Enum:
"""Structure ``val`` if possible and return the enum it corresponds to.

Uses type hints for the "_value_" attribute if they exist to structure
the enum values before returning the result."""
hints = get_type_hints(cl)
if "_value_" in hints:
val = self.structure(val, hints["_value_"])
return cl(val)

@staticmethod
def _structure_enum_literal(val, type):
vals = {(x.value if isinstance(x, Enum) else x): x for x in type.__args__}
Expand Down
30 changes: 30 additions & 0 deletions tests/test_enums.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Tests for enums."""

from enum import Enum

from hypothesis import given
from hypothesis.strategies import data, sampled_from
from pytest import raises
Expand Down Expand Up @@ -29,3 +31,31 @@ def test_enum_failure(enum):
converter.structure("", type)

assert exc_info.value.args[0] == f" not in literal {type!r}"


class SimpleEnum(Enum):
_value_: int
A = 0
B = 1
C = 2


class ComplexEnum(Enum):
_value_: tuple[SimpleEnum, int]
A0 = (SimpleEnum.A, 0)
A1 = (SimpleEnum.A, 1)
B1 = (SimpleEnum.B, 1)
B2 = (SimpleEnum.B, 2)
C1 = (SimpleEnum.C, 1)


def test_unstructure_complex_enum() -> None:
converter = BaseConverter()
assert converter.unstructure(SimpleEnum.A) == 0
assert converter.unstructure(ComplexEnum.A1) == (0, 1)


def test_structure_complex_enum() -> None:
converter = BaseConverter()
assert converter.structure(0, SimpleEnum) == SimpleEnum.A
assert converter.structure((0, 1), ComplexEnum) == ComplexEnum.A1
Loading