from datetime import datetime from typing import Dict, List import attr class TestTransformHook: """ Tests for `attrs(tranform_value_serializer=func)` """ def test_hook_applied(self): """ The transform hook is applied to all attributes. Types can be missing, explicitly set, or annotated. """ results = [] def hook(cls, attribs): results[:] = [(a.name, a.type) for a in attribs] return attribs @attr.s(field_transformer=hook) class C: x = attr.ib() y = attr.ib(type=int) z: float = attr.ib() assert results == [("x", None), ("y", int), ("z", float)] def test_hook_applied_auto_attrib(self): """ The transform hook is applied to all attributes and type annotations are detected. """ results = [] def hook(cls, attribs): results[:] = [(a.name, a.type) for a in attribs] return attribs @attr.s(auto_attribs=True, field_transformer=hook) class C: x: int y: str = attr.ib() assert results == [("x", int), ("y", str)] def test_hook_applied_modify_attrib(self): """ The transform hook can modify attributes. """ def hook(cls, attribs): return [a.evolve(converter=a.type) for a in attribs] @attr.s(auto_attribs=True, field_transformer=hook) class C: x: int = attr.ib(converter=int) y: float c = C(x="3", y="3.14") assert c == C(x=3, y=3.14) def test_hook_remove_field(self): """ It is possible to remove fields via the hook. """ def hook(cls, attribs): return [a for a in attribs if a.type is not int] @attr.s(auto_attribs=True, field_transformer=hook) class C: x: int y: float assert attr.asdict(C(2.7)) == {"y": 2.7} def test_hook_add_field(self): """ It is possible to add fields via the hook. """ def hook(cls, attribs): a1 = attribs[0] a2 = a1.evolve(name="new") return [a1, a2] @attr.s(auto_attribs=True, field_transformer=hook) class C: x: int assert attr.asdict(C(1, 2)) == {"x": 1, "new": 2} def test_hook_with_inheritance(self): """ The hook receives all fields from base classes. """ def hook(cls, attribs): assert [a.name for a in attribs] == ["x", "y"] # Remove Base' "x" return attribs[1:] @attr.s(auto_attribs=True) class Base: x: int @attr.s(auto_attribs=True, field_transformer=hook) class Sub(Base): y: int assert attr.asdict(Sub(2)) == {"y": 2} class TestAsDictHook: def test_asdict(self): """ asdict() calls the hooks in attrs classes and in other datastructures like lists or dicts. """ def hook(inst, a, v): if isinstance(v, datetime): return v.isoformat() return v @attr.dataclass class Child: x: datetime y: List[datetime] @attr.dataclass class Parent: a: Child b: List[Child] c: Dict[str, Child] d: Dict[str, datetime] inst = Parent( a=Child(1, [datetime(2020, 7, 1)]), b=[Child(2, [datetime(2020, 7, 2)])], c={"spam": Child(3, [datetime(2020, 7, 3)])}, d={"eggs": datetime(2020, 7, 4)}, ) result = attr.asdict(inst, value_serializer=hook) assert result == { "a": {"x": 1, "y": ["2020-07-01T00:00:00"]}, "b": [{"x": 2, "y": ["2020-07-02T00:00:00"]}], "c": {"spam": {"x": 3, "y": ["2020-07-03T00:00:00"]}}, "d": {"eggs": "2020-07-04T00:00:00"}, } def test_asdict_calls(self): """ The correct instances and attribute names are passed to the hook. """ calls = [] def hook(inst, a, v): calls.append((inst, a.name if a else a, v)) return v @attr.dataclass class Child: x: int @attr.dataclass class Parent: a: Child b: List[Child] c: Dict[str, Child] inst = Parent(a=Child(1), b=[Child(2)], c={"spam": Child(3)}) attr.asdict(inst, value_serializer=hook) assert calls == [ (inst, "a", inst.a), (inst.a, "x", inst.a.x), (inst, "b", inst.b), (inst.b[0], "x", inst.b[0].x), (inst, "c", inst.c), (None, None, "spam"), (inst.c["spam"], "x", inst.c["spam"].x), ]