Repository: python-attrs/attrs
Pull request #824 (2021-06-13 20:28:26)

Convert transformed attrs to AttrsClass
Fixes: #821

# Pull Request Check List

This is just a friendly reminder about the most common mistakes.  Please make sure that you tick all boxes.  But please read our [contribution guide](https://www.attrs.org/en/latest/contributing.html) at least once, it will save you unnecessary review cycles!

If an item doesn't apply to your pull request, **check it anyway** to make it apparent that there's nothing left to do.  If your pull request is a documentation fix or a trivial typo, feel free to delete the whole thing.

- [x] Added **tests** for changed code.
- [x] New features have been added to our [Hypothesis testing strategy](https://github.com/python-attrs/attrs/blob/main/tests/strategies.py).
- [x] Changes or additions to public APIs are reflected in our type stubs (files ending in ``.pyi``).
    - [x] ...and used in the stub test file `tests/typing_example.py`.
- [x] Updated **documentation** for changed code.
    - [x] New functions/classes have to be added to `docs/api.rst` by hand.
    - [x] Changes to the signature of `@attr.s()` have to be added by hand too.
    - [x] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/markup/para.html#directive-versionadded).  Find the appropriate next version in our [``__init__.py``](https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py) file.
- [x] Documentation in `.rst` files is written using [semantic newlines](https://rhodesmill.org/brandon/2012/one-sentence-per-line/).
- [x] Changes (and possible deprecations) have news fragments in [`changelog.d`](https://github.com/python-attrs/attrs/blob/main/changelog.d).

If you have *any* questions to *any* of the points above, just **submit and ask**!  This checklist is here to *help* you, not to deter you from contributing!


hynek: I'm somewhat confident that the CI breakage is related to #771.
hynek: I'm sorry but I've completely forgotten about this. Is there anything open or can it be merged as is?
sscherfke: I thinks this can be merged. The issue is fixed, I added a regression test and all checks are green.

Patch:
diff --git a/changelog.d/824.changes.rst b/changelog.d/824.changes.rst
new file mode 100644
index 0000000..4d3e6ac
--- /dev/null
+++ b/changelog.d/824.changes.rst
@@ -0,0 +1,1 @@
+Attributes transformed via ``field_transformer`` are wrapped with ``AttrsClass`` again.
diff --git a/src/attr/_make.py b/src/attr/_make.py
index 95a37ea..ac6bbc1 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:


Test patch:
diff --git a/tests/test_hooks.py b/tests/test_hooks.py
index 5fcb100..7e5ac3d 100644
--- a/tests/test_hooks.py
+++ b/tests/test_hooks.py
@@ -117,6 +117,22 @@ class TestTransformHook:
 
         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):