Skip to content

make_class(): Populate the __annotations__ dict #1271

@sscherfke

Description

@sscherfke

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 --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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions