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

This commit is contained in:
Jelle Zijlstra 2025-05-25 08:40:58 -07:00 committed by GitHub
parent 4443110c34
commit 3e562b3942
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 84 additions and 4 deletions

View file

@ -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")

View file

@ -7,7 +7,7 @@ import collections
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 @@ class TestGetAnnotations(unittest.TestCase):
{"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

View file

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