Skip to content

Commit 3b6bdb7

Browse files
structure typing_extension.Literal to support old code (or libraries that still support Python 3.7) (#467)
1 parent 6d0a6d0 commit 3b6bdb7

3 files changed

Lines changed: 34 additions & 3 deletions

File tree

HISTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
([#463](https://github.com/python-attrs/cattrs/pull/463))
99
- More robust support for `Annotated` and `NotRequired` in TypedDicts.
1010
([#450](https://github.com/python-attrs/cattrs/pull/450))
11+
- `typing_extensions.Literal` is now automatically structured, just like `typing.Literal`.
12+
([#460](https://github.com/python-attrs/cattrs/issues/460) [#467](https://github.com/python-attrs/cattrs/pull/467))
1113
- [PEP 695](https://peps.python.org/pep-0695/) generics are now tested.
1214
([#452](https://github.com/python-attrs/cattrs/pull/452))
1315
- Imports are now sorted using Ruff.

src/cattrs/_compat.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
except ImportError: # pragma: no cover
4949
ExtensionsTypedDict = None
5050

51-
5251
if sys.version_info >= (3, 11):
5352
from builtins import ExceptionGroup
5453
else:
@@ -66,6 +65,14 @@
6665
assert sys.version_info >= (3, 11)
6766
from typing import TypeAlias
6867

68+
LITERALS = {Literal}
69+
try:
70+
from typing_extensions import Literal as teLiteral
71+
72+
LITERALS.add(teLiteral)
73+
except ImportError: # pragma: no cover
74+
pass
75+
6976

7077
def is_typeddict(cls):
7178
"""Thin wrapper around typing(_extensions).is_typeddict"""
@@ -203,7 +210,12 @@ def get_final_base(type) -> Optional[type]:
203210
from typing import _LiteralGenericAlias
204211

205212
def is_literal(type) -> bool:
206-
return type.__class__ is _LiteralGenericAlias
213+
return type in LITERALS or (
214+
isinstance(
215+
type, (_GenericAlias, _LiteralGenericAlias, _SpecialGenericAlias)
216+
)
217+
and type.__origin__ in LITERALS
218+
)
207219

208220
except ImportError: # pragma: no cover
209221

@@ -479,7 +491,9 @@ def is_counter(type):
479491
)
480492

481493
def is_literal(type) -> bool:
482-
return type.__class__ is _GenericAlias and type.__origin__ is Literal
494+
return type in LITERALS or (
495+
isinstance(type, _GenericAlias) and type.__origin__ in LITERALS
496+
)
483497

484498
def is_generic(obj):
485499
return isinstance(obj, _GenericAlias) or (

tests/test_structure_attrs.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,21 @@ class ClassWithLiteral:
150150
) == ClassWithLiteral(4)
151151

152152

153+
@pytest.mark.parametrize("converter_cls", [BaseConverter, Converter])
154+
def test_structure_typing_extensions_literal(converter_cls):
155+
"""Structuring a class with a typing_extensions.Literal field works."""
156+
converter = converter_cls()
157+
import typing_extensions
158+
159+
@define
160+
class ClassWithLiteral:
161+
literal_field: typing_extensions.Literal[8] = 8
162+
163+
assert converter.structure(
164+
{"literal_field": 8}, ClassWithLiteral
165+
) == ClassWithLiteral(8)
166+
167+
153168
@pytest.mark.parametrize("converter_cls", [BaseConverter, Converter])
154169
def test_structure_literal_enum(converter_cls):
155170
"""Structuring a class with a literal field works."""

0 commit comments

Comments
 (0)