python-attrs / cattrs

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

`prefer_attrib_converters` doesn't work when the field is aliased #527

Closed ljnsn closed 3 months ago

ljnsn commented 3 months ago

Description

I want to use the converter set on a field of an attrs model of mine. The field also has an alias. Trying to structure with cattrs doesn't work in this case.

What I Did

import enum
from typing import Any, Callable, Mapping

import attrs
import cattrs
from cattrs.gen import make_dict_unstructure_fn, make_dict_structure_fn

class X(enum.Enum):
    A = "a"
    B = "b"
    OTHER = "other"

def _x_or_other(value: Any) -> X:
    try:
        return X(value)
    except ValueError:
        return X.OTHER

@attrs.define()
class Bar:
    x: X = attrs.field(converter=_x_or_other, alias="Y")

converter = cattrs.Converter(prefer_attrib_converters=True)

def _to_alias_unstructure(cls: type[Any]) -> Callable[[Any], dict[str, Any]]:
    """Unstructure hook using alias."""
    return make_dict_unstructure_fn(
        cls,
        converter,
        _cattrs_use_alias=True,
    )

def _to_alias_structure(
    cls: type[Any],
) -> Callable[[Mapping[str, Any], Any], Callable[[Any, Any], Any]]:
    """Structure hook using alias."""
    return make_dict_structure_fn(
        cls,
        converter,
        _cattrs_use_alias=True,
    )

converter.register_unstructure_hook_factory(attrs.has, _to_alias_unstructure)
converter.register_structure_hook_factory(attrs.has, _to_alias_structure)

bar1 = {"Y": "a"}
bar2 = {"Y": "whatever"}

converter.structure(bar1, Bar)  # works
converter.structure(bar2, Bar)  # fails
Tinche commented 3 months ago

Hi,

if you're overriding the entire attrs hook you'll need to pass in _cattrs_prefer_attrib_converter yourself, like this:

def _to_alias_structure(
    cls: type[Any],
) -> Callable[[Mapping[str, Any], Any], Callable[[Any, Any], Any]]:
    """Structure hook using alias."""
    return make_dict_structure_fn(
        cls, converter, _cattrs_use_alias=True, _cattrs_prefer_attrib_converters=True
    )

That said, I realize it'd be better if make_dict_structure_fn took the value of that parameter from the converter itself, so I've created https://github.com/python-attrs/cattrs/pull/528 to address this for the next version.

ljnsn commented 3 months ago

Ah makes sense, thanks for the quick response and explanation :slightly_smiling_face: