Skip to content

Commit 2f2b698

Browse files
bpo-43901: Lazy-create an empty annotations dict in all unannotated user classes and modules (#25623)
Change class and module objects to lazy-create empty annotations dicts on demand. The annotations dicts are stored in the object's `__dict__` for backwards compatibility.
1 parent dbe60ee commit 2f2b698

File tree

9 files changed

+308
-8
lines changed

9 files changed

+308
-8
lines changed

Lib/test/ann_module4.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# This ann_module isn't for test_typing,
2+
# it's for test_module
3+
4+
a:int=3
5+
b:str=4

Lib/test/test_grammar.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,7 @@ class CC(metaclass=CMeta):
382382
self.assertEqual(CC.__annotations__['xx'], 'ANNOT')
383383

384384
def test_var_annot_module_semantics(self):
385-
with self.assertRaises(AttributeError):
386-
print(test.__annotations__)
385+
self.assertEqual(test.__annotations__, {})
387386
self.assertEqual(ann_module.__annotations__,
388387
{1: 2, 'x': int, 'y': str, 'f': typing.Tuple[int, int]})
389388
self.assertEqual(ann_module.M.__annotations__,

Lib/test/test_module.py

+54
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,60 @@ class M(ModuleType):
286286
melon = Descr()
287287
self.assertRaises(RuntimeError, getattr, M("mymod"), "melon")
288288

289+
def test_lazy_create_annotations(self):
290+
# module objects lazy create their __annotations__ dict on demand.
291+
# the annotations dict is stored in module.__dict__.
292+
# a freshly created module shouldn't have an annotations dict yet.
293+
foo = ModuleType("foo")
294+
for i in range(4):
295+
self.assertFalse("__annotations__" in foo.__dict__)
296+
d = foo.__annotations__
297+
self.assertTrue("__annotations__" in foo.__dict__)
298+
self.assertEqual(foo.__annotations__, d)
299+
self.assertEqual(foo.__dict__['__annotations__'], d)
300+
if i % 2:
301+
del foo.__annotations__
302+
else:
303+
del foo.__dict__['__annotations__']
304+
305+
def test_setting_annotations(self):
306+
foo = ModuleType("foo")
307+
for i in range(4):
308+
self.assertFalse("__annotations__" in foo.__dict__)
309+
d = {'a': int}
310+
foo.__annotations__ = d
311+
self.assertTrue("__annotations__" in foo.__dict__)
312+
self.assertEqual(foo.__annotations__, d)
313+
self.assertEqual(foo.__dict__['__annotations__'], d)
314+
if i % 2:
315+
del foo.__annotations__
316+
else:
317+
del foo.__dict__['__annotations__']
318+
319+
def test_annotations_getset_raises(self):
320+
# module has no dict, all operations fail
321+
foo = ModuleType.__new__(ModuleType)
322+
with self.assertRaises(TypeError):
323+
print(foo.__annotations__)
324+
with self.assertRaises(TypeError):
325+
foo.__annotations__ = {}
326+
with self.assertRaises(TypeError):
327+
del foo.__annotations__
328+
329+
# double delete
330+
foo = ModuleType("foo")
331+
foo.__annotations__ = {}
332+
del foo.__annotations__
333+
with self.assertRaises(AttributeError):
334+
del foo.__annotations__
335+
336+
def test_annotations_are_created_correctly(self):
337+
from test import ann_module4
338+
self.assertTrue("__annotations__" in ann_module4.__dict__)
339+
del ann_module4.__annotations__
340+
self.assertFalse("__annotations__" in ann_module4.__dict__)
341+
342+
289343
# frozen and namespace module reprs are tested in importlib.
290344

291345

Lib/test/test_opcodes.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,9 @@ def test_setup_annotations_line(self):
3131
except OSError:
3232
pass
3333

34-
def test_no_annotations_if_not_needed(self):
34+
def test_default_annotations_exist(self):
3535
class C: pass
36-
with self.assertRaises(AttributeError):
37-
C.__annotations__
36+
self.assertEqual(C.__annotations__, {})
3837

3938
def test_use_existing_annotations(self):
4039
ns = {'__annotations__': {1: 2}}

Lib/test/test_type_annotations.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import unittest
2+
3+
class TypeAnnotationTests(unittest.TestCase):
4+
5+
def test_lazy_create_annotations(self):
6+
# type objects lazy create their __annotations__ dict on demand.
7+
# the annotations dict is stored in type.__dict__.
8+
# a freshly created type shouldn't have an annotations dict yet.
9+
foo = type("Foo", (), {})
10+
for i in range(3):
11+
self.assertFalse("__annotations__" in foo.__dict__)
12+
d = foo.__annotations__
13+
self.assertTrue("__annotations__" in foo.__dict__)
14+
self.assertEqual(foo.__annotations__, d)
15+
self.assertEqual(foo.__dict__['__annotations__'], d)
16+
del foo.__annotations__
17+
18+
def test_setting_annotations(self):
19+
foo = type("Foo", (), {})
20+
for i in range(3):
21+
self.assertFalse("__annotations__" in foo.__dict__)
22+
d = {'a': int}
23+
foo.__annotations__ = d
24+
self.assertTrue("__annotations__" in foo.__dict__)
25+
self.assertEqual(foo.__annotations__, d)
26+
self.assertEqual(foo.__dict__['__annotations__'], d)
27+
del foo.__annotations__
28+
29+
def test_annotations_getset_raises(self):
30+
# builtin types don't have __annotations__ (yet!)
31+
with self.assertRaises(AttributeError):
32+
print(float.__annotations__)
33+
with self.assertRaises(TypeError):
34+
float.__annotations__ = {}
35+
with self.assertRaises(TypeError):
36+
del float.__annotations__
37+
38+
# double delete
39+
foo = type("Foo", (), {})
40+
foo.__annotations__ = {}
41+
del foo.__annotations__
42+
with self.assertRaises(AttributeError):
43+
del foo.__annotations__
44+
45+
def test_annotations_are_created_correctly(self):
46+
class C:
47+
a:int=3
48+
b:str=4
49+
self.assertTrue("__annotations__" in C.__dict__)
50+
del C.__annotations__
51+
self.assertFalse("__annotations__" in C.__dict__)
52+
53+
def test_descriptor_still_works(self):
54+
class C:
55+
def __init__(self, name=None, bases=None, d=None):
56+
self.my_annotations = None
57+
58+
@property
59+
def __annotations__(self):
60+
if not hasattr(self, 'my_annotations'):
61+
self.my_annotations = {}
62+
if not isinstance(self.my_annotations, dict):
63+
self.my_annotations = {}
64+
return self.my_annotations
65+
66+
@__annotations__.setter
67+
def __annotations__(self, value):
68+
if not isinstance(value, dict):
69+
raise ValueError("can only set __annotations__ to a dict")
70+
self.my_annotations = value
71+
72+
@__annotations__.deleter
73+
def __annotations__(self):
74+
if hasattr(self, 'my_annotations') and self.my_annotations == None:
75+
raise AttributeError('__annotations__')
76+
self.my_annotations = None
77+
78+
c = C()
79+
self.assertEqual(c.__annotations__, {})
80+
d = {'a':'int'}
81+
c.__annotations__ = d
82+
self.assertEqual(c.__annotations__, d)
83+
with self.assertRaises(ValueError):
84+
c.__annotations__ = 123
85+
del c.__annotations__
86+
with self.assertRaises(AttributeError):
87+
del c.__annotations__
88+
self.assertEqual(c.__annotations__, {})
89+
90+
91+
class D(metaclass=C):
92+
pass
93+
94+
self.assertEqual(D.__annotations__, {})
95+
d = {'a':'int'}
96+
D.__annotations__ = d
97+
self.assertEqual(D.__annotations__, d)
98+
with self.assertRaises(ValueError):
99+
D.__annotations__ = 123
100+
del D.__annotations__
101+
with self.assertRaises(AttributeError):
102+
del D.__annotations__
103+
self.assertEqual(D.__annotations__, {})

Lib/typing.py

+2
Original file line numberDiff line numberDiff line change
@@ -1677,6 +1677,8 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
16771677
else:
16781678
base_globals = globalns
16791679
ann = base.__dict__.get('__annotations__', {})
1680+
if isinstance(ann, types.GetSetDescriptorType):
1681+
ann = {}
16801682
base_locals = dict(vars(base)) if localns is None else localns
16811683
if localns is None and globalns is None:
16821684
# This is surprising, but required. Before Python 3.10,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Change class and module objects to lazy-create empty annotations dicts on
2+
demand. The annotations dicts are stored in the object's __dict__ for
3+
backwards compatibility.

Objects/moduleobject.c

+69-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ static Py_ssize_t max_module_number;
1212
_Py_IDENTIFIER(__doc__);
1313
_Py_IDENTIFIER(__name__);
1414
_Py_IDENTIFIER(__spec__);
15+
_Py_IDENTIFIER(__dict__);
16+
_Py_IDENTIFIER(__dir__);
17+
_Py_IDENTIFIER(__annotations__);
1518

1619
static PyMemberDef module_members[] = {
1720
{"__dict__", T_OBJECT, offsetof(PyModuleObject, md_dict), READONLY},
@@ -807,8 +810,6 @@ module_clear(PyModuleObject *m)
807810
static PyObject *
808811
module_dir(PyObject *self, PyObject *args)
809812
{
810-
_Py_IDENTIFIER(__dict__);
811-
_Py_IDENTIFIER(__dir__);
812813
PyObject *result = NULL;
813814
PyObject *dict = _PyObject_GetAttrId(self, &PyId___dict__);
814815

@@ -841,6 +842,71 @@ static PyMethodDef module_methods[] = {
841842
{0}
842843
};
843844

845+
static PyObject *
846+
module_get_annotations(PyModuleObject *m, void *Py_UNUSED(ignored))
847+
{
848+
PyObject *dict = _PyObject_GetAttrId((PyObject *)m, &PyId___dict__);
849+
850+
if ((dict == NULL) || !PyDict_Check(dict)) {
851+
PyErr_Format(PyExc_TypeError, "<module>.__dict__ is not a dictionary");
852+
return NULL;
853+
}
854+
855+
PyObject *annotations;
856+
/* there's no _PyDict_GetItemId without WithError, so let's LBYL. */
857+
if (_PyDict_ContainsId(dict, &PyId___annotations__)) {
858+
annotations = _PyDict_GetItemIdWithError(dict, &PyId___annotations__);
859+
/*
860+
** _PyDict_GetItemIdWithError could still fail,
861+
** for instance with a well-timed Ctrl-C or a MemoryError.
862+
** so let's be totally safe.
863+
*/
864+
if (annotations) {
865+
Py_INCREF(annotations);
866+
}
867+
} else {
868+
annotations = PyDict_New();
869+
if (annotations) {
870+
int result = _PyDict_SetItemId(dict, &PyId___annotations__, annotations);
871+
if (result) {
872+
Py_CLEAR(annotations);
873+
}
874+
}
875+
}
876+
Py_DECREF(dict);
877+
return annotations;
878+
}
879+
880+
static int
881+
module_set_annotations(PyModuleObject *m, PyObject *value, void *Py_UNUSED(ignored))
882+
{
883+
PyObject *dict = _PyObject_GetAttrId((PyObject *)m, &PyId___dict__);
884+
885+
if ((dict == NULL) || !PyDict_Check(dict)) {
886+
PyErr_Format(PyExc_TypeError, "<module>.__dict__ is not a dictionary");
887+
return -1;
888+
}
889+
890+
if (value != NULL) {
891+
/* set */
892+
return _PyDict_SetItemId(dict, &PyId___annotations__, value);
893+
}
894+
895+
/* delete */
896+
if (!_PyDict_ContainsId(dict, &PyId___annotations__)) {
897+
PyErr_Format(PyExc_AttributeError, "__annotations__");
898+
return -1;
899+
}
900+
901+
return _PyDict_DelItemId(dict, &PyId___annotations__);
902+
}
903+
904+
905+
static PyGetSetDef module_getsets[] = {
906+
{"__annotations__", (getter)module_get_annotations, (setter)module_set_annotations},
907+
{NULL}
908+
};
909+
844910
PyTypeObject PyModule_Type = {
845911
PyVarObject_HEAD_INIT(&PyType_Type, 0)
846912
"module", /* tp_name */
@@ -872,7 +938,7 @@ PyTypeObject PyModule_Type = {
872938
0, /* tp_iternext */
873939
module_methods, /* tp_methods */
874940
module_members, /* tp_members */
875-
0, /* tp_getset */
941+
module_getsets, /* tp_getset */
876942
0, /* tp_base */
877943
0, /* tp_dict */
878944
0, /* tp_descr_get */

Objects/typeobject.c

+69
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ typedef struct PySlot_Offset {
5252

5353
/* alphabetical order */
5454
_Py_IDENTIFIER(__abstractmethods__);
55+
_Py_IDENTIFIER(__annotations__);
5556
_Py_IDENTIFIER(__class__);
5657
_Py_IDENTIFIER(__class_getitem__);
5758
_Py_IDENTIFIER(__classcell__);
@@ -930,6 +931,73 @@ type_set_doc(PyTypeObject *type, PyObject *value, void *context)
930931
return _PyDict_SetItemId(type->tp_dict, &PyId___doc__, value);
931932
}
932933

934+
static PyObject *
935+
type_get_annotations(PyTypeObject *type, void *context)
936+
{
937+
if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
938+
PyErr_Format(PyExc_AttributeError, "type object '%s' has no attribute '__annotations__'", type->tp_name);
939+
return NULL;
940+
}
941+
942+
PyObject *annotations;
943+
/* there's no _PyDict_GetItemId without WithError, so let's LBYL. */
944+
if (_PyDict_ContainsId(type->tp_dict, &PyId___annotations__)) {
945+
annotations = _PyDict_GetItemIdWithError(type->tp_dict, &PyId___annotations__);
946+
/*
947+
** _PyDict_GetItemIdWithError could still fail,
948+
** for instance with a well-timed Ctrl-C or a MemoryError.
949+
** so let's be totally safe.
950+
*/
951+
if (annotations) {
952+
if (Py_TYPE(annotations)->tp_descr_get) {
953+
annotations = Py_TYPE(annotations)->tp_descr_get(annotations, NULL,
954+
(PyObject *)type);
955+
} else {
956+
Py_INCREF(annotations);
957+
}
958+
}
959+
} else {
960+
annotations = PyDict_New();
961+
if (annotations) {
962+
int result = _PyDict_SetItemId(type->tp_dict, &PyId___annotations__, annotations);
963+
if (result) {
964+
Py_CLEAR(annotations);
965+
} else {
966+
PyType_Modified(type);
967+
}
968+
}
969+
}
970+
return annotations;
971+
}
972+
973+
static int
974+
type_set_annotations(PyTypeObject *type, PyObject *value, void *context)
975+
{
976+
if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
977+
PyErr_Format(PyExc_TypeError, "can't set attributes of built-in/extension type '%s'", type->tp_name);
978+
return -1;
979+
}
980+
981+
int result;
982+
if (value != NULL) {
983+
/* set */
984+
result = _PyDict_SetItemId(type->tp_dict, &PyId___annotations__, value);
985+
} else {
986+
/* delete */
987+
if (!_PyDict_ContainsId(type->tp_dict, &PyId___annotations__)) {
988+
PyErr_Format(PyExc_AttributeError, "__annotations__");
989+
return -1;
990+
}
991+
result = _PyDict_DelItemId(type->tp_dict, &PyId___annotations__);
992+
}
993+
994+
if (result == 0) {
995+
PyType_Modified(type);
996+
}
997+
return result;
998+
}
999+
1000+
9331001
/*[clinic input]
9341002
type.__instancecheck__ -> bool
9351003
@@ -973,6 +1041,7 @@ static PyGetSetDef type_getsets[] = {
9731041
{"__dict__", (getter)type_dict, NULL, NULL},
9741042
{"__doc__", (getter)type_get_doc, (setter)type_set_doc, NULL},
9751043
{"__text_signature__", (getter)type_get_text_signature, NULL, NULL},
1044+
{"__annotations__", (getter)type_get_annotations, (setter)type_set_annotations, NULL},
9761045
{0}
9771046
};
9781047

0 commit comments

Comments
 (0)