Skip to content

Commit

Permalink
Tin/defaultdicts (#588)
Browse files Browse the repository at this point in the history
* Defaultdicts WIP

* Reformat

* Docs

* More docs

* Tweak docs

* Introduce SimpleStructureHook
  • Loading branch information
Tinche authored Nov 7, 2024
1 parent 456c749 commit f81d9af
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 143 deletions.
12 changes: 9 additions & 3 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@ Our backwards-compatibility policy can be found [here](https://github.com/python

- **Potentially breaking**: The converters raise {class}`StructureHandlerNotFoundError` more eagerly (on hook creation, instead of on hook use).
This helps surfacing problems with missing hooks sooner.
See [Migrations](https://catt.rs/latest/migrations.html#the-default-structure-hook-fallback-factory) for steps to restore legacy behavior.
See [Migrations](https://catt.rs/en/latest/migrations.html#the-default-structure-hook-fallback-factory) for steps to restore legacy behavior.
([#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.
- Add a [Migrations](https://catt.rs/en/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`.
- Some `defaultdicts` are now [supported by default](https://catt.rs/en/latest/defaulthooks.html#defaultdicts), and
{func}`cattrs.cols.is_defaultdict`{func} and `cattrs.cols.defaultdict_structure_factory` are exposed through {mod}`cattrs.cols`.
([#519](https://github.com/python-attrs/cattrs/issues/519) [#588](https://github.com/python-attrs/cattrs/pull/588))
- Replace `cattrs.gen.MappingStructureFn` with `cattrs.SimpleStructureHook[In, T]`.
- Python 3.13 is now supported.
([#543](https://github.com/python-attrs/cattrs/pull/543) [#547](https://github.com/python-attrs/cattrs/issues/547))
- Python 3.8 is no longer supported, as it is end-of-life. Use previous versions on this Python version.
- Change type of Converter.__init__.unstruct_collection_overrides from Callable to Mapping[type, UnstructureHook] ([#594](https://github.com/python-attrs/cattrs/pull/594).
([#591](https://github.com/python-attrs/cattrs/pull/591))
- Change type of `Converter.__init__.unstruct_collection_overrides` from `Callable` to `Mapping[type, UnstructureHook]`
([#594](https://github.com/python-attrs/cattrs/pull/594)).

## 24.1.2 (2024-09-22)

Expand Down
241 changes: 124 additions & 117 deletions docs/customizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,131 +155,20 @@ Here's an example of using an unstructure hook factory to handle unstructuring [
[1, 2]
```

## Customizing Collections

The {mod}`cattrs.cols` module contains predicates and hook factories useful for customizing collection handling.
These hook factories can be wrapped to apply complex customizations.

Available predicates are:

* {meth}`is_any_set <cattrs.cols.is_any_set>`
* {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}
These predicates aren't _cattrs_-specific and may be useful in other contexts.
```{doctest} predicates
>>> from cattrs.cols import is_sequence

>>> is_sequence(list[str])
True
```
````


Available hook factories are:

* {meth}`iterable_unstructure_factory <cattrs.cols.iterable_unstructure_factory>`
* {meth}`list_structure_factory <cattrs.cols.list_structure_factory>`
* {meth}`namedtuple_structure_factory <cattrs.cols.namedtuple_structure_factory>`
* {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>`
* {meth}`mapping_unstructure_factory <cattrs.cols.mapping_unstructure_factory>`

Additional predicates and hook factories will be added as requested.

For example, by default sequences are structured from any iterable into lists.
This may be too lax, and additional validation may be applied by wrapping the default list structuring hook factory.

```{testcode} list-customization
from cattrs.cols import is_sequence, list_structure_factory

c = Converter()

@c.register_structure_hook_factory(is_sequence)
def strict_list_hook_factory(type, converter):

# First, we generate the default hook...
list_hook = list_structure_factory(type, converter)

# Then, we wrap it with a function of our own...
def strict_list_hook(value, type):
if not isinstance(value, list):
raise ValueError("Not a list!")
return list_hook(value, type)
## Using `cattrs.gen` Hook Factories

# And finally, we return our own composite hook.
return strict_list_hook
```

Now, all sequence structuring will be stricter:

```{doctest} list-customization
>>> c.structure({"a", "b", "c"}, list[str])
Traceback (most recent call last):
...
ValueError: Not a list!
```

```{versionadded} 24.1.0

```

### Customizing Named Tuples

Named tuples can be un/structured using dictionaries using the {meth}`namedtuple_dict_structure_factory <cattrs.cols.namedtuple_dict_structure_factory>`
and {meth}`namedtuple_dict_unstructure_factory <cattrs.cols.namedtuple_dict_unstructure_factory>`
hook factories.

To unstructure _all_ named tuples into dictionaries:

```{doctest} namedtuples
>>> from typing import NamedTuple

>>> from cattrs.cols import is_namedtuple, namedtuple_dict_unstructure_factory
>>> c = Converter()

>>> c.register_unstructure_hook_factory(is_namedtuple, namedtuple_dict_unstructure_factory)
<function namedtuple_dict_unstructure_factory at ...>

>>> class MyNamedTuple(NamedTuple):
... a: int

>>> c.unstructure(MyNamedTuple(1))
{'a': 1}
```

To only un/structure _some_ named tuples into dictionaries,
change the predicate function when registering the hook factory:

```{doctest} namedtuples
:options: +ELLIPSIS

>>> c.register_unstructure_hook_factory(
... lambda t: t is MyNamedTuple,
... namedtuple_dict_unstructure_factory,
... )
<function namedtuple_dict_unstructure_factory at ...>
```

## Using `cattrs.gen` Generators

The {mod}`cattrs.gen` module allows for generating and compiling specialized hooks for unstructuring _attrs_ classes, dataclasses and typed dicts.
The {mod}`cattrs.gen` module contains [hook factories](#hook-factories) for un/structuring _attrs_ classes, dataclasses and typed dicts.
The default {class}`Converter <cattrs.Converter>`, upon first encountering one of these types,
will use the generation functions mentioned here to generate specialized hooks for it,
will use the hook factories mentioned here to generate specialized hooks for it,
register the hooks and use them.

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_.
The hooks are also good building blocks for more complex customizations.
The hook factories are also good building blocks for more complex customizations.

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

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`.
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`.

### `omit_if_default`

Expand Down Expand Up @@ -491,3 +380,121 @@ ClassWithInitFalse(number=2)
```{versionadded} 23.2.0

```

## Customizing Collections

The {mod}`cattrs.cols` module contains predicates and hook factories useful for customizing collection handling.
These hook factories can be wrapped to apply complex customizations.

Available predicates are:

* {meth}`is_any_set <cattrs.cols.is_any_set>`
* {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>`
* {meth}`is_defaultdict <cattrs.cols.is_defaultdict>`

````{tip}
These predicates aren't _cattrs_-specific and may be useful in other contexts.
```{doctest} predicates
>>> from cattrs.cols import is_sequence

>>> is_sequence(list[str])
True
```
````


Available hook factories are:

* {meth}`iterable_unstructure_factory <cattrs.cols.iterable_unstructure_factory>`
* {meth}`list_structure_factory <cattrs.cols.list_structure_factory>`
* {meth}`namedtuple_structure_factory <cattrs.cols.namedtuple_structure_factory>`
* {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>`
* {meth}`mapping_unstructure_factory <cattrs.cols.mapping_unstructure_factory>`
* {meth}`defaultdict_structure_factory <cattrs.cols.defaultdict_structure_factory>`

Additional predicates and hook factories will be added as requested.

For example, by default sequences are structured from any iterable into lists.
This may be too lax, and additional validation may be applied by wrapping the default list structuring hook factory.

```{testcode} list-customization
from cattrs.cols import is_sequence, list_structure_factory

c = Converter()

@c.register_structure_hook_factory(is_sequence)
def strict_list_hook_factory(type, converter):

# First, we generate the default hook...
list_hook = list_structure_factory(type, converter)

# Then, we wrap it with a function of our own...
def strict_list_hook(value, type):
if not isinstance(value, list):
raise ValueError("Not a list!")
return list_hook(value, type)

# And finally, we return our own composite hook.
return strict_list_hook
```

Now, all sequence structuring will be stricter:

```{doctest} list-customization
>>> c.structure({"a", "b", "c"}, list[str])
Traceback (most recent call last):
...
ValueError: Not a list!
```

```{versionadded} 24.1.0

```

### Customizing Named Tuples

Named tuples can be un/structured using dictionaries using the {meth}`namedtuple_dict_structure_factory <cattrs.cols.namedtuple_dict_structure_factory>`
and {meth}`namedtuple_dict_unstructure_factory <cattrs.cols.namedtuple_dict_unstructure_factory>`
hook factories.

To unstructure _all_ named tuples into dictionaries:

```{doctest} namedtuples
>>> from typing import NamedTuple

>>> from cattrs.cols import is_namedtuple, namedtuple_dict_unstructure_factory
>>> c = Converter()

>>> c.register_unstructure_hook_factory(is_namedtuple, namedtuple_dict_unstructure_factory)
<function namedtuple_dict_unstructure_factory at ...>

>>> class MyNamedTuple(NamedTuple):
... a: int

>>> c.unstructure(MyNamedTuple(1))
{'a': 1}
```

To only un/structure _some_ named tuples into dictionaries,
change the predicate function when registering the hook factory:

```{doctest} namedtuples
:options: +ELLIPSIS

>>> c.register_unstructure_hook_factory(
... lambda t: t is MyNamedTuple,
... namedtuple_dict_unstructure_factory,
... )
<function namedtuple_dict_unstructure_factory at ...>
```

```{versionadded} 24.1.0

```
40 changes: 40 additions & 0 deletions docs/defaulthooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,46 @@ Both keys and values are converted.
{'1': None, '2': 2}
```

### defaultdicts

[`defaultdicts`](https://docs.python.org/3/library/collections.html#collections.defaultdict)
can be structured by default if they can be initialized using their value type hint.
Supported types are:

- `collections.defaultdict[K, V]`
- `typing.DefaultDict[K, V]`

For example, `defaultdict[str, int]` works since _cattrs_ will initialize it with `defaultdict(int)`.

This also means `defaultdicts` without key and value annotations (bare `defaultdicts`) cannot be structured by default.

`defaultdicts` with arbitrary default factories can be structured by using {meth}`defaultdict_structure_factory <cattrs.cols.defaultdict_structure_factory>`:

```{doctest}
>>> from collections import defaultdict
>>> from cattrs.cols import defaultdict_structure_factory

>>> converter = Converter()
>>> hook = defaultdict_structure_factory(
... defaultdict[str, int],
... converter,
... default_factory=lambda: 1
... )

>>> hook({"key": 1})
defaultdict(<function <lambda> at ...>, {'key': 1})
```

`defaultdicts` are unstructured into plain dictionaries.

```{note}
`defaultdicts` are not supported by the BaseConverter.
```

```{versionadded} 24.2.0

```

### 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,
Expand Down
28 changes: 15 additions & 13 deletions src/cattrs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,34 @@
StructureHandlerNotFoundError,
)
from .gen import override
from .types import SimpleStructureHook
from .v import transform_error

__all__ = [
"structure",
"unstructure",
"get_structure_hook",
"get_unstructure_hook",
"register_structure_hook_func",
"register_structure_hook",
"register_unstructure_hook_func",
"register_unstructure_hook",
"structure_attrs_fromdict",
"structure_attrs_fromtuple",
"global_converter",
"BaseConverter",
"Converter",
"AttributeValidationNote",
"BaseConverter",
"BaseValidationError",
"ClassValidationError",
"Converter",
"ForbiddenExtraKeysError",
"GenConverter",
"get_structure_hook",
"get_unstructure_hook",
"global_converter",
"IterableValidationError",
"IterableValidationNote",
"override",
"register_structure_hook_func",
"register_structure_hook",
"register_unstructure_hook_func",
"register_unstructure_hook",
"SimpleStructureHook",
"structure_attrs_fromdict",
"structure_attrs_fromtuple",
"structure",
"StructureHandlerNotFoundError",
"transform_error",
"unstructure",
"UnstructureStrategy",
]

Expand Down
Loading

0 comments on commit f81d9af

Please sign in to comment.