Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
([#463](https://github.com/python-attrs/cattrs/pull/463))
- More robust support for `Annotated` and `NotRequired` in TypedDicts.
([#450](https://github.com/python-attrs/cattrs/pull/450))
- `typing_extensions.Literal` is now automatically structured, just like `typing.Literal`.
([#460](https://github.com/python-attrs/cattrs/issues/460) [#467](https://github.com/python-attrs/cattrs/pull/467))
- [PEP 695](https://peps.python.org/pep-0695/) generics are now tested.
([#452](https://github.com/python-attrs/cattrs/pull/452))
- Imports are now sorted using Ruff.
Expand Down
20 changes: 17 additions & 3 deletions src/cattrs/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
except ImportError: # pragma: no cover
ExtensionsTypedDict = None


if sys.version_info >= (3, 11):
from builtins import ExceptionGroup
else:
Expand All @@ -66,6 +65,14 @@
assert sys.version_info >= (3, 11)
from typing import TypeAlias

LITERALS = {Literal}
try:
from typing_extensions import Literal as teLiteral

LITERALS.add(teLiteral)
except ImportError: # pragma: no cover
pass


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

def is_literal(type) -> bool:
return type.__class__ is _LiteralGenericAlias
return type in LITERALS or (
isinstance(
type, (_GenericAlias, _LiteralGenericAlias, _SpecialGenericAlias)
)
and type.__origin__ in LITERALS
)
Comment on lines +213 to +218
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is isomorphic to what typing-inspect does and it works in our company's testing as well.


except ImportError: # pragma: no cover

Expand Down Expand Up @@ -479,7 +491,9 @@ def is_counter(type):
)

def is_literal(type) -> bool:
return type.__class__ is _GenericAlias and type.__origin__ is Literal
return type in LITERALS or (
isinstance(type, _GenericAlias) and type.__origin__ in LITERALS
)

def is_generic(obj):
return isinstance(obj, _GenericAlias) or (
Expand Down
15 changes: 15 additions & 0 deletions tests/test_structure_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,21 @@ class ClassWithLiteral:
) == ClassWithLiteral(4)


@pytest.mark.parametrize("converter_cls", [BaseConverter, Converter])
def test_structure_typing_extensions_literal(converter_cls):
"""Structuring a class with a typing_extensions.Literal field works."""
converter = converter_cls()
import typing_extensions

@define
class ClassWithLiteral:
literal_field: typing_extensions.Literal[8] = 8

assert converter.structure(
{"literal_field": 8}, ClassWithLiteral
) == ClassWithLiteral(8)


@pytest.mark.parametrize("converter_cls", [BaseConverter, Converter])
def test_structure_literal_enum(converter_cls):
"""Structuring a class with a literal field works."""
Expand Down