A few syntax sugar ideas
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?
- 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)
- Same as above, but for attr classes. Also, maybe support passing
attr.asdictandattr.astupleinstead ofcattr.{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)
- 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!
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)
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):
...
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.
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()
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 ;)