]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-132261: Store annotations at hidden internal keys in the class dict (#132345)
authorJelle Zijlstra <jelle.zijlstra@gmail.com>
Fri, 11 Apr 2025 04:13:26 +0000 (21:13 -0700)
committerGitHub <noreply@github.com>
Fri, 11 Apr 2025 04:13:26 +0000 (21:13 -0700)
16 files changed:
Doc/library/annotationlib.rst
Include/internal/pycore_global_objects_fini_generated.h
Include/internal/pycore_global_strings.h
Include/internal/pycore_magic_number.h
Include/internal/pycore_runtime_init_generated.h
Include/internal/pycore_unicodeobject_generated.h
Lib/annotationlib.py
Lib/pydoc.py
Lib/test/test_ast/test_ast.py
Lib/test/test_pydoc/test_pydoc.py
Lib/test/test_type_annotations.py
Lib/test/test_typing.py
Lib/typing.py
Misc/NEWS.d/next/Core_and_Builtins/2025-04-09-21-51-37.gh-issue-132261.gL8thm.rst [new file with mode: 0644]
Objects/typeobject.c
Python/codegen.c

index e07081e3c5dd7afe88c8b77a071cbfad54c3b487..140e1aa12e2938db5f9e41cb936d3bc8eaf79c1e 100644 (file)
@@ -303,12 +303,12 @@ Functions
 .. function:: get_annotate_function(obj)
 
    Retrieve the :term:`annotate function` for *obj*. Return :const:`!None`
-   if *obj* does not have an annotate function.
+   if *obj* does not have an annotate function. *obj* may be a class, function,
+   module, or a namespace dictionary for a class. The last case is useful during
+   class creation, e.g. in the ``__new__`` method of a metaclass.
 
    This is usually equivalent to accessing the :attr:`~object.__annotate__`
-   attribute of *obj*, but direct access to the attribute may return the wrong
-   object in certain situations involving metaclasses. This function should be
-   used instead of accessing the attribute directly.
+   attribute of *obj*, but access through this public function is preferred.
 
    .. versionadded:: 3.14
 
index 410a3734f1a607c427e0e1cfd2a4aa5fca06a38e..5485d0bd64f3f109019b90cf2b9abafd270633e0 100644 (file)
@@ -587,7 +587,9 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__and__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__anext__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__annotate__));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__annotate_func__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__annotations__));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__annotations_cache__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__args__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__await__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__bases__));
index cadbc01b01de2624467609c9c8d3f82d397f539a..3ce192511e387947c5a11e780ce2507cd3d298bd 100644 (file)
@@ -78,7 +78,9 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(__and__)
         STRUCT_FOR_ID(__anext__)
         STRUCT_FOR_ID(__annotate__)
+        STRUCT_FOR_ID(__annotate_func__)
         STRUCT_FOR_ID(__annotations__)
+        STRUCT_FOR_ID(__annotations_cache__)
         STRUCT_FOR_ID(__args__)
         STRUCT_FOR_ID(__await__)
         STRUCT_FOR_ID(__bases__)
index 3fa2b714cb6f23c4a894fbbb77985bde35122f2d..f75b05893affc15acf383becb64e7bd8e0ef48bc 100644 (file)
@@ -274,6 +274,7 @@ Known values:
     Python 3.14a6 3619 (Renumber RESUME opcode from 149 to 128)
     Python 3.14a6 3620 (Optimize bytecode for all/any/tuple called on a genexp)
     Python 3.14a7 3621 (Optimize LOAD_FAST opcodes into LOAD_FAST_BORROW)
+    Python 3.14a7 3622 (Store annotations in different class dict keys)
 
     Python 3.15 will start with 3650
 
@@ -286,7 +287,7 @@ PC/launcher.c must also be updated.
 
 */
 
-#define PYC_MAGIC_NUMBER 3621
+#define PYC_MAGIC_NUMBER 3622
 /* This is equivalent to converting PYC_MAGIC_NUMBER to 2 bytes
    (little-endian) and then appending b'\r\n'. */
 #define PYC_MAGIC_NUMBER_TOKEN \
