| # SPDX-License-Identifier: MIT |
| |
| from __future__ import annotations |
| |
| from datetime import datetime |
| |
| 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): |
| attr.resolve_types(cls, attribs=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): |
| attr.resolve_types(cls, attribs=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): |
| attr.resolve_types(cls, attribs=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): |
| attr.resolve_types(cls, attribs=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_override_alias(self): |
| """ |
| It is possible to set field alias via hook |
| """ |
| |
| def use_dataclass_names(cls, attribs): |
| return [a.evolve(alias=a.name) for a in attribs] |
| |
| @attr.s(auto_attribs=True, field_transformer=use_dataclass_names) |
| class NameCase: |
| public: int |
| _private: int |
| __dunder__: int |
| |
| assert NameCase(public=1, _private=2, __dunder__=3) == NameCase( |
| 1, 2, 3 |
| ) |
| |
| 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} |
| |
| def test_attrs_attrclass(self): |
| """ |
| The list of attrs returned by a field_transformer is converted to |
| "AttrsClass" again. |
| |
| Regression test for #821. |
| """ |
| |
| @attr.s(auto_attribs=True, field_transformer=lambda c, a: list(a)) |
| class C: |
| x: int |
| |
| fields_type = type(attr.fields(C)) |
| assert fields_type.__name__ == "CAttributes" |
| assert issubclass(fields_type, tuple) |
| |
| |
| 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), |
| ] |