Skip to content
Open
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: 3 additions & 1 deletion HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python

## NEXT

- Document a converter-level hook factory recipe for including `init=False` fields in _attrs_ classes.
([#482](https://github.com/python-attrs/cattrs/issues/482))
- Support the [`frozendict`](https://docs.python.org/3.15/builtins/stdtypes.html#frozendict) built-in on Python 3.15+.
([#787](https://github.com/python-attrs/cattrs/pull/787))
- Python 3.15 is now supported and part of the test matrix.
Expand Down Expand Up @@ -301,7 +303,7 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
## 23.2.0 (2023-11-17)

- **Potentially breaking**: skip _attrs_ fields marked as `init=False` by default. This change is potentially breaking for unstructuring.
See [here](https://catt.rs/en/latest/customizing.html#include_init_false) for instructions on how to restore the old behavior.
See the {ref}`migration recipe <include-init-false-fields>` for including these fields across classes, or {ref}`customizing individual classes <customizing-include-init-false>`.
([#40](https://github.com/python-attrs/cattrs/issues/40) [#395](https://github.com/python-attrs/cattrs/pull/395))
- **Potentially breaking**: {py:func}`cattrs.gen.make_dict_structure_fn` and {py:func}`cattrs.gen.typeddicts.make_dict_structure_fn` will use the values for the `detailed_validation` and `forbid_extra_keys` parameters from the given converter by default now.
If you're using these functions directly, the old behavior can be restored by passing in the desired values directly.
Expand Down
3 changes: 3 additions & 0 deletions docs/customizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,11 +375,14 @@ AliasClass(number=2)

```

(customizing-include-init-false)=
### `include_init_false`

By default, _attrs_ fields defined as `init=False` are skipped when un/structuring.
By generating your un/structure function with `_cattrs_include_init_false=True`, all `init=False` fields will be included for un/structuring.

To include these fields across multiple _attrs_ classes on one converter, see the {ref}`hook factory migration recipe <include-init-false-fields>`.

```{doctest}

>>> from cattrs.gen import make_dict_structure_fn
Expand Down
58 changes: 58 additions & 0 deletions docs/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,61 @@ The old behavior can be restored by explicitly passing in the old hook fallback
The internal `cattrs.gen.MappingStructureFn` and `cattrs.gen.DictStructureFn` types were replaced by a more general type, `cattrs.SimpleStructureHook[In, T]`.
If you were using `MappingStructureFn`, use `SimpleStructureHook[Mapping[Any, Any], T]` instead.
If you were using `DictStructureFn`, use `SimpleStructureHook[Mapping[str, Any], T]` instead.

## 23.2.0

(include-init-false-fields)=
### Including `init=False` fields on a converter

From this version on, _attrs_ fields declared with `init=False` are skipped by default when structuring and unstructuring.
To include these fields for multiple classes, register hook factories on a converter before generating other hooks or converting values, since generated hooks resolve nested hooks early.
The factories below generate dict hooks for each _attrs_ class encountered, including nested classes, with `_cattrs_include_init_false=True`.

```{doctest}
>>> from attrs import define, field, has
>>> from cattrs import Converter
>>> from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn
>>>
>>> @define
... class Record:
... number: int
... cached: int = field(init=False, default=0)
>>>
>>> @define
... class Batch:
... record: Record
... label: str = field(init=False, default="")
>>>
>>> batch = Batch(Record(1))
>>> batch.record.cached = 7
>>> batch.label = "ready"
>>> default_converter = Converter()
>>> default_converter.unstructure(batch)
{'record': {'number': 1}}
>>>
>>> converter = Converter()
>>> _ = converter.register_unstructure_hook_factory(
... has,
... lambda cls: make_dict_unstructure_fn(
... cls, converter, _cattrs_include_init_false=True
... ),
... )
>>> _ = converter.register_structure_hook_factory(
... has,
... lambda cls: make_dict_structure_fn(
... cls, converter, _cattrs_include_init_false=True
... ),
... )
>>> data = converter.unstructure(batch)
>>> data
{'record': {'number': 1, 'cached': 7}, 'label': 'ready'}
>>> converter.structure(data, Batch)
Batch(record=Record(number=1, cached=7), label='ready')
>>> default_converter.unstructure(batch)
{'record': {'number': 1}}
```

Register both factories to include the fields in both directions; registering only the unstructuring factory changes only the generated output.
These registrations affect this converter, not other converters or the module-level conversion functions.
The example uses mutable classes: structuring assigns `init=False` fields after constructing the instance.
Use the {ref}`per-class or per-field customization <customizing-include-init-false>` when only selected classes or fields should be included.