cattrs icon indicating copy to clipboard operation
cattrs copied to clipboard

Using make_dict_structure_fn seems to break my other structure hooks

Open fcrbe opened this issue 3 years ago • 1 comments

  • cattrs version: 1.10.0
  • Python version: 3.9.7
  • Operating System: MacOS 11.4

Description

I'm registering two structure hooks which should help me transform my object into a dict and vice versa. One is renaming my field (from id to _id), while the other is transforming my pendulum DateTime into an isostring. For some reason the DateTime unstructure hook is getting lost if I define it after the Class structure hook. If I start with it, everything works.

What I Did

import cattr
import pendulum
from attr import define, field
from cattr.gen import make_dict_unstructure_fn, override, make_dict_structure_fn
from pendulum import DateTime


@define
class Car:
    id: int
    manufactured: DateTime

volvo = Car(id=5, manufactured=DateTime.now())

converter = cattr.Converter()

converter.register_unstructure_hook(Car, make_dict_unstructure_fn(Car, converter, id=override(rename="_id")))
converter.register_unstructure_hook(DateTime, lambda dt: dt.to_iso8601_string())

converter.register_structure_hook(Car, make_dict_structure_fn(Car, converter, id=override(rename="_id")))
converter.register_structure_hook(DateTime, lambda dt, _: pendulum.parse(dt))

print(volvo)

unstructured = converter.unstructure(volvo)
print(unstructured)

structured_again = converter.structure(unstructured, Car)
print(structured_again)

The above results in following error (note the "manufactured" field also did not get unstructured the way it should). It complains I should register a structure hook, which I actually did...:

Car(id=5, manufactured=DateTime(2022, 4, 3, 16, 22, 40, 51844, tzinfo=Timezone('Europe/Brussels')))
{'_id': 5, 'manufactured': DateTime(2022, 4, 3, 16, 22, 40, 51844, tzinfo=Timezone('Europe/Brussels'))}
Traceback (most recent call last):
...
cattr.errors.StructureHandlerNotFoundError: Unsupported type: <class 'pendulum.datetime.DateTime'>. Register a structure hook for it.

Whereas if I switch the order of the (un)structure hooks like below, everything just works:

converter.register_unstructure_hook(DateTime, lambda dt: dt.to_iso8601_string())
converter.register_unstructure_hook(Car, make_dict_unstructure_fn(Car, converter, id=override(rename="_id")))

converter.register_structure_hook(DateTime, lambda dt, _: pendulum.parse(dt))
converter.register_structure_hook(Car, make_dict_structure_fn(Car, converter, id=override(rename="_id")))

Results in ("manufactured" got unstructured correctly in an iso8601 timestamp and id got renamed into _id during unstructure and vice versa):

Car(id=5, manufactured=DateTime(2022, 4, 3, 16, 25, 12, 840444, tzinfo=Timezone('Europe/Brussels')))
{'_id': 5, 'manufactured': '2022-04-03T16:25:12.840444+02:00'}
Car(id=5, manufactured=DateTime(2022, 4, 3, 16, 25, 12, 840444, tzinfo=Timezone('+02:00')))

fcrbe avatar Apr 03 '22 14:04 fcrbe

Yeah, that's expected. make_dict_unstructure_fn resolves the hooks for all attributes from the given converter at the time it's created - there's no dispatch at the time it's called. So you need to register dependent hooks earlier, or call make_dict_unstructure_fn again. It's a speed thing, and I've found the condition isn't too difficult to satisfy.

You might also consider using a GenConverter instead - it's faster and has more features.

Tinche avatar Apr 03 '22 18:04 Tinche