Skip to content

Commit 74adfc2

Browse files
authored
An initial implementation of unstructuring and structuring enums with complex values (#702)
* An initial implementation of unstructuring and structuring enums with complex values * Add docs and history for complex enums update * Better class names for complex enum tests * Update HISTORY.md with better wording * Add a link to the Python typing documentation for enums * Update the enum (un)structuring to only happen if `_value_` is in `__attributes__` * whoops, fix linting errors
1 parent 39a4d6e commit 74adfc2

4 files changed

Lines changed: 77 additions & 3 deletions

File tree

HISTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
1919
([#698](https://github.com/python-attrs/cattrs/pull/698))
2020
- 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.
2121
([#696](https://github.com/python-attrs/cattrs/pull/696))
22+
- Use the optional `_value_` type hint to structure and unstructure enums if present.
23+
([##699](https://github.com/python-attrs/cattrs/issues/699))
2224

2325
## 25.3.0 (2025-10-07)
2426

docs/defaulthooks.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ When unstructuring, these types are passed through unchanged.
5353
### Enums
5454

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

5857
```{doctest}
5958

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

72+
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.
73+
74+
```{doctest}
75+
76+
>>> @unique
77+
... class VideoStandard(Enum):
78+
... NTSC = "ntsc"
79+
... PAL = "pal"
80+
81+
>>> @unique
82+
... class Resolution(Enum):
83+
... _value_: tuple[VideoStandard, int]
84+
... NTSC_0 = (VideoStandard.NTSC, 0)
85+
... PAL_0 = (VideoStandard.PAL, 0)
86+
... NTSC_1 = (VideoStandard.NTSC, 1)
87+
... PAL_1 = (VideoStandard.PAL, 1)
88+
89+
>>> cattrs.structure(("ntsc", 1), Resolution)
90+
<Resolution.NTSC_1: (<VideoStandard.NTSC: 'ntsc'>, 1)>
91+
92+
>>> cattrs.unstructure(Resolution.PAL_0)
93+
['pal', 0]
94+
```
95+
7396
Again, in case of errors, the expected exceptions are raised.
7497

7598
### `pathlib.Path`

src/cattrs/converters.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ def __init__(
308308
(bytes, self._structure_call),
309309
(int, self._structure_call),
310310
(float, self._structure_call),
311-
(Enum, self._structure_call),
311+
(Enum, self._structure_enum),
312312
(Path, self._structure_call),
313313
]
314314
)
@@ -631,7 +631,9 @@ def unstructure_attrs_astuple(self, obj: Any) -> tuple[Any, ...]:
631631
return tuple(res)
632632

633633
def _unstructure_enum(self, obj: Enum) -> Any:
634-
"""Convert an enum to its value."""
634+
"""Convert an enum to its unstructured value."""
635+
if "_value_" in obj.__class__.__annotations__:
636+
return self._unstructure_func.dispatch(obj.value.__class__)(obj.value)
635637
return obj.value
636638

637639
def _unstructure_seq(self, seq: Sequence[T]) -> Sequence[T]:
@@ -713,6 +715,15 @@ def _structure_simple_literal(val, type):
713715
raise Exception(f"{val} not in literal {type}")
714716
return val
715717

718+
def _structure_enum(self, val: Any, cl: type[Enum]) -> Enum:
719+
"""Structure ``val`` if possible and return the enum it corresponds to.
720+
721+
Uses type hints for the "_value_" attribute if they exist to structure
722+
the enum values before returning the result."""
723+
if "_value_" in cl.__annotations__:
724+
val = self.structure(val, cl.__annotations__["_value_"])
725+
return cl(val)
726+
716727
@staticmethod
717728
def _structure_enum_literal(val, type):
718729
vals = {(x.value if isinstance(x, Enum) else x): x for x in type.__args__}

tests/test_enums.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Tests for enums."""
22

3+
from enum import Enum
4+
35
from hypothesis import given
46
from hypothesis.strategies import data, sampled_from
57
from pytest import raises
@@ -29,3 +31,39 @@ def test_enum_failure(enum):
2931
converter.structure("", type)
3032

3133
assert exc_info.value.args[0] == f" not in literal {type!r}"
34+
35+
36+
class SimpleEnum(Enum):
37+
A = 0
38+
B = 1
39+
C = 2
40+
41+
42+
class SimpleEnumWithTypeHint(Enum):
43+
_value_: str
44+
D = "D"
45+
E = "E"
46+
F = "F"
47+
48+
49+
class ComplexEnum(Enum):
50+
_value_: tuple[SimpleEnum, SimpleEnumWithTypeHint]
51+
AD = (SimpleEnum.A, SimpleEnumWithTypeHint.D)
52+
AE = (SimpleEnum.A, SimpleEnumWithTypeHint.E)
53+
BE = (SimpleEnum.B, SimpleEnumWithTypeHint.E)
54+
BF = (SimpleEnum.B, SimpleEnumWithTypeHint.F)
55+
CE = (SimpleEnum.C, SimpleEnumWithTypeHint.E)
56+
57+
58+
def test_unstructure_complex_enum() -> None:
59+
converter = BaseConverter()
60+
assert converter.unstructure(SimpleEnum.A) == 0
61+
assert converter.unstructure(SimpleEnumWithTypeHint.F) == "F"
62+
assert converter.unstructure(ComplexEnum.AE) == (0, "E")
63+
64+
65+
def test_structure_complex_enum() -> None:
66+
converter = BaseConverter()
67+
assert converter.structure(0, SimpleEnum) == SimpleEnum.A
68+
assert converter.structure("E", SimpleEnumWithTypeHint) == SimpleEnumWithTypeHint.E
69+
assert converter.structure((0, "D"), ComplexEnum) == ComplexEnum.AD

0 commit comments

Comments
 (0)