Skip to content

Commit 45614ec

Browse files
gh-119180: Use type descriptors to access annotations (PEP 749) (#122074)
1 parent 4e75509 commit 45614ec

File tree

3 files changed

+117
-5
lines changed

3 files changed

+117
-5
lines changed

Lib/annotationlib.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,27 @@ def call_annotate_function(annotate, format, owner=None):
524524
raise ValueError(f"Invalid format: {format!r}")
525525

526526

527+
# We use the descriptors from builtins.type instead of accessing
528+
# .__annotations__ and .__annotate__ directly on class objects, because
529+
# otherwise we could get wrong results in some cases involving metaclasses.
530+
# See PEP 749.
531+
_BASE_GET_ANNOTATE = type.__dict__["__annotate__"].__get__
532+
_BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__
533+
534+
535+
def get_annotate_function(obj):
536+
"""Get the __annotate__ function for an object.
537+
538+
obj may be a function, class, or module, or a user-defined type with
539+
an `__annotate__` attribute.
540+
541+
Returns the __annotate__ function or None.
542+
"""
543+
if isinstance(obj, type):
544+
return _BASE_GET_ANNOTATE(obj)
545+
return getattr(obj, "__annotate__", None)
546+
547+
527548
def get_annotations(
528549
obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE
529550
):
@@ -576,16 +597,23 @@ def get_annotations(
576597

577598
# For VALUE format, we look at __annotations__ directly.
578599
if format != Format.VALUE:
579-
annotate = getattr(obj, "__annotate__", None)
600+
annotate = get_annotate_function(obj)
580601
if annotate is not None:
581602
ann = call_annotate_function(annotate, format, owner=obj)
582603
if not isinstance(ann, dict):
583604
raise ValueError(f"{obj!r}.__annotate__ returned a non-dict")
584605
return dict(ann)
585606

586-
ann = getattr(obj, "__annotations__", None)
587-
if ann is None:
588-
return {}
607+
if isinstance(obj, type):
608+
try:
609+
ann = _BASE_GET_ANNOTATIONS(obj)
610+
except AttributeError:
611+
# For static types, the descriptor raises AttributeError.
612+
return {}
613+
else:
614+
ann = getattr(obj, "__annotations__", None)
615+
if ann is None:
616+
return {}
589617

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

Lib/test/test_annotationlib.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import annotationlib
44
import functools
5+
import itertools
56
import pickle
67
import unittest
8+
from annotationlib import Format, get_annotations, get_annotate_function
79
from typing import Unpack
810

911
from test.test_inspect import inspect_stock_annotations
@@ -767,5 +769,85 @@ def test_pep_695_generics_with_future_annotations_nested_in_function(self):
767769

768770
self.assertEqual(
769771
set(results.generic_func_annotations.values()),
770-
set(results.generic_func.__type_params__)
772+
set(results.generic_func.__type_params__),
771773
)
774+
775+
776+
class MetaclassTests(unittest.TestCase):
777+
def test_annotated_meta(self):
778+
class Meta(type):
779+
a: int
780+
781+
class X(metaclass=Meta):
782+
pass
783+
784+
class Y(metaclass=Meta):
785+
b: float
786+
787+
self.assertEqual(get_annotations(Meta), {"a": int})
788+
self.assertEqual(get_annotate_function(Meta)(Format.VALUE), {"a": int})
789+
790+
self.assertEqual(get_annotations(X), {})
791+
self.assertIs(get_annotate_function(X), None)
792+
793+
self.assertEqual(get_annotations(Y), {"b": float})
794+
self.assertEqual(get_annotate_function(Y)(Format.VALUE), {"b": float})
795+
796+
def test_unannotated_meta(self):
797+
class Meta(type): pass
798+
799+
class X(metaclass=Meta):
800+
a: str
801+
802+
class Y(X): pass
803+
804+
self.assertEqual(get_annotations(Meta), {})
805+
self.assertIs(get_annotate_function(Meta), None)
806+
807+
self.assertEqual(get_annotations(Y), {})
808+
self.assertIs(get_annotate_function(Y), None)
809+
810+
self.assertEqual(get_annotations(X), {"a": str})
811+
self.assertEqual(get_annotate_function(X)(Format.VALUE), {"a": str})
812+
813+
def test_ordering(self):
814+
# Based on a sample by David Ellis
815+
# https://discuss.python.org/t/pep-749-implementing-pep-649/54974/38
816+
817+
def make_classes():
818+
class Meta(type):
819+
a: int
820+
expected_annotations = {"a": int}
821+
822+
class A(type, metaclass=Meta):
823+
b: float
824+
expected_annotations = {"b": float}
825+
826+
class B(metaclass=A):
827+
c: str
828+
expected_annotations = {"c": str}
829+
830+
class C(B):
831+
expected_annotations = {}
832+
833+
class D(metaclass=Meta):
834+
expected_annotations = {}
835+
836+
return Meta, A, B, C, D
837+
838+
classes = make_classes()
839+
class_count = len(classes)
840+
for order in itertools.permutations(range(class_count), class_count):
841+
names = ", ".join(classes[i].__name__ for i in order)
842+
with self.subTest(names=names):
843+
classes = make_classes() # Regenerate classes
844+
for i in order:
845+
get_annotations(classes[i])
846+
for c in classes:
847+
with self.subTest(c=c):
848+
self.assertEqual(get_annotations(c), c.expected_annotations)
849+
annotate_func = get_annotate_function(c)
850+
if c.expected_annotations:
851+
self.assertEqual(annotate_func(Format.VALUE), c.expected_annotations)
852+
else:
853+
self.assertIs(annotate_func, None)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix handling of classes with custom metaclasses in
2+
``annotationlib.get_annotations``.

0 commit comments

Comments
 (0)