Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 07b8d31

Browse files
gh-132261: Store annotations at hidden internal keys in the class dict (#132345)
1 parent e5f68fd commit 07b8d31

16 files changed

+100
-52
lines changed

Doc/library/annotationlib.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -303,12 +303,12 @@ Functions
303303
.. function:: get_annotate_function(obj)
304304

305305
Retrieve the :term:`annotate function` for *obj*. Return :const:`!None`
306-
if *obj* does not have an annotate function.
306+
if *obj* does not have an annotate function. *obj* may be a class, function,
307+
module, or a namespace dictionary for a class. The last case is useful during
308+
class creation, e.g. in the ``__new__`` method of a metaclass.
307309

308310
This is usually equivalent to accessing the :attr:`~object.__annotate__`
309-
attribute of *obj*, but direct access to the attribute may return the wrong
310-
object in certain situations involving metaclasses. This function should be
311-
used instead of accessing the attribute directly.
311+
attribute of *obj*, but access through this public function is preferred.
312312

313313
.. versionadded:: 3.14
314314

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ struct _Py_global_strings {
7878
STRUCT_FOR_ID(__and__)
7979
STRUCT_FOR_ID(__anext__)
8080
STRUCT_FOR_ID(__annotate__)
81+
STRUCT_FOR_ID(__annotate_func__)
8182
STRUCT_FOR_ID(__annotations__)
83+
STRUCT_FOR_ID(__annotations_cache__)
8284
STRUCT_FOR_ID(__args__)
8385
STRUCT_FOR_ID(__await__)
8486
STRUCT_FOR_ID(__bases__)

Include/internal/pycore_magic_number.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ Known values:
274274
Python 3.14a6 3619 (Renumber RESUME opcode from 149 to 128)
275275
Python 3.14a6 3620 (Optimize bytecode for all/any/tuple called on a genexp)
276276
Python 3.14a7 3621 (Optimize LOAD_FAST opcodes into LOAD_FAST_BORROW)
277+
Python 3.14a7 3622 (Store annotations in different class dict keys)
277278
278279
Python 3.15 will start with 3650
279280
@@ -286,7 +287,7 @@ PC/launcher.c must also be updated.
286287
287288
*/
288289

289-
#define PYC_MAGIC_NUMBER 3621
290+
#define PYC_MAGIC_NUMBER 3622
290291
/* This is equivalent to converting PYC_MAGIC_NUMBER to 2 bytes
291292
(little-endian) and then appending b'\r\n'. */
292293
#define PYC_MAGIC_NUMBER_TOKEN \

Include/internal/pycore_runtime_init_generated.h

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/annotationlib.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -619,14 +619,6 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
619619
raise ValueError(f"Invalid format: {format!r}")
620620

621621

622-
# We use the descriptors from builtins.type instead of accessing
623-
# .__annotations__ and .__annotate__ directly on class objects, because
624-
# otherwise we could get wrong results in some cases involving metaclasses.
625-
# See PEP 749.
626-
_BASE_GET_ANNOTATE = type.__dict__["__annotate__"].__get__
627-
_BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__
628-
629-
630622
def get_annotate_function(obj):
631623
"""Get the __annotate__ function for an object.
632624
@@ -635,12 +627,11 @@ def get_annotate_function(obj):
635627
636628
Returns the __annotate__ function or None.
637629
"""
638-
if isinstance(obj, type):
630+
if isinstance(obj, dict):
639631
try:
640-
return _BASE_GET_ANNOTATE(obj)
641-
except AttributeError:
642-
# AttributeError is raised for static types.
643-
return None
632+
return obj["__annotate__"]
633+
except KeyError:
634+
return obj.get("__annotate_func__", None)
644635
return getattr(obj, "__annotate__", None)
645636

646637

@@ -833,7 +824,7 @@ def _get_and_call_annotate(obj, format):
833824
def _get_dunder_annotations(obj):
834825
if isinstance(obj, type):
835826
try:
836-
ann = _BASE_GET_ANNOTATIONS(obj)
827+
ann = obj.__annotations__
837828
except AttributeError:
838829
# For static types, the descriptor raises AttributeError.
839830
return {}

Lib/pydoc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,8 @@ def visiblename(name, all=None, obj=None):
330330
'__date__', '__doc__', '__file__', '__spec__',
331331
'__loader__', '__module__', '__name__', '__package__',
332332
'__path__', '__qualname__', '__slots__', '__version__',
333-
'__static_attributes__', '__firstlineno__'}:
333+
'__static_attributes__', '__firstlineno__',
334+
'__annotate_func__', '__annotations_cache__'}:
334335
return 0
335336
# Private names are hidden, but special names are displayed.
336337
if name.startswith('__') and name.endswith('__'): return 1

