https://github.com/python/cpython/commit/3e562b394252ff75d9809b7940020a775e4df68b
commit: 3e562b394252ff75d9809b7940020a775e4df68b
branch: main
author: Jelle Zijlstra <jelle.zijls...@gmail.com>
committer: JelleZijlstra <jelle.zijls...@gmail.com>
date: 2025-05-25T08:40:58-07:00
summary:

gh-133684: Fix get_annotations() where PEP 563 is involved (#133795)

files:
A Misc/NEWS.d/next/Library/2025-05-09-18-29-25.gh-issue-133684.Y1DFSt.rst
M Lib/annotationlib.py
M Lib/test/test_annotationlib.py

diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index 32b8553458930c..a7dfb91515a1c4 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -1042,14 +1042,27 @@ def _get_and_call_annotate(obj, format):
     return None
 
 
+_BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__
+
+
 def _get_dunder_annotations(obj):
     """Return the annotations for an object, checking that it is a dictionary.
 
     Does not return a fresh dictionary.
     """
-    ann = getattr(obj, "__annotations__", None)
-    if ann is None:
-        return None
+    # This special case is needed to support types defined under
+    # from __future__ import annotations, where accessing the __annotations__
+    # attribute directly might return annotations for the wrong class.
+    if isinstance(obj, type):
+        try:
+            ann = _BASE_GET_ANNOTATIONS(obj)
+        except AttributeError:
+            # For static types, the descriptor raises AttributeError.
+            return None
+    else:
+        ann = getattr(obj, "__annotations__", None)
+        if ann is None:
+            return None
 
     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 73a821d15e3481..fe091e52a86dc4 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -7,7 +7,7 @@
 import functools
 import itertools
 import pickle
-from string.templatelib import Interpolation, Template
+from string.templatelib import Template
 import typing
 import unittest
 from annotationlib import (
@@ -815,6 +815,70 @@ def test_stringized_annotations_on_class(self):
             {"x": int},
         )
 
+    def test_stringized_annotation_permutations(self):
+        def define_class(name, has_future, has_annos, base_text, 
extra_names=None):
+            lines = []
+            if has_future:
+                lines.append("from __future__ import annotations")
+            lines.append(f"class {name}({base_text}):")
+            if has_annos:
+                lines.append(f"    {name}_attr: int")
+            else:
+                lines.append("    pass")
+            code = "\n".join(lines)
+            ns = support.run_code(code, extra_names=extra_names)
+            return ns[name]
+
+        def check_annotations(cls, has_future, has_annos):
+            if has_annos:
+                if has_future:
+                    anno = "int"
+                else:
+                    anno = int
+                self.assertEqual(get_annotations(cls), 
{f"{cls.__name__}_attr": anno})
+            else:
+                self.assertEqual(get_annotations(cls), {})
+
+        for meta_future, base_future, child_future, meta_has_annos, 
base_has_annos, child_has_annos in itertools.product(
+            (False, True),
+            (False, True),
+            (False, True),
+            (False, True),
+            (False, True),
+            (False, True),
+        ):
+            with self.subTest(
+                meta_future=meta_future,
+                base_future=base_future,
+                child_future=child_future,
+                meta_has_annos=meta_has_annos,
+                base_has_annos=base_has_annos,
+                child_has_annos=child_has_annos,
+            ):
+                meta = define_class(
+                    "Meta",
+                    has_future=meta_future,
+                    has_annos=meta_has_annos,
+                    base_text="type",
+                )
+                base = define_class(
+                    "Base",
+                    has_future=base_future,
+                    has_annos=base_has_annos,
+                    base_text="metaclass=Meta",
+                    extra_names={"Meta": meta},
+                )
+                child = define_class(
+                    "Child",
+                    has_future=child_future,
+                    has_annos=child_has_annos,
+                    base_text="Base",
+                    extra_names={"Base": base},
+                )
+                check_annotations(meta, meta_future, meta_has_annos)
+                check_annotations(base, base_future, base_has_annos)
+                check_annotations(child, child_future, child_has_annos)
+
     def test_modify_annotations(self):
         def f(x: int):
             pass
diff --git 
a/Misc/NEWS.d/next/Library/2025-05-09-18-29-25.gh-issue-133684.Y1DFSt.rst 
b/Misc/NEWS.d/next/Library/2025-05-09-18-29-25.gh-issue-133684.Y1DFSt.rst
new file mode 100644
index 00000000000000..0cb1bc237a12c9
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-05-09-18-29-25.gh-issue-133684.Y1DFSt.rst
@@ -0,0 +1,3 @@
+Fix bug where :func:`annotationlib.get_annotations` would return the wrong
+result for certain classes that are part of a class hierarchy where ``from
+__future__ import annotations`` is used.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: arch...@mail-archive.com

Reply via email to