Skip to content

Commit d82d445

Browse files
[3.14] gh-133778: Fix setting __annotations__ under PEP 563 (GH-133794) (#134655)
gh-133778: Fix setting `__annotations__` under PEP 563 (GH-133794) (cherry picked from commit 4443110) Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
1 parent 93aee56 commit d82d445

File tree

3 files changed

+60
-9
lines changed

3 files changed

+60
-9
lines changed

Lib/test/test_type_annotations.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,28 @@ def f(x: int) -> int: pass
498498
self.assertEqual(f.__annotate__(annotationlib.Format.VALUE), annos)
499499
self.assertEqual(f.__annotations__, annos)
500500

501+
def test_set_annotations(self):
502+
function_code = textwrap.dedent("""
503+
def f(x: int):
504+
pass
505+
""")
506+
class_code = textwrap.dedent("""
507+
class f:
508+
x: int
509+
""")
510+
for future in (False, True):
511+
for label, code in (("function", function_code), ("class", class_code)):
512+
with self.subTest(future=future, label=label):
513+
if future:
514+
code = "from __future__ import annotations\n" + code
515+
ns = run_code(code)
516+
f = ns["f"]
517+
anno = "int" if future else int
518+
self.assertEqual(f.__annotations__, {"x": anno})
519+
520+
f.__annotations__ = {"x": str}
521+
self.assertEqual(f.__annotations__, {"x": str})
522+
501523
def test_name_clash_with_format(self):
502524
# this test would fail if __annotate__'s parameter was called "format"
503525
# during symbol table construction
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix bug where assigning to the :attr:`~type.__annotations__` attributes of
2+
classes defined under ``from __future__ import annotations`` had no effect.

Objects/typeobject.c

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2065,19 +2065,46 @@ type_set_annotations(PyObject *tp, PyObject *value, void *Py_UNUSED(closure))
20652065
return -1;
20662066
}
20672067

2068-
int result;
20692068
PyObject *dict = PyType_GetDict(type);
2070-
if (value != NULL) {
2071-
/* set */
2072-
result = PyDict_SetItem(dict, &_Py_ID(__annotations_cache__), value);
2073-
} else {
2074-
/* delete */
2075-
result = PyDict_Pop(dict, &_Py_ID(__annotations_cache__), NULL);
2076-
if (result == 0) {
2077-
PyErr_SetString(PyExc_AttributeError, "__annotations__");
2069+
int result = PyDict_ContainsString(dict, "__annotations__");
2070+
if (result < 0) {
2071+
Py_DECREF(dict);
2072+
return -1;
2073+
}
2074+
if (result) {
2075+
// If __annotations__ is currently in the dict, we update it,
2076+
if (value != NULL) {
2077+
result = PyDict_SetItem(dict, &_Py_ID(__annotations__), value);
2078+
} else {
2079+
result = PyDict_Pop(dict, &_Py_ID(__annotations__), NULL);
2080+
if (result == 0) {
2081+
// Somebody else just deleted it?
2082+
PyErr_SetString(PyExc_AttributeError, "__annotations__");
2083+
Py_DECREF(dict);
2084+
return -1;
2085+
}
2086+
}
2087+
if (result < 0) {
20782088
Py_DECREF(dict);
20792089
return -1;
20802090
}
2091+
// Also clear __annotations_cache__ just in case.
2092+
result = PyDict_Pop(dict, &_Py_ID(__annotations_cache__), NULL);
2093+
}
2094+
else {
2095+
// Else we update only __annotations_cache__.
2096+
if (value != NULL) {
2097+
/* set */
2098+
result = PyDict_SetItem(dict, &_Py_ID(__annotations_cache__), value);
2099+
} else {
2100+
/* delete */
2101+
result = PyDict_Pop(dict, &_Py_ID(__annotations_cache__), NULL);
2102+
if (result == 0) {
2103+
PyErr_SetString(PyExc_AttributeError, "__annotations__");
2104+
Py_DECREF(dict);
2105+
return -1;
2106+
}
2107+
}
20812108
}
20822109
if (result < 0) {
20832110
Py_DECREF(dict);

0 commit comments

Comments
 (0)