https://github.com/python/cpython/commit/07b8d3117fdbc4e5be55aab0be428c278ec84e12 commit: 07b8d3117fdbc4e5be55aab0be428c278ec84e12 branch: main author: Jelle Zijlstra <jelle.zijls...@gmail.com> committer: JelleZijlstra <jelle.zijls...@gmail.com> date: 2025-04-10T21:13:26-07:00 summary:
gh-132261: Store annotations at hidden internal keys in the class dict (#132345) files: A Misc/NEWS.d/next/Core_and_Builtins/2025-04-09-21-51-37.gh-issue-132261.gL8thm.rst M Doc/library/annotationlib.rst M Include/internal/pycore_global_objects_fini_generated.h M Include/internal/pycore_global_strings.h M Include/internal/pycore_magic_number.h M Include/internal/pycore_runtime_init_generated.h M Include/internal/pycore_unicodeobject_generated.h M Lib/annotationlib.py M Lib/pydoc.py M Lib/test/test_ast/test_ast.py M Lib/test/test_pydoc/test_pydoc.py M Lib/test/test_type_annotations.py M Lib/test/test_typing.py M Lib/typing.py M Objects/typeobject.c M Python/codegen.c diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index e07081e3c5dd7a..140e1aa12e2938 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -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 diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 410a3734f1a607..5485d0bd64f3f1 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -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__)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index cadbc01b01de26..3ce192511e3879 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -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__) diff --git a/Include/internal/pycore_magic_number.h b/Include/internal/pycore_magic_number.h index 3fa2b714cb6f23..f75b05893affc1 100644 --- a/Include/internal/pycore_magic_number.h +++ b/Include/internal/pycore_magic_number.h @@ -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 \ diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 07a74dd26cd11f..5c95d0feddecba 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -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__), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 1e1e32bbd42eed..a1fc9736d66618 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -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)); diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index d6243c8863610e..237b3470b831fd 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -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 {} diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 1839b88fec28b1..169194b99cb826 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -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 diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 090544726c31a4..dd459487afef1c 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -298,7 +298,7 @@ def test_arguments(self): 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, diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 2b1a4484c680fc..8cb253f67ea2eb 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -78,11 +78,6 @@ class A(builtins.object) | __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 diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 29e2c7a0cd837e..31df7668db0976 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -4,32 +4,33 @@ 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 C: 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 diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index edf3cf9d4a3658..9f9e3eb17b9fc9 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -3825,6 +3825,7 @@ def meth(self): pass 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( diff --git a/Lib/typing.py b/Lib/typing.py index e67284fc571b1b..e5d14b03a4fc94 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1784,7 +1784,7 @@ class _TypingEllipsis: '__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 @@ def annotate(format): '_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 @@ def __new__(cls, typename, bases, ns): 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 @@ def __new__(cls, name, bases, ns, total=True): 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 index 00000000000000..0e58cf7f957dfd --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-09-21-51-37.gh-issue-132261.gL8thm.rst @@ -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. diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 75c23ddd91b1a1..b817ae6e68438b 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -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); diff --git a/Python/codegen.c b/Python/codegen.c index dc50737840f002..379d37c65ca8e6 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -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: _______________________________________________ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com