Skip to content

Commit 946bb10

Browse files
authored
Support typing_extensions.Any (#490)
* Support `typing_extensions.Any` * Add PR link
1 parent 6a5c6f1 commit 946bb10

6 files changed

Lines changed: 40 additions & 9 deletions

File tree

HISTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
4040
([#450](https://github.com/python-attrs/cattrs/pull/450))
4141
- `typing_extensions.Literal` is now automatically structured, just like `typing.Literal`.
4242
([#460](https://github.com/python-attrs/cattrs/issues/460) [#467](https://github.com/python-attrs/cattrs/pull/467))
43+
- `typing_extensions.Any` is now supported and handled like `typing.Any`.
44+
([#488](https://github.com/python-attrs/cattrs/issues/488) [#490](https://github.com/python-attrs/cattrs/pull/490))
4345
- [PEP 695](https://peps.python.org/pep-0695/) generics are now tested.
4446
([#452](https://github.com/python-attrs/cattrs/pull/452))
4547
- Imports are now sorted using Ruff.

docs/defaulthooks.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,10 @@ When unstructuring, `typing.Any` will make the value be unstructured according t
469469
Previously, the unstructuring rules for `Any` were underspecified, leading to inconsistent behavior.
470470
```
471471

472+
```{versionchanged} 24.1.0
473+
`typing_extensions.Any` is now also supported.
474+
```
475+
472476
### `typing.Literal`
473477

474478
When structuring, [PEP 586](https://peps.python.org/pep-0586/) literals are validated to be in the allowed set of values.

src/cattrs/_compat.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from attrs import fields_dict as attrs_fields_dict
3636

3737
__all__ = [
38+
"ANIES",
3839
"adapted_fields",
3940
"fields_dict",
4041
"ExceptionGroup",
@@ -77,6 +78,15 @@
7778
except ImportError: # pragma: no cover
7879
pass
7980

81+
# On some Python versions, `typing_extensions.Any` is different than
82+
# `typing.Any`.
83+
try:
84+
from typing_extensions import Any as teAny
85+
86+
ANIES = frozenset([Any, teAny])
87+
except ImportError: # pragma: no cover
88+
ANIES = frozenset([Any])
89+
8090
NoneType = type(None)
8191

8292

src/cattrs/converters.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from attrs import has as attrs_has
1212

1313
from ._compat import (
14+
ANIES,
1415
FrozenSetSubscriptable,
1516
Mapping,
1617
MutableMapping,
@@ -171,7 +172,7 @@ def __init__(
171172
(lambda t: issubclass(t, Enum), self._unstructure_enum),
172173
(has, self._unstructure_attrs),
173174
(is_union_type, self._unstructure_union),
174-
(lambda t: t is Any, self.unstructure),
175+
(lambda t: t in ANIES, self.unstructure),
175176
]
176177
)
177178

@@ -181,7 +182,10 @@ def __init__(
181182
self._structure_func = MultiStrategyDispatch(structure_fallback_factory)
182183
self._structure_func.register_func_list(
183184
[
184-
(lambda cl: cl is Any or cl is Optional or cl is None, lambda v, _: v),
185+
(
186+
lambda cl: cl in ANIES or cl is Optional or cl is None,
187+
lambda v, _: v,
188+
),
185189
(is_generic_attrs, self._gen_structure_generic, True),
186190
(lambda t: get_newtype_base(t) is not None, self._structure_newtype),
187191
(is_type_alias, self._find_type_alias_structure_hook, True),
@@ -545,7 +549,7 @@ def structure_attrs_fromdict(self, obj: Mapping[str, Any], cl: type[T]) -> T:
545549

546550
def _structure_list(self, obj: Iterable[T], cl: Any) -> list[T]:
547551
"""Convert an iterable to a potentially generic list."""
548-
if is_bare(cl) or cl.__args__[0] is Any:
552+
if is_bare(cl) or cl.__args__[0] in ANIES:
549553
res = list(obj)
550554
else:
551555
elem_type = cl.__args__[0]
@@ -575,7 +579,7 @@ def _structure_list(self, obj: Iterable[T], cl: Any) -> list[T]:
575579

576580
def _structure_deque(self, obj: Iterable[T], cl: Any) -> deque[T]:
577581
"""Convert an iterable to a potentially generic deque."""
578-
if is_bare(cl) or cl.__args__[0] is Any:
582+
if is_bare(cl) or cl.__args__[0] in ANIES:
579583
res = deque(e for e in obj)
580584
else:
581585
elem_type = cl.__args__[0]
@@ -607,7 +611,7 @@ def _structure_set(
607611
self, obj: Iterable[T], cl: Any, structure_to: type = set
608612
) -> Set[T]:
609613
"""Convert an iterable into a potentially generic set."""
610-
if is_bare(cl) or cl.__args__[0] is Any:
614+
if is_bare(cl) or cl.__args__[0] in ANIES:
611615
return structure_to(obj)
612616
elem_type = cl.__args__[0]
613617
handler = self._structure_func.dispatch(elem_type)
@@ -646,10 +650,10 @@ def _structure_dict(self, obj: Mapping[T, V], cl: Any) -> dict[T, V]:
646650
if is_bare(cl) or cl.__args__ == (Any, Any):
647651
return dict(obj)
648652
key_type, val_type = cl.__args__
649-
if key_type is Any:
653+
if key_type in ANIES:
650654
val_conv = self._structure_func.dispatch(val_type)
651655
return {k: val_conv(v, val_type) for k, v in obj.items()}
652-
if val_type is Any:
656+
if val_type in ANIES:
653657
key_conv = self._structure_func.dispatch(key_type)
654658
return {key_conv(k, key_type): v for k, v in obj.items()}
655659
key_conv = self._structure_func.dispatch(key_type)
@@ -673,7 +677,7 @@ def _structure_tuple(self, obj: Any, tup: type[T]) -> T:
673677
"""Deal with structuring into a tuple."""
674678
tup_params = None if tup in (Tuple, tuple) else tup.__args__
675679
has_ellipsis = tup_params and tup_params[-1] is Ellipsis
676-
if tup_params is None or (has_ellipsis and tup_params[0] is Any):
680+
if tup_params is None or (has_ellipsis and tup_params[0] in ANIES):
677681
# Just a Tuple. (No generic information.)
678682
return tuple(obj)
679683
if has_ellipsis:

src/cattrs/gen/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from attrs import NOTHING, Factory, resolve_types
77

88
from .._compat import (
9+
ANIES,
910
TypeAlias,
1011
adapted_fields,
1112
get_args,
@@ -831,7 +832,7 @@ def make_mapping_structure_fn(
831832
(key_type,) = args
832833
val_type = Any
833834

834-
is_bare_dict = val_type is Any and key_type is Any
835+
is_bare_dict = val_type in ANIES and key_type in ANIES
835836
if not is_bare_dict:
836837
# We can do the dispatch here and now.
837838
key_handler = converter.get_structure_hook(key_type, cache_result=False)

tests/test_any.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Any, Dict, Optional
33

44
from attrs import define
5+
from typing_extensions import Any as ExtendedAny
56

67

78
@define
@@ -24,3 +25,12 @@ def test_unstructure_optional_any(converter):
2425
"""Unstructuring `Optional[Any]` should use the runtime value."""
2526

2627
assert converter.unstructure(A(), Optional[Any]) == {}
28+
29+
30+
def test_extended_any(converter):
31+
"""`typing_extensions.Any` works."""
32+
33+
assert converter.unstructure(A(), unstructure_as=ExtendedAny) == {}
34+
35+
d = {}
36+
assert converter.structure(d, ExtendedAny) is d

0 commit comments

Comments
 (0)