Skip to content

Use default factory if attribute is None or treat None as missing #570

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
danielnelson opened this issue Aug 19, 2024 · 2 comments
Closed

Comments

@danielnelson
Copy link

  • cattrs version: 23.2.3
  • Python version: 3.11.9
  • Operating System: Linux

Description

This is an issue I can workaround but I would like to know if there is a better way to do it.

I have a type that should have a field with a list type and I want it to default to an empty list. I'm converting from a JSON API that sets the field as null if it is not set and I want cattrs to use the default factory for the field if it is None.

I used attrs.converters.default_if_none, though I'm not particularly fond of it. If there is a way in the converter to make this go away I would prefer it.

What I Did

import attrs
import cattrs


@attrs.define
class A:
    x: list[str] = attrs.field(
        converter=attrs.converters.default_if_none(factory=list),
        factory=list,
    )


print(A())

print(A(x=None))

a = cattrs.structure({}, A)
print(a)

a = cattrs.structure({"x": None}, A)
print(a)

Output:

A(x=[])
A(x=[])
A(x=[])
  + Exception Group Traceback (most recent call last):
  |   File "/home/dbn/src/rockfish/cuttlefish/bar.py", line 21, in <module>
  |     a = cattrs.structure({"x": None}, A)
  |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "/home/dbn/usr/py-3.11/lib/python3.11/site-packages/cattrs/converters.py", line 332, in structure
  |     return self._structure_func.dispatch(cl)(obj, cl)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "<cattrs generated structure __main__.A>", line 10, in structure_A
  | cattrs.errors.ClassValidationError: While structuring A (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<cattrs generated structure __main__.A>", line 6, in structure_A
    |   File "/home/dbn/usr/py-3.11/lib/python3.11/site-packages/cattrs/converters.py", line 519, in _structure_list
    |     for e in obj:
    | TypeError: 'NoneType' object is not iterable
    | Structuring class A @ attribute x
    +------------------------------------

My desired output would be 4 lines of A(x=[]), no exception.

@Tinche
Copy link
Member

Tinche commented Aug 25, 2024

Hi,

I'm on paternity leave right now so my availability is limited ;)

Here's how I would approach this: instead of installing a converter on the class itself, I would customize how cattrs deals with the x field when loading the data. I would do this my writing my custom hook, and registering it on the x field. Here's the code:

import attrs

import cattrs


@attrs.define
class A:
    x: list[str] = attrs.Factory(list)


c = cattrs.Converter()


def none_aware_list_hook(val, type):
    """A structure hook that can also handle None."""
    if val is None:
        return []
    return c.structure(val, type)


c.register_structure_hook(
    A,
    cattrs.gen.make_dict_structure_fn(
        A, c, x=cattrs.override(struct_hook=none_aware_list_hook)
    ),
)

print(A())

print(A(x=None))

a = c.structure({}, A)
print(a)

a = c.structure({"x": None}, A)
print(a)

This will print:

A(x=[])
A(x=None)
A(x=[])
A(x=[])

Personally I would be OK with this since it isolates the special case None handling to cattrs - when dealing with the class directly, there's no special behavior. That's one of the benefits of using cattrs - weirdness can be isolated to exactly where it's needed ;)

This isn't the only way of handling cases like this, but it's what I'd use first. Let me know if you have any other questions, will close this now ;)

@Tinche Tinche closed this as completed Aug 25, 2024
@danielnelson
Copy link
Author

This is perfect, thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants