cattrs icon indicating copy to clipboard operation
cattrs copied to clipboard

A few syntax sugar ideas

Open aldanor opened this issue 8 years ago • 3 comments

Hi! First, a very neat idea and a neat implementation :)

As I munged with it for a bit, I was wondering if you'd consider a few potential syntactic shortcuts?

  1. Registering both hooks at once:

For example, if I want pandas Timedeltas to be serialized/deserialized as strings, I have to do something like

cattr.register_structure_hook(pd.Timedelta, pd.Timedelta)
cattr.register_unstructure_hook(pd.Timedelta, str)

This seems like a quite common case (registering both at once), may something like this could work?

cattr.register_hooks(pd.Timedelta, pd.Timedelta, str)
  1. Same as above, but for attr classes. Also, maybe support passing attr.asdict and attr.astuple instead of cattr.{structure,unstructure}_attrs_from{dict,tuple}? E.g., instead of
cattr.register_structure_hook(Foo, cattr.structure_attrs_fromtuple)
cattr.register_unstructure_hook(Foo, cattr.unstructure_attrs_fromtuple)

maybe you could also just do

cattr.register_hooks(Foo, attr.astuple)
  1. Registering hooks for attr types. Maybe allow embedding conversion information into the types themselves? E.g. via some predefined dunder magics, or decorators? Hypothetically, kind of like:
@attr.s(slots=True)
class Foo:
    a: int = attr.ib()
    
    __cattr__ = attr.astuple  # or separate __structure__ / __unstructure__

or maybe even

@cattr.s(slots=True, cattr=attr.astuple)  # or structure= / unstructure=
class Foo:
    a: int = attr.ib()

This one's may be a bit too much, but I thought I'd share all my somewhat random thoughts anyhow :)

Thanks!

aldanor avatar Dec 28 '17 01:12 aldanor

2.5) I've noticed the example in 2) doesn't actually work at all, since cattr.unstructure_attrs_fromtuple doesn't even exist. I guess you currently have to do something like this:

cattr.global_converter.register_unstructure_hook(
    Foo,
    cattr.global_converter.unstructure_attrs_astuple
)

whereas it could be at least

cattr.register_unstructure_hook(Foo, cattr.unstructure_attrs_astuple)

or, as suggested above,

cattr.register_unstructure_hook(Foo, attr.astuple)

aldanor avatar Dec 28 '17 12:12 aldanor

I'd like to add another suggestion, Can the register_<>_hook be used as a decorator.

@cattr.register_structure_hook(MyClass)
def parse_my_class(value, _type):
  ...

software-opal avatar Oct 30 '18 05:10 software-opal

When using make_dict_unstructure_fn/make_dict_structure_fn it becomes even more verbose. So I usually just create a function like:

def converter_register_hooks(cls, **kwargs):
    converter.register_unstructure_hook(cls, make_dict_unstructure_fn(cls, converter, **kwargs))
    converter.register_structure_hook(cls, make_dict_structure_fn(cls, converter, **kwargs))

And then instead of:

converter.register_unstructure_hook(Person, make_dict_unstructure_fn(
    Person, converter, full_name=override(rename='fullName')))
converter.register_structure_hook(Person, make_dict_structure_fn(
    Person, converter, full_name=override(rename='fullName')))

I just need to do the following for each of my classes:

converter_register_hooks(Person, full_name=override(rename='fullName'))

It would be very nice to see something simpler like this already build in.

ericbn avatar Oct 13 '21 15:10 ericbn

Here is another API booster

class HashAble:
    """Must be attrs class"""
    converter = cattrs.Converter()
    __types__: list[Type[HashAble]] = []

    def __init_subclass__(cls, **kwargs):
        cls.__types__.append(cls)

    @classmethod
    def generate(cls):
        for klass in cls.__types__:
            with contextlib.suppress(AttributeError):
                cls.converter.register_unstructure_hook(klass, klass.__unstructure__)

            with contextlib.suppress(AttributeError):
                omits = klass.__omit__()
                kwargs = {name: cattrs.gen.override(omit=True) for name in omits}
                cls.converter.register_unstructure_hook(
                    klass,
                    cattrs.gen.make_dict_unstructure_fn(
                        klass, cls.converter, **kwargs
                    )
                )


    if TYPE_CHECKING:
        @classmethod
        def __unstructure__(cls, inst: Self) -> dict:
            raise NotImplementedError
        @classmethod
        def __omit__(cls) -> list[str]:
            raise NotImplementedError

    def asdict(self) -> dict:
        return self.converter.unstructure(self)

@define
class SomeType(HashAble):
    remove_me: str
    
    __omit__ = ['remove_me']
    
    @classmethod
    def __unstructure__(cls, inst):
        return {
        'no': "fields!!!"
        }

HashAble.generate()

nrbnlulu avatar Jan 12 '23 15:01 nrbnlulu

Thanks for the ideas. There's a new API for validation and customization coming in 24.1. Going to close this as stale though, the OP is 6 years old almost ;)

Tinche avatar Nov 19 '23 22:11 Tinche