Skip to content
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

Register structure hook only for optional types #529

Closed
ljnsn opened this issue Mar 29, 2024 · 2 comments · Fixed by #530
Closed

Register structure hook only for optional types #529

ljnsn opened this issue Mar 29, 2024 · 2 comments · Fixed by #530
Milestone

Comments

@ljnsn
Copy link

ljnsn commented Mar 29, 2024

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

Description

I'd like to register a structure hook only for optional types. I've search both existing issues and the docs, but please let me know if I missed this somehow.

What I Did

Suppose I receive some data where values can be "" and I want to convert them to None. I only want to convert the fields that are annotated as optional on my model though, fields that are not annotated as optional should raise a validation error.

from typing import Any, Optional

import attrs
import cattrs

converter = cattrs.Converter()


@attrs.define()
class Foo:
    bar: int | None
    baz: int


def _int_or_none(value: Any, _type: type[Any]) -> int | None:
    return None if value == "" else int(value)

I've tried both these, but it seems like they aren't registered at all:

converter.register_structure_hook(Optional[int], _int_or_none)
converter.register_structure_hook(int | None, _int_or_none)

d = {"bar": "", "baz": "2"}
converter.structure(d, Foo)  # with either of the above, tries to convert "" to int

This works, but it also converts non-optional fields to None:

converter.register_structure_hook(int, _int_or_none)
d = {"bar": "", "baz": ""}
converter.structure(d, Foo)  # Foo(bar=None, baz=None)
@Tinche
Copy link
Member

Tinche commented Mar 29, 2024

Bleh, you're right. It's a quirk of the order in which the default hooks are installed. It's fixable, and I will do so.

In the meantime, you can work around it like this:

from typing import Any

import attrs

import cattrs

converter = cattrs.Converter()


@attrs.define()
class Foo:
    bar: int | None
    baz: int


def _int_or_none(value: Any, _: type[Any]) -> int | None:
    return None if value == "" else int(value)


converter.register_structure_hook_func(lambda t: t == int | None, _int_or_none)

d = {"bar": "", "baz": "2"}
print(converter.structure("", int | None))

@Tinche Tinche added this to the 24.1 milestone Mar 29, 2024
@ljnsn
Copy link
Author

ljnsn commented Mar 29, 2024

Great, thanks for the workaround!

@Tinche Tinche linked a pull request Mar 30, 2024 that will close this issue
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

Successfully merging a pull request may close this issue.

2 participants