python-attrs / cattrs

Composable custom class converters for attrs, dataclasses and friends.
https://catt.rs
MIT License
781 stars 108 forks source link

Option to include `init=False` globally on a converter, not per class #482

Open MicaelJarniac opened 6 months ago

MicaelJarniac commented 6 months ago

Description

On previous cattrs versions, I used to structure and unstructure a lot of attrs classes that had many init=False fields.

Now, though, with the new versions of cattrs (v23.2.0), this behavior changed, breaking my code.

I'd like to be able to enable the old behavior back globally on a converter, instead of having to enable it for every single class, as I have many classes all over the place that would benefit from re-enabling init=False fields in conversion.

import attrs
import cattrs

@attrs.define
class Foo:
    bar: int
    baz: str = attrs.field(init=False)

foo = Foo(bar=1)
foo.baz = "hello"

converter = cattrs.Converter()
print(converter.unstructure(foo))
# before: {'bar': 1, 'baz': 'hello'}
# after: {'bar': 1}

I thought about something like this:

import cattrs
from cattrs.strategies import include_init_false

converter = cattrs.Converter()
include_init_false(converter)

# OR

converter = cattrs.Converter(include_init_false=True)
MicaelJarniac commented 6 months ago

I think I found a workaround on the docs, but it wasn't linked in the places I quoted:

I'll try it out and see if it solves all my problems - I think it likely will.

But anyway, I think it'd be nice to link this workaround to these places:

(Also note that the first link includes a broken reference to the second one, which would also be nice to fix.)

Edit: Just noticed that the example is for the opposite case - to exclude init=False fields. But by changing it a tiny bit it does work to do what I want.

Edit: I simplified the code from the example, using newer features, and made it into a function:

import cattrs
from attrs import has
from cattrs.gen import make_dict_unstructure_fn

def include_init_false(c: cattrs.Converter) -> None:
    c.register_unstructure_hook_factory(
        has,
        lambda cl: make_dict_unstructure_fn(
            cl=cl,
            converter=c,
            _cattrs_include_init_false=True,
        ),
    )
Tinche commented 6 months ago

Howdy!

I think a converter parameter is almost certainly not the way to go, even though I've went that route before. Simply because the converter will end up a huge mess of different parameters.

Also the point of cattrs is to customize it with hooks and hook factories ;) I think your solution is pretty good.

Documentation is good though. I've been playing around with the idea of creating a migrations page in the docs. This would be a collection of recipes for dealing with breaking changes in cattrs, per version. And it would be in parallel with the changelog, just a different page. The idea being, if you're upgrading cattrs you can just look here for recipes on how to restore old behavior. I think this would be a good candidate for something like that.