python-attrs / cattrs

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

Support transparent empty sequence structuring when None is present #49

Closed petergaultney closed 1 year ago

petergaultney commented 6 years ago

Description

Structuring a dictionary to a custom @attr.s type, with defaults provided for all optional arguments.

Cattrs fails to structure the object when there are non-null keys with null/None values where a List/Tuple/Set is expected, because None is not valid (it's not an Optional[List[Foo]], it's just a foolist: List[Foo] = attr.Factory(list).

Suggestion

This is clearly not a bug in cattrs. This is more of a feature request/possible PR: Would you be interested in supporting the use case where a Value of None for a non-Optional List/Tuple/Set in an attrs object would result in filling the attribute with its default value, instead of throwing an error?

Basically, I would like for my attrs types to remain fairly pure - no 'Optional' typing annotation added all over the place - instead, I'd be able to rely on cattrs substituting the default empty list/set/tuple/whatever in the case where there was no underlying object to iterate over because it was null.

Looking through the code, I think this could be supported only for attrs classes by checking in structure_attrs_fromdict to see if val is None, and if so, checking if a.default is not None, and if so, using the default and/or the Factory to generate the 'expected' default.

Again, I realize this is not a bug. Just wanted to raise the possibility. I could probably contribute a PR if it was likely to be accepted. There are already workarounds, like registering structure hooks for every List[Type] for which I want to enable this behavior, but it starts to get a little messy as more types get defined.

Tinche commented 1 year ago

So first of all, apologies for taking almost 5 years to respond. Must have slipped my radar somehow ;)

I don't think I'd like this in cattrs proper since it feels like a niche case. Just out of curiosity, I tried implementing this to see how difficult it'd actually be.

Here's my solution:

from typing import get_origin

from cattrs import Converter

c = Converter()

c.register_structure_hook_func(
    lambda t: get_origin(t) is list, lambda v, t: c._structure_list(v, t) if v else []
)

print(c.structure(None, list[str]))
print(c.structure([1, 2], list[str]))

Maybe not as easy as I'd like, but also not super hard.