python-attrs / cattrs

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

Hook factories for built in types #561

Closed salotz closed 4 months ago

salotz commented 4 months ago

Description

I am trying to register custom converters for built in types and I found this doesn't always work as expected.

I have a better workaround than using factories like this, but I wanted to understand why this doesn't work as expected.

What I Did

import cattrs

conv = cattrs.Converter()

assert conv.structure(100, int) == 100

def is_int(typ):
    return typ is int

assert is_int(int)
assert not is_int(float)

def factory(typ):
    def hook(val, cl):
        return str(val)

conv.register_structure_hook_factory(
    is_int,
    factory,
)

# returns int
result = conv.structure(100, int)

# fails
assert type(result) is str, f"type: {type(result)}"
assert conv.structure(100, int) == "100"

# try another builtin type
conv.register_structure_hook_factory(
    lambda t: t is float,
    factory,
)

result = conv.structure(1.0, float)
# fails
assert type(result) is str, f"type: {type(result)}"
assert conv.structure(100, int) == "100"

# base converter
base_conv = cattrs.BaseConverter()
base_conv.register_structure_hook_factory(
    is_int,
    factory,
)

result = base_conv.structure(100, int)
assert type(result) is str, f"type: {type(result)}"

## Unstructuring also doesn't work

conv.register_unstructure_hook_factory(
    is_int,
    factory,
)

result = conv.unstructure(100)
# fails
assert type(result) is str, f"type: {type(result)}"

result = conv.unstructure(100, int)
# fails
assert type(result) is str, f"type: {type(result)}"
Tinche commented 4 months ago

Hello!

The short answer: you should use register_structure_hook for primitive types like int, str, bytes, floats.

The long answer: cattrs has two mechanisms for looking up hooks for types. The first one is based on functools.singledispatch, it's the older of the two, and it's easy and simple. It doesn't support factories, so it's appropriate for simple types.

The second one is based on predicates. It's the more complex and more powerful one.

cattrs has to check one, and then the other. Because of historical reasons, for backwards compatibility and also because it I think it makes sense, the singledispatch mechanism is used first and then the predicate mechanism.

You're trying to customize int behavior using the predicate mechanism, but the default hook for int uses singledispatch, so that triggers first. So in order to successfully override these hooks, you can use register_structure_hook instead.

Hope this helps!