index 07a74dd26cd11ff8d5948e6a7355d7bb04605ab8..5c95d0feddecbaa7d5ad14c51406d7778d419cf7 100644 (file)
@@ -585,7 +585,9 @@ extern "C" {
     INIT_ID(__and__), \
     INIT_ID(__anext__), \
     INIT_ID(__annotate__), \
+    INIT_ID(__annotate_func__), \
     INIT_ID(__annotations__), \
+    INIT_ID(__annotations_cache__), \
     INIT_ID(__args__), \
     INIT_ID(__await__), \
     INIT_ID(__bases__), \
index 1e1e32bbd42eedd637ac592f2d17ce7440548ab4..a1fc9736d66618730bd4b13776a45959d6a02172 100644 (file)
@@ -100,10 +100,18 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
     assert(PyUnicode_GET_LENGTH(string) != 1);
+    string = &_Py_ID(__annotate_func__);
+    _PyUnicode_InternStatic(interp, &string);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    assert(PyUnicode_GET_LENGTH(string) != 1);
     string = &_Py_ID(__annotations__);
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
     assert(PyUnicode_GET_LENGTH(string) != 1);
+    string = &_Py_ID(__annotations_cache__);
+    _PyUnicode_InternStatic(interp, &string);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    assert(PyUnicode_GET_LENGTH(string) != 1);
     string = &_Py_ID(__args__);
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
index d6243c8863610ef1a1675ca7ec4d2b39ce230ce4..237b3470b831fdf9a5fcb61a551df82c95db4d65 100644 (file)
@@ -619,14 +619,6 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
         raise ValueError(f"Invalid format: {format!r}")
 
 
-# We use the descriptors from builtins.type instead of accessing
-# .__annotations__ and .__annotate__ directly on class objects, because
-# otherwise we could get wrong results in some cases involving metaclasses.
-# See PEP 749.
-_BASE_GET_ANNOTATE = type.__dict__["__annotate__"].__get__
-_BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__
-
-
 def get_annotate_function(obj):
     """Get the __annotate__ function for an object.
 
@@ -635,12 +627,11 @@ def get_annotate_function(obj):
 
     Returns the __annotate__ function or None.
     """
-    if isinstance(obj, type):
+    if isinstance(obj, dict):
         try:
-            return _BASE_GET_ANNOTATE(obj)
-        except AttributeError:
-            # AttributeError is raised for static types.
-            return None
+            return obj["__annotate__"]
+        except KeyError:
+            return obj.get("__annotate_func__", None)
     return getattr(obj, "__annotate__", None)
 
 
@@ -833,7 +824,7 @@ def _get_and_call_annotate(obj, format):
 def _get_dunder_annotations(obj):
     if isinstance(obj, type):
         try:
-            ann = _BASE_GET_ANNOTATIONS(obj)
+            ann = obj.__annotations__
         except AttributeError:
             # For static types, the descriptor raises AttributeError.
             return {}
index 1839b88fec28b139c2a37466fe47838ddb13cee3..169194b99cb826a91e7f707d8a6eaa9995054eb1 100644 (file)
@@ -330,7 +330,8 @@ 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__',
+                '__annotate_func__', '__annotations_cache__'}:
         return 0
     # Private names are hidden, but special names are displayed.
     if name.startswith('__') and name.endswith('__'): return 1
index 090544726c31a41a0e418cfe4cd4bb4044f9cd6b..dd459487afef1c5533990954a3069b43afc7ac3a 100644 (file)
@@ -298,7 +298,7 @@ class AST_Tests(unittest.TestCase):
         x = ast.arguments()
         self.assertEqual(x._fields, ('posonlyargs', 'args', 'vararg', 'kwonlyargs',
                                      'kw_defaults', 'kwarg', 'defaults'))
