Skip to content

Commit 0b9fd0d

Browse files
authored
Tin/override hooks (#326)
* Override hooks * Update css * Docs * Tweak GitHub * Tweak HISTORY
1 parent 9ff744f commit 0b9fd0d

File tree

7 files changed

+286
-200
lines changed

7 files changed

+286
-200
lines changed

Diff for: .github/workflows/main.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ jobs:
1313
name: "Python ${{ matrix.python-version }}"
1414
runs-on: "ubuntu-latest"
1515
env:
16-
USING_COVERAGE: "3.7,3.8,3.9,3.10,pypy-3.8"
16+
USING_COVERAGE: "3.7,3.8,3.9,3.10,3.11,pypy-3.8"
1717

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

2222
steps:
2323
- uses: "actions/checkout@v2"

Diff for: HISTORY.rst

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ History
55
22.3.0 (UNRELEASED)
66
-------------------
77
* Introduce the `tagged_union` strategy. (`#318 <https://github.com/python-attrs/cattrs/pull/318>`_ `#317 <https://github.com/python-attrs/cattrs/issues/317>`_)
8+
* Introduce `override.struct_hook` and `override.unstruct_hook`. Learn more `here <https://catt.rs/en/latest/customizing.html#struct-hook-and-unstruct-hook>`_.
9+
(`#326 <https://github.com/python-attrs/cattrs/pull/326>`_)
810

911
22.2.0 (2022-10-03)
1012
-------------------

Diff for: docs/_static/custom.css

+4
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,7 @@ div.tab-set {
133133
div.tab-set pre {
134134
padding: 1.25em;
135135
}
136+
137+
body:not([data-theme="light"]) .highlight {
138+
background: #18181a;
139+
}

Diff for: docs/customizing.md

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Customizing class un/structuring
2+
3+
This section deals with customizing the unstructuring and structuring processes in `cattrs`.
4+
5+
## Using `cattr.Converter`
6+
7+
The default `Converter`, upon first encountering an `attrs` class, will use
8+
the generation functions mentioned here to generate the specialized hooks for it,
9+
register the hooks and use them.
10+
11+
## Manual un/structuring hooks
12+
13+
You can write your own structuring and unstructuring functions and register
14+
them for types using {meth}`Converter.register_structure_hook <cattrs.BaseConverter.register_structure_hook>` and
15+
{meth}`Converter.register_unstructure_hook <cattrs.BaseConverter.register_unstructure_hook>`. This approach is the most
16+
flexible but also requires the most amount of boilerplate.
17+
18+
## Using `cattrs.gen` generators
19+
20+
`cattrs` includes a module, {mod}`cattrs.gen`, which allows for generating and
21+
compiling specialized functions for unstructuring `attrs` classes.
22+
23+
One reason for generating these functions in advance is that they can bypass
24+
a lot of `cattrs` machinery and be significantly faster than normal `cattrs`.
25+
26+
Another reason is that it's possible to override behavior on a per-attribute basis.
27+
28+
Currently, the overrides only support generating dictionary un/structuring functions
29+
(as opposed to tuples), and support `omit_if_default`, `forbid_extra_keys`,
30+
`rename` and `omit`.
31+
32+
### `omit_if_default`
33+
34+
This override can be applied on a per-class or per-attribute basis. The generated
35+
unstructuring function will skip unstructuring values that are equal to their
36+
default or factory values.
37+
38+
```{doctest}
39+
40+
>>> from cattrs.gen import make_dict_unstructure_fn, override
41+
>>>
42+
>>> @define
43+
... class WithDefault:
44+
... a: int
45+
... b: dict = Factory(dict)
46+
>>>
47+
>>> c = cattrs.Converter()
48+
>>> c.register_unstructure_hook(WithDefault, make_dict_unstructure_fn(WithDefault, c, b=override(omit_if_default=True)))
49+
>>> c.unstructure(WithDefault(1))
50+
{'a': 1}
51+
```
52+
53+
Note that the per-attribute value overrides the per-class value. A side-effect
54+
of this is the ability to force the presence of a subset of fields.
55+
For example, consider a class with a `DateTime` field and a factory for it:
56+
skipping the unstructuring of the `DateTime` field would be inconsistent and
57+
based on the current time. So we apply the `omit_if_default` rule to the class,
58+
but not to the `DateTime` field.
59+
60+
```{note}
61+
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``.
62+
```
63+
64+
```{doctest}
65+
66+
>>> from pendulum import DateTime
67+
>>> from cattrs.gen import make_dict_unstructure_fn, override
68+
>>>
69+
>>> @define
70+
... class TestClass:
71+
... a: Optional[int] = None
72+
... b: DateTime = Factory(DateTime.utcnow)
73+
>>>
74+
>>> c = cattrs.Converter()
75+
>>> hook = make_dict_unstructure_fn(TestClass, c, _cattrs_omit_if_default=True, b=override(omit_if_default=False))
76+
>>> c.register_unstructure_hook(TestClass, hook)
77+
>>> c.unstructure(TestClass())
78+
{'b': ...}
79+
```
80+
81+
This override has no effect when generating structuring functions.
82+
83+
### `forbid_extra_keys`
84+
85+
By default `cattrs` is lenient in accepting unstructured input. If extra
86+
keys are present in a dictionary, they will be ignored when generating a
87+
structured object. Sometimes it may be desirable to enforce a stricter
88+
contract, and to raise an error when unknown keys are present - in particular
89+
when fields have default values this may help with catching typos.
90+
`forbid_extra_keys` can also be enabled (or disabled) on a per-class basis when
91+
creating structure hooks with `make_dict_structure_fn`.
92+
93+
```{doctest}
94+
:options: +SKIP
95+
96+
>>> from cattrs.gen import make_dict_structure_fn
97+
>>>
98+
>>> @define
99+
... class TestClass:
100+
... number: int = 1
101+
>>>
102+
>>> c = cattrs.Converter(forbid_extra_keys=True)
103+
>>> c.structure({"nummber": 2}, TestClass)
104+
Traceback (most recent call last):
105+
...
106+
ForbiddenExtraKeyError: Extra fields in constructor for TestClass: nummber
107+
>>> hook = make_dict_structure_fn(TestClass, c, _cattrs_forbid_extra_keys=False)
108+
>>> c.register_structure_hook(TestClass, hook)
109+
>>> c.structure({"nummber": 2}, TestClass)
110+
TestClass(number=1)
111+
```
112+
113+
This behavior can only be applied to classes or to the default for the
114+
`Converter`, and has no effect when generating unstructuring functions.
115+
116+
### `rename`
117+
118+
Using the rename override makes `cattrs` simply use the provided name instead
119+
of the real attribute name. This is useful if an attribute name is a reserved
120+
keyword in Python.
121+
122+
```{doctest}
123+
124+
>>> from pendulum import DateTime
125+
>>> from cattrs.gen import make_dict_unstructure_fn, make_dict_structure_fn, override
126+
>>>
127+
>>> @define
128+
... class ExampleClass:
129+
... klass: Optional[int]
130+
>>>
131+
>>> c = cattrs.Converter()
132+
>>> unst_hook = make_dict_unstructure_fn(ExampleClass, c, klass=override(rename="class"))
133+
>>> st_hook = make_dict_structure_fn(ExampleClass, c, klass=override(rename="class"))
134+
>>> c.register_unstructure_hook(ExampleClass, unst_hook)
135+
>>> c.register_structure_hook(ExampleClass, st_hook)
136+
>>> c.unstructure(ExampleClass(1))
137+
{'class': 1}
138+
>>> c.structure({'class': 1}, ExampleClass)
139+
ExampleClass(klass=1)
140+
```
141+
142+
### `omit`
143+
144+
This override can only be applied to individual attributes. Using the `omit`
145+
override will simply skip the attribute completely when generating a structuring
146+
or unstructuring function.
147+
148+
```{doctest}
149+
150+
>>> from cattrs.gen import make_dict_unstructure_fn, override
151+
>>>
152+
>>> @define
153+
... class ExampleClass:
154+
... an_int: int
155+
>>>
156+
>>> c = cattrs.Converter()
157+
>>> unst_hook = make_dict_unstructure_fn(ExampleClass, c, an_int=override(omit=True))
158+
>>> c.register_unstructure_hook(ExampleClass, unst_hook)
159+
>>> c.unstructure(ExampleClass(1))
160+
{}
161+
```
162+
163+
### `struct_hook` and `unstruct_hook`
164+
165+
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.
166+
167+
This process can be overriden by passing in the desired un/structure manually.
168+
169+
```{doctest}
170+
171+
>>> from cattrs.gen import make_dict_structure_fn, override
172+
173+
>>> @define
174+
... class ExampleClass:
175+
... an_int: int
176+
177+
>>> c = cattrs.Converter()
178+
>>> st_hook = make_dict_structure_fn(
179+
... ExampleClass, c, an_int=override(struct_hook=lambda v, _: v + 1)
180+
... )
181+
>>> c.register_structure_hook(ExampleClass, st_hook)
182+
183+
>>> c.structure({"an_int": 1}, ExampleClass)
184+
ExampleClass(an_int=2)
185+
```

Diff for: docs/customizing.rst

-166
This file was deleted.

0 commit comments

Comments
 (0)