python-attrs / cattrs

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

Recursive structuring with custom converters #357

Closed alexkoay closed 1 year ago

alexkoay commented 1 year ago

Description

from typing import Any, Dict, Type, Union
import cattrs
from attrs import define

@define
class Bar:
    bar: int

@define
class Foo:
    foo: Union[str, Dict[str, Bar]]

def hook(val: Any, typ_: Type[Union[str, Dict[str, Bar]]]):
    if isinstance(val, str):
        return val
    elif isinstance(val, dict):
        return cattrs.structure(val, Dict[str, Bar])
    else:
        raise Exception("invalid type")

cattrs.register_structure_hook(Union[str, Dict[str, Bar]], hook)
print(cattrs.structure(dict(foo=dict(abc=dict(bar="1"))), Foo))

In this code, I can call cattrs.structure in the hook to process the nested field which has to be processed. But if I create a custom converter, I have to define the hook after it so that it can use the converter to perform further conversions. Is it possible to create a generic hook that doesn't rely on such a trick?

Essentially what it boils down to is that I have to:

  1. Create a "global" converter.
  2. Create some hooks using said converter.
  3. Register those converters.

Ideally it would be nice to have it as such:

  1. Create some hooks
  2. Create a converter and register said hooks.

Implementation-wise, I think it could only work if the converter is passed in as an argument for the hook. Unless this has been done, I can't think of an alternative method.

Tinche commented 1 year ago

We can't really change the hook signatures now due to backwards compatibility, and even if we could I would prefer to actually make them simpler rather than more complex.

Can you solve your problem by simply creating a function to register your hooks?

def register_my_hooks(converter):
    def hook(val: Any, typ_: Type[Union[str, Dict[str, Bar]]]):
        if isinstance(val, str):
            return val
        elif isinstance(val, dict):
            return converter.structure(val, Dict[str, Bar])
        else:
            raise Exception("invalid type")
    converter.register_structure_hook(...)

c = Converter()
register_my_hooks(c)
Tinche commented 1 year ago

Let me know if you have further thoughts!