Lib/test/test_ast/test_ast.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def test_arguments(self):
298298
x = ast.arguments()
299299
self.assertEqual(x._fields, ('posonlyargs', 'args', 'vararg', 'kwonlyargs',
300300
'kw_defaults', 'kwarg', 'defaults'))
301-
self.assertEqual(x.__annotations__, {
301+
self.assertEqual(ast.arguments.__annotations__, {
302302
'posonlyargs': list[ast.arg],
303303
'args': list[ast.arg],
304304
'vararg': ast.arg | None,

Lib/test/test_pydoc/test_pydoc.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,6 @@ class A(builtins.object)
7878
| __weakref__%s
7979
8080
class B(builtins.object)
81-
| Methods defined here:
82-
|
83-
| __annotate__(format, /)
84-
|
85-
| ----------------------------------------------------------------------
8681
| Data descriptors defined here:
8782
|
8883
| __dict__%s
@@ -180,9 +175,6 @@ class A(builtins.object)
180175
list of weak references to the object
181176
182177
class B(builtins.object)
183-
Methods defined here:
184-
__annotate__(format, /)
185-
----------------------------------------------------------------------
186178
Data descriptors defined here:
187179
__dict__
188180
dictionary for instance variables

Lib/test/test_type_annotations.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,33 @@
44
import types
55
import unittest
66
from test.support import run_code, check_syntax_error, cpython_only
7+
from test.test_inspect import inspect_stringized_annotations
78

89

910
class TypeAnnotationTests(unittest.TestCase):
1011

1112
def test_lazy_create_annotations(self):
1213
# type objects lazy create their __annotations__ dict on demand.
13-
# the annotations dict is stored in type.__dict__.
14+
# the annotations dict is stored in type.__dict__ (as __annotations_cache__).
1415
# a freshly created type shouldn't have an annotations dict yet.
1516
foo = type("Foo", (), {})
1617
for i in range(3):
17-
self.assertFalse("__annotations__" in foo.__dict__)
18+
self.assertFalse("__annotations_cache__" in foo.__dict__)
1819
d = foo.__annotations__
19-
self.assertTrue("__annotations__" in foo.__dict__)
20+
self.assertTrue("__annotations_cache__" in foo.__dict__)
2021
self.assertEqual(foo.__annotations__, d)
21-
self.assertEqual(foo.__dict__['__annotations__'], d)
22+
self.assertEqual(foo.__dict__['__annotations_cache__'], d)
2223
del foo.__annotations__
2324

2425
def test_setting_annotations(self):
2526
foo = type("Foo", (), {})
2627
for i in range(3):
27-
self.assertFalse("__annotations__" in foo.__dict__)
28+
self.assertFalse("__annotations_cache__" in foo.__dict__)
2829
d = {'a': int}
2930
foo.__annotations__ = d
30-
self.assertTrue("__annotations__" in foo.__dict__)
31+
self.assertTrue("__annotations_cache__" in foo.__dict__)
3132
self.assertEqual(foo.__annotations__, d)
32-
self.assertEqual(foo.__dict__['__annotations__'], d)
33+
self.assertEqual(foo.__dict__['__annotations_cache__'], d)
3334
del foo.__annotations__
3435

3536
def test_annotations_getset_raises(self):
@@ -53,9 +54,30 @@ class C:
5354
a:int=3
5455
b:str=4
5556
self.assertEqual(C.__annotations__, {"a": int, "b": str})
56-
self.assertTrue("__annotations__" in C.__dict__)
57+
self.assertTrue("__annotations_cache__" in C.__dict__)
5758
del C.__annotations__
58-
self.assertFalse("__annotations__" in C.__dict__)
59+
self.assertFalse("__annotations_cache__" in C.__dict__)
60+
61+
def test_pep563_annotations(self):
62+
isa = inspect_stringized_annotations
63+
self.assertEqual(
64+
isa.__annotations__, {"a": "int", "b": "str"},
65+
)
66+
self.assertEqual(
67+
isa.MyClass.__annotations__, {"a": "int", "b": "str"},
68+
)
69+
70+
def test_explicitly_set_annotations(self):
71+
class C:
72+
__annotations__ = {"what": int}
73+
self.assertEqual(C.__annotations__, {"what": int})
74+
75+
def test_explicitly_set_annotate(self):
76+
class C:
77+
__annotate__ = lambda format: {"what": int}
78+
self.assertEqual(C.__annotations__, {"what": int})
79+
self.assertIsInstance(C.__annotate__, types.FunctionType)
80+
self.assertEqual(C.__annotate__(annotationlib.Format.VALUE), {"what": int})
5981

6082
def test_del_annotations_and_annotate(self):
6183
# gh-132285

Lib/test/test_typing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3825,6 +3825,7 @@ def meth(self): pass
38253825
acceptable_extra_attrs = {
38263826
'_is_protocol', '_is_runtime_protocol', '__parameters__',
38273827
'__init__', '__annotations__', '__subclasshook__', '__annotate__',
3828+
'__annotations_cache__', '__annotate_func__',
38283829
}
38293830
self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs)
38303831
self.assertLessEqual(

Lib/typing.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1784,7 +1784,7 @@ class _TypingEllipsis:
17841784
'__init__', '__module__', '__new__', '__slots__',
17851785
'__subclasshook__', '__weakref__', '__class_getitem__',
17861786
'__match_args__', '__static_attributes__', '__firstlineno__',
1787-
'__annotate__',
1787+
'__annotate__', '__annotate_func__', '__annotations_cache__',
17881788
})
17891789

17901790
# These special attributes will be not collected as protocol members.
@@ -2875,7 +2875,8 @@ def annotate(format):
28752875
'_fields', '_field_defaults',
28762876
'_make', '_replace', '_asdict', '_source'})
28772877

