|
1 | 1 | """The list-from-dict implementation.""" |
2 | 2 |
|
| 3 | +from __future__ import annotations |
| 4 | + |
3 | 5 | from collections.abc import Mapping |
4 | 6 | from typing import Any, TypeVar, get_args |
5 | 7 |
|
| 8 | +from attrs import Attribute |
| 9 | + |
6 | 10 | from .. import BaseConverter, SimpleStructureHook |
7 | 11 | from ..dispatch import UnstructureHook |
| 12 | +from ..fns import identity |
| 13 | +from ..gen.typeddicts import is_typeddict |
8 | 14 |
|
9 | 15 | T = TypeVar("T") |
10 | 16 |
|
11 | 17 |
|
12 | 18 | def configure_list_from_dict( |
13 | | - seq_type: list[T], field: str, converter: BaseConverter |
| 19 | + seq_type: list[T], field: str | Attribute, converter: BaseConverter |
14 | 20 | ) -> tuple[SimpleStructureHook[Mapping, T], UnstructureHook]: |
15 | 21 | """ |
16 | | - Configure a list subtype to be structured and unstructured using a dictionary. |
| 22 | + Configure a list subtype to be structured and unstructured into a dictionary, |
| 23 | + using a single field of the element as the dictionary key. This effectively |
| 24 | + ensures the resulting list is unique with regard to that field. |
| 25 | +
|
| 26 | + List elements have to be able to be structured/unstructured using mappings. |
| 27 | + One field of the element is extracted into a dictionary key; the rest of the |
| 28 | + data is stored under that key. |
| 29 | +
|
| 30 | + The types un/structuring into dictionaries by default are: |
| 31 | + * attrs classes and dataclasses |
| 32 | + * TypedDicts |
| 33 | + * named tuples when using the `namedtuple_dict_un/structure_factory` |
17 | 34 |
|
18 | | - List elements have to be an attrs class or a dataclass. One field of the element |
19 | | - type is extracted into a dictionary key; the rest of the data is stored under that |
20 | | - key. |
| 35 | + :param field: The name of the field to extract. When working with _attrs_ classes, |
| 36 | + consider passing in the attribute (as returned by `attrs.field(cls)`) for |
| 37 | + added safety. |
| 38 | +
|
| 39 | + :return: A tuple of generated structure and unstructure hooks. |
| 40 | +
|
| 41 | + .. versionadded:: 24.2.0 |
21 | 42 |
|
22 | 43 | """ |
23 | 44 | arg_type = get_args(seq_type)[0] |
24 | 45 |
|
25 | 46 | arg_structure_hook = converter.get_structure_hook(arg_type, cache_result=False) |
26 | 47 |
|
| 48 | + if isinstance(field, Attribute): |
| 49 | + field = field.name |
| 50 | + |
27 | 51 | def structure_hook( |
28 | | - value: Mapping, type: Any = seq_type, _arg_type=arg_type |
| 52 | + value: Mapping, |
| 53 | + _: Any = seq_type, |
| 54 | + _arg_type=arg_type, |
| 55 | + _arg_hook=arg_structure_hook, |
| 56 | + _field=field, |
29 | 57 | ) -> list[T]: |
30 | | - return [arg_structure_hook(v | {field: k}, _arg_type) for k, v in value.items()] |
| 58 | + return [_arg_hook(v | {_field: k}, _arg_type) for k, v in value.items()] |
31 | 59 |
|
32 | 60 | arg_unstructure_hook = converter.get_unstructure_hook(arg_type, cache_result=False) |
33 | 61 |
|
34 | | - def unstructure_hook(val: list[T]) -> dict: |
35 | | - return { |
36 | | - (unstructured := arg_unstructure_hook(v)).pop(field): unstructured |
37 | | - for v in val |
38 | | - } |
| 62 | + # TypedDicts can end up being unstructured via identity, in that case we make a copy |
| 63 | + # so we don't destroy the original. |
| 64 | + if is_typeddict(arg_type) and arg_unstructure_hook == identity: |
| 65 | + arg_unstructure_hook = dict |
| 66 | + |
| 67 | + def unstructure_hook(val: list[T], _arg_hook=arg_unstructure_hook) -> dict: |
| 68 | + return {(unstructured := _arg_hook(v)).pop(field): unstructured for v in val} |
39 | 69 |
|
40 | 70 | return structure_hook, unstructure_hook |
0 commit comments