mirror of
https://github.com/python/cpython.git
synced 2025-08-02 16:13:13 +00:00
[3.14] gh-133684: Fix get_annotations() where PEP 563 is involved (GH-133795) (#134656)
gh-133684: Fix get_annotations() where PEP 563 is involved (GH-133795)
(cherry picked from commit 3e562b3942
)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
This commit is contained in:
parent
d82d445b18
commit
cbf4ccf1d0
3 changed files with 84 additions and 4 deletions
|
@ -1042,14 +1042,27 @@ def _get_and_call_annotate(obj, format):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__
|
||||||
|
|
||||||
|
|
||||||
def _get_dunder_annotations(obj):
|
def _get_dunder_annotations(obj):
|
||||||
"""Return the annotations for an object, checking that it is a dictionary.
|
"""Return the annotations for an object, checking that it is a dictionary.
|
||||||
|
|
||||||
Does not return a fresh dictionary.
|
Does not return a fresh dictionary.
|
||||||
"""
|
"""
|
||||||
ann = getattr(obj, "__annotations__", None)
|
# This special case is needed to support types defined under
|
||||||
if ann is None:
|
# from __future__ import annotations, where accessing the __annotations__
|
||||||
return None
|
# 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):
|
if not isinstance(ann, dict):
|
||||||
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")
|
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")
|
||||||
|
|
|
@ -7,7 +7,7 @@ import collections
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
import pickle
|
import pickle
|
||||||
from string.templatelib import Interpolation, Template
|
from string.templatelib import Template
|
||||||
import typing
|
import typing
|
||||||
import unittest
|
import unittest
|
||||||
from annotationlib import (
|
from annotationlib import (
|
||||||
|
@ -815,6 +815,70 @@ class TestGetAnnotations(unittest.TestCase):
|
||||||
{"x": int},
|
{"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 test_modify_annotations(self):
|
||||||
def f(x: int):
|
def f(x: int):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -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.
|
Loading…
Add table
Add a link
Reference in a new issue