https://github.com/python/cpython/commit/45614ecb2bdc2b984f051c7eade39458a3f8709f
commit: 45614ecb2bdc2b984f051c7eade39458a3f8709f
branch: main
author: Jelle Zijlstra <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2024-07-27T16:36:06Z
summary:

gh-119180: Use type descriptors to access annotations (PEP 749) (#122074)

files:
A Misc/NEWS.d/next/Library/2024-07-23-17-13-10.gh-issue-119180.5PZELo.rst
M Lib/annotationlib.py
M Lib/test/test_annotationlib.py

diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index b4036ffb189c2d..eea24232f9f0d0 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -524,6 +524,27 @@ def call_annotate_function(annotate, format, owner=None):
         raise ValueError(f"Invalid format: {format!r}")
 
 
+# We use the descriptors from builtins.type instead of accessing
+# .__annotations__ and .__annotate__ directly on class objects, because
+# otherwise we could get wrong results in some cases involving metaclasses.
+# See PEP 749.
+_BASE_GET_ANNOTATE = type.__dict__["__annotate__"].__get__
+_BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__
+
+
+def get_annotate_function(obj):
+    """Get the __annotate__ function for an object.
+
+    obj may be a function, class, or module, or a user-defined type with
+    an `__annotate__` attribute.
+
+    Returns the __annotate__ function or None.
+    """
+    if isinstance(obj, type):
+        return _BASE_GET_ANNOTATE(obj)
+    return getattr(obj, "__annotate__", None)
+
+
 def get_annotations(
     obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE
 ):
@@ -576,16 +597,23 @@ def get_annotations(
 
     # For VALUE format, we look at __annotations__ directly.
     if format != Format.VALUE:
-        annotate = getattr(obj, "__annotate__", None)
+        annotate = get_annotate_function(obj)
         if annotate is not None:
             ann = call_annotate_function(annotate, format, owner=obj)
             if not isinstance(ann, dict):
                 raise ValueError(f"{obj!r}.__annotate__ returned a non-dict")
             return dict(ann)
 
-    ann = getattr(obj, "__annotations__", None)
-    if ann is None:
-        return {}
+    if isinstance(obj, type):
+        try:
+            ann = _BASE_GET_ANNOTATIONS(obj)
+        except AttributeError:
+            # For static types, the descriptor raises AttributeError.
+            return {}
+    else:
+        ann = getattr(obj, "__annotations__", None)
+        if ann is None:
+            return {}
 
     if not isinstance(ann, dict):
         raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index e68d63c91d1a73..e459d27d3c4b38 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -2,8 +2,10 @@
 
 import annotationlib
 import functools
+import itertools
 import pickle
 import unittest
+from annotationlib import Format, get_annotations, get_annotate_function
 from typing import Unpack
 
 from test.test_inspect import inspect_stock_annotations
@@ -767,5 +769,85 @@ def 
test_pep_695_generics_with_future_annotations_nested_in_function(self):
 
         self.assertEqual(
             set(results.generic_func_annotations.values()),
-            set(results.generic_func.__type_params__)
+            set(results.generic_func.__type_params__),
         )
+
+
+class MetaclassTests(unittest.TestCase):
+    def test_annotated_meta(self):
+        class Meta(type):
+            a: int
+
+        class X(metaclass=Meta):
+            pass
+
+        class Y(metaclass=Meta):
+            b: float
+
+        self.assertEqual(get_annotations(Meta), {"a": int})
+        self.assertEqual(get_annotate_function(Meta)(Format.VALUE), {"a": int})
+
+        self.assertEqual(get_annotations(X), {})
+        self.assertIs(get_annotate_function(X), None)
+
+        self.assertEqual(get_annotations(Y), {"b": float})
+        self.assertEqual(get_annotate_function(Y)(Format.VALUE), {"b": float})
+
+    def test_unannotated_meta(self):
+        class Meta(type): pass
+
+        class X(metaclass=Meta):
+            a: str
+
+        class Y(X): pass
+
+        self.assertEqual(get_annotations(Meta), {})
+        self.assertIs(get_annotate_function(Meta), None)
+
+        self.assertEqual(get_annotations(Y), {})
+        self.assertIs(get_annotate_function(Y), None)
+
+        self.assertEqual(get_annotations(X), {"a": str})
+        self.assertEqual(get_annotate_function(X)(Format.VALUE), {"a": str})
+
+    def test_ordering(self):
+        # Based on a sample by David Ellis
+        # https://discuss.python.org/t/pep-749-implementing-pep-649/54974/38
+
+        def make_classes():
+            class Meta(type):
+                a: int
+                expected_annotations = {"a": int}
+
+            class A(type, metaclass=Meta):
+                b: float
+                expected_annotations = {"b": float}
+
+            class B(metaclass=A):
+                c: str
+                expected_annotations = {"c": str}
+
+            class C(B):
+                expected_annotations = {}
+
+            class D(metaclass=Meta):
+                expected_annotations = {}
+
+            return Meta, A, B, C, D
+
+        classes = make_classes()
+        class_count = len(classes)
+        for order in itertools.permutations(range(class_count), class_count):
+            names = ", ".join(classes[i].__name__ for i in order)
+            with self.subTest(names=names):
+                classes = make_classes()  # Regenerate classes
+                for i in order:
+                    get_annotations(classes[i])
+                for c in classes:
+                    with self.subTest(c=c):
+                        self.assertEqual(get_annotations(c), 
c.expected_annotations)
+                        annotate_func = get_annotate_function(c)
+                        if c.expected_annotations:
+                            self.assertEqual(annotate_func(Format.VALUE), 
c.expected_annotations)
+                        else:
+                            self.assertIs(annotate_func, None)
diff --git 
a/Misc/NEWS.d/next/Library/2024-07-23-17-13-10.gh-issue-119180.5PZELo.rst 
b/Misc/NEWS.d/next/Library/2024-07-23-17-13-10.gh-issue-119180.5PZELo.rst
new file mode 100644
index 00000000000000..d65e89f7523b0a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-07-23-17-13-10.gh-issue-119180.5PZELo.rst
@@ -0,0 +1,2 @@
+Fix handling of classes with custom metaclasses in
+``annotationlib.get_annotations``.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to