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
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))
- Use the optional `_value_` type hint to structure and unstructure enums if present.
([##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. See [the Python typing documentation](https://typing.python.org/en/latest/spec/enums.html#member-values) for more information on this type hint.

```{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
15 changes: 13 additions & 2 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
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,7 +631,9 @@ 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."""
"""Convert an enum to its unstructured value."""
if "_value_" in obj.__class__.__annotations__:
return self._unstructure_func.dispatch(obj.value.__class__)(obj.value)
return obj.value

def _unstructure_seq(self, seq: Sequence[T]) -> Sequence[T]:
Expand Down Expand Up @@ -713,6 +715,15 @@ 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."""
if "_value_" in cl.__annotations__:
val = self.structure(val, cl.__annotations__["_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
38 changes: 38 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,39 @@ def test_enum_failure(enum):
converter.structure("", type)

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


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


class SimpleEnumWithTypeHint(Enum):
_value_: str
D = "D"
E = "E"
F = "F"


class ComplexEnum(Enum):
_value_: tuple[SimpleEnum, SimpleEnumWithTypeHint]
AD = (SimpleEnum.A, SimpleEnumWithTypeHint.D)
AE = (SimpleEnum.A, SimpleEnumWithTypeHint.E)
BE = (SimpleEnum.B, SimpleEnumWithTypeHint.E)
BF = (SimpleEnum.B, SimpleEnumWithTypeHint.F)
CE = (SimpleEnum.C, SimpleEnumWithTypeHint.E)


def test_unstructure_complex_enum() -> None:
converter = BaseConverter()
assert converter.unstructure(SimpleEnum.A) == 0
assert converter.unstructure(SimpleEnumWithTypeHint.F) == "F"
assert converter.unstructure(ComplexEnum.AE) == (0, "E")


def test_structure_complex_enum() -> None:
converter = BaseConverter()
assert converter.structure(0, SimpleEnum) == SimpleEnum.A
assert converter.structure("E", SimpleEnumWithTypeHint) == SimpleEnumWithTypeHint.E
assert converter.structure((0, "D"), ComplexEnum) == ComplexEnum.AD
Loading