2021-12-27 08:29:09 +00:00
|
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
|
2023-08-20 10:03:53 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2020-10-15 07:33:59 +00:00
|
|
|
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):
|
2021-03-06 00:01:04 +00:00
|
|
|
attr.resolve_types(cls, attribs=attribs)
|
2020-10-15 07:33:59 +00:00
|
|
|
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):
|
2021-03-06 00:01:04 +00:00
|
|
|
attr.resolve_types(cls, attribs=attribs)
|
2020-10-15 07:33:59 +00:00
|
|
|
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):
|
2021-03-06 00:01:04 +00:00
|
|
|
attr.resolve_types(cls, attribs=attribs)
|
2020-10-16 10:40:12 +00:00
|
|
|
return [a.evolve(converter=a.type) for a in attribs]
|
2020-10-15 07:33:59 +00:00
|
|
|
|
|
|
|
@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):
|
2021-03-06 00:01:04 +00:00
|
|
|
attr.resolve_types(cls, attribs=attribs)
|
2020-10-15 07:33:59 +00:00
|
|
|
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")
|
2020-10-15 07:33:59 +00:00
|
|
|
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}
|
|
|
|
|
2022-11-30 14:39:57 +00:00
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2020-10-15 07:33:59 +00:00
|
|
|
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}
|
|
|
|
|
2021-09-22 19:58:32 +00:00
|
|
|
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)
|
|
|
|
|
2020-10-15 07:33:59 +00:00
|
|
|
|
|
|
|
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
|
2023-08-20 10:03:53 +00:00
|
|
|
y: list[datetime]
|
2020-10-15 07:33:59 +00:00
|
|
|
|
|
|
|
@attr.dataclass
|
|
|
|
class Parent:
|
|
|
|
a: Child
|
2023-08-20 10:03:53 +00:00
|
|
|
b: list[Child]
|
|
|
|
c: dict[str, Child]
|
|
|
|
d: dict[str, datetime]
|
2020-10-15 07:33:59 +00:00
|
|
|
|
|
|
|
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
|
2023-08-20 10:03:53 +00:00
|
|
|
b: list[Child]
|
|
|
|
c: dict[str, Child]
|
2020-10-15 07:33:59 +00:00
|
|
|
|
|
|
|
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),
|
|
|
|
]
|