/
niceSOFT
/
python3-attrs
Обзор
Документация
Войти
/
niceSOFT
/
python3-attrs
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
tests/test_hooks.py
453 строки
12 KB
Hynek Schlawack
update
02 авг 2026, 10:03
02 авг 2026, 10:03
116681b
Код
Авторство
О чём код?
# SPDX-License-Identifier: MIT from __future__ import annotations from datetime import datetime import pytest 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 [("x", None), ("y", int), ("z", float)] == results 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 [("x", int), ("y", str)] == results 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(x=3, y=3.14) == c 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 {"y": 2.7} == attr.asdict(C(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 {"x": 1, "new": 2} == attr.asdict(C(1, 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_reorder_fields(self): """ It is possible to reorder fields via the hook. """ def hook(cls, attribs): return sorted(attribs, key=lambda x: x.metadata["field_order"]) @attr.s(field_transformer=hook) class C: x: int = attr.ib(metadata={"field_order": 1}) y: int = attr.ib(metadata={"field_order": 0}) assert {"x": 0, "y": 1} == attr.asdict(C(1, 0)) def test_hook_reorder_fields_before_order_check(self): """ It is possible to reorder fields via the hook before order-based errors are raised. Regression test for #1147. """ def hook(cls, attribs): return sorted(attribs, key=lambda x: x.metadata["field_order"]) @attr.s(field_transformer=hook) class C: x: int = attr.ib(metadata={"field_order": 1}, default=0) y: int = attr.ib(metadata={"field_order": 0}) assert {"x": 0, "y": 1} == attr.asdict(C(1)) def test_hook_conflicting_defaults_after_reorder(self): """ Raises `ValueError` if attributes with defaults are followed by mandatory attributes after the hook reorders fields. Regression test for #1147. """ def hook(cls, attribs): return sorted(attribs, key=lambda x: x.metadata["field_order"]) with pytest.raises(ValueError) as e: @attr.s(field_transformer=hook) class C: x: int = attr.ib(metadata={"field_order": 1}) y: int = attr.ib(metadata={"field_order": 0}, default=0) assert ( ( "No mandatory attributes allowed after an attribute with a " "default value or factory. Attribute in question: Attribute" "(name='x', default=NOTHING, validator=None, repr=True, " "eq=True, eq_key=None, order=True, order_key=None, " "hash=None, init=True, " "metadata=mappingproxy({'field_order': 1}), type='int', converter=None, " "kw_only=False, inherited=False, on_setattr=None, alias='x')" ), ) == e.value.args def test_hook_with_inheritance(self): """ The hook receives all fields from base classes. """ def hook(cls, attribs): assert ["x", "y"] == [a.name for a in attribs] # 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 {"y": 2} == attr.asdict(Sub(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 "CAttributes" == fields_type.__name__ assert issubclass(fields_type, tuple) def test_hook_generator(self): """ field_transfromers can be a generators. Regression test for #1416. """ def hook(cls, attribs): yield from attribs @attr.s(auto_attribs=True, field_transformer=hook) class Base: x: int assert ["x"] == [a.name for a in attr.fields(Base)] def test_hook_alias_available(self): """ The field_transformer receives attributes with default aliases already resolved, not None. Regression test for #1479. """ seen = [] def hook(cls, attribs): seen[:] = [(a.name, a.alias, a.alias_is_default) for a in attribs] return attribs @attr.s(auto_attribs=True, field_transformer=hook) class C: _private: int _explicit: int = attr.ib(alias="_explicit") public: int assert [ ("_private", "private", True), ("_explicit", "_explicit", False), ("public", "public", True), ] == seen def test_hook_evolve_name_updates_auto_alias(self): """ When a field_transformer evolves a field's name, the alias is automatically updated if it was auto-generated. Regression test for #1479. """ def hook(cls, attribs): return [a.evolve(name="renamed") for a in attribs] @attr.s(auto_attribs=True, field_transformer=hook) class C: _original: int f = attr.fields(C).renamed assert "renamed" == f.alias assert f.alias_is_default is True def test_hook_evolve_name_keeps_explicit_alias(self): """ When a field_transformer evolves a field's name but the field had an explicit alias, the alias is preserved. Regression test for #1479. """ def hook(cls, attribs): return [a.evolve(name="renamed") for a in attribs] @attr.s(auto_attribs=True, field_transformer=hook) class C: original: int = attr.ib(alias="my_alias") f = attr.fields(C).renamed assert "my_alias" == f.alias assert f.alias_is_default is False def test_hook_new_field_without_alias(self): """ When a field_transformer adds a brand-new field without setting an alias, the post-transformer alias resolution fills it in. Regression test for #1479. """ def hook(cls, attribs): return [ *list(attribs), attr.Attribute( name="_extra", default=0, validator=None, repr=True, cmp=None, hash=None, init=True, metadata={}, type=int, converter=None, kw_only=False, eq=True, eq_key=None, order=True, order_key=None, on_setattr=None, alias=None, inherited=False, ), ] @attr.s(auto_attribs=True, field_transformer=hook) class C: x: int f = attr.fields(C)._extra assert "extra" == f.alias assert f.alias_is_default is True def test_hook_explicit_alias_matching_default(self): """ When a user explicitly sets an alias that happens to equal the auto-generated default, alias_is_default is still False. Regression test for #1479. """ @attr.s(auto_attribs=True) class C: _private: int = attr.ib(alias="private") f = attr.fields(C)._private assert "private" == f.alias assert f.alias_is_default is False def test_hook_evolve_alias_sets_not_default(self): """ When a field_transformer uses evolve() to set an explicit alias, alias_is_default becomes False. Regression test for #1479. """ def hook(cls, attribs): return [a.evolve(alias="custom") for a in attribs] @attr.s(auto_attribs=True, field_transformer=hook) class C: x: int f = attr.fields(C).x assert "custom" == f.alias assert f.alias_is_default is False 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 { "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"}, } == result 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 [ (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), ] == calls