From 0f3cf768e7027520677a2237a18367260e408911 Mon Sep 17 00:00:00 2001
From: Stefan Scherfke <stefan@sofa-rockers.org>
Date: Wed, 25 Dec 2024 16:12:24 +0100
Subject: [PATCH] Add "resolve_types" argument to define()

Fixes: #1286
---
 src/attr/__init__.pyi  |  2 ++
 src/attr/_make.py      | 13 +++++++++++++
 src/attr/_next_gen.py  | 12 ++++++++++++
 src/attrs/__init__.pyi |  2 ++
 tests/test_next_gen.py | 21 +++++++++++++++++++++
 5 files changed, 50 insertions(+)

diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi
index 133e50105..e6181c640 100644
--- a/src/attr/__init__.pyi
+++ b/src/attr/__init__.pyi
@@ -279,6 +279,7 @@ def attrs(
     field_transformer: _FieldTransformer | None = ...,
     match_args: bool = ...,
     unsafe_hash: bool | None = ...,
+    resolve_types: bool = ...,
 ) -> _C: ...
 @overload
 @dataclass_transform(order_default=True, field_specifiers=(attrib, field))
@@ -307,6 +308,7 @@ def attrs(
     field_transformer: _FieldTransformer | None = ...,
     match_args: bool = ...,
     unsafe_hash: bool | None = ...,
+    resolve_types: bool = ...,
 ) -> Callable[[_C], _C]: ...
 def fields(cls: type[AttrsInstance]) -> Any: ...
 def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ...
diff --git a/src/attr/_make.py b/src/attr/_make.py
index f00fec48c..0b3ec90d3 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -645,6 +645,7 @@ class _ClassBuilder:
         "_is_exc",
         "_on_setattr",
         "_pre_init_has_args",
+        "_resolve_types",
         "_slots",
         "_weakref_slot",
         "_wrote_own_setattr",
@@ -666,6 +667,7 @@ def __init__(
         on_setattr,
         has_custom_setattr,
         field_transformer,
+        resolve_types,
     ):
         attrs, base_attrs, base_map = _transform_attrs(
             cls,
@@ -683,6 +685,7 @@ def __init__(
         self._base_attr_map = base_map
         self._attr_names = tuple(a.name for a in attrs)
         self._slots = slots
+        self._resolve_types = resolve_types
         self._frozen = frozen
         self._weakref_slot = weakref_slot
         self._cache_hash = cache_hash
@@ -766,6 +769,12 @@ def build_class(self):
         ):
             cls.__attrs_init_subclass__()
 
+        if self._resolve_types:
+            # Need to import here to avoid circular imports
+            from . import _funcs
+
+            cls = _funcs.resolve_types(cls)
+
         return cls
 
     def _patch_original_class(self):
@@ -1267,6 +1276,7 @@ def attrs(
     field_transformer=None,
     match_args=True,
     unsafe_hash=None,
+    resolve_types=False,
 ):
     r"""
     A class decorator that adds :term:`dunder methods` according to the
@@ -1333,6 +1343,8 @@ def attrs(
        If a class has an *inherited* classmethod called
        ``__attrs_init_subclass__``, it is executed after the class is created.
     .. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*.
+    .. versionadded:: 25.1.0
+       Added the *resolve_types* argument.
     """
     if repr_ns is not None:
         import warnings
@@ -1385,6 +1397,7 @@ def wrap(cls):
             on_setattr,
             has_own_setattr,
             field_transformer,
+            resolve_types,
         )
         if _determine_whether_to_implement(
             cls, repr, auto_detect, ("__repr__",)
diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py
index 9290664b2..ca182223e 100644
--- a/src/attr/_next_gen.py
+++ b/src/attr/_next_gen.py
@@ -43,6 +43,7 @@ def define(
     on_setattr=None,
     field_transformer=None,
     match_args=True,
+    resolve_types=False,
 ):
     r"""
     A class decorator that adds :term:`dunder methods` according to
@@ -235,6 +236,14 @@ def define(
             non-keyword-only ``__init__`` parameter names on Python 3.10 and
             later. Ignored on older Python versions.
 
+        resolve_types (bool):
+            If True, automatically call :func:`~attrs.resolve_types()` on the
+            class.
+
+            If you need to explicitly pass a global or local namespace, you
+            should leave this at False and explicitly call
+            :func:`~attrs.resolve_types()` instead.
+
         collect_by_mro (bool):
             If True, *attrs* collects attributes from base classes correctly
             according to the `method resolution order
@@ -319,6 +328,8 @@ def define(
     .. versionadded:: 24.3.0
        Unless already present, a ``__replace__`` method is automatically
        created for `copy.replace` (Python 3.13+ only).
+    .. versionadded:: 25.1.0
+       Added the *resolve_types* argument.
 
     .. note::
 
@@ -366,6 +377,7 @@ def do_it(cls, auto_attribs):
             on_setattr=on_setattr,
             field_transformer=field_transformer,
             match_args=match_args,
+            resolve_types=resolve_types,
         )
 
     def wrap(cls):
diff --git a/src/attrs/__init__.pyi b/src/attrs/__init__.pyi
index 648fa7a34..03c0e115b 100644
--- a/src/attrs/__init__.pyi
+++ b/src/attrs/__init__.pyi
@@ -179,6 +179,7 @@ def define(
     on_setattr: _OnSetAttrArgType | None = ...,
     field_transformer: _FieldTransformer | None = ...,
     match_args: bool = ...,
+    resolve_types: bool = ...,
 ) -> _C: ...
 @overload
 @dataclass_transform(field_specifiers=(attrib, field))
@@ -205,6 +206,7 @@ def define(
     on_setattr: _OnSetAttrArgType | None = ...,
     field_transformer: _FieldTransformer | None = ...,
     match_args: bool = ...,
+    resolve_types: bool = ...,
 ) -> Callable[[_C], _C]: ...
 
 mutable = define
diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py
index 41e534df0..e7afdd921 100644
--- a/tests/test_next_gen.py
+++ b/tests/test_next_gen.py
@@ -425,6 +425,27 @@ class D(B, C):
 
         assert d.x == d.xx()
 
+    def test_resolve_types(self):
+        """
+        Types can optionally be resolve directly by the decorator.
+        """
+        @attrs.define(resolve_types=True)
+        class A:
+            x: "int" = 10
+
+        assert attrs.fields(A).x.type is int
+
+    def test_resolve_types_default_off(self):
+        """
+        Types are not resolved by default.
+        """
+
+        @attrs.define(resolve_types=False)
+        class A:
+            x: "int" = 10
+
+        assert attrs.fields(A).x.type == "int"
+
 
 class TestAsTuple:
     def test_smoke(self):