Skip to content

Commit

Permalink
Fix handling of Enums in Literal types
Browse files Browse the repository at this point in the history
  • Loading branch information
brakhane committed Feb 18, 2022
1 parent 3ee4b40 commit ae8330e
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 4 deletions.
3 changes: 2 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ History
(`#218 <https://github.com/python-attrs/cattrs/issues/218>`_)
* Fix structuring bare ``typing.Tuple`` on Pythons lower than 3.9.
(`#218 <https://github.com/python-attrs/cattrs/issues/218>`_)

* Fix a wrong ``AttributeError`` of an missing ``__parameters__`` attribute. This could happen
when inheriting certain generic classes – for example ``typing.*`` classes are affected.
(`#217 <https://github.com/python-attrs/cattrs/issues/217>`_)
* Fix structuring of ``enum.Enum`` instances in ``typing.Literal`` types.
(`#231 <https://github.com/python-attrs/cattrs/pull/231>`_)

1.10.0 (2022-01-04)
-------------------
Expand Down
21 changes: 19 additions & 2 deletions src/cattr/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ def is_optional(typ):
)


def is_literal_containing_enums(typ):
return is_literal(typ) and any(
isinstance(val, Enum) for val in typ.__args__
)


class Converter(object):
"""Converts between structured and unstructured data."""

Expand Down Expand Up @@ -155,7 +161,8 @@ def __init__(
lambda v, _: v,
),
(is_generic_attrs, self._gen_structure_generic, True),
(is_literal, self._structure_literal),
(is_literal, self._structure_simple_literal),
(is_literal_containing_enums, self._structure_enum_literal),
(is_sequence, self._structure_list),
(is_mutable_set, self._structure_set),
(is_frozenset, self._structure_frozenset),
Expand Down Expand Up @@ -404,11 +411,21 @@ def _structure_call(obj, cl):
return cl(obj)

@staticmethod
def _structure_literal(val, type):
def _structure_simple_literal(val, type):
if val not in type.__args__:
raise Exception(f"{val} not in literal {type}")
return val

@staticmethod
def _structure_enum_literal(val, type):
vals = {
(x.value if isinstance(x, Enum) else x): x for x in type.__args__
}
try:
return vals[val]
except KeyError:
raise Exception(f"{val} not in literal {type}") from None

# Attrs classes.

def structure_attrs_fromtuple(
Expand Down
40 changes: 39 additions & 1 deletion tests/test_structure_attrs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Loading of attrs classes."""
from enum import Enum
from ipaddress import IPv4Address, IPv6Address, ip_address
from typing import Union
from unittest.mock import Mock
Expand Down Expand Up @@ -164,6 +165,27 @@ class ClassWithLiteral:
) == ClassWithLiteral(4)


@pytest.mark.skipif(is_py37, reason="Not supported on 3.7")
@pytest.mark.parametrize("converter_cls", [Converter, GenConverter])
def test_structure_literal_enum(converter_cls):
"""Structuring a class with a literal field works."""
from typing import Literal

converter = converter_cls()

class Foo(Enum):
FOO = 1
BAR = 2

@define
class ClassWithLiteral:
literal_field: Literal[Foo.FOO] = Foo.FOO

assert converter.structure(
{"literal_field": 1}, ClassWithLiteral
) == ClassWithLiteral(Foo.FOO)


@pytest.mark.skipif(is_py37, reason="Not supported on 3.7")
@pytest.mark.parametrize("converter_cls", [Converter, GenConverter])
def test_structure_literal_multiple(converter_cls):
Expand All @@ -172,9 +194,17 @@ def test_structure_literal_multiple(converter_cls):

converter = converter_cls()

class Foo(Enum):
FOO = 7
FOOFOO = 77

class Bar(int, Enum):
BAR = 8
BARBAR = 88

@define
class ClassWithLiteral:
literal_field: Literal[4, 5] = 4
literal_field: Literal[4, 5, Foo.FOO, Bar.BARBAR] = 4

assert converter.structure(
{"literal_field": 4}, ClassWithLiteral
Expand All @@ -183,6 +213,14 @@ class ClassWithLiteral:
{"literal_field": 5}, ClassWithLiteral
) == ClassWithLiteral(5)

assert converter.structure(
{"literal_field": 7}, ClassWithLiteral
) == ClassWithLiteral(Foo.FOO)

cwl = converter.structure({"literal_field": 88}, ClassWithLiteral)
assert cwl == ClassWithLiteral(Bar.BARBAR)
assert isinstance(cwl.literal_field, Bar)


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

0 comments on commit ae8330e

Please sign in to comment.