-        self.assertEqual(x.__annotations__, {
+        self.assertEqual(ast.arguments.__annotations__, {
             'posonlyargs': list[ast.arg],
             'args': list[ast.arg],
             'vararg': ast.arg | None,
index 2b1a4484c680fc7353055f95bf5aa64aeec49d42..8cb253f67ea2ebe6f3d4eb3d8a856b286f88f9ba 100644 (file)
@@ -78,11 +78,6 @@ CLASSES
      |  __weakref__%s
 
     class B(builtins.object)
-     |  Methods defined here:
-     |
-     |  __annotate__(format, /)
-     |
-     |  ----------------------------------------------------------------------
      |  Data descriptors defined here:
      |
      |  __dict__%s
@@ -180,9 +175,6 @@ class A(builtins.object)
             list of weak references to the object
 
 class B(builtins.object)
-    Methods defined here:
-        __annotate__(format, /)
-    ----------------------------------------------------------------------
     Data descriptors defined here:
         __dict__
             dictionary for instance variables
index 29e2c7a0cd837e27ae8e4dc241746e1053fbd777..31df7668db0976bafdf2b5c1f793376cf35c82fa 100644 (file)
@@ -4,32 +4,33 @@ import textwrap
 import types
 import unittest
 from test.support import run_code, check_syntax_error, cpython_only
+from test.test_inspect import inspect_stringized_annotations
 
 
 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__.
+        # the annotations dict is stored in type.__dict__ (as __annotations_cache__).
         # 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__)
+            self.assertFalse("__annotations_cache__" in foo.__dict__)
             d = foo.__annotations__
-            self.assertTrue("__annotations__" in foo.__dict__)
+            self.assertTrue("__annotations_cache__" in foo.__dict__)
             self.assertEqual(foo.__annotations__, d)
-            self.assertEqual(foo.__dict__['__annotations__'], d)
+            self.assertEqual(foo.__dict__['__annotations_cache__'], d)
             del foo.__annotations__
 
     def test_setting_annotations(self):
         foo = type("Foo", (), {})
         for i in range(3):
-            self.assertFalse("__annotations__" in foo.__dict__)
+            self.assertFalse("__annotations_cache__" in foo.__dict__)
             d = {'a': int}
             foo.__annotations__ = d
-            self.assertTrue("__annotations__" in foo.__dict__)
+            self.assertTrue("__annotations_cache__" in foo.__dict__)
             self.assertEqual(foo.__annotations__, d)
-            self.assertEqual(foo.__dict__['__annotations__'], d)
+            self.assertEqual(foo.__dict__['__annotations_cache__'], d)
             del foo.__annotations__
 
     def test_annotations_getset_raises(self):
@@ -53,9 +54,30 @@ class TypeAnnotationTests(unittest.TestCase):
             a:int=3
             b:str=4
         self.assertEqual(C.__annotations__, {"a": int, "b": str})
-        self.assertTrue("__annotations__" in C.__dict__)
+        self.assertTrue("__annotations_cache__" in C.__dict__)
         del C.__annotations__
-        self.assertFalse("__annotations__" in C.__dict__)
+        self.assertFalse("__annotations_cache__" in C.__dict__)
+
+    def test_pep563_annotations(self):
+        isa = inspect_stringized_annotations
+        self.assertEqual(
+            isa.__annotations__, {"a": "int", "b": "str"},
+        )
+        self.assertEqual(
+            isa.MyClass.__annotations__, {"a": "int", "b": "str"},
+        )
+
+    def test_explicitly_set_annotations(self):
+        class C:
+            __annotations__ = {"what": int}
+        self.assertEqual(C.__annotations__, {"what": int})
+
+    def test_explicitly_set_annotate(self):
+        class C:
+            __annotate__ = lambda format: {"what": int}
+        self.assertEqual(C.__annotations__, {"what": int})
+        self.assertIsInstance(C.__annotate__, types.FunctionType)
+        self.assertEqual(C.__annotate__(annotationlib.Format.VALUE), {"what": int})
 
     def test_del_annotations_and_annotate(self):
         # gh-132285
index edf3cf9d4a3658920b1afda4da8aae56b95582d5..9f9e3eb17b9fc9ca775e584a510f5e6b05a3279b 100644 (file)
@@ -3825,6 +3825,7 @@ class ProtocolTests(BaseTestCase):
         acceptable_extra_attrs = {
             '_is_protocol', '_is_runtime_protocol', '__parameters__',
             '__init__', '__annotations__', '__subclasshook__', '__annotate__',
+            '__annotations_cache__', '__annotate_func__',
         }
         self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs)
         self.assertLessEqual(
index e67284fc571b1b8620a6a4030bc29333cabc5b65..e5d14b03a4fc94b8cc258fc13c12fa858677cb1e 100644 (file)
@@ -1784,7 +1784,7 @@ _SPECIAL_NAMES = frozenset({
     '__init__', '__module__', '__new__', '__slots__',
     '__subclasshook__', '__weakref__', '__class_getitem__',
     '__match_args__', '__static_attributes__', '__firstlineno__',
-    '__annotate__',
+    '__annotate__', '__annotate_func__', '__annotations_cache__',
 })
 
 # These special attributes will be not collected as protocol members.
@@ -2875,7 +2875,8 @@ _prohibited = frozenset({'__new__', '__init__', '__slots__', '__getnewargs__',
                          '_fields', '_field_defaults',
                          '_make', '_replace', '_asdict', '_source'})
 
-_special = frozenset({'__module__', '__name__', '__annotations__', '__annotate__'})
+_special = frozenset({'__module__', '__name__', '__annotations__', '__annotate__',
+                      '__annotate_func__', '__annotations_cache__'})
 
 
 class NamedTupleMeta(type):
@@ -2893,8 +2894,7 @@ class NamedTupleMeta(type):
             types = ns["__annotations__"]
             field_names = list(types)
             annotate = _make_eager_annotate(types)
-        elif "__annotate__" in ns:
-            original_annotate = ns["__annotate__"]
+        elif (original_annotate := _lazy_annotationlib.get_annotate_function(ns)) is not None:
             types = _lazy_annotationlib.call_annotate_function(
                 original_annotate, _lazy_annotationlib.Format.FORWARDREF)
             field_names = list(types)
@@ -3080,8 +3080,7 @@ class _TypedDictMeta(type):
         if "__annotations__" in ns:
             own_annotate = None
             own_annotations = ns["__annotations__"]
-        elif "__annotate__" in ns:
-            own_annotate = ns["__annotate__"]
+        elif (own_annotate := _lazy_annotationlib.get_annotate_function(ns)) is not None:
             own_annotations = _lazy_annotationlib.call_annotate_function(
                 own_annotate, _lazy_annotationlib.Format.FORWARDREF, owner=tp_dict
             )
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-09-21-51-37.gh-issue-132261.gL8thm.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-09-21-51-37.gh-issue-132261.gL8thm.rst
new file mode 100644 (file)
index 0000000..0e58cf7
--- /dev/null
@@ -0,0 +1,4 @@
+The internal storage for annotations and annotate functions on classes now
+uses different keys in the class dictionary. This eliminates various edge
+cases where access to the ``__annotate__`` and ``__annotations__``
+attributes would behave unpredictably.
index 75c23ddd91b1a1e9a07ec17812e75a160131ddd0..b817ae6e68438b57b92f4a4e0718ad196e6e90e2 100644 (file)
@@ -1915,10 +1915,17 @@ type_get_annotate(PyObject *tp, void *Py_UNUSED(closure))
 
     PyObject *annotate;
     PyObject *dict = PyType_GetDict(type);
+    // First try __annotate__, in case that's been set explicitly
     if (PyDict_GetItemRef(dict, &_Py_ID(__annotate__), &annotate) < 0) {
         Py_DECREF(dict);
         return NULL;
     }
+    if (!annotate) {
+        if (PyDict_GetItemRef(dict, &_Py_ID(__annotate_func__), &annotate) < 0) {
+            Py_DECREF(dict);
+            return NULL;
+        }
+    }
     if (annotate) {
         descrgetfunc get = Py_TYPE(annotate)->tp_descr_get;
         if (get) {
@@ -1927,7 +1934,7 @@ type_get_annotate(PyObject *tp, void *Py_UNUSED(closure))
     }
     else {
         annotate = Py_None;
-        int result = PyDict_SetItem(dict, &_Py_ID(__annotate__), annotate);
+        int result = PyDict_SetItem(dict, &_Py_ID(__annotate_func__), annotate);
         if (result < 0) {
             Py_DECREF(dict);
             return NULL;
@@ -1959,13 +1966,13 @@ type_set_annotate(PyObject *tp, PyObject *value, void *Py_UNUSED(closure))
 
     PyObject *dict = PyType_GetDict(type);
     assert(PyDict_Check(dict));
-    int result = PyDict_SetItem(dict, &_Py_ID(__annotate__), value);
+    int result = PyDict_SetItem(dict, &_Py_ID(__annotate_func__), value);
     if (result < 0) {
         Py_DECREF(dict);
         return -1;
     }
     if (!Py_IsNone(value)) {
-        if (PyDict_Pop(dict, &_Py_ID(__annotations__), NULL) == -1) {
+        if (PyDict_Pop(dict, &_Py_ID(__annotations_cache__), NULL) == -1) {
             Py_DECREF(dict);
             PyType_Modified(type);
             return -1;
@@ -1987,10 +1994,18 @@ type_get_annotations(PyObject *tp, void *Py_UNUSED(closure))
 
     PyObject *annotations;
     PyObject *dict = PyType_GetDict(type);
+    // First try __annotations__ (e.g. for "from __future__ import annotations")
     if (PyDict_GetItemRef(dict, &_Py_ID(__annotations__), &annotations) < 0) {
         Py_DECREF(dict);
         return NULL;
     }
+    if (!annotations) {
+        if (PyDict_GetItemRef(dict, &_Py_ID(__annotations_cache__), &annotations) < 0) {
+            Py_DECREF(dict);
+            return NULL;
+        }
+    }
+
     if (annotations) {
         descrgetfunc get = Py_TYPE(annotations)->tp_descr_get;
         if (get) {
@@ -1998,7 +2013,7 @@ type_get_annotations(PyObject *tp, void *Py_UNUSED(closure))
         }
     }
     else {
-        PyObject *annotate = type_get_annotate(tp, NULL);
+        PyObject *annotate = PyObject_GetAttrString((PyObject *)type, "__annotate__");
         if (annotate == NULL) {
             Py_DECREF(dict);
             return NULL;
@@ -2026,7 +2041,7 @@ type_get_annotations(PyObject *tp, void *Py_UNUSED(closure))
         Py_DECREF(annotate);
         if (annotations) {
             int result = PyDict_SetItem(
-                    dict, &_Py_ID(__annotations__), annotations);
+                    dict, &_Py_ID(__annotations_cache__), annotations);
             if (result) {
                 Py_CLEAR(annotations);
             } else {
@@ -2053,10 +2068,10 @@ type_set_annotations(PyObject *tp, PyObject *value, void *Py_UNUSED(closure))
     PyObject *dict = PyType_GetDict(type);
     if (value != NULL) {
         /* set */
-        result = PyDict_SetItem(dict, &_Py_ID(__annotations__), value);
+        result = PyDict_SetItem(dict, &_Py_ID(__annotations_cache__), value);
     } else {
         /* delete */
-        result = PyDict_Pop(dict, &_Py_ID(__annotations__), NULL);
+        result = PyDict_Pop(dict, &_Py_ID(__annotations_cache__), NULL);
         if (result == 0) {
             PyErr_SetString(PyExc_AttributeError, "__annotations__");
             Py_DECREF(dict);
@@ -2067,6 +2082,11 @@ type_set_annotations(PyObject *tp, PyObject *value, void *Py_UNUSED(closure))
         Py_DECREF(dict);
         return -1;
     } else {  // result can be 0 or 1
+        if (PyDict_Pop(dict, &_Py_ID(__annotate_func__), NULL) < 0) {
+            PyType_Modified(type);
+            Py_DECREF(dict);
+            return -1;
+        }
         if (PyDict_Pop(dict, &_Py_ID(__annotate__), NULL) < 0) {
             PyType_Modified(type);
             Py_DECREF(dict);
index dc50737840f002059b8ba8a3ed85bbd74f9ab789..379d37c65ca8e6695c196bcab62f83153e58041a 100644 (file)
@@ -815,7 +815,10 @@ codegen_process_deferred_annotations(compiler *c, location loc)
     Py_DECREF(conditional_annotation_indices);
 
     RETURN_IF_ERROR(codegen_leave_annotations_scope(c, loc));
-    RETURN_IF_ERROR(codegen_nameop(c, loc, &_Py_ID(__annotate__), Store));
+    RETURN_IF_ERROR(codegen_nameop(
+        c, loc,
+        ste->ste_type == ClassBlock ? &_Py_ID(__annotate_func__) : &_Py_ID(__annotate__),
+        Store));
 
     return SUCCESS;
 error: