Skip to content

gh-119180: Fix annotations lookup on classes with custom metaclasses #120719

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
Closed
1 change: 1 addition & 0 deletions Include/internal/pycore_typeobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ extern PyObject* _Py_slot_tp_getattro(PyObject *self, PyObject *name);
extern PyObject* _Py_slot_tp_getattr_hook(PyObject *self, PyObject *name);

extern PyTypeObject _PyBufferWrapper_Type;
extern PyTypeObject _PyAnnotationsDescriptor_Type;

PyAPI_FUNC(PyObject*) _PySuper_Lookup(PyTypeObject *su_type, PyObject *su_obj,
PyObject *name, int *meth_found);
Expand Down
1 change: 1 addition & 0 deletions Lib/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,7 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen):
# Recurse to methods, properties, and nested classes.
if ((inspect.isroutine(val) or inspect.isclass(val) or
isinstance(val, property)) and
valname != '__annotations__' and
self._from_module(module, val)):
valname = '%s.%s' % (name, valname)
self._find(tests, val, valname, module, source_lines,
Expand Down
4 changes: 3 additions & 1 deletion Lib/pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,10 @@ def visiblename(name, all=None, obj=None):
'__date__', '__doc__', '__file__', '__spec__',
'__loader__', '__module__', '__name__', '__package__',
'__path__', '__qualname__', '__slots__', '__version__',
'__static_attributes__', '__firstlineno__'}:
'__static_attributes__', '__firstlineno__', '__annotations__'}:
return 0
if name == '__annotate__' and getattr(obj, name, None) is None:
return False
# Private names are hidden, but special names are displayed.
if name.startswith('__') and name.endswith('__'): return 1
# Namedtuples have public fields and methods with a single leading underscore
Expand Down
8 changes: 5 additions & 3 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5123,7 +5123,8 @@ def test_iter_keys(self):
self.assertNotIsInstance(it, list)
keys = list(it)
keys.sort()
self.assertEqual(keys, ['__dict__', '__doc__', '__firstlineno__',
self.assertEqual(keys, ['__annotate__', '__annotations__',
'__dict__', '__doc__', '__firstlineno__',
'__module__',
'__static_attributes__', '__weakref__',
'meth'])
Expand All @@ -5135,7 +5136,7 @@ def test_iter_values(self):
it = self.C.__dict__.values()
self.assertNotIsInstance(it, list)
values = list(it)
self.assertEqual(len(values), 7)
self.assertEqual(len(values), 9)

@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
'trace function introduces __local__')
Expand All @@ -5145,7 +5146,8 @@ def test_iter_items(self):
self.assertNotIsInstance(it, list)
keys = [item[0] for item in it]
keys.sort()
self.assertEqual(keys, ['__dict__', '__doc__', '__firstlineno__',
self.assertEqual(keys, ['__annotate__', '__annotations__',
'__dict__', '__doc__', '__firstlineno__',
'__module__',
'__static_attributes__', '__weakref__',
'meth'])
Expand Down
68 changes: 63 additions & 5 deletions Lib/test/test_type_annotations.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import itertools
import textwrap
import types
import unittest
Expand All @@ -11,12 +12,8 @@
class TypeAnnotationTests(unittest.TestCase):

def test_lazy_create_annotations(self):
# type objects lazy create their __annotations__ dict on demand.
# the annotations dict is stored in type.__dict__.
# a freshly created type shouldn't have an annotations dict yet.
foo = type("Foo", (), {})
for i in range(3):
self.assertFalse("__annotations__" in foo.__dict__)
d = foo.__annotations__
self.assertTrue("__annotations__" in foo.__dict__)
self.assertEqual(foo.__annotations__, d)
Expand All @@ -26,7 +23,6 @@ def test_lazy_create_annotations(self):
def test_setting_annotations(self):
foo = type("Foo", (), {})
for i in range(3):
self.assertFalse("__annotations__" in foo.__dict__)
d = {'a': int}
foo.__annotations__ = d
self.assertTrue("__annotations__" in foo.__dict__)
Expand Down Expand Up @@ -270,6 +266,68 @@ def check_annotations(self, f):
self.assertIs(f.__annotate__, None)


class MetaclassTests(unittest.TestCase):
def test_annotated_meta(self):
class Meta(type):
a: int

class X(metaclass=Meta):
pass

class Y(metaclass=Meta):
b: float

self.assertEqual(Meta.__annotations__, {"a": int})
self.assertEqual(Meta.__annotate__(1), {"a": int})

self.assertEqual(X.__annotations__, {})
self.assertIs(X.__annotate__, None)

self.assertEqual(Y.__annotations__, {"b": float})
self.assertEqual(Y.__annotate__(1), {"b": float})

def test_ordering(self):
# Based on a sample by David Ellis
# https://discuss.python.org/t/pep-749-implementing-pep-649/54974/38

def make_classes():
class Meta(type):
a: int
expected_annotations = {"a": int}

class A(type, metaclass=Meta):
b: float
expected_annotations = {"b": float}

class B(metaclass=A):
c: str
expected_annotations = {"c": str}

class C(B):
expected_annotations = {}

class D(metaclass=Meta):
expected_annotations = {}

return Meta, A, B, C, D

classes = make_classes()
class_count = len(classes)
for order in itertools.permutations(range(class_count), class_count):
names = ", ".join(classes[i].__name__ for i in order)
with self.subTest(names=names):
classes = make_classes() # Regenerate classes
for i in order:
classes[i].__annotations__
for c in classes:
with self.subTest(c=c):
self.assertEqual(c.__annotations__, c.expected_annotations)
if c.expected_annotations:
self.assertEqual(c.__annotate__(1), c.expected_annotations)
else:
self.assertIs(c.__annotate__, None)


class DeferredEvaluationTests(unittest.TestCase):
def test_function(self):
def func(x: undefined, /, y: undefined, *args: undefined, z: undefined, **kwargs: undefined) -> undefined:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Make lookup of ``__annotate__`` and ``__annotations__`` on classes more
robust in the presence of metaclasses.
1 change: 1 addition & 0 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -2269,6 +2269,7 @@ static PyTypeObject* static_types[] = {
&PyZip_Type,
&Py_GenericAliasType,
&_PyAnextAwaitable_Type,
&_PyAnnotationsDescriptor_Type,
&_PyAsyncGenASend_Type,
&_PyAsyncGenAThrow_Type,
&_PyAsyncGenWrappedValue_Type,
Expand Down
Loading
Loading