Skip to content

Commit f81d9af

Browse files
authored
Tin/defaultdicts (#588)
* Defaultdicts WIP * Reformat * Docs * More docs * Tweak docs * Introduce SimpleStructureHook
1 parent 456c749 commit f81d9af

File tree

9 files changed

+299
-143
lines changed

9 files changed

+299
-143
lines changed

HISTORY.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,21 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
1414

1515
- **Potentially breaking**: The converters raise {class}`StructureHandlerNotFoundError` more eagerly (on hook creation, instead of on hook use).
1616
This helps surfacing problems with missing hooks sooner.
17-
See [Migrations](https://catt.rs/latest/migrations.html#the-default-structure-hook-fallback-factory) for steps to restore legacy behavior.
17+
See [Migrations](https://catt.rs/en/latest/migrations.html#the-default-structure-hook-fallback-factory) for steps to restore legacy behavior.
1818
([#577](https://github.com/python-attrs/cattrs/pull/577))
19-
- Add a [Migrations](https://catt.rs/latest/migrations.html) page, with instructions on migrating changed behavior for each version.
19+
- Add a [Migrations](https://catt.rs/en/latest/migrations.html) page, with instructions on migrating changed behavior for each version.
2020
([#577](https://github.com/python-attrs/cattrs/pull/577))
2121
- Expose {func}`cattrs.cols.mapping_unstructure_factory` through {mod}`cattrs.cols`.
22+
- Some `defaultdicts` are now [supported by default](https://catt.rs/en/latest/defaulthooks.html#defaultdicts), and
23+
{func}`cattrs.cols.is_defaultdict`{func} and `cattrs.cols.defaultdict_structure_factory` are exposed through {mod}`cattrs.cols`.
24+
([#519](https://github.com/python-attrs/cattrs/issues/519) [#588](https://github.com/python-attrs/cattrs/pull/588))
25+
- Replace `cattrs.gen.MappingStructureFn` with `cattrs.SimpleStructureHook[In, T]`.
2226
- Python 3.13 is now supported.
2327
([#543](https://github.com/python-attrs/cattrs/pull/543) [#547](https://github.com/python-attrs/cattrs/issues/547))
2428
- Python 3.8 is no longer supported, as it is end-of-life. Use previous versions on this Python version.
25-
- Change type of Converter.__init__.unstruct_collection_overrides from Callable to Mapping[type, UnstructureHook] ([#594](https://github.com/python-attrs/cattrs/pull/594).
29+
([#591](https://github.com/python-attrs/cattrs/pull/591))
30+
- Change type of `Converter.__init__.unstruct_collection_overrides` from `Callable` to `Mapping[type, UnstructureHook]`
31+
([#594](https://github.com/python-attrs/cattrs/pull/594)).
2632

2733
## 24.1.2 (2024-09-22)
2834

docs/customizing.md

Lines changed: 124 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -155,131 +155,20 @@ Here's an example of using an unstructure hook factory to handle unstructuring [
155155
[1, 2]
156156
```
157157

158-
## Customizing Collections
159-
160-
The {mod}`cattrs.cols` module contains predicates and hook factories useful for customizing collection handling.
161-
These hook factories can be wrapped to apply complex customizations.
162-
163-
Available predicates are:
164-
165-
* {meth}`is_any_set <cattrs.cols.is_any_set>`
166-
* {meth}`is_frozenset <cattrs.cols.is_frozenset>`
167-
* {meth}`is_set <cattrs.cols.is_set>`
168-
* {meth}`is_sequence <cattrs.cols.is_sequence>`
169-
* {meth}`is_mapping <cattrs.cols.is_mapping>`
170-
* {meth}`is_namedtuple <cattrs.cols.is_namedtuple>`
171-
172-
````{tip}
173-
These predicates aren't _cattrs_-specific and may be useful in other contexts.
174-
```{doctest} predicates
175-
>>> from cattrs.cols import is_sequence
176-
177-
>>> is_sequence(list[str])
178-
True
179-
```
180-
````
181-
182-
183-
Available hook factories are:
184-
185-
* {meth}`iterable_unstructure_factory <cattrs.cols.iterable_unstructure_factory>`
186-
* {meth}`list_structure_factory <cattrs.cols.list_structure_factory>`
187-
* {meth}`namedtuple_structure_factory <cattrs.cols.namedtuple_structure_factory>`
188-
* {meth}`namedtuple_unstructure_factory <cattrs.cols.namedtuple_unstructure_factory>`
189-
* {meth}`namedtuple_dict_structure_factory <cattrs.cols.namedtuple_dict_structure_factory>`
190-
* {meth}`namedtuple_dict_unstructure_factory <cattrs.cols.namedtuple_dict_unstructure_factory>`
191-
* {meth}`mapping_structure_factory <cattrs.cols.mapping_structure_factory>`
192-
* {meth}`mapping_unstructure_factory <cattrs.cols.mapping_unstructure_factory>`
193-
194-
Additional predicates and hook factories will be added as requested.
195-
196-
For example, by default sequences are structured from any iterable into lists.
197-
This may be too lax, and additional validation may be applied by wrapping the default list structuring hook factory.
198-
199-
```{testcode} list-customization
200-
from cattrs.cols import is_sequence, list_structure_factory
201-
202-
c = Converter()
203-
204-
@c.register_structure_hook_factory(is_sequence)
205-
def strict_list_hook_factory(type, converter):
206-
207-
# First, we generate the default hook...
208-
list_hook = list_structure_factory(type, converter)
209-
210-
# Then, we wrap it with a function of our own...
211-
def strict_list_hook(value, type):
212-
if not isinstance(value, list):
213-
raise ValueError("Not a list!")
214-
return list_hook(value, type)
158+
## Using `cattrs.gen` Hook Factories
215159

216-
# And finally, we return our own composite hook.
217-
return strict_list_hook
218-
```
219-
220-
Now, all sequence structuring will be stricter:
221-
222-
```{doctest} list-customization
223-
>>> c.structure({"a", "b", "c"}, list[str])
224-
Traceback (most recent call last):
225-
...
226-
ValueError: Not a list!
227-
```
228-
229-
```{versionadded} 24.1.0
230-
231-
```
232-
233-
### Customizing Named Tuples
234-
235-
Named tuples can be un/structured using dictionaries using the {meth}`namedtuple_dict_structure_factory <cattrs.cols.namedtuple_dict_structure_factory>`
236-
and {meth}`namedtuple_dict_unstructure_factory <cattrs.cols.namedtuple_dict_unstructure_factory>`
237-
hook factories.
238-
239-
To unstructure _all_ named tuples into dictionaries:
240-
241-
```{doctest} namedtuples
242-
>>> from typing import NamedTuple
243-
244-
>>> from cattrs.cols import is_namedtuple, namedtuple_dict_unstructure_factory
245-
>>> c = Converter()
246-
247-
>>> c.register_unstructure_hook_factory(is_namedtuple, namedtuple_dict_unstructure_factory)
248-
<function namedtuple_dict_unstructure_factory at ...>
249-
250-
>>> class MyNamedTuple(NamedTuple):
251-
... a: int
252-
253-
>>> c.unstructure(MyNamedTuple(1))
254-
{'a': 1}
255-
```
256-
257-
To only un/structure _some_ named tuples into dictionaries,
258-
change the predicate function when registering the hook factory:
259-
260-
```{doctest} namedtuples
261-
:options: +ELLIPSIS
262-
263-
>>> c.register_unstructure_hook_factory(
264-
... lambda t: t is MyNamedTuple,
265-
... namedtuple_dict_unstructure_factory,
266-
... )
267-
<function namedtuple_dict_unstructure_factory at ...>
268-
```
269-
270-
## Using `cattrs.gen` Generators
271-
272-
The {mod}`cattrs.gen` module allows for generating and compiling specialized hooks for unstructuring _attrs_ classes, dataclasses and typed dicts.
160+
The {mod}`cattrs.gen` module contains [hook factories](#hook-factories) for un/structuring _attrs_ classes, dataclasses and typed dicts.
273161
The default {class}`Converter <cattrs.Converter>`, upon first encountering one of these types,
274-
will use the generation functions mentioned here to generate specialized hooks for it,
162+
will use the hook factories mentioned here to generate specialized hooks for it,
275163
register the hooks and use them.
276164

277165
One reason for generating these hooks in advance is that they can bypass a lot of _cattrs_ machinery and be significantly faster than normal _cattrs_.
278-
The hooks are also good building blocks for more complex customizations.
166+
The hook factories are also good building blocks for more complex customizations.
279167

280168
Another reason is overriding behavior on a per-attribute basis.
281169

282-
Currently, the overrides only support generating dictionary un/structuring hooks (as opposed to tuples), and support `omit_if_default`, `forbid_extra_keys`, `rename` and `omit`.
170+
Currently, the overrides only support generating dictionary un/structuring hooks (as opposed to tuples),
171+
and support `omit_if_default`, `forbid_extra_keys`, `rename` and `omit`.
283172

284173
### `omit_if_default`
285174

@@ -491,3 +380,121 @@ ClassWithInitFalse(number=2)
491380
```{versionadded} 23.2.0
492381

493382
```
383+
384+
## Customizing Collections
385+
386+
The {mod}`cattrs.cols` module contains predicates and hook factories useful for customizing collection handling.
387+
These hook factories can be wrapped to apply complex customizations.
388+
389+
Available predicates are:
390+
391+
* {meth}`is_any_set <cattrs.cols.is_any_set>`
392+
* {meth}`is_frozenset <cattrs.cols.is_frozenset>`
393+
* {meth}`is_set <cattrs.cols.is_set>`
394+
* {meth}`is_sequence <cattrs.cols.is_sequence>`
395+
* {meth}`is_mapping <cattrs.cols.is_mapping>`
396+
* {meth}`is_namedtuple <cattrs.cols.is_namedtuple>`
397+
* {meth}`is_defaultdict <cattrs.cols.is_defaultdict>`
398+
399+
````{tip}
400+
These predicates aren't _cattrs_-specific and may be useful in other contexts.
401+
```{doctest} predicates
402+
>>> from cattrs.cols import is_sequence
403+
404+
>>> is_sequence(list[str])
405+
True
406+
```
407+
````
408+
409+
410+
Available hook factories are:
411+
412+
* {meth}`iterable_unstructure_factory <cattrs.cols.iterable_unstructure_factory>`
413+
* {meth}`list_structure_factory <cattrs.cols.list_structure_factory>`
414+
* {meth}`namedtuple_structure_factory <cattrs.cols.namedtuple_structure_factory>`
415+
* {meth}`namedtuple_unstructure_factory <cattrs.cols.namedtuple_unstructure_factory>`
416+
* {meth}`namedtuple_dict_structure_factory <cattrs.cols.namedtuple_dict_structure_factory>`
417+
* {meth}`namedtuple_dict_unstructure_factory <cattrs.cols.namedtuple_dict_unstructure_factory>`
418+
* {meth}`mapping_structure_factory <cattrs.cols.mapping_structure_factory>`
419+
* {meth}`mapping_unstructure_factory <cattrs.cols.mapping_unstructure_factory>`
420+
* {meth}`defaultdict_structure_factory <cattrs.cols.defaultdict_structure_factory>`
421+
422+
Additional predicates and hook factories will be added as requested.
423+
424+
For example, by default sequences are structured from any iterable into lists.
425+
This may be too lax, and additional validation may be applied by wrapping the default list structuring hook factory.
426+
427+
```{testcode} list-customization
428+
from cattrs.cols import is_sequence, list_structure_factory
429+
430+
c = Converter()
431+
432+
@c.register_structure_hook_factory(is_sequence)
433+
def strict_list_hook_factory(type, converter):
434+
435+
# First, we generate the default hook...
436+
list_hook = list_structure_factory(type, converter)
437+
438+
# Then, we wrap it with a function of our own...
439+
def strict_list_hook(value, type):
440+
if not isinstance(value, list):
441+
raise ValueError("Not a list!")
442+
return list_hook(value, type)
443+
444+
# And finally, we return our own composite hook.
445+
return strict_list_hook
446+
```
447+
448+
Now, all sequence structuring will be stricter:
449+
450+
```{doctest} list-customization
451+
>>> c.structure({"a", "b", "c"}, list[str])
452+
Traceback (most recent call last):
453+
...
454+
ValueError: Not a list!
455+
```
456+
457+
```{versionadded} 24.1.0
458+
459+
```
460+
461+
### Customizing Named Tuples
462+
463+
Named tuples can be un/structured using dictionaries using the {meth}`namedtuple_dict_structure_factory <cattrs.cols.namedtuple_dict_structure_factory>`
464+
and {meth}`namedtuple_dict_unstructure_factory <cattrs.cols.namedtuple_dict_unstructure_factory>`
465+
hook factories.
466+
467+
To unstructure _all_ named tuples into dictionaries:
468+
469+
```{doctest} namedtuples
470+
>>> from typing import NamedTuple
471+
472+
>>> from cattrs.cols import is_namedtuple, namedtuple_dict_unstructure_factory
473+
>>> c = Converter()
474+
475+
>>> c.register_unstructure_hook_factory(is_namedtuple, namedtuple_dict_unstructure_factory)
476+
<function namedtuple_dict_unstructure_factory at ...>
477+
478+
>>> class MyNamedTuple(NamedTuple):
479+
... a: int
480+
481+
>>> c.unstructure(MyNamedTuple(1))
482+
{'a': 1}
483+
```
484+
485+
To only un/structure _some_ named tuples into dictionaries,
486+
change the predicate function when registering the hook factory:
487+
488+
```{doctest} namedtuples
489+
:options: +ELLIPSIS
490+
491+
>>> c.register_unstructure_hook_factory(
492+
... lambda t: t is MyNamedTuple,
493+
... namedtuple_dict_unstructure_factory,
494+
... )
495+
<function namedtuple_dict_unstructure_factory at ...>
496+
```
497+
498+
```{versionadded} 24.1.0
499+
500+
```

docs/defaulthooks.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,46 @@ Both keys and values are converted.
183183
{'1': None, '2': 2}
184184
```
185185

186+
### defaultdicts
187+
188+
[`defaultdicts`](https://docs.python.org/3/library/collections.html#collections.defaultdict)
189+
can be structured by default if they can be initialized using their value type hint.
190+
Supported types are:
191+
192+
- `collections.defaultdict[K, V]`
193+
- `typing.DefaultDict[K, V]`
194+
195+
For example, `defaultdict[str, int]` works since _cattrs_ will initialize it with `defaultdict(int)`.
196+
197+
This also means `defaultdicts` without key and value annotations (bare `defaultdicts`) cannot be structured by default.
198+
199+
`defaultdicts` with arbitrary default factories can be structured by using {meth}`defaultdict_structure_factory <cattrs.cols.defaultdict_structure_factory>`:
200+
201+
```{doctest}
202+
>>> from collections import defaultdict
203+
>>> from cattrs.cols import defaultdict_structure_factory
204+
205+
>>> converter = Converter()
206+
>>> hook = defaultdict_structure_factory(
207+
... defaultdict[str, int],
208+
... converter,
209+
... default_factory=lambda: 1
210+
... )
211+
212+
>>> hook({"key": 1})
213+
defaultdict(<function <lambda> at ...>, {'key': 1})
214+
```
215+
216+
`defaultdicts` are unstructured into plain dictionaries.
217+
218+
```{note}
219+
`defaultdicts` are not supported by the BaseConverter.
220+
```
221+
222+
```{versionadded} 24.2.0
223+
224+
```
225+
186226
### 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)
187227

188228
If a class declares itself a virtual subclass of `collections.abc.Mapping` or `collections.abc.MutableMapping` and its initializer accepts a dictionary,

src/cattrs/__init__.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,34 @@
1111
StructureHandlerNotFoundError,
1212
)
1313
from .gen import override
14+
from .types import SimpleStructureHook
1415
from .v import transform_error
1516

1617
__all__ = [
17-
"structure",
18-
"unstructure",
19-
"get_structure_hook",
20-
"get_unstructure_hook",
21-
"register_structure_hook_func",
22-
"register_structure_hook",
23-
"register_unstructure_hook_func",
24-
"register_unstructure_hook",
25-
"structure_attrs_fromdict",
26-
"structure_attrs_fromtuple",
27-
"global_converter",
28-
"BaseConverter",
29-
"Converter",
3018
"AttributeValidationNote",
19+
"BaseConverter",
3120
"BaseValidationError",
3221
"ClassValidationError",
22+
"Converter",
3323
"ForbiddenExtraKeysError",
3424
"GenConverter",
25+
"get_structure_hook",
26+
"get_unstructure_hook",
27+
"global_converter",
3528
"IterableValidationError",
3629
"IterableValidationNote",
3730
"override",
31+
"register_structure_hook_func",
32+
"register_structure_hook",
33+
"register_unstructure_hook_func",
34+
"register_unstructure_hook",
35+
"SimpleStructureHook",
36+
"structure_attrs_fromdict",
37+
"structure_attrs_fromtuple",
38+
"structure",
3839
"StructureHandlerNotFoundError",
3940
"transform_error",
41+
"unstructure",
4042
"UnstructureStrategy",
4143
]
4244

0 commit comments

Comments
 (0)