diff --git a/HISTORY.md b/HISTORY.md index 4d271dc4..3dbac75e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -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. @@ -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 ` for including these fields across classes, or {ref}`customizing individual classes `. ([#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. diff --git a/docs/customizing.md b/docs/customizing.md index bc099791..02cff5d9 100644 --- a/docs/customizing.md +++ b/docs/customizing.md @@ -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 `. + ```{doctest} >>> from cattrs.gen import make_dict_structure_fn diff --git a/docs/migrations.md b/docs/migrations.md index 585039f9..c5750c0a 100644 --- a/docs/migrations.md +++ b/docs/migrations.md @@ -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 ` when only selected classes or fields should be included.