Skip to content

Commit 158d9d3

Browse files
authored
Merge branch 'main' into bibajz/tuple_unstructuring
2 parents 73054da + c580287 commit 158d9d3

File tree

4 files changed

+58
-5
lines changed

4 files changed

+58
-5
lines changed

HISTORY.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ History
1111
(`#218 <https://github.com/python-attrs/cattrs/issues/218>`_)
1212
* Fix structuring bare ``typing.Tuple`` on Pythons lower than 3.9.
1313
(`#218 <https://github.com/python-attrs/cattrs/issues/218>`_)
14-
1514
* Fix a wrong ``AttributeError`` of an missing ``__parameters__`` attribute. This could happen
1615
when inheriting certain generic classes – for example ``typing.*`` classes are affected.
1716
(`#217 <https://github.com/python-attrs/cattrs/issues/217>`_)
17+
* Fix structuring of ``enum.Enum`` instances in ``typing.Literal`` types.
18+
(`#231 <https://github.com/python-attrs/cattrs/pull/231>`_)
1819
* Fix unstructuring all tuples - unannotated, variable-length, homogenous and heterogenous - to `list`.
1920
(`#226 <https://github.com/python-attrs/cattrs/issues/226>`_)
2021

22+
2123
1.10.0 (2022-01-04)
2224
-------------------
2325
* Add PEP 563 (string annotations) support for dataclasses.

src/cattr/converters.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ def is_optional(typ):
8181
return is_union_type(typ) and NoneType in typ.__args__ and len(typ.__args__) == 2
8282

8383

84+
def is_literal_containing_enums(typ):
85+
return is_literal(typ) and any(isinstance(val, Enum) for val in typ.__args__)
86+
87+
8488
class Converter:
8589
"""Converts between structured and unstructured data."""
8690

@@ -146,7 +150,8 @@ def __init__(
146150
[
147151
(lambda cl: cl is Any or cl is Optional or cl is None, lambda v, _: v),
148152
(is_generic_attrs, self._gen_structure_generic, True),
149-
(is_literal, self._structure_literal),
153+
(is_literal, self._structure_simple_literal),
154+
(is_literal_containing_enums, self._structure_enum_literal),
150155
(is_sequence, self._structure_list),
151156
(is_mutable_set, self._structure_set),
152157
(is_frozenset, self._structure_frozenset),
@@ -375,11 +380,19 @@ def _structure_call(obj, cl):
375380
return cl(obj)
376381

377382
@staticmethod
378-
def _structure_literal(val, type):
383+
def _structure_simple_literal(val, type):
379384
if val not in type.__args__:
380385
raise Exception(f"{val} not in literal {type}")
381386
return val
382387

388+
@staticmethod
389+
def _structure_enum_literal(val, type):
390+
vals = {(x.value if isinstance(x, Enum) else x): x for x in type.__args__}
391+
try:
392+
return vals[val]
393+
except KeyError:
394+
raise Exception(f"{val} not in literal {type}") from None
395+
383396
# Attrs classes.
384397

385398
def structure_attrs_fromtuple(self, obj: Tuple[Any, ...], cl: Type[T]) -> T:

tests/test_preconf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def everythings(
101101
)
102102
)
103103
dts = datetimes(
104-
min_value=datetime(1904, 1, 1),
104+
min_value=datetime(1970, 1, 1),
105105
max_value=datetime(2038, 1, 1),
106106
timezones=just(timezone.utc),
107107
)

tests/test_structure_attrs.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Loading of attrs classes."""
2+
from enum import Enum
23
from ipaddress import IPv4Address, IPv6Address, ip_address
34
from typing import Union
45
from unittest.mock import Mock
@@ -153,6 +154,27 @@ class ClassWithLiteral:
153154
) == ClassWithLiteral(4)
154155

155156

157+
@pytest.mark.skipif(is_py37, reason="Not supported on 3.7")
158+
@pytest.mark.parametrize("converter_cls", [Converter, GenConverter])
159+
def test_structure_literal_enum(converter_cls):
160+
"""Structuring a class with a literal field works."""
161+
from typing import Literal
162+
163+
converter = converter_cls()
164+
165+
class Foo(Enum):
166+
FOO = 1
167+
BAR = 2
168+
169+
@define
170+
class ClassWithLiteral:
171+
literal_field: Literal[Foo.FOO] = Foo.FOO
172+
173+
assert converter.structure(
174+
{"literal_field": 1}, ClassWithLiteral
175+
) == ClassWithLiteral(Foo.FOO)
176+
177+
156178
@pytest.mark.skipif(is_py37, reason="Not supported on 3.7")
157179
@pytest.mark.parametrize("converter_cls", [Converter, GenConverter])
158180
def test_structure_literal_multiple(converter_cls):
@@ -161,9 +183,17 @@ def test_structure_literal_multiple(converter_cls):
161183

162184
converter = converter_cls()
163185

186+
class Foo(Enum):
187+
FOO = 7
188+
FOOFOO = 77
189+
190+
class Bar(int, Enum):
191+
BAR = 8
192+
BARBAR = 88
193+
164194
@define
165195
class ClassWithLiteral:
166-
literal_field: Literal[4, 5] = 4
196+
literal_field: Literal[4, 5, Foo.FOO, Bar.BARBAR] = 4
167197

168198
assert converter.structure(
169199
{"literal_field": 4}, ClassWithLiteral
@@ -172,6 +202,14 @@ class ClassWithLiteral:
172202
{"literal_field": 5}, ClassWithLiteral
173203
) == ClassWithLiteral(5)
174204

205+
assert converter.structure(
206+
{"literal_field": 7}, ClassWithLiteral
207+
) == ClassWithLiteral(Foo.FOO)
208+
209+
cwl = converter.structure({"literal_field": 88}, ClassWithLiteral)
210+
assert cwl == ClassWithLiteral(Bar.BARBAR)
211+
assert isinstance(cwl.literal_field, Bar)
212+
175213

176214
@pytest.mark.skipif(is_py37, reason="Not supported on 3.7")
177215
@pytest.mark.parametrize("converter_cls", [Converter, GenConverter])

0 commit comments

Comments
 (0)