Skip to content

Commit 6f71f30

Browse files
committed
pythongh-119180: Fix annotations lookup on classes with custom metaclasses
See https://discuss.python.org/t/pep-749-implementing-pep-649/54974/28 and subsequent posts.
1 parent a0dce37 commit 6f71f30

7 files changed

+105
-13
lines changed

Include/internal/pycore_global_objects_fini_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ struct _Py_global_strings {
8282
STRUCT_FOR_ID(__anext__)
8383
STRUCT_FOR_ID(__annotate__)
8484
STRUCT_FOR_ID(__annotations__)
85+
STRUCT_FOR_ID(__annotations_cache__)
8586
STRUCT_FOR_ID(__args__)
8687
STRUCT_FOR_ID(__asyncio_running_event_loop__)
8788
STRUCT_FOR_ID(__await__)

Include/internal/pycore_runtime_init_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_type_annotations.py

+71-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import itertools
12
import textwrap
23
import types
34
import unittest
@@ -16,22 +17,22 @@ def test_lazy_create_annotations(self):
1617
# a freshly created type shouldn't have an annotations dict yet.
1718
foo = type("Foo", (), {})
1819
for i in range(3):
19-
self.assertFalse("__annotations__" in foo.__dict__)
20+
self.assertFalse("__annotations_cache__" in foo.__dict__)
2021
d = foo.__annotations__
21-
self.assertTrue("__annotations__" in foo.__dict__)
22+
self.assertTrue("__annotations_cache__" in foo.__dict__)
2223
self.assertEqual(foo.__annotations__, d)
23-
self.assertEqual(foo.__dict__['__annotations__'], d)
24+
self.assertEqual(foo.__dict__['__annotations_cache__'], d)
2425
del foo.__annotations__
2526

2627
def test_setting_annotations(self):
2728
foo = type("Foo", (), {})
2829
for i in range(3):
29-
self.assertFalse("__annotations__" in foo.__dict__)
30+
self.assertFalse("__annotations_cache__" in foo.__dict__)
3031
d = {'a': int}
3132
foo.__annotations__ = d
32-
self.assertTrue("__annotations__" in foo.__dict__)
33+
self.assertTrue("__annotations_cache__" in foo.__dict__)
3334
self.assertEqual(foo.__annotations__, d)
34-
self.assertEqual(foo.__dict__['__annotations__'], d)
35+
self.assertEqual(foo.__dict__['__annotations_cache__'], d)
3536
del foo.__annotations__
3637

3738
def test_annotations_getset_raises(self):
@@ -55,9 +56,9 @@ class C:
5556
a:int=3
5657
b:str=4
5758
self.assertEqual(C.__annotations__, {"a": int, "b": str})
58-
self.assertTrue("__annotations__" in C.__dict__)
59+
self.assertTrue("__annotations_cache__" in C.__dict__)
5960
del C.__annotations__
60-
self.assertFalse("__annotations__" in C.__dict__)
61+
self.assertFalse("__annotations_cache__" in C.__dict__)
6162

6263
def test_descriptor_still_works(self):
6364
class C:
@@ -270,6 +271,68 @@ def check_annotations(self, f):
270271
self.assertIs(f.__annotate__, None)
271272

272273

274+
class MetaclassTests(unittest.TestCase):
275+
def test_annotated_meta(self):
276+
class Meta(type):
277+
a: int
278+
279+
class X(metaclass=Meta):
280+
pass
281+
282+
class Y(metaclass=Meta):
283+
b: float
284+
285+
self.assertEqual(Meta.__annotations__, {"a": int})
286+
self.assertEqual(Meta.__annotate__(1), {"a": int})
287+
288+
self.assertEqual(X.__annotations__, {})
289+
self.assertIs(X.__annotate__, None)
290+
291+
self.assertEqual(Y.__annotations__, {"b": float})
292+
self.assertEqual(Y.__annotate__(1), {"b": float})
293+
294+
def test_ordering(self):
295+
# Based on a sample by David Ellis
296+
# https://discuss.python.org/t/pep-749-implementing-pep-649/54974/38
297+
298+
def make_classes():
299+
class Meta(type):
300+
a: int
301+
expected_annotations = {"a": int}
302+
303+
class A(type, metaclass=Meta):
304+
b: float
305+
expected_annotations = {"b": float}
306+
307+
class B(metaclass=A):
308+
c: str
309+
expected_annotations = {"c": str}
310+
311+
class C(B):
312+
expected_annotations = {}
313+
314+
class D(metaclass=Meta):
315+
expected_annotations = {}
316+
317+
return Meta, A, B, C, D
318+
319+
classes = make_classes()
320+
class_count = len(classes)
321+
for order in itertools.permutations(range(class_count), class_count):
322+
names = ", ".join(classes[i].__name__ for i in order)
323+
with self.subTest(names=names):
324+
classes = make_classes() # Regenerate classes
325+
for i in order:
326+
classes[i].__annotations__
327+
for c in classes:
328+
with self.subTest(c=c):
329+
self.assertEqual(c.__annotations__, c.expected_annotations)
330+
if c.expected_annotations:
331+
self.assertEqual(c.__annotate__(1), c.expected_annotations)
332+
else:
333+
self.assertIs(c.__annotate__, None)
334+
335+
273336
class DeferredEvaluationTests(unittest.TestCase):
274337
def test_function(self):
275338
def func(x: undefined, /, y: undefined, *args: undefined, z: undefined, **kwargs: undefined) -> undefined:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Make lookup of ``__annotate__`` and ``__annotations__`` on classes more
2+
robust in the presence of metaclasses.

Objects/typeobject.c

+26-5
Original file line numberDiff line numberDiff line change
@@ -1902,7 +1902,7 @@ type_set_annotate(PyTypeObject *type, PyObject *value, void *Py_UNUSED(ignored))
19021902
return -1;
19031903
}
19041904
if (!Py_IsNone(value)) {
1905-
if (PyDict_Pop(dict, &_Py_ID(__annotations__), NULL) == -1) {
1905+
if (PyDict_Pop(dict, &_Py_ID(__annotations_cache__), NULL) == -1) {
19061906
Py_DECREF(dict);
19071907
PyType_Modified(type);
19081908
return -1;
@@ -1923,7 +1923,7 @@ type_get_annotations(PyTypeObject *type, void *context)
19231923

19241924
PyObject *annotations;
19251925
PyObject *dict = PyType_GetDict(type);
1926-
if (PyDict_GetItemRef(dict, &_Py_ID(__annotations__), &annotations) < 0) {
1926+
if (PyDict_GetItemRef(dict, &_Py_ID(__annotations_cache__), &annotations) < 0) {
19271927
Py_DECREF(dict);
19281928
return NULL;
19291929
}
@@ -1962,7 +1962,7 @@ type_get_annotations(PyTypeObject *type, void *context)
19621962
Py_DECREF(annotate);
19631963
if (annotations) {
19641964
int result = PyDict_SetItem(
1965-
dict, &_Py_ID(__annotations__), annotations);
1965+
dict, &_Py_ID(__annotations_cache__), annotations);
19661966
if (result) {
19671967
Py_CLEAR(annotations);
19681968
} else {
@@ -1988,10 +1988,10 @@ type_set_annotations(PyTypeObject *type, PyObject *value, void *context)
19881988
PyObject *dict = PyType_GetDict(type);
19891989
if (value != NULL) {
19901990
/* set */
1991-
result = PyDict_SetItem(dict, &_Py_ID(__annotations__), value);
1991+
result = PyDict_SetItem(dict, &_Py_ID(__annotations_cache__), value);
19921992
} else {
19931993
/* delete */
1994-
result = PyDict_Pop(dict, &_Py_ID(__annotations__), NULL);
1994+
result = PyDict_Pop(dict, &_Py_ID(__annotations_cache__), NULL);
19951995
if (result == 0) {
19961996
PyErr_SetString(PyExc_AttributeError, "__annotations__");
19971997
Py_DECREF(dict);
@@ -4223,6 +4223,24 @@ type_new_set_classcell(PyTypeObject *type)
42234223
return 0;
42244224
}
42254225

4226+
static int
4227+
type_new_set_annotate(PyTypeObject *type)
4228+
{
4229+
PyObject *dict = lookup_tp_dict(type);
4230+
// If __annotate__ is not set (i.e., the class has no annotations),
4231+
// set it to None
4232+
int result = PyDict_Contains(dict, &_Py_ID(__annotate__));
4233+
if (result < 0) {
4234+
return -1;
4235+
}
4236+
else if (result == 0) {
4237+
if (PyDict_SetItem(dict, &_Py_ID(__annotate__), Py_None) < 0) {
4238+
return -1;
4239+
}
4240+
}
4241+
return 0;
4242+
}
4243+
42264244
static int
42274245
type_new_set_classdictcell(PyTypeObject *type)
42284246
{
@@ -4296,6 +4314,9 @@ type_new_set_attrs(const type_new_ctx *ctx, PyTypeObject *type)
42964314
if (type_new_set_classdictcell(type) < 0) {
42974315
return -1;
42984316
}
4317+
if (type_new_set_annotate(type) < 0) {
4318+
return -1;
4319+
}
42994320
return 0;
43004321
}
43014322

0 commit comments

Comments
 (0)