attrs/tests/test_hooks.py

229 lines
6.0 KiB
Python
Raw Permalink Normal View History

2021-12-27 08:29:09 +00:00
# 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)
2020-10-16 10:40:12 +00:00
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]
2020-10-16 10:40:12 +00:00
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),
]