Skip to content

Commit 735446d

Browse files
committed
Fix unstructuring literals with enums
1 parent 3126f3f commit 735446d

File tree

5 files changed

+35
-5
lines changed

5 files changed

+35
-5
lines changed

HISTORY.md

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
2222
- Some `defaultdicts` are now [supported by default](https://catt.rs/en/latest/defaulthooks.html#defaultdicts), and
2323
{func}`cattrs.cols.is_defaultdict`{func} and `cattrs.cols.defaultdict_structure_factory` are exposed through {mod}`cattrs.cols`.
2424
([#519](https://github.com/python-attrs/cattrs/issues/519) [#588](https://github.com/python-attrs/cattrs/pull/588))
25+
- Literals containing enums are now unstructured properly.
2526
- Replace `cattrs.gen.MappingStructureFn` with `cattrs.SimpleStructureHook[In, T]`.
2627
- Python 3.13 is now supported.
2728
([#543](https://github.com/python-attrs/cattrs/pull/543) [#547](https://github.com/python-attrs/cattrs/issues/547))

src/cattrs/_compat.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,8 @@ def get_final_base(type) -> Optional[type]:
236236
# Not present on 3.9.0, so we try carefully.
237237
from typing import _LiteralGenericAlias
238238

239-
def is_literal(type) -> bool:
239+
def is_literal(type: Any) -> bool:
240+
"""Is this a literal?"""
240241
return type in LITERALS or (
241242
isinstance(
242243
type, (_GenericAlias, _LiteralGenericAlias, _SpecialGenericAlias)

src/cattrs/converters.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
)
9292
from .gen.typeddicts import make_dict_structure_fn as make_typeddict_dict_struct_fn
9393
from .gen.typeddicts import make_dict_unstructure_fn as make_typeddict_dict_unstruct_fn
94+
from .literals import is_literal_containing_enums
9495
from .types import SimpleStructureHook
9596

9697
__all__ = ["UnstructureStrategy", "BaseConverter", "Converter", "GenConverter"]
@@ -146,10 +147,6 @@ class UnstructureStrategy(Enum):
146147
AS_TUPLE = "astuple"
147148

148149

149-
def is_literal_containing_enums(typ: type) -> bool:
150-
return is_literal(typ) and any(isinstance(val, Enum) for val in typ.__args__)
151-
152-
153150
def _is_extended_factory(factory: Callable) -> bool:
154151
"""Does this factory also accept a converter arg?"""
155152
# We use the original `inspect.signature` to not evaluate string
@@ -238,6 +235,7 @@ def __init__(
238235
lambda t: self.get_unstructure_hook(get_type_alias_base(t)),
239236
True,
240237
),
238+
(is_literal_containing_enums, self.unstructure),
241239
(is_mapping, self._unstructure_mapping),
242240
(is_sequence, self._unstructure_seq),
243241
(is_mutable_set, self._unstructure_seq),

src/cattrs/literals.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from enum import Enum
2+
from typing import Any
3+
4+
from ._compat import is_literal
5+
6+
__all__ = ["is_literal", "is_literal_containing_enums"]
7+
8+
9+
def is_literal_containing_enums(type: Any) -> bool:
10+
"""Is this a literal containing at least one Enum?"""
11+
return is_literal(type) and any(isinstance(val, Enum) for val in type.__args__)

tests/test_literals.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from enum import Enum
2+
from typing import Literal
3+
4+
from cattrs import BaseConverter
5+
from cattrs.fns import identity
6+
7+
8+
class TestEnum(Enum):
9+
TEST = "test"
10+
11+
12+
def test_unstructure_literal(converter: BaseConverter):
13+
"""Literals without enums are passed through by default."""
14+
assert converter.get_unstructure_hook(1, Literal[1]) == identity
15+
16+
17+
def test_unstructure_literal_with_enum(converter: BaseConverter):
18+
"""Literals with enums are properly unstructured."""
19+
assert converter.unstructure(TestEnum.TEST, Literal[TestEnum.TEST]) == "test"

0 commit comments

Comments
 (0)