python-attrs / attrs

Python Classes Without Boilerplate

Home Page:https://www.attrs.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

make_class(): Populate the __annotations__ dict

sscherfke opened this issue · comments

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.

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