Skip to content

Commit

Permalink
Expose mapping_unstructure_factory (#587)
Browse files Browse the repository at this point in the history
* Expose `mapping_unstructure_factory`

* Optimize a little

* Use new names

* Fix syntax

* Reformat

* Optimize and tests

* Cleanup

* Fix `unstructure_to`
  • Loading branch information
Tinche authored Oct 7, 2024
1 parent ae80674 commit 9bce8aa
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 21 deletions.
3 changes: 2 additions & 1 deletion HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
([#577](https://github.com/python-attrs/cattrs/pull/577))
- Add a [Migrations](https://catt.rs/latest/migrations.html) page, with instructions on migrating changed behavior for each version.
([#577](https://github.com/python-attrs/cattrs/pull/577))
- Expose {func}`cattrs.cols.mapping_unstructure_factory` through {mod}`cattrs.cols`.
- Python 3.13 is now supported.
([#543](https://github.com/python-attrs/cattrs/pull/543) [#547](https://github.com/python-attrs/cattrs/issues/547))

Expand All @@ -39,7 +40,7 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
([#473](https://github.com/python-attrs/cattrs/pull/473))
- **Minor change**: Heterogeneous tuples are now unstructured into tuples instead of lists by default; this is significantly faster and widely supported by serialization libraries.
([#486](https://github.com/python-attrs/cattrs/pull/486))
- **Minor change**: {py:func}`cattrs.gen.make_dict_structure_fn` will use the value for the `prefer_attrib_converters` parameter from the given converter by default now.
- **Minor change**: {func}`cattrs.gen.make_dict_structure_fn` will use the value for the `prefer_attrib_converters` parameter from the given converter by default now.
If you're using this function directly, the old behavior can be restored by passing in the desired values explicitly.
([#527](https://github.com/python-attrs/cattrs/issues/527) [#528](https://github.com/python-attrs/cattrs/pull/528))
- Introduce {meth}`BaseConverter.get_structure_hook` and {meth}`BaseConverter.get_unstructure_hook` methods.
Expand Down
1 change: 1 addition & 0 deletions docs/customizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ Available hook factories are:
* {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>`
* {meth}`mapping_unstructure_factory <cattrs.cols.mapping_unstructure_factory>`

Additional predicates and hook factories will be added as requested.

Expand Down
2 changes: 2 additions & 0 deletions src/cattrs/cols.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
make_dict_unstructure_fn_from_attrs,
make_hetero_tuple_unstructure_fn,
mapping_structure_factory,
mapping_unstructure_factory,
)
from .gen import make_iterable_unstructure_fn as iterable_unstructure_factory

Expand All @@ -48,6 +49,7 @@
"namedtuple_dict_structure_factory",
"namedtuple_dict_unstructure_factory",
"mapping_structure_factory",
"mapping_unstructure_factory",
]


Expand Down
10 changes: 5 additions & 5 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
is_namedtuple,
iterable_unstructure_factory,
list_structure_factory,
mapping_structure_factory,
mapping_unstructure_factory,
namedtuple_structure_factory,
namedtuple_unstructure_factory,
)
Expand Down Expand Up @@ -86,8 +88,6 @@
make_dict_structure_fn,
make_dict_unstructure_fn,
make_hetero_tuple_unstructure_fn,
make_mapping_structure_fn,
make_mapping_unstructure_fn,
)
from .gen.typeddicts import make_dict_structure_fn as make_typeddict_dict_struct_fn
from .gen.typeddicts import make_dict_unstructure_fn as make_typeddict_dict_unstruct_fn
Expand Down Expand Up @@ -1335,14 +1335,14 @@ def gen_unstructure_mapping(
unstructure_to = self._unstruct_collection_overrides.get(
get_origin(cl) or cl, unstructure_to or dict
)
h = make_mapping_unstructure_fn(
h = mapping_unstructure_factory(
cl, self, unstructure_to=unstructure_to, key_handler=key_handler
)
self._unstructure_func.register_cls_list([(cl, h)], direct=True)
return h

def gen_structure_counter(self, cl: Any) -> MappingStructureFn[T]:
h = make_mapping_structure_fn(
h = mapping_structure_factory(
cl,
self,
structure_to=Counter,
Expand All @@ -1361,7 +1361,7 @@ def gen_structure_mapping(self, cl: Any) -> MappingStructureFn[T]:
AbcMapping,
): # These default to dicts
structure_to = dict
h = make_mapping_structure_fn(
h = mapping_structure_factory(
cl, self, structure_to, detailed_validation=self.detailed_validation
)
self._structure_func.register_cls_list([(cl, h)], direct=True)
Expand Down
41 changes: 27 additions & 14 deletions src/cattrs/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -844,17 +844,23 @@ def make_hetero_tuple_unstructure_fn(
MappingUnstructureFn = Callable[[Mapping[Any, Any]], Any]


def make_mapping_unstructure_fn(
# This factory is here for backwards compatibility and circular imports.
def mapping_unstructure_factory(
cl: Any,
converter: BaseConverter,
unstructure_to: Any = None,
key_handler: Callable[[Any, Any | None], Any] | None = None,
) -> MappingUnstructureFn:
"""Generate a specialized unstructure function for a mapping."""
"""Generate a specialized unstructure function for a mapping.
:param unstructure_to: The class to unstructure to; defaults to the
same class as the mapping being unstructured.
"""
kh = key_handler or converter.unstructure
val_handler = converter.unstructure

fn_name = "unstructure_mapping"
origin = cl

# Let's try fishing out the type args.
if getattr(cl, "__args__", None) is not None:
Expand All @@ -873,29 +879,36 @@ def make_mapping_unstructure_fn(
if val_handler == identity:
val_handler = None

globs = {
"__cattr_mapping_cl": unstructure_to or cl,
"__cattr_k_u": kh,
"__cattr_v_u": val_handler,
}
origin = get_origin(cl)

globs = {"__cattr_k_u": kh, "__cattr_v_u": val_handler}

k_u = "__cattr_k_u(k)" if kh is not None else "k"
v_u = "__cattr_v_u(v)" if val_handler is not None else "v"

lines = []
lines = [f"def {fn_name}(mapping):"]

lines.append(f"def {fn_name}(mapping):")
lines.append(
f" res = __cattr_mapping_cl(({k_u}, {v_u}) for k, v in mapping.items())"
)
if unstructure_to is dict or unstructure_to is None and origin is dict:
if kh is None and val_handler is None:
# Simplest path.
return dict

total_lines = [*lines, " return res"]
lines.append(f" return {{{k_u}: {v_u} for k, v in mapping.items()}}")
else:
globs["__cattr_mapping_cl"] = unstructure_to or cl
lines.append(
f" res = __cattr_mapping_cl(({k_u}, {v_u}) for k, v in mapping.items())"
)

eval(compile("\n".join(total_lines), "", "exec"), globs)
lines = [*lines, " return res"]

eval(compile("\n".join(lines), "", "exec"), globs)

return globs[fn_name]


make_mapping_unstructure_fn: Final = mapping_unstructure_factory

MappingStructureFn = Callable[[Mapping[Any, Any], Any], T]


Expand Down
28 changes: 27 additions & 1 deletion tests/test_cols.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
"""Tests for the `cattrs.cols` module."""

from typing import Dict

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
from cattrs.cols import (
is_any_set,
iterable_unstructure_factory,
mapping_unstructure_factory,
)

from ._compat import is_py310_plus


def test_set_overriding(converter: BaseConverter):
Expand All @@ -26,3 +34,21 @@ def test_set_overriding(converter: BaseConverter):
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)


def test_mapping_unstructure_direct(genconverter: Converter):
"""Some cases reduce to just `dict`."""
assert genconverter.get_unstructure_hook(Dict[str, int]) is dict

# `dict` is equivalent to `dict[Any, Any]`, which should not reduce to
# just `dict`.
assert genconverter.get_unstructure_hook(dict) is not dict

if is_py310_plus:
assert genconverter.get_unstructure_hook(dict[str, int]) is dict


def test_mapping_unstructure_to(genconverter: Converter):
"""`unstructure_to` works."""
hook = mapping_unstructure_factory(Dict[str, str], genconverter, unstructure_to=Map)
assert hook({"a": "a"}).__class__ is Map

0 comments on commit 9bce8aa

Please sign in to comment.