Skip to content

Commit c87cccd

Browse files
committed
pythongh-119180: Yet another approach for fixing metaclass annotations
See python/peps#3847 (comment)
1 parent 1f3a63c commit c87cccd

File tree

2 files changed

+101
-12
lines changed

2 files changed

+101
-12
lines changed

Lib/annotationlib.py

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

526526

527+
_BASE_GET_ANNOTATE = type.__dict__["__annotate__"].__get__
528+
_BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__
529+
530+
531+
def get_annotate_function(obj):
532+
"""Get the __annotate__ function for an object.
533+
534+
obj may be a function, class, or module, or a user-defined type with
535+
an `__annotate__` attribute.
536+
537+
Returns the __annotate__ function or None.
538+
"""
539+
if isinstance(obj, type):
540+
return _BASE_GET_ANNOTATE(obj)
541+
return getattr(obj, "__annotate__", None)
542+
543+
527544
def get_annotations(
528545
obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE
529546
):
@@ -576,16 +593,23 @@ def get_annotations(
576593

577594
# For VALUE format, we look at __annotations__ directly.
578595
if format != Format.VALUE:
579-
annotate = getattr(obj, "__annotate__", None)
596+
annotate = get_annotate_function(obj)
580597
if annotate is not None:
581598
ann = call_annotate_function(annotate, format, owner=obj)
582599
if not isinstance(ann, dict):
583600
raise ValueError(f"{obj!r}.__annotate__ returned a non-dict")
584601
return dict(ann)
585602

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

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

Lib/test/test_annotationlib.py

Lines changed: 73 additions & 8 deletions
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 get_annotations, get_annotate_function
79
from typing import Unpack
810

911
from test.test_inspect import inspect_stock_annotations
@@ -747,25 +749,88 @@ def test_pep_695_generics_with_future_annotations_nested_in_function(self):
747749
results = inspect_stringized_annotations_pep695.nested()
748750

749751
self.assertEqual(
750-
set(results.F_annotations.values()),
751-
set(results.F.__type_params__)
752+
set(results.F_annotations.values()), set(results.F.__type_params__)
752753
)
753754
self.assertEqual(
754755
set(results.F_meth_annotations.values()),
755-
set(results.F.generic_method.__type_params__)
756+
set(results.F.generic_method.__type_params__),
756757
)
757758
self.assertNotEqual(
758-
set(results.F_meth_annotations.values()),
759-
set(results.F.__type_params__)
759+
set(results.F_meth_annotations.values()), set(results.F.__type_params__)
760760
)
761761
self.assertEqual(
762-
set(results.F_meth_annotations.values()).intersection(results.F.__type_params__),
763-
set()
762+
set(results.F_meth_annotations.values()).intersection(
763+
results.F.__type_params__
764+
),
765+
set(),
764766
)
765767

766768
self.assertEqual(results.G_annotations, {"x": str})
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)(1), {"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)(1), {"b": float})
795+
796+
def test_ordering(self):
797+
# Based on a sample by David Ellis
798+
# https://discuss.python.org/t/pep-749-implementing-pep-649/54974/38
799+
800+
def make_classes():
801+
class Meta(type):
802+
a: int
803+
expected_annotations = {"a": int}
804+
805+
class A(type, metaclass=Meta):
806+
b: float
807+
expected_annotations = {"b": float}
808+
809+
class B(metaclass=A):
810+
c: str
811+
expected_annotations = {"c": str}
812+
813+
class C(B):
814+
expected_annotations = {}
815+
816+
class D(metaclass=Meta):
817+
expected_annotations = {}
818+
819+
return Meta, A, B, C, D
820+
821+
classes = make_classes()
822+
class_count = len(classes)
823+
for order in itertools.permutations(range(class_count), class_count):
824+
names = ", ".join(classes[i].__name__ for i in order)
825+
with self.subTest(names=names):
826+
classes = make_classes() # Regenerate classes
827+
for i in order:
828+
get_annotations(classes[i])
829+
for c in classes:
830+
with self.subTest(c=c):
831+
self.assertEqual(get_annotations(c), c.expected_annotations)
832+
annotate_func = get_annotate_function(c)
833+
if c.expected_annotations:
834+
self.assertEqual(annotate_func(1), c.expected_annotations)
835+
else:
836+
self.assertIs(annotate_func, None)

0 commit comments

Comments
 (0)