-
-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
113 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,40 +1,70 @@ | ||
"""The list-from-dict implementation.""" | ||
|
||
from __future__ import annotations | ||
|
||
from collections.abc import Mapping | ||
from typing import Any, TypeVar, get_args | ||
|
||
from attrs import Attribute | ||
|
||
from .. import BaseConverter, SimpleStructureHook | ||
from ..dispatch import UnstructureHook | ||
from ..fns import identity | ||
from ..gen.typeddicts import is_typeddict | ||
|
||
T = TypeVar("T") | ||
|
||
|
||
def configure_list_from_dict( | ||
seq_type: list[T], field: str, converter: BaseConverter | ||
seq_type: list[T], field: str | Attribute, converter: BaseConverter | ||
) -> tuple[SimpleStructureHook[Mapping, T], UnstructureHook]: | ||
""" | ||
Configure a list subtype to be structured and unstructured using a dictionary. | ||
Configure a list subtype to be structured and unstructured into a dictionary, | ||
using a single field of the element as the dictionary key. This effectively | ||
ensures the resulting list is unique with regard to that field. | ||
List elements have to be able to be structured/unstructured using mappings. | ||
One field of the element is extracted into a dictionary key; the rest of the | ||
data is stored under that key. | ||
The types un/structuring into dictionaries by default are: | ||
* attrs classes and dataclasses | ||
* TypedDicts | ||
* named tuples when using the `namedtuple_dict_un/structure_factory` | ||
List elements have to be an attrs class or a dataclass. One field of the element | ||
type is extracted into a dictionary key; the rest of the data is stored under that | ||
key. | ||
:param field: The name of the field to extract. When working with _attrs_ classes, | ||
consider passing in the attribute (as returned by `attrs.field(cls)`) for | ||
added safety. | ||
:return: A tuple of generated structure and unstructure hooks. | ||
.. versionadded:: 24.2.0 | ||
""" | ||
arg_type = get_args(seq_type)[0] | ||
|
||
arg_structure_hook = converter.get_structure_hook(arg_type, cache_result=False) | ||
|
||
if isinstance(field, Attribute): | ||
field = field.name | ||
|
||
def structure_hook( | ||
value: Mapping, type: Any = seq_type, _arg_type=arg_type | ||
value: Mapping, | ||
_: Any = seq_type, | ||
_arg_type=arg_type, | ||
_arg_hook=arg_structure_hook, | ||
_field=field, | ||
) -> list[T]: | ||
return [arg_structure_hook(v | {field: k}, _arg_type) for k, v in value.items()] | ||
return [_arg_hook(v | {_field: k}, _arg_type) for k, v in value.items()] | ||
|
||
arg_unstructure_hook = converter.get_unstructure_hook(arg_type, cache_result=False) | ||
|
||
def unstructure_hook(val: list[T]) -> dict: | ||
return { | ||
(unstructured := arg_unstructure_hook(v)).pop(field): unstructured | ||
for v in val | ||
} | ||
# TypedDicts can end up being unstructured via identity, in that case we make a copy | ||
# so we don't destroy the original. | ||
if is_typeddict(arg_type) and arg_unstructure_hook == identity: | ||
arg_unstructure_hook = dict | ||
|
||
def unstructure_hook(val: list[T], _arg_hook=arg_unstructure_hook) -> dict: | ||
return {(unstructured := _arg_hook(v)).pop(field): unstructured for v in val} | ||
|
||
return structure_hook, unstructure_hook |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
"""Tests for the list-from-dict strategy.""" | ||
|
||
from dataclasses import dataclass | ||
from typing import TypedDict | ||
|
||
import pytest | ||
from attrs import define, fields | ||
|
||
from cattrs import BaseConverter | ||
from cattrs.strategies import configure_list_from_dict | ||
|
||
|
||
@define | ||
class AttrsA: | ||
a: int | ||
b: str | ||
|
||
|
||
@dataclass | ||
class DataclassA: | ||
a: int | ||
b: str | ||
|
||
|
||
class TypedDictA(TypedDict): | ||
a: int | ||
b: str | ||
|
||
|
||
@pytest.mark.parametrize("cls", [AttrsA, DataclassA, TypedDictA]) | ||
def test_simple_roundtrip( | ||
cls: type[AttrsA] | type[DataclassA], converter: BaseConverter | ||
): | ||
hook, hook2 = configure_list_from_dict(list[cls], "a", converter) | ||
|
||
structured = [cls(a=1, b="2"), cls(a=3, b="4")] | ||
unstructured = hook2(structured) | ||
assert unstructured == {1: {"b": "2"}, 3: {"b": "4"}} | ||
|
||
assert hook(unstructured) == structured | ||
|
||
|
||
def test_simple_roundtrip_attrs(converter: BaseConverter): | ||
hook, hook2 = configure_list_from_dict(list[AttrsA], fields(AttrsA).a, converter) | ||
|
||
structured = [AttrsA(a=1, b="2"), AttrsA(a=3, b="4")] | ||
unstructured = hook2(structured) | ||
assert unstructured == {1: {"b": "2"}, 3: {"b": "4"}} | ||
|
||
assert hook(unstructured) == structured |