2878-
_special = frozenset({'__module__', '__name__', '__annotations__', '__annotate__'})
2878+
_special = frozenset({'__module__', '__name__', '__annotations__', '__annotate__',
2879+
'__annotate_func__', '__annotations_cache__'})
28792880

28802881

28812882
class NamedTupleMeta(type):
@@ -2893,8 +2894,7 @@ def __new__(cls, typename, bases, ns):
28932894
types = ns["__annotations__"]
28942895
field_names = list(types)
28952896
annotate = _make_eager_annotate(types)
2896-
elif "__annotate__" in ns:
2897-
original_annotate = ns["__annotate__"]
2897+
elif (original_annotate := _lazy_annotationlib.get_annotate_function(ns)) is not None:
28982898
types = _lazy_annotationlib.call_annotate_function(
28992899
original_annotate, _lazy_annotationlib.Format.FORWARDREF)
29002900
field_names = list(types)
@@ -3080,8 +3080,7 @@ def __new__(cls, name, bases, ns, total=True):
30803080
if "__annotations__" in ns:
30813081
own_annotate = None
30823082
own_annotations = ns["__annotations__"]
3083-
elif "__annotate__" in ns:
3084-
own_annotate = ns["__annotate__"]
3083+
elif (own_annotate := _lazy_annotationlib.get_annotate_function(ns)) is not None:
30853084
own_annotations = _lazy_annotationlib.call_annotate_function(
30863085
own_annotate, _lazy_annotationlib.Format.FORWARDREF, owner=tp_dict
30873086
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The internal storage for annotations and annotate functions on classes now
2+
uses different keys in the class dictionary. This eliminates various edge
3+
cases where access to the ``__annotate__`` and ``__annotations__``
4+
attributes would behave unpredictably.

Objects/typeobject.c

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1915,10 +1915,17 @@ type_get_annotate(PyObject *tp, void *Py_UNUSED(closure))
19151915

19161916
PyObject *annotate;
19171917
PyObject *dict = PyType_GetDict(type);
1918+
// First try __annotate__, in case that's been set explicitly
19181919
if (PyDict_GetItemRef(dict, &_Py_ID(__annotate__), &annotate) < 0) {
19191920
Py_DECREF(dict);
19201921
return NULL;
19211922
}
1923+
if (!annotate) {
1924+
if (PyDict_GetItemRef(dict, &_Py_ID(__annotate_func__), &annotate) < 0) {
1925+
Py_DECREF(dict);
1926+
return NULL;
1927+
}
1928+
}
19221929
if (annotate) {
19231930
descrgetfunc get = Py_TYPE(annotate)->tp_descr_get;
19241931
if (get) {
@@ -1927,7 +1934,7 @@ type_get_annotate(PyObject *tp, void *Py_UNUSED(closure))
19271934
}
19281935
else {
19291936
annotate = Py_None;
1930-
int result = PyDict_SetItem(dict, &_Py_ID(__annotate__), annotate);
1937+
int result = PyDict_SetItem(dict, &_Py_ID(__annotate_func__), annotate);
19311938
if (result < 0) {
19321939
Py_DECREF(dict);
19331940
return NULL;
@@ -1959,13 +1966,13 @@ type_set_annotate(PyObject *tp, PyObject *value, void *Py_UNUSED(closure))
19591966

19601967
PyObject *dict = PyType_GetDict(type);
19611968
assert(PyDict_Check(dict));
1962-
int result = PyDict_SetItem(dict, &_Py_ID(__annotate__), value);
1969+
int result = PyDict_SetItem(dict, &_Py_ID(__annotate_func__), value);
19631970
if (result < 0) {
19641971
Py_DECREF(dict);
19651972
return -1;
19661973
}
19671974
if (!Py_IsNone(value)) {
1968-
if (PyDict_Pop(dict, &_Py_ID(__annotations__), NULL) == -1) {
1975+
if (PyDict_Pop(dict, &_Py_ID(__annotations_cache__), NULL) == -1) {
19691976
Py_DECREF(dict);
19701977
PyType_Modified(type);
19711978
return -1;
@@ -1987,18 +1994,26 @@ type_get_annotations(PyObject *tp, void *Py_UNUSED(closure))
19871994

19881995
PyObject *annotations;
19891996
PyObject *dict = PyType_GetDict(type);
1997+
// First try __annotations__ (e.g. for "from __future__ import annotations")
19901998
if (PyDict_GetItemRef(dict, &_Py_ID(__annotations__), &annotations) < 0) {
19911999
Py_DECREF(dict);
19922000
return NULL;
19932001
}
2002+
if (!annotations) {
2003+
if (PyDict_GetItemRef(dict, &_Py_ID(__annotations_cache__), &annotations) < 0) {
2004+
Py_DECREF(dict);
2005+
return NULL;
2006+
}
2007+
}
2008+
19942009
if (annotations) {
19952010
descrgetfunc get = Py_TYPE(annotations)->tp_descr_get;
19962011
if (get) {
19972012
Py_SETREF(annotations, get(annotations, NULL, tp));
19982013
}
19992014
}
20002015
else {
2001-
PyObject *annotate = type_get_annotate(tp, NULL);
2016+
PyObject *annotate = PyObject_GetAttrString((PyObject *)type, "__annotate__");
20022017
if (annotate == NULL) {
20032018
Py_DECREF(dict);
20042019
return NULL;
@@ -2026,7 +2041,7 @@ type_get_annotations(PyObject *tp, void *Py_UNUSED(closure))
20262041
Py_DECREF(annotate);
20272042
if (annotations) {
20282043
int result = PyDict_SetItem(
2029-
dict, &_Py_ID(__annotations__), annotations);
2044+
dict, &_Py_ID(__annotations_cache__), annotations);
20302045
if (result) {
20312046
Py_CLEAR(annotations);
20322047
} else {
@@ -2053,10 +2068,10 @@ type_set_annotations(PyObject *tp, PyObject *value, void *Py_UNUSED(closure))
20532068
PyObject *dict = PyType_GetDict(type);
20542069
if (value != NULL) {
20552070
/* set */
2056-
result = PyDict_SetItem(dict, &_Py_ID(__annotations__), value);
2071+
result = PyDict_SetItem(dict, &_Py_ID(__annotations_cache__), value);
20572072
} else {
20582073
/* delete */
2059-
result = PyDict_Pop(dict, &_Py_ID(__annotations__), NULL);
2074+
result = PyDict_Pop(dict, &_Py_ID(__annotations_cache__), NULL);
20602075
if (result == 0) {
20612076
PyErr_SetString(PyExc_AttributeError, "__annotations__");
20622077
Py_DECREF(dict);
@@ -2067,6 +2082,11 @@ type_set_annotations(PyObject *tp, PyObject *value, void *Py_UNUSED(closure))
20672082
Py_DECREF(dict);
20682083
return -1;
20692084
} else { // result can be 0 or 1
2085+
if (PyDict_Pop(dict, &_Py_ID(__annotate_func__), NULL) < 0) {
2086+
PyType_Modified(type);
2087+
Py_DECREF(dict);
2088+
return -1;
2089+
}
20702090
if (PyDict_Pop(dict, &_Py_ID(__annotate__), NULL) < 0) {
20712091
PyType_Modified(type);
20722092
Py_DECREF(dict);

0 commit comments

Comments
 (0)