diff --git a/changelog.d/824.changes.rst b/changelog.d/824.changes.rst new file mode 100644 index 000000000..4d3e6acda --- /dev/null +++ b/changelog.d/824.changes.rst @@ -0,0 +1 @@ +Attributes transformed via ``field_transformer`` are wrapped with ``AttrsClass`` again. diff --git a/src/attr/_make.py b/src/attr/_make.py index 95a37ea64..ac6bbc10e 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -581,15 +581,11 @@ def _transform_attrs( cls, {a.name for a in own_attrs} ) - attr_names = [a.name for a in base_attrs + own_attrs] - - AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) - if kw_only: own_attrs = [a.evolve(kw_only=True) for a in own_attrs] base_attrs = [a.evolve(kw_only=True) for a in base_attrs] - attrs = AttrsClass(base_attrs + own_attrs) + attrs = base_attrs + own_attrs # Mandatory vs non-mandatory attr order only matters when they are part of # the __init__ signature and when they aren't kw_only (which are moved to @@ -608,7 +604,13 @@ def _transform_attrs( if field_transformer is not None: attrs = field_transformer(cls, attrs) - return _Attributes((attrs, base_attrs, base_attr_map)) + + # Create AttrsClass *after* applying the field_transformer since it may + # add or remove attributes! + attr_names = [a.name for a in attrs] + AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) + + return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map)) if PYPY: diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 5fcb10047..7e5ac3d9e 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -117,6 +117,22 @@ class Sub(Base): 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):