https://github.com/python/cpython/commit/3e562b394252ff75d9809b7940020a775e4df68b
commit: 3e562b394252ff75d9809b7940020a775e4df68b
branch: main
author: Jelle Zijlstra <[email protected]>
committer: JelleZijlstra <[email protected]>
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 -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]