python-attrs / cattrs

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

Why does structuring of datetime in attrs class need a structure hook? #526

Closed kkg-else42 closed 3 months ago

kkg-else42 commented 3 months ago

Hey there!

In the following situation it is necessary to use a structure hook for the handling of datetime. And I know how to do that. But I would be interested to know why it is necessary.

import attrs
import cattrs
import datetime

@attrs.frozen
class A:
    dt: datetime.datetime

print(cattrs.structure({'dt': datetime.datetime(2024, 3, 26, 16, 40, 30)}, A))
Tinche commented 3 months ago

So this is how it works. Cattrs binds types to functions (hooks). The defaults are different when structuring and unstructuring because data is treated differently when entering and exiting your system.

Every type needs a hook. If a hook does not exist, the fallback hook factory gets called, and by default that will cause an exception to be raised.

You may be wondering why the fallback hook doesn't do an isinstance check before raising, and the answer is that there are a lot of edge cases where that wouldn't work (especially with fancy types, protocols, newtypes, type aliases...), and a whitelist approach is easier to understand.

That said, you can assume responsibility for this yourself like this:

def structure_hook_factory(type):
    def hook(val, _):
        if not isinstance(val, type):
            raise ValueError("Wrong type")
        return val

    return hook

c = Converter(structure_fallback_factory=structure_hook_factory)

print(c.structure({"dt": datetime.datetime(2024, 3, 26, 16, 40, 30)}, A))

>>> A(dt=datetime.datetime(2024, 3, 26, 16, 40, 30))

So it's a design choice!