Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decorators for hooks #487

Merged
merged 8 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
([#486](https://github.com/python-attrs/cattrs/pull/486))
- Introduce {meth}`BaseConverter.get_structure_hook` and {meth}`BaseConverter.get_unstructure_hook` methods.
([#432](https://github.com/python-attrs/cattrs/issues/432) [#472](https://github.com/python-attrs/cattrs/pull/472))
- {meth}`BaseConverter.register_structure_hook`, {meth}`BaseConverter.register_unstructure_hook`,
{meth}`BaseConverter.register_unstructure_hook_factory` and {meth}`BaseConverter.register_structure_hook_factory`
can now be used as decorators and have gained new features when used this way.
See [here](https://catt.rs/en/latest/customizing.html#use-as-decorators) and [here](https://catt.rs/en/latest/customizing.html#id1) for more details.
([#487](https://github.com/python-attrs/cattrs/pull/487))
- Introduce the [_msgspec_](https://jcristharif.com/msgspec/) {mod}`preconf converter <cattrs.preconf.msgspec>`.
Only JSON is supported for now, with other formats supported by _msgspec_ to come later.
([#481](https://github.com/python-attrs/cattrs/pull/481))
Expand Down
4 changes: 2 additions & 2 deletions docs/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ A base hook can be obtained from a converter and then be subjected to the very r
... return result
```

(`cattrs.structure({}, Model)` is shorthand for `cattrs.get_structure_hook(Model)({}, Model)`.)
(`cattrs.structure({}, Model)` is equivalent to `cattrs.get_structure_hook(Model)({}, Model)`.)

This new hook can be used directly or registered to a converter (the original instance, or a different one):

Expand All @@ -72,7 +72,7 @@ This new hook can be used directly or registered to a converter (the original in
```


Now if we use this hook to structure a `Model`, through the magic of function composition✨ that hook will use our old `int_hook`.
Now if we use this hook to structure a `Model`, through the magic of function composition✨ that hook will use our old `int_hook`.

```python
>>> converter.structure({"a": "1"}, Model)
Expand Down
1 change: 0 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
"sphinx.ext.doctest",
"sphinx.ext.autosectionlabel",
"sphinx_copybutton",
"myst_parser",
]
Expand Down
76 changes: 73 additions & 3 deletions docs/customizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,44 @@ Some examples of this are:
* protocols, unless they are `runtime_checkable`
* various modifiers, such as `Final` and `NotRequired`
* newtypes and 3.12 type aliases
* `typing.Annotated`

... and many others. In these cases, predicate functions should be used instead.

### Use as Decorators

{meth}`register_structure_hook() <cattrs.BaseConverter.register_structure_hook>` and {meth}`register_unstructure_hook() <cattrs.BaseConverter.register_unstructure_hook>` can also be used as _decorators_.
When used this way they behave a little differently.

{meth}`register_structure_hook() <cattrs.BaseConverter.register_structure_hook>` will inspect the return type of the hook and register the hook for that type.

```python
@converter.register_structure_hook
def my_int_hook(val: Any, _) -> int:
"""This hook will be registered for `int`s."""
return int(val)
```

{meth}`register_unstructure_hook() <cattrs.BaseConverter.register_unstructure_hook>` will inspect the type of the first argument and register the hook for that type.

```python
from datetime import datetime

@converter.register_unstructure_hook
def my_datetime_hook(val: datetime) -> str:
"""This hook will be registered for `datetime`s."""
return val.isoformat()
```

The non-decorator approach is still recommended when dealing with lambdas, hooks produced elsewhere, unannotated hooks and situations where type introspection doesn't work.

```{versionadded} 24.1.0
```

### Predicate Hooks

A predicate is a function that takes a type and returns true or false, depending on whether the associated hook can handle the given type.
A _predicate_ is a function that takes a type and returns true or false
depending on whether the associated hook can handle the given type.

The {meth}`register_unstructure_hook_func() <cattrs.BaseConverter.register_unstructure_hook_func>` and {meth}`register_structure_hook_func() <cattrs.BaseConverter.register_structure_hook_func>` are used
to link un/structuring hooks to arbitrary types. These hooks are then called _predicate hooks_, and are very powerful.
Expand Down Expand Up @@ -64,9 +96,11 @@ Here's an example showing how to use hook factories to apply the `forbid_extra_k

```python
>>> from attrs import define, has
>>> from cattrs import Converter
>>> from cattrs.gen import make_dict_structure_fn

>>> c = cattrs.Converter()
>>> c = Converter()

>>> c.register_structure_hook_factory(
... has,
... lambda cl: make_dict_structure_fn(cl, c, _cattrs_forbid_extra_keys=True)
Expand All @@ -82,8 +116,44 @@ Traceback (most recent call last):
cattrs.errors.ForbiddenExtraKeysError: Extra fields in constructor for E: else
```

A complex use case for hook factories is described over at {ref}`usage:Using factory hooks`.
A complex use case for hook factories is described over at [](usage.md#using-factory-hooks).

#### Use as Decorators

{meth}`register_unstructure_hook_factory() <cattrs.BaseConverter.register_unstructure_hook_factory>` and
{meth}`register_structure_hook_factory() <cattrs.BaseConverter.register_structure_hook_factory>` can also be used as decorators.

When registered via decorators, hook factories can receive the current converter by exposing an additional required parameter.

Here's an example of using an unstructure hook factory to handle unstructuring [queues](https://docs.python.org/3/library/queue.html#queue.Queue).

```{doctest}
>>> from queue import Queue
>>> from typing import get_origin
>>> from cattrs import Converter

>>> c = Converter()

>>> @c.register_unstructure_hook_factory(lambda t: get_origin(t) is Queue)
... def queue_hook_factory(cl: Any, converter: Converter) -> Callable:
... type_arg = get_args(cl)[0]
... elem_handler = converter.get_unstructure_hook(type_arg)
...
... def unstructure_hook(v: Queue) -> list:
... res = []
... while not v.empty():
... res.append(elem_handler(v.get_nowait()))
... return res
...
... return unstructure_hook

>>> q = Queue()
>>> q.put(1)
>>> q.put(2)

>>> c.unstructure(q, unstructure_as=Queue[int])
[1, 2]
```

## Using `cattrs.gen` Generators

Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ source = [
".tox/pypy*/site-packages",
]

[tool.coverage.report]
exclude_also = [
"@overload",
]

[tool.ruff]
src = ["src", "tests"]
select = [
Expand Down
7 changes: 7 additions & 0 deletions src/cattrs/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from collections.abc import Set as AbcSet
from dataclasses import MISSING, Field, is_dataclass
from dataclasses import fields as dataclass_fields
from functools import partial
from inspect import signature as _signature
from typing import AbstractSet as TypingAbstractSet
from typing import (
Any,
Expand Down Expand Up @@ -211,6 +213,11 @@ def get_final_base(type) -> Optional[type]:
OriginAbstractSet = AbcSet
OriginMutableSet = AbcMutableSet

signature = _signature

if sys.version_info >= (3, 10):
signature = partial(_signature, eval_str=True)

if sys.version_info >= (3, 9):
from collections import Counter
from collections.abc import Mapping as AbcMapping
Expand Down
Loading
Loading