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

Tin/override hooks #326

Merged
merged 5 commits into from
Jan 3, 2023
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
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ jobs:
name: "Python ${{ matrix.python-version }}"
runs-on: "ubuntu-latest"
env:
USING_COVERAGE: "3.7,3.8,3.9,3.10,pypy-3.8"
USING_COVERAGE: "3.7,3.8,3.9,3.10,3.11,pypy-3.8"

strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11.0-rc.2", "pypy-3.8"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.8"]

steps:
- uses: "actions/checkout@v2"
Expand Down
2 changes: 2 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ History
22.3.0 (UNRELEASED)
-------------------
* Introduce the `tagged_union` strategy. (`#318 <https://github.com/python-attrs/cattrs/pull/318>`_ `#317 <https://github.com/python-attrs/cattrs/issues/317>`_)
* Introduce `override.struct_hook` and `override.unstruct_hook`. Learn more `here <https://catt.rs/en/latest/customizing.html#struct-hook-and-unstruct-hook>`_.
(`#326 <https://github.com/python-attrs/cattrs/pull/326>`_)

22.2.0 (2022-10-03)
-------------------
Expand Down
4 changes: 4 additions & 0 deletions docs/_static/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,7 @@ div.tab-set {
div.tab-set pre {
padding: 1.25em;
}

body:not([data-theme="light"]) .highlight {
background: #18181a;
}
185 changes: 185 additions & 0 deletions docs/customizing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Customizing class un/structuring

This section deals with customizing the unstructuring and structuring processes in `cattrs`.

## Using `cattr.Converter`

The default `Converter`, upon first encountering an `attrs` class, will use
the generation functions mentioned here to generate the specialized hooks for it,
register the hooks and use them.

## Manual un/structuring hooks

You can write your own structuring and unstructuring functions and register
them for types using {meth}`Converter.register_structure_hook <cattrs.BaseConverter.register_structure_hook>` and
{meth}`Converter.register_unstructure_hook <cattrs.BaseConverter.register_unstructure_hook>`. This approach is the most
flexible but also requires the most amount of boilerplate.

## Using `cattrs.gen` generators

`cattrs` includes a module, {mod}`cattrs.gen`, which allows for generating and
compiling specialized functions for unstructuring `attrs` classes.

One reason for generating these functions in advance is that they can bypass
a lot of `cattrs` machinery and be significantly faster than normal `cattrs`.

Another reason is that it's possible to override behavior on a per-attribute basis.

Currently, the overrides only support generating dictionary un/structuring functions
(as opposed to tuples), and support `omit_if_default`, `forbid_extra_keys`,
`rename` and `omit`.

### `omit_if_default`

This override can be applied on a per-class or per-attribute basis. The generated
unstructuring function will skip unstructuring values that are equal to their
default or factory values.

```{doctest}

>>> from cattrs.gen import make_dict_unstructure_fn, override
>>>
>>> @define
... class WithDefault:
... a: int
... b: dict = Factory(dict)
>>>
>>> c = cattrs.Converter()
>>> c.register_unstructure_hook(WithDefault, make_dict_unstructure_fn(WithDefault, c, b=override(omit_if_default=True)))
>>> c.unstructure(WithDefault(1))
{'a': 1}
```

Note that the per-attribute value overrides the per-class value. A side-effect
of this is the ability to force the presence of a subset of fields.
For example, consider a class with a `DateTime` field and a factory for it:
skipping the unstructuring of the `DateTime` field would be inconsistent and
based on the current time. So we apply the `omit_if_default` rule to the class,
but not to the `DateTime` field.

```{note}
The parameter to `make_dict_unstructure_function` is named ``_cattrs_omit_if_default`` instead of just ``omit_if_default`` to avoid potential collisions with an override for a field named ``omit_if_default``.
```

```{doctest}

>>> from pendulum import DateTime
>>> from cattrs.gen import make_dict_unstructure_fn, override
>>>
>>> @define
... class TestClass:
... a: Optional[int] = None
... b: DateTime = Factory(DateTime.utcnow)
>>>
>>> c = cattrs.Converter()
>>> hook = make_dict_unstructure_fn(TestClass, c, _cattrs_omit_if_default=True, b=override(omit_if_default=False))
>>> c.register_unstructure_hook(TestClass, hook)
>>> c.unstructure(TestClass())
{'b': ...}
```

This override has no effect when generating structuring functions.

### `forbid_extra_keys`

By default `cattrs` is lenient in accepting unstructured input. If extra
keys are present in a dictionary, they will be ignored when generating a
structured object. Sometimes it may be desirable to enforce a stricter
contract, and to raise an error when unknown keys are present - in particular
when fields have default values this may help with catching typos.
`forbid_extra_keys` can also be enabled (or disabled) on a per-class basis when
creating structure hooks with `make_dict_structure_fn`.

```{doctest}
:options: +SKIP

>>> from cattrs.gen import make_dict_structure_fn
>>>
>>> @define
... class TestClass:
... number: int = 1
>>>
>>> c = cattrs.Converter(forbid_extra_keys=True)
>>> c.structure({"nummber": 2}, TestClass)
Traceback (most recent call last):
...
ForbiddenExtraKeyError: Extra fields in constructor for TestClass: nummber
>>> hook = make_dict_structure_fn(TestClass, c, _cattrs_forbid_extra_keys=False)
>>> c.register_structure_hook(TestClass, hook)
>>> c.structure({"nummber": 2}, TestClass)
TestClass(number=1)
```

This behavior can only be applied to classes or to the default for the
`Converter`, and has no effect when generating unstructuring functions.

### `rename`

Using the rename override makes `cattrs` simply use the provided name instead
of the real attribute name. This is useful if an attribute name is a reserved
keyword in Python.

```{doctest}

>>> from pendulum import DateTime
>>> from cattrs.gen import make_dict_unstructure_fn, make_dict_structure_fn, override
>>>
>>> @define
... class ExampleClass:
... klass: Optional[int]
>>>
>>> c = cattrs.Converter()
>>> unst_hook = make_dict_unstructure_fn(ExampleClass, c, klass=override(rename="class"))
>>> st_hook = make_dict_structure_fn(ExampleClass, c, klass=override(rename="class"))
>>> c.register_unstructure_hook(ExampleClass, unst_hook)
>>> c.register_structure_hook(ExampleClass, st_hook)
>>> c.unstructure(ExampleClass(1))
{'class': 1}
>>> c.structure({'class': 1}, ExampleClass)
ExampleClass(klass=1)
```

### `omit`

This override can only be applied to individual attributes. Using the `omit`
override will simply skip the attribute completely when generating a structuring
or unstructuring function.

```{doctest}

>>> from cattrs.gen import make_dict_unstructure_fn, override
>>>
>>> @define
... class ExampleClass:
... an_int: int
>>>
>>> c = cattrs.Converter()
>>> unst_hook = make_dict_unstructure_fn(ExampleClass, c, an_int=override(omit=True))
>>> c.register_unstructure_hook(ExampleClass, unst_hook)
>>> c.unstructure(ExampleClass(1))
{}
```

### `struct_hook` and `unstruct_hook`

By default, the generators will determine the right un/structure hook for each attribute of a class at time of generation according to the type of each individual attribute.

This process can be overriden by passing in the desired un/structure manually.

```{doctest}

>>> from cattrs.gen import make_dict_structure_fn, override

>>> @define
... class ExampleClass:
... an_int: int

>>> c = cattrs.Converter()
>>> st_hook = make_dict_structure_fn(
... ExampleClass, c, an_int=override(struct_hook=lambda v, _: v + 1)
... )
>>> c.register_structure_hook(ExampleClass, st_hook)

>>> c.structure({"an_int": 1}, ExampleClass)
ExampleClass(an_int=2)
```
166 changes: 0 additions & 166 deletions docs/customizing.rst

This file was deleted.

Loading