From ed171572719b3e49b4051322d19dc988044bb96a Mon Sep 17 00:00:00 2001 From: Stefan Scherfke Date: Sun, 13 Jun 2021 22:27:53 +0200 Subject: [PATCH 1/5] Convert transformed attrs to AttrsClass Fixes: #821 --- src/attr/_make.py | 2 +- tests/test_hooks.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 658eb047b..10663683e 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -599,7 +599,7 @@ def _transform_attrs( had_default = True if field_transformer is not None: - attrs = field_transformer(cls, attrs) + attrs = AttrsClass(field_transformer(cls, attrs)) return _Attributes((attrs, base_attrs, base_attr_map)) 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): From 14f490a18ad8faff9eace1ddfd03c371a40b1e79 Mon Sep 17 00:00:00 2001 From: Stefan Scherfke Date: Sun, 13 Jun 2021 22:30:39 +0200 Subject: [PATCH 2/5] Add cangelog entry --- changelog.d/824.changes.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/824.changes.rst 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. From 3a4c50d978f87252e43a941792cb13cc64195322 Mon Sep 17 00:00:00 2001 From: Stefan Scherfke Date: Mon, 14 Jun 2021 20:07:16 +0200 Subject: [PATCH 3/5] Only call AttrsClass once --- src/attr/_make.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 10663683e..1dbe50b28 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -581,7 +581,7 @@ def _transform_attrs( 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 @@ -599,7 +599,9 @@ def _transform_attrs( had_default = True if field_transformer is not None: - attrs = AttrsClass(field_transformer(cls, attrs)) + attrs = field_transformer(cls, attrs) + + attrs = AttrsClass(attrs) return _Attributes((attrs, base_attrs, base_attr_map)) From a50339fdd0d00df4c871c1f7c3d213b0ac77ade4 Mon Sep 17 00:00:00 2001 From: Stefan Scherfke Date: Mon, 14 Jun 2021 20:08:01 +0200 Subject: [PATCH 4/5] Calm mypy by inline the AttrsClass call --- src/attr/_make.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 1dbe50b28..64f43a4d2 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -601,8 +601,7 @@ def _transform_attrs( if field_transformer is not None: attrs = field_transformer(cls, attrs) - attrs = AttrsClass(attrs) - return _Attributes((attrs, base_attrs, base_attr_map)) + return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map)) if PYPY: From b385b311b1324cb48871aa7f2390e721553bb653 Mon Sep 17 00:00:00 2001 From: Stefan Scherfke Date: Mon, 14 Jun 2021 20:55:07 +0200 Subject: [PATCH 5/5] Defer AttrsClass creation as long as possible --- src/attr/_make.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 64f43a4d2..5f83eff5d 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -573,10 +573,6 @@ 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] @@ -601,6 +597,11 @@ def _transform_attrs( if field_transformer is not None: attrs = field_transformer(cls, attrs) + # 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))