Skip to content

Commit

Permalink
Support typing_extensions.Any (#490)
Browse files Browse the repository at this point in the history
* Support `typing_extensions.Any`

* Add PR link
  • Loading branch information
Tinche authored Jan 21, 2024
1 parent 6a5c6f1 commit 946bb10
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 9 deletions.
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
([#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))
- `typing_extensions.Any` is now supported and handled like `typing.Any`.
([#488](https://github.com/python-attrs/cattrs/issues/488) [#490](https://github.com/python-attrs/cattrs/pull/490))
- [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
4 changes: 4 additions & 0 deletions docs/defaulthooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,10 @@ When unstructuring, `typing.Any` will make the value be unstructured according t
Previously, the unstructuring rules for `Any` were underspecified, leading to inconsistent behavior.
```

```{versionchanged} 24.1.0
`typing_extensions.Any` is now also supported.
```

### `typing.Literal`

When structuring, [PEP 586](https://peps.python.org/pep-0586/) literals are validated to be in the allowed set of values.
Expand Down
10 changes: 10 additions & 0 deletions src/cattrs/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from attrs import fields_dict as attrs_fields_dict

__all__ = [
"ANIES",
"adapted_fields",
"fields_dict",
"ExceptionGroup",
Expand Down Expand Up @@ -77,6 +78,15 @@
except ImportError: # pragma: no cover
pass

# On some Python versions, `typing_extensions.Any` is different than
# `typing.Any`.
try:
from typing_extensions import Any as teAny

ANIES = frozenset([Any, teAny])
except ImportError: # pragma: no cover
ANIES = frozenset([Any])

NoneType = type(None)


Expand Down
20 changes: 12 additions & 8 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from attrs import has as attrs_has

from ._compat import (
ANIES,
FrozenSetSubscriptable,
Mapping,
MutableMapping,
Expand Down Expand Up @@ -171,7 +172,7 @@ def __init__(
(lambda t: issubclass(t, Enum), self._unstructure_enum),
(has, self._unstructure_attrs),
(is_union_type, self._unstructure_union),
(lambda t: t is Any, self.unstructure),
(lambda t: t in ANIES, self.unstructure),
]
)

Expand All @@ -181,7 +182,10 @@ def __init__(
self._structure_func = MultiStrategyDispatch(structure_fallback_factory)
self._structure_func.register_func_list(
[
(lambda cl: cl is Any or cl is Optional or cl is None, lambda v, _: v),
(
lambda cl: cl in ANIES or cl is Optional or cl is None,
lambda v, _: v,
),
(is_generic_attrs, self._gen_structure_generic, True),
(lambda t: get_newtype_base(t) is not None, self._structure_newtype),
(is_type_alias, self._find_type_alias_structure_hook, True),
Expand Down Expand Up @@ -545,7 +549,7 @@ def structure_attrs_fromdict(self, obj: Mapping[str, Any], cl: type[T]) -> T:

def _structure_list(self, obj: Iterable[T], cl: Any) -> list[T]:
"""Convert an iterable to a potentially generic list."""
if is_bare(cl) or cl.__args__[0] is Any:
if is_bare(cl) or cl.__args__[0] in ANIES:
res = list(obj)
else:
elem_type = cl.__args__[0]
Expand Down Expand Up @@ -575,7 +579,7 @@ def _structure_list(self, obj: Iterable[T], cl: Any) -> list[T]:

def _structure_deque(self, obj: Iterable[T], cl: Any) -> deque[T]:
"""Convert an iterable to a potentially generic deque."""
if is_bare(cl) or cl.__args__[0] is Any:
if is_bare(cl) or cl.__args__[0] in ANIES:
res = deque(e for e in obj)
else:
elem_type = cl.__args__[0]
Expand Down Expand Up @@ -607,7 +611,7 @@ def _structure_set(
self, obj: Iterable[T], cl: Any, structure_to: type = set
) -> Set[T]:
"""Convert an iterable into a potentially generic set."""
if is_bare(cl) or cl.__args__[0] is Any:
if is_bare(cl) or cl.__args__[0] in ANIES:
return structure_to(obj)
elem_type = cl.__args__[0]
handler = self._structure_func.dispatch(elem_type)
Expand Down Expand Up @@ -646,10 +650,10 @@ def _structure_dict(self, obj: Mapping[T, V], cl: Any) -> dict[T, V]:
if is_bare(cl) or cl.__args__ == (Any, Any):
return dict(obj)
key_type, val_type = cl.__args__
if key_type is Any:
if key_type in ANIES:
val_conv = self._structure_func.dispatch(val_type)
return {k: val_conv(v, val_type) for k, v in obj.items()}
if val_type is Any:
if val_type in ANIES:
key_conv = self._structure_func.dispatch(key_type)
return {key_conv(k, key_type): v for k, v in obj.items()}
key_conv = self._structure_func.dispatch(key_type)
Expand All @@ -673,7 +677,7 @@ def _structure_tuple(self, obj: Any, tup: type[T]) -> T:
"""Deal with structuring into a tuple."""
tup_params = None if tup in (Tuple, tuple) else tup.__args__
has_ellipsis = tup_params and tup_params[-1] is Ellipsis
if tup_params is None or (has_ellipsis and tup_params[0] is Any):
if tup_params is None or (has_ellipsis and tup_params[0] in ANIES):
# Just a Tuple. (No generic information.)
return tuple(obj)
if has_ellipsis:
Expand Down
3 changes: 2 additions & 1 deletion src/cattrs/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from attrs import NOTHING, Factory, resolve_types

from .._compat import (
ANIES,
TypeAlias,
adapted_fields,
get_args,
Expand Down Expand Up @@ -831,7 +832,7 @@ def make_mapping_structure_fn(
(key_type,) = args
val_type = Any

is_bare_dict = val_type is Any and key_type is Any
is_bare_dict = val_type in ANIES and key_type in ANIES
if not is_bare_dict:
# We can do the dispatch here and now.
key_handler = converter.get_structure_hook(key_type, cache_result=False)
Expand Down
10 changes: 10 additions & 0 deletions tests/test_any.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Any, Dict, Optional

from attrs import define
from typing_extensions import Any as ExtendedAny


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

assert converter.unstructure(A(), Optional[Any]) == {}


def test_extended_any(converter):
"""`typing_extensions.Any` works."""

assert converter.unstructure(A(), unstructure_as=ExtendedAny) == {}

d = {}
assert converter.structure(d, ExtendedAny) is d

0 comments on commit 946bb10

Please # to comment.