Skip to content

Commit 1563a8d

Browse files
committed
gh-133684: Fix get_annotations() where PEP 563 is involved
1 parent 1978904 commit 1563a8d

File tree

4 files changed

+81
-5
lines changed

4 files changed

+81
-5
lines changed

Lib/annotationlib.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,14 +1042,24 @@ def _get_and_call_annotate(obj, format):
10421042
return None
10431043

10441044

1045+
_BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__
1046+
1047+
10451048
def _get_dunder_annotations(obj):
10461049
"""Return the annotations for an object, checking that it is a dictionary.
10471050
10481051
Does not return a fresh dictionary.
10491052
"""
1050-
ann = getattr(obj, "__annotations__", None)
1051-
if ann is None:
1052-
return None
1053+
if isinstance(obj, type):
1054+
try:
1055+
ann = _BASE_GET_ANNOTATIONS(obj)
1056+
except AttributeError:
1057+
# For static types, the descriptor raises AttributeError.
1058+
return None
1059+
else:
1060+
ann = getattr(obj, "__annotations__", None)
1061+
if ann is None:
1062+
return None
10531063

10541064
if not isinstance(ann, dict):
10551065
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")

Lib/test/support/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -696,9 +696,11 @@ def sortdict(dict):
696696
return "{%s}" % withcommas
697697

698698

699-
def run_code(code: str) -> dict[str, object]:
699+
def run_code(code: str, extra_names: dict[str, object] | None = None) -> dict[str, object]:
700700
"""Run a piece of code after dedenting it, and return its global namespace."""
701701
ns = {}
702+
if extra_names:
703+
ns.update(extra_names)
702704
exec(textwrap.dedent(code), ns)
703705
return ns
704706

Lib/test/test_annotationlib.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import functools
88
import itertools
99
import pickle
10-
from string.templatelib import Interpolation, Template
10+
from string.templatelib import Template
1111
import typing
1212
import unittest
1313
from annotationlib import (
@@ -815,6 +815,67 @@ def test_stringized_annotations_on_class(self):
815815
{"x": int},
816816
)
817817

818+
def test_stringized_annotation_permutations(self):
819+
def define_class(name, has_future, has_annos, base_text, extra_names=None):
820+
lines = []
821+
if has_future:
822+
lines.append("from __future__ import annotations")
823+
lines.append(f"class {name}({base_text}):")
824+
if has_annos:
825+
lines.append(f" {name}_attr: int")
826+
else:
827+
lines.append(" pass")
828+
code = "\n".join(lines)
829+
ns = support.run_code(code, extra_names=extra_names)
830+
return ns[name]
831+
832+
def check_annotations(cls, name, has_future, has_annos):
833+
if has_annos:
834+
if has_future:
835+
anno = "int"
836+
else:
837+
anno = int
838+
self.assertEqual(get_annotations(cls), {f"{name}_attr": anno})
839+
else:
840+
self.assertEqual(get_annotations(cls), {})
841+
842+
for meta_future, base_future, child_future, meta_has_annos, base_has_annos, child_has_annos in itertools.product(
843+
(False, True),
844+
(False, True),
845+
(False, True),
846+
(False, True),
847+
(False, True),
848+
(False, True),
849+
):
850+
with self.subTest(
851+
meta_future=meta_future,
852+
base_future=base_future,
853+
child_future=child_future,
854+
):
855+
meta = define_class(
856+
"Meta",
857+
has_future=meta_future,
858+
has_annos=meta_has_annos,
859+
base_text="type",
860+
)
861+
base = define_class(
862+
"Base",
863+
has_future=base_future,
864+
has_annos=base_has_annos,
865+
base_text="metaclass=Meta",
866+
extra_names={"Meta": meta},
867+
)
868+
child = define_class(
869+
"Child",
870+
has_future=child_future,
871+
has_annos=child_has_annos,
872+
base_text="Base",
873+
extra_names={"Base": base},
874+
)
875+
check_annotations(meta, "Meta", meta_future, meta_has_annos)
876+
check_annotations(base, "Base", base_future, base_has_annos)
877+
check_annotations(child, "Child", child_future, child_has_annos)
878+
818879
def test_modify_annotations(self):
819880
def f(x: int):
820881
pass
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix bug where :func:`annotationlib.get_annotations` would return the wrong
2+
result for certain classes that are part of a class hierarchy where ``from
3+
__future__ import annotations`` is used.

0 commit comments

Comments
 (0)