Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pull out and document is_mapping and mapping_structure_factory #556

Merged
merged 5 commits into from
Jul 24, 2024
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
7 changes: 5 additions & 2 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
- Introduce {meth}`BaseConverter.get_structure_hook` and {meth}`BaseConverter.get_unstructure_hook` methods.
([#432](https://github.com/python-attrs/cattrs/issues/432) [#472](https://github.com/python-attrs/cattrs/pull/472))
- {meth}`BaseConverter.register_structure_hook`, {meth}`BaseConverter.register_unstructure_hook`,
{meth}`BaseConverter.register_unstructure_hook_factory` and {meth}`BaseConverter.register_structure_hook_factory`
can now be used as decorators and have gained new features.
{meth}`BaseConverter.register_unstructure_hook_factory` and {meth}`BaseConverter.register_structure_hook_factory`
can now be used as decorators and have gained new features.
See [here](https://catt.rs/en/latest/customizing.html#use-as-decorators) and [here](https://catt.rs/en/latest/customizing.html#id1) for more details.
([#487](https://github.com/python-attrs/cattrs/pull/487))
- Introduce and [document](https://catt.rs/en/latest/customizing.html#customizing-collections) the {mod}`cattrs.cols` module for better collection customizations.
([#504](https://github.com/python-attrs/cattrs/issues/504) [#540](https://github.com/python-attrs/cattrs/pull/540))
- Enhance the {func}`cattrs.cols.is_mapping` predicate function to also cover virtual subclasses of `abc.Mapping`.
This enables map classes from libraries such as _immutables_ or _sortedcontainers_ to structure out-of-the-box.
([#555](https://github.com/python-attrs/cattrs/issues/555) [#556](https://github.com/python-attrs/cattrs/pull/556))
- Introduce the [_msgspec_](https://jcristharif.com/msgspec/) {mod}`preconf converter <cattrs.preconf.msgspec>`.
Only JSON is supported for now, with other formats supported by _msgspec_ to come later.
([#481](https://github.com/python-attrs/cattrs/pull/481))
Expand Down
2 changes: 2 additions & 0 deletions docs/customizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ Available predicates are:
* {meth}`is_frozenset <cattrs.cols.is_frozenset>`
* {meth}`is_set <cattrs.cols.is_set>`
* {meth}`is_sequence <cattrs.cols.is_sequence>`
* {meth}`is_mapping <cattrs.cols.is_mapping>`
* {meth}`is_namedtuple <cattrs.cols.is_namedtuple>`

````{tip}
Expand All @@ -187,6 +188,7 @@ Available hook factories are:
* {meth}`namedtuple_unstructure_factory <cattrs.cols.namedtuple_unstructure_factory>`
* {meth}`namedtuple_dict_structure_factory <cattrs.cols.namedtuple_dict_structure_factory>`
* {meth}`namedtuple_dict_unstructure_factory <cattrs.cols.namedtuple_dict_unstructure_factory>`
* {meth}`mapping_structure_factory <cattrs.cols.mapping_structure_factory>`

Additional predicates and hook factories will be added as requested.

Expand Down
14 changes: 9 additions & 5 deletions docs/defaulthooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,13 @@ A useful use case for unstructuring collections is to create a deep copy of a co
### Dictionaries

Dictionaries can be produced from other mapping objects.
More precisely, the unstructured object must expose an [`items()`](https://docs.python.org/3/library/stdtypes.html#dict.items) method producing an iterable of key-value tuples, and be able to be passed to the `dict` constructor as an argument.
More precisely, the unstructured object must expose an [`items()`](https://docs.python.org/3/library/stdtypes.html#dict.items) method producing an iterable of key-value tuples,
and be able to be passed to the `dict` constructor as an argument.
Types converting to dictionaries are:

- `typing.Dict[K, V]`
- `typing.MutableMapping[K, V]`
- `typing.Mapping[K, V]`
- `dict[K, V]`
- `dict[K, V]` and `typing.Dict[K, V]`
- `collections.abc.MutableMapping[K, V]` and `typing.MutableMapping[K, V]`
- `collections.abc.Mapping[K, V]` and `typing.Mapping[K, V]`

In all cases, a new dict will be returned, so this operation can be used to copy a mapping into a dict.
Any type parameters set to `typing.Any` will be passed through unconverted.
Expand All @@ -183,6 +183,10 @@ Both keys and values are converted.
{'1': None, '2': 2}
```

### Virtual Subclasses of [`abc.Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) and [`abc.MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

If a class declares itself a virtual subclass of `collections.abc.Mapping` or `collections.abc.MutableMapping` and its initializer accepts a dictionary,
_cattrs_ will be able to structure it by default.

### Homogeneous and Heterogeneous Tuples

Expand Down
28 changes: 17 additions & 11 deletions src/cattrs/_compat.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import sys
from collections import deque
from collections.abc import Mapping as AbcMapping
from collections.abc import MutableMapping as AbcMutableMapping
from collections.abc import MutableSet as AbcMutableSet
from collections.abc import Set as AbcSet
from dataclasses import MISSING, Field, is_dataclass
Expand Down Expand Up @@ -219,8 +221,6 @@ def get_final_base(type) -> Optional[type]:

if sys.version_info >= (3, 9):
from collections import Counter
from collections.abc import Mapping as AbcMapping
from collections.abc import MutableMapping as AbcMutableMapping
from collections.abc import MutableSequence as AbcMutableSequence
from collections.abc import MutableSet as AbcMutableSet
from collections.abc import Sequence as AbcSequence
Expand Down Expand Up @@ -404,18 +404,17 @@ def is_bare(type):
not hasattr(type, "__origin__") and not hasattr(type, "__args__")
)

def is_mapping(type):
def is_mapping(type: Any) -> bool:
"""A predicate function for mappings."""
return (
type in (dict, Dict, TypingMapping, TypingMutableMapping, AbcMutableMapping)
or (
type.__class__ is _GenericAlias
and is_subclass(type.__origin__, TypingMapping)
)
or (
getattr(type, "__origin__", None)
in (dict, AbcMutableMapping, AbcMapping)
or is_subclass(
getattr(type, "__origin__", type), (dict, AbcMutableMapping, AbcMapping)
)
or is_subclass(type, dict)
)

def is_counter(type):
Expand Down Expand Up @@ -515,10 +514,17 @@ def is_frozenset(type):
type.__class__ is _GenericAlias and is_subclass(type.__origin__, FrozenSet)
)

def is_mapping(type):
return type in (TypingMapping, dict) or (
type.__class__ is _GenericAlias
and is_subclass(type.__origin__, TypingMapping)
def is_mapping(type: Any) -> bool:
"""A predicate function for mappings."""
return (
type in (TypingMapping, dict)
or (
type.__class__ is _GenericAlias
and is_subclass(type.__origin__, TypingMapping)
)
or is_subclass(
getattr(type, "__origin__", type), (dict, AbcMutableMapping, AbcMapping)
)
)

bare_generic_args = {
Expand Down
5 changes: 4 additions & 1 deletion src/cattrs/cols.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from attrs import NOTHING, Attribute

from ._compat import ANIES, is_bare, is_frozenset, is_sequence, is_subclass
from ._compat import ANIES, is_bare, is_frozenset, is_mapping, is_sequence, is_subclass
from ._compat import is_mutable_set as is_set
from .dispatch import StructureHook, UnstructureHook
from .errors import IterableValidationError, IterableValidationNote
Expand All @@ -27,6 +27,7 @@
make_dict_structure_fn_from_attrs,
make_dict_unstructure_fn_from_attrs,
make_hetero_tuple_unstructure_fn,
mapping_structure_factory,
)
from .gen import make_iterable_unstructure_fn as iterable_unstructure_factory

Expand All @@ -37,6 +38,7 @@
"is_any_set",
"is_frozenset",
"is_namedtuple",
"is_mapping",
"is_set",
"is_sequence",
"iterable_unstructure_factory",
Expand All @@ -45,6 +47,7 @@
"namedtuple_unstructure_factory",
"namedtuple_dict_structure_factory",
"namedtuple_dict_unstructure_factory",
"mapping_structure_factory",
]


Expand Down
12 changes: 11 additions & 1 deletion src/cattrs/converters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

from collections import Counter, deque
from collections.abc import Mapping as AbcMapping
from collections.abc import MutableMapping as AbcMutableMapping
from collections.abc import MutableSet as AbcMutableSet
from dataclasses import Field
from enum import Enum
Expand Down Expand Up @@ -1289,8 +1291,16 @@ def gen_structure_counter(self, cl: Any) -> MappingStructureFn[T]:
return h

def gen_structure_mapping(self, cl: Any) -> MappingStructureFn[T]:
structure_to = get_origin(cl) or cl
if structure_to in (
MutableMapping,
AbcMutableMapping,
Mapping,
AbcMapping,
): # These default to dicts
structure_to = dict
h = make_mapping_structure_fn(
cl, self, detailed_validation=self.detailed_validation
cl, self, structure_to, detailed_validation=self.detailed_validation
)
self._structure_func.register_cls_list([(cl, h)], direct=True)
return h
Expand Down
6 changes: 5 additions & 1 deletion src/cattrs/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,8 @@ def make_mapping_unstructure_fn(
MappingStructureFn = Callable[[Mapping[Any, Any], Any], T]


def make_mapping_structure_fn(
# This factory is here for backwards compatibility and circular imports.
def mapping_structure_factory(
cl: type[T],
converter: BaseConverter,
structure_to: type = dict,
Expand Down Expand Up @@ -1018,6 +1019,9 @@ def make_mapping_structure_fn(
return globs[fn_name]


make_mapping_structure_fn: Final = mapping_structure_factory


# This factory is here for backwards compatibility and circular imports.
def iterable_unstructure_factory(
cl: Any, converter: BaseConverter, unstructure_to: Any = None
Expand Down
9 changes: 8 additions & 1 deletion tests/test_cols.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Tests for the `cattrs.cols` module."""

from cattrs import BaseConverter
from immutables import Map

from cattrs import BaseConverter, Converter
from cattrs._compat import AbstractSet, FrozenSet
from cattrs.cols import is_any_set, iterable_unstructure_factory

Expand All @@ -19,3 +21,8 @@ def test_set_overriding(converter: BaseConverter):
"b",
"c",
]


def test_structuring_immutables_map(genconverter: Converter):
"""This should work due to our new is_mapping predicate."""
assert genconverter.structure({"a": 1}, Map[str, int]) == Map(a=1)
Loading