Repository: python-attrs/attrs
Pull request #1407 (2025-02-09 20:32:36)

ClassBuilder: eval only once

codspeed-hq[bot]: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Performance Report](https://codspeed.io/python-attrs/attrs/branches/tin%2Fspeed-up-eval)

### Merging #1407 will **degrade performances by 91.09%**

<sub>Comparing <code>tin/speed-up-eval</code> (d473ae9) with <code>main</code> (e048efc)</sub>



### Summary

`❌ 1` regressions  
`✅ 7` untouched benchmarks  


> :warning: _Please fix the performance issues or [acknowledge them on CodSpeed](https://codspeed.io/python-attrs/attrs/branches/tin%2Fspeed-up-eval)._

### Benchmarks breakdown

|     | Benchmark | `BASE` | `HEAD` | Change |
| --- | --------- | ----------------------- | ------------------- | ------ |
| ❌ | `` test_create_frozen_class `` | 1.8 s | 19.8 s | -91.09% |

Tinche: I added type hints to some internal functions. This was to make dev easier for me, not to type-check formally.
hynek: > I added type hints to some internal functions. This was to make dev easier for me, not to type-check formally.

do you think it would be a lot of work to make the code base pass in lenient mode? I.e. if there's hints, they must not contradict otherwise YOLO?
Tinche: > News fragment please, then I think this is good to go.

Done!


Patch:
diff --git a/changelog.d/1407.change.md b/changelog.d/1407.change.md
new file mode 100644
index 0000000..7da44ab
--- /dev/null
+++ b/changelog.d/1407.change.md
@@ -0,0 +1,1 @@
+Speed up class creation by 30%-50% by compiling methods only once and using a variety of other techniques.
diff --git a/src/attr/_cmp.py b/src/attr/_cmp.py
index f367bb3..09bab49 100644
--- a/src/attr/_cmp.py
+++ b/src/attr/_cmp.py
@@ -4,7 +4,7 @@
 import functools
 import types
 
-from ._make import _make_ne
+from ._make import __ne__
 
 
 _operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="}
@@ -71,7 +71,7 @@ def cmp_using(
     if eq is not None:
         has_eq_function = True
         body["__eq__"] = _make_operator("eq", eq)
-        body["__ne__"] = _make_ne()
+        body["__ne__"] = __ne__
 
     if lt is not None:
         num_order_functions += 1
diff --git a/src/attr/_make.py b/src/attr/_make.py
index b1550eb..0c5aa0b 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -6,16 +6,16 @@ import abc
 import contextlib
 import copy
 import enum
-import functools
 import inspect
 import itertools
 import linecache
 import sys
 import types
-import typing
 import unicodedata
 
-from operator import itemgetter
+from collections.abc import Callable, Mapping
+from functools import cached_property
+from typing import Any, NamedTuple, TypeVar
 
 # We need to import _compat itself in addition to the _compat members to avoid
 # having the thread-local in the globals here.
@@ -209,7 +209,12 @@ def attrib(
     )
 
 
-def _compile_and_eval(script, globs, locs=None, filename=""):
+def _compile_and_eval(
+    script: str,
+    globs: dict[str, Any] | None,
+    locs: Mapping[str, object] | None = None,
+    filename: str = "",
+) -> None:
     """
     Evaluate the script with the given global (globs) and local (locs)
     variables.
@@ -218,10 +223,16 @@ def _compile_and_eval(script, globs, locs=None, filename=""):
     eval(bytecode, globs, locs)
 
 
-def _make_method(name, script, filename, globs, locals=None):
+def _linecache_and_compile(
+    script: str,
+    filename: str,
+    globs: dict[str, Any] | None,
+    locals: Mapping[str, object] | None = None,
+) -> dict[str, Any]:
     """
-    Create the method with the script given and return the method object.
+    Cache the script with _linecache_, compile it and return the _locals_.
     """
+
     locs = {} if locals is None else locals
 
     # In order of debuggers like PDB being able to step through the code,
@@ -244,10 +255,10 @@ def _make_method(name, script, filename, globs, locals=None):
 
     _compile_and_eval(script, globs, locs, filename)
 
-    return locs[name]
+    return locs
 
 
-def _make_attr_tuple_class(cls_name, attr_names):
+def _make_attr_tuple_class(cls_name: str, attr_names: list[str]) -> type:
     """
     Create a tuple subclass to hold `Attribute`s for an `attrs` class.
 
@@ -258,35 +269,22 @@ def _make_attr_tuple_class(cls_name, attr_names):
         x = property(itemgetter(0))
     """
     attr_class_name = f"{cls_name}Attributes"
-    attr_class_template = [
-        f"class {attr_class_name}(tuple):",
-        "    __slots__ = ()",
-    ]
-    if attr_names:
-        for i, attr_name in enumerate(attr_names):
-            attr_class_template.append(
-                f"    {attr_name} = _attrs_property(_attrs_itemgetter({i}))"
-            )
-    else:
-        attr_class_template.append("    pass")
-    globs = {"_attrs_itemgetter": itemgetter, "_attrs_property": property}
-    _compile_and_eval("\n".join(attr_class_template), globs)
-    return globs[attr_class_name]
+    body = {}
+    for i, attr_name in enumerate(attr_names):
+
+        def getter(self, i=i):
+            return self[i]
+
+        body[attr_name] = property(getter)
+    return type(attr_class_name, (tuple,), body)
 
 
 # Tuple class for extracted attributes from a class definition.
 # `base_attrs` is a subset of `attrs`.
-_Attributes = _make_attr_tuple_class(
-    "_Attributes",
-    [
-        # all attributes to build dunder methods for
-        "attrs",
-        # attributes that have been inherited
-        "base_attrs",
-        # map inherited attributes to their originating classes
-        "base_attrs_map",
-    ],
-)
+class _Attributes(NamedTuple):
+    attrs: type
+    base_attrs: list[Attribute]
+    base_attrs_map: dict[str, type]
 
 
 def _is_class_var(annot):
@@ -313,7 +311,9 @@ def _has_own_attribute(cls, attrib_name):
     return attrib_name in cls.__dict__
 
 
-def _collect_base_attrs(cls, taken_attr_names):
+def _collect_base_attrs(
+    cls, taken_attr_names
+) -> tuple[list[Attribute], dict[str, type]]:
     """
     Collect attr.ibs from base classes of *cls*, except *taken_attr_names*.
     """
@@ -374,7 +374,7 @@ def _collect_base_attrs_broken(cls, taken_attr_names):
 
 def _transform_attrs(
     cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer
-):
+) -> _Attributes:
     """
     Transform all `_CountingAttr`s on a class into `Attribute`s.
 
@@ -394,7 +394,7 @@ def _transform_attrs(
         ca_names = {
             name
             for name, attr in cd.items()
-            if isinstance(attr, _CountingAttr)
+            if attr.__class__ is _CountingAttr
         }
         ca_list = []
         annot_names = set()
@@ -404,12 +404,12 @@ def _transform_attrs(
             annot_names.add(attr_name)
             a = cd.get(attr_name, NOTHING)
 
-            if not isinstance(a, _CountingAttr):
-                a = attrib() if a is NOTHING else attrib(default=a)
+            if a.__class__ is not _CountingAttr:
+                a = attrib(a)
             ca_list.append((attr_name, a))
 
         unannotated = ca_names - annot_names
-        if len(unannotated) > 0:
+        if unannotated:
             raise UnannotatedAttributeError(
                 "The following `attr.ib`s lack a type annotation: "
                 + ", ".join(
@@ -422,16 +422,14 @@ def _transform_attrs(
             (
                 (name, attr)
                 for name, attr in cd.items()
-                if isinstance(attr, _CountingAttr)
+                if attr.__class__ is _CountingAttr
             ),
             key=lambda e: e[1].counter,
         )
 
+    fca = Attribute.from_counting_attr
     own_attrs = [
-        Attribute.from_counting_attr(
-            name=attr_name, ca=ca, type=anns.get(attr_name)
-        )
-        for attr_name, ca in ca_list
+        fca(attr_name, ca, anns.get(attr_name)) for attr_name, ca in ca_list
     ]
 
     if collect_by_mro:
@@ -469,17 +467,17 @@ def _transform_attrs(
     # Resolve default field alias after executing field_transformer.
     # This allows field_transformer to differentiate between explicit vs
     # default aliases and supply their own defaults.
-    attrs = [
-        a.evolve(alias=_default_init_alias_for(a.name)) if not a.alias else a
-        for a in attrs
-    ]
+    for a in attrs:
+        if not a.alias:
+            # Evolve is very slow, so we hold our nose and do it dirty.
+            _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name))
 
     # 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))
+    return _Attributes(AttrsClass(attrs), base_attrs, base_attr_map)
 
 
 def _make_cached_property_getattr(cached_properties, original_getattr, cls):
@@ -529,15 +527,9 @@ def _make_cached_property_getattr(cached_properties, original_getattr, cls):
         "original_getattr": original_getattr,
     }
 
-    return _make_method(
-        "__getattr__",
-        "\n".join(lines),
-        unique_filename,
-        glob,
-        locals={
-            "_cls": cls,
-        },
-    )
+    return _linecache_and_compile(
+        "\n".join(lines), unique_filename, glob, locals={"_cls": cls}
+    )["__getattr__"]
 
 
 def _frozen_setattrs(self, name, value):
@@ -632,6 +624,7 @@ class _ClassBuilder:
     """
 
     __slots__ = (
+        "_add_method_dunders",
         "_attr_names",
         "_attrs",
         "_base_attr_map",
@@ -647,6 +640,8 @@ class _ClassBuilder:
         "_is_exc",
         "_on_setattr",
         "_pre_init_has_args",
+        "_repr_added",
+        "_script_snippets",
         "_slots",
         "_weakref_slot",
         "_wrote_own_setattr",
@@ -654,7 +649,7 @@ class _ClassBuilder:
 
     def __init__(
         self,
-        cls,
+        cls: type,
         these,
         slots,
         frozen,
@@ -744,15 +739,49 @@ class _ClassBuilder:
                 self._cls_dict["__setstate__"],
             ) = self._make_getstate_setstate()
 
+        # tuples of script, globs, hook
+        self._script_snippets: list[
+            tuple[str, dict, Callable[[dict, dict], Any]]
+        ] = []
+        self._repr_added = False
+
+        # We want to only do this check once; in 99.9% of cases these
+        # exist.
+        if not hasattr(self._cls, "__module__") or not hasattr(
+            self._cls, "__qualname__"
+        ):
+            self._add_method_dunders = self._add_method_dunders_safe
+        else:
+            self._add_method_dunders = self._add_method_dunders_unsafe
+
     def __repr__(self):
         return f"<_ClassBuilder(cls={self._cls.__name__})>"
 
+    def _eval_snippets(self) -> None:
+        """
+        Evaluate any registered snippets in one go.
+        """
+        script = "\n".join([snippet[0] for snippet in self._script_snippets])
+        globs = {}
+        for _, snippet_globs, _ in self._script_snippets:
+            globs.update(snippet_globs)
+
+        locs = _linecache_and_compile(
+            script,
+            _generate_unique_filename(self._cls, "methods"),
+            globs,
+        )
+
+        for _, _, hook in self._script_snippets:
+            hook(self._cls_dict, locs)
+
     def build_class(self):
         """
         Finalize class based on the accumulated configuration.
 
         Builder cannot be used after calling this method.
         """
+        self._eval_snippets()
         if self._slots is True:
             cls = self._create_slots_class()
         else:
@@ -859,9 +888,9 @@ class _ClassBuilder:
             names += ("__weakref__",)
 
         cached_properties = {
-            name: cached_property.func
-            for name, cached_property in cd.items()
-            if isinstance(cached_property, functools.cached_property)
+            name: cached_prop.func
+            for name, cached_prop in cd.items()
+            if isinstance(cached_prop, cached_property)
         }
 
         # Collect methods with a `__class__` reference that are shadowed in the new class.
@@ -946,14 +975,17 @@ class _ClassBuilder:
         return cls
 
     def add_repr(self, ns):
-        self._cls_dict["__repr__"] = self._add_method_dunders(
-            _make_repr(self._attrs, ns, self._cls)
-        )
+        script, globs = _make_repr_script(self._attrs, ns)
+
+        def _attach_repr(cls_dict, globs):
+            cls_dict["__repr__"] = self._add_method_dunders(globs["__repr__"])
+
+        self._script_snippets.append((script, globs, _attach_repr))
+        self._repr_added = True
         return self
 
     def add_str(self):
-        repr = self._cls_dict.get("__repr__")
-        if repr is None:
+        if not self._repr_added:
             msg = "__str__ can only be generated if a __repr__ exists."
             raise ValueError(msg)
 
@@ -1009,35 +1041,43 @@ class _ClassBuilder:
         return self
 
     def add_hash(self):
-        self._cls_dict["__hash__"] = self._add_method_dunders(
-            _make_hash(
-                self._cls,
-                self._attrs,
-                frozen=self._frozen,
-                cache_hash=self._cache_hash,
-            )
+        script, globs = _make_hash_script(
+            self._cls,
+            self._attrs,
+            frozen=self._frozen,
+            cache_hash=self._cache_hash,
         )
 
+        def attach_hash(cls_dict: dict, locs: dict) -> None:
+            cls_dict["__hash__"] = self._add_method_dunders(locs["__hash__"])
+
+        self._script_snippets.append((script, globs, attach_hash))
+
         return self
 
     def add_init(self):
-        self._cls_dict["__init__"] = self._add_method_dunders(
-            _make_init(
-                self._cls,
-                self._attrs,
-                self._has_pre_init,
-                self._pre_init_has_args,
-                self._has_post_init,
-                self._frozen,
-                self._slots,
-                self._cache_hash,
-                self._base_attr_map,
-                self._is_exc,
-                self._on_setattr,
-                attrs_init=False,
-            )
+        script, globs, annotations = _make_init_script(
+            self._cls,
+            self._attrs,
+            self._has_pre_init,
+            self._pre_init_has_args,
+            self._has_post_init,
+            self._frozen,
+            self._slots,
+            self._cache_hash,
+            self._base_attr_map,
+            self._is_exc,
+            self._on_setattr,
+            attrs_init=False,
         )
 
+        def _attach_init(cls_dict, globs):
+            init = globs["__init__"]
+            init.__annotations__ = annotations
+            cls_dict["__init__"] = self._add_method_dunders(init)
+
+        self._script_snippets.append((script, globs, _attach_init))
+
         return self
 
     def add_replace(self):
@@ -1054,32 +1094,41 @@ class _ClassBuilder:
         )
 
     def add_attrs_init(self):
-        self._cls_dict["__attrs_init__"] = self._add_method_dunders(
-            _make_init(
-                self._cls,
-                self._attrs,
-                self._has_pre_init,
-                self._pre_init_has_args,
-                self._has_post_init,
-                self._frozen,
-                self._slots,
-                self._cache_hash,
-                self._base_attr_map,
-                self._is_exc,
-                self._on_setattr,
-                attrs_init=True,
-            )
+        script, globs, annotations = _make_init_script(
+            self._cls,
+            self._attrs,
+            self._has_pre_init,
+            self._pre_init_has_args,
+            self._has_post_init,
+            self._frozen,
+            self._slots,
+            self._cache_hash,
+            self._base_attr_map,
+            self._is_exc,
+            self._on_setattr,
+            attrs_init=True,
         )
 
+        def _attach_attrs_init(cls_dict, globs):
+            init = globs["__attrs_init__"]
+            init.__annotations__ = annotations
+            cls_dict["__attrs_init__"] = self._add_method_dunders(init)
+
+        self._script_snippets.append((script, globs, _attach_attrs_init))
+
         return self
 
     def add_eq(self):
         cd = self._cls_dict
 
-        cd["__eq__"] = self._add_method_dunders(
-            _make_eq(self._cls, self._attrs)
-        )
-        cd["__ne__"] = self._add_method_dunders(_make_ne())
+        script, globs = _make_eq_script(self._attrs)
+
+        def _attach_eq(cls_dict, globs):
+            cls_dict["__eq__"] = self._add_method_dunders(globs["__eq__"])
+
+        self._script_snippets.append((script, globs, _attach_eq))
+
+        cd["__ne__"] = __ne__
 
         return self
 
@@ -1094,9 +1143,6 @@ class _ClassBuilder:
         return self
 
     def add_setattr(self):
-        if self._frozen:
-            return self
-
         sa_attrs = {}
         for a in self._attrs:
             on_setattr = a.on_setattr or self._on_setattr
@@ -1128,7 +1174,21 @@ class _ClassBuilder:
 
         return self
 
-    def _add_method_dunders(self, method):
+    def _add_method_dunders_unsafe(self, method: Callable) -> Callable:
+        """
+        Add __module__ and __qualname__ to a *method*.
+        """
+        method.__module__ = self._cls.__module__
+
+        method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}"
+
+        method.__doc__ = (
+            f"Method generated by attrs for class {self._cls.__qualname__}."
+        )
+
+        return method
+
+    def _add_method_dunders_safe(self, method: Callable) -> Callable:
         """
         Add __module__ and __qualname__ to a *method* if possible.
         """
@@ -1139,10 +1199,7 @@ class _ClassBuilder:
             method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}"
 
         with contextlib.suppress(AttributeError):
-            method.__doc__ = (
-                "Method generated by attrs for class "
-                f"{self._cls.__qualname__}."
-            )
+            method.__doc__ = f"Method generated by attrs for class {self._cls.__qualname__}."
 
         return method
 
@@ -1388,10 +1445,12 @@ def attrs(
             has_own_setattr,
             field_transformer,
         )
+
         if _determine_whether_to_implement(
             cls, repr, auto_detect, ("__repr__",)
         ):
             builder.add_repr(repr_ns)
+
         if str is True:
             builder.add_str()
 
@@ -1405,7 +1464,8 @@ def attrs(
         ):
             builder.add_order()
 
-        builder.add_setattr()
+        if not frozen:
+            builder.add_setattr()
 
         nonlocal hash
         if (
@@ -1483,7 +1543,7 @@ def _has_frozen_base_class(cls):
     return cls.__setattr__ is _frozen_setattrs
 
 
-def _generate_unique_filename(cls, func_name):
+def _generate_unique_filename(cls: type, func_name: str) -> str:
     """
     Create a "filename" suitable for a function being generated.
     """
@@ -1493,15 +1553,16 @@ def _generate_unique_filename(cls, func_name):
     )
 
 
-def _make_hash(cls, attrs, frozen, cache_hash):
+def _make_hash_script(
+    cls: type, attrs: list[Attribute], frozen: bool, cache_hash: bool
+) -> tuple[str, dict]:
     attrs = tuple(
         a for a in attrs if a.hash is True or (a.hash is None and a.eq is True)
     )
 
     tab = "        "
 
-    unique_filename = _generate_unique_filename(cls, "hash")
-    type_hash = hash(unique_filename)
+    type_hash = hash(_generate_unique_filename(cls, "hash"))
     # If eq is custom generated, we need to include the functions in globs
     globs = {}
 
@@ -1561,43 +1622,41 @@ def _make_hash(cls, attrs, frozen, cache_hash):
         append_hash_computation_lines("return ", tab)
 
     script = "\n".join(method_lines)
-    return _make_method("__hash__", script, unique_filename, globs)
+    return script, globs
 
 
-def _add_hash(cls, attrs):
+def _add_hash(cls: type, attrs: list[Attribute]):
     """
     Add a hash method to *cls*.
     """
-    cls.__hash__ = _make_hash(cls, attrs, frozen=False, cache_hash=False)
+    script, globs = _make_hash_script(
+        cls, attrs, frozen=False, cache_hash=False
+    )
+    _compile_and_eval(
+        script, globs, filename=_generate_unique_filename(cls, "__hash__")
+    )
+    cls.__hash__ = globs["__hash__"]
     return cls
 
 
-def _make_ne():
+def __ne__(self, other):
     """
-    Create __ne__ method.
+    Check equality and either forward a NotImplemented or
+    return the result negated.
     """
+    result = self.__eq__(other)
+    if result is NotImplemented:
+        return NotImplemented
 
-    def __ne__(self, other):
-        """
-        Check equality and either forward a NotImplemented or
-        return the result negated.
-        """
-        result = self.__eq__(other)
-        if result is NotImplemented:
-            return NotImplemented
-
-        return not result
-
-    return __ne__
+    return not result
 
 
-def _make_eq(cls, attrs):
+def _make_eq_script(attrs: list) -> tuple[str, dict]:
     """
     Create __eq__ method for *cls* with *attrs*.
     """
     attrs = [a for a in attrs if a.eq]
 
-    unique_filename = _generate_unique_filename(cls, "eq")
     lines = [
         "def __eq__(self, other):",
         "    if other.__class__ is not self.__class__:",
@@ -1626,7 +1685,7 @@ def _make_eq(cls, attrs):
 
     script = "\n".join(lines)
 
-    return _make_method("__eq__", script, unique_filename, globs)
+    return script, globs
 
 
 def _make_order(cls, attrs):
@@ -1692,14 +1751,20 @@ def _add_eq(cls, attrs=None):
     if attrs is None:
         attrs = cls.__attrs_attrs__
 
-    cls.__eq__ = _make_eq(cls, attrs)
-    cls.__ne__ = _make_ne()
+    script, globs = _make_eq_script(attrs)
+    _compile_and_eval(
+        script, globs, filename=_generate_unique_filename(cls, "__eq__")
+    )
+    cls.__eq__ = globs["__eq__"]
+    cls.__ne__ = __ne__
 
     return cls
 
 
-def _make_repr(attrs, ns, cls):
-    unique_filename = _generate_unique_filename(cls, "repr")
+def _make_repr_script(attrs, ns) -> tuple[str, dict]:
+    """
+    Create the source and globs for a __repr__ and return it.
+    """
     # Figure out which attributes to include, and which function to use to
     # format them. The a.repr value can be either bool or a custom
     # callable.
@@ -1750,9 +1815,7 @@ def _make_repr(attrs, ns, cls):
         "    already_repring.remove(id(self))",
     ]
 
-    return _make_method(
-        "__repr__", "\n".join(lines), unique_filename, globs=globs
-    )
+    return "\n".join(lines), globs
 
 
 def _add_repr(cls, ns=None, attrs=None):
@@ -1762,7 +1825,11 @@ def _add_repr(cls, ns=None, attrs=None):
     if attrs is None:
         attrs = cls.__attrs_attrs__
 
-    cls.__repr__ = _make_repr(attrs, ns, cls)
+    script, globs = _make_repr_script(attrs, ns)
+    _compile_and_eval(
+        script, globs, filename=_generate_unique_filename(cls, "__repr__")
+    )
+    cls.__repr__ = globs["__repr__"]
     return cls
 
 
@@ -1867,7 +1934,7 @@ def _is_slot_attr(a_name, base_attr_map):
     return cls and "__slots__" in cls.__dict__
 
 
-def _make_init(
+def _make_init_script(
     cls,
     attrs,
     pre_init,
@@ -1880,7 +1947,7 @@ def _make_init(
     is_exc,
     cls_on_setattr,
     attrs_init,
-):
+) -> tuple[str, dict, dict]:
     has_cls_on_setattr = (
         cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP
     )
@@ -1908,8 +1975,6 @@ def _make_init(
         elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP:
             needs_cached_setattr = True
 
-    unique_filename = _generate_unique_filename(cls, "init")
-
     script, globs, annotations = _attrs_to_init_script(
         filtered_attrs,
         frozen,
@@ -1935,15 +2000,7 @@ def _make_init(
         # setattr hooks.
         globs["_cached_setattr_get"] = _OBJ_SETATTR.__get__
 
-    init = _make_method(
-        "__attrs_init__" if attrs_init else "__init__",
-        script,
-        unique_filename,
-        globs,
-    )
-    init.__annotations__ = annotations
-
-    return init
+    return script, globs, annotations
 
 
 def _setattr(attr_name: str, value_var: str, has_on_setattr: bool) -> str:
@@ -2429,33 +2486,32 @@ class Attribute:
         raise FrozenInstanceError
 
     @classmethod
-    def from_counting_attr(cls, name, ca, type=None):
+    def from_counting_attr(cls, name: str, ca: _CountingAttr, type=None):
         # type holds the annotated value. deal with conflicts:
         if type is None:
             type = ca.type
         elif ca.type is not None:
             msg = "Type annotation and type argument cannot both be present"
             raise ValueError(msg)
-        inst_dict = {
-            k: getattr(ca, k)
-            for k in Attribute.__slots__
-            if k
-            not in (
-                "name",
-                "validator",
-                "default",
-                "type",
-                "inherited",
-            )  # exclude methods and deprecated alias
-        }
         return cls(
-            name=name,
-            validator=ca._validator,
-            default=ca._default,
-            type=type,
-            cmp=None,
-            inherited=False,
-            **inst_dict,
+            name,
+            ca._default,
+            ca._validator,
+            ca.repr,
+            None,
+            ca.hash,
+            ca.init,
+            False,
+            ca.metadata,
+            type,
+            ca.converter,
+            ca.kw_only,
+            ca.eq,
+            ca.eq_key,
+            ca.order,
+            ca.order_key,
+            ca.on_setattr,
+            ca.alias,
         )
 
     # Don't use attrs.evolve since fields(Attribute) doesn't work
@@ -3045,7 +3101,7 @@ def pipe(*converters):
 
     if not converters:
         # If the converter list is empty, pipe_converter is the identity.
-        A = typing.TypeVar("A")
+        A = TypeVar("A")
         pipe_converter.__annotations__.update({"val": A, "return": A})
     else:
         # Get parameter type from first converter.


Test patch:
diff --git a/tests/test_cmp.py b/tests/test_cmp.py
index b84b66f..8edfbd8 100644
--- a/tests/test_cmp.py
+++ b/tests/test_cmp.py
@@ -328,7 +328,7 @@ class TestDundersUnnamedClass:
         method = self.cls.__ne__
         assert method.__doc__.strip() == (
             "Check equality and either forward a NotImplemented or\n"
-            f"{indent}return the result negated."
+            f"{'' if PY_3_13_PLUS else ' ' * 4}return the result negated."
         )
         assert method.__name__ == "__ne__"
 
@@ -396,7 +396,7 @@ class TestDundersPartialOrdering:
         method = self.cls.__ne__
         assert method.__doc__.strip() == (
             "Check equality and either forward a NotImplemented or\n"
-            f"{indent}return the result negated."
+            f"{'' if PY_3_13_PLUS else ' ' * 4}return the result negated."
         )
         assert method.__name__ == "__ne__"
 
@@ -468,7 +468,7 @@ class TestDundersFullOrdering:
         method = self.cls.__ne__
         assert method.__doc__.strip() == (
             "Check equality and either forward a NotImplemented or\n"
-            f"{indent}return the result negated."
+            f"{'' if PY_3_13_PLUS else ' ' * 4}return the result negated."
         )
         assert method.__name__ == "__ne__"
 
diff --git a/tests/test_dunders.py b/tests/test_dunders.py
index cfd51c8..b98929c 100644
--- a/tests/test_dunders.py
+++ b/tests/test_dunders.py
@@ -19,7 +19,8 @@ from attr._make import (
     NOTHING,
     Factory,
     _add_repr,
-    _make_init,
+    _compile_and_eval,
+    _make_init_script,
     fields,
     make_class,
 )
@@ -85,7 +86,7 @@ def _add_init(cls, frozen):
     """
     has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False))
 
-    cls.__init__ = _make_init(
+    script, globs, annots = _make_init_script(
         cls,
         cls.__attrs_attrs__,
         has_pre_init,
@@ -103,6 +104,9 @@ def _add_init(cls, frozen):
         cls_on_setattr=None,
         attrs_init=False,
     )
+    _compile_and_eval(script, globs, filename="__init__")
+    cls.__init__ = globs["__init__"]
+    cls.__init__.__annotations__ = annots
     return cls
 
 
@@ -1002,37 +1006,37 @@ class TestFilenames:
         """
         assert (
             OriginalC.__init__.__code__.co_filename
-            == "<attrs generated init tests.test_dunders.C>"
+            == "<attrs generated methods tests.test_dunders.C>"
         )
         assert (
             OriginalC.__eq__.__code__.co_filename
-            == "<attrs generated eq tests.test_dunders.C>"
+            == "<attrs generated methods tests.test_dunders.C>"
         )
         assert (
             OriginalC.__hash__.__code__.co_filename
-            == "<attrs generated hash tests.test_dunders.C>"
+            == "<attrs generated methods tests.test_dunders.C>"
         )
         assert (
             CopyC.__init__.__code__.co_filename
-            == "<attrs generated init tests.test_dunders.C>"
+            == "<attrs generated methods tests.test_dunders.C>"
         )
         assert (
             CopyC.__eq__.__code__.co_filename
-            == "<attrs generated eq tests.test_dunders.C>"
+            == "<attrs generated methods tests.test_dunders.C>"
         )
         assert (
             CopyC.__hash__.__code__.co_filename
-            == "<attrs generated hash tests.test_dunders.C>"
+            == "<attrs generated methods tests.test_dunders.C>"
         )
         assert (
             C.__init__.__code__.co_filename
-            == "<attrs generated init tests.test_dunders.C-1>"
+            == "<attrs generated methods tests.test_dunders.C-1>"
         )
         assert (
             C.__eq__.__code__.co_filename
-            == "<attrs generated eq tests.test_dunders.C-1>"
+            == "<attrs generated methods tests.test_dunders.C-1>"
         )
         assert (
             C.__hash__.__code__.co_filename
-            == "<attrs generated hash tests.test_dunders.C-1>"
+            == "<attrs generated methods tests.test_dunders.C-1>"
         )
diff --git a/tests/test_make.py b/tests/test_make.py
index f5e3b31..80c0066 100644
--- a/tests/test_make.py
+++ b/tests/test_make.py
@@ -201,7 +201,7 @@ class TestTransformAttrs:
         class C:
             pass
 
-        assert _Attributes(((), [], {})) == _transform_attrs(
+        assert _Attributes((), [], {}) == _transform_attrs(
             C, None, False, False, True, None
         )
 
@@ -1778,7 +1778,6 @@ class TestClassBuilder:
             "__repr__",
             "__str__",
             "__eq__",
-            "__ne__",
             "__lt__",
             "__le__",
             "__gt__",
@@ -1810,7 +1809,9 @@ class TestClassBuilder:
         organic_prefix = C.organic.__qualname__.rsplit(".", 1)[0]
         assert organic_prefix + "." + meth_name == meth_C.__qualname__
 
-    def test_handles_missing_meta_on_class(self):
+    def test_handles_missing_meta_on_class(
+        self, monkeypatch: pytest.MonkeyPatch
+    ):
         """
         If the class hasn't a __module__ or __qualname__, the method hasn't
         either.
@@ -1819,6 +1820,19 @@ class TestClassBuilder:
         class C:
             pass
 
+        orig_hasattr = __builtins__["hasattr"]
+
+        def our_hasattr(obj, name, /) -> bool:
+            if name in ("__module__", "__qualname__"):
+                return False
+            return orig_hasattr(obj, name)
+
+        monkeypatch.setitem(
+            _ClassBuilder.__init__.__globals__["__builtins__"],
+            "hasattr",
+            our_hasattr,
+        )
+
         b = _ClassBuilder(
             C,
             these=None,
@@ -1835,7 +1849,6 @@ class TestClassBuilder:
             has_custom_setattr=False,
             field_transformer=None,
         )
-        b._cls = {}  # no __module__; no __qualname__
 
         def fake_meth(self):
             pass
@@ -1843,6 +1856,8 @@ class TestClassBuilder:
         fake_meth.__module__ = "42"
         fake_meth.__qualname__ = "23"
 
+        b._cls = {}  # No module and qualname
+
         rv = b._add_method_dunders(fake_meth)
 
         assert "42" == rv.__module__ == fake_meth.__module__
@@ -2225,7 +2240,6 @@ class TestDocs:
             "__init__",
             "__repr__",
             "__eq__",
-            "__ne__",
             "__lt__",
             "__le__",
             "__gt__",