Repository: python-attrs/attrs

Issue:
make_class(): Populate the __annotations__ dict
`make_class()` currently doesn't touch the `__annotations__` dict when it creates a new class.

This can become a problem when a class is created dynamically with unresolved types.  And when `__annotations__` is empty, `resolve_type()` on the generated class will do nothing.

Here is a use case: https://gitlab.com/sscherfke/typed-settings/-/issues/54

There's not much required to fix this:

```diff
diff --git a/src/attr/_make.py b/src/attr/_make.py
index 0114582..bbdfc15 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -3058,7 +3058,12 @@ def make_class(
         True,
     )
 
-    return _attrs(these=cls_dict, **attributes_arguments)(type_)
+    cls = _attrs(these=cls_dict, **attributes_arguments)(type_)
+    # Only add type annotations now or "_attrs()" will complain:
+    cls.__annotations__ = {
+        k: v.type for k, v in cls_dict.items() if v.type is not None
+    }
+    return cls
 
 
 # These are required by within this module so we define them here and merely
diff --git a/tests/test_make.py b/tests/test_make.py
index 736ab8c..e89ee87 100644
--- a/tests/test_make.py
+++ b/tests/test_make.py
@@ -1164,6 +1164,32 @@ class TestMakeClass:
 
         attr.make_class("test", {"id": attr.ib(type=str)}, (MyParent[int],))
 
+    def test_annotations(self):
+        """
+        make_class fills the __annotations__ dict for attributes with a known
+        type.
+        """
+        a = attr.ib(type=bool)
+        b = attr.ib(type=None)  # Won't be added to ann. b/c of unfavorable default
+        c = attr.ib()
+
+        C = attr.make_class("C", {"a": a, "b": b, "c": c})
+        C = attr.resolve_types(C)
+
+        assert C.__annotations__ == {"a": bool}
+
+    def test_annotations_resolve(self):
+        """
+        resolve_types() resolves the annotations added by make_class().
+        """
+        a = attr.ib(type="bool")
+
+        C = attr.make_class("C", {"a": a})
+        C = attr.resolve_types(C)
+
+        assert attr.fields(C).a.type is bool
+        assert C.__annotations__ == {"a": "bool"}
+
 
 class TestFields:
     """
```

The only problem with this is the unfortunate default of `field(type=None)`, b/c we cannot differentiate between an explicit `None` type an no type.  I’m afraid though, that changing the default of `type=` to a sentinel object would be a braking change.


Discussion:
Solved it in Typed Settings, so fixing this is not super important.  The `__annotations__` dict being available for dynamic classes would be nice nonetheless, though.
Any reason you dumped a diff instead of a PR? 😅
It's just a draft for helping you decide whether you like this change or not.  If you like it, I will prepare a PR. :-)



Patch:
diff --git a/changelog.d/1285.change.md b/changelog.d/1285.change.md
new file mode 100644
index 000000000..5f5a2997a
--- /dev/null
+++ b/changelog.d/1285.change.md
@@ -0,0 +1,1 @@
+`attrs.make_class()` now populates the `__annotations__` dict of the generated class, so that `attrs.resolve_types()` can resolve them.
diff --git a/src/attr/_make.py b/src/attr/_make.py
index d1c08dea4..097534fbb 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -3056,7 +3056,12 @@ def make_class(
         True,
     )
 
-    return _attrs(these=cls_dict, **attributes_arguments)(type_)
+    cls = _attrs(these=cls_dict, **attributes_arguments)(type_)
+    # Only add type annotations now or "_attrs()" will complain:
+    cls.__annotations__ = {
+        k: v.type for k, v in cls_dict.items() if v.type is not None
+    }
+    return cls
 
 
 # These are required by within this module so we define them here and merely


Test patch:
diff --git a/tests/test_make.py b/tests/test_make.py
index a5490f9e5..364dee170 100644
--- a/tests/test_make.py
+++ b/tests/test_make.py
@@ -1164,6 +1164,34 @@ def test_generic_dynamic_class(self):
 
         attr.make_class("test", {"id": attr.ib(type=str)}, (MyParent[int],))
 
+    def test_annotations(self):
+        """
+        make_class fills the __annotations__ dict for attributes with a known
+        type.
+        """
+        a = attr.ib(type=bool)
+        b = attr.ib(
+            type=None
+        )  # Won't be added to ann. b/c of unfavorable default
+        c = attr.ib()
+
+        C = attr.make_class("C", {"a": a, "b": b, "c": c})
+        C = attr.resolve_types(C)
+
+        assert {"a": bool} == C.__annotations__
+
+    def test_annotations_resolve(self):
+        """
+        resolve_types() resolves the annotations added by make_class().
+        """
+        a = attr.ib(type="bool")
+
+        C = attr.make_class("C", {"a": a})
+        C = attr.resolve_types(C)
+
+        assert attr.fields(C).a.type is bool
+        assert {"a": "bool"} == C.__annotations__
+
 
 class TestFields:
     """