From 985f8df0801d7cacbe0c7ab274a8e2e4aca6d50d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 May 2024 09:59:42 -0400 Subject: [PATCH 001/105] Add __annotate__ descriptors --- Include/cpython/funcobject.h | 1 + .../pycore_global_objects_fini_generated.h | 2 + Include/internal/pycore_global_strings.h | 2 + .../internal/pycore_runtime_init_generated.h | 2 + .../internal/pycore_unicodeobject_generated.h | 6 ++ Lib/test/test_type_annotations.py | 44 ++++++++ Objects/funcobject.c | 69 +++++++++++- Objects/moduleobject.c | 101 ++++++++++++++++-- Objects/typeobject.c | 93 +++++++++++++++- 9 files changed, 305 insertions(+), 15 deletions(-) diff --git a/Include/cpython/funcobject.h b/Include/cpython/funcobject.h index 5433ba48eefc69..598cd330bc9ca9 100644 --- a/Include/cpython/funcobject.h +++ b/Include/cpython/funcobject.h @@ -41,6 +41,7 @@ typedef struct { PyObject *func_weakreflist; /* List of weak references */ PyObject *func_module; /* The __module__ attribute, can be anything */ PyObject *func_annotations; /* Annotations, a dict or NULL */ + PyObject *func_annotate; /* Callable to fill the annotations dictionary */ PyObject *func_typeparams; /* Tuple of active type variables or NULL */ vectorcallfunc vectorcall; /* Version number for use by specializer. diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index ca7355b2b61aa7..158584b504e465 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -590,6 +590,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__all__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__and__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__anext__)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__annotate__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__annotations__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__args__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__asyncio_running_event_loop__)); @@ -989,6 +990,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(hi)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(hook)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(hour)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(id)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ident)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(identity_hint)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ignore)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index fbb25285f0f282..769aa88b54c710 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -79,6 +79,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(__all__) STRUCT_FOR_ID(__and__) STRUCT_FOR_ID(__anext__) + STRUCT_FOR_ID(__annotate__) STRUCT_FOR_ID(__annotations__) STRUCT_FOR_ID(__args__) STRUCT_FOR_ID(__asyncio_running_event_loop__) @@ -478,6 +479,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(hi) STRUCT_FOR_ID(hook) STRUCT_FOR_ID(hour) + STRUCT_FOR_ID(id) STRUCT_FOR_ID(ident) STRUCT_FOR_ID(identity_hint) STRUCT_FOR_ID(ignore) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 508da40c53422d..065b18efd7922c 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -588,6 +588,7 @@ extern "C" { INIT_ID(__all__), \ INIT_ID(__and__), \ INIT_ID(__anext__), \ + INIT_ID(__annotate__), \ INIT_ID(__annotations__), \ INIT_ID(__args__), \ INIT_ID(__asyncio_running_event_loop__), \ @@ -987,6 +988,7 @@ extern "C" { INIT_ID(hi), \ INIT_ID(hook), \ INIT_ID(hour), \ + INIT_ID(id), \ INIT_ID(ident), \ INIT_ID(identity_hint), \ INIT_ID(ignore), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index cc2fc15ac5cabf..334dcce03aaf49 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -78,6 +78,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { string = &_Py_ID(__anext__); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); + string = &_Py_ID(__annotate__); + assert(_PyUnicode_CheckConsistency(string, 1)); + _PyUnicode_InternInPlace(interp, &string); string = &_Py_ID(__annotations__); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); @@ -1275,6 +1278,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { string = &_Py_ID(hour); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); + string = &_Py_ID(id); + assert(_PyUnicode_CheckConsistency(string, 1)); + _PyUnicode_InternInPlace(interp, &string); string = &_Py_ID(ident); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 3dbb35afcb620f..55053df5710844 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -1,4 +1,5 @@ import textwrap +import types import unittest from test.support import run_code @@ -212,3 +213,46 @@ def test_match(self): case 0: x: int = 1 """) + + +class AnnotateTests(unittest.TestCase): + """See PEP 649.""" + def test_manual_annotate_function(self): + def f(): + pass + mod = types.ModuleType("mod") + class X: + pass + + for obj in (f, mod, X): + with self.subTest(obj=obj): + self.check_annotations(obj) + + def check_annotations(self, f): + self.assertEqual(f.__annotations__, {}) + self.assertIs(f.__annotate__, None) + + with self.assertRaises(TypeError): + f.__annotate__ = 42 + f.__annotate__ = lambda: 42 + with self.assertRaisesRegex(TypeError, r"takes 0 positional arguments but 1 was given"): + print(f.__annotations__) + + f.__annotate__ = lambda x: 42 + with self.assertRaisesRegex(TypeError, r"__annotate__ returned a non-dict"): + print(f.__annotations__) + + f.__annotate__ = lambda x: {"x": x} + self.assertEqual(f.__annotations__, {"x": 1}) + + # Setting annotate to None does not invalidate the cached __annotations__ + f.__annotate__ = None + self.assertEqual(f.__annotations__, {"x": 1}) + + # But setting it to a new callable does + f.__annotate__ = lambda x: {"y": x} + self.assertEqual(f.__annotations__, {"y": 1}) + + # Setting f.__annotations__ also clears __annotate__ + f.__annotations__ = {"z": 43} + self.assertIs(f.__annotate__, None) diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 8a30213888ef87..72d74516cc4f53 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -3,6 +3,7 @@ #include "Python.h" #include "pycore_ceval.h" // _PyEval_BuiltinsFromGlobals() +#include "pycore_long.h" // _PyLong_GetOne() #include "pycore_modsupport.h" // _PyArg_NoKeywords() #include "pycore_object.h" // _PyObject_GC_UNTRACK() #include "pycore_pyerrors.h" // _PyErr_Occurred() @@ -124,6 +125,7 @@ _PyFunction_FromConstructor(PyFrameConstructor *constr) op->func_weakreflist = NULL; op->func_module = module; op->func_annotations = NULL; + op->func_annotate = NULL; op->func_typeparams = NULL; op->vectorcall = _PyFunction_Vectorcall; op->func_version = 0; @@ -202,6 +204,7 @@ PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname op->func_weakreflist = NULL; op->func_module = module; op->func_annotations = NULL; + op->func_annotate = NULL; op->func_typeparams = NULL; op->vectorcall = _PyFunction_Vectorcall; op->func_version = 0; @@ -512,7 +515,27 @@ static PyObject * func_get_annotation_dict(PyFunctionObject *op) { if (op->func_annotations == NULL) { - return NULL; + if (op->func_annotate == NULL || !PyCallable_Check(op->func_annotate)) { + return NULL; + } + PyObject *one = _PyLong_GetOne(); + PyObject *ann_dict = _PyObject_CallOneArg(op->func_annotate, one); + if (ann_dict == NULL) { + return NULL; + } + if (op->func_annotations != NULL) { + Py_DECREF(ann_dict); + assert(PyDict_Check(op->func_annotations)); + return op->func_annotations; + } + if (!PyDict_Check(ann_dict)) { + PyErr_SetString(PyExc_TypeError, + "__annotate__ returned a non-dict"); + Py_DECREF(ann_dict); + return NULL; + } + Py_XSETREF(op->func_annotations, ann_dict); + return ann_dict; } if (PyTuple_CheckExact(op->func_annotations)) { PyObject *ann_tuple = op->func_annotations; @@ -565,7 +588,9 @@ PyFunction_SetAnnotations(PyObject *op, PyObject *annotations) "non-dict annotations"); return -1; } - Py_XSETREF(((PyFunctionObject *)op)->func_annotations, annotations); + PyFunctionObject *func = (PyFunctionObject *)op; + Py_XSETREF(func->func_annotations, annotations); + Py_CLEAR(func->func_annotate); return 0; } @@ -763,10 +788,44 @@ func_set_kwdefaults(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(ignor return 0; } +static PyObject * +func_get_annotate(PyFunctionObject *op, void *Py_UNUSED(ignored)) +{ + if (op->func_annotate == NULL) { + Py_RETURN_NONE; + } + return Py_NewRef(op->func_annotate); +} + +static int +func_set_annotate(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(ignored)) +{ + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, + "__annotate__ cannot be deleted"); + return -1; + } + if (Py_IsNone(value)) { + Py_XSETREF(op->func_annotate, value); + return 0; + } + else if (PyCallable_Check(value)) { + Py_XSETREF(op->func_annotate, Py_XNewRef(value)); + Py_CLEAR(op->func_annotations); + return 0; + } + else { + PyErr_SetString(PyExc_TypeError, + "__annotate__ must be set to a callable object or None"); + return -1; + } +} + static PyObject * func_get_annotations(PyFunctionObject *op, void *Py_UNUSED(ignored)) { - if (op->func_annotations == NULL) { + if (op->func_annotations == NULL && + (op->func_annotate == NULL || !PyCallable_Check(op->func_annotate))) { op->func_annotations = PyDict_New(); if (op->func_annotations == NULL) return NULL; @@ -789,6 +848,7 @@ func_set_annotations(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(igno return -1; } Py_XSETREF(op->func_annotations, Py_XNewRef(value)); + Py_CLEAR(op->func_annotate); return 0; } @@ -836,6 +896,7 @@ static PyGetSetDef func_getsetlist[] = { (setter)func_set_kwdefaults}, {"__annotations__", (getter)func_get_annotations, (setter)func_set_annotations}, + {"__annotate__", (getter)func_get_annotate, (setter)func_set_annotate}, {"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict}, {"__name__", (getter)func_get_name, (setter)func_set_name}, {"__qualname__", (getter)func_get_qualname, (setter)func_set_qualname}, @@ -972,6 +1033,7 @@ func_clear(PyFunctionObject *op) Py_CLEAR(op->func_dict); Py_CLEAR(op->func_closure); Py_CLEAR(op->func_annotations); + Py_CLEAR(op->func_annotate); Py_CLEAR(op->func_typeparams); // Don't Py_CLEAR(op->func_code), since code is always required // to be non-NULL. Similarly, name and qualname shouldn't be NULL. @@ -1028,6 +1090,7 @@ func_traverse(PyFunctionObject *f, visitproc visit, void *arg) Py_VISIT(f->func_dict); Py_VISIT(f->func_closure); Py_VISIT(f->func_annotations); + Py_VISIT(f->func_annotate); Py_VISIT(f->func_typeparams); Py_VISIT(f->func_qualname); return 0; diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 46995b948a28e7..3b378482be72f8 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -5,6 +5,7 @@ #include "pycore_call.h" // _PyObject_CallNoArgs() #include "pycore_fileutils.h" // _Py_wgetcwd #include "pycore_interp.h" // PyInterpreterState.importlib +#include "pycore_long.h" // _PyLong_GetOne() #include "pycore_modsupport.h" // _PyModule_CreateInitialized() #include "pycore_moduleobject.h" // _PyModule_GetDef() #include "pycore_object.h" // _PyType_AllocNoTrack @@ -1133,7 +1134,7 @@ static PyMethodDef module_methods[] = { }; static PyObject * -module_get_annotations(PyModuleObject *m, void *Py_UNUSED(ignored)) +module_get_dict(PyModuleObject *m) { PyObject *dict = PyObject_GetAttr((PyObject *)m, &_Py_ID(__dict__)); if (dict == NULL) { @@ -1144,10 +1145,93 @@ module_get_annotations(PyModuleObject *m, void *Py_UNUSED(ignored)) Py_DECREF(dict); return NULL; } + return dict; +} + +static PyObject * +module_get_annotate(PyModuleObject *m, void *Py_UNUSED(ignored)) +{ + PyObject *dict = module_get_dict(m); + if (dict == NULL) { + return NULL; + } + + PyObject *annotate; + if (PyDict_GetItemRef(dict, &_Py_ID(__annotate__), &annotate) == 0) { + annotate = Py_None; + int result = PyDict_SetItem(dict, &_Py_ID(__annotate__), annotate); + if (result == -1) { + Py_CLEAR(annotate); + } + } + Py_DECREF(dict); + return annotate; +} + +static int +module_set_annotate(PyModuleObject *m, PyObject *value, void *Py_UNUSED(ignored)) +{ + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, "cannot delete __annotate__ attribute"); + return -1; + } + PyObject *dict = module_get_dict(m); + if (dict == NULL) { + return -1; + } + + if (!Py_IsNone(value) && !PyCallable_Check(value)) { + PyErr_SetString(PyExc_TypeError, "__annotate__ must be callable or None"); + Py_DECREF(dict); + return -1; + } + + if (PyDict_SetItem(dict, &_Py_ID(__annotate__), value) == -1) { + Py_DECREF(dict); + return -1; + } + if (!Py_IsNone(value)) { + if (PyDict_Pop(dict, &_Py_ID(__annotations__), NULL) == -1) { + Py_DECREF(dict); + return -1; + } + } + Py_DECREF(dict); + return 0; +} + +static PyObject * +module_get_annotations(PyModuleObject *m, void *Py_UNUSED(ignored)) +{ + PyObject *dict = module_get_dict(m); + if (dict == NULL) { + return NULL; + } PyObject *annotations; if (PyDict_GetItemRef(dict, &_Py_ID(__annotations__), &annotations) == 0) { - annotations = PyDict_New(); + PyObject *annotate; + int annotate_result = PyDict_GetItemRef(dict, &_Py_ID(__annotate__), &annotate); + if (annotate_result < 0) { + Py_DECREF(dict); + return NULL; + } + if (annotate_result == 1 && PyCallable_Check(annotate)) { + PyObject *one = _PyLong_GetOne(); + annotations = _PyObject_CallOneArg(annotate, one); + if (annotations == NULL) { + return NULL; + } + if (!PyDict_Check(annotations)) { + PyErr_SetString(PyExc_TypeError, "__annotate__ returned a non-dict"); + Py_DECREF(annotations); + Py_DECREF(dict); + return NULL; + } + } + else { + annotations = PyDict_New(); + } if (annotations) { int result = PyDict_SetItem( dict, &_Py_ID(__annotations__), annotations); @@ -1164,14 +1248,10 @@ static int module_set_annotations(PyModuleObject *m, PyObject *value, void *Py_UNUSED(ignored)) { int ret = -1; - PyObject *dict = PyObject_GetAttr((PyObject *)m, &_Py_ID(__dict__)); + PyObject *dict = module_get_dict(m); if (dict == NULL) { return -1; } - if (!PyDict_Check(dict)) { - PyErr_Format(PyExc_TypeError, ".__dict__ is not a dictionary"); - goto exit; - } if (value != NULL) { /* set */ @@ -1188,8 +1268,12 @@ module_set_annotations(PyModuleObject *m, PyObject *value, void *Py_UNUSED(ignor ret = 0; } } + if (ret == 0) { + if (PyDict_Pop(dict, &_Py_ID(__annotate__), NULL) < 0) { + ret = -1; + } + } -exit: Py_DECREF(dict); return ret; } @@ -1197,6 +1281,7 @@ module_set_annotations(PyModuleObject *m, PyObject *value, void *Py_UNUSED(ignor static PyGetSetDef module_getsets[] = { {"__annotations__", (getter)module_get_annotations, (setter)module_set_annotations}, + {"__annotate__", (getter)module_get_annotate, (setter)module_set_annotate}, {NULL} }; diff --git a/Objects/typeobject.c b/Objects/typeobject.c index b7c3fcf47f23fc..82bd707c8b55cf 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -7,7 +7,7 @@ #include "pycore_dict.h" // _PyDict_KeysSize() #include "pycore_frame.h" // _PyInterpreterFrame #include "pycore_lock.h" // _PySeqLock_* -#include "pycore_long.h" // _PyLong_IsNegative() +#include "pycore_long.h" // _PyLong_IsNegative(), _PyLong_GetOne() #include "pycore_memoryobject.h" // _PyMemoryView_FromBufferProc() #include "pycore_modsupport.h" // _PyArg_NoKwnames() #include "pycore_moduleobject.h" // _PyModule_GetDef() @@ -1674,6 +1674,68 @@ type_set_doc(PyTypeObject *type, PyObject *value, void *context) return PyDict_SetItem(dict, &_Py_ID(__doc__), value); } +static PyObject * +type_get_annotate(PyTypeObject *type, void *Py_UNUSED(ignored)) +{ + if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) { + PyErr_Format(PyExc_AttributeError, "type object '%s' has no attribute '__annotate__'", type->tp_name); + return NULL; + } + + PyObject *annotate; + PyObject *dict = lookup_tp_dict(type); + if (PyDict_GetItemRef(dict, &_Py_ID(__annotate__), &annotate) < 0) { + return NULL; + } + if (annotate) { + descrgetfunc get = Py_TYPE(annotate)->tp_descr_get; + if (get) { + Py_SETREF(annotate, get(annotate, NULL, (PyObject *)type)); + } + } + else { + annotate = Py_None; + int result = PyDict_SetItem(dict, &_Py_ID(__annotate__), annotate); + if (result == 0) { + PyType_Modified(type); + } + } + return annotate; +} + +static int +type_set_annotate(PyTypeObject *type, PyObject *value, void *Py_UNUSED(ignored)) +{ + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, "cannot delete __annotate__ attribute"); + return -1; + } + if (_PyType_HasFeature(type, Py_TPFLAGS_IMMUTABLETYPE)) { + PyErr_Format(PyExc_TypeError, + "cannot set '__annotate__' attribute of immutable type '%s'", + type->tp_name); + return -1; + } + + if (!Py_IsNone(value) && !PyCallable_Check(value)) { + PyErr_SetString(PyExc_TypeError, "__annotate__ must be callable or None"); + return -1; + } + + PyObject *dict = lookup_tp_dict(type); + assert(PyDict_Check(dict)); + int result = PyDict_SetItem(dict, &_Py_ID(__annotate__), value); + if (result < 0) { + return -1; + } + if (!Py_IsNone(value)) { + if (PyDict_Pop(dict, &_Py_ID(__annotations__), NULL) == -1) { + return -1; + } + } + return 0; +} + static PyObject * type_get_annotations(PyTypeObject *type, void *context) { @@ -1694,7 +1756,25 @@ type_get_annotations(PyTypeObject *type, void *context) } } else { - annotations = PyDict_New(); + PyObject *annotate = type_get_annotate(type, NULL); + if (annotate == NULL) { + return NULL; + } + if (PyCallable_Check(annotate)) { + PyObject *one = _PyLong_GetOne(); + annotations = _PyObject_CallOneArg(annotate, one); + if (annotations == NULL) { + return NULL; + } + if (!PyDict_Check(annotations)) { + PyErr_SetString(PyExc_TypeError, "__annotate__ returned a non-dict"); + Py_DECREF(annotations); + return NULL; + } + } + else { + annotations = PyDict_New(); + } if (annotations) { int result = PyDict_SetItem( dict, &_Py_ID(__annotations__), annotations); @@ -1731,11 +1811,15 @@ type_set_annotations(PyTypeObject *type, PyObject *value, void *context) return -1; } } + PyType_Modified(type); if (result < 0) { return -1; } - - PyType_Modified(type); + else if (result == 0) { + if (PyDict_Pop(dict, &_Py_ID(__annotate__), NULL) < 0) { + return -1; + } + } return 0; } @@ -1811,6 +1895,7 @@ static PyGetSetDef type_getsets[] = { {"__doc__", (getter)type_get_doc, (setter)type_set_doc, NULL}, {"__text_signature__", (getter)type_get_text_signature, NULL, NULL}, {"__annotations__", (getter)type_get_annotations, (setter)type_set_annotations, NULL}, + {"__annotate__", (getter)type_get_annotate, (setter)type_set_annotate, NULL}, {"__type_params__", (getter)type_get_type_params, (setter)type_set_type_params, NULL}, {0} }; From c822ffab59966c02b5acb12c6991b778d03dfade Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 May 2024 10:09:30 -0400 Subject: [PATCH 002/105] fix refleaks --- Lib/test/test_type_annotations.py | 4 ++-- Objects/funcobject.c | 2 +- Objects/moduleobject.c | 4 ++++ Objects/typeobject.c | 3 +++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 55053df5710844..ef1569487de18c 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -217,7 +217,7 @@ def test_match(self): class AnnotateTests(unittest.TestCase): """See PEP 649.""" - def test_manual_annotate_function(self): + def test_manual_annotate(self): def f(): pass mod = types.ModuleType("mod") @@ -232,7 +232,7 @@ def check_annotations(self, f): self.assertEqual(f.__annotations__, {}) self.assertIs(f.__annotate__, None) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, "__annotate__ must be callable or None"): f.__annotate__ = 42 f.__annotate__ = lambda: 42 with self.assertRaisesRegex(TypeError, r"takes 0 positional arguments but 1 was given"): diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 72d74516cc4f53..d957f27a0a6949 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -816,7 +816,7 @@ func_set_annotate(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(ignored } else { PyErr_SetString(PyExc_TypeError, - "__annotate__ must be set to a callable object or None"); + "__annotate__ must be callable or None"); return -1; } } diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 3b378482be72f8..8ec74a4702b898 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -1220,10 +1220,13 @@ module_get_annotations(PyModuleObject *m, void *Py_UNUSED(ignored)) PyObject *one = _PyLong_GetOne(); annotations = _PyObject_CallOneArg(annotate, one); if (annotations == NULL) { + Py_DECREF(annotate); + Py_DECREF(dict); return NULL; } if (!PyDict_Check(annotations)) { PyErr_SetString(PyExc_TypeError, "__annotate__ returned a non-dict"); + Py_DECREF(annotate); Py_DECREF(annotations); Py_DECREF(dict); return NULL; @@ -1232,6 +1235,7 @@ module_get_annotations(PyModuleObject *m, void *Py_UNUSED(ignored)) else { annotations = PyDict_New(); } + Py_XDECREF(annotate); if (annotations) { int result = PyDict_SetItem( dict, &_Py_ID(__annotations__), annotations); diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 82bd707c8b55cf..14d879236d58d2 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -1764,17 +1764,20 @@ type_get_annotations(PyTypeObject *type, void *context) PyObject *one = _PyLong_GetOne(); annotations = _PyObject_CallOneArg(annotate, one); if (annotations == NULL) { + Py_DECREF(annotate); return NULL; } if (!PyDict_Check(annotations)) { PyErr_SetString(PyExc_TypeError, "__annotate__ returned a non-dict"); Py_DECREF(annotations); + Py_DECREF(annotate); return NULL; } } else { annotations = PyDict_New(); } + Py_DECREF(annotate); if (annotations) { int result = PyDict_SetItem( dict, &_Py_ID(__annotations__), annotations); From e80095ed46df46cfb37ddb815832513caf6a0014 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 May 2024 10:10:57 -0400 Subject: [PATCH 003/105] blurb --- .../2024-05-20-10-10-51.gh-issue-119180.35xqpu.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-05-20-10-10-51.gh-issue-119180.35xqpu.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-05-20-10-10-51.gh-issue-119180.35xqpu.rst b/Misc/NEWS.d/next/Core and Builtins/2024-05-20-10-10-51.gh-issue-119180.35xqpu.rst new file mode 100644 index 00000000000000..5a88ce097274fb --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-05-20-10-10-51.gh-issue-119180.35xqpu.rst @@ -0,0 +1,2 @@ +Add an ``__annotate__`` attribute to functions, classes, and modules as part +of :pep:`649`. Patch by Jelle Zijlstra. From 90ff2c40e562d7e5018189bb011bd42e7919f210 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 May 2024 10:25:12 -0400 Subject: [PATCH 004/105] Fix some tests --- Lib/test/test_sys.py | 2 +- Lib/test/test_typing.py | 2 +- Lib/typing.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index ee3bd0092f9bf3..8fe1d77756866a 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1564,7 +1564,7 @@ def func(): check(x, size('3Pi2cP7P2ic??2P')) # function def func(): pass - check(func, size('15Pi')) + check(func, size('16Pi')) class c(): @staticmethod def foo(): diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 64c4c497eb8934..dac55ceb9e99e0 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -3723,7 +3723,7 @@ def meth(self): pass acceptable_extra_attrs = { '_is_protocol', '_is_runtime_protocol', '__parameters__', - '__init__', '__annotations__', '__subclasshook__', + '__init__', '__annotations__', '__subclasshook__', '__annotate__', } self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs) self.assertLessEqual( diff --git a/Lib/typing.py b/Lib/typing.py index 434574559e04fc..be49aa63464f05 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1889,6 +1889,7 @@ class _TypingEllipsis: '__init__', '__module__', '__new__', '__slots__', '__subclasshook__', '__weakref__', '__class_getitem__', '__match_args__', '__static_attributes__', '__firstlineno__', + '__annotate__', }) # These special attributes will be not collected as protocol members. From 026c0ff458de7ea60af2569560a9f4dacddc18ae Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 May 2024 10:26:17 -0400 Subject: [PATCH 005/105] regen globals --- Include/internal/pycore_global_objects_fini_generated.h | 1 - Include/internal/pycore_global_strings.h | 1 - Include/internal/pycore_runtime_init_generated.h | 1 - Include/internal/pycore_unicodeobject_generated.h | 3 --- 4 files changed, 6 deletions(-) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 158584b504e465..33133aaaf00893 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -990,7 +990,6 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(hi)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(hook)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(hour)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(id)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ident)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(identity_hint)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ignore)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 769aa88b54c710..f5ea7b9bd7d433 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -479,7 +479,6 @@ struct _Py_global_strings { STRUCT_FOR_ID(hi) STRUCT_FOR_ID(hook) STRUCT_FOR_ID(hour) - STRUCT_FOR_ID(id) STRUCT_FOR_ID(ident) STRUCT_FOR_ID(identity_hint) STRUCT_FOR_ID(ignore) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 065b18efd7922c..c73408d6315312 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -988,7 +988,6 @@ extern "C" { INIT_ID(hi), \ INIT_ID(hook), \ INIT_ID(hour), \ - INIT_ID(id), \ INIT_ID(ident), \ INIT_ID(identity_hint), \ INIT_ID(ignore), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 334dcce03aaf49..d84c45a6b57887 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -1278,9 +1278,6 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { string = &_Py_ID(hour); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); - string = &_Py_ID(id); - assert(_PyUnicode_CheckConsistency(string, 1)); - _PyUnicode_InternInPlace(interp, &string); string = &_Py_ID(ident); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); From 7968744f50c48c9e5afd872d37ee0fc70d04e473 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 May 2024 08:07:03 -0400 Subject: [PATCH 006/105] Some initial work --- Include/cpython/code.h | 2 +- Include/internal/pycore_symtable.h | 1 + Python/future.c | 4 ++- Python/symtable.c | 53 +++++++++++++++++++++++++++++- 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/Include/cpython/code.h b/Include/cpython/code.h index ef8f9304ccab56..d64f79bceaa6c7 100644 --- a/Include/cpython/code.h +++ b/Include/cpython/code.h @@ -219,7 +219,7 @@ struct PyCodeObject _PyCode_DEF(1); #define CO_FUTURE_GENERATOR_STOP 0x800000 #define CO_FUTURE_ANNOTATIONS 0x1000000 -#define CO_NO_MONITORING_EVENTS 0x2000000 +#define CO_NO_MONITORING_EVENTS 0x4000000 /* This should be defined if a future statement modifies the syntax. For example, when a keyword is added. diff --git a/Include/internal/pycore_symtable.h b/Include/internal/pycore_symtable.h index 16e89f80d9d0c8..8a5f56668c17a6 100644 --- a/Include/internal/pycore_symtable.h +++ b/Include/internal/pycore_symtable.h @@ -109,6 +109,7 @@ typedef struct _symtable_entry { int ste_end_col_offset; /* end offset of first line of block */ int ste_opt_lineno; /* lineno of last exec or import * */ int ste_opt_col_offset; /* offset of last exec or import * */ + struct _symtable_entry *ste_annotation_block; /* symbol table entry for this entry's annotations */ struct symtable *ste_table; } PySTEntryObject; diff --git a/Python/future.c b/Python/future.c index 8d94d515605dcd..9e528bf121346a 100644 --- a/Python/future.c +++ b/Python/future.c @@ -37,7 +37,9 @@ future_check_features(_PyFutureFeatures *ff, stmt_ty s, PyObject *filename) } else if (strcmp(feature, FUTURE_GENERATOR_STOP) == 0) { continue; } else if (strcmp(feature, FUTURE_ANNOTATIONS) == 0) { - ff->ff_features |= CO_FUTURE_ANNOTATIONS; + // For now, we ignore this future. We may make it stringify again + // in the future. + // ff->ff_features |= CO_FUTURE_ANNOTATIONS; } else if (strcmp(feature, "braces") == 0) { PyErr_SetString(PyExc_SyntaxError, "not a chance"); diff --git a/Python/symtable.c b/Python/symtable.c index 2ec21a2d376da2..dcfe42305e3214 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -131,6 +131,7 @@ ste_new(struct symtable *st, identifier name, _Py_block_ty block, ste->ste_can_see_class_scope = 0; ste->ste_comp_iter_expr = 0; ste->ste_needs_classdict = 0; + ste->ste_annotation_block = NULL; ste->ste_symbols = PyDict_New(); ste->ste_varnames = PyList_New(0); @@ -166,6 +167,7 @@ ste_dealloc(PySTEntryObject *ste) Py_XDECREF(ste->ste_varnames); Py_XDECREF(ste->ste_children); Py_XDECREF(ste->ste_directives); + Py_XDECREF(ste->ste_annotation_block); PyObject_Free(ste); } @@ -1360,6 +1362,35 @@ symtable_enter_block(struct symtable *st, identifier name, _Py_block_ty block, return 1; } +static int +symtable_reenter_block(struct symtable *st, PySTEntryObject* ste) +{ + PySTEntryObject *prev = NULL; + + if (PyList_Append(st->st_stack, (PyObject *)ste) < 0) { + Py_DECREF(ste); + return 0; + } + prev = st->st_cur; + /* bpo-37757: For now, disallow *all* assignment expressions in the + * outermost iterator expression of a comprehension, even those inside + * a nested comprehension or a lambda expression. + */ + if (prev) { + ste->ste_comp_iter_expr = prev->ste_comp_iter_expr; + } + /* The entry is owned by the stack. Borrow it for st_cur. */ + Py_DECREF(ste); + st->st_cur = ste; + + if (prev) { + if (PyList_Append(prev->ste_children, (PyObject *)ste) < 0) { + return 0; + } + } + return 1; +} + static long symtable_lookup_entry(struct symtable *st, PySTEntryObject *ste, PyObject *name) { @@ -1666,6 +1697,7 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s) if (!symtable_exit_block(st)) VISIT_QUIT(st, 0); } + Py_CLEAR(st->st_cur->ste_annotation_block); break; case ClassDef_kind: { PyObject *tmp; @@ -1981,6 +2013,7 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s) if (!symtable_exit_block(st)) VISIT_QUIT(st, 0); } + Py_CLEAR(st->st_cur->ste_annotation_block); break; case AsyncWith_kind: VISIT_SEQ(st, withitem, s->v.AsyncWith.items); @@ -2435,8 +2468,26 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation) annotation->end_col_offset)) { VISIT_QUIT(st, 0); } + else { + if (st->st_cur->ste_annotation_block == NULL) { + struct _symtable_entry *st_parent = st->st_cur; + if (!symtable_enter_block(st, &_Py_ID(_annotation), AnnotationBlock, + (void *)annotation, annotation->lineno, + annotation->col_offset, annotation->end_lineno, + annotation->end_col_offset)) { + VISIT_QUIT(st, 0); + } + st_parent->ste_annotation_block = + (struct _symtable_entry *)Py_NewRef(st->st_cur); + } + else { + if (!symtable_reenter_block(st, st->st_cur->ste_annotation_block)) { + VISIT_QUIT(st, 0); + } + } + } VISIT(st, expr, annotation); - if (future_annotations && !symtable_exit_block(st)) { + if (!symtable_exit_block(st)) { VISIT_QUIT(st, 0); } return 1; From 4e54197422859fc8ad229cea917ee9c48c1d903f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 May 2024 11:34:16 -0400 Subject: [PATCH 007/105] Add bytecode for adding annotate --- Include/internal/pycore_opcode_utils.h | 1 + Python/bytecodes.c | 5 +++++ Python/executor_cases.c.h | 5 +++++ Python/generated_cases.c.h | 5 +++++ 4 files changed, 16 insertions(+) diff --git a/Include/internal/pycore_opcode_utils.h b/Include/internal/pycore_opcode_utils.h index 208bfb2f75308b..1be0820dbac5e0 100644 --- a/Include/internal/pycore_opcode_utils.h +++ b/Include/internal/pycore_opcode_utils.h @@ -57,6 +57,7 @@ extern "C" { #define MAKE_FUNCTION_KWDEFAULTS 0x02 #define MAKE_FUNCTION_ANNOTATIONS 0x04 #define MAKE_FUNCTION_CLOSURE 0x08 +#define MAKE_FUNCTION_ANNOTATE 0x10 /* Values used in the oparg for RESUME */ #define RESUME_AT_FUNC_START 0 diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 55eda9711dea1f..eb1acbdc13048e 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -3957,6 +3957,11 @@ dummy_func( assert(func_obj->func_defaults == NULL); func_obj->func_defaults = attr; break; + case MAKE_FUNCTION_ANNOTATE: + assert(PyCallable_Check(attr)); + assert(func_obj->func_annotate == NULL); + func_obj->func_annotate = attr; + break; default: Py_UNREACHABLE(); } diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 347a1e677a0832..a7b9ac69c52bf0 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -4026,6 +4026,11 @@ assert(func_obj->func_defaults == NULL); func_obj->func_defaults = attr; break; + case MAKE_FUNCTION_ANNOTATE: + assert(PyCallable_Check(attr)); + assert(func_obj->func_annotate == NULL); + func_obj->func_annotate = attr; + break; default: Py_UNREACHABLE(); } diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 8b8112209cc78a..d96825b6fdeff0 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -5417,6 +5417,11 @@ assert(func_obj->func_defaults == NULL); func_obj->func_defaults = attr; break; + case MAKE_FUNCTION_ANNOTATE: + assert(PyCallable_Check(attr)); + assert(func_obj->func_annotate == NULL); + func_obj->func_annotate = attr; + break; default: Py_UNREACHABLE(); } From 4469b32bee8226fa04061cea9d1d9800fa534f68 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 May 2024 13:38:41 -0400 Subject: [PATCH 008/105] compiler changes --- Python/compile.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Python/compile.c b/Python/compile.c index 79f3baadca6b4a..4037ec4cdec093 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1828,6 +1828,9 @@ compiler_make_closure(struct compiler *c, location loc, if (flags & MAKE_FUNCTION_DEFAULTS) { ADDOP_I(c, loc, SET_FUNCTION_ATTRIBUTE, MAKE_FUNCTION_DEFAULTS); } + if (flags & MAKE_FUNCTION_ANNOTATE) { + ADDOP_I(c, loc, SET_FUNCTION_ATTRIBUTE, MAKE_FUNCTION_ANNOTATE); + } return SUCCESS; } @@ -1982,9 +1985,10 @@ compiler_visit_annotations(struct compiler *c, location loc, /* Push arg annotation names and values. The expressions are evaluated out-of-order wrt the source code. - Return -1 on error, 0 if no annotations pushed, 1 if a annotations is pushed. + Return -1 on error, or a combination of flags to add to the function. */ Py_ssize_t annotations_len = 0; + int future_annotations = c->c_future.ff_features & CO_FUTURE_ANNOTATIONS; RETURN_IF_ERROR( compiler_visit_argannotations(c, args->args, &annotations_len, loc)); @@ -2012,7 +2016,7 @@ compiler_visit_annotations(struct compiler *c, location loc, if (annotations_len) { ADDOP_I(c, loc, BUILD_TUPLE, annotations_len); - return 1; + return MAKE_FUNCTION_ANNOTATIONS; } return 0; @@ -2413,9 +2417,7 @@ compiler_function(struct compiler *c, stmt_ty s, int is_async) } return ERROR; } - if (annotations > 0) { - funcflags |= MAKE_FUNCTION_ANNOTATIONS; - } + funcflags |= annotations; if (compiler_function_body(c, s, is_async, funcflags, firstlineno) < 0) { if (is_generic) { From 47d672ee233658bda051784b0655cb5738b1502d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 10:34:21 -0400 Subject: [PATCH 009/105] Functions work --- .../pycore_global_objects_fini_generated.h | 1 + Include/internal/pycore_global_strings.h | 1 + .../internal/pycore_runtime_init_generated.h | 1 + Include/internal/pycore_symtable.h | 7 +- Python/compile.c | 104 ++++++++--- Python/symtable.c | 176 ++++++++++-------- 6 files changed, 184 insertions(+), 106 deletions(-) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 33133aaaf00893..dc4fd3282b9e13 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -559,6 +559,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(dot)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(dot_locals)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(empty)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(format)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(generic_base)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(json_decoder)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(kwdefaults)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index f5ea7b9bd7d433..ab005022dd5ab2 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -45,6 +45,7 @@ struct _Py_global_strings { STRUCT_FOR_STR(dot, ".") STRUCT_FOR_STR(dot_locals, ".") STRUCT_FOR_STR(empty, "") + STRUCT_FOR_STR(format, ".format") STRUCT_FOR_STR(generic_base, ".generic_base") STRUCT_FOR_STR(json_decoder, "json.decoder") STRUCT_FOR_STR(kwdefaults, ".kwdefaults") diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index c73408d6315312..dda7b1329deb2a 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -554,6 +554,7 @@ extern "C" { INIT_STR(dot, "."), \ INIT_STR(dot_locals, "."), \ INIT_STR(empty, ""), \ + INIT_STR(format, ".format"), \ INIT_STR(generic_base, ".generic_base"), \ INIT_STR(json_decoder, "json.decoder"), \ INIT_STR(kwdefaults, ".kwdefaults"), \ diff --git a/Include/internal/pycore_symtable.h b/Include/internal/pycore_symtable.h index 8a5f56668c17a6..f109a7229c35df 100644 --- a/Include/internal/pycore_symtable.h +++ b/Include/internal/pycore_symtable.h @@ -12,8 +12,9 @@ struct _mod; // Type defined in pycore_ast.h typedef enum _block_type { FunctionBlock, ClassBlock, ModuleBlock, - // Used for annotations if 'from __future__ import annotations' is active. - // Annotation blocks cannot bind names and are not evaluated. + // Used for annotations. If 'from __future__ import annotations' is active, + // annotation blocks cannot bind names and are not evaluated. Otherwise, they + // are lazily evaluated (see PEP 649). AnnotationBlock, // Used for generics and type aliases. These work mostly like functions // (see PEP 695 for details). The three different blocks function identically; @@ -88,6 +89,7 @@ typedef struct _symtable_entry { including free refs to globals */ unsigned ste_generator : 1; /* true if namespace is a generator */ unsigned ste_coroutine : 1; /* true if namespace is a coroutine */ + unsigned ste_annotations_used : 1; /* true if there are any annotations in this scope */ _Py_comprehension_ty ste_comprehension; /* Kind of comprehension (if any) */ unsigned ste_varargs : 1; /* true if block has varargs */ unsigned ste_varkeywords : 1; /* true if block has varkeywords */ @@ -126,6 +128,7 @@ extern struct symtable* _PySymtable_Build( PyObject *filename, _PyFutureFeatures *future); extern PySTEntryObject* _PySymtable_Lookup(struct symtable *, void *); +extern int _PySymtable_LookupOptional(struct symtable *, void *, PySTEntryObject **); extern void _PySymtable_Free(struct symtable *); diff --git a/Python/compile.c b/Python/compile.c index 4037ec4cdec093..c76c9b146c9c34 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -132,7 +132,7 @@ enum { COMPILER_SCOPE_ASYNC_FUNCTION, COMPILER_SCOPE_LAMBDA, COMPILER_SCOPE_COMPREHENSION, - COMPILER_SCOPE_TYPEPARAMS, + COMPILER_SCOPE_ANNOTATIONS, }; @@ -623,8 +623,8 @@ compiler_set_qualname(struct compiler *c) capsule = PyList_GET_ITEM(c->c_stack, stack_size - 1); parent = (struct compiler_unit *)PyCapsule_GetPointer(capsule, CAPSULE_NAME); assert(parent); - if (parent->u_scope_type == COMPILER_SCOPE_TYPEPARAMS) { - /* The parent is a type parameter scope, so we need to + if (parent->u_scope_type == COMPILER_SCOPE_ANNOTATIONS) { + /* The parent is an annotation scope, so we need to look at the grandparent. */ if (stack_size == 2) { // If we're immediately within the module, we can skip @@ -1822,15 +1822,15 @@ compiler_make_closure(struct compiler *c, location loc, if (flags & MAKE_FUNCTION_ANNOTATIONS) { ADDOP_I(c, loc, SET_FUNCTION_ATTRIBUTE, MAKE_FUNCTION_ANNOTATIONS); } + if (flags & MAKE_FUNCTION_ANNOTATE) { + ADDOP_I(c, loc, SET_FUNCTION_ATTRIBUTE, MAKE_FUNCTION_ANNOTATE); + } if (flags & MAKE_FUNCTION_KWDEFAULTS) { ADDOP_I(c, loc, SET_FUNCTION_ATTRIBUTE, MAKE_FUNCTION_KWDEFAULTS); } if (flags & MAKE_FUNCTION_DEFAULTS) { ADDOP_I(c, loc, SET_FUNCTION_ATTRIBUTE, MAKE_FUNCTION_DEFAULTS); } - if (flags & MAKE_FUNCTION_ANNOTATE) { - ADDOP_I(c, loc, SET_FUNCTION_ATTRIBUTE, MAKE_FUNCTION_ANNOTATE); - } return SUCCESS; } @@ -1956,7 +1956,7 @@ compiler_visit_argannotation(struct compiler *c, identifier id, VISIT(c, expr, annotation); } } - *annotations_len += 2; + *annotations_len += 1; return SUCCESS; } @@ -1990,6 +1990,32 @@ compiler_visit_annotations(struct compiler *c, location loc, Py_ssize_t annotations_len = 0; int future_annotations = c->c_future.ff_features & CO_FUTURE_ANNOTATIONS; + PySTEntryObject *ste; + int result = _PySymtable_LookupOptional(c->c_st, args, &ste); + if (result == -1) { + return ERROR; + } + assert(ste != NULL); + + if (!future_annotations && ste->ste_annotations_used) { + PyObject *annotations_name = PyUnicode_FromFormat("", ste->ste_name); + if (!annotations_name) { + return ERROR; + } + if (compiler_enter_scope(c, annotations_name, COMPILER_SCOPE_ANNOTATIONS, + (void *)args, loc.lineno) == -1) { + Py_DECREF(annotations_name); + return ERROR; + } + Py_DECREF(annotations_name); + c->u->u_metadata.u_posonlyargcount = 1; + _Py_DECLARE_STR(format, ".format"); + // RETURN_IF_ERROR(compiler_nameop(c, loc, &_Py_STR(format), Load)); + // ADDOP(c, loc, POP_TOP); + // ADDOP_LOAD_CONST(c, loc, _PyLong_GetOne()); + // TODO: emit "if .format != 1: raise NotImplementedError()" + } + RETURN_IF_ERROR( compiler_visit_argannotations(c, args->args, &annotations_len, loc)); @@ -2014,10 +2040,28 @@ compiler_visit_annotations(struct compiler *c, location loc, RETURN_IF_ERROR( compiler_visit_argannotation(c, &_Py_ID(return), returns, &annotations_len, loc)); - if (annotations_len) { - ADDOP_I(c, loc, BUILD_TUPLE, annotations_len); + if (future_annotations) { + ADDOP_I(c, loc, BUILD_TUPLE, annotations_len * 2); return MAKE_FUNCTION_ANNOTATIONS; } + else { + assert(ste != NULL); + if (ste->ste_annotations_used) { + ADDOP_I(c, loc, BUILD_MAP, annotations_len); + ADDOP_IN_SCOPE(c, loc, RETURN_VALUE); + PyCodeObject *co = optimize_and_assemble(c, 1); + compiler_exit_scope(c); + if (co == NULL) { + return ERROR; + } + if (compiler_make_closure(c, loc, co, 0) < 0) { + Py_DECREF(co); + return ERROR; + } + Py_DECREF(co); + return MAKE_FUNCTION_ANNOTATE; + } + } return 0; } @@ -2125,7 +2169,7 @@ compiler_type_param_bound_or_default(struct compiler *c, expr_ty e, identifier name, void *key, bool allow_starred) { - if (compiler_enter_scope(c, name, COMPILER_SCOPE_TYPEPARAMS, + if (compiler_enter_scope(c, name, COMPILER_SCOPE_ANNOTATIONS, key, e->lineno) == -1) { return ERROR; } @@ -2398,7 +2442,7 @@ compiler_function(struct compiler *c, stmt_ty s, int is_async) if (!type_params_name) { return ERROR; } - if (compiler_enter_scope(c, type_params_name, COMPILER_SCOPE_TYPEPARAMS, + if (compiler_enter_scope(c, type_params_name, COMPILER_SCOPE_ANNOTATIONS, (void *)type_params, firstlineno) == -1) { Py_DECREF(type_params_name); return ERROR; @@ -2633,7 +2677,7 @@ compiler_class(struct compiler *c, stmt_ty s) if (!type_params_name) { return ERROR; } - if (compiler_enter_scope(c, type_params_name, COMPILER_SCOPE_TYPEPARAMS, + if (compiler_enter_scope(c, type_params_name, COMPILER_SCOPE_ANNOTATIONS, (void *)type_params, firstlineno) == -1) { Py_DECREF(type_params_name); return ERROR; @@ -2752,7 +2796,7 @@ compiler_typealias(struct compiler *c, stmt_ty s) if (!type_params_name) { return ERROR; } - if (compiler_enter_scope(c, type_params_name, COMPILER_SCOPE_TYPEPARAMS, + if (compiler_enter_scope(c, type_params_name, COMPILER_SCOPE_ANNOTATIONS, (void *)type_params, loc.lineno) == -1) { Py_DECREF(type_params_name); return ERROR; @@ -6504,20 +6548,20 @@ compiler_annassign(struct compiler *c, stmt_ty s) return ERROR; } /* If we have a simple name in a module or class, store annotation. */ - if (s->v.AnnAssign.simple && - (c->u->u_scope_type == COMPILER_SCOPE_MODULE || - c->u->u_scope_type == COMPILER_SCOPE_CLASS)) { - if (c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) { - VISIT(c, annexpr, s->v.AnnAssign.annotation) - } - else { - VISIT(c, expr, s->v.AnnAssign.annotation); - } - ADDOP_NAME(c, loc, LOAD_NAME, &_Py_ID(__annotations__), names); - mangled = _Py_Mangle(c->u->u_private, targ->v.Name.id); - ADDOP_LOAD_CONST_NEW(c, loc, mangled); - ADDOP(c, loc, STORE_SUBSCR); - } + // if (s->v.AnnAssign.simple && + // (c->u->u_scope_type == COMPILER_SCOPE_MODULE || + // c->u->u_scope_type == COMPILER_SCOPE_CLASS)) { + // if (c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) { + // VISIT(c, annexpr, s->v.AnnAssign.annotation) + // } + // else { + // VISIT(c, expr, s->v.AnnAssign.annotation); + // } + // ADDOP_NAME(c, loc, LOAD_NAME, &_Py_ID(__annotations__), names); + // mangled = _Py_Mangle(c->u->u_private, targ->v.Name.id); + // ADDOP_LOAD_CONST_NEW(c, loc, mangled); + // ADDOP(c, loc, STORE_SUBSCR); + // } break; case Attribute_kind: if (forbidden_name(c, loc, targ->v.Attribute.attr, Store)) { @@ -6542,9 +6586,9 @@ compiler_annassign(struct compiler *c, stmt_ty s) return ERROR; } /* Annotation is evaluated last. */ - if (!s->v.AnnAssign.simple && check_annotation(c, s) < 0) { - return ERROR; - } + // if (!s->v.AnnAssign.simple && check_annotation(c, s) < 0) { + // return ERROR; + // } return SUCCESS; } diff --git a/Python/symtable.c b/Python/symtable.c index dcfe42305e3214..438751ac1aec8d 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -245,10 +245,12 @@ static int symtable_visit_alias(struct symtable *st, alias_ty); static int symtable_visit_comprehension(struct symtable *st, comprehension_ty); static int symtable_visit_keyword(struct symtable *st, keyword_ty); static int symtable_visit_params(struct symtable *st, asdl_arg_seq *args); -static int symtable_visit_annotation(struct symtable *st, expr_ty annotation); +static int symtable_visit_annotation(struct symtable *st, expr_ty annotation, + struct _symtable_entry *parent_ste, void *key); static int symtable_visit_argannotations(struct symtable *st, asdl_arg_seq *args); static int symtable_implicit_arg(struct symtable *st, int pos); -static int symtable_visit_annotations(struct symtable *st, stmt_ty, arguments_ty, expr_ty); +static int symtable_visit_annotations(struct symtable *st, stmt_ty, arguments_ty, expr_ty, + struct _symtable_entry *parent_ste); static int symtable_visit_withitem(struct symtable *st, withitem_ty item); static int symtable_visit_match_case(struct symtable *st, match_case_ty m); static int symtable_visit_pattern(struct symtable *st, pattern_ty s); @@ -504,6 +506,21 @@ _PySymtable_Lookup(struct symtable *st, void *key) return (PySTEntryObject *)v; } +int +_PySymtable_LookupOptional(struct symtable *st, void *key, + PySTEntryObject **out) +{ + PyObject *k = PyLong_FromVoidPtr(key); + if (k == NULL) { + *out = NULL; + return -1; + } + int result = PyDict_GetItemRef(st->st_blocks, k, (PyObject **)out); + Py_DECREF(k); + assert(*out == NULL || PySTEntry_Check(*out)); + return result; +} + long _PyST_GetSymbol(PySTEntryObject *ste, PyObject *name) { @@ -1319,20 +1336,12 @@ symtable_exit_block(struct symtable *st) } static int -symtable_enter_block(struct symtable *st, identifier name, _Py_block_ty block, - void *ast, int lineno, int col_offset, - int end_lineno, int end_col_offset) +symtable_enter_existing_block(struct symtable *st, PySTEntryObject* ste) { - PySTEntryObject *prev = NULL, *ste; - - ste = ste_new(st, name, block, ast, lineno, col_offset, end_lineno, end_col_offset); - if (ste == NULL) - return 0; if (PyList_Append(st->st_stack, (PyObject *)ste) < 0) { - Py_DECREF(ste); return 0; } - prev = st->st_cur; + PySTEntryObject *prev = st->st_cur; /* bpo-37757: For now, disallow *all* assignment expressions in the * outermost iterator expression of a comprehension, even those inside * a nested comprehension or a lambda expression. @@ -1341,17 +1350,16 @@ symtable_enter_block(struct symtable *st, identifier name, _Py_block_ty block, ste->ste_comp_iter_expr = prev->ste_comp_iter_expr; } /* The entry is owned by the stack. Borrow it for st_cur. */ - Py_DECREF(ste); st->st_cur = ste; /* Annotation blocks shouldn't have any affect on the symbol table since in * the compilation stage, they will all be transformed to strings. They are * only created if future 'annotations' feature is activated. */ - if (block == AnnotationBlock) { + if (st->st_future->ff_features & CO_FUTURE_ANNOTATIONS && ste->ste_type == AnnotationBlock) { return 1; } - if (block == ModuleBlock) + if (ste->ste_type == ModuleBlock) st->st_global = st->st_cur->ste_symbols; if (prev) { @@ -1363,32 +1371,17 @@ symtable_enter_block(struct symtable *st, identifier name, _Py_block_ty block, } static int -symtable_reenter_block(struct symtable *st, PySTEntryObject* ste) +symtable_enter_block(struct symtable *st, identifier name, _Py_block_ty block, + void *ast, int lineno, int col_offset, + int end_lineno, int end_col_offset) { - PySTEntryObject *prev = NULL; - - if (PyList_Append(st->st_stack, (PyObject *)ste) < 0) { - Py_DECREF(ste); + PySTEntryObject *ste = ste_new(st, name, block, ast, + lineno, col_offset, end_lineno, end_col_offset); + if (ste == NULL) return 0; - } - prev = st->st_cur; - /* bpo-37757: For now, disallow *all* assignment expressions in the - * outermost iterator expression of a comprehension, even those inside - * a nested comprehension or a lambda expression. - */ - if (prev) { - ste->ste_comp_iter_expr = prev->ste_comp_iter_expr; - } - /* The entry is owned by the stack. Borrow it for st_cur. */ + int result = symtable_enter_existing_block(st, ste); Py_DECREF(ste); - st->st_cur = ste; - - if (prev) { - if (PyList_Append(prev->ste_children, (PyObject *)ste) < 0) { - return 0; - } - } - return 1; + return result; } static long @@ -1660,7 +1653,7 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s) VISIT_QUIT(st, 0); } switch (s->kind) { - case FunctionDef_kind: + case FunctionDef_kind: { if (!symtable_add_def(st, s->v.FunctionDef.name, DEF_LOCAL, LOCATION(s))) VISIT_QUIT(st, 0); if (s->v.FunctionDef.args->defaults) @@ -1682,13 +1675,20 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s) } VISIT_SEQ(st, type_param, s->v.FunctionDef.type_params); } + PySTEntryObject *new_ste = ste_new(st, s->v.FunctionDef.name, FunctionBlock, (void *)s, + LOCATION(s)); + if (!new_ste) { + VISIT_QUIT(st, 0); + } + if (!symtable_visit_annotations(st, s, s->v.FunctionDef.args, - s->v.FunctionDef.returns)) + s->v.FunctionDef.returns, new_ste)) VISIT_QUIT(st, 0); - if (!symtable_enter_block(st, s->v.FunctionDef.name, - FunctionBlock, (void *)s, - LOCATION(s))) + if (!symtable_enter_existing_block(st, new_ste)) { + Py_DECREF(new_ste); VISIT_QUIT(st, 0); + } + Py_DECREF(new_ste); VISIT(st, arguments, s->v.FunctionDef.args); VISIT_SEQ(st, stmt, s->v.FunctionDef.body); if (!symtable_exit_block(st)) @@ -1697,8 +1697,8 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s) if (!symtable_exit_block(st)) VISIT_QUIT(st, 0); } - Py_CLEAR(st->st_cur->ste_annotation_block); break; + } case ClassDef_kind: { PyObject *tmp; if (!symtable_add_def(st, s->v.ClassDef.name, DEF_LOCAL, LOCATION(s))) @@ -1823,7 +1823,8 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s) else { VISIT(st, expr, s->v.AnnAssign.target); } - if (!symtable_visit_annotation(st, s->v.AnnAssign.annotation)) { + if (!symtable_visit_annotation(st, s->v.AnnAssign.annotation, st->st_cur, + (void *)((uintptr_t)st->st_cur->ste_id + 1))) { VISIT_QUIT(st, 0); } @@ -1973,7 +1974,7 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s) VISIT_SEQ(st, withitem, s->v.With.items); VISIT_SEQ(st, stmt, s->v.With.body); break; - case AsyncFunctionDef_kind: + case AsyncFunctionDef_kind: { if (!symtable_add_def(st, s->v.AsyncFunctionDef.name, DEF_LOCAL, LOCATION(s))) VISIT_QUIT(st, 0); if (s->v.AsyncFunctionDef.args->defaults) @@ -1996,14 +1997,21 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s) } VISIT_SEQ(st, type_param, s->v.AsyncFunctionDef.type_params); } + PySTEntryObject *new_ste = ste_new(st, s->v.FunctionDef.name, FunctionBlock, (void *)s, + LOCATION(s)); + if (!new_ste) { + VISIT_QUIT(st, 0); + } + if (!symtable_visit_annotations(st, s, s->v.AsyncFunctionDef.args, - s->v.AsyncFunctionDef.returns)) + s->v.AsyncFunctionDef.returns, new_ste)) VISIT_QUIT(st, 0); - if (!symtable_enter_block(st, s->v.AsyncFunctionDef.name, - FunctionBlock, (void *)s, - s->lineno, s->col_offset, - s->end_lineno, s->end_col_offset)) + if (!symtable_enter_existing_block(st, new_ste)) { + Py_DECREF(new_ste); VISIT_QUIT(st, 0); + } + Py_DECREF(new_ste); + st->st_cur->ste_coroutine = 1; VISIT(st, arguments, s->v.AsyncFunctionDef.args); VISIT_SEQ(st, stmt, s->v.AsyncFunctionDef.body); @@ -2013,8 +2021,8 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s) if (!symtable_exit_block(st)) VISIT_QUIT(st, 0); } - Py_CLEAR(st->st_cur->ste_annotation_block); break; + } case AsyncWith_kind: VISIT_SEQ(st, withitem, s->v.AsyncWith.items); VISIT_SEQ(st, stmt, s->v.AsyncWith.body); @@ -2458,30 +2466,37 @@ symtable_visit_params(struct symtable *st, asdl_arg_seq *args) } static int -symtable_visit_annotation(struct symtable *st, expr_ty annotation) +symtable_visit_annotation(struct symtable *st, expr_ty annotation, + struct _symtable_entry *parent_ste, void *key) { int future_annotations = st->st_future->ff_features & CO_FUTURE_ANNOTATIONS; if (future_annotations && !symtable_enter_block(st, &_Py_ID(_annotation), AnnotationBlock, - (void *)annotation, annotation->lineno, - annotation->col_offset, annotation->end_lineno, - annotation->end_col_offset)) { + key, LOCATION(annotation))) { VISIT_QUIT(st, 0); } else { + printf("enter block %p\n", key); if (st->st_cur->ste_annotation_block == NULL) { - struct _symtable_entry *st_parent = st->st_cur; if (!symtable_enter_block(st, &_Py_ID(_annotation), AnnotationBlock, - (void *)annotation, annotation->lineno, - annotation->col_offset, annotation->end_lineno, - annotation->end_col_offset)) { + key, LOCATION(annotation))) { VISIT_QUIT(st, 0); } - st_parent->ste_annotation_block = + parent_ste->ste_annotation_block = (struct _symtable_entry *)Py_NewRef(st->st_cur); + _Py_DECLARE_STR(format, ".format"); + // We need to insert code that reads this "parameter" to the function. + if (!symtable_add_def(st, &_Py_STR(format), DEF_PARAM, + LOCATION(annotation))) { + return 0; + } + if (!symtable_add_def(st, &_Py_STR(format), USE, + LOCATION(annotation))) { + return 0; + } } else { - if (!symtable_reenter_block(st, st->st_cur->ste_annotation_block)) { + if (!symtable_enter_existing_block(st, st->st_cur->ste_annotation_block)) { VISIT_QUIT(st, 0); } } @@ -2503,37 +2518,50 @@ symtable_visit_argannotations(struct symtable *st, asdl_arg_seq *args) for (i = 0; i < asdl_seq_LEN(args); i++) { arg_ty arg = (arg_ty)asdl_seq_GET(args, i); - if (arg->annotation) + if (arg->annotation) { + st->st_cur->ste_annotations_used = 1; VISIT(st, expr, arg->annotation); + } } return 1; } static int -symtable_visit_annotations(struct symtable *st, stmt_ty o, arguments_ty a, expr_ty returns) +symtable_visit_annotations(struct symtable *st, stmt_ty o, arguments_ty a, expr_ty returns, + struct _symtable_entry *function_ste) { - int future_annotations = st->st_future->ff_features & CO_FUTURE_ANNOTATIONS; - if (future_annotations && - !symtable_enter_block(st, &_Py_ID(_annotation), AnnotationBlock, - (void *)o, o->lineno, o->col_offset, o->end_lineno, - o->end_col_offset)) { + if (!symtable_enter_block(st, function_ste->ste_name, AnnotationBlock, + (void *)a, LOCATION(o))) { VISIT_QUIT(st, 0); } + _Py_DECLARE_STR(format, ".format"); + // We need to insert code that reads this "parameter" to the function. + if (!symtable_add_def(st, &_Py_STR(format), DEF_PARAM, LOCATION(o))) { + return 0; + } + if (!symtable_add_def(st, &_Py_STR(format), USE, LOCATION(o))) { + return 0; + } if (a->posonlyargs && !symtable_visit_argannotations(st, a->posonlyargs)) return 0; if (a->args && !symtable_visit_argannotations(st, a->args)) return 0; - if (a->vararg && a->vararg->annotation) + if (a->vararg && a->vararg->annotation) { + st->st_cur->ste_annotations_used = 1; VISIT(st, expr, a->vararg->annotation); - if (a->kwarg && a->kwarg->annotation) + } + if (a->kwarg && a->kwarg->annotation) { + st->st_cur->ste_annotations_used = 1; VISIT(st, expr, a->kwarg->annotation); + } if (a->kwonlyargs && !symtable_visit_argannotations(st, a->kwonlyargs)) return 0; - if (future_annotations && !symtable_exit_block(st)) { - VISIT_QUIT(st, 0); + if (returns) { + st->st_cur->ste_annotations_used = 1; + VISIT(st, expr, returns); } - if (returns && !symtable_visit_annotation(st, returns)) { + if (!symtable_exit_block(st)) { VISIT_QUIT(st, 0); } return 1; From ab9359cd30bb39e92bfa03d4a6de799be16d75bd Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 10:46:59 -0400 Subject: [PATCH 010/105] Raise AssertionError on the wrong format --- Python/compile.c | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Python/compile.c b/Python/compile.c index c76c9b146c9c34..dcc97244ddc6cf 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -142,6 +142,15 @@ typedef _PyInstructionSequence instr_sequence; #define INITIAL_INSTR_SEQUENCE_SIZE 100 #define INITIAL_INSTR_SEQUENCE_LABELS_MAP_SIZE 10 +static const int compare_masks[] = { + [Py_LT] = COMPARISON_LESS_THAN, + [Py_LE] = COMPARISON_LESS_THAN | COMPARISON_EQUALS, + [Py_EQ] = COMPARISON_EQUALS, + [Py_NE] = COMPARISON_NOT_EQUALS, + [Py_GT] = COMPARISON_GREATER_THAN, + [Py_GE] = COMPARISON_GREATER_THAN | COMPARISON_EQUALS, +}; + /* * Resize the array if index is out of range. * @@ -1996,6 +2005,7 @@ compiler_visit_annotations(struct compiler *c, location loc, return ERROR; } assert(ste != NULL); + NEW_JUMP_TARGET_LABEL(c, raise_notimp); if (!future_annotations && ste->ste_annotations_used) { PyObject *annotations_name = PyUnicode_FromFormat("", ste->ste_name); @@ -2010,10 +2020,10 @@ compiler_visit_annotations(struct compiler *c, location loc, Py_DECREF(annotations_name); c->u->u_metadata.u_posonlyargcount = 1; _Py_DECLARE_STR(format, ".format"); - // RETURN_IF_ERROR(compiler_nameop(c, loc, &_Py_STR(format), Load)); - // ADDOP(c, loc, POP_TOP); - // ADDOP_LOAD_CONST(c, loc, _PyLong_GetOne()); - // TODO: emit "if .format != 1: raise NotImplementedError()" + ADDOP_I(c, loc, LOAD_FAST, 0); + ADDOP_LOAD_CONST(c, loc, _PyLong_GetOne()); + ADDOP_I(c, loc, COMPARE_OP, (Py_EQ << 5) | compare_masks[Py_EQ]); + ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, raise_notimp); } RETURN_IF_ERROR( @@ -2049,6 +2059,9 @@ compiler_visit_annotations(struct compiler *c, location loc, if (ste->ste_annotations_used) { ADDOP_I(c, loc, BUILD_MAP, annotations_len); ADDOP_IN_SCOPE(c, loc, RETURN_VALUE); + USE_LABEL(c, raise_notimp); + ADDOP_IN_SCOPE(c, loc, LOAD_ASSERTION_ERROR); + ADDOP_I(c, loc, RAISE_VARARGS, 1); PyCodeObject *co = optimize_and_assemble(c, 1); compiler_exit_scope(c); if (co == NULL) { @@ -2885,15 +2898,6 @@ check_compare(struct compiler *c, expr_ty e) return SUCCESS; } -static const int compare_masks[] = { - [Py_LT] = COMPARISON_LESS_THAN, - [Py_LE] = COMPARISON_LESS_THAN | COMPARISON_EQUALS, - [Py_EQ] = COMPARISON_EQUALS, - [Py_NE] = COMPARISON_NOT_EQUALS, - [Py_GT] = COMPARISON_GREATER_THAN, - [Py_GE] = COMPARISON_GREATER_THAN | COMPARISON_EQUALS, -}; - static int compiler_addcompare(struct compiler *c, location loc, cmpop_ty op) { From e50cd62156cc6e2eca313ca775091cf9ebcd6665 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 15:51:36 -0400 Subject: [PATCH 011/105] Modules and classes --- Python/compile.c | 216 ++++++++++++++++++++++++++++++---------------- Python/symtable.c | 1 - 2 files changed, 142 insertions(+), 75 deletions(-) diff --git a/Python/compile.c b/Python/compile.c index dcc97244ddc6cf..cec332d12d47e4 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -380,6 +380,8 @@ static int compiler_pattern(struct compiler *, pattern_ty, pattern_context *); static int compiler_match(struct compiler *, stmt_ty); static int compiler_pattern_subpattern(struct compiler *, pattern_ty, pattern_context *); +static int compiler_make_closure(struct compiler *c, location loc, + PyCodeObject *co, Py_ssize_t flags); static PyCodeObject *optimize_and_assemble(struct compiler *, int addNone); @@ -1631,6 +1633,124 @@ compiler_unwind_fblock_stack(struct compiler *c, location *ploc, return SUCCESS; } +static int +compiler_setup_annotations_scope(struct compiler *c, location loc, + void *key, jump_target_label label) +{ + PyObject *annotations_name = PyUnicode_FromFormat( + "", c->u->u_ste->ste_name); + if (!annotations_name) { + return ERROR; + } + if (compiler_enter_scope(c, annotations_name, COMPILER_SCOPE_ANNOTATIONS, + key, loc.lineno) == -1) { + Py_DECREF(annotations_name); + return ERROR; + } + Py_DECREF(annotations_name); + c->u->u_metadata.u_posonlyargcount = 1; + _Py_DECLARE_STR(format, ".format"); + ADDOP_I(c, loc, LOAD_FAST, 0); + ADDOP_LOAD_CONST(c, loc, _PyLong_GetOne()); + ADDOP_I(c, loc, COMPARE_OP, (Py_EQ << 5) | compare_masks[Py_EQ]); + ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, label); + return 0; +} + +static int +compiler_leave_annotations_scope(struct compiler *c, location loc, + int annotations_len, jump_target_label label) +{ + ADDOP_I(c, loc, BUILD_MAP, annotations_len); + ADDOP_IN_SCOPE(c, loc, RETURN_VALUE); + USE_LABEL(c, label); + ADDOP_IN_SCOPE(c, loc, LOAD_ASSERTION_ERROR); + ADDOP_I(c, loc, RAISE_VARARGS, 1); + PyCodeObject *co = optimize_and_assemble(c, 1); + compiler_exit_scope(c); + if (co == NULL) { + return ERROR; + } + if (compiler_make_closure(c, loc, co, 0) < 0) { + Py_DECREF(co); + return ERROR; + } + Py_DECREF(co); + return 0; +} + +static int +compiler_collect_annotations(struct compiler *c, asdl_stmt_seq *stmts, + int *annotations_len) +{ + for (int i = 0; i < asdl_seq_LEN(stmts); i++) { + stmt_ty st = (stmt_ty)asdl_seq_GET(stmts, i); + switch (st->kind) { + case AnnAssign_kind: + if (st->v.AnnAssign.target->kind == Name_kind) { + PyObject *mangled = _Py_Mangle(c->u->u_private, st->v.AnnAssign.target->v.Name.id); + ADDOP_LOAD_CONST_NEW(c, LOC(st), mangled); + VISIT(c, expr, st->v.AnnAssign.annotation); + *annotations_len += 1; + } + break; + case For_kind: + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.For.body, annotations_len)); + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.For.orelse, annotations_len)); + break; + case AsyncFor_kind: + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.AsyncFor.body, annotations_len)); + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.AsyncFor.orelse, annotations_len)); + break; + case While_kind: + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.While.body, annotations_len)); + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.While.orelse, annotations_len)); + break; + case If_kind: + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.If.body, annotations_len)); + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.If.orelse, annotations_len)); + break; + case With_kind: + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.With.body, annotations_len)); + break; + case AsyncWith_kind: + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.AsyncWith.body, annotations_len)); + break; + case Match_kind: + for (int j = 0; j < asdl_seq_LEN(st->v.Match.cases); j++) { + match_case_ty match_case = (match_case_ty)asdl_seq_GET( + st->v.Match.cases, j); + RETURN_IF_ERROR(compiler_collect_annotations(c, match_case->body, annotations_len)); + } + break; + case Try_kind: + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.Try.body, annotations_len)); + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.Try.orelse, annotations_len)); + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.Try.finalbody, annotations_len)); + for (int j = 0; j < asdl_seq_LEN(st->v.Try.handlers); j++) { + excepthandler_ty handler = (excepthandler_ty)asdl_seq_GET( + st->v.Try.handlers, j); + RETURN_IF_ERROR(compiler_collect_annotations(c, handler->v.ExceptHandler.body, annotations_len)); + } + break; + case TryStar_kind: + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.TryStar.body, annotations_len)); + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.TryStar.orelse, annotations_len)); + RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.TryStar.finalbody, annotations_len)); + for (int j = 0; j < asdl_seq_LEN(st->v.TryStar.handlers); j++) { + excepthandler_ty handler = (excepthandler_ty)asdl_seq_GET( + st->v.Try.handlers, j); + RETURN_IF_ERROR(compiler_collect_annotations(c, handler->v.ExceptHandler.body, annotations_len)); + } + break; + default: + break; + } + } + return SUCCESS; + +} + /* Compile a sequence of statements, checking for a docstring and for annotations. */ @@ -1639,17 +1759,11 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) { /* Set current line number to the line number of first statement. - This way line number for SETUP_ANNOTATIONS will always - coincide with the line number of first "real" statement in module. If body is empty, then lineno will be set later in optimize_and_assemble. */ if (c->u->u_scope_type == COMPILER_SCOPE_MODULE && asdl_seq_LEN(stmts)) { stmt_ty st = (stmt_ty)asdl_seq_GET(stmts, 0); loc = LOC(st); } - /* Every annotated class and module should have __annotations__. */ - if (find_ann(stmts)) { - ADDOP(c, loc, SETUP_ANNOTATIONS); - } if (!asdl_seq_LEN(stmts)) { return SUCCESS; } @@ -1674,6 +1788,21 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) for (Py_ssize_t i = first_instr; i < asdl_seq_LEN(stmts); i++) { VISIT(c, stmt, (stmt_ty)asdl_seq_GET(stmts, i)); } + if (c->u->u_ste->ste_annotation_block != NULL) { + NEW_JUMP_TARGET_LABEL(c, raise_notimp); + void *key = (void *)((uintptr_t)c->u->u_ste->ste_id + 1); + RETURN_IF_ERROR(compiler_setup_annotations_scope(c, loc, key, raise_notimp)); + int annotations_len = 0; + RETURN_IF_ERROR( + compiler_collect_annotations(c, stmts, &annotations_len) + ); + RETURN_IF_ERROR( + compiler_leave_annotations_scope(c, loc, annotations_len, raise_notimp) + ); + RETURN_IF_ERROR( + compiler_nameop(c, loc, &_Py_ID(__annotate__), Store) + ); + } return SUCCESS; } @@ -2008,22 +2137,9 @@ compiler_visit_annotations(struct compiler *c, location loc, NEW_JUMP_TARGET_LABEL(c, raise_notimp); if (!future_annotations && ste->ste_annotations_used) { - PyObject *annotations_name = PyUnicode_FromFormat("", ste->ste_name); - if (!annotations_name) { - return ERROR; - } - if (compiler_enter_scope(c, annotations_name, COMPILER_SCOPE_ANNOTATIONS, - (void *)args, loc.lineno) == -1) { - Py_DECREF(annotations_name); - return ERROR; - } - Py_DECREF(annotations_name); - c->u->u_metadata.u_posonlyargcount = 1; - _Py_DECLARE_STR(format, ".format"); - ADDOP_I(c, loc, LOAD_FAST, 0); - ADDOP_LOAD_CONST(c, loc, _PyLong_GetOne()); - ADDOP_I(c, loc, COMPARE_OP, (Py_EQ << 5) | compare_masks[Py_EQ]); - ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, raise_notimp); + RETURN_IF_ERROR( + compiler_setup_annotations_scope(c, loc, (void *)args, raise_notimp) + ); } RETURN_IF_ERROR( @@ -2057,21 +2173,9 @@ compiler_visit_annotations(struct compiler *c, location loc, else { assert(ste != NULL); if (ste->ste_annotations_used) { - ADDOP_I(c, loc, BUILD_MAP, annotations_len); - ADDOP_IN_SCOPE(c, loc, RETURN_VALUE); - USE_LABEL(c, raise_notimp); - ADDOP_IN_SCOPE(c, loc, LOAD_ASSERTION_ERROR); - ADDOP_I(c, loc, RAISE_VARARGS, 1); - PyCodeObject *co = optimize_and_assemble(c, 1); - compiler_exit_scope(c); - if (co == NULL) { - return ERROR; - } - if (compiler_make_closure(c, loc, co, 0) < 0) { - Py_DECREF(co); - return ERROR; - } - Py_DECREF(co); + RETURN_IF_ERROR( + compiler_leave_annotations_scope(c, loc, annotations_len, raise_notimp) + ); return MAKE_FUNCTION_ANNOTATE; } } @@ -6485,23 +6589,6 @@ check_ann_expr(struct compiler *c, expr_ty e) return SUCCESS; } -static int -check_annotation(struct compiler *c, stmt_ty s) -{ - /* Annotations of complex targets does not produce anything - under annotations future */ - if (c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) { - return SUCCESS; - } - - /* Annotations are only evaluated in a module or class. */ - if (c->u->u_scope_type == COMPILER_SCOPE_MODULE || - c->u->u_scope_type == COMPILER_SCOPE_CLASS) { - return check_ann_expr(c, s->v.AnnAssign.annotation); - } - return SUCCESS; -} - static int check_ann_subscr(struct compiler *c, expr_ty e) { @@ -6537,7 +6624,6 @@ compiler_annassign(struct compiler *c, stmt_ty s) { location loc = LOC(s); expr_ty targ = s->v.AnnAssign.target; - PyObject* mangled; assert(s->kind == AnnAssign_kind); @@ -6551,21 +6637,6 @@ compiler_annassign(struct compiler *c, stmt_ty s) if (forbidden_name(c, loc, targ->v.Name.id, Store)) { return ERROR; } - /* If we have a simple name in a module or class, store annotation. */ - // if (s->v.AnnAssign.simple && - // (c->u->u_scope_type == COMPILER_SCOPE_MODULE || - // c->u->u_scope_type == COMPILER_SCOPE_CLASS)) { - // if (c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) { - // VISIT(c, annexpr, s->v.AnnAssign.annotation) - // } - // else { - // VISIT(c, expr, s->v.AnnAssign.annotation); - // } - // ADDOP_NAME(c, loc, LOAD_NAME, &_Py_ID(__annotations__), names); - // mangled = _Py_Mangle(c->u->u_private, targ->v.Name.id); - // ADDOP_LOAD_CONST_NEW(c, loc, mangled); - // ADDOP(c, loc, STORE_SUBSCR); - // } break; case Attribute_kind: if (forbidden_name(c, loc, targ->v.Attribute.attr, Store)) { @@ -6589,10 +6660,7 @@ compiler_annassign(struct compiler *c, stmt_ty s) targ->kind); return ERROR; } - /* Annotation is evaluated last. */ - // if (!s->v.AnnAssign.simple && check_annotation(c, s) < 0) { - // return ERROR; - // } + /* For non-simple AnnAssign, the annotation is not evaluated. */ return SUCCESS; } diff --git a/Python/symtable.c b/Python/symtable.c index 438751ac1aec8d..0f594c27186d5b 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -2476,7 +2476,6 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, VISIT_QUIT(st, 0); } else { - printf("enter block %p\n", key); if (st->st_cur->ste_annotation_block == NULL) { if (!symtable_enter_block(st, &_Py_ID(_annotation), AnnotationBlock, key, LOCATION(annotation))) { From afae5c07ede77b73d2d423e86e8cc57923df186b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 16:10:56 -0400 Subject: [PATCH 012/105] attempts --- Lib/_pyrepl/readline.py | 4 ++-- Lib/inspect.py | 8 +------- Python/compile.c | 13 +++++++++++++ Python/future.c | 4 +--- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 0adecf235a4eb4..5771cbb404b8b4 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -84,8 +84,8 @@ @dataclass class ReadlineConfig: - readline_completer: Completer | None = readline.get_completer() - completer_delims: frozenset[str] = frozenset(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?") + #readline_completer: Completer | None = readline.get_completer() + pass #completer_delims: frozenset[str] = frozenset(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?") @dataclass(kw_only=True) diff --git a/Lib/inspect.py b/Lib/inspect.py index e6e49a4ffa673a..0ca3c3ff44e3ba 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -220,13 +220,7 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False): """ if isinstance(obj, type): # class - obj_dict = getattr(obj, '__dict__', None) - if obj_dict and hasattr(obj_dict, 'get'): - ann = obj_dict.get('__annotations__', None) - if isinstance(ann, types.GetSetDescriptorType): - ann = None - else: - ann = None + ann = obj.__annotations__ obj_globals = None module_name = getattr(obj, '__module__', None) diff --git a/Python/compile.c b/Python/compile.c index cec332d12d47e4..be6f2b6b5280dd 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -4403,6 +4403,7 @@ compiler_nameop(struct compiler *c, location loc, } /* XXX Leave assert here, but handle __doc__ and the like better */ + printf("name %s %s\n", PyUnicode_AsUTF8(name), PyUnicode_AsUTF8(c->u->u_ste->ste_name)); assert(scope || PyUnicode_READ_CHAR(name, 0) == '_'); switch (optype) { @@ -6624,6 +6625,7 @@ compiler_annassign(struct compiler *c, stmt_ty s) { location loc = LOC(s); expr_ty targ = s->v.AnnAssign.target; + PyObject *mangled; assert(s->kind == AnnAssign_kind); @@ -6637,6 +6639,17 @@ compiler_annassign(struct compiler *c, stmt_ty s) if (forbidden_name(c, loc, targ->v.Name.id, Store)) { return ERROR; } + /* If we have a simple name in a module or class, store annotation. */ + if (c->c_future.ff_features & CO_FUTURE_ANNOTATIONS && + s->v.AnnAssign.simple && + (c->u->u_scope_type == COMPILER_SCOPE_MODULE || + c->u->u_scope_type == COMPILER_SCOPE_CLASS)) { + VISIT(c, annexpr, s->v.AnnAssign.annotation); + ADDOP_NAME(c, loc, LOAD_NAME, &_Py_ID(__annotations__), names); + mangled = _Py_Mangle(c->u->u_private, targ->v.Name.id); + ADDOP_LOAD_CONST_NEW(c, loc, mangled); + ADDOP(c, loc, STORE_SUBSCR); + } break; case Attribute_kind: if (forbidden_name(c, loc, targ->v.Attribute.attr, Store)) { diff --git a/Python/future.c b/Python/future.c index 9e528bf121346a..8d94d515605dcd 100644 --- a/Python/future.c +++ b/Python/future.c @@ -37,9 +37,7 @@ future_check_features(_PyFutureFeatures *ff, stmt_ty s, PyObject *filename) } else if (strcmp(feature, FUTURE_GENERATOR_STOP) == 0) { continue; } else if (strcmp(feature, FUTURE_ANNOTATIONS) == 0) { - // For now, we ignore this future. We may make it stringify again - // in the future. - // ff->ff_features |= CO_FUTURE_ANNOTATIONS; + ff->ff_features |= CO_FUTURE_ANNOTATIONS; } else if (strcmp(feature, "braces") == 0) { PyErr_SetString(PyExc_SyntaxError, "not a chance"); From e5a7b1a86faad709e2f16bbc55a2d535cc29ccdf Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 16:43:26 -0400 Subject: [PATCH 013/105] Fixes --- Lib/_pyrepl/readline.py | 4 ++-- Python/compile.c | 20 ++++++++++++++------ Python/symtable.c | 9 +++++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 5771cbb404b8b4..0adecf235a4eb4 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -84,8 +84,8 @@ @dataclass class ReadlineConfig: - #readline_completer: Completer | None = readline.get_completer() - pass #completer_delims: frozenset[str] = frozenset(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?") + readline_completer: Completer | None = readline.get_completer() + completer_delims: frozenset[str] = frozenset(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?") @dataclass(kw_only=True) diff --git a/Python/compile.c b/Python/compile.c index be6f2b6b5280dd..5be0beadacc162 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1759,11 +1759,17 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) { /* Set current line number to the line number of first statement. + This way line number for SETUP_ANNOTATIONS will always + coincide with the line number of first "real" statement in module. If body is empty, then lineno will be set later in optimize_and_assemble. */ if (c->u->u_scope_type == COMPILER_SCOPE_MODULE && asdl_seq_LEN(stmts)) { stmt_ty st = (stmt_ty)asdl_seq_GET(stmts, 0); loc = LOC(st); } + /* Every annotated class and module should have __annotations__. */ + if ((c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) && find_ann(stmts)) { + ADDOP(c, loc, SETUP_ANNOTATIONS); + } if (!asdl_seq_LEN(stmts)) { return SUCCESS; } @@ -1788,7 +1794,8 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) for (Py_ssize_t i = first_instr; i < asdl_seq_LEN(stmts); i++) { VISIT(c, stmt, (stmt_ty)asdl_seq_GET(stmts, i)); } - if (c->u->u_ste->ste_annotation_block != NULL) { + if (!(c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) && + c->u->u_ste->ste_annotation_block != NULL) { NEW_JUMP_TARGET_LABEL(c, raise_notimp); void *key = (void *)((uintptr_t)c->u->u_ste->ste_id + 1); RETURN_IF_ERROR(compiler_setup_annotations_scope(c, loc, key, raise_notimp)); @@ -2167,8 +2174,10 @@ compiler_visit_annotations(struct compiler *c, location loc, compiler_visit_argannotation(c, &_Py_ID(return), returns, &annotations_len, loc)); if (future_annotations) { - ADDOP_I(c, loc, BUILD_TUPLE, annotations_len * 2); - return MAKE_FUNCTION_ANNOTATIONS; + if (annotations_len) { + ADDOP_I(c, loc, BUILD_TUPLE, annotations_len * 2); + return MAKE_FUNCTION_ANNOTATIONS; + } } else { assert(ste != NULL); @@ -4403,7 +4412,6 @@ compiler_nameop(struct compiler *c, location loc, } /* XXX Leave assert here, but handle __doc__ and the like better */ - printf("name %s %s\n", PyUnicode_AsUTF8(name), PyUnicode_AsUTF8(c->u->u_ste->ste_name)); assert(scope || PyUnicode_READ_CHAR(name, 0) == '_'); switch (optype) { @@ -6625,7 +6633,7 @@ compiler_annassign(struct compiler *c, stmt_ty s) { location loc = LOC(s); expr_ty targ = s->v.AnnAssign.target; - PyObject *mangled; + PyObject* mangled; assert(s->kind == AnnAssign_kind); @@ -6648,7 +6656,7 @@ compiler_annassign(struct compiler *c, stmt_ty s) ADDOP_NAME(c, loc, LOAD_NAME, &_Py_ID(__annotations__), names); mangled = _Py_Mangle(c->u->u_private, targ->v.Name.id); ADDOP_LOAD_CONST_NEW(c, loc, mangled); - ADDOP(c, loc, STORE_SUBSCR); + ADDOP(c, loc, STORE_SUBSCR); } break; case Attribute_kind: diff --git a/Python/symtable.c b/Python/symtable.c index 0f594c27186d5b..6895393b0d5c27 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -2470,10 +2470,11 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, struct _symtable_entry *parent_ste, void *key) { int future_annotations = st->st_future->ff_features & CO_FUTURE_ANNOTATIONS; - if (future_annotations && - !symtable_enter_block(st, &_Py_ID(_annotation), AnnotationBlock, - key, LOCATION(annotation))) { - VISIT_QUIT(st, 0); + if (future_annotations) { + if(!symtable_enter_block(st, &_Py_ID(_annotation), AnnotationBlock, + key, LOCATION(annotation))) { + VISIT_QUIT(st, 0); + } } else { if (st->st_cur->ste_annotation_block == NULL) { From 31a4471a3210628c67179a62cc727045ff956ffc Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 17:00:28 -0400 Subject: [PATCH 014/105] Fix code object name --- Python/compile.c | 11 +++++++---- Python/symtable.c | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Python/compile.c b/Python/compile.c index 5be0beadacc162..8a83799071bd0a 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1635,10 +1635,11 @@ compiler_unwind_fblock_stack(struct compiler *c, location *ploc, static int compiler_setup_annotations_scope(struct compiler *c, location loc, - void *key, jump_target_label label) + void *key, jump_target_label label, + PyObject *name) { PyObject *annotations_name = PyUnicode_FromFormat( - "", c->u->u_ste->ste_name); + "", name); if (!annotations_name) { return ERROR; } @@ -1798,7 +1799,8 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) c->u->u_ste->ste_annotation_block != NULL) { NEW_JUMP_TARGET_LABEL(c, raise_notimp); void *key = (void *)((uintptr_t)c->u->u_ste->ste_id + 1); - RETURN_IF_ERROR(compiler_setup_annotations_scope(c, loc, key, raise_notimp)); + RETURN_IF_ERROR(compiler_setup_annotations_scope(c, loc, key, raise_notimp, + c->u->u_ste->ste_name)); int annotations_len = 0; RETURN_IF_ERROR( compiler_collect_annotations(c, stmts, &annotations_len) @@ -2145,7 +2147,8 @@ compiler_visit_annotations(struct compiler *c, location loc, if (!future_annotations && ste->ste_annotations_used) { RETURN_IF_ERROR( - compiler_setup_annotations_scope(c, loc, (void *)args, raise_notimp) + compiler_setup_annotations_scope(c, loc, (void *)args, raise_notimp, + ste->ste_name) ); } diff --git a/Python/symtable.c b/Python/symtable.c index 6895393b0d5c27..0d7baf71072b14 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -2478,7 +2478,7 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, } else { if (st->st_cur->ste_annotation_block == NULL) { - if (!symtable_enter_block(st, &_Py_ID(_annotation), AnnotationBlock, + if (!symtable_enter_block(st, parent_ste->ste_name, AnnotationBlock, key, LOCATION(annotation))) { VISIT_QUIT(st, 0); } From fbb1d888d9af1818a08b2ffd26c3e05baa4f8515 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 20:20:56 -0400 Subject: [PATCH 015/105] Start fixing test_type_annotations --- Lib/test/test_type_annotations.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index ef1569487de18c..2ad4ceb4dfcecc 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -3,6 +3,7 @@ import unittest from test.support import run_code + class TypeAnnotationTests(unittest.TestCase): def test_lazy_create_annotations(self): @@ -49,6 +50,7 @@ def test_annotations_are_created_correctly(self): class C: a:int=3 b:str=4 + self.assertEqual(C.__annotations__, {"a": int, "b": str}) self.assertTrue("__annotations__" in C.__dict__) del C.__annotations__ self.assertFalse("__annotations__" in C.__dict__) @@ -117,7 +119,9 @@ def check(self, code: str): if scope == "class": annotations = ns["C"].__annotations__ else: - annotations = ns["__annotations__"] + mod = types.ModuleType("mod") + mod.__dict__.update(ns) + annotations = mod.__annotations__ self.assertEqual(annotations, {"x": int}) def test_top_level(self): @@ -256,3 +260,22 @@ def check_annotations(self, f): # Setting f.__annotations__ also clears __annotate__ f.__annotations__ = {"z": 43} self.assertIs(f.__annotate__, None) + + +class DeferredEvaluationTests(unittest.TestCase): + def test_function(self): + def func(x: undefined, /, y: undefined, *args: undefined, z: undefined, **kwargs: undefined) -> undefined: + pass + + with self.assertRaises(NameError): + func.__annotations__ + + undefined = 1 + self.assertEqual(func.__annotations__, { + "x": 1, + "y": 1, + "args": 1, + "z": 1, + "kwargs": 1, + "return": 1, + }) From f452eb2e9233f0e7884b648f45a53cb6428fbaf4 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 20:38:12 -0400 Subject: [PATCH 016/105] Fix class scoping --- Lib/test/test_type_annotations.py | 48 +++++++++++++++++++++++++++++++ Python/symtable.c | 18 +++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 2ad4ceb4dfcecc..3b9552f98ea4c7 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -279,3 +279,51 @@ def func(x: undefined, /, y: undefined, *args: undefined, z: undefined, **kwargs "kwargs": 1, "return": 1, }) + + def test_async_function(self): + async def func(x: undefined, /, y: undefined, *args: undefined, z: undefined, **kwargs: undefined) -> undefined: + pass + + with self.assertRaises(NameError): + func.__annotations__ + + undefined = 1 + self.assertEqual(func.__annotations__, { + "x": 1, + "y": 1, + "args": 1, + "z": 1, + "kwargs": 1, + "return": 1, + }) + + def test_class(self): + class X: + a: undefined + + with self.assertRaises(NameError): + X.__annotations__ + + undefined = 1 + self.assertEqual(X.__annotations__, {"a": 1}) + + def test_module(self): + ns = run_code("x: undefined = 1") + anno = ns["__annotate__"] + with self.assertRaises(AssertionError): # TODO NotImplementedError + anno(2) + + with self.assertRaises(NameError): + anno(1) + + ns["undefined"] = 1 + self.assertEqual(anno(1), {"x": 1}) + + def test_class_scoping(self): + class Outer: + def meth(self, x: Nested): ... + x: Nested + class Nested: ... + + self.assertEqual(Outer.meth.__annotations__, {"x": Outer.Nested}) + self.assertEqual(Outer.__annotations__, {"x": Outer.Nested}) diff --git a/Python/symtable.c b/Python/symtable.c index 0d7baf71072b14..4aa18518ae688b 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -542,6 +542,7 @@ int _PyST_IsFunctionLike(PySTEntryObject *ste) { return ste->ste_type == FunctionBlock + || ste->ste_type == AnnotationBlock || ste->ste_type == TypeVarBoundBlock || ste->ste_type == TypeAliasBlock || ste->ste_type == TypeParamBlock; @@ -2478,12 +2479,20 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, } else { if (st->st_cur->ste_annotation_block == NULL) { + _Py_block_ty current_type = st->st_cur->ste_type; if (!symtable_enter_block(st, parent_ste->ste_name, AnnotationBlock, key, LOCATION(annotation))) { VISIT_QUIT(st, 0); } parent_ste->ste_annotation_block = (struct _symtable_entry *)Py_NewRef(st->st_cur); + if (current_type == ClassBlock) { + st->st_cur->ste_can_see_class_scope = 1; + if (!symtable_add_def(st, &_Py_ID(__classdict__), USE, LOCATION(annotation))) { + return 0; + } + } + _Py_DECLARE_STR(format, ".format"); // We need to insert code that reads this "parameter" to the function. if (!symtable_add_def(st, &_Py_STR(format), DEF_PARAM, @@ -2531,10 +2540,17 @@ static int symtable_visit_annotations(struct symtable *st, stmt_ty o, arguments_ty a, expr_ty returns, struct _symtable_entry *function_ste) { + _Py_block_ty current_type = st->st_cur->ste_type; if (!symtable_enter_block(st, function_ste->ste_name, AnnotationBlock, (void *)a, LOCATION(o))) { VISIT_QUIT(st, 0); } + if (current_type == ClassBlock) { + st->st_cur->ste_can_see_class_scope = 1; + if (!symtable_add_def(st, &_Py_ID(__classdict__), USE, LOCATION(o))) { + return 0; + } + } _Py_DECLARE_STR(format, ".format"); // We need to insert code that reads this "parameter" to the function. if (!symtable_add_def(st, &_Py_STR(format), DEF_PARAM, LOCATION(o))) { @@ -2793,7 +2809,7 @@ symtable_visit_dictcomp(struct symtable *st, expr_ty e) static int symtable_raise_if_annotation_block(struct symtable *st, const char *name, expr_ty e) { - enum _block_type type = st->st_cur->ste_type; + _Py_block_ty type = st->st_cur->ste_type; if (type == AnnotationBlock) PyErr_Format(PyExc_SyntaxError, ANNOTATION_NOT_ALLOWED, name); else if (type == TypeVarBoundBlock) From 8c4b4e3c0fb49d1f5f3e8853d39cd2be18e98050 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 20:40:08 -0400 Subject: [PATCH 017/105] unyielding --- Lib/test/test_type_annotations.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 3b9552f98ea4c7..4374afd6f57efb 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -1,7 +1,7 @@ import textwrap import types import unittest -from test.support import run_code +from test.support import run_code, check_syntax_error class TypeAnnotationTests(unittest.TestCase): @@ -327,3 +327,9 @@ class Nested: ... self.assertEqual(Outer.meth.__annotations__, {"x": Outer.Nested}) self.assertEqual(Outer.__annotations__, {"x": Outer.Nested}) + + def test_no_exotic_expressions(self): + check_syntax_error(self, "def func(x: (yield)): ...", "yield expression cannot be used within an annotation") + check_syntax_error(self, "def func(x: (yield from x)): ...", "yield expression cannot be used within an annotation") + check_syntax_error(self, "def func(x: (y := 3)): ...", "named expression cannot be used within an annotation") + check_syntax_error(self, "def func(x: (await 42)): ...", "await expression cannot be used within an annotation") From cbf9a3da172c7648006ca50868a5b425249b12bb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 20:56:10 -0400 Subject: [PATCH 018/105] Fix test_typing --- Lib/test/test_typing.py | 4 ++-- Lib/test/typinganndata/ann_module.py | 4 ---- Lib/typing.py | 19 +++++++++++++++---- Python/compile.c | 5 +++-- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index dac55ceb9e99e0..9800b3b6a7da29 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6634,7 +6634,7 @@ def test_get_type_hints_from_various_objects(self): gth(None) def test_get_type_hints_modules(self): - ann_module_type_hints = {1: 2, 'f': Tuple[int, int], 'x': int, 'y': str, 'u': int | float} + ann_module_type_hints = {'f': Tuple[int, int], 'x': int, 'y': str, 'u': int | float} self.assertEqual(gth(ann_module), ann_module_type_hints) self.assertEqual(gth(ann_module2), {}) self.assertEqual(gth(ann_module3), {}) @@ -6652,7 +6652,7 @@ def test_get_type_hints_classes(self): self.assertEqual(gth(ann_module.C), # gth will find the right globalns {'y': Optional[ann_module.C]}) self.assertIsInstance(gth(ann_module.j_class), dict) - self.assertEqual(gth(ann_module.M), {'123': 123, 'o': type}) + self.assertEqual(gth(ann_module.M), {'o': type}) self.assertEqual(gth(ann_module.D), {'j': str, 'k': str, 'y': Optional[ann_module.C]}) self.assertEqual(gth(ann_module.Y), {'z': int}) diff --git a/Lib/test/typinganndata/ann_module.py b/Lib/test/typinganndata/ann_module.py index 5081e6b58345a9..e1a1792cb4a867 100644 --- a/Lib/test/typinganndata/ann_module.py +++ b/Lib/test/typinganndata/ann_module.py @@ -8,8 +8,6 @@ from typing import Optional from functools import wraps -__annotations__[1] = 2 - class C: x = 5; y: Optional['C'] = None @@ -18,8 +16,6 @@ class C: x: int = 5; y: str = x; f: Tuple[int, int] class M(type): - - __annotations__['123'] = 123 o: type = object (pars): bool = True diff --git a/Lib/typing.py b/Lib/typing.py index be49aa63464f05..dcbacde3099cfc 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2412,7 +2412,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): base_globals = getattr(sys.modules.get(base.__module__, None), '__dict__', {}) else: base_globals = globalns - ann = base.__dict__.get('__annotations__', {}) + ann = getattr(base, '__annotations__', {}) if isinstance(ann, types.GetSetDescriptorType): ann = {} base_locals = dict(vars(base)) if localns is None else localns @@ -2970,7 +2970,12 @@ def __new__(cls, typename, bases, ns): raise TypeError( 'can only inherit from a NamedTuple type and Generic') bases = tuple(tuple if base is _NamedTuple else base for base in bases) - types = ns.get('__annotations__', {}) + if "__annotations__" in ns: + types = ns["__annotations__"] + elif "__annotate__" in ns: + types = ns["__annotate__"](1) + else: + types = {} default_names = [] for field_name in types: if field_name in ns: @@ -3131,7 +3136,12 @@ def __new__(cls, name, bases, ns, total=True): tp_dict.__orig_bases__ = bases annotations = {} - own_annotations = ns.get('__annotations__', {}) + if "__annotations__" in ns: + own_annotations = ns["__annotations__"] + elif "__annotate__" in ns: + own_annotations = ns["__annotate__"](1) + else: + own_annotations = {} msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" own_annotations = { n: _type_check(tp, msg, module=tp_dict.__module__) @@ -3143,7 +3153,8 @@ def __new__(cls, name, bases, ns, total=True): mutable_keys = set() for base in bases: - annotations.update(base.__dict__.get('__annotations__', {})) + # TODO: preserve laziness + annotations.update(base.__annotations__) base_required = base.__dict__.get('__required_keys__', set()) required_keys |= base_required diff --git a/Python/compile.c b/Python/compile.c index 8a83799071bd0a..4afa9ed74024bc 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1688,7 +1688,8 @@ compiler_collect_annotations(struct compiler *c, asdl_stmt_seq *stmts, stmt_ty st = (stmt_ty)asdl_seq_GET(stmts, i); switch (st->kind) { case AnnAssign_kind: - if (st->v.AnnAssign.target->kind == Name_kind) { + // Only "simple" names (i.e., unparenthesized names) are stored. + if (st->v.AnnAssign.simple) { PyObject *mangled = _Py_Mangle(c->u->u_private, st->v.AnnAssign.target->v.Name.id); ADDOP_LOAD_CONST_NEW(c, LOC(st), mangled); VISIT(c, expr, st->v.AnnAssign.annotation); @@ -6651,7 +6652,7 @@ compiler_annassign(struct compiler *c, stmt_ty s) return ERROR; } /* If we have a simple name in a module or class, store annotation. */ - if (c->c_future.ff_features & CO_FUTURE_ANNOTATIONS && + if ((c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) && s->v.AnnAssign.simple && (c->u->u_scope_type == COMPILER_SCOPE_MODULE || c->u->u_scope_type == COMPILER_SCOPE_CLASS)) { From 5d182fceeadec2fe96425f1500a19aa2ff5007dc Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 20:57:53 -0400 Subject: [PATCH 019/105] Fix test_type_parmas --- Python/symtable.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Python/symtable.c b/Python/symtable.c index 4aa18518ae688b..579d047d526218 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -2540,12 +2540,13 @@ static int symtable_visit_annotations(struct symtable *st, stmt_ty o, arguments_ty a, expr_ty returns, struct _symtable_entry *function_ste) { + int is_in_class = st->st_cur->ste_can_see_class_scope; _Py_block_ty current_type = st->st_cur->ste_type; if (!symtable_enter_block(st, function_ste->ste_name, AnnotationBlock, (void *)a, LOCATION(o))) { VISIT_QUIT(st, 0); } - if (current_type == ClassBlock) { + if (is_in_class || current_type == ClassBlock) { st->st_cur->ste_can_see_class_scope = 1; if (!symtable_add_def(st, &_Py_ID(__classdict__), USE, LOCATION(o))) { return 0; From ce98c1907a0c732391114fc66f36e996bbb80fea Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 21:16:45 -0400 Subject: [PATCH 020/105] Add test, add to inspect --- Lib/inspect.py | 11 ++++++++++ Lib/test/test_inspect/test_inspect.py | 5 +++++ Lib/test/test_type_annotations.py | 31 ++++++++++++++++++++++----- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 0ca3c3ff44e3ba..725c5efb4787d1 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -38,6 +38,7 @@ "AGEN_CREATED", "AGEN_RUNNING", "AGEN_SUSPENDED", + "AnnotationsFormat", "ArgInfo", "Arguments", "Attribute", @@ -61,6 +62,7 @@ "ClassFoundException", "ClosureVars", "EndOfBlock", + "FORWARDREF", "FrameInfo", "FullArgSpec", "GEN_CLOSED", @@ -134,9 +136,11 @@ "istraceback", "markcoroutinefunction", "signature", + "SOURCE", "stack", "trace", "unwrap", + "VALUE", "walktree", ] @@ -173,6 +177,13 @@ TPFLAGS_IS_ABSTRACT = 1 << 20 +@enum.global_enum +class AnnotationsFormat(enum.IntEnum): + VALUE = 1 + FORWARDREF = 2 + SOURCE = 3 + + def get_annotations(obj, *, globals=None, locals=None, eval_str=False): """Compute the annotations dict for an object. diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 011d42f34b6461..546c9fea390abd 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -1592,6 +1592,11 @@ class C(metaclass=M): attrs = [a[0] for a in inspect.getmembers(C)] self.assertNotIn('missing', attrs) + def test_annotation_format(self): + self.assertIs(inspect.VALUE, inspect.AnnotationsFormat.VALUE) + self.assertEqual(inspect.VALUE.value, 1) + self.assertEqual(inspect.VALUE, 1) + def test_get_annotations_with_stock_annotations(self): def foo(a:int, b:str): pass self.assertEqual(inspect.get_annotations(foo), {'a': int, 'b': str}) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 4374afd6f57efb..f84f34e394931a 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -108,6 +108,13 @@ class D(metaclass=C): self.assertEqual(D.__annotations__, {}) +def build_module(code: str, name: str = "top") -> types.ModuleType: + ns = run_code(code) + mod = types.ModuleType(name) + mod.__dict__.update(ns) + return mod + + class TestSetupAnnotations(unittest.TestCase): def check(self, code: str): code = textwrap.dedent(code) @@ -115,13 +122,10 @@ def check(self, code: str): with self.subTest(scope=scope): if scope == "class": code = f"class C:\n{textwrap.indent(code, ' ')}" - ns = run_code(code) - if scope == "class": + ns = run_code(code) annotations = ns["C"].__annotations__ else: - mod = types.ModuleType("mod") - mod.__dict__.update(ns) - annotations = mod.__annotations__ + annotations = build_module(code).__annotations__ self.assertEqual(annotations, {"x": int}) def test_top_level(self): @@ -333,3 +337,20 @@ def test_no_exotic_expressions(self): check_syntax_error(self, "def func(x: (yield from x)): ...", "yield expression cannot be used within an annotation") check_syntax_error(self, "def func(x: (y := 3)): ...", "named expression cannot be used within an annotation") check_syntax_error(self, "def func(x: (await 42)): ...", "await expression cannot be used within an annotation") + + def test_generated_annotate(self): + def func(x: int): + pass + class X: + x: int + mod = build_module("x: int") + for obj in (func, X, mod): + with self.subTest(obj=obj): + annotate = obj.__annotate__ + self.assertIsInstance(annotate, types.FunctionType) + self.assertEqual(annotate.__name__, f"") + with self.assertRaises(AssertionError): # TODO NotImplementedError + annotate(2) + with self.assertRaises(AssertionError): # TODO NotImplementedError + annotate(None) + self.assertEqual(annotate(1), {"x": int}) From e0578fcf1d8ecd3935dc3331fccf92fb68a2cb5f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 21:18:33 -0400 Subject: [PATCH 021/105] Use inspect constants --- Lib/test/test_type_annotations.py | 5 +++-- Lib/typing.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index f84f34e394931a..43b81f1470453e 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -1,3 +1,4 @@ +import inspect import textwrap import types import unittest @@ -350,7 +351,7 @@ class X: self.assertIsInstance(annotate, types.FunctionType) self.assertEqual(annotate.__name__, f"") with self.assertRaises(AssertionError): # TODO NotImplementedError - annotate(2) + annotate(inspect.FORWARDREF) with self.assertRaises(AssertionError): # TODO NotImplementedError annotate(None) - self.assertEqual(annotate(1), {"x": int}) + self.assertEqual(annotate(inspect.VALUE), {"x": int}) diff --git a/Lib/typing.py b/Lib/typing.py index dcbacde3099cfc..15307f28de6c8a 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -24,6 +24,7 @@ import collections.abc import copyreg import functools +import inspect import operator import sys import types @@ -2973,7 +2974,7 @@ def __new__(cls, typename, bases, ns): if "__annotations__" in ns: types = ns["__annotations__"] elif "__annotate__" in ns: - types = ns["__annotate__"](1) + types = ns["__annotate__"](inspect.VALUE) else: types = {} default_names = [] @@ -3139,7 +3140,7 @@ def __new__(cls, name, bases, ns, total=True): if "__annotations__" in ns: own_annotations = ns["__annotations__"] elif "__annotate__" in ns: - own_annotations = ns["__annotate__"](1) + own_annotations = ns["__annotate__"](inspect.VALUE) else: own_annotations = {} msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" From ed161677081e882529a327ea55d8c51b515370ed Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 21:24:51 -0400 Subject: [PATCH 022/105] test_grammar tweaks --- Lib/test/test_grammar.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_grammar.py b/Lib/test/test_grammar.py index c72f4387108ca8..388bc72fe2385e 100644 --- a/Lib/test/test_grammar.py +++ b/Lib/test/test_grammar.py @@ -459,9 +459,9 @@ class CC(metaclass=CMeta): def test_var_annot_module_semantics(self): self.assertEqual(test.__annotations__, {}) self.assertEqual(ann_module.__annotations__, - {1: 2, 'x': int, 'y': str, 'f': typing.Tuple[int, int], 'u': int | float}) + {'x': int, 'y': str, 'f': typing.Tuple[int, int], 'u': int | float}) self.assertEqual(ann_module.M.__annotations__, - {'123': 123, 'o': type}) + {'o': type}) self.assertEqual(ann_module2.__annotations__, {}) def test_var_annot_in_module(self): @@ -476,13 +476,12 @@ def test_var_annot_in_module(self): ann_module3.D_bad_ann(5) def test_var_annot_simple_exec(self): - gns = {}; lns= {} + gns = {}; lns = {} exec("'docstring'\n" - "__annotations__[1] = 2\n" "x: int = 5\n", gns, lns) - self.assertEqual(lns["__annotations__"], {1: 2, 'x': int}) + self.assertEqual(lns["__annotate__"](inspect.VALUE), {'x': int}) with self.assertRaises(KeyError): - gns['__annotations__'] + gns['__annotate__'] def test_var_annot_custom_maps(self): # tests with custom locals() and __annotations__ From f38de202041cd2e20e218a263d9efb6679edd863 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 21:27:47 -0400 Subject: [PATCH 023/105] fix test_positional_only_arg --- Lib/test/test_positional_only_arg.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_positional_only_arg.py b/Lib/test/test_positional_only_arg.py index 1a193814d7535d..ad452c032d1614 100644 --- a/Lib/test/test_positional_only_arg.py +++ b/Lib/test/test_positional_only_arg.py @@ -2,6 +2,7 @@ import dis import pickle +import types import unittest from test.support import check_syntax_error @@ -440,7 +441,9 @@ def f(x: not (int is int), /): ... # without constant folding we end up with # COMPARE_OP(is), IS_OP (0) # with constant folding we should expect a IS_OP (1) - codes = [(i.opname, i.argval) for i in dis.get_instructions(g)] + code_obj = next(const for const in g.__code__.co_consts + if isinstance(const, types.CodeType) and "annotations" in const.co_name) + codes = [(i.opname, i.argval) for i in dis.get_instructions(code_obj)] self.assertNotIn(('UNARY_NOT', None), codes) self.assertIn(('IS_OP', 1), codes) From 87baca29bda95caf736c5ba687e4c18c64e9212d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 21:29:14 -0400 Subject: [PATCH 024/105] Fix test_module --- Lib/test/test_module/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_module/__init__.py b/Lib/test/test_module/__init__.py index 952ba43f72504d..56edd0c637f376 100644 --- a/Lib/test/test_module/__init__.py +++ b/Lib/test/test_module/__init__.py @@ -360,6 +360,8 @@ def test_annotations_are_created_correctly(self): ann_module4 = import_helper.import_fresh_module( 'test.typinganndata.ann_module4', ) + self.assertFalse("__annotations__" in ann_module4.__dict__) + self.assertEqual(ann_module4.__annotations__, {"a": int, "b": str}) self.assertTrue("__annotations__" in ann_module4.__dict__) del ann_module4.__annotations__ self.assertFalse("__annotations__" in ann_module4.__dict__) From 355d3dfee055c9972945aa2b9ef26258c8d41bc6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 21:52:18 -0400 Subject: [PATCH 025/105] Fix symtable tests --- Lib/symtable.py | 2 ++ Lib/test/test_symtable.py | 6 ++++-- Python/compile.c | 11 ++--------- Python/symtable.c | 18 ++++++++++++++++-- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Lib/symtable.py b/Lib/symtable.py index 17f820abd56660..cfd9d0f63bbf5d 100644 --- a/Lib/symtable.py +++ b/Lib/symtable.py @@ -218,6 +218,8 @@ def get_methods(self): if self.__methods is None: d = {} for st in self._table.children: + if st.type == _symtable.TYPE_ANNOTATION: + continue d[st.name] = 1 self.__methods = tuple(d) return self.__methods diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index 92b78a8086a83d..a2e83f40fac812 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -205,12 +205,14 @@ def test_assigned(self): def test_annotated(self): st1 = symtable.symtable('def f():\n x: int\n', 'test', 'exec') - st2 = st1.get_children()[0] + st2 = st1.get_children()[1] + self.assertEqual(st2.get_type(), "function") self.assertTrue(st2.lookup('x').is_local()) self.assertTrue(st2.lookup('x').is_annotated()) self.assertFalse(st2.lookup('x').is_global()) st3 = symtable.symtable('def f():\n x = 1\n', 'test', 'exec') - st4 = st3.get_children()[0] + st4 = st3.get_children()[1] + self.assertEqual(st4.get_type(), "function") self.assertTrue(st4.lookup('x').is_local()) self.assertFalse(st4.lookup('x').is_annotated()) diff --git a/Python/compile.c b/Python/compile.c index 4afa9ed74024bc..2a4dea76973c1c 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1638,17 +1638,10 @@ compiler_setup_annotations_scope(struct compiler *c, location loc, void *key, jump_target_label label, PyObject *name) { - PyObject *annotations_name = PyUnicode_FromFormat( - "", name); - if (!annotations_name) { - return ERROR; - } - if (compiler_enter_scope(c, annotations_name, COMPILER_SCOPE_ANNOTATIONS, + if (compiler_enter_scope(c, name, COMPILER_SCOPE_ANNOTATIONS, key, loc.lineno) == -1) { - Py_DECREF(annotations_name); return ERROR; } - Py_DECREF(annotations_name); c->u->u_metadata.u_posonlyargcount = 1; _Py_DECLARE_STR(format, ".format"); ADDOP_I(c, loc, LOAD_FAST, 0); @@ -1801,7 +1794,7 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) NEW_JUMP_TARGET_LABEL(c, raise_notimp); void *key = (void *)((uintptr_t)c->u->u_ste->ste_id + 1); RETURN_IF_ERROR(compiler_setup_annotations_scope(c, loc, key, raise_notimp, - c->u->u_ste->ste_name)); + c->u->u_ste->ste_annotation_block->ste_name)); int annotations_len = 0; RETURN_IF_ERROR( compiler_collect_annotations(c, stmts, &annotations_len) diff --git a/Python/symtable.c b/Python/symtable.c index 579d047d526218..9b7b09dfe68ade 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -2479,11 +2479,18 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, } else { if (st->st_cur->ste_annotation_block == NULL) { + PyObject *annotations_name = PyUnicode_FromFormat( + "", parent_ste->ste_name); + if (!annotations_name) { + VISIT_QUIT(st, 0); + } _Py_block_ty current_type = st->st_cur->ste_type; - if (!symtable_enter_block(st, parent_ste->ste_name, AnnotationBlock, + if (!symtable_enter_block(st, annotations_name, AnnotationBlock, key, LOCATION(annotation))) { + Py_DECREF(annotations_name); VISIT_QUIT(st, 0); } + Py_DECREF(annotations_name); parent_ste->ste_annotation_block = (struct _symtable_entry *)Py_NewRef(st->st_cur); if (current_type == ClassBlock) { @@ -2540,12 +2547,19 @@ static int symtable_visit_annotations(struct symtable *st, stmt_ty o, arguments_ty a, expr_ty returns, struct _symtable_entry *function_ste) { + PyObject *annotations_name = PyUnicode_FromFormat( + "", function_ste->ste_name); + if (!annotations_name) { + VISIT_QUIT(st, 0); + } int is_in_class = st->st_cur->ste_can_see_class_scope; _Py_block_ty current_type = st->st_cur->ste_type; - if (!symtable_enter_block(st, function_ste->ste_name, AnnotationBlock, + if (!symtable_enter_block(st, annotations_name, AnnotationBlock, (void *)a, LOCATION(o))) { + Py_DECREF(annotations_name); VISIT_QUIT(st, 0); } + Py_DECREF(annotations_name); if (is_in_class || current_type == ClassBlock) { st->st_cur->ste_can_see_class_scope = 1; if (!symtable_add_def(st, &_Py_ID(__classdict__), USE, LOCATION(o))) { From f9d81b676a366ce6bccbcaeadac6cd1c85c91d8c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 21:57:55 -0400 Subject: [PATCH 026/105] fix test_pydoc --- Lib/test/test_pydoc/test_pydoc.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 436fdb38756ddd..b4489d77d19948 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -76,6 +76,11 @@ class A(builtins.object) | __weakref__%s class B(builtins.object) + | Methods defined here: + | + | __annotate__ = (...) + | + | ---------------------------------------------------------------------- | Data descriptors defined here: | | __dict__%s @@ -86,8 +91,6 @@ class B(builtins.object) | Data and other attributes defined here: | | NO_MEANING = 'eggs' - | - | __annotations__ = {'NO_MEANING': } class C(builtins.object) | Methods defined here: @@ -175,6 +178,9 @@ class A(builtins.object) list of weak references to the object class B(builtins.object) + Methods defined here: + __annotate__ = (...) + ---------------------------------------------------------------------- Data descriptors defined here: __dict__ dictionary for instance variables @@ -183,7 +189,6 @@ class B(builtins.object) ---------------------------------------------------------------------- Data and other attributes defined here: NO_MEANING = 'eggs' - __annotations__ = {'NO_MEANING': } class C(builtins.object) From dd1f64a40051456491320b11cc464a038e73e76b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 22:02:05 -0400 Subject: [PATCH 027/105] fix test_traceback --- Lib/test/test_traceback.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 5987ec382e6c85..75a7c54ad73a72 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -622,6 +622,7 @@ def test_caret_in_type_annotation(self): def f_with_type(): def foo(a: THIS_DOES_NOT_EXIST ) -> int: return 0 + foo.__annotations__ lineno_f = f_with_type.__code__.co_firstlineno expected_f = ( @@ -629,7 +630,9 @@ def foo(a: THIS_DOES_NOT_EXIST ) -> int: f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' ' ~~~~~~~~^^\n' - f' File "{__file__}", line {lineno_f+1}, in f_with_type\n' + f' File "{__file__}", line {lineno_f+3}, in f_with_type\n' + ' foo.__annotations__\n' + f' File "{__file__}", line {lineno_f+1}, in \n' ' def foo(a: THIS_DOES_NOT_EXIST ) -> int:\n' ' ^^^^^^^^^^^^^^^^^^^\n' ) From 62f5b3ba3a30afb602752784e9cd3251a7afc21f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 22:04:23 -0400 Subject: [PATCH 028/105] fix test_opcodes --- Lib/test/test_opcodes.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_opcodes.py b/Lib/test/test_opcodes.py index 72488b2bb6b4ff..f7cc8331b8d844 100644 --- a/Lib/test/test_opcodes.py +++ b/Lib/test/test_opcodes.py @@ -39,16 +39,19 @@ class C: pass def test_use_existing_annotations(self): ns = {'__annotations__': {1: 2}} exec('x: int', ns) - self.assertEqual(ns['__annotations__'], {'x': int, 1: 2}) + self.assertEqual(ns['__annotations__'], {1: 2}) def test_do_not_recreate_annotations(self): # Don't rely on the existence of the '__annotations__' global. with support.swap_item(globals(), '__annotations__', {}): - del globals()['__annotations__'] + globals().pop('__annotations__', None) class C: - del __annotations__ - with self.assertRaises(NameError): - x: int + try: + del __annotations__ + except NameError: + pass + x: int + self.assertEqual(C.__annotations__, {"x": int}) def test_raise_class_exceptions(self): From c5b308b33cee93ccef04ba6717d3bd1b9aae47fd Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 21 May 2024 22:53:54 -0400 Subject: [PATCH 029/105] Some work on inspect --- Lib/inspect.py | 115 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 725c5efb4787d1..2d8414005fc869 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -184,7 +184,114 @@ class AnnotationsFormat(enum.IntEnum): SOURCE = 3 -def get_annotations(obj, *, globals=None, locals=None, eval_str=False): +class _ForwardRef: + def __init__(self, name): + self.name = name + + +class _ForwardReffer(dict): + def __missing__(self, key): + return _ForwardRef(key) + + +class Stringifier: + def __init__(self, node): + self.node = node + + def _convert(self, other): + if isinstance(other, Stringifier): + return other.node + else: + return ast.Name(id=repr(other)) + + def _make_binop(op): + def binop(self, other): + return Stringifier(ast.BinOp(self.node, op, self._convert(other))) + return binop + + __add__ = _make_binop(ast.Add()) + __sub__ = _make_binop(ast.Sub()) + __mul__ = _make_binop(ast.Mult()) + __matmul__ = _make_binop(ast.MatMult()) + __div__ = _make_binop(ast.Div()) + __mod__ = _make_binop(ast.Mod()) + __lshift__ = _make_binop(ast.LShift()) + __rshift__ = _make_binop(ast.RShift()) + __or__ = _make_binop(ast.BitOr()) + __xor__ = _make_binop(ast.BitXor()) + __and__ = _make_binop(ast.And()) + __floordiv__ = _make_binop(ast.FloorDiv()) + __pow__ = _make_binop(ast.Pow()) + + def _make_unary_op(op): + def unary_op(self): + return Stringifier(ast.UnaryOp(self.node, op)) + return unary_op + + __invert__ = _make_unary_op(ast.Invert()) + __pos__ = _make_binop(ast.UAdd()) + __neg__ = _make_binop(ast.USub()) + + def __getitem__(self, other): + if isinstance(other, tuple): + elts = [self._convert(elt) for elt in other] + other = ast.Tuple(elts) + else: + other = self._convert(other) + return Stringifier(ast.Subscript(self.node, other)) + + def __getattr__(self, attr): + return Stringifier(ast.Attribute(self.node, attr)) + + def __call__(self, *args, **kwargs): + return Stringifier(ast.Call( + self.node, + [self._convert(arg) for arg in args], + [ast.keyword(key, self._convert(value)) for key, value in kwargs.items()] + )) + + def __iter__(self): + return self + + def __next__(self): + return Stringifier(ast.Starred(self.node)) + + +class _StringifierDict(dict): + def __missing__(self, key): + return Stringifier(ast.Name(key)) + + +def _call_dunder_annotate(annotate, format): + try: + return annotate(format) + except AssertionError: + pass + if format == FORWARDREF: + globals = {**annotate.__builtins__, **annotate.__globals__} + globals = _ForwardReffer(globals) + func = types.FunctionType( + annotate.__code__, + globals, + closure=annotate.__closure__ + ) + return func(VALUE) + elif format == SOURCE: + globals = _StringifierDict() + func = types.FunctionType( + annotate.__code__, + globals, + # TODO: also replace the closure with stringifiers + closure=annotate.__closure__ + ) + annos = func(VALUE) + return { + key: ast.unparse(val.node) if isinstance(val, Stringifier) else repr(val) + for key, val in annos.items() + } + + +def get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=VALUE): """Compute the annotations dict for an object. obj may be a callable, class, or module. @@ -229,7 +336,11 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False): although if obj is a wrapped function (using functools.update_wrapper()) it is first unwrapped. """ - if isinstance(obj, type): + annotate = getattr(obj, "__annotate__", None) + # TODO remove format != VALUE condition + if annotate is not None and format != VALUE: + ann = _call_dunder_annotate(annotate, format) + elif isinstance(obj, type): # class ann = obj.__annotations__ From 1a63f5d2582d7468a2eccde4c0fd01048e165c28 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 22 May 2024 05:29:05 -0700 Subject: [PATCH 030/105] Raise NotImplementedError --- Lib/test/test_type_annotations.py | 6 +++--- Python/compile.c | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 6b39ad771677e5..84bec8e0afc426 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -315,7 +315,7 @@ class X: def test_module(self): ns = run_code("x: undefined = 1") anno = ns["__annotate__"] - with self.assertRaises(AssertionError): # TODO NotImplementedError + with self.assertRaises(NotImplementedError): anno(2) with self.assertRaises(NameError): @@ -350,8 +350,8 @@ class X: annotate = obj.__annotate__ self.assertIsInstance(annotate, types.FunctionType) self.assertEqual(annotate.__name__, f"") - with self.assertRaises(AssertionError): # TODO NotImplementedError + with self.assertRaises(NotImplementedError): annotate(inspect.FORWARDREF) - with self.assertRaises(AssertionError): # TODO NotImplementedError + with self.assertRaises(NotImplementedError): annotate(None) self.assertEqual(annotate(inspect.VALUE), {"x": int}) diff --git a/Python/compile.c b/Python/compile.c index e1807fedd8054f..381f3eb4b2a486 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1658,7 +1658,7 @@ compiler_leave_annotations_scope(struct compiler *c, location loc, ADDOP_I(c, loc, BUILD_MAP, annotations_len); ADDOP_IN_SCOPE(c, loc, RETURN_VALUE); USE_LABEL(c, label); - ADDOP_IN_SCOPE(c, loc, LOAD_ASSERTION_ERROR); + ADDOP_I(c, loc, LOAD_COMMON_CONSTANT, CONSTANT_NOTIMPLEMENTEDERROR); ADDOP_I(c, loc, RAISE_VARARGS, 1); PyCodeObject *co = optimize_and_assemble(c, 1); compiler_exit_scope(c); From a0c39b5770acd773ee359dcb79643b4bf1e0aca2 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 22 May 2024 06:22:50 -0700 Subject: [PATCH 031/105] blurb --- .../2024-05-22-06-22-47.gh-issue-119180.vZMiXm.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-05-22-06-22-47.gh-issue-119180.vZMiXm.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-05-22-06-22-47.gh-issue-119180.vZMiXm.rst b/Misc/NEWS.d/next/Core and Builtins/2024-05-22-06-22-47.gh-issue-119180.vZMiXm.rst new file mode 100644 index 00000000000000..dfb3958535a16b --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-05-22-06-22-47.gh-issue-119180.vZMiXm.rst @@ -0,0 +1 @@ +Evaluation of annotations is now deferred. See PEP 649 for details. From 083bbc58b3340da82eb57d54ddc6050d84d40cd6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 22 May 2024 06:27:51 -0700 Subject: [PATCH 032/105] Fix test_dis --- Lib/test/test_dis.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 67a630e1346109..b0873a4f9381b7 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -352,32 +352,21 @@ def wrap_func_w_kwargs(): dis_annot_stmt_str = """\ 0 RESUME 0 - 2 SETUP_ANNOTATIONS - LOAD_CONST 0 (1) + 2 LOAD_CONST 0 (1) STORE_NAME 0 (x) - LOAD_NAME 1 (int) - LOAD_NAME 2 (__annotations__) - LOAD_CONST 1 ('x') - STORE_SUBSCR - - 3 LOAD_NAME 3 (fun) - PUSH_NULL - LOAD_CONST 0 (1) - CALL 1 - LOAD_NAME 2 (__annotations__) - LOAD_CONST 2 ('y') - STORE_SUBSCR 4 LOAD_CONST 0 (1) - LOAD_NAME 4 (lst) - LOAD_NAME 3 (fun) + LOAD_NAME 1 (lst) + LOAD_NAME 2 (fun) PUSH_NULL - LOAD_CONST 3 (0) + LOAD_CONST 1 (0) CALL 1 STORE_SUBSCR - LOAD_NAME 1 (int) - POP_TOP - RETURN_CONST 4 (None) + + 2 LOAD_CONST 2 ( at 0x..., file "", line 2>) + MAKE_FUNCTION + STORE_NAME 3 (__annotate__) + RETURN_CONST 3 (None) """ compound_stmt_str = """\ From de1b23569f9b875dee4a7da4d688e4a88e70e1c7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 22 May 2024 06:28:41 -0700 Subject: [PATCH 033/105] Remove broken tests --- Lib/test/test_grammar.py | 58 ---------------------------------------- 1 file changed, 58 deletions(-) diff --git a/Lib/test/test_grammar.py b/Lib/test/test_grammar.py index 388bc72fe2385e..aa10efb892b953 100644 --- a/Lib/test/test_grammar.py +++ b/Lib/test/test_grammar.py @@ -306,16 +306,6 @@ def test_eof_error(self): var_annot_global: int # a global annotated is necessary for test_var_annot -# custom namespace for testing __annotations__ - -class CNS: - def __init__(self): - self._dct = {} - def __setitem__(self, item, value): - self._dct[item.lower()] = value - def __getitem__(self, item): - return self._dct[item] - class GrammarTests(unittest.TestCase): @@ -446,16 +436,6 @@ class F(C, A): self.assertEqual(E.__annotations__, {}) self.assertEqual(F.__annotations__, {}) - - def test_var_annot_metaclass_semantics(self): - class CMeta(type): - @classmethod - def __prepare__(metacls, name, bases, **kwds): - return {'__annotations__': CNS()} - class CC(metaclass=CMeta): - XX: 'ANNOT' - self.assertEqual(CC.__annotations__['xx'], 'ANNOT') - def test_var_annot_module_semantics(self): self.assertEqual(test.__annotations__, {}) self.assertEqual(ann_module.__annotations__, @@ -483,44 +463,6 @@ def test_var_annot_simple_exec(self): with self.assertRaises(KeyError): gns['__annotate__'] - def test_var_annot_custom_maps(self): - # tests with custom locals() and __annotations__ - ns = {'__annotations__': CNS()} - exec('X: int; Z: str = "Z"; (w): complex = 1j', ns) - self.assertEqual(ns['__annotations__']['x'], int) - self.assertEqual(ns['__annotations__']['z'], str) - with self.assertRaises(KeyError): - ns['__annotations__']['w'] - nonloc_ns = {} - class CNS2: - def __init__(self): - self._dct = {} - def __setitem__(self, item, value): - nonlocal nonloc_ns - self._dct[item] = value - nonloc_ns[item] = value - def __getitem__(self, item): - return self._dct[item] - exec('x: int = 1', {}, CNS2()) - self.assertEqual(nonloc_ns['__annotations__']['x'], int) - - def test_var_annot_refleak(self): - # complex case: custom locals plus custom __annotations__ - # this was causing refleak - cns = CNS() - nonloc_ns = {'__annotations__': cns} - class CNS2: - def __init__(self): - self._dct = {'__annotations__': cns} - def __setitem__(self, item, value): - nonlocal nonloc_ns - self._dct[item] = value - nonloc_ns[item] = value - def __getitem__(self, item): - return self._dct[item] - exec('X: str', {}, CNS2()) - self.assertEqual(nonloc_ns['__annotations__']['x'], str) - def test_var_annot_rhs(self): ns = {} exec('x: tuple = 1, 2', ns) From 5f5cf115122815e85b1b7d6528c6898231aedaa5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 23 May 2024 22:08:53 -0700 Subject: [PATCH 034/105] No deferred evaluation in interactive mode --- Include/internal/pycore_symtable.h | 1 + Python/compile.c | 39 ++++++++++++++++++++++++++---- Python/symtable.c | 8 ++++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/Include/internal/pycore_symtable.h b/Include/internal/pycore_symtable.h index f109a7229c35df..b3eb2e12ddd2c9 100644 --- a/Include/internal/pycore_symtable.h +++ b/Include/internal/pycore_symtable.h @@ -72,6 +72,7 @@ struct symtable { the symbol table */ int recursion_depth; /* current recursion depth */ int recursion_limit; /* recursion limit */ + int st_kind; /* kind of module */ }; typedef struct _symtable_entry { diff --git a/Python/compile.c b/Python/compile.c index 381f3eb4b2a486..f888aba177e7d8 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1653,7 +1653,7 @@ compiler_setup_annotations_scope(struct compiler *c, location loc, static int compiler_leave_annotations_scope(struct compiler *c, location loc, - int annotations_len, jump_target_label label) + Py_ssize_t annotations_len, jump_target_label label) { ADDOP_I(c, loc, BUILD_MAP, annotations_len); ADDOP_IN_SCOPE(c, loc, RETURN_VALUE); @@ -6595,6 +6595,23 @@ check_ann_expr(struct compiler *c, expr_ty e) return SUCCESS; } +static int +check_annotation(struct compiler *c, stmt_ty s) +{ + /* Annotations of complex targets does not produce anything + under annotations future */ + if (c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) { + return SUCCESS; + } + + /* Annotations are only evaluated in a module or class. */ + if (c->u->u_scope_type == COMPILER_SCOPE_MODULE || + c->u->u_scope_type == COMPILER_SCOPE_CLASS) { + return check_ann_expr(c, s->v.AnnAssign.annotation); + } + return SUCCESS; +} + static int check_ann_subscr(struct compiler *c, expr_ty e) { @@ -6630,7 +6647,11 @@ compiler_annassign(struct compiler *c, stmt_ty s) { location loc = LOC(s); expr_ty targ = s->v.AnnAssign.target; - PyObject* mangled; + bool is_interactive = ( + c->c_st->st_kind == Interactive_kind && c->u->u_scope_type == COMPILER_SCOPE_MODULE + ); + bool future_annotations = c->c_future.ff_features & CO_FUTURE_ANNOTATIONS; + PyObject *mangled; assert(s->kind == AnnAssign_kind); @@ -6645,11 +6666,16 @@ compiler_annassign(struct compiler *c, stmt_ty s) return ERROR; } /* If we have a simple name in a module or class, store annotation. */ - if ((c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) && + if ((future_annotations || is_interactive) && s->v.AnnAssign.simple && (c->u->u_scope_type == COMPILER_SCOPE_MODULE || c->u->u_scope_type == COMPILER_SCOPE_CLASS)) { - VISIT(c, annexpr, s->v.AnnAssign.annotation); + if (future_annotations) { + VISIT(c, annexpr, s->v.AnnAssign.annotation); + } + else { + VISIT(c, expr, s->v.AnnAssign.annotation); + } ADDOP_NAME(c, loc, LOAD_NAME, &_Py_ID(__annotations__), names); mangled = _Py_Mangle(c->u->u_private, targ->v.Name.id); ADDOP_LOAD_CONST_NEW(c, loc, mangled); @@ -6678,7 +6704,10 @@ compiler_annassign(struct compiler *c, stmt_ty s) targ->kind); return ERROR; } - /* For non-simple AnnAssign, the annotation is not evaluated. */ + /* Annotation is evaluated last. */ + if ((future_annotations || is_interactive) && !s->v.AnnAssign.simple && check_annotation(c, s) < 0) { + return ERROR; + } return SUCCESS; } diff --git a/Python/symtable.c b/Python/symtable.c index 9b7b09dfe68ade..a10e77e5738a9b 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -427,6 +427,7 @@ _PySymtable_Build(mod_ty mod, PyObject *filename, _PyFutureFeatures *future) } st->st_top = st->st_cur; + st->st_kind = mod->kind; switch (mod->kind) { case Module_kind: seq = mod->v.Module.body; @@ -2471,13 +2472,16 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, struct _symtable_entry *parent_ste, void *key) { int future_annotations = st->st_future->ff_features & CO_FUTURE_ANNOTATIONS; + int is_top_level_interactive = ( + st->st_kind == Interactive_kind && st->st_cur->ste_type == ModuleBlock + ); if (future_annotations) { if(!symtable_enter_block(st, &_Py_ID(_annotation), AnnotationBlock, key, LOCATION(annotation))) { VISIT_QUIT(st, 0); } } - else { + else if (!is_top_level_interactive) { if (st->st_cur->ste_annotation_block == NULL) { PyObject *annotations_name = PyUnicode_FromFormat( "", parent_ste->ste_name); @@ -2518,7 +2522,7 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, } } VISIT(st, expr, annotation); - if (!symtable_exit_block(st)) { + if ((future_annotations || !is_top_level_interactive) && !symtable_exit_block(st)) { VISIT_QUIT(st, 0); } return 1; From 77f3b1c524b80fd5a87ab748acd4f65a1f7b6f18 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 23 May 2024 22:30:20 -0700 Subject: [PATCH 035/105] gh-119443: Turn off from __future__ import annotations in REPL --- Lib/_pyrepl/simple_interact.py | 2 +- Lib/test/test_pyrepl/test_interact.py | 9 +++++++++ .../2024-05-23-22-29-59.gh-issue-119443.KAGz6S.rst | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2024-05-23-22-29-59.gh-issue-119443.KAGz6S.rst diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index 8ab4dab757685e..3dfb1d7ad736e3 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -95,7 +95,7 @@ def runsource(self, source, filename="", symbol="single"): the_symbol = symbol if stmt is last_stmt else "exec" item = wrapper([stmt]) try: - code = compile(item, filename, the_symbol) + code = compile(item, filename, the_symbol, dont_inherit=True) except (OverflowError, ValueError): self.showsyntaxerror(filename) return False diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index 10e34045bcf92d..6ebd51fe14dd62 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -94,3 +94,12 @@ def test_runsource_shows_syntax_error_for_failed_compilation(self): with patch.object(console, "showsyntaxerror") as mock_showsyntaxerror: console.runsource(source) mock_showsyntaxerror.assert_called_once() + + def test_no_active_future(self): + console = InteractiveColoredConsole() + source = "x: int = 1; print(__annotations__)" + f = io.StringIO() + with contextlib.redirect_stdout(f): + result = console.runsource(source) + self.assertFalse(result) + self.assertEqual(f.getvalue(), "{'x': }\n") diff --git a/Misc/NEWS.d/next/Library/2024-05-23-22-29-59.gh-issue-119443.KAGz6S.rst b/Misc/NEWS.d/next/Library/2024-05-23-22-29-59.gh-issue-119443.KAGz6S.rst new file mode 100644 index 00000000000000..4470c566a37d88 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-05-23-22-29-59.gh-issue-119443.KAGz6S.rst @@ -0,0 +1,2 @@ +The interactive REPL no longer runs with ``from __future__ import +annotations`` enabled. Patch by Jelle Zijlstra. From 421783051464ff2ef7d38bac668f1925e7e6c578 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 24 May 2024 09:03:12 -0700 Subject: [PATCH 036/105] Fix refleak --- Python/compile.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Python/compile.c b/Python/compile.c index f888aba177e7d8..3737eb3aecf3f6 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -2131,20 +2131,23 @@ compiler_visit_annotations(struct compiler *c, location loc, Py_ssize_t annotations_len = 0; int future_annotations = c->c_future.ff_features & CO_FUTURE_ANNOTATIONS; + NEW_JUMP_TARGET_LABEL(c, raise_notimp); + PySTEntryObject *ste; int result = _PySymtable_LookupOptional(c->c_st, args, &ste); if (result == -1) { return ERROR; } assert(ste != NULL); - NEW_JUMP_TARGET_LABEL(c, raise_notimp); if (!future_annotations && ste->ste_annotations_used) { - RETURN_IF_ERROR( - compiler_setup_annotations_scope(c, loc, (void *)args, raise_notimp, - ste->ste_name) - ); + if (compiler_setup_annotations_scope(c, loc, (void *)args, raise_notimp, + ste->ste_name) < 0) { + Py_DECREF(ste); + return ERROR; + } } + Py_DECREF(ste); RETURN_IF_ERROR( compiler_visit_argannotations(c, args->args, &annotations_len, loc)); From 13f5d761743b4bc41890e8c62aec38b548eecfd6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 24 May 2024 09:09:31 -0700 Subject: [PATCH 037/105] Fix another refleak --- Python/symtable.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Python/symtable.c b/Python/symtable.c index a10e77e5738a9b..1c655167a13948 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -1684,8 +1684,10 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s) } if (!symtable_visit_annotations(st, s, s->v.FunctionDef.args, - s->v.FunctionDef.returns, new_ste)) + s->v.FunctionDef.returns, new_ste)) { + Py_DECREF(new_ste); VISIT_QUIT(st, 0); + } if (!symtable_enter_existing_block(st, new_ste)) { Py_DECREF(new_ste); VISIT_QUIT(st, 0); From 242301c3b6fe04b1353d1e456d01b559aa687258 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 24 May 2024 09:15:51 -0700 Subject: [PATCH 038/105] Exit scope correctly on error --- Python/compile.c | 65 +++++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/Python/compile.c b/Python/compile.c index 3737eb3aecf3f6..75eef62b587424 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -2119,6 +2119,38 @@ compiler_visit_argannotations(struct compiler *c, asdl_arg_seq* args, return SUCCESS; } +static int +compiler_visit_annotations_in_scope(struct compiler *c, location loc, + arguments_ty args, expr_ty returns, + Py_ssize_t *annotations_len) +{ + RETURN_IF_ERROR( + compiler_visit_argannotations(c, args->args, annotations_len, loc)); + + RETURN_IF_ERROR( + compiler_visit_argannotations(c, args->posonlyargs, annotations_len, loc)); + + if (args->vararg && args->vararg->annotation) { + RETURN_IF_ERROR( + compiler_visit_argannotation(c, args->vararg->arg, + args->vararg->annotation, annotations_len, loc)); + } + + RETURN_IF_ERROR( + compiler_visit_argannotations(c, args->kwonlyargs, annotations_len, loc)); + + if (args->kwarg && args->kwarg->annotation) { + RETURN_IF_ERROR( + compiler_visit_argannotation(c, args->kwarg->arg, + args->kwarg->annotation, annotations_len, loc)); + } + + RETURN_IF_ERROR( + compiler_visit_argannotation(c, &_Py_ID(return), returns, annotations_len, loc)); + + return 0; +} + static int compiler_visit_annotations(struct compiler *c, location loc, arguments_ty args, expr_ty returns) @@ -2139,8 +2171,9 @@ compiler_visit_annotations(struct compiler *c, location loc, return ERROR; } assert(ste != NULL); + bool annotations_used = ste->ste_annotations_used; - if (!future_annotations && ste->ste_annotations_used) { + if (!future_annotations && annotations_used) { if (compiler_setup_annotations_scope(c, loc, (void *)args, raise_notimp, ste->ste_name) < 0) { Py_DECREF(ste); @@ -2149,30 +2182,13 @@ compiler_visit_annotations(struct compiler *c, location loc, } Py_DECREF(ste); - RETURN_IF_ERROR( - compiler_visit_argannotations(c, args->args, &annotations_len, loc)); - - RETURN_IF_ERROR( - compiler_visit_argannotations(c, args->posonlyargs, &annotations_len, loc)); - - if (args->vararg && args->vararg->annotation) { - RETURN_IF_ERROR( - compiler_visit_argannotation(c, args->vararg->arg, - args->vararg->annotation, &annotations_len, loc)); - } - - RETURN_IF_ERROR( - compiler_visit_argannotations(c, args->kwonlyargs, &annotations_len, loc)); - - if (args->kwarg && args->kwarg->annotation) { - RETURN_IF_ERROR( - compiler_visit_argannotation(c, args->kwarg->arg, - args->kwarg->annotation, &annotations_len, loc)); + if (compiler_visit_annotations_in_scope(c, loc, args, returns, &annotations_len) < 0) { + if (!future_annotations && annotations_used) { + compiler_exit_scope(c); + } + return ERROR; } - RETURN_IF_ERROR( - compiler_visit_argannotation(c, &_Py_ID(return), returns, &annotations_len, loc)); - if (future_annotations) { if (annotations_len) { ADDOP_I(c, loc, BUILD_TUPLE, annotations_len * 2); @@ -2180,8 +2196,7 @@ compiler_visit_annotations(struct compiler *c, location loc, } } else { - assert(ste != NULL); - if (ste->ste_annotations_used) { + if (annotations_used) { RETURN_IF_ERROR( compiler_leave_annotations_scope(c, loc, annotations_len, raise_notimp) ); From 239ba23c66f03aa996f9569a57d6f05327aead84 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 24 May 2024 09:41:49 -0700 Subject: [PATCH 039/105] fix test --- Lib/test/test_pyrepl/test_interact.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index 6ebd51fe14dd62..976915591110f5 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -97,7 +97,7 @@ def test_runsource_shows_syntax_error_for_failed_compilation(self): def test_no_active_future(self): console = InteractiveColoredConsole() - source = "x: int = 1; print(__annotations__)" + source = "if True:\n x: int = 1; print(__annotations__)" f = io.StringIO() with contextlib.redirect_stdout(f): result = console.runsource(source) From 787ad33186dac4a7b30d151af8fe8558e1cf9fe6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 24 May 2024 10:59:42 -0700 Subject: [PATCH 040/105] Update except --- Lib/inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 2d8414005fc869..0cb4f1bdf66967 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -265,7 +265,7 @@ def __missing__(self, key): def _call_dunder_annotate(annotate, format): try: return annotate(format) - except AssertionError: + except NotImplementedError: pass if format == FORWARDREF: globals = {**annotate.__builtins__, **annotate.__globals__} From cfef2c6081a8b188b7d4d925e722dda54fd84ac0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 24 May 2024 17:39:22 -0700 Subject: [PATCH 041/105] Initialize field --- Python/compile.c | 4 ++-- Python/symtable.c | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Python/compile.c b/Python/compile.c index 75eef62b587424..ecee0043252af4 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -2173,7 +2173,7 @@ compiler_visit_annotations(struct compiler *c, location loc, assert(ste != NULL); bool annotations_used = ste->ste_annotations_used; - if (!future_annotations && annotations_used) { + if (annotations_used && !future_annotations) { if (compiler_setup_annotations_scope(c, loc, (void *)args, raise_notimp, ste->ste_name) < 0) { Py_DECREF(ste); @@ -2183,7 +2183,7 @@ compiler_visit_annotations(struct compiler *c, location loc, Py_DECREF(ste); if (compiler_visit_annotations_in_scope(c, loc, args, returns, &annotations_len) < 0) { - if (!future_annotations && annotations_used) { + if (annotations_used && !future_annotations) { compiler_exit_scope(c); } return ERROR; diff --git a/Python/symtable.c b/Python/symtable.c index 1c655167a13948..b7660b6a4a369e 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -111,6 +111,7 @@ ste_new(struct symtable *st, identifier name, _Py_block_ty block, ste->ste_varkeywords = 0; ste->ste_opt_lineno = 0; ste->ste_opt_col_offset = 0; + ste->ste_annotations_used = 0; ste->ste_lineno = lineno; ste->ste_col_offset = col_offset; ste->ste_end_lineno = end_lineno; From b62e04c4c7808260f4442da1039974a54f49cacb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 24 May 2024 17:39:22 -0700 Subject: [PATCH 042/105] Initialize field --- Python/compile.c | 4 ++-- Python/symtable.c | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Python/compile.c b/Python/compile.c index 75eef62b587424..ecee0043252af4 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -2173,7 +2173,7 @@ compiler_visit_annotations(struct compiler *c, location loc, assert(ste != NULL); bool annotations_used = ste->ste_annotations_used; - if (!future_annotations && annotations_used) { + if (annotations_used && !future_annotations) { if (compiler_setup_annotations_scope(c, loc, (void *)args, raise_notimp, ste->ste_name) < 0) { Py_DECREF(ste); @@ -2183,7 +2183,7 @@ compiler_visit_annotations(struct compiler *c, location loc, Py_DECREF(ste); if (compiler_visit_annotations_in_scope(c, loc, args, returns, &annotations_len) < 0) { - if (!future_annotations && annotations_used) { + if (annotations_used && !future_annotations) { compiler_exit_scope(c); } return ERROR; diff --git a/Python/symtable.c b/Python/symtable.c index 1c655167a13948..b7660b6a4a369e 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -111,6 +111,7 @@ ste_new(struct symtable *st, identifier name, _Py_block_ty block, ste->ste_varkeywords = 0; ste->ste_opt_lineno = 0; ste->ste_opt_col_offset = 0; + ste->ste_annotations_used = 0; ste->ste_lineno = lineno; ste->ste_col_offset = col_offset; ste->ste_end_lineno = end_lineno; From 5ae206dfa04aa8df525dd10284705b387c4c493f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 24 May 2024 18:02:23 -0700 Subject: [PATCH 043/105] self-review --- Include/cpython/code.h | 2 +- Objects/typeobject.c | 1 - Python/compile.c | 13 ++++++++++--- Python/symtable.c | 3 ++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Include/cpython/code.h b/Include/cpython/code.h index d64f79bceaa6c7..ef8f9304ccab56 100644 --- a/Include/cpython/code.h +++ b/Include/cpython/code.h @@ -219,7 +219,7 @@ struct PyCodeObject _PyCode_DEF(1); #define CO_FUTURE_GENERATOR_STOP 0x800000 #define CO_FUTURE_ANNOTATIONS 0x1000000 -#define CO_NO_MONITORING_EVENTS 0x4000000 +#define CO_NO_MONITORING_EVENTS 0x2000000 /* This should be defined if a future statement modifies the syntax. For example, when a keyword is added. diff --git a/Objects/typeobject.c b/Objects/typeobject.c index f03508e5fb6a05..11f9c570ac4971 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -1829,7 +1829,6 @@ type_set_annotations(PyTypeObject *type, PyObject *value, void *context) return -1; } } - PyType_Modified(type); if (result < 0) { Py_DECREF(dict); return -1; diff --git a/Python/compile.c b/Python/compile.c index ecee0043252af4..4987f8fe25dcc5 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1643,6 +1643,8 @@ compiler_setup_annotations_scope(struct compiler *c, location loc, return ERROR; } c->u->u_metadata.u_posonlyargcount = 1; + // if .format != 1: raise NotImplementedError + // The raise is at the end of the function so there are no jumps in the happy path. _Py_DECLARE_STR(format, ".format"); ADDOP_I(c, loc, LOAD_FAST, 0); ADDOP_LOAD_CONST(c, loc, _PyLong_GetOne()); @@ -1681,7 +1683,7 @@ compiler_collect_annotations(struct compiler *c, asdl_stmt_seq *stmts, stmt_ty st = (stmt_ty)asdl_seq_GET(stmts, i); switch (st->kind) { case AnnAssign_kind: - // Only "simple" names (i.e., unparenthesized names) are stored. + // Only "simple" (i.e., unparenthesized) names are stored. if (st->v.AnnAssign.simple) { PyObject *mangled = _Py_Mangle(c->u->u_private, st->v.AnnAssign.target->v.Name.id); ADDOP_LOAD_CONST_NEW(c, LOC(st), mangled); @@ -1761,7 +1763,9 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) stmt_ty st = (stmt_ty)asdl_seq_GET(stmts, 0); loc = LOC(st); } - /* Every annotated class and module should have __annotations__. */ + /* If from __future__ import annotations is active, + * every annotated class and module should have __annotations__. + * Else __annotate__ is created when necessary. */ if ((c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) && find_ann(stmts)) { ADDOP(c, loc, SETUP_ANNOTATIONS); } @@ -1789,6 +1793,9 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) for (Py_ssize_t i = first_instr; i < asdl_seq_LEN(stmts); i++) { VISIT(c, stmt, (stmt_ty)asdl_seq_GET(stmts, i)); } + // If there are annotations and the future import is not on, we + // collect the annotations in a separate pass and generate an + // __annotate__ function. See PEP 649. if (!(c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) && c->u->u_ste->ste_annotation_block != NULL) { NEW_JUMP_TARGET_LABEL(c, raise_notimp); @@ -2156,7 +2163,7 @@ compiler_visit_annotations(struct compiler *c, location loc, arguments_ty args, expr_ty returns) { /* Push arg annotation names and values. - The expressions are evaluated out-of-order wrt the source code. + The expressions are evaluated separately from the rest of the source code. Return -1 on error, or a combination of flags to add to the function. */ diff --git a/Python/symtable.c b/Python/symtable.c index b7660b6a4a369e..c9d2f800ed9c54 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -2508,7 +2508,8 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, } _Py_DECLARE_STR(format, ".format"); - // We need to insert code that reads this "parameter" to the function. + // The generated __annotate__ function takes a single parameter with the + // internal name ".format". if (!symtable_add_def(st, &_Py_STR(format), DEF_PARAM, LOCATION(annotation))) { return 0; From 24fd3284ba93b8b0b9e6d55989617d8cfdfdfb8c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 24 May 2024 18:26:10 -0700 Subject: [PATCH 044/105] Fix crash found by CIFuzz --- Lib/test/test_type_annotations.py | 5 +++++ Python/compile.c | 28 ++++++++++++---------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 84bec8e0afc426..a40dfd5f5069ed 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -355,3 +355,8 @@ class X: with self.assertRaises(NotImplementedError): annotate(None) self.assertEqual(annotate(inspect.VALUE), {"x": int}) + + def test_comprehension_in_annotation(self): + # This crashed in an earlier version of the code + ns = run_code("x: [y for y in range(10)]") + self.assertEqual(ns["__annotate__"](1), {"x": list(range(10))}) diff --git a/Python/compile.c b/Python/compile.c index 4987f8fe25dcc5..9b0b3bfd6cebd9 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1635,8 +1635,7 @@ compiler_unwind_fblock_stack(struct compiler *c, location *ploc, static int compiler_setup_annotations_scope(struct compiler *c, location loc, - void *key, jump_target_label label, - PyObject *name) + void *key, PyObject *name) { if (compiler_enter_scope(c, name, COMPILER_SCOPE_ANNOTATIONS, key, loc.lineno) == -1) { @@ -1644,24 +1643,24 @@ compiler_setup_annotations_scope(struct compiler *c, location loc, } c->u->u_metadata.u_posonlyargcount = 1; // if .format != 1: raise NotImplementedError - // The raise is at the end of the function so there are no jumps in the happy path. _Py_DECLARE_STR(format, ".format"); ADDOP_I(c, loc, LOAD_FAST, 0); ADDOP_LOAD_CONST(c, loc, _PyLong_GetOne()); - ADDOP_I(c, loc, COMPARE_OP, (Py_EQ << 5) | compare_masks[Py_EQ]); - ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, label); + ADDOP_I(c, loc, COMPARE_OP, (Py_NE << 5) | compare_masks[Py_NE]); + NEW_JUMP_TARGET_LABEL(c, body); + ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, body); + ADDOP_I(c, loc, LOAD_COMMON_CONSTANT, CONSTANT_NOTIMPLEMENTEDERROR); + ADDOP_I(c, loc, RAISE_VARARGS, 1); + USE_LABEL(c, body); return 0; } static int compiler_leave_annotations_scope(struct compiler *c, location loc, - Py_ssize_t annotations_len, jump_target_label label) + Py_ssize_t annotations_len) { ADDOP_I(c, loc, BUILD_MAP, annotations_len); ADDOP_IN_SCOPE(c, loc, RETURN_VALUE); - USE_LABEL(c, label); - ADDOP_I(c, loc, LOAD_COMMON_CONSTANT, CONSTANT_NOTIMPLEMENTEDERROR); - ADDOP_I(c, loc, RAISE_VARARGS, 1); PyCodeObject *co = optimize_and_assemble(c, 1); compiler_exit_scope(c); if (co == NULL) { @@ -1798,16 +1797,15 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) // __annotate__ function. See PEP 649. if (!(c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) && c->u->u_ste->ste_annotation_block != NULL) { - NEW_JUMP_TARGET_LABEL(c, raise_notimp); void *key = (void *)((uintptr_t)c->u->u_ste->ste_id + 1); - RETURN_IF_ERROR(compiler_setup_annotations_scope(c, loc, key, raise_notimp, + RETURN_IF_ERROR(compiler_setup_annotations_scope(c, loc, key, c->u->u_ste->ste_annotation_block->ste_name)); int annotations_len = 0; RETURN_IF_ERROR( compiler_collect_annotations(c, stmts, &annotations_len) ); RETURN_IF_ERROR( - compiler_leave_annotations_scope(c, loc, annotations_len, raise_notimp) + compiler_leave_annotations_scope(c, loc, annotations_len) ); RETURN_IF_ERROR( compiler_nameop(c, loc, &_Py_ID(__annotate__), Store) @@ -2170,8 +2168,6 @@ compiler_visit_annotations(struct compiler *c, location loc, Py_ssize_t annotations_len = 0; int future_annotations = c->c_future.ff_features & CO_FUTURE_ANNOTATIONS; - NEW_JUMP_TARGET_LABEL(c, raise_notimp); - PySTEntryObject *ste; int result = _PySymtable_LookupOptional(c->c_st, args, &ste); if (result == -1) { @@ -2181,7 +2177,7 @@ compiler_visit_annotations(struct compiler *c, location loc, bool annotations_used = ste->ste_annotations_used; if (annotations_used && !future_annotations) { - if (compiler_setup_annotations_scope(c, loc, (void *)args, raise_notimp, + if (compiler_setup_annotations_scope(c, loc, (void *)args, ste->ste_name) < 0) { Py_DECREF(ste); return ERROR; @@ -2205,7 +2201,7 @@ compiler_visit_annotations(struct compiler *c, location loc, else { if (annotations_used) { RETURN_IF_ERROR( - compiler_leave_annotations_scope(c, loc, annotations_len, raise_notimp) + compiler_leave_annotations_scope(c, loc, annotations_len) ); return MAKE_FUNCTION_ANNOTATE; } From c18186499017e7efbe875bd1db4993dbb1f323ca Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 May 2024 17:10:04 -0700 Subject: [PATCH 045/105] fix merge --- Python/symtable.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/symtable.c b/Python/symtable.c index 80dbd29541a37c..9d248d52d8de2a 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -1356,7 +1356,7 @@ symtable_enter_existing_block(struct symtable *st, PySTEntryObject* ste) } /* No need to inherit ste_mangled_names in classes, where all names * are mangled. */ - if (prev && prev->ste_mangled_names != NULL && block != ClassBlock) { + if (prev && prev->ste_mangled_names != NULL && ste->ste_type != ClassBlock) { ste->ste_mangled_names = Py_NewRef(prev->ste_mangled_names); } /* The entry is owned by the stack. Borrow it for st_cur. */ From 0befff50694769b7abd54104fe54c3c05fd63135 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 May 2024 17:17:34 -0700 Subject: [PATCH 046/105] Name the function as __annotate__ --- Python/symtable.c | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/Python/symtable.c b/Python/symtable.c index 9d248d52d8de2a..1943f725802277 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -2501,18 +2501,11 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, } else if (!is_top_level_interactive) { if (st->st_cur->ste_annotation_block == NULL) { - PyObject *annotations_name = PyUnicode_FromFormat( - "", parent_ste->ste_name); - if (!annotations_name) { - VISIT_QUIT(st, 0); - } _Py_block_ty current_type = st->st_cur->ste_type; - if (!symtable_enter_block(st, annotations_name, AnnotationBlock, + if (!symtable_enter_block(st, &_Py_ID(__annotate__), AnnotationBlock, key, LOCATION(annotation))) { - Py_DECREF(annotations_name); VISIT_QUIT(st, 0); } - Py_DECREF(annotations_name); parent_ste->ste_annotation_block = (struct _symtable_entry *)Py_NewRef(st->st_cur); if (current_type == ClassBlock) { @@ -2570,19 +2563,12 @@ static int symtable_visit_annotations(struct symtable *st, stmt_ty o, arguments_ty a, expr_ty returns, struct _symtable_entry *function_ste) { - PyObject *annotations_name = PyUnicode_FromFormat( - "", function_ste->ste_name); - if (!annotations_name) { - VISIT_QUIT(st, 0); - } int is_in_class = st->st_cur->ste_can_see_class_scope; _Py_block_ty current_type = st->st_cur->ste_type; - if (!symtable_enter_block(st, annotations_name, AnnotationBlock, + if (!symtable_enter_block(st, &_Py_ID(__annotate__), AnnotationBlock, (void *)a, LOCATION(o))) { - Py_DECREF(annotations_name); VISIT_QUIT(st, 0); } - Py_DECREF(annotations_name); if (is_in_class || current_type == ClassBlock) { st->st_cur->ste_can_see_class_scope = 1; if (!symtable_add_def(st, &_Py_ID(__classdict__), USE, LOCATION(o))) { From ae7714cb338b8c1244b94ef028a111f6046d2fda Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 May 2024 17:41:18 -0700 Subject: [PATCH 047/105] Replace find_ann() --- Python/compile.c | 83 ++--------------------------------------------- Python/symtable.c | 1 + 2 files changed, 3 insertions(+), 81 deletions(-) diff --git a/Python/compile.c b/Python/compile.c index 50cd88f8d93655..81f63f6cd52eed 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1332,85 +1332,6 @@ compiler_exit_scope(struct compiler *c) PyErr_SetRaisedException(exc); } -/* Search if variable annotations are present statically in a block. */ - -static bool -find_ann(asdl_stmt_seq *stmts) -{ - int i, j, res = 0; - stmt_ty st; - - for (i = 0; i < asdl_seq_LEN(stmts); i++) { - st = (stmt_ty)asdl_seq_GET(stmts, i); - switch (st->kind) { - case AnnAssign_kind: - return true; - case For_kind: - res = find_ann(st->v.For.body) || - find_ann(st->v.For.orelse); - break; - case AsyncFor_kind: - res = find_ann(st->v.AsyncFor.body) || - find_ann(st->v.AsyncFor.orelse); - break; - case While_kind: - res = find_ann(st->v.While.body) || - find_ann(st->v.While.orelse); - break; - case If_kind: - res = find_ann(st->v.If.body) || - find_ann(st->v.If.orelse); - break; - case With_kind: - res = find_ann(st->v.With.body); - break; - case AsyncWith_kind: - res = find_ann(st->v.AsyncWith.body); - break; - case Try_kind: - for (j = 0; j < asdl_seq_LEN(st->v.Try.handlers); j++) { - excepthandler_ty handler = (excepthandler_ty)asdl_seq_GET( - st->v.Try.handlers, j); - if (find_ann(handler->v.ExceptHandler.body)) { - return true; - } - } - res = find_ann(st->v.Try.body) || - find_ann(st->v.Try.finalbody) || - find_ann(st->v.Try.orelse); - break; - case TryStar_kind: - for (j = 0; j < asdl_seq_LEN(st->v.TryStar.handlers); j++) { - excepthandler_ty handler = (excepthandler_ty)asdl_seq_GET( - st->v.TryStar.handlers, j); - if (find_ann(handler->v.ExceptHandler.body)) { - return true; - } - } - res = find_ann(st->v.TryStar.body) || - find_ann(st->v.TryStar.finalbody) || - find_ann(st->v.TryStar.orelse); - break; - case Match_kind: - for (j = 0; j < asdl_seq_LEN(st->v.Match.cases); j++) { - match_case_ty match_case = (match_case_ty)asdl_seq_GET( - st->v.Match.cases, j); - if (find_ann(match_case->body)) { - return true; - } - } - break; - default: - res = false; - break; - } - if (res) { - break; - } - } - return res; -} - /* * Frame block handling functions */ @@ -1757,7 +1678,7 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) /* If from __future__ import annotations is active, * every annotated class and module should have __annotations__. * Else __annotate__ is created when necessary. */ - if ((c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) && find_ann(stmts)) { + if ((c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) && c->u->u_ste->ste_annotations_used) { ADDOP(c, loc, SETUP_ANNOTATIONS); } if (!asdl_seq_LEN(stmts)) { @@ -1817,7 +1738,7 @@ compiler_codegen(struct compiler *c, mod_ty mod) } break; case Interactive_kind: - if (find_ann(mod->v.Interactive.body)) { + if (c->u->u_ste->ste_annotations_used) { ADDOP(c, loc, SETUP_ANNOTATIONS); } c->c_interactive = 1; diff --git a/Python/symtable.c b/Python/symtable.c index 1943f725802277..7314eb91fb44ee 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -1809,6 +1809,7 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s) VISIT(st, expr, s->v.Assign.value); break; case AnnAssign_kind: + st->st_cur->ste_annotations_used = 1; if (s->v.AnnAssign.target->kind == Name_kind) { expr_ty e_name = s->v.AnnAssign.target; long cur = symtable_lookup(st, e_name->v.Name.id); From 431811a3a4ec9892b3e6eea45ab21855b8cb2d8a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 May 2024 17:41:48 -0700 Subject: [PATCH 048/105] fix test --- Lib/test/test_type_annotations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index a40dfd5f5069ed..205802a1f7b6e2 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -349,7 +349,7 @@ class X: with self.subTest(obj=obj): annotate = obj.__annotate__ self.assertIsInstance(annotate, types.FunctionType) - self.assertEqual(annotate.__name__, f"") + self.assertEqual(annotate.__name__, "__annotate__") with self.assertRaises(NotImplementedError): annotate(inspect.FORWARDREF) with self.assertRaises(NotImplementedError): From 7ca24d3b40da6258b5b884e8b580d05624bdbdc9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 May 2024 17:59:56 -0700 Subject: [PATCH 049/105] Remove second compiler pass --- Python/compile.c | 132 +++++++++++++++++------------------------------ 1 file changed, 46 insertions(+), 86 deletions(-) diff --git a/Python/compile.c b/Python/compile.c index 81f63f6cd52eed..45ca6b8995c52c 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -258,6 +258,7 @@ struct compiler_unit { PyObject *u_private; /* for private name mangling */ PyObject *u_static_attributes; /* for class: attributes accessed via self.X */ + PyObject *u_deferred_annotations; /* AnnAssign nodes deferred to the end of compilation */ instr_sequence *u_instr_sequence; /* codegen output */ @@ -597,6 +598,7 @@ compiler_unit_free(struct compiler_unit *u) Py_CLEAR(u->u_metadata.u_fasthidden); Py_CLEAR(u->u_private); Py_CLEAR(u->u_static_attributes); + Py_CLEAR(u->u_deferred_annotations); PyMem_Free(u); } @@ -1251,6 +1253,7 @@ compiler_enter_scope(struct compiler *c, identifier name, } u->u_private = NULL; + u->u_deferred_annotations = NULL; if (scope_type == COMPILER_SCOPE_CLASS) { u->u_static_attributes = PySet_New(0); if (!u->u_static_attributes) { @@ -1587,79 +1590,6 @@ compiler_leave_annotations_scope(struct compiler *c, location loc, return 0; } -static int -compiler_collect_annotations(struct compiler *c, asdl_stmt_seq *stmts, - int *annotations_len) -{ - for (int i = 0; i < asdl_seq_LEN(stmts); i++) { - stmt_ty st = (stmt_ty)asdl_seq_GET(stmts, i); - switch (st->kind) { - case AnnAssign_kind: - // Only "simple" (i.e., unparenthesized) names are stored. - if (st->v.AnnAssign.simple) { - PyObject *mangled = _Py_Mangle(c->u->u_private, st->v.AnnAssign.target->v.Name.id); - ADDOP_LOAD_CONST_NEW(c, LOC(st), mangled); - VISIT(c, expr, st->v.AnnAssign.annotation); - *annotations_len += 1; - } - break; - case For_kind: - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.For.body, annotations_len)); - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.For.orelse, annotations_len)); - break; - case AsyncFor_kind: - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.AsyncFor.body, annotations_len)); - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.AsyncFor.orelse, annotations_len)); - break; - case While_kind: - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.While.body, annotations_len)); - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.While.orelse, annotations_len)); - break; - case If_kind: - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.If.body, annotations_len)); - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.If.orelse, annotations_len)); - break; - case With_kind: - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.With.body, annotations_len)); - break; - case AsyncWith_kind: - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.AsyncWith.body, annotations_len)); - break; - case Match_kind: - for (int j = 0; j < asdl_seq_LEN(st->v.Match.cases); j++) { - match_case_ty match_case = (match_case_ty)asdl_seq_GET( - st->v.Match.cases, j); - RETURN_IF_ERROR(compiler_collect_annotations(c, match_case->body, annotations_len)); - } - break; - case Try_kind: - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.Try.body, annotations_len)); - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.Try.orelse, annotations_len)); - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.Try.finalbody, annotations_len)); - for (int j = 0; j < asdl_seq_LEN(st->v.Try.handlers); j++) { - excepthandler_ty handler = (excepthandler_ty)asdl_seq_GET( - st->v.Try.handlers, j); - RETURN_IF_ERROR(compiler_collect_annotations(c, handler->v.ExceptHandler.body, annotations_len)); - } - break; - case TryStar_kind: - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.TryStar.body, annotations_len)); - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.TryStar.orelse, annotations_len)); - RETURN_IF_ERROR(compiler_collect_annotations(c, st->v.TryStar.finalbody, annotations_len)); - for (int j = 0; j < asdl_seq_LEN(st->v.TryStar.handlers); j++) { - excepthandler_ty handler = (excepthandler_ty)asdl_seq_GET( - st->v.Try.handlers, j); - RETURN_IF_ERROR(compiler_collect_annotations(c, handler->v.ExceptHandler.body, annotations_len)); - } - break; - default: - break; - } - } - return SUCCESS; - -} - /* Compile a sequence of statements, checking for a docstring and for annotations. */ @@ -1711,12 +1641,25 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) if (!(c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) && c->u->u_ste->ste_annotation_block != NULL) { void *key = (void *)((uintptr_t)c->u->u_ste->ste_id + 1); + assert(c->u->u_deferred_annotations != NULL); + PyObject *deferred_anno = Py_NewRef(c->u->u_deferred_annotations); RETURN_IF_ERROR(compiler_setup_annotations_scope(c, loc, key, c->u->u_ste->ste_annotation_block->ste_name)); - int annotations_len = 0; - RETURN_IF_ERROR( - compiler_collect_annotations(c, stmts, &annotations_len) - ); + Py_ssize_t annotations_len = PyList_Size(deferred_anno); + for (Py_ssize_t i = 0; i < annotations_len; i++) { + PyObject *ptr = PyList_GET_ITEM(deferred_anno, i); + stmt_ty st = (stmt_ty)PyLong_AsVoidPtr(ptr); + if (st == NULL) { + compiler_exit_scope(c); + Py_DECREF(deferred_anno); + return ERROR; + } + PyObject *mangled = _Py_Mangle(c->u->u_private, st->v.AnnAssign.target->v.Name.id); + ADDOP_LOAD_CONST_NEW(c, LOC(st), mangled); + VISIT(c, expr, st->v.AnnAssign.annotation); + } + Py_DECREF(deferred_anno); + RETURN_IF_ERROR( compiler_leave_annotations_scope(c, loc, annotations_len) ); @@ -6600,20 +6543,37 @@ compiler_annassign(struct compiler *c, stmt_ty s) return ERROR; } /* If we have a simple name in a module or class, store annotation. */ - if ((future_annotations || is_interactive) && - s->v.AnnAssign.simple && + if (s->v.AnnAssign.simple && (c->u->u_scope_type == COMPILER_SCOPE_MODULE || c->u->u_scope_type == COMPILER_SCOPE_CLASS)) { - if (future_annotations) { - VISIT(c, annexpr, s->v.AnnAssign.annotation); + if (future_annotations || is_interactive) { + if (future_annotations) { + VISIT(c, annexpr, s->v.AnnAssign.annotation); + } + else { + VISIT(c, expr, s->v.AnnAssign.annotation); + } + ADDOP_NAME(c, loc, LOAD_NAME, &_Py_ID(__annotations__), names); + mangled = _Py_MaybeMangle(c->u->u_private, c->u->u_ste, targ->v.Name.id); + ADDOP_LOAD_CONST_NEW(c, loc, mangled); + ADDOP(c, loc, STORE_SUBSCR); } else { - VISIT(c, expr, s->v.AnnAssign.annotation); + if (c->u->u_deferred_annotations == NULL) { + c->u->u_deferred_annotations = PyList_New(0); + if (c->u->u_deferred_annotations == NULL) { + return ERROR; + } + } + PyObject *ptr = PyLong_FromVoidPtr((void *)s); + if (ptr == NULL) { + return ERROR; + } + if (PyList_Append(c->u->u_deferred_annotations, ptr) < 0) { + Py_DECREF(ptr); + return ERROR; + } } - ADDOP_NAME(c, loc, LOAD_NAME, &_Py_ID(__annotations__), names); - mangled = _Py_MaybeMangle(c->u->u_private, c->u->u_ste, targ->v.Name.id); - ADDOP_LOAD_CONST_NEW(c, loc, mangled); - ADDOP(c, loc, STORE_SUBSCR); } break; case Attribute_kind: From 2ab5d0777c139b1f61e983c472cdad23282a1302 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 May 2024 18:08:16 -0700 Subject: [PATCH 050/105] Fix refleak --- Python/compile.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Python/compile.c b/Python/compile.c index 45ca6b8995c52c..0597b722df4f96 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1640,11 +1640,15 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) // __annotate__ function. See PEP 649. if (!(c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) && c->u->u_ste->ste_annotation_block != NULL) { - void *key = (void *)((uintptr_t)c->u->u_ste->ste_id + 1); + assert(c->u->u_deferred_annotations != NULL); PyObject *deferred_anno = Py_NewRef(c->u->u_deferred_annotations); - RETURN_IF_ERROR(compiler_setup_annotations_scope(c, loc, key, - c->u->u_ste->ste_annotation_block->ste_name)); + void *key = (void *)((uintptr_t)c->u->u_ste->ste_id + 1); + if (compiler_setup_annotations_scope(c, loc, key, + c->u->u_ste->ste_annotation_block->ste_name) == -1) { + Py_DECREF(deferred_anno); + return ERROR; + } Py_ssize_t annotations_len = PyList_Size(deferred_anno); for (Py_ssize_t i = 0; i < annotations_len; i++) { PyObject *ptr = PyList_GET_ITEM(deferred_anno, i); @@ -6573,6 +6577,7 @@ compiler_annassign(struct compiler *c, stmt_ty s) Py_DECREF(ptr); return ERROR; } + Py_DECREF(ptr); } } break; From 3b4a645ee8ecb1375c01c5f3ddef286e3f60e64b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 May 2024 18:37:05 -0700 Subject: [PATCH 051/105] Fix a test --- Lib/test/test_positional_only_arg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_positional_only_arg.py b/Lib/test/test_positional_only_arg.py index ad452c032d1614..eea0625012da6d 100644 --- a/Lib/test/test_positional_only_arg.py +++ b/Lib/test/test_positional_only_arg.py @@ -442,7 +442,7 @@ def f(x: not (int is int), /): ... # COMPARE_OP(is), IS_OP (0) # with constant folding we should expect a IS_OP (1) code_obj = next(const for const in g.__code__.co_consts - if isinstance(const, types.CodeType) and "annotations" in const.co_name) + if isinstance(const, types.CodeType) and const.co_name == "__annotate__") codes = [(i.opname, i.argval) for i in dis.get_instructions(code_obj)] self.assertNotIn(('UNARY_NOT', None), codes) self.assertIn(('IS_OP', 1), codes) From 1dfd02bb72a62e7a5f84eec5f7ff40ce3e1c73f9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 May 2024 19:04:53 -0700 Subject: [PATCH 052/105] Fix bug when there are only non-simple annotations --- Lib/test/test_type_annotations.py | 22 ++++++++++++++++++++++ Python/compile.c | 8 ++++++-- Python/symtable.c | 1 + 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 205802a1f7b6e2..9e79e8453e3c15 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -339,6 +339,28 @@ def test_no_exotic_expressions(self): check_syntax_error(self, "def func(x: (y := 3)): ...", "named expression cannot be used within an annotation") check_syntax_error(self, "def func(x: (await 42)): ...", "await expression cannot be used within an annotation") + def test_no_exotic_expressions_in_unevaluated_annotations(self): + preludes = [ + "", + "class X: ", + "def f(): ", + "async def f(): ", + ] + for prelude in preludes: + with self.subTest(prelude=prelude): + check_syntax_error(self, prelude + "(x): (yield)", "yield expression cannot be used within an annotation") + check_syntax_error(self, prelude + "(x): (yield from x)", "yield expression cannot be used within an annotation") + check_syntax_error(self, prelude + "(x): (y := 3)", "named expression cannot be used within an annotation") + check_syntax_error(self, prelude + "(x): (await 42)", "await expression cannot be used within an annotation") + + def test_ignore_non_simple_annotations(self): + ns = run_code("class X: (y): int") + self.assertEqual(ns["X"].__annotations__, {}) + ns = run_code("class X: int.b: int") + self.assertEqual(ns["X"].__annotations__, {}) + ns = run_code("class X: int[str]: int") + self.assertEqual(ns["X"].__annotations__, {}) + def test_generated_annotate(self): def func(x: int): pass diff --git a/Python/compile.c b/Python/compile.c index 0597b722df4f96..b150fd84e2a8e2 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1639,9 +1639,13 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) // collect the annotations in a separate pass and generate an // __annotate__ function. See PEP 649. if (!(c->c_future.ff_features & CO_FUTURE_ANNOTATIONS) && - c->u->u_ste->ste_annotation_block != NULL) { + c->u->u_deferred_annotations != NULL) { - assert(c->u->u_deferred_annotations != NULL); + // It's possible that ste_annotations_block is set but + // u_deferred_annotations is not, because the former is still + // set if there are only non-simple annotations. However, the + // reverse should not be possible. + assert(c->u->u_ste->ste_annotation_block != NULL); PyObject *deferred_anno = Py_NewRef(c->u->u_deferred_annotations); void *key = (void *)((uintptr_t)c->u->u_ste->ste_id + 1); if (compiler_setup_annotations_scope(c, loc, key, diff --git a/Python/symtable.c b/Python/symtable.c index 7314eb91fb44ee..f04f37d13e50c4 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -2500,6 +2500,7 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, VISIT_QUIT(st, 0); } } + // At the top level, we evaluate annotations eagerly, as specified by PEP 649. else if (!is_top_level_interactive) { if (st->st_cur->ste_annotation_block == NULL) { _Py_block_ty current_type = st->st_cur->ste_type; From daba318a722cd3a6c88ca4aa14d52fbc45b90863 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 May 2024 21:31:01 -0700 Subject: [PATCH 053/105] Fix more tests --- Lib/test/test_dis.py | 2 +- Lib/test/test_pyclbr.py | 2 ++ Lib/test/test_pydoc/test_pydoc.py | 4 ++-- Lib/test/test_traceback.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index b0873a4f9381b7..3313a4c5370723 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -363,7 +363,7 @@ def wrap_func_w_kwargs(): CALL 1 STORE_SUBSCR - 2 LOAD_CONST 2 ( at 0x..., file "", line 2>) + 2 LOAD_CONST 2 (", line 2>) MAKE_FUNCTION STORE_NAME 3 (__annotate__) RETURN_CONST 3 (None) diff --git a/Lib/test/test_pyclbr.py b/Lib/test/test_pyclbr.py index 46206accbafc36..0c12a3085b12af 100644 --- a/Lib/test/test_pyclbr.py +++ b/Lib/test/test_pyclbr.py @@ -109,6 +109,8 @@ def ismethod(oclass, obj, name): actualMethods = [] for m in py_item.__dict__.keys(): + if m == "__annotate__": + continue if ismethod(py_item, getattr(py_item, m), m): actualMethods.append(m) foundMethods = [] diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index b4489d77d19948..afed23b3af5670 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -78,7 +78,7 @@ class A(builtins.object) class B(builtins.object) | Methods defined here: | - | __annotate__ = (...) + | __annotate__(...) | | ---------------------------------------------------------------------- | Data descriptors defined here: @@ -179,7 +179,7 @@ class A(builtins.object) class B(builtins.object) Methods defined here: - __annotate__ = (...) + __annotate__(...) ---------------------------------------------------------------------- Data descriptors defined here: __dict__ diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 3da1671b477cee..1895c88d23b70d 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -632,7 +632,7 @@ def foo(a: THIS_DOES_NOT_EXIST ) -> int: ' ~~~~~~~~^^\n' f' File "{__file__}", line {lineno_f+3}, in f_with_type\n' ' foo.__annotations__\n' - f' File "{__file__}", line {lineno_f+1}, in \n' + f' File "{__file__}", line {lineno_f+1}, in __annotate__\n' ' def foo(a: THIS_DOES_NOT_EXIST ) -> int:\n' ' ^^^^^^^^^^^^^^^^^^^\n' ) From b066b3d92bf9dfe0f2230cf406ee3f467e8858cd Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 May 2024 06:59:53 -0700 Subject: [PATCH 054/105] Remove REPL special case --- Include/internal/pycore_symtable.h | 1 - Python/compile.c | 14 +++++--------- Python/symtable.c | 9 ++------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/Include/internal/pycore_symtable.h b/Include/internal/pycore_symtable.h index bd1330c3968251..5d544765237df5 100644 --- a/Include/internal/pycore_symtable.h +++ b/Include/internal/pycore_symtable.h @@ -72,7 +72,6 @@ struct symtable { the symbol table */ int recursion_depth; /* current recursion depth */ int recursion_limit; /* recursion limit */ - int st_kind; /* kind of module */ }; typedef struct _symtable_entry { diff --git a/Python/compile.c b/Python/compile.c index f8caefb29093ca..e2aecbe57ac7eb 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1660,11 +1660,10 @@ compiler_codegen(struct compiler *c, mod_ty mod) } break; case Interactive_kind: - if (c->u->u_ste->ste_annotations_used) { - ADDOP(c, loc, SETUP_ANNOTATIONS); - } c->c_interactive = 1; - VISIT_SEQ(c, stmt, mod->v.Interactive.body); + if (compiler_body(c, loc, mod->v.Interactive.body) < 0) { + return ERROR; + } break; case Expression_kind: VISIT(c, expr, mod->v.Expression.body); @@ -6503,9 +6502,6 @@ compiler_annassign(struct compiler *c, stmt_ty s) { location loc = LOC(s); expr_ty targ = s->v.AnnAssign.target; - bool is_interactive = ( - c->c_st->st_kind == Interactive_kind && c->u->u_scope_type == COMPILER_SCOPE_MODULE - ); bool future_annotations = c->c_future.ff_features & CO_FUTURE_ANNOTATIONS; PyObject *mangled; @@ -6525,7 +6521,7 @@ compiler_annassign(struct compiler *c, stmt_ty s) if (s->v.AnnAssign.simple && (c->u->u_scope_type == COMPILER_SCOPE_MODULE || c->u->u_scope_type == COMPILER_SCOPE_CLASS)) { - if (future_annotations || is_interactive) { + if (future_annotations) { if (future_annotations) { VISIT(c, annexpr, s->v.AnnAssign.annotation); } @@ -6579,7 +6575,7 @@ compiler_annassign(struct compiler *c, stmt_ty s) return ERROR; } /* Annotation is evaluated last. */ - if ((future_annotations || is_interactive) && !s->v.AnnAssign.simple && check_annotation(c, s) < 0) { + if (future_annotations && !s->v.AnnAssign.simple && check_annotation(c, s) < 0) { return ERROR; } return SUCCESS; diff --git a/Python/symtable.c b/Python/symtable.c index f04f37d13e50c4..42025c65cce992 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -430,7 +430,6 @@ _PySymtable_Build(mod_ty mod, PyObject *filename, _PyFutureFeatures *future) } st->st_top = st->st_cur; - st->st_kind = mod->kind; switch (mod->kind) { case Module_kind: seq = mod->v.Module.body; @@ -2491,17 +2490,13 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, struct _symtable_entry *parent_ste, void *key) { int future_annotations = st->st_future->ff_features & CO_FUTURE_ANNOTATIONS; - int is_top_level_interactive = ( - st->st_kind == Interactive_kind && st->st_cur->ste_type == ModuleBlock - ); if (future_annotations) { if(!symtable_enter_block(st, &_Py_ID(_annotation), AnnotationBlock, key, LOCATION(annotation))) { VISIT_QUIT(st, 0); } } - // At the top level, we evaluate annotations eagerly, as specified by PEP 649. - else if (!is_top_level_interactive) { + else { if (st->st_cur->ste_annotation_block == NULL) { _Py_block_ty current_type = st->st_cur->ste_type; if (!symtable_enter_block(st, &_Py_ID(__annotate__), AnnotationBlock, @@ -2536,7 +2531,7 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, } } VISIT(st, expr, annotation); - if ((future_annotations || !is_top_level_interactive) && !symtable_exit_block(st)) { + if (!symtable_exit_block(st)) { VISIT_QUIT(st, 0); } return 1; From 7669361be51e3d5d9d216ac218a4ddf3728ecac8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 May 2024 08:10:10 -0700 Subject: [PATCH 055/105] no docstrings in the repl --- Python/compile.c | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Python/compile.c b/Python/compile.c index e2aecbe57ac7eb..9d6f181de56c1c 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1586,21 +1586,23 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) return SUCCESS; } Py_ssize_t first_instr = 0; - PyObject *docstring = _PyAST_GetDocString(stmts); - if (docstring) { - first_instr = 1; - /* if not -OO mode, set docstring */ - if (c->c_optimize < 2) { - PyObject *cleandoc = _PyCompile_CleanDoc(docstring); - if (cleandoc == NULL) { - return ERROR; + if (!c->c_interactive) { + PyObject *docstring = _PyAST_GetDocString(stmts); + if (docstring) { + first_instr = 1; + /* if not -OO mode, set docstring */ + if (c->c_optimize < 2) { + PyObject *cleandoc = _PyCompile_CleanDoc(docstring); + if (cleandoc == NULL) { + return ERROR; + } + stmt_ty st = (stmt_ty)asdl_seq_GET(stmts, 0); + assert(st->kind == Expr_kind); + location loc = LOC(st->v.Expr.value); + ADDOP_LOAD_CONST(c, loc, cleandoc); + Py_DECREF(cleandoc); + RETURN_IF_ERROR(compiler_nameop(c, NO_LOCATION, &_Py_ID(__doc__), Store)); } - stmt_ty st = (stmt_ty)asdl_seq_GET(stmts, 0); - assert(st->kind == Expr_kind); - location loc = LOC(st->v.Expr.value); - ADDOP_LOAD_CONST(c, loc, cleandoc); - Py_DECREF(cleandoc); - RETURN_IF_ERROR(compiler_nameop(c, NO_LOCATION, &_Py_ID(__doc__), Store)); } } for (Py_ssize_t i = first_instr; i < asdl_seq_LEN(stmts); i++) { From c6a1b80f5a321f3eab98efb07af495d054580465 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 May 2024 08:47:52 -0700 Subject: [PATCH 056/105] Fix pyrepl test --- Lib/test/test_pyrepl/test_interact.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index 6bc297d8d2aa6b..72ed3a00190f0f 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -105,7 +105,7 @@ def test_runsource_shows_syntax_error_for_failed_compilation(self): def test_no_active_future(self): console = InteractiveColoredConsole() - source = "if True:\n x: int = 1; print(__annotations__)" + source = "x: int = 1; print(__annotate__(1))" f = io.StringIO() with contextlib.redirect_stdout(f): result = console.runsource(source) From 5cdbdd732dc8ae63ea34a176cefdc8c6d6b40d12 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 May 2024 22:26:49 -0700 Subject: [PATCH 057/105] CR feedback --- Lib/test/test_type_annotations.py | 2 ++ Lib/typing.py | 6 +++++- Python/compile.c | 17 ++++++----------- Python/symtable.c | 21 ++++++++++----------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 9e79e8453e3c15..25dc4d883eb356 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -374,6 +374,8 @@ class X: self.assertEqual(annotate.__name__, "__annotate__") with self.assertRaises(NotImplementedError): annotate(inspect.FORWARDREF) + with self.assertRaises(NotImplementedError): + annotate(inspect.SOURCE) with self.assertRaises(NotImplementedError): annotate(None) self.assertEqual(annotate(inspect.VALUE), {"x": int}) diff --git a/Lib/typing.py b/Lib/typing.py index 15307f28de6c8a..f158530359ab1b 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3154,7 +3154,11 @@ def __new__(cls, name, bases, ns, total=True): mutable_keys = set() for base in bases: - # TODO: preserve laziness + # TODO: Avoid eagerly evaluating annotations in VALUE format. + # Instead, evaluate in FORWARDREF format to figure out which + # keys have Required/NotRequired/ReadOnly qualifiers, and create + # a new __annotate__ function for the resulting TypedDict that + # combines the annotations from this class and its parents. annotations.update(base.__annotations__) base_required = base.__dict__.get('__required_keys__', set()) diff --git a/Python/compile.c b/Python/compile.c index 9d6f181de56c1c..96a4a38bf38e85 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1616,7 +1616,8 @@ compiler_body(struct compiler *c, location loc, asdl_stmt_seq *stmts) // It's possible that ste_annotations_block is set but // u_deferred_annotations is not, because the former is still - // set if there are only non-simple annotations. However, the + // set if there are only non-simple annotations (i.e., annotations + // for attributes, subscripts, or parenthesized names). However, the // reverse should not be possible. assert(c->u->u_ste->ste_annotation_block != NULL); PyObject *deferred_anno = Py_NewRef(c->u->u_deferred_annotations); @@ -2368,7 +2369,6 @@ compiler_function(struct compiler *c, stmt_ty s, int is_async) asdl_expr_seq *decos; asdl_type_param_seq *type_params; Py_ssize_t funcflags; - int annotations; int firstlineno; if (is_async) { @@ -2434,14 +2434,14 @@ compiler_function(struct compiler *c, stmt_ty s, int is_async) } } - annotations = compiler_visit_annotations(c, loc, args, returns); - if (annotations < 0) { + int annotations_flag = compiler_visit_annotations(c, loc, args, returns); + if (annotations_flag < 0) { if (is_generic) { compiler_exit_scope(c); } return ERROR; } - funcflags |= annotations; + funcflags |= annotations_flag; if (compiler_function_body(c, s, is_async, funcflags, firstlineno) < 0) { if (is_generic) { @@ -6524,12 +6524,7 @@ compiler_annassign(struct compiler *c, stmt_ty s) (c->u->u_scope_type == COMPILER_SCOPE_MODULE || c->u->u_scope_type == COMPILER_SCOPE_CLASS)) { if (future_annotations) { - if (future_annotations) { - VISIT(c, annexpr, s->v.AnnAssign.annotation); - } - else { - VISIT(c, expr, s->v.AnnAssign.annotation); - } + VISIT(c, annexpr, s->v.AnnAssign.annotation); ADDOP_NAME(c, loc, LOAD_NAME, &_Py_ID(__annotations__), names); mangled = _Py_MaybeMangle(c->u->u_private, c->u->u_ste, targ->v.Name.id); ADDOP_LOAD_CONST_NEW(c, loc, mangled); diff --git a/Python/symtable.c b/Python/symtable.c index 42025c65cce992..ebf936ab25799a 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -248,8 +248,7 @@ static int symtable_visit_alias(struct symtable *st, alias_ty); static int symtable_visit_comprehension(struct symtable *st, comprehension_ty); static int symtable_visit_keyword(struct symtable *st, keyword_ty); static int symtable_visit_params(struct symtable *st, asdl_arg_seq *args); -static int symtable_visit_annotation(struct symtable *st, expr_ty annotation, - struct _symtable_entry *parent_ste, void *key); +static int symtable_visit_annotation(struct symtable *st, expr_ty annotation, void *key); static int symtable_visit_argannotations(struct symtable *st, asdl_arg_seq *args); static int symtable_implicit_arg(struct symtable *st, int pos); static int symtable_visit_annotations(struct symtable *st, stmt_ty, arguments_ty, expr_ty, @@ -1361,9 +1360,9 @@ symtable_enter_existing_block(struct symtable *st, PySTEntryObject* ste) /* The entry is owned by the stack. Borrow it for st_cur. */ st->st_cur = ste; - /* Annotation blocks shouldn't have any affect on the symbol table since in - * the compilation stage, they will all be transformed to strings. They are - * only created if future 'annotations' feature is activated. */ + /* If "from __future__ import annotations" is active, + * annotation blocks shouldn't have any affect on the symbol table since in + * the compilation stage, they will all be transformed to strings. */ if (st->st_future->ff_features & CO_FUTURE_ANNOTATIONS && ste->ste_type == AnnotationBlock) { return 1; } @@ -1843,7 +1842,7 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s) else { VISIT(st, expr, s->v.AnnAssign.target); } - if (!symtable_visit_annotation(st, s->v.AnnAssign.annotation, st->st_cur, + if (!symtable_visit_annotation(st, s->v.AnnAssign.annotation, (void *)((uintptr_t)st->st_cur->ste_id + 1))) { VISIT_QUIT(st, 0); } @@ -2486,9 +2485,9 @@ symtable_visit_params(struct symtable *st, asdl_arg_seq *args) } static int -symtable_visit_annotation(struct symtable *st, expr_ty annotation, - struct _symtable_entry *parent_ste, void *key) +symtable_visit_annotation(struct symtable *st, expr_ty annotation, void *key) { + struct _symtable_entry *parent_ste = st->st_cur; int future_annotations = st->st_future->ff_features & CO_FUTURE_ANNOTATIONS; if (future_annotations) { if(!symtable_enter_block(st, &_Py_ID(_annotation), AnnotationBlock, @@ -2497,8 +2496,8 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, } } else { - if (st->st_cur->ste_annotation_block == NULL) { - _Py_block_ty current_type = st->st_cur->ste_type; + if (parent_ste->ste_annotation_block == NULL) { + _Py_block_ty current_type = parent_ste->ste_type; if (!symtable_enter_block(st, &_Py_ID(__annotate__), AnnotationBlock, key, LOCATION(annotation))) { VISIT_QUIT(st, 0); @@ -2525,7 +2524,7 @@ symtable_visit_annotation(struct symtable *st, expr_ty annotation, } } else { - if (!symtable_enter_existing_block(st, st->st_cur->ste_annotation_block)) { + if (!symtable_enter_existing_block(st, parent_ste->ste_annotation_block)) { VISIT_QUIT(st, 0); } } From 26c731b9f7be6ed29e09b3a0d7a04f80a5f1d578 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 30 May 2024 22:14:00 -0700 Subject: [PATCH 058/105] New annotations module --- Lib/annotations.py | 373 +++++++++++++++++++++++++++++++++++ Lib/test/test_annotations.py | 52 +++++ Lib/test/test_typing.py | 28 +-- Lib/typing.py | 146 +++++--------- 4 files changed, 481 insertions(+), 118 deletions(-) create mode 100644 Lib/annotations.py create mode 100644 Lib/test/test_annotations.py diff --git a/Lib/annotations.py b/Lib/annotations.py new file mode 100644 index 00000000000000..4deb48ba427b67 --- /dev/null +++ b/Lib/annotations.py @@ -0,0 +1,373 @@ +"""Helpers for introspecting and wrapping annotations.""" + +import ast +import enum +import sys +import types + + +class Format(enum.IntEnum): + VALUE = 1 + FORWARDREF = 2 + SOURCE = 3 + +_Union = None + + +class ForwardRef: + """Internal wrapper to hold a forward reference.""" + + __slots__ = ('__forward_arg__', + '__forward_evaluated__', '__forward_value__', + '__forward_is_argument__', '__forward_is_class__', + '__forward_module__', '__weakref__', + '_forward_code', '_globals', '_owner', '_cell') + + def __init__(self, arg, is_argument=True, module=None, *, is_class=False, + _globals=None, _owner=None, _cell=None): + if not isinstance(arg, str): + raise TypeError(f"Forward reference must be a string -- got {arg!r}") + + self.__forward_arg__ = arg + self.__forward_evaluated__ = False + self.__forward_value__ = None + self.__forward_is_argument__ = is_argument + self.__forward_is_class__ = is_class + self.__forward_module__ = module + self._forward_code = None + self._globals = _globals + self._cell = _cell + self._owner = _owner + + def __init_subclass__(cls, /, *args, **kwds): + raise TypeError("Cannot subclass ForwardRef") + + def evaluate(self, *, globals=None, locals=None): + """Evaluate the forward reference and return the value. + + If the forward reference is not evaluatable, raise a SyntaxError. + """ + if self.__forward_evaluated__: + return self.__forward_value__ + if self._cell is not None: + try: + value = self._cell.cell_contents + except ValueError: + pass + else: + self.__forward_evaluated__ = True + self.__forward_value__ = value + return value + + code = self.__forward_code__ + if globals is None: + globals = self._globals + if globals is None: + globals = {} + if locals is None: + locals = {} + if isinstance(self._owner, type): + locals.update(vars(self._owner)) + if self._owner is not None: + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + type_params = getattr(self._owner, '__type_params__', None) + if type_params: + locals = {param.__name__: param for param in type_params} | locals + value = eval(code, globals=globals, locals=locals) + self.__forward_evaluated__ = True + self.__forward_value__ = value + return value + + @property + def __forward_code__(self): + if self._forward_code is not None: + return self._forward_code + arg = self.__forward_arg__ + # If we do `def f(*args: *Ts)`, then we'll have `arg = '*Ts'`. + # Unfortunately, this isn't a valid expression on its own, so we + # do the unpacking manually. + if arg.startswith('*'): + arg_to_compile = f'({arg},)[0]' # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] + else: + arg_to_compile = arg + try: + self._forward_code = compile(arg_to_compile, '', 'eval') + except SyntaxError: + raise SyntaxError(f"Forward reference must be an expression -- got {arg!r}") + return self._forward_code + + def __eq__(self, other): + if not isinstance(other, ForwardRef): + return NotImplemented + if self.__forward_evaluated__ and other.__forward_evaluated__: + return (self.__forward_arg__ == other.__forward_arg__ and + self.__forward_value__ == other.__forward_value__) + return (self.__forward_arg__ == other.__forward_arg__ and + self.__forward_module__ == other.__forward_module__) + + def __hash__(self): + return hash((self.__forward_arg__, self.__forward_module__)) + + def __or__(self, other): + global _Union + if _Union is None: + from typing import Union as _Union + return _Union[self, other] + + def __ror__(self, other): + global _Union + if _Union is None: + from typing import Union as _Union + return _Union[other, self] + + def __repr__(self): + if self.__forward_module__ is None: + module_repr = '' + else: + module_repr = f', module={self.__forward_module__!r}' + return f'ForwardRef({self.__forward_arg__!r}{module_repr})' + + +class _ForwardReffer(dict): + def __init__(self, namespace, globals, owner, is_class): + super().__init__(namespace) + self.namespace = namespace + self.globals = globals + self.owner = owner + self.is_class = is_class + + def __missing__(self, key): + return ForwardRef(key, _globals=self.globals, _owner=self.owner, + is_class=self.is_class) + + +class Stringifier: + def __init__(self, node): + self.node = node + + def _convert(self, other): + if isinstance(other, Stringifier): + return other.node + else: + return ast.Name(id=repr(other)) + + def _make_binop(op: ast.AST): + def binop(self, other): + return Stringifier(ast.BinOp(self.node, op, self._convert(other))) + return binop + + __add__ = _make_binop(ast.Add()) + __sub__ = _make_binop(ast.Sub()) + __mul__ = _make_binop(ast.Mult()) + __matmul__ = _make_binop(ast.MatMult()) + __div__ = _make_binop(ast.Div()) + __mod__ = _make_binop(ast.Mod()) + __lshift__ = _make_binop(ast.LShift()) + __rshift__ = _make_binop(ast.RShift()) + __or__ = _make_binop(ast.BitOr()) + __xor__ = _make_binop(ast.BitXor()) + __and__ = _make_binop(ast.And()) + __floordiv__ = _make_binop(ast.FloorDiv()) + __pow__ = _make_binop(ast.Pow()) + + def _make_unary_op(op): + def unary_op(self): + return Stringifier(ast.UnaryOp(self.node, op)) + return unary_op + + __invert__ = _make_unary_op(ast.Invert()) + __pos__ = _make_binop(ast.UAdd()) + __neg__ = _make_binop(ast.USub()) + + def __getitem__(self, other): + if isinstance(other, tuple): + elts = [self._convert(elt) for elt in other] + other = ast.Tuple(elts) + else: + other = self._convert(other) + return Stringifier(ast.Subscript(self.node, other)) + + def __getattr__(self, attr): + return Stringifier(ast.Attribute(self.node, attr)) + + def __call__(self, *args, **kwargs): + return Stringifier(ast.Call( + self.node, + [self._convert(arg) for arg in args], + [ast.keyword(key, self._convert(value)) for key, value in kwargs.items()] + )) + + def __iter__(self): + return self + + def __next__(self): + return Stringifier(ast.Starred(self.node)) + + +class _StringifierDict(dict): + def __missing__(self, key): + return Stringifier(ast.Name(key)) + + +def _call_dunder_annotate(annotate, format, owner=None): + try: + return annotate(format) + except NotImplementedError: + pass + if format == Format.FORWARDREF: + namespace = {**annotate.__builtins__, **annotate.__globals__} + is_class = isinstance(owner, type) + globals = _ForwardReffer(namespace, annotate.__globals__, owner, is_class) + if annotate.__closure__: + freevars = annotate.__code__.co_freevars + new_closure = [] + for i, cell in enumerate(annotate.__closure__): + try: + cell.cell_contents + except ValueError: + if i < len(freevars): + name = freevars[i] + else: + name = "__cell__" + fwdref = ForwardRef(name, _cell=cell, _owner=owner, _globals=annotate.__globals__, + is_class=is_class) + new_closure.append(types.CellType(fwdref)) + else: + new_closure.append(cell) + closure = tuple(new_closure) + else: + closure = None + func = types.FunctionType( + annotate.__code__, + globals, + closure=closure + ) + return func(Format.VALUE) + elif format == Format.SOURCE: + globals = _StringifierDict() + func = types.FunctionType( + annotate.__code__, + globals, + # TODO: also replace the closure with stringifiers + closure=annotate.__closure__ + ) + annos = func(Format.VALUE) + return { + key: ast.unparse(val.node) if isinstance(val, Stringifier) else repr(val) + for key, val in annos.items() + } + + +def get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE): + """Compute the annotations dict for an object. + + obj may be a callable, class, or module. + Passing in an object of any other type raises TypeError. + + Returns a dict. get_annotations() returns a new dict every time + it's called; calling it twice on the same object will return two + different but equivalent dicts. + + This function handles several details for you: + + * If eval_str is true, values of type str will + be un-stringized using eval(). This is intended + for use with stringized annotations + ("from __future__ import annotations"). + * If obj doesn't have an annotations dict, returns an + empty dict. (Functions and methods always have an + annotations dict; classes, modules, and other types of + callables may not.) + * Ignores inherited annotations on classes. If a class + doesn't have its own annotations dict, returns an empty dict. + * All accesses to object members and dict values are done + using getattr() and dict.get() for safety. + * Always, always, always returns a freshly-created dict. + + eval_str controls whether or not values of type str are replaced + with the result of calling eval() on those values: + + * If eval_str is true, eval() is called on values of type str. + * If eval_str is false (the default), values of type str are unchanged. + + globals and locals are passed in to eval(); see the documentation + for eval() for more information. If either globals or locals is + None, this function may replace that value with a context-specific + default, contingent on type(obj): + + * If obj is a module, globals defaults to obj.__dict__. + * If obj is a class, globals defaults to + sys.modules[obj.__module__].__dict__ and locals + defaults to the obj class namespace. + * If obj is a callable, globals defaults to obj.__globals__, + although if obj is a wrapped function (using + functools.update_wrapper()) it is first unwrapped. + """ + annotate = getattr(obj, "__annotate__", None) + # TODO remove format != VALUE condition + if annotate is not None and format != Format.VALUE: + ann = _call_dunder_annotate(annotate, format) + elif isinstance(obj, type): + # class + ann = obj.__annotations__ + + obj_globals = None + module_name = getattr(obj, '__module__', None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + obj_globals = getattr(module, '__dict__', None) + obj_locals = dict(vars(obj)) + unwrap = obj + elif isinstance(obj, types.ModuleType): + # module + ann = getattr(obj, '__annotations__', None) + obj_globals = getattr(obj, '__dict__') + obj_locals = None + unwrap = None + elif callable(obj): + # this includes types.Function, types.BuiltinFunctionType, + # types.BuiltinMethodType, functools.partial, functools.singledispatch, + # "class funclike" from Lib/test/test_inspect... on and on it goes. + ann = getattr(obj, '__annotations__', None) + obj_globals = getattr(obj, '__globals__', None) + obj_locals = None + unwrap = obj + else: + raise TypeError(f"{obj!r} is not a module, class, or callable.") + + if ann is None: + return {} + + if not isinstance(ann, dict): + raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") + + if not ann: + return {} + + if not eval_str: + return dict(ann) + + if unwrap is not None: + while True: + if hasattr(unwrap, '__wrapped__'): + unwrap = unwrap.__wrapped__ + continue + if isinstance(unwrap, functools.partial): + unwrap = unwrap.func + continue + break + if hasattr(unwrap, "__globals__"): + obj_globals = unwrap.__globals__ + + if globals is None: + globals = obj_globals + if locals is None: + locals = obj_locals + + return_value = {key: + value if not isinstance(value, str) else eval(value, globals, locals) + for key, value in ann.items() } + return return_value diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py new file mode 100644 index 00000000000000..0a9d61f6eeb18d --- /dev/null +++ b/Lib/test/test_annotations.py @@ -0,0 +1,52 @@ +"""Tests for the annotations module.""" + +import annotations +import pickle +import unittest + + +class TestForwardRefFormat(unittest.TestCase): + def test_closure(self): + def inner(arg: x): + pass + + anno = annotations.get_annotations(inner, format=annotations.Format.FORWARDREF) + fwdref = anno["arg"] + self.assertIsInstance(fwdref, annotations.ForwardRef) + self.assertEqual(fwdref.__forward_arg__, 'x') + with self.assertRaises(NameError): + fwdref.evaluate() + + x = 1 + self.assertEqual(fwdref.evaluate(), x) + + anno = annotations.get_annotations(inner, format=annotations.Format.FORWARDREF) + self.assertEqual(anno["arg"], x) + + def test_function(self): + def f(x: int, y: doesntexist): + pass + + anno = annotations.get_annotations(f, format=annotations.Format.FORWARDREF) + self.assertIs(anno["x"], int) + fwdref = anno["y"] + self.assertIsInstance(fwdref, annotations.ForwardRef) + self.assertEqual(fwdref.__forward_arg__, "doesntexist") + with self.assertRaises(NameError): + fwdref.evaluate() + self.assertEqual(fwdref.evaluate(globals={"doesntexist": 1}), 1) + + +class TestForwardRefClass(unittest.TestCase): + def test_special_attrs(self): + # Forward refs provide a different introspection API. __name__ and + # __qualname__ make little sense for forward refs as they can store + # complex typing expressions. + fr = annotations.ForwardRef('set[Any]') + self.assertFalse(hasattr(fr, '__name__')) + self.assertFalse(hasattr(fr, '__qualname__')) + self.assertEqual(fr.__module__, 'typing') + # Forward refs are currently unpicklable. + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.assertRaises(TypeError): + pickle.dumps(fr, proto) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 9800b3b6a7da29..39ba0de3358e09 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -5981,7 +5981,9 @@ def fun(x: a): return a self.assertEqual(namespace1(), namespace1()) - self.assertNotEqual(namespace1(), namespace2()) + # TODO(PEP 649): Used to be assertNotEqual because get_type_hints() + # would mutate the ForwardRef objects. Do we need to preserve this test? + self.assertEqual(namespace1(), namespace2()) def test_forward_repr(self): self.assertEqual(repr(List['int']), "typing.List[ForwardRef('int')]") @@ -6046,7 +6048,9 @@ def cmp(o1, o2): r1 = namespace1() r2 = namespace2() self.assertIsNot(r1, r2) - self.assertRaises(RecursionError, cmp, r1, r2) + self.assertEqual(r1, r2) + # TODO(PEP 649): do we need to preserve this test somehow? + # self.assertRaises(RecursionError, cmp, r1, r2) def test_union_forward_recursion(self): ValueList = List['Value'] @@ -6345,10 +6349,10 @@ def test_deprecation_for_no_type_params_passed_to__evaluate(self): DeprecationWarning, ( "Failing to pass a value to the 'type_params' parameter " - "of 'typing.ForwardRef._evaluate' is deprecated" + "of 'typing._evaluate_forward_ref' is deprecated" ) ) as cm: - self.assertIs(f._evaluate(globals(), {}, recursive_guard=frozenset()), int) + self.assertIs(typing._evaluate_forward_ref(f, globals(), {}, recursive_guard=frozenset()), int) self.assertEqual(cm.filename, __file__) @@ -10017,7 +10021,6 @@ def test_special_attrs(self): typing.ClassVar: 'ClassVar', typing.Concatenate: 'Concatenate', typing.Final: 'Final', - typing.ForwardRef: 'ForwardRef', typing.Literal: 'Literal', typing.NewType: 'NewType', typing.NoReturn: 'NoReturn', @@ -10029,7 +10032,7 @@ def test_special_attrs(self): typing.TypeVar: 'TypeVar', typing.Union: 'Union', typing.Self: 'Self', - # Subscribed special forms + # Subscripted special forms typing.Annotated[Any, "Annotation"]: 'Annotated', typing.Annotated[int, 'Annotation']: 'Annotated', typing.ClassVar[Any]: 'ClassVar', @@ -10044,7 +10047,6 @@ def test_special_attrs(self): typing.Union[Any]: 'Any', typing.Union[int, float]: 'Union', # Incompatible special forms (tested in test_special_attrs2) - # - typing.ForwardRef('set[Any]') # - typing.NewType('TypeName', Any) # - typing.ParamSpec('SpecialAttrsP') # - typing.TypeVar('T') @@ -10063,18 +10065,6 @@ def test_special_attrs(self): TypeName = typing.NewType('SpecialAttrsTests.TypeName', Any) def test_special_attrs2(self): - # Forward refs provide a different introspection API. __name__ and - # __qualname__ make little sense for forward refs as they can store - # complex typing expressions. - fr = typing.ForwardRef('set[Any]') - self.assertFalse(hasattr(fr, '__name__')) - self.assertFalse(hasattr(fr, '__qualname__')) - self.assertEqual(fr.__module__, 'typing') - # Forward refs are currently unpicklable. - for proto in range(pickle.HIGHEST_PROTOCOL + 1): - with self.assertRaises(TypeError): - pickle.dumps(fr, proto) - self.assertEqual(SpecialAttrsTests.TypeName.__name__, 'TypeName') self.assertEqual( SpecialAttrsTests.TypeName.__qualname__, diff --git a/Lib/typing.py b/Lib/typing.py index f158530359ab1b..4e093f0d9982bc 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -19,6 +19,7 @@ """ from abc import abstractmethod, ABCMeta +from annotations import ForwardRef import collections from collections import defaultdict import collections.abc @@ -166,7 +167,7 @@ def _type_convert(arg, module=None, *, allow_special_forms=False): if arg is None: return type(None) if isinstance(arg, str): - return ForwardRef(arg, module=module, is_class=allow_special_forms) + return _make_forward_ref(arg, module=module, is_class=allow_special_forms) return arg @@ -471,11 +472,11 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f _deprecation_warning_for_no_type_params_passed("typing._eval_type") type_params = () if isinstance(t, ForwardRef): - return t._evaluate(globalns, localns, type_params, recursive_guard=recursive_guard) + return _evaluate_forward_ref(t, globalns, localns, type_params, recursive_guard=recursive_guard) if isinstance(t, (_GenericAlias, GenericAlias, types.UnionType)): if isinstance(t, GenericAlias): args = tuple( - ForwardRef(arg) if isinstance(arg, str) else arg + _make_forward_ref(arg) if isinstance(arg, str) else arg for arg in t.__args__ ) is_unpacked = t.__unpacked__ @@ -1012,102 +1013,49 @@ def run(arg: Child | Unrelated): return _GenericAlias(self, (item,)) -class ForwardRef(_Final, _root=True): - """Internal wrapper to hold a forward reference.""" +def _make_forward_ref(code, **kwargs): + forward_ref = ForwardRef(code, **kwargs) + # For compatibility, eagerly compile the forwardref's code. + forward_ref.__forward_code__ + return forward_ref - __slots__ = ('__forward_arg__', '__forward_code__', - '__forward_evaluated__', '__forward_value__', - '__forward_is_argument__', '__forward_is_class__', - '__forward_module__') - def __init__(self, arg, is_argument=True, module=None, *, is_class=False): - if not isinstance(arg, str): - raise TypeError(f"Forward reference must be a string -- got {arg!r}") - - # If we do `def f(*args: *Ts)`, then we'll have `arg = '*Ts'`. - # Unfortunately, this isn't a valid expression on its own, so we - # do the unpacking manually. - if arg.startswith('*'): - arg_to_compile = f'({arg},)[0]' # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] - else: - arg_to_compile = arg - try: - code = compile(arg_to_compile, '', 'eval') - except SyntaxError: - raise SyntaxError(f"Forward reference must be an expression -- got {arg!r}") - - self.__forward_arg__ = arg - self.__forward_code__ = code - self.__forward_evaluated__ = False - self.__forward_value__ = None - self.__forward_is_argument__ = is_argument - self.__forward_is_class__ = is_class - self.__forward_module__ = module - - def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard): - if type_params is _sentinel: - _deprecation_warning_for_no_type_params_passed("typing.ForwardRef._evaluate") - type_params = () - if self.__forward_arg__ in recursive_guard: - return self - if not self.__forward_evaluated__ or localns is not globalns: - if globalns is None and localns is None: - globalns = localns = {} - elif globalns is None: - globalns = localns - elif localns is None: - localns = globalns - if self.__forward_module__ is not None: - globalns = getattr( - sys.modules.get(self.__forward_module__, None), '__dict__', globalns - ) - if type_params: - # "Inject" type parameters into the local namespace - # (unless they are shadowed by assignments *in* the local namespace), - # as a way of emulating annotation scopes when calling `eval()` - locals_to_pass = {param.__name__: param for param in type_params} | localns - else: - locals_to_pass = localns - type_ = _type_check( - eval(self.__forward_code__, globalns, locals_to_pass), - "Forward references must evaluate to types.", - is_argument=self.__forward_is_argument__, - allow_special_forms=self.__forward_is_class__, - ) - self.__forward_value__ = _eval_type( - type_, - globalns, - localns, - type_params, - recursive_guard=(recursive_guard | {self.__forward_arg__}), - ) - self.__forward_evaluated__ = True - return self.__forward_value__ - - def __eq__(self, other): - if not isinstance(other, ForwardRef): - return NotImplemented - if self.__forward_evaluated__ and other.__forward_evaluated__: - return (self.__forward_arg__ == other.__forward_arg__ and - self.__forward_value__ == other.__forward_value__) - return (self.__forward_arg__ == other.__forward_arg__ and - self.__forward_module__ == other.__forward_module__) - - def __hash__(self): - return hash((self.__forward_arg__, self.__forward_module__)) - - def __or__(self, other): - return Union[self, other] - - def __ror__(self, other): - return Union[other, self] - - def __repr__(self): - if self.__forward_module__ is None: - module_repr = '' - else: - module_repr = f', module={self.__forward_module__!r}' - return f'ForwardRef({self.__forward_arg__!r}{module_repr})' +def _evaluate_forward_ref(forward_ref, globalns, localns, type_params=_sentinel, *, recursive_guard): + if type_params is _sentinel: + _deprecation_warning_for_no_type_params_passed("typing._evaluate_forward_ref") + type_params = () + if forward_ref.__forward_arg__ in recursive_guard: + return forward_ref + if globalns is None and localns is None: + globalns = localns = {} + elif globalns is None: + globalns = localns + elif localns is None: + localns = globalns + if forward_ref.__forward_module__ is not None: + globalns = getattr( + sys.modules.get(forward_ref.__forward_module__, None), '__dict__', globalns + ) + if type_params: + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + locals_to_pass = {param.__name__: param for param in type_params} | localns + else: + locals_to_pass = localns + type_ = _type_check( + eval(forward_ref.__forward_code__, globalns, locals_to_pass), + "Forward references must evaluate to types.", + is_argument=forward_ref.__forward_is_argument__, + allow_special_forms=forward_ref.__forward_is_class__, + ) + return _eval_type( + type_, + globalns, + localns, + type_params, + recursive_guard=(recursive_guard | {forward_ref.__forward_arg__}), + ) def _is_unpacked_typevartuple(x: Any) -> bool: @@ -2429,7 +2377,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): if value is None: value = type(None) if isinstance(value, str): - value = ForwardRef(value, is_argument=False, is_class=True) + value = _make_forward_ref(value, is_argument=False, is_class=True) value = _eval_type(value, base_globals, base_locals, base.__type_params__) hints[name] = value return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()} @@ -2463,7 +2411,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): if isinstance(value, str): # class-level forward refs were handled above, this must be either # a module-level annotation or a function argument annotation - value = ForwardRef( + value = _make_forward_ref( value, is_argument=not isinstance(obj, types.ModuleType), is_class=False, From 4d767237e2682ea33275c8b9508d1bee27ff4f13 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 30 May 2024 22:14:43 -0700 Subject: [PATCH 059/105] run black on new modules --- Lib/annotations.py | 124 ++++++++++++++++++++++------------- Lib/test/test_annotations.py | 10 +-- 2 files changed, 84 insertions(+), 50 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index 4deb48ba427b67..f3f82b62e174ef 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -11,20 +11,38 @@ class Format(enum.IntEnum): FORWARDREF = 2 SOURCE = 3 + _Union = None class ForwardRef: """Internal wrapper to hold a forward reference.""" - __slots__ = ('__forward_arg__', - '__forward_evaluated__', '__forward_value__', - '__forward_is_argument__', '__forward_is_class__', - '__forward_module__', '__weakref__', - '_forward_code', '_globals', '_owner', '_cell') - - def __init__(self, arg, is_argument=True, module=None, *, is_class=False, - _globals=None, _owner=None, _cell=None): + __slots__ = ( + "__forward_arg__", + "__forward_evaluated__", + "__forward_value__", + "__forward_is_argument__", + "__forward_is_class__", + "__forward_module__", + "__weakref__", + "_forward_code", + "_globals", + "_owner", + "_cell", + ) + + def __init__( + self, + arg, + is_argument=True, + module=None, + *, + is_class=False, + _globals=None, + _owner=None, + _cell=None, + ): if not isinstance(arg, str): raise TypeError(f"Forward reference must be a string -- got {arg!r}") @@ -72,7 +90,7 @@ def evaluate(self, *, globals=None, locals=None): # "Inject" type parameters into the local namespace # (unless they are shadowed by assignments *in* the local namespace), # as a way of emulating annotation scopes when calling `eval()` - type_params = getattr(self._owner, '__type_params__', None) + type_params = getattr(self._owner, "__type_params__", None) if type_params: locals = {param.__name__: param for param in type_params} | locals value = eval(code, globals=globals, locals=locals) @@ -88,12 +106,12 @@ def __forward_code__(self): # If we do `def f(*args: *Ts)`, then we'll have `arg = '*Ts'`. # Unfortunately, this isn't a valid expression on its own, so we # do the unpacking manually. - if arg.startswith('*'): - arg_to_compile = f'({arg},)[0]' # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] + if arg.startswith("*"): + arg_to_compile = f"({arg},)[0]" # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] else: arg_to_compile = arg try: - self._forward_code = compile(arg_to_compile, '', 'eval') + self._forward_code = compile(arg_to_compile, "", "eval") except SyntaxError: raise SyntaxError(f"Forward reference must be an expression -- got {arg!r}") return self._forward_code @@ -102,10 +120,14 @@ def __eq__(self, other): if not isinstance(other, ForwardRef): return NotImplemented if self.__forward_evaluated__ and other.__forward_evaluated__: - return (self.__forward_arg__ == other.__forward_arg__ and - self.__forward_value__ == other.__forward_value__) - return (self.__forward_arg__ == other.__forward_arg__ and - self.__forward_module__ == other.__forward_module__) + return ( + self.__forward_arg__ == other.__forward_arg__ + and self.__forward_value__ == other.__forward_value__ + ) + return ( + self.__forward_arg__ == other.__forward_arg__ + and self.__forward_module__ == other.__forward_module__ + ) def __hash__(self): return hash((self.__forward_arg__, self.__forward_module__)) @@ -124,10 +146,10 @@ def __ror__(self, other): def __repr__(self): if self.__forward_module__ is None: - module_repr = '' + module_repr = "" else: - module_repr = f', module={self.__forward_module__!r}' - return f'ForwardRef({self.__forward_arg__!r}{module_repr})' + module_repr = f", module={self.__forward_module__!r}" + return f"ForwardRef({self.__forward_arg__!r}{module_repr})" class _ForwardReffer(dict): @@ -139,8 +161,9 @@ def __init__(self, namespace, globals, owner, is_class): self.is_class = is_class def __missing__(self, key): - return ForwardRef(key, _globals=self.globals, _owner=self.owner, - is_class=self.is_class) + return ForwardRef( + key, _globals=self.globals, _owner=self.owner, is_class=self.is_class + ) class Stringifier: @@ -156,6 +179,7 @@ def _convert(self, other): def _make_binop(op: ast.AST): def binop(self, other): return Stringifier(ast.BinOp(self.node, op, self._convert(other))) + return binop __add__ = _make_binop(ast.Add()) @@ -175,6 +199,7 @@ def binop(self, other): def _make_unary_op(op): def unary_op(self): return Stringifier(ast.UnaryOp(self.node, op)) + return unary_op __invert__ = _make_unary_op(ast.Invert()) @@ -193,11 +218,16 @@ def __getattr__(self, attr): return Stringifier(ast.Attribute(self.node, attr)) def __call__(self, *args, **kwargs): - return Stringifier(ast.Call( - self.node, - [self._convert(arg) for arg in args], - [ast.keyword(key, self._convert(value)) for key, value in kwargs.items()] - )) + return Stringifier( + ast.Call( + self.node, + [self._convert(arg) for arg in args], + [ + ast.keyword(key, self._convert(value)) + for key, value in kwargs.items() + ], + ) + ) def __iter__(self): return self @@ -231,19 +261,20 @@ def _call_dunder_annotate(annotate, format, owner=None): name = freevars[i] else: name = "__cell__" - fwdref = ForwardRef(name, _cell=cell, _owner=owner, _globals=annotate.__globals__, - is_class=is_class) + fwdref = ForwardRef( + name, + _cell=cell, + _owner=owner, + _globals=annotate.__globals__, + is_class=is_class, + ) new_closure.append(types.CellType(fwdref)) else: new_closure.append(cell) closure = tuple(new_closure) else: closure = None - func = types.FunctionType( - annotate.__code__, - globals, - closure=closure - ) + func = types.FunctionType(annotate.__code__, globals, closure=closure) return func(Format.VALUE) elif format == Format.SOURCE: globals = _StringifierDict() @@ -251,7 +282,7 @@ def _call_dunder_annotate(annotate, format, owner=None): annotate.__code__, globals, # TODO: also replace the closure with stringifiers - closure=annotate.__closure__ + closure=annotate.__closure__, ) annos = func(Format.VALUE) return { @@ -260,7 +291,9 @@ def _call_dunder_annotate(annotate, format, owner=None): } -def get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE): +def get_annotations( + obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE +): """Compute the annotations dict for an object. obj may be a callable, class, or module. @@ -314,25 +347,25 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Fo ann = obj.__annotations__ obj_globals = None - module_name = getattr(obj, '__module__', None) + module_name = getattr(obj, "__module__", None) if module_name: module = sys.modules.get(module_name, None) if module: - obj_globals = getattr(module, '__dict__', None) + obj_globals = getattr(module, "__dict__", None) obj_locals = dict(vars(obj)) unwrap = obj elif isinstance(obj, types.ModuleType): # module - ann = getattr(obj, '__annotations__', None) - obj_globals = getattr(obj, '__dict__') + ann = getattr(obj, "__annotations__", None) + obj_globals = getattr(obj, "__dict__") obj_locals = None unwrap = None elif callable(obj): # this includes types.Function, types.BuiltinFunctionType, # types.BuiltinMethodType, functools.partial, functools.singledispatch, # "class funclike" from Lib/test/test_inspect... on and on it goes. - ann = getattr(obj, '__annotations__', None) - obj_globals = getattr(obj, '__globals__', None) + ann = getattr(obj, "__annotations__", None) + obj_globals = getattr(obj, "__globals__", None) obj_locals = None unwrap = obj else: @@ -352,7 +385,7 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Fo if unwrap is not None: while True: - if hasattr(unwrap, '__wrapped__'): + if hasattr(unwrap, "__wrapped__"): unwrap = unwrap.__wrapped__ continue if isinstance(unwrap, functools.partial): @@ -367,7 +400,8 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Fo if locals is None: locals = obj_locals - return_value = {key: - value if not isinstance(value, str) else eval(value, globals, locals) - for key, value in ann.items() } + return_value = { + key: value if not isinstance(value, str) else eval(value, globals, locals) + for key, value in ann.items() + } return return_value diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index 0a9d61f6eeb18d..2d055cc8070a8a 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -13,7 +13,7 @@ def inner(arg: x): anno = annotations.get_annotations(inner, format=annotations.Format.FORWARDREF) fwdref = anno["arg"] self.assertIsInstance(fwdref, annotations.ForwardRef) - self.assertEqual(fwdref.__forward_arg__, 'x') + self.assertEqual(fwdref.__forward_arg__, "x") with self.assertRaises(NameError): fwdref.evaluate() @@ -42,10 +42,10 @@ def test_special_attrs(self): # Forward refs provide a different introspection API. __name__ and # __qualname__ make little sense for forward refs as they can store # complex typing expressions. - fr = annotations.ForwardRef('set[Any]') - self.assertFalse(hasattr(fr, '__name__')) - self.assertFalse(hasattr(fr, '__qualname__')) - self.assertEqual(fr.__module__, 'typing') + fr = annotations.ForwardRef("set[Any]") + self.assertFalse(hasattr(fr, "__name__")) + self.assertFalse(hasattr(fr, "__qualname__")) + self.assertEqual(fr.__module__, "typing") # Forward refs are currently unpicklable. for proto in range(pickle.HIGHEST_PROTOCOL + 1): with self.assertRaises(TypeError): From 9a7e1301cea6868338adf4158756ea26b211aeb7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 30 May 2024 22:31:47 -0700 Subject: [PATCH 060/105] Some work --- Lib/dataclasses.py | 6 ++++-- Lib/test/test_annotations.py | 5 +++-- Lib/test/test_dataclasses/__init__.py | 6 ++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index aeafbfbbe6e9c4..a43ee59507e228 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -5,9 +5,10 @@ import inspect import keyword import itertools +import annotations import abc from reprlib import recursive_repr -from types import FunctionType, GenericAlias +from types import GenericAlias __all__ = ['dataclass', @@ -982,7 +983,8 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, # actual default value. Pseudo-fields ClassVars and InitVars are # included, despite the fact that they're not real fields. That's # dealt with later. - cls_annotations = inspect.get_annotations(cls) + cls_annotations = annotations.get_annotations(cls, + format=annotations.Format.FORWARDREF) # Now find fields in our class. While doing so, validate some # things, and set the default values (as class attributes) where diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index 2d055cc8070a8a..536f77ae3e9165 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -45,8 +45,9 @@ def test_special_attrs(self): fr = annotations.ForwardRef("set[Any]") self.assertFalse(hasattr(fr, "__name__")) self.assertFalse(hasattr(fr, "__qualname__")) - self.assertEqual(fr.__module__, "typing") - # Forward refs are currently unpicklable. + self.assertEqual(fr.__module__, "annotations") + # Forward refs are currently unpicklable once they contain a code object. + fr.__forward_code__ # fill the cache for proto in range(pickle.HIGHEST_PROTOCOL + 1): with self.assertRaises(TypeError): pickle.dumps(fr, proto) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 04dd9f3265bb33..b3db89aaabefce 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -4789,6 +4789,12 @@ def test_make_dataclass(self): self.assertTrue(fields(B)[0].kw_only) self.assertFalse(fields(B)[1].kw_only) + def test_deferred_annotations(self): + @dataclass + class A: + x: undefined + y: ClassVar[undefined] + if __name__ == '__main__': unittest.main() From ed79b40bf93d3f87db54b1ad2f0dcaebf17db46f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 30 May 2024 23:01:21 -0700 Subject: [PATCH 061/105] Support non-dict globals in LOAD_FROM_DICT_OR_GLOBALS The implementation basically copies LOAD_GLOBAL. Possibly it could be deduplicated, but that seems like it may get hairy since the two operations have different operands. This is important to fix in 3.14 for PEP 649, but it's a bug in earlier versions too, and we should backport to 3.13 and 3.12 if possible. --- Include/internal/pycore_opcode_metadata.h | 1 - Include/internal/pycore_uop_metadata.h | 4 --- Lib/test/test_type_aliases.py | 20 +++++++++++ ...-05-30-23-01-00.gh-issue-119821.jPGfvt.rst | 2 ++ Python/bytecodes.c | 35 ++++++++++++++----- Python/executor_cases.c.h | 30 +--------------- Python/generated_cases.c.h | 35 ++++++++++++++----- Python/optimizer_cases.c.h | 7 +--- 8 files changed, 76 insertions(+), 58 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-05-30-23-01-00.gh-issue-119821.jPGfvt.rst diff --git a/Include/internal/pycore_opcode_metadata.h b/Include/internal/pycore_opcode_metadata.h index d3535800139a66..0b835230974e39 100644 --- a/Include/internal/pycore_opcode_metadata.h +++ b/Include/internal/pycore_opcode_metadata.h @@ -1323,7 +1323,6 @@ _PyOpcode_macro_expansion[256] = { [LOAD_FAST_CHECK] = { .nuops = 1, .uops = { { _LOAD_FAST_CHECK, 0, 0 } } }, [LOAD_FAST_LOAD_FAST] = { .nuops = 2, .uops = { { _LOAD_FAST, 5, 0 }, { _LOAD_FAST, 6, 0 } } }, [LOAD_FROM_DICT_OR_DEREF] = { .nuops = 1, .uops = { { _LOAD_FROM_DICT_OR_DEREF, 0, 0 } } }, - [LOAD_FROM_DICT_OR_GLOBALS] = { .nuops = 1, .uops = { { _LOAD_FROM_DICT_OR_GLOBALS, 0, 0 } } }, [LOAD_GLOBAL] = { .nuops = 1, .uops = { { _LOAD_GLOBAL, 0, 0 } } }, [LOAD_GLOBAL_BUILTIN] = { .nuops = 3, .uops = { { _GUARD_GLOBALS_VERSION, 1, 1 }, { _GUARD_BUILTINS_VERSION, 1, 2 }, { _LOAD_GLOBAL_BUILTINS, 1, 3 } } }, [LOAD_GLOBAL_MODULE] = { .nuops = 2, .uops = { { _GUARD_GLOBALS_VERSION, 1, 1 }, { _LOAD_GLOBAL_MODULE, 1, 3 } } }, diff --git a/Include/internal/pycore_uop_metadata.h b/Include/internal/pycore_uop_metadata.h index 78f0eafaa32042..690ae34a6eef98 100644 --- a/Include/internal/pycore_uop_metadata.h +++ b/Include/internal/pycore_uop_metadata.h @@ -107,7 +107,6 @@ const uint16_t _PyUop_Flags[MAX_UOP_ID+1] = { [_STORE_GLOBAL] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_DELETE_GLOBAL] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_LOAD_LOCALS] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_LOAD_FROM_DICT_OR_GLOBALS] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_LOAD_GLOBAL] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_GUARD_GLOBALS_VERSION] = HAS_DEOPT_FLAG, [_GUARD_BUILTINS_VERSION] = HAS_DEOPT_FLAG, @@ -439,7 +438,6 @@ const char *const _PyOpcode_uop_name[MAX_UOP_ID+1] = { [_LOAD_FAST_CHECK] = "_LOAD_FAST_CHECK", [_LOAD_FAST_LOAD_FAST] = "_LOAD_FAST_LOAD_FAST", [_LOAD_FROM_DICT_OR_DEREF] = "_LOAD_FROM_DICT_OR_DEREF", - [_LOAD_FROM_DICT_OR_GLOBALS] = "_LOAD_FROM_DICT_OR_GLOBALS", [_LOAD_GLOBAL] = "_LOAD_GLOBAL", [_LOAD_GLOBAL_BUILTINS] = "_LOAD_GLOBAL_BUILTINS", [_LOAD_GLOBAL_MODULE] = "_LOAD_GLOBAL_MODULE", @@ -692,8 +690,6 @@ int _PyUop_num_popped(int opcode, int oparg) return 0; case _LOAD_LOCALS: return 0; - case _LOAD_FROM_DICT_OR_GLOBALS: - return 1; case _LOAD_GLOBAL: return 0; case _GUARD_GLOBALS_VERSION: diff --git a/Lib/test/test_type_aliases.py b/Lib/test/test_type_aliases.py index 9c325bc595f585..f8b395fdc8bb1d 100644 --- a/Lib/test/test_type_aliases.py +++ b/Lib/test/test_type_aliases.py @@ -1,4 +1,5 @@ import pickle +import textwrap import types import unittest from test.support import check_syntax_error, run_code @@ -328,3 +329,22 @@ def test_pickling_local(self): with self.subTest(thing=thing, proto=proto): with self.assertRaises(pickle.PickleError): pickle.dumps(thing, protocol=proto) + + +class TypeParamsExoticGlobalsTest(unittest.TestCase): + def test_exec_with_unusual_globals(self): + class customdict(dict): + def __missing__(self, key): + return key + + code = compile("type Alias = undefined", "test", "exec") + ns = customdict() + exec(code, ns) + Alias = ns["Alias"] + self.assertEqual(Alias.__value__, "undefined") + + code = compile("class A: type Alias = undefined", "test", "exec") + ns = customdict() + exec(code, ns) + Alias = ns["A"].Alias + self.assertEqual(Alias.__value__, "undefined") diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-05-30-23-01-00.gh-issue-119821.jPGfvt.rst b/Misc/NEWS.d/next/Core and Builtins/2024-05-30-23-01-00.gh-issue-119821.jPGfvt.rst new file mode 100644 index 00000000000000..cc25eee6dd6ae4 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-05-30-23-01-00.gh-issue-119821.jPGfvt.rst @@ -0,0 +1,2 @@ +Fix execution of :ref:`annotation scopes ` within classes +when ``globals`` is set to a non-dict. Patch by Jelle Zijlstra. diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 9a8198515dea5e..1c12e1cddbbc10 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -1385,18 +1385,35 @@ dummy_func( ERROR_NO_POP(); } if (v == NULL) { - if (PyDict_GetItemRef(GLOBALS(), name, &v) < 0) { - ERROR_NO_POP(); - } - if (v == NULL) { - if (PyMapping_GetOptionalItem(BUILTINS(), name, &v) < 0) { + if (PyDict_CheckExact(GLOBALS()) + && PyDict_CheckExact(BUILTINS())) + { + v = _PyDict_LoadGlobal((PyDictObject *)GLOBALS(), + (PyDictObject *)BUILTINS(), + name); + if (v == NULL) { + if (!_PyErr_Occurred(tstate)) { + /* _PyDict_LoadGlobal() returns NULL without raising + * an exception if the key doesn't exist */ + _PyEval_FormatExcCheckArg(tstate, PyExc_NameError, + NAME_ERROR_MSG, name); + } ERROR_NO_POP(); } + } + else { + /* Slow-path if globals or builtins is not a dict */ + /* namespace 1: globals */ + ERROR_IF(PyMapping_GetOptionalItem(GLOBALS(), name, &v) < 0, error); if (v == NULL) { - _PyEval_FormatExcCheckArg( - tstate, PyExc_NameError, - NAME_ERROR_MSG, name); - ERROR_NO_POP(); + /* namespace 2: builtins */ + ERROR_IF(PyMapping_GetOptionalItem(BUILTINS(), name, &v) < 0, error); + if (v == NULL) { + _PyEval_FormatExcCheckArg( + tstate, PyExc_NameError, + NAME_ERROR_MSG, name); + ERROR_IF(true, error); + } } } } diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index e862364cb23e7a..0dfe490cb37047 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -1405,35 +1405,7 @@ break; } - case _LOAD_FROM_DICT_OR_GLOBALS: { - PyObject *mod_or_class_dict; - PyObject *v; - oparg = CURRENT_OPARG(); - mod_or_class_dict = stack_pointer[-1]; - PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); - if (PyMapping_GetOptionalItem(mod_or_class_dict, name, &v) < 0) { - JUMP_TO_ERROR(); - } - if (v == NULL) { - if (PyDict_GetItemRef(GLOBALS(), name, &v) < 0) { - JUMP_TO_ERROR(); - } - if (v == NULL) { - if (PyMapping_GetOptionalItem(BUILTINS(), name, &v) < 0) { - JUMP_TO_ERROR(); - } - if (v == NULL) { - _PyEval_FormatExcCheckArg( - tstate, PyExc_NameError, - NAME_ERROR_MSG, name); - JUMP_TO_ERROR(); - } - } - } - Py_DECREF(mod_or_class_dict); - stack_pointer[-1] = v; - break; - } + /* _LOAD_FROM_DICT_OR_GLOBALS is not a viable micro-op for tier 2 because it has both popping and not-popping errors */ /* _LOAD_NAME is not a viable micro-op for tier 2 because it has both popping and not-popping errors */ diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 4402787d96f12e..1a991608385405 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -4401,18 +4401,35 @@ goto error; } if (v == NULL) { - if (PyDict_GetItemRef(GLOBALS(), name, &v) < 0) { - goto error; - } - if (v == NULL) { - if (PyMapping_GetOptionalItem(BUILTINS(), name, &v) < 0) { + if (PyDict_CheckExact(GLOBALS()) + && PyDict_CheckExact(BUILTINS())) + { + v = _PyDict_LoadGlobal((PyDictObject *)GLOBALS(), + (PyDictObject *)BUILTINS(), + name); + if (v == NULL) { + if (!_PyErr_Occurred(tstate)) { + /* _PyDict_LoadGlobal() returns NULL without raising + * an exception if the key doesn't exist */ + _PyEval_FormatExcCheckArg(tstate, PyExc_NameError, + NAME_ERROR_MSG, name); + } goto error; } + } + else { + /* Slow-path if globals or builtins is not a dict */ + /* namespace 1: globals */ + if (PyMapping_GetOptionalItem(GLOBALS(), name, &v) < 0) goto pop_1_error; if (v == NULL) { - _PyEval_FormatExcCheckArg( - tstate, PyExc_NameError, - NAME_ERROR_MSG, name); - goto error; + /* namespace 2: builtins */ + if (PyMapping_GetOptionalItem(BUILTINS(), name, &v) < 0) goto pop_1_error; + if (v == NULL) { + _PyEval_FormatExcCheckArg( + tstate, PyExc_NameError, + NAME_ERROR_MSG, name); + if (true) goto pop_1_error; + } } } } diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 1b76f1480b4f11..b3787345ec6714 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -740,12 +740,7 @@ break; } - case _LOAD_FROM_DICT_OR_GLOBALS: { - _Py_UopsSymbol *v; - v = sym_new_not_null(ctx); - stack_pointer[-1] = v; - break; - } + /* _LOAD_FROM_DICT_OR_GLOBALS is not a viable micro-op for tier 2 */ /* _LOAD_NAME is not a viable micro-op for tier 2 */ From 3e65032583aa0256e554b4f0b0b29acd8de9d6d4 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 07:29:38 -0700 Subject: [PATCH 062/105] Work on functools --- Lib/functools.py | 12 ++++++-- Lib/test/test_functools.py | 21 ++++++++++++++ Lib/typing.py | 57 ++++++++++++++++++++++++-------------- 3 files changed, 66 insertions(+), 24 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index a80e1a6c6a56ac..164ea2f1c618df 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -878,8 +878,8 @@ def register(cls, func=None): f"Invalid first argument to `register()`. " f"{cls!r} is not a class or union type." ) - ann = getattr(cls, '__annotations__', {}) - if not ann: + ann = getattr(cls, '__annotate__', None) + if ann is None: raise TypeError( f"Invalid first argument to `register()`: {cls!r}. " f"Use either `@register(some_class)` or plain `@register` " @@ -889,13 +889,19 @@ def register(cls, func=None): # only import typing if annotation parsing is necessary from typing import get_type_hints - argname, cls = next(iter(get_type_hints(func).items())) + from annotations import Format, ForwardRef + argname, cls = next(iter(get_type_hints(func, format=Format.FORWARDREF).items())) if not _is_valid_dispatch_type(cls): if _is_union_type(cls): raise TypeError( f"Invalid annotation for {argname!r}. " f"{cls!r} not all arguments are classes." ) + elif isinstance(cls, ForwardRef): + raise TypeError( + f"Invalid annotation for {argname!r}. " + f"{cls!r} is an unresolved forward reference." + ) else: raise TypeError( f"Invalid annotation for {argname!r}. " diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 26701ea8b4daf9..5ddd32d5918631 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3036,6 +3036,27 @@ def _(arg: typing.List[float] | bytes): self.assertEqual(f(""), "default") self.assertEqual(f(b""), "default") + def test_forward_reference(self): + @functools.singledispatch + def f(arg, arg2=None): + return "default" + + @f.register + def _(arg: str, arg2: undefined = None): + return "forward reference" + + self.assertEqual(f(1), "default") + self.assertEqual(f(""), "forward reference") + + def test_unresolved_forward_reference(self): + @functools.singledispatch + def f(arg): + return "default" + + with self.assertRaisesRegex(TypeError, "is an unresolved forward reference"): + @f.register + def _(arg: undefined): + return "forward reference" class CachedCostItem: _cost = 1 diff --git a/Lib/typing.py b/Lib/typing.py index 4e093f0d9982bc..828b8f22fb38b9 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -19,6 +19,7 @@ """ from abc import abstractmethod, ABCMeta +import annotations from annotations import ForwardRef import collections from collections import defaultdict @@ -461,7 +462,8 @@ def __repr__(self): _sentinel = _Sentinel() -def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=frozenset()): +def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=frozenset(), + format=annotations.Format.VALUE): """Evaluate all forward references in the given type t. For use of globalns and localns see the docstring for get_type_hints(). @@ -472,7 +474,8 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f _deprecation_warning_for_no_type_params_passed("typing._eval_type") type_params = () if isinstance(t, ForwardRef): - return _evaluate_forward_ref(t, globalns, localns, type_params, recursive_guard=recursive_guard) + return _evaluate_forward_ref(t, globalns, localns, type_params, + recursive_guard=recursive_guard, format=format) if isinstance(t, (_GenericAlias, GenericAlias, types.UnionType)): if isinstance(t, GenericAlias): args = tuple( @@ -489,7 +492,8 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f ev_args = tuple( _eval_type( - a, globalns, localns, type_params, recursive_guard=recursive_guard + a, globalns, localns, type_params, recursive_guard=recursive_guard, + format=format ) for a in t.__args__ ) @@ -1020,7 +1024,8 @@ def _make_forward_ref(code, **kwargs): return forward_ref -def _evaluate_forward_ref(forward_ref, globalns, localns, type_params=_sentinel, *, recursive_guard): +def _evaluate_forward_ref(forward_ref, globalns, localns, type_params=_sentinel, *, + recursive_guard, format=annotations.Format.VALUE): if type_params is _sentinel: _deprecation_warning_for_no_type_params_passed("typing._evaluate_forward_ref") type_params = () @@ -1043,8 +1048,15 @@ def _evaluate_forward_ref(forward_ref, globalns, localns, type_params=_sentinel, locals_to_pass = {param.__name__: param for param in type_params} | localns else: locals_to_pass = localns + try: + value = eval(forward_ref.__forward_code__, globalns, locals_to_pass) + except NameError: + if format is annotations.Format.FORWARDREF: + return forward_ref + else: + raise type_ = _type_check( - eval(forward_ref.__forward_code__, globalns, locals_to_pass), + value, "Forward references must evaluate to types.", is_argument=forward_ref.__forward_is_argument__, allow_special_forms=forward_ref.__forward_is_class__, @@ -1055,6 +1067,7 @@ def _evaluate_forward_ref(forward_ref, globalns, localns, type_params=_sentinel, localns, type_params, recursive_guard=(recursive_guard | {forward_ref.__forward_arg__}), + format=format, ) @@ -2320,7 +2333,8 @@ def greet(name: str) -> None: WrapperDescriptorType, MethodWrapperType, MethodDescriptorType) -def get_type_hints(obj, globalns=None, localns=None, include_extras=False): +def get_type_hints(obj, globalns=None, localns=None, include_extras=False, + *, format=annotations.Format.VALUE): """Return type hints for an object. This is often the same as obj.__annotations__, but it handles @@ -2357,13 +2371,14 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): if isinstance(obj, type): hints = {} for base in reversed(obj.__mro__): + ann = annotations.get_annotations(base, format=format) + if format is annotations.Format.SOURCE: + hints.update(ann) + continue if globalns is None: base_globals = getattr(sys.modules.get(base.__module__, None), '__dict__', {}) else: base_globals = globalns - ann = getattr(base, '__annotations__', {}) - if isinstance(ann, types.GetSetDescriptorType): - ann = {} base_locals = dict(vars(base)) if localns is None else localns if localns is None and globalns is None: # This is surprising, but required. Before Python 3.10, @@ -2378,9 +2393,18 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): value = type(None) if isinstance(value, str): value = _make_forward_ref(value, is_argument=False, is_class=True) - value = _eval_type(value, base_globals, base_locals, base.__type_params__) + value = _eval_type(value, base_globals, base_locals, base.__type_params__, + format=format) hints[name] = value - return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()} + if include_extras or format is annotations.Format.SOURCE: + return hints + else: + return {k: _strip_annotations(t) for k, t in hints.items()} + + hints = annotations.get_annotations(obj, format=format) + hints = dict(hints) + if format is annotations.Format.SOURCE: + return hints if globalns is None: if isinstance(obj, types.ModuleType): @@ -2395,15 +2419,6 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): localns = globalns elif localns is None: localns = globalns - hints = getattr(obj, '__annotations__', None) - if hints is None: - # Return empty annotations for something that _could_ have them. - if isinstance(obj, _allowed_types): - return {} - else: - raise TypeError('{!r} is not a module, class, method, ' - 'or function.'.format(obj)) - hints = dict(hints) type_params = getattr(obj, "__type_params__", ()) for name, value in hints.items(): if value is None: @@ -2416,7 +2431,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): is_argument=not isinstance(obj, types.ModuleType), is_class=False, ) - hints[name] = _eval_type(value, globalns, localns, type_params) + hints[name] = _eval_type(value, globalns, localns, type_params, format=format) return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()} From f0d430ff4c2760cf53463a0d92ac94abb32e6aba Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 08:24:22 -0700 Subject: [PATCH 063/105] gh-119180: Lazily wrap annotations on classmethod and staticmethod --- Lib/test/test_descr.py | 38 ++++++- ...-05-31-08-23-41.gh-issue-119180.KL4VxZ.rst | 3 + Objects/funcobject.c | 100 +++++++++++++++++- 3 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-05-31-08-23-41.gh-issue-119180.KL4VxZ.rst diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index c3f292467a6738..7742f075285602 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -1593,8 +1593,7 @@ def f(cls, arg): self.fail("classmethod shouldn't accept keyword args") cm = classmethod(f) - cm_dict = {'__annotations__': {}, - '__doc__': ( + cm_dict = {'__doc__': ( "f docstring" if support.HAVE_DOCSTRINGS else None @@ -1610,6 +1609,41 @@ def f(cls, arg): del cm.x self.assertNotHasAttr(cm, "x") + def test_classmethod_staticmethod_annotations(self): + for deco in (classmethod, staticmethod): + @deco + def unannotated(cls): pass + @deco + def annotated(cls) -> int: pass + + for method in (annotated, unannotated): + with self.subTest(deco=deco, method=method): + original_annotations = dict(method.__wrapped__.__annotations__) + self.assertNotIn('__annotations__', method.__dict__) + self.assertEqual(method.__annotations__, original_annotations) + self.assertIn('__annotations__', method.__dict__) + + new_annotations = {"a": "b"} + method.__annotations__ = new_annotations + self.assertEqual(method.__annotations__, new_annotations) + self.assertEqual(method.__wrapped__.__annotations__, original_annotations) + + del method.__annotations__ + self.assertEqual(method.__annotations__, original_annotations) + + original_annotate = method.__wrapped__.__annotate__ + self.assertNotIn('__annotate__', method.__dict__) + self.assertIs(method.__annotate__, original_annotate) + self.assertIn('__annotate__', method.__dict__) + + new_annotate = lambda: {"annotations": 1} + method.__annotate__ = new_annotate + self.assertIs(method.__annotate__, new_annotate) + self.assertIs(method.__wrapped__.__annotate__, original_annotate) + + del method.__annotate__ + self.assertIs(method.__annotate__, original_annotate) + @support.refcount_test def test_refleaks_in_classmethod___init__(self): gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount') diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-05-31-08-23-41.gh-issue-119180.KL4VxZ.rst b/Misc/NEWS.d/next/Core and Builtins/2024-05-31-08-23-41.gh-issue-119180.KL4VxZ.rst new file mode 100644 index 00000000000000..1e5ad7d08eed7c --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-05-31-08-23-41.gh-issue-119180.KL4VxZ.rst @@ -0,0 +1,3 @@ +:func:`classmethod` and :func:`staticmethod` now wrap the +:attr:`__annotations__` and :attr:`!__annotate__` attributes of their +underlying callable lazily. See :pep:`649`. Patch by Jelle Zijlstra. diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 4e78252052932c..40211297be20c0 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -1172,12 +1172,57 @@ functools_wraps(PyObject *wrapper, PyObject *wrapped) COPY_ATTR(__name__); COPY_ATTR(__qualname__); COPY_ATTR(__doc__); - COPY_ATTR(__annotations__); return 0; #undef COPY_ATTR } +// Used for wrapping __annotations__ and __annotate__ on classmethod +// and staticmethod objects. +static PyObject * +descriptor_get_wrapped_attribute(PyObject *wrapped, PyObject *dict, PyObject *name) +{ + PyObject *res; + if (PyDict_GetItemRef(dict, name, &res) < 0) { + return NULL; + } + if (res != NULL) { + return res; + } + res = PyObject_GetAttr(wrapped, name); + if (res == NULL) { + return NULL; + } + if (PyDict_SetItem(dict, name, res) < 0) { + Py_DECREF(res); + return NULL; + } + return res; +} + +static int +descriptor_set_wrapped_attribute(PyObject *dict, PyObject *name, PyObject *value, + char *type_name) +{ + if (value == NULL) { + if (PyDict_DelItem(dict, name) < 0) { + if (PyErr_ExceptionMatches(PyExc_KeyError)) { + PyErr_Clear(); + PyErr_Format(PyExc_AttributeError, + "'%.200s' object has no attribute '%U'", + type_name, name); + } + else { + return -1; + } + } + return 0; + } + else { + return PyDict_SetItem(dict, name, value); + } +} + /* Class method object */ @@ -1283,10 +1328,37 @@ cm_get___isabstractmethod__(classmethod *cm, void *closure) Py_RETURN_FALSE; } +static PyObject * +cm_get___annotations__(classmethod *cm, void *closure) +{ + return descriptor_get_wrapped_attribute(cm->cm_callable, cm->cm_dict, &_Py_ID(__annotations__)); +} + +static int +cm_set___annotations__(classmethod *cm, PyObject *value, void *closure) +{ + return descriptor_set_wrapped_attribute(cm->cm_dict, &_Py_ID(__annotations__), value, "classmethod"); +} + +static PyObject * +cm_get___annotate__(classmethod *cm, void *closure) +{ + return descriptor_get_wrapped_attribute(cm->cm_callable, cm->cm_dict, &_Py_ID(__annotate__)); +} + +static int +cm_set___annotate__(classmethod *cm, PyObject *value, void *closure) +{ + return descriptor_set_wrapped_attribute(cm->cm_dict, &_Py_ID(__annotate__), value, "classmethod"); +} + + static PyGetSetDef cm_getsetlist[] = { {"__isabstractmethod__", (getter)cm_get___isabstractmethod__, NULL, NULL, NULL}, {"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict, NULL, NULL}, + {"__annotations__", (getter)cm_get___annotations__, (setter)cm_set___annotations__, NULL, NULL}, + {"__annotate__", (getter)cm_get___annotate__, (setter)cm_set___annotate__, NULL, NULL}, {NULL} /* Sentinel */ }; @@ -1479,10 +1551,36 @@ sm_get___isabstractmethod__(staticmethod *sm, void *closure) Py_RETURN_FALSE; } +static PyObject * +sm_get___annotations__(staticmethod *sm, void *closure) +{ + return descriptor_get_wrapped_attribute(sm->sm_callable, sm->sm_dict, &_Py_ID(__annotations__)); +} + +static int +sm_set___annotations__(staticmethod *sm, PyObject *value, void *closure) +{ + return descriptor_set_wrapped_attribute(sm->sm_dict, &_Py_ID(__annotations__), value, "staticmethod"); +} + +static PyObject * +sm_get___annotate__(staticmethod *sm, void *closure) +{ + return descriptor_get_wrapped_attribute(sm->sm_callable, sm->sm_dict, &_Py_ID(__annotate__)); +} + +static int +sm_set___annotate__(staticmethod *sm, PyObject *value, void *closure) +{ + return descriptor_set_wrapped_attribute(sm->sm_dict, &_Py_ID(__annotate__), value, "staticmethod"); +} + static PyGetSetDef sm_getsetlist[] = { {"__isabstractmethod__", (getter)sm_get___isabstractmethod__, NULL, NULL, NULL}, {"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict, NULL, NULL}, + {"__annotations__", (getter)sm_get___annotations__, (setter)sm_set___annotations__, NULL, NULL}, + {"__annotate__", (getter)sm_get___annotate__, (setter)sm_set___annotate__, NULL, NULL}, {NULL} /* Sentinel */ }; From c11cc6d6f981a5c0ddfa8f537b61741e272fd1fd Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 08:35:10 -0700 Subject: [PATCH 064/105] Use the new annotations module --- Lib/annotations.py | 1 + Lib/inspect.py | 232 +------------------------- Lib/test/test_annotations.py | 12 ++ Lib/test/test_grammar.py | 3 +- Lib/test/test_inspect/test_inspect.py | 7 +- Lib/test/test_type_annotations.py | 8 +- Lib/typing.py | 5 +- 7 files changed, 23 insertions(+), 245 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index f3f82b62e174ef..12cb4fcf194f55 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -2,6 +2,7 @@ import ast import enum +import functools import sys import types diff --git a/Lib/inspect.py b/Lib/inspect.py index 0cb4f1bdf66967..e05dc12ed1679b 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -38,7 +38,6 @@ "AGEN_CREATED", "AGEN_RUNNING", "AGEN_SUSPENDED", - "AnnotationsFormat", "ArgInfo", "Arguments", "Attribute", @@ -62,7 +61,6 @@ "ClassFoundException", "ClosureVars", "EndOfBlock", - "FORWARDREF", "FrameInfo", "FullArgSpec", "GEN_CLOSED", @@ -136,16 +134,15 @@ "istraceback", "markcoroutinefunction", "signature", - "SOURCE", "stack", "trace", "unwrap", - "VALUE", "walktree", ] import abc +from annotations import get_annotations import ast import dis import collections.abc @@ -177,233 +174,6 @@ TPFLAGS_IS_ABSTRACT = 1 << 20 -@enum.global_enum -class AnnotationsFormat(enum.IntEnum): - VALUE = 1 - FORWARDREF = 2 - SOURCE = 3 - - -class _ForwardRef: - def __init__(self, name): - self.name = name - - -class _ForwardReffer(dict): - def __missing__(self, key): - return _ForwardRef(key) - - -class Stringifier: - def __init__(self, node): - self.node = node - - def _convert(self, other): - if isinstance(other, Stringifier): - return other.node - else: - return ast.Name(id=repr(other)) - - def _make_binop(op): - def binop(self, other): - return Stringifier(ast.BinOp(self.node, op, self._convert(other))) - return binop - - __add__ = _make_binop(ast.Add()) - __sub__ = _make_binop(ast.Sub()) - __mul__ = _make_binop(ast.Mult()) - __matmul__ = _make_binop(ast.MatMult()) - __div__ = _make_binop(ast.Div()) - __mod__ = _make_binop(ast.Mod()) - __lshift__ = _make_binop(ast.LShift()) - __rshift__ = _make_binop(ast.RShift()) - __or__ = _make_binop(ast.BitOr()) - __xor__ = _make_binop(ast.BitXor()) - __and__ = _make_binop(ast.And()) - __floordiv__ = _make_binop(ast.FloorDiv()) - __pow__ = _make_binop(ast.Pow()) - - def _make_unary_op(op): - def unary_op(self): - return Stringifier(ast.UnaryOp(self.node, op)) - return unary_op - - __invert__ = _make_unary_op(ast.Invert()) - __pos__ = _make_binop(ast.UAdd()) - __neg__ = _make_binop(ast.USub()) - - def __getitem__(self, other): - if isinstance(other, tuple): - elts = [self._convert(elt) for elt in other] - other = ast.Tuple(elts) - else: - other = self._convert(other) - return Stringifier(ast.Subscript(self.node, other)) - - def __getattr__(self, attr): - return Stringifier(ast.Attribute(self.node, attr)) - - def __call__(self, *args, **kwargs): - return Stringifier(ast.Call( - self.node, - [self._convert(arg) for arg in args], - [ast.keyword(key, self._convert(value)) for key, value in kwargs.items()] - )) - - def __iter__(self): - return self - - def __next__(self): - return Stringifier(ast.Starred(self.node)) - - -class _StringifierDict(dict): - def __missing__(self, key): - return Stringifier(ast.Name(key)) - - -def _call_dunder_annotate(annotate, format): - try: - return annotate(format) - except NotImplementedError: - pass - if format == FORWARDREF: - globals = {**annotate.__builtins__, **annotate.__globals__} - globals = _ForwardReffer(globals) - func = types.FunctionType( - annotate.__code__, - globals, - closure=annotate.__closure__ - ) - return func(VALUE) - elif format == SOURCE: - globals = _StringifierDict() - func = types.FunctionType( - annotate.__code__, - globals, - # TODO: also replace the closure with stringifiers - closure=annotate.__closure__ - ) - annos = func(VALUE) - return { - key: ast.unparse(val.node) if isinstance(val, Stringifier) else repr(val) - for key, val in annos.items() - } - - -def get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=VALUE): - """Compute the annotations dict for an object. - - obj may be a callable, class, or module. - Passing in an object of any other type raises TypeError. - - Returns a dict. get_annotations() returns a new dict every time - it's called; calling it twice on the same object will return two - different but equivalent dicts. - - This function handles several details for you: - - * If eval_str is true, values of type str will - be un-stringized using eval(). This is intended - for use with stringized annotations - ("from __future__ import annotations"). - * If obj doesn't have an annotations dict, returns an - empty dict. (Functions and methods always have an - annotations dict; classes, modules, and other types of - callables may not.) - * Ignores inherited annotations on classes. If a class - doesn't have its own annotations dict, returns an empty dict. - * All accesses to object members and dict values are done - using getattr() and dict.get() for safety. - * Always, always, always returns a freshly-created dict. - - eval_str controls whether or not values of type str are replaced - with the result of calling eval() on those values: - - * If eval_str is true, eval() is called on values of type str. - * If eval_str is false (the default), values of type str are unchanged. - - globals and locals are passed in to eval(); see the documentation - for eval() for more information. If either globals or locals is - None, this function may replace that value with a context-specific - default, contingent on type(obj): - - * If obj is a module, globals defaults to obj.__dict__. - * If obj is a class, globals defaults to - sys.modules[obj.__module__].__dict__ and locals - defaults to the obj class namespace. - * If obj is a callable, globals defaults to obj.__globals__, - although if obj is a wrapped function (using - functools.update_wrapper()) it is first unwrapped. - """ - annotate = getattr(obj, "__annotate__", None) - # TODO remove format != VALUE condition - if annotate is not None and format != VALUE: - ann = _call_dunder_annotate(annotate, format) - elif isinstance(obj, type): - # class - ann = obj.__annotations__ - - obj_globals = None - module_name = getattr(obj, '__module__', None) - if module_name: - module = sys.modules.get(module_name, None) - if module: - obj_globals = getattr(module, '__dict__', None) - obj_locals = dict(vars(obj)) - unwrap = obj - elif isinstance(obj, types.ModuleType): - # module - ann = getattr(obj, '__annotations__', None) - obj_globals = getattr(obj, '__dict__') - obj_locals = None - unwrap = None - elif callable(obj): - # this includes types.Function, types.BuiltinFunctionType, - # types.BuiltinMethodType, functools.partial, functools.singledispatch, - # "class funclike" from Lib/test/test_inspect... on and on it goes. - ann = getattr(obj, '__annotations__', None) - obj_globals = getattr(obj, '__globals__', None) - obj_locals = None - unwrap = obj - else: - raise TypeError(f"{obj!r} is not a module, class, or callable.") - - if ann is None: - return {} - - if not isinstance(ann, dict): - raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") - - if not ann: - return {} - - if not eval_str: - return dict(ann) - - if unwrap is not None: - while True: - if hasattr(unwrap, '__wrapped__'): - unwrap = unwrap.__wrapped__ - continue - if isinstance(unwrap, functools.partial): - unwrap = unwrap.func - continue - break - if hasattr(unwrap, "__globals__"): - obj_globals = unwrap.__globals__ - - if globals is None: - globals = obj_globals - if locals is None: - locals = obj_locals - - return_value = {key: - value if not isinstance(value, str) else eval(value, globals, locals) - for key, value in ann.items() } - return return_value - - # ----------------------------------------------------------- type-checking def ismodule(object): """Return true if the object is a module.""" diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index 536f77ae3e9165..109a881840422c 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -5,6 +5,18 @@ import unittest +class TestFormat(unittest.TestCase): + def test_enum(self): + self.assertEqual(annotations.Format.VALUE.value, 1) + self.assertEqual(annotations.Format.VALUE, 1) + + self.assertEqual(annotations.Format.FORWARDREF.value, 2) + self.assertEqual(annotations.Format.FORWARDREF, 2) + + self.assertEqual(annotations.Format.SOURCE.value, 3) + self.assertEqual(annotations.Format.SOURCE, 3) + + class TestForwardRefFormat(unittest.TestCase): def test_closure(self): def inner(arg: x): diff --git a/Lib/test/test_grammar.py b/Lib/test/test_grammar.py index aa10efb892b953..09b989a5a4f7f5 100644 --- a/Lib/test/test_grammar.py +++ b/Lib/test/test_grammar.py @@ -3,6 +3,7 @@ from test.support import check_syntax_error from test.support import import_helper +import annotations import inspect import unittest import sys @@ -459,7 +460,7 @@ def test_var_annot_simple_exec(self): gns = {}; lns = {} exec("'docstring'\n" "x: int = 5\n", gns, lns) - self.assertEqual(lns["__annotate__"](inspect.VALUE), {'x': int}) + self.assertEqual(lns["__annotate__"](annotations.Format.VALUE), {'x': int}) with self.assertRaises(KeyError): gns['__annotate__'] diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 546c9fea390abd..e6d5ebb71a306a 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -125,7 +125,7 @@ def istest(self, predicate, exp): self.assertFalse(other(obj), 'not %s(%s)' % (other.__name__, exp)) def test__all__(self): - support.check__all__(self, inspect, not_exported=("modulesbyfile",)) + support.check__all__(self, inspect, not_exported=("modulesbyfile",), extra=("get_annotations",)) def generator_function_example(self): for i in range(2): @@ -1592,11 +1592,6 @@ class C(metaclass=M): attrs = [a[0] for a in inspect.getmembers(C)] self.assertNotIn('missing', attrs) - def test_annotation_format(self): - self.assertIs(inspect.VALUE, inspect.AnnotationsFormat.VALUE) - self.assertEqual(inspect.VALUE.value, 1) - self.assertEqual(inspect.VALUE, 1) - def test_get_annotations_with_stock_annotations(self): def foo(a:int, b:str): pass self.assertEqual(inspect.get_annotations(foo), {'a': int, 'b': str}) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 25dc4d883eb356..8db70de09627da 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -1,4 +1,4 @@ -import inspect +import annotations import textwrap import types import unittest @@ -373,12 +373,12 @@ class X: self.assertIsInstance(annotate, types.FunctionType) self.assertEqual(annotate.__name__, "__annotate__") with self.assertRaises(NotImplementedError): - annotate(inspect.FORWARDREF) + annotate(annotations.Format.FORWARDREF) with self.assertRaises(NotImplementedError): - annotate(inspect.SOURCE) + annotate(annotations.Format.SOURCE) with self.assertRaises(NotImplementedError): annotate(None) - self.assertEqual(annotate(inspect.VALUE), {"x": int}) + self.assertEqual(annotate(annotations.Format.VALUE), {"x": int}) def test_comprehension_in_annotation(self): # This crashed in an earlier version of the code diff --git a/Lib/typing.py b/Lib/typing.py index 828b8f22fb38b9..291161457d3fad 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -26,7 +26,6 @@ import collections.abc import copyreg import functools -import inspect import operator import sys import types @@ -2937,7 +2936,7 @@ def __new__(cls, typename, bases, ns): if "__annotations__" in ns: types = ns["__annotations__"] elif "__annotate__" in ns: - types = ns["__annotate__"](inspect.VALUE) + types = ns["__annotate__"](annotations.Format.VALUE) else: types = {} default_names = [] @@ -3103,7 +3102,7 @@ def __new__(cls, name, bases, ns, total=True): if "__annotations__" in ns: own_annotations = ns["__annotations__"] elif "__annotate__" in ns: - own_annotations = ns["__annotate__"](inspect.VALUE) + own_annotations = ns["__annotate__"](annotations.Format.VALUE) else: own_annotations = {} msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" From 672511a6d7b7e565c7cca864a5b330f9ebdeff83 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 17:32:14 -0700 Subject: [PATCH 065/105] update_wrapper() updates __annotate__, not __annotations__ --- Lib/functools.py | 2 +- Lib/test/test_functools.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Lib/functools.py b/Lib/functools.py index 164ea2f1c618df..52d807982e2b8e 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -31,7 +31,7 @@ # wrapper functions that can handle naive introspection WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__', - '__annotations__', '__type_params__') + '__annotate__', '__type_params__') WRAPPER_UPDATES = ('__dict__',) def update_wrapper(wrapper, wrapped, diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 5ddd32d5918631..c2b85114757211 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -718,6 +718,24 @@ def wrapper(*args): pass self.assertEqual(wrapper.__annotations__, {}) self.assertEqual(wrapper.__type_params__, ()) + def test_update_wrapper_annotations(self): + def inner(x: int): pass + def wrapper(*args): pass + + functools.update_wrapper(wrapper, inner) + self.assertEqual(wrapper.__annotations__, {'x': int}) + self.assertIs(wrapper.__annotate__, inner.__annotate__) + + def with_forward_ref(x: undefined): pass + def wrapper(*args): pass + + functools.update_wrapper(wrapper, with_forward_ref) + + self.assertIs(wrapper.__annotate__, with_forward_ref.__annotate__) + + undefined = str + self.assertEqual(wrapper.__annotations__, {'x': undefined}) + class TestWraps(TestUpdateWrapper): From 0f321c6d1a1f919f77efcb3a35a66e6b490a62d2 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 17:34:21 -0700 Subject: [PATCH 066/105] Expand test --- Lib/test/test_functools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index c2b85114757211..6448f892fed80d 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -732,6 +732,8 @@ def wrapper(*args): pass functools.update_wrapper(wrapper, with_forward_ref) self.assertIs(wrapper.__annotate__, with_forward_ref.__annotate__) + with self.assertRaises(NameError): + wrapper.__annotations__ undefined = str self.assertEqual(wrapper.__annotations__, {'x': undefined}) From c64aa73d3d5654f31345cd97b58d18b22c5c1167 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 17:44:15 -0700 Subject: [PATCH 067/105] new stdlib module name --- Python/stdlib_module_names.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h index 9686d10563aa4d..db178c7ebc075c 100644 --- a/Python/stdlib_module_names.h +++ b/Python/stdlib_module_names.h @@ -99,6 +99,7 @@ static const char* _Py_stdlib_module_names[] = { "_winapi", "_zoneinfo", "abc", +"annotations", "antigravity", "argparse", "array", From 44d890e466c8162cfc21e994b6d08465cf30873e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 17:50:02 -0700 Subject: [PATCH 068/105] More dataclass tests, fix get_annotations() bug --- Lib/annotations.py | 2 +- Lib/test/test_annotations.py | 6 ++++++ Lib/test/test_dataclasses/__init__.py | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index 12cb4fcf194f55..72d9b6d1b7b52f 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -345,7 +345,7 @@ def get_annotations( ann = _call_dunder_annotate(annotate, format) elif isinstance(obj, type): # class - ann = obj.__annotations__ + ann = getattr(obj, '__annotations__', None) obj_globals = None module_name = getattr(obj, "__module__", None) diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index 109a881840422c..f366afbfc8135a 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -63,3 +63,9 @@ def test_special_attrs(self): for proto in range(pickle.HIGHEST_PROTOCOL + 1): with self.assertRaises(TypeError): pickle.dumps(fr, proto) + + +class TestGetAnnotations(unittest.TestCase): + def test_builtin_type(self): + self.assertEqual(annotations.get_annotations(int), {}) + self.assertEqual(annotations.get_annotations(object), {}) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 6eea0ece95f3da..b93c99d8c90bf3 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -4813,6 +4813,10 @@ class A: x: undefined y: ClassVar[undefined] + fs = fields(A) + self.assertEqual(len(fs), 1) + self.assertEqual(fs[0].name, 'x') + if __name__ == '__main__': unittest.main() From 7c21c6a3f82ee409581ea2bd2db34bd4055be508 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 17:56:45 -0700 Subject: [PATCH 069/105] Fix some bugs --- Lib/annotations.py | 2 ++ Lib/test/test_annotations.py | 7 +++++++ Lib/typing.py | 8 ++++---- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index 72d9b6d1b7b52f..34ba99ffbdbf60 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -369,6 +369,8 @@ def get_annotations( obj_globals = getattr(obj, "__globals__", None) obj_locals = None unwrap = obj + elif (ann := getattr(obj, "__annotations__", None)) is not None: + obj_globals = obj_locals = unwrap = None else: raise TypeError(f"{obj!r} is not a module, class, or callable.") diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index f366afbfc8135a..c920720d4a3469 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -69,3 +69,10 @@ class TestGetAnnotations(unittest.TestCase): def test_builtin_type(self): self.assertEqual(annotations.get_annotations(int), {}) self.assertEqual(annotations.get_annotations(object), {}) + + def test_custom_object_with_annotations(self): + class C: + def __init__(self, x: int, y: str): + self.__annotations__ = {"x": int, "y": str} + + self.assertEqual(annotations.get_annotations(C()), {"x": int, "y": str}) diff --git a/Lib/typing.py b/Lib/typing.py index 291161457d3fad..654648464b2ce3 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3098,7 +3098,7 @@ def __new__(cls, name, bases, ns, total=True): if not hasattr(tp_dict, '__orig_bases__'): tp_dict.__orig_bases__ = bases - annotations = {} + new_annotations = {} if "__annotations__" in ns: own_annotations = ns["__annotations__"] elif "__annotate__" in ns: @@ -3121,7 +3121,7 @@ def __new__(cls, name, bases, ns, total=True): # keys have Required/NotRequired/ReadOnly qualifiers, and create # a new __annotate__ function for the resulting TypedDict that # combines the annotations from this class and its parents. - annotations.update(base.__annotations__) + new_annotations.update(base.__annotations__) base_required = base.__dict__.get('__required_keys__', set()) required_keys |= base_required @@ -3134,7 +3134,7 @@ def __new__(cls, name, bases, ns, total=True): readonly_keys.update(base.__dict__.get('__readonly_keys__', ())) mutable_keys.update(base.__dict__.get('__mutable_keys__', ())) - annotations.update(own_annotations) + new_annotations.update(own_annotations) for annotation_key, annotation_type in own_annotations.items(): qualifiers = set(_get_typeddict_qualifiers(annotation_type)) if Required in qualifiers: @@ -3166,7 +3166,7 @@ def __new__(cls, name, bases, ns, total=True): f"Required keys overlap with optional keys in {name}:" f" {required_keys=}, {optional_keys=}" ) - tp_dict.__annotations__ = annotations + tp_dict.__annotations__ = new_annotations tp_dict.__required_keys__ = frozenset(required_keys) tp_dict.__optional_keys__ = frozenset(optional_keys) tp_dict.__readonly_keys__ = frozenset(readonly_keys) From bd469abc2bb1d7c438ef622c4c429ffc0a5b1ccb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 18:19:12 -0700 Subject: [PATCH 070/105] Still generate __annotate__ if "from __future__ import annotations" is on This makes it so functools.update_wrapper can always copy __annotate__ without having to worry about whether or not the future is enabled. --- Lib/test/test_type_annotations.py | 13 +++++++ Python/compile.c | 23 ++++-------- Python/symtable.c | 60 ++++++++++++++----------------- 3 files changed, 46 insertions(+), 50 deletions(-) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 25dc4d883eb356..810201746edf11 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -384,3 +384,16 @@ def test_comprehension_in_annotation(self): # This crashed in an earlier version of the code ns = run_code("x: [y for y in range(10)]") self.assertEqual(ns["__annotate__"](1), {"x": list(range(10))}) + + def test_future_annotations(self): + code = """ + from __future__ import annotations + + def f(x: int) -> int: pass + """ + ns = run_code(textwrap.dedent(code)) + f = ns["f"] + self.assertIsInstance(f.__annotate__, types.FunctionType) + annos = {"x": "int", "return": "int"} + self.assertEqual(f.__annotate__(inspect.VALUE), annos) + self.assertEqual(f.__annotations__, annos) diff --git a/Python/compile.c b/Python/compile.c index fd91c72f1fa4a5..fa81cf463cb902 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1920,7 +1920,6 @@ compiler_visit_annotations(struct compiler *c, location loc, Return -1 on error, or a combination of flags to add to the function. */ Py_ssize_t annotations_len = 0; - int future_annotations = c->c_future.ff_features & CO_FUTURE_ANNOTATIONS; PySTEntryObject *ste; int result = _PySymtable_LookupOptional(c->c_st, args, &ste); @@ -1930,7 +1929,7 @@ compiler_visit_annotations(struct compiler *c, location loc, assert(ste != NULL); bool annotations_used = ste->ste_annotations_used; - if (annotations_used && !future_annotations) { + if (annotations_used) { if (compiler_setup_annotations_scope(c, loc, (void *)args, ste->ste_name) < 0) { Py_DECREF(ste); @@ -1940,25 +1939,17 @@ compiler_visit_annotations(struct compiler *c, location loc, Py_DECREF(ste); if (compiler_visit_annotations_in_scope(c, loc, args, returns, &annotations_len) < 0) { - if (annotations_used && !future_annotations) { + if (annotations_used) { compiler_exit_scope(c); } return ERROR; } - if (future_annotations) { - if (annotations_len) { - ADDOP_I(c, loc, BUILD_TUPLE, annotations_len * 2); - return MAKE_FUNCTION_ANNOTATIONS; - } - } - else { - if (annotations_used) { - RETURN_IF_ERROR( - compiler_leave_annotations_scope(c, loc, annotations_len) - ); - return MAKE_FUNCTION_ANNOTATE; - } + if (annotations_used) { + RETURN_IF_ERROR( + compiler_leave_annotations_scope(c, loc, annotations_len) + ); + return MAKE_FUNCTION_ANNOTATE; } return 0; diff --git a/Python/symtable.c b/Python/symtable.c index ebf936ab25799a..e53cef9bf2d0fb 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -2488,45 +2488,37 @@ static int symtable_visit_annotation(struct symtable *st, expr_ty annotation, void *key) { struct _symtable_entry *parent_ste = st->st_cur; - int future_annotations = st->st_future->ff_features & CO_FUTURE_ANNOTATIONS; - if (future_annotations) { - if(!symtable_enter_block(st, &_Py_ID(_annotation), AnnotationBlock, - key, LOCATION(annotation))) { + if (parent_ste->ste_annotation_block == NULL) { + _Py_block_ty current_type = parent_ste->ste_type; + if (!symtable_enter_block(st, &_Py_ID(__annotate__), AnnotationBlock, + key, LOCATION(annotation))) { VISIT_QUIT(st, 0); } - } - else { - if (parent_ste->ste_annotation_block == NULL) { - _Py_block_ty current_type = parent_ste->ste_type; - if (!symtable_enter_block(st, &_Py_ID(__annotate__), AnnotationBlock, - key, LOCATION(annotation))) { - VISIT_QUIT(st, 0); - } - parent_ste->ste_annotation_block = - (struct _symtable_entry *)Py_NewRef(st->st_cur); - if (current_type == ClassBlock) { - st->st_cur->ste_can_see_class_scope = 1; - if (!symtable_add_def(st, &_Py_ID(__classdict__), USE, LOCATION(annotation))) { - return 0; - } - } - - _Py_DECLARE_STR(format, ".format"); - // The generated __annotate__ function takes a single parameter with the - // internal name ".format". - if (!symtable_add_def(st, &_Py_STR(format), DEF_PARAM, - LOCATION(annotation))) { - return 0; - } - if (!symtable_add_def(st, &_Py_STR(format), USE, - LOCATION(annotation))) { + parent_ste->ste_annotation_block = + (struct _symtable_entry *)Py_NewRef(st->st_cur); + int future_annotations = st->st_future->ff_features & CO_FUTURE_ANNOTATIONS; + if (current_type == ClassBlock && !future_annotations) { + st->st_cur->ste_can_see_class_scope = 1; + if (!symtable_add_def(st, &_Py_ID(__classdict__), USE, LOCATION(annotation))) { return 0; } } - else { - if (!symtable_enter_existing_block(st, parent_ste->ste_annotation_block)) { - VISIT_QUIT(st, 0); - } + + _Py_DECLARE_STR(format, ".format"); + // The generated __annotate__ function takes a single parameter with the + // internal name ".format". + if (!symtable_add_def(st, &_Py_STR(format), DEF_PARAM, + LOCATION(annotation))) { + return 0; + } + if (!symtable_add_def(st, &_Py_STR(format), USE, + LOCATION(annotation))) { + return 0; + } + } + else { + if (!symtable_enter_existing_block(st, parent_ste->ste_annotation_block)) { + VISIT_QUIT(st, 0); } } VISIT(st, expr, annotation); From 174aa472ac52ad9d3e64867bb0a69e7c7c22aae1 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 19:28:30 -0700 Subject: [PATCH 071/105] Support for deferred evaluation of annotations in NamedTuple --- Lib/annotations.py | 30 +++++++++++++++++++++--- Lib/test/test_typing.py | 45 +++++++++++++++++++++++++++++++++++- Lib/typing.py | 51 ++++++++++++++++++++++++++++++++--------- 3 files changed, 111 insertions(+), 15 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index 34ba99ffbdbf60..945942b34c792a 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -236,13 +236,35 @@ def __iter__(self): def __next__(self): return Stringifier(ast.Starred(self.node)) + def __str__(self): + return ast.unparse(self.node) + class _StringifierDict(dict): def __missing__(self, key): return Stringifier(ast.Name(key)) -def _call_dunder_annotate(annotate, format, owner=None): +def call_annotate_function(annotate, format, owner=None): + """Call an __annotate__ function. __annotate__ functions are normally + generated by the compiler to defer the evaluation of annotations. They + can be called with any of the format arguments in the Format enum, but + compiler-generated __annotate__ functions only support the VALUE format. + This function provides additional functionality to call __annotate__ + functions with the FORWARDREF and SOURCE formats. + + *annotate* must be an __annotate__ function, which takes a single argument + and returns a dict of annotations. + + *format* must be a member of the Format enum or one of the corresponding + integer values. + + *owner* can be the object that owns the annotations (i.e., the module, + class, or function that the __annotate__ function derives from). With the + FORWARDREF format, it is used to provide better evaluation capabilities + on the generated ForwardRef objects. + + """ try: return annotate(format) except NotImplementedError: @@ -287,9 +309,11 @@ def _call_dunder_annotate(annotate, format, owner=None): ) annos = func(Format.VALUE) return { - key: ast.unparse(val.node) if isinstance(val, Stringifier) else repr(val) + key: val if isinstance(val, str) else repr(val) for key, val in annos.items() } + elif format == Format.VALUE: + raise RuntimeError("annotate function does not support VALUE format") def get_annotations( @@ -342,7 +366,7 @@ def get_annotations( annotate = getattr(obj, "__annotate__", None) # TODO remove format != VALUE condition if annotate is not None and format != Format.VALUE: - ann = _call_dunder_annotate(annotate, format) + ann = call_annotate_function(annotate, format) elif isinstance(obj, type): # class ann = getattr(obj, '__annotations__', None) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 39ba0de3358e09..db67acf966cef1 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1,3 +1,4 @@ +import annotations import contextlib import collections import collections.abc @@ -45,7 +46,7 @@ import weakref import types -from test.support import captured_stderr, cpython_only, infinite_recursion, requires_docstrings, import_helper +from test.support import captured_stderr, cpython_only, infinite_recursion, requires_docstrings, import_helper, run_code from test.typinganndata import ann_module695, mod_generics_cache, _typed_dict_helper @@ -7758,6 +7759,48 @@ class XMethBad2(NamedTuple): def _source(self): return 'no chance for this as well' + def test_annotation_type_check(self): + # These are rejected by _type_check + with self.assertRaises(TypeError): + class X(NamedTuple): + a: Final + with self.assertRaises(TypeError): + class Y(NamedTuple): + a: (1, 2) + + # Conversion by _type_convert + class Z(NamedTuple): + a: None + b: "str" + annos = {'a': type(None), 'b': ForwardRef("str")} + self.assertEqual(Z.__annotations__, annos) + self.assertEqual(Z.__annotate__(annotations.Format.VALUE), annos) + self.assertEqual(Z.__annotate__(annotations.Format.FORWARDREF), annos) + self.assertEqual(Z.__annotate__(annotations.Format.SOURCE), {"a": "None", "b": "str"}) + + def test_future_annotations(self): + code = """ + from __future__ import annotations + from typing import NamedTuple + class X(NamedTuple): + a: int + b: None + """ + ns = run_code(textwrap.dedent(code)) + X = ns['X'] + self.assertEqual(X.__annotations__, {'a': ForwardRef("int"), 'b': ForwardRef("None")}) + + def test_deferred_annotations(self): + class X(NamedTuple): + y: undefined + + self.assertEqual(X._fields, ('y',)) + with self.assertRaises(NameError): + X.__annotations__ + + undefined = int + self.assertEqual(X.__annotations__, {'y': int}) + def test_multiple_inheritance(self): class A: pass diff --git a/Lib/typing.py b/Lib/typing.py index 654648464b2ce3..ef2e644d61d850 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2907,22 +2907,30 @@ def __round__(self, ndigits: int = 0) -> T: pass -def _make_nmtuple(name, types, module, defaults = ()): - fields = [n for n, t in types] - types = {n: _type_check(t, f"field {n} annotation must be a type") - for n, t in types} +def _make_nmtuple(name, fields, annotate_func, module, defaults = ()): nm_tpl = collections.namedtuple(name, fields, defaults=defaults, module=module) - nm_tpl.__annotations__ = nm_tpl.__new__.__annotations__ = types + nm_tpl.__annotate__ = nm_tpl.__new__.__annotate__ = annotate_func return nm_tpl +def _make_eager_annotate(types): + checked_types = {key: _type_check(val, f"field {key} annotation must be a type") + for key, val in types.items()} + def annotate(format): + if format in (annotations.Format.VALUE, annotations.Format.FORWARDREF): + return checked_types + else: + return {n: t if isinstance(t, str) else _type_repr(t) for n, t in types.items()} + return annotate + + # attributes prohibited to set in NamedTuple class syntax _prohibited = frozenset({'__new__', '__init__', '__slots__', '__getnewargs__', '_fields', '_field_defaults', '_make', '_replace', '_asdict', '_source'}) -_special = frozenset({'__module__', '__name__', '__annotations__'}) +_special = frozenset({'__module__', '__name__', '__annotations__', '__annotate__'}) class NamedTupleMeta(type): @@ -2935,12 +2943,29 @@ def __new__(cls, typename, bases, ns): bases = tuple(tuple if base is _NamedTuple else base for base in bases) if "__annotations__" in ns: types = ns["__annotations__"] + field_names = list(types) + annotate = _make_eager_annotate(types) elif "__annotate__" in ns: - types = ns["__annotate__"](annotations.Format.VALUE) + original_annotate = ns["__annotate__"] + types = annotations.call_annotate_function(original_annotate, annotations.Format.FORWARDREF) + field_names = list(types) + + # For backward compatibility, type-check all the types at creation time + for typ in types.values(): + _type_check(typ, "field annotation must be a type") + + def annotate(format): + annos = annotations.call_annotate_function(original_annotate, format) + if format != annotations.Format.SOURCE: + return {key: _type_check(val, f"field {key} annotation must be a type") + for key, val in annos.items()} + return annos else: - types = {} + # Empty NamedTuple + field_names = [] + annotate = lambda format: {} default_names = [] - for field_name in types: + for field_name in field_names: if field_name in ns: default_names.append(field_name) elif default_names: @@ -2948,7 +2973,7 @@ def __new__(cls, typename, bases, ns): f"cannot follow default field" f"{'s' if len(default_names) > 1 else ''} " f"{', '.join(default_names)}") - nm_tpl = _make_nmtuple(typename, types.items(), + nm_tpl = _make_nmtuple(typename, field_names, annotate, defaults=[ns[n] for n in default_names], module=ns['__module__']) nm_tpl.__bases__ = bases @@ -3039,7 +3064,11 @@ class Employee(NamedTuple): import warnings warnings._deprecated(deprecated_thing, message=deprecation_msg, remove=(3, 15)) fields = kwargs.items() - nt = _make_nmtuple(typename, fields, module=_caller()) + types = {n: _type_check(t, f"field {n} annotation must be a type") + for n, t in fields} + field_names = [n for n, _ in fields] + + nt = _make_nmtuple(typename, field_names, _make_eager_annotate(types), module=_caller()) nt.__orig_bases__ = (NamedTuple,) return nt From cdcd8c8f328220cfa21bc8e1dbd154faaf803ae4 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 19:28:59 -0700 Subject: [PATCH 072/105] Fix test --- Lib/test/test_annotations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index c920720d4a3469..d6ceb5ecb6b60e 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -72,7 +72,7 @@ def test_builtin_type(self): def test_custom_object_with_annotations(self): class C: - def __init__(self, x: int, y: str): + def __init__(self, x: int = 0, y: str = ""): self.__annotations__ = {"x": int, "y": str} self.assertEqual(annotations.get_annotations(C()), {"x": int, "y": str}) From 62ff51c243eedc9cf831bf552f722fc52dc1cb63 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 20:04:36 -0700 Subject: [PATCH 073/105] TypedDict --- Lib/annotations.py | 2 +- Lib/test/test_typing.py | 77 ++++++++++++++++++++++++++++++++++++++++- Lib/typing.py | 53 ++++++++++++++++++++-------- 3 files changed, 116 insertions(+), 16 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index 945942b34c792a..6d95f7261f984f 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -236,7 +236,7 @@ def __iter__(self): def __next__(self): return Stringifier(ast.Starred(self.node)) - def __str__(self): + def __repr__(self): return ast.unparse(self.node) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index db67acf966cef1..ea53c4156a9e20 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -8115,7 +8115,11 @@ def test_basics_functional_syntax(self): self.assertEqual(Emp.__name__, 'Emp') self.assertEqual(Emp.__module__, __name__) self.assertEqual(Emp.__bases__, (dict,)) - self.assertEqual(Emp.__annotations__, {'name': str, 'id': int}) + annos = {'name': str, 'id': int} + self.assertEqual(Emp.__annotations__, annos) + self.assertEqual(Emp.__annotate__(annotations.Format.VALUE), annos) + self.assertEqual(Emp.__annotate__(annotations.Format.FORWARDREF), annos) + self.assertEqual(Emp.__annotate__(annotations.Format.SOURCE), {'name': 'str', 'id': 'int'}) self.assertEqual(Emp.__total__, True) self.assertEqual(Emp.__required_keys__, {'name', 'id'}) self.assertIsInstance(Emp.__required_keys__, frozenset) @@ -8476,6 +8480,8 @@ class A[T](TypedDict): self.assertEqual(A.__bases__, (Generic, dict)) self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T])) self.assertEqual(A.__mro__, (A, Generic, dict, object)) + self.assertEqual(A.__annotations__, {'a': T}) + self.assertEqual(A.__annotate__(annotations.Format.SOURCE), {'a': 'T'}) self.assertEqual(A.__parameters__, (T,)) self.assertEqual(A[str].__parameters__, ()) self.assertEqual(A[str].__args__, (str,)) @@ -8487,6 +8493,8 @@ class A(TypedDict, Generic[T]): self.assertEqual(A.__bases__, (Generic, dict)) self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T])) self.assertEqual(A.__mro__, (A, Generic, dict, object)) + self.assertEqual(A.__annotations__, {'a': T}) + self.assertEqual(A.__annotate__(annotations.Format.SOURCE), {'a': 'T'}) self.assertEqual(A.__parameters__, (T,)) self.assertEqual(A[str].__parameters__, ()) self.assertEqual(A[str].__args__, (str,)) @@ -8497,6 +8505,8 @@ class A2(Generic[T], TypedDict): self.assertEqual(A2.__bases__, (Generic, dict)) self.assertEqual(A2.__orig_bases__, (Generic[T], TypedDict)) self.assertEqual(A2.__mro__, (A2, Generic, dict, object)) + self.assertEqual(A2.__annotations__, {'a': T}) + self.assertEqual(A2.__annotate__(annotations.Format.SOURCE), {'a': 'T'}) self.assertEqual(A2.__parameters__, (T,)) self.assertEqual(A2[str].__parameters__, ()) self.assertEqual(A2[str].__args__, (str,)) @@ -8507,6 +8517,8 @@ class B(A[KT], total=False): self.assertEqual(B.__bases__, (Generic, dict)) self.assertEqual(B.__orig_bases__, (A[KT],)) self.assertEqual(B.__mro__, (B, Generic, dict, object)) + self.assertEqual(B.__annotations__, {'a': T, 'b': KT}) + self.assertEqual(B.__annotate__(annotations.Format.SOURCE), {'a': 'T', 'b': 'KT'}) self.assertEqual(B.__parameters__, (KT,)) self.assertEqual(B.__total__, False) self.assertEqual(B.__optional_keys__, frozenset(['b'])) @@ -8531,6 +8543,11 @@ class C(B[int]): 'b': KT, 'c': int, }) + self.assertEqual(C.__annotate__(annotations.Format.SOURCE), { + 'a': 'T', + 'b': 'KT', + 'c': 'int', + }) with self.assertRaises(TypeError): C[str] @@ -8550,6 +8567,11 @@ class Point3D(Point2DGeneric[T], Generic[T, KT]): 'b': T, 'c': KT, }) + self.assertEqual(Point3D.__annotate__(annotations.Format.SOURCE), { + 'a': 'T', + 'b': 'T', + 'c': 'KT', + }) self.assertEqual(Point3D[int, str].__origin__, Point3D) with self.assertRaises(TypeError): @@ -8581,6 +8603,11 @@ class WithImplicitAny(B): 'b': KT, 'c': int, }) + self.assertEqual(WithImplicitAny.__annotate__(annotations.Format.SOURCE), { + 'a': 'T', + 'b': 'KT', + 'c': 'int', + }) with self.assertRaises(TypeError): WithImplicitAny[str] @@ -8737,6 +8764,54 @@ class AllTheThings(TypedDict): }, ) + def test_annotations(self): + # _type_check is applied + with self.assertRaisesRegex(TypeError, "Plain typing.Final is not valid as type argument"): + class X(TypedDict): + a: Final + + # _type_convert is applied + class Y(TypedDict): + a: None + b: "int" + fwdref = ForwardRef('int', module='test.test_typing') + self.assertEqual(Y.__annotations__, {'a': type(None), 'b': fwdref}) + self.assertEqual(Y.__annotate__(annotations.Format.FORWARDREF), {'a': type(None), 'b': fwdref}) + + # _type_check is also applied later + class Z(TypedDict): + a: undefined + + with self.assertRaises(NameError): + Z.__annotations__ + + undefined = Final + with self.assertRaisesRegex(TypeError, "Plain typing.Final is not valid as type argument"): + Z.__annotations__ + + undefined = None + self.assertEqual(Z.__annotations__, {'a': type(None)}) + + def test_deferred_evaluation(self): + class A(TypedDict): + x: NotRequired[undefined] + y: ReadOnly[undefined] + z: Required[undefined] + + self.assertEqual(A.__required_keys__, frozenset({'y', 'z'})) + self.assertEqual(A.__optional_keys__, frozenset({'x'})) + self.assertEqual(A.__readonly_keys__, frozenset({'y'})) + self.assertEqual(A.__mutable_keys__, frozenset({'x', 'z'})) + + with self.assertRaises(NameError): + A.__annotations__ + + self.assertEqual( + A.__annotate__(annotations.Format.SOURCE), + {'x': 'NotRequired[undefined]', 'y': 'ReadOnly[undefined]', + 'z': 'Required[undefined]'}, + ) + class RequiredTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index ef2e644d61d850..e72130289e939b 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2921,10 +2921,14 @@ def annotate(format): if format in (annotations.Format.VALUE, annotations.Format.FORWARDREF): return checked_types else: - return {n: t if isinstance(t, str) else _type_repr(t) for n, t in types.items()} + return _convert_to_source(types) return annotate +def _convert_to_source(types): + return {n: t if isinstance(t, str) else _type_repr(t) for n, t in types.items()} + + # attributes prohibited to set in NamedTuple class syntax _prohibited = frozenset({'__new__', '__init__', '__slots__', '__getnewargs__', '_fields', '_field_defaults', @@ -3127,15 +3131,19 @@ def __new__(cls, name, bases, ns, total=True): if not hasattr(tp_dict, '__orig_bases__'): tp_dict.__orig_bases__ = bases - new_annotations = {} if "__annotations__" in ns: + own_annotate = None own_annotations = ns["__annotations__"] elif "__annotate__" in ns: - own_annotations = ns["__annotate__"](annotations.Format.VALUE) + own_annotate = ns["__annotate__"] + own_annotations = annotations.call_annotate_function( + own_annotate, annotations.Format.FORWARDREF, owner=tp_dict + ) else: + own_annotate = None own_annotations = {} msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" - own_annotations = { + own_checked_annotations = { n: _type_check(tp, msg, module=tp_dict.__module__) for n, tp in own_annotations.items() } @@ -3145,13 +3153,6 @@ def __new__(cls, name, bases, ns, total=True): mutable_keys = set() for base in bases: - # TODO: Avoid eagerly evaluating annotations in VALUE format. - # Instead, evaluate in FORWARDREF format to figure out which - # keys have Required/NotRequired/ReadOnly qualifiers, and create - # a new __annotate__ function for the resulting TypedDict that - # combines the annotations from this class and its parents. - new_annotations.update(base.__annotations__) - base_required = base.__dict__.get('__required_keys__', set()) required_keys |= base_required optional_keys -= base_required @@ -3163,8 +3164,7 @@ def __new__(cls, name, bases, ns, total=True): readonly_keys.update(base.__dict__.get('__readonly_keys__', ())) mutable_keys.update(base.__dict__.get('__mutable_keys__', ())) - new_annotations.update(own_annotations) - for annotation_key, annotation_type in own_annotations.items(): + for annotation_key, annotation_type in own_checked_annotations.items(): qualifiers = set(_get_typeddict_qualifiers(annotation_type)) if Required in qualifiers: is_required = True @@ -3195,7 +3195,32 @@ def __new__(cls, name, bases, ns, total=True): f"Required keys overlap with optional keys in {name}:" f" {required_keys=}, {optional_keys=}" ) - tp_dict.__annotations__ = new_annotations + + def __annotate__(format): + annos = {} + for base in bases: + if base is Generic: + continue + base_annotate = base.__annotate__ + if base_annotate is None: + continue + base_annos = annotations.call_annotate_function(base.__annotate__, format, owner=base) + annos.update(base_annos) + if own_annotate is not None: + own = annotations.call_annotate_function(own_annotate, format, owner=tp_dict) + if format != annotations.Format.SOURCE: + own = { + n: _type_check(tp, msg, module=tp_dict.__module__) + for n, tp in own.items() + } + elif format == annotations.Format.SOURCE: + own = _convert_to_source(own_annotations) + else: + own = own_checked_annotations + annos.update(own) + return annos + + tp_dict.__annotate__ = __annotate__ tp_dict.__required_keys__ = frozenset(required_keys) tp_dict.__optional_keys__ = frozenset(optional_keys) tp_dict.__readonly_keys__ = frozenset(readonly_keys) From 643b21542c990b2be7fa9f05b2d62cfaed6d164d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 21:31:55 -0700 Subject: [PATCH 074/105] Move get_annotations tests --- Lib/test/test_annotations.py | 104 ++++++++++++++++++++++++++ Lib/test/test_inspect/test_inspect.py | 100 ------------------------- 2 files changed, 104 insertions(+), 100 deletions(-) diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index d6ceb5ecb6b60e..c41891e0760faa 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -1,9 +1,14 @@ """Tests for the annotations module.""" import annotations +import functools import pickle import unittest +from test.test_inspect import inspect_stock_annotations +from test.test_inspect import inspect_stringized_annotations +from test.test_inspect import inspect_stringized_annotations_2 + class TestFormat(unittest.TestCase): def test_enum(self): @@ -76,3 +81,102 @@ def __init__(self, x: int = 0, y: str = ""): self.__annotations__ = {"x": int, "y": str} self.assertEqual(annotations.get_annotations(C()), {"x": int, "y": str}) + + def test_get_annotations_with_stock_annotations(self): + def foo(a:int, b:str): pass + self.assertEqual(annotations.get_annotations(foo), {'a': int, 'b': str}) + + foo.__annotations__ = {'a': 'foo', 'b':'str'} + self.assertEqual(annotations.get_annotations(foo), {'a': 'foo', 'b': 'str'}) + + self.assertEqual(annotations.get_annotations(foo, eval_str=True, locals=locals()), {'a': foo, 'b': str}) + self.assertEqual(annotations.get_annotations(foo, eval_str=True, globals=locals()), {'a': foo, 'b': str}) + + isa = inspect_stock_annotations + self.assertEqual(annotations.get_annotations(isa), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.MyClass), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.function), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function2), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function3), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) + self.assertEqual(annotations.get_annotations(annotations), {}) # annotations module has no annotations + self.assertEqual(annotations.get_annotations(isa.UnannotatedClass), {}) + self.assertEqual(annotations.get_annotations(isa.unannotated_function), {}) + + self.assertEqual(annotations.get_annotations(isa, eval_str=True), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.MyClass, eval_str=True), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.function, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function2, eval_str=True), {'a': int, 'b': str, 'c': isa.MyClass, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function3, eval_str=True), {'a': int, 'b': str, 'c': isa.MyClass}) + self.assertEqual(annotations.get_annotations(annotations, eval_str=True), {}) + self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, eval_str=True), {}) + self.assertEqual(annotations.get_annotations(isa.unannotated_function, eval_str=True), {}) + + self.assertEqual(annotations.get_annotations(isa, eval_str=False), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.MyClass, eval_str=False), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.function, eval_str=False), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function2, eval_str=False), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function3, eval_str=False), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) + self.assertEqual(annotations.get_annotations(annotations, eval_str=False), {}) + self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, eval_str=False), {}) + self.assertEqual(annotations.get_annotations(isa.unannotated_function, eval_str=False), {}) + + def times_three(fn): + @functools.wraps(fn) + def wrapper(a, b): + return fn(a*3, b*3) + return wrapper + + wrapped = times_three(isa.function) + self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx')) + self.assertIsNot(wrapped.__globals__, isa.function.__globals__) + self.assertEqual(annotations.get_annotations(wrapped), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(wrapped, eval_str=False), {'a': int, 'b': str, 'return': isa.MyClass}) + + def test_get_annotations_with_stringized_annotations(self): + isa = inspect_stringized_annotations + self.assertEqual(annotations.get_annotations(isa), {'a': 'int', 'b': 'str'}) + self.assertEqual(annotations.get_annotations(isa.MyClass), {'a': 'int', 'b': 'str'}) + self.assertEqual(annotations.get_annotations(isa.function), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) + self.assertEqual(annotations.get_annotations(isa.function2), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'}) + self.assertEqual(annotations.get_annotations(isa.function3), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"}) + self.assertEqual(annotations.get_annotations(isa.UnannotatedClass), {}) + self.assertEqual(annotations.get_annotations(isa.unannotated_function), {}) + + self.assertEqual(annotations.get_annotations(isa, eval_str=True), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.MyClass, eval_str=True), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.function, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function2, eval_str=True), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function3, eval_str=True), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) + self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, eval_str=True), {}) + self.assertEqual(annotations.get_annotations(isa.unannotated_function, eval_str=True), {}) + + self.assertEqual(annotations.get_annotations(isa, eval_str=False), {'a': 'int', 'b': 'str'}) + self.assertEqual(annotations.get_annotations(isa.MyClass, eval_str=False), {'a': 'int', 'b': 'str'}) + self.assertEqual(annotations.get_annotations(isa.function, eval_str=False), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) + self.assertEqual(annotations.get_annotations(isa.function2, eval_str=False), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'}) + self.assertEqual(annotations.get_annotations(isa.function3, eval_str=False), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"}) + self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, eval_str=False), {}) + self.assertEqual(annotations.get_annotations(isa.unannotated_function, eval_str=False), {}) + + isa2 = inspect_stringized_annotations_2 + self.assertEqual(annotations.get_annotations(isa2), {}) + self.assertEqual(annotations.get_annotations(isa2, eval_str=True), {}) + self.assertEqual(annotations.get_annotations(isa2, eval_str=False), {}) + + def times_three(fn): + @functools.wraps(fn) + def wrapper(a, b): + return fn(a*3, b*3) + return wrapper + + wrapped = times_three(isa.function) + self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx')) + self.assertIsNot(wrapped.__globals__, isa.function.__globals__) + self.assertEqual(annotations.get_annotations(wrapped), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) + self.assertEqual(annotations.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(wrapped, eval_str=False), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) + + # test that local namespace lookups work + self.assertEqual(annotations.get_annotations(isa.MyClassWithLocalAnnotations), {'x': 'mytype'}) + self.assertEqual(annotations.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {'x': int}) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index e6d5ebb71a306a..225f5fe472087a 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -44,7 +44,6 @@ from test.test_inspect import inspect_fodder as mod from test.test_inspect import inspect_fodder2 as mod2 -from test.test_inspect import inspect_stock_annotations from test.test_inspect import inspect_stringized_annotations from test.test_inspect import inspect_stringized_annotations_2 @@ -1592,105 +1591,6 @@ class C(metaclass=M): attrs = [a[0] for a in inspect.getmembers(C)] self.assertNotIn('missing', attrs) - def test_get_annotations_with_stock_annotations(self): - def foo(a:int, b:str): pass - self.assertEqual(inspect.get_annotations(foo), {'a': int, 'b': str}) - - foo.__annotations__ = {'a': 'foo', 'b':'str'} - self.assertEqual(inspect.get_annotations(foo), {'a': 'foo', 'b': 'str'}) - - self.assertEqual(inspect.get_annotations(foo, eval_str=True, locals=locals()), {'a': foo, 'b': str}) - self.assertEqual(inspect.get_annotations(foo, eval_str=True, globals=locals()), {'a': foo, 'b': str}) - - isa = inspect_stock_annotations - self.assertEqual(inspect.get_annotations(isa), {'a': int, 'b': str}) - self.assertEqual(inspect.get_annotations(isa.MyClass), {'a': int, 'b': str}) - self.assertEqual(inspect.get_annotations(isa.function), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(inspect.get_annotations(isa.function2), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) - self.assertEqual(inspect.get_annotations(isa.function3), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) - self.assertEqual(inspect.get_annotations(inspect), {}) # inspect module has no annotations - self.assertEqual(inspect.get_annotations(isa.UnannotatedClass), {}) - self.assertEqual(inspect.get_annotations(isa.unannotated_function), {}) - - self.assertEqual(inspect.get_annotations(isa, eval_str=True), {'a': int, 'b': str}) - self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=True), {'a': int, 'b': str}) - self.assertEqual(inspect.get_annotations(isa.function, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(inspect.get_annotations(isa.function2, eval_str=True), {'a': int, 'b': str, 'c': isa.MyClass, 'return': isa.MyClass}) - self.assertEqual(inspect.get_annotations(isa.function3, eval_str=True), {'a': int, 'b': str, 'c': isa.MyClass}) - self.assertEqual(inspect.get_annotations(inspect, eval_str=True), {}) - self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=True), {}) - self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=True), {}) - - self.assertEqual(inspect.get_annotations(isa, eval_str=False), {'a': int, 'b': str}) - self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=False), {'a': int, 'b': str}) - self.assertEqual(inspect.get_annotations(isa.function, eval_str=False), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(inspect.get_annotations(isa.function2, eval_str=False), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) - self.assertEqual(inspect.get_annotations(isa.function3, eval_str=False), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) - self.assertEqual(inspect.get_annotations(inspect, eval_str=False), {}) - self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=False), {}) - self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=False), {}) - - def times_three(fn): - @functools.wraps(fn) - def wrapper(a, b): - return fn(a*3, b*3) - return wrapper - - wrapped = times_three(isa.function) - self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx')) - self.assertIsNot(wrapped.__globals__, isa.function.__globals__) - self.assertEqual(inspect.get_annotations(wrapped), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(inspect.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(inspect.get_annotations(wrapped, eval_str=False), {'a': int, 'b': str, 'return': isa.MyClass}) - - def test_get_annotations_with_stringized_annotations(self): - isa = inspect_stringized_annotations - self.assertEqual(inspect.get_annotations(isa), {'a': 'int', 'b': 'str'}) - self.assertEqual(inspect.get_annotations(isa.MyClass), {'a': 'int', 'b': 'str'}) - self.assertEqual(inspect.get_annotations(isa.function), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) - self.assertEqual(inspect.get_annotations(isa.function2), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'}) - self.assertEqual(inspect.get_annotations(isa.function3), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"}) - self.assertEqual(inspect.get_annotations(isa.UnannotatedClass), {}) - self.assertEqual(inspect.get_annotations(isa.unannotated_function), {}) - - self.assertEqual(inspect.get_annotations(isa, eval_str=True), {'a': int, 'b': str}) - self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=True), {'a': int, 'b': str}) - self.assertEqual(inspect.get_annotations(isa.function, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(inspect.get_annotations(isa.function2, eval_str=True), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) - self.assertEqual(inspect.get_annotations(isa.function3, eval_str=True), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) - self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=True), {}) - self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=True), {}) - - self.assertEqual(inspect.get_annotations(isa, eval_str=False), {'a': 'int', 'b': 'str'}) - self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=False), {'a': 'int', 'b': 'str'}) - self.assertEqual(inspect.get_annotations(isa.function, eval_str=False), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) - self.assertEqual(inspect.get_annotations(isa.function2, eval_str=False), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'}) - self.assertEqual(inspect.get_annotations(isa.function3, eval_str=False), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"}) - self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=False), {}) - self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=False), {}) - - isa2 = inspect_stringized_annotations_2 - self.assertEqual(inspect.get_annotations(isa2), {}) - self.assertEqual(inspect.get_annotations(isa2, eval_str=True), {}) - self.assertEqual(inspect.get_annotations(isa2, eval_str=False), {}) - - def times_three(fn): - @functools.wraps(fn) - def wrapper(a, b): - return fn(a*3, b*3) - return wrapper - - wrapped = times_three(isa.function) - self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx')) - self.assertIsNot(wrapped.__globals__, isa.function.__globals__) - self.assertEqual(inspect.get_annotations(wrapped), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) - self.assertEqual(inspect.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(inspect.get_annotations(wrapped, eval_str=False), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) - - # test that local namespace lookups work - self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations), {'x': 'mytype'}) - self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {'x': int}) - class TestFormatAnnotation(unittest.TestCase): def test_typing_replacement(self): From 278de22c69d81520ec12976ebdd3ae2851b11f78 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 22:09:42 -0700 Subject: [PATCH 075/105] Regen globals --- Include/internal/pycore_global_objects_fini_generated.h | 1 - Include/internal/pycore_global_strings.h | 1 - Include/internal/pycore_runtime_init_generated.h | 1 - Include/internal/pycore_unicodeobject_generated.h | 3 --- 4 files changed, 6 deletions(-) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index b9480728869b36..474d8205e95ec4 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -747,7 +747,6 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_abstract_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_active)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_align_)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_annotation)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_anonymous_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_argtypes_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_as_parameter_)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index d0821a6b19cc03..373e2ce8037ed7 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -236,7 +236,6 @@ struct _Py_global_strings { STRUCT_FOR_ID(_abstract_) STRUCT_FOR_ID(_active) STRUCT_FOR_ID(_align_) - STRUCT_FOR_ID(_annotation) STRUCT_FOR_ID(_anonymous_) STRUCT_FOR_ID(_argtypes_) STRUCT_FOR_ID(_as_parameter_) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index a0ee110bbfcba4..598610b6ab4def 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -745,7 +745,6 @@ extern "C" { INIT_ID(_abstract_), \ INIT_ID(_active), \ INIT_ID(_align_), \ - INIT_ID(_annotation), \ INIT_ID(_anonymous_), \ INIT_ID(_argtypes_), \ INIT_ID(_as_parameter_), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 892f580e8a6846..568474f06e0747 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -546,9 +546,6 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { string = &_Py_ID(_align_); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); - string = &_Py_ID(_annotation); - assert(_PyUnicode_CheckConsistency(string, 1)); - _PyUnicode_InternInPlace(interp, &string); string = &_Py_ID(_anonymous_); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); From 921534683b52bf062a5e0a25c637cb638709bf6f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 May 2024 22:13:32 -0700 Subject: [PATCH 076/105] Fix test --- Lib/test/test_type_annotations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 784c934643b6f4..8e3070b15dee96 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -395,5 +395,5 @@ def f(x: int) -> int: pass f = ns["f"] self.assertIsInstance(f.__annotate__, types.FunctionType) annos = {"x": "int", "return": "int"} - self.assertEqual(f.__annotate__(inspect.VALUE), annos) + self.assertEqual(f.__annotate__(annotations.Format.VALUE), annos) self.assertEqual(f.__annotations__, annos) From 11a96f9a6154f733b56daecdcdf3e8409baf77c4 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 07:30:04 -0700 Subject: [PATCH 077/105] fix doctest --- Doc/howto/descriptor.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 51f9f4a6556e57..9cd3077eb2fb4c 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1366,6 +1366,10 @@ Using the non-data descriptor protocol, a pure Python version of def __call__(self, *args, **kwds): return self.f(*args, **kwds) + @property + def __annotations__(self): + return self.f.__annotations__ + The :func:`functools.update_wrapper` call adds a ``__wrapped__`` attribute that refers to the underlying function. Also it carries forward the attributes necessary to make the wrapper look like the wrapped From af5231c999e35af7a97f2398c285d6f060f356a6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 16:07:55 -0700 Subject: [PATCH 078/105] evaluate_forward_ref() --- Lib/annotations.py | 65 ++++++++++++++++++++++++------- Lib/test/test_typing.py | 12 ++---- Lib/typing.py | 84 +++++++++++++++++++++++++---------------- 3 files changed, 107 insertions(+), 54 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index 6d95f7261f984f..12e9b11d603aa2 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -14,6 +14,7 @@ class Format(enum.IntEnum): _Union = None +_sentinel = object() class ForwardRef: @@ -36,12 +37,12 @@ class ForwardRef: def __init__( self, arg, - is_argument=True, - module=None, *, + module=None, + owner=None, + is_argument=True, is_class=False, _globals=None, - _owner=None, _cell=None, ): if not isinstance(arg, str): @@ -56,15 +57,15 @@ def __init__( self._forward_code = None self._globals = _globals self._cell = _cell - self._owner = _owner + self._owner = owner def __init_subclass__(cls, /, *args, **kwds): raise TypeError("Cannot subclass ForwardRef") - def evaluate(self, *, globals=None, locals=None): + def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None): """Evaluate the forward reference and return the value. - If the forward reference is not evaluatable, raise a SyntaxError. + If the forward reference is not evaluatable, raise an exception. """ if self.__forward_evaluated__: return self.__forward_value__ @@ -77,28 +78,64 @@ def evaluate(self, *, globals=None, locals=None): self.__forward_evaluated__ = True self.__forward_value__ = value return value + if owner is None: + owner = self._owner + if owner is None and type_params is None: + raise TypeError("Either 'owner' or 'type_params' must be provided") - code = self.__forward_code__ if globals is None: globals = self._globals if globals is None: - globals = {} + if isinstance(owner, type): + module_name = getattr(owner, "__module__", None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + globals = getattr(module, "__dict__", None) + elif isinstance(owner, types.ModuleType): + globals = getattr(owner, "__dict__", None) + elif callable(owner): + globals = getattr(owner, "__globals__", None) + if self.__forward_module__ is not None: + globals = getattr( + sys.modules.get(self.__forward_module__, None), '__dict__', globals + ) + if locals is None: locals = {} if isinstance(self._owner, type): locals.update(vars(self._owner)) - if self._owner is not None: + + if type_params is None and self._owner is not None: # "Inject" type parameters into the local namespace # (unless they are shadowed by assignments *in* the local namespace), # as a way of emulating annotation scopes when calling `eval()` type_params = getattr(self._owner, "__type_params__", None) - if type_params: - locals = {param.__name__: param for param in type_params} | locals + if type_params is not None: + locals = {param.__name__: param for param in type_params} | locals + + code = self.__forward_code__ value = eval(code, globals=globals, locals=locals) self.__forward_evaluated__ = True self.__forward_value__ = value return value + def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard): + import typing + import warnings + + if type_params is _sentinel: + typing._deprecation_warning_for_no_type_params_passed("typing.ForwardRef._evaluate") + type_params = () + warnings._deprecated( + "ForwardRef._evaluate", + "{name} is a private API and is retained for compatibility, but will be removed" + " in Python 3.16. Use ForwardRef.evaluate() or typing.evaluate_forward_ref() instead.", + remove=(3, 16), + ) + return typing.evaluate_forward_ref(self, globals=globalns, locals=localns, type_params=type_params, + _recursive_guard=recursive_guard) + @property def __forward_code__(self): if self._forward_code is not None: @@ -163,7 +200,7 @@ def __init__(self, namespace, globals, owner, is_class): def __missing__(self, key): return ForwardRef( - key, _globals=self.globals, _owner=self.owner, is_class=self.is_class + key, _globals=self.globals, owner=self.owner, is_class=self.is_class ) @@ -287,7 +324,7 @@ def call_annotate_function(annotate, format, owner=None): fwdref = ForwardRef( name, _cell=cell, - _owner=owner, + owner=owner, _globals=annotate.__globals__, is_class=is_class, ) @@ -366,7 +403,7 @@ def get_annotations( annotate = getattr(obj, "__annotate__", None) # TODO remove format != VALUE condition if annotate is not None and format != Format.VALUE: - ann = call_annotate_function(annotate, format) + ann = call_annotate_function(annotate, format, owner=obj) elif isinstance(obj, type): # class ann = getattr(obj, '__annotations__', None) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index ea53c4156a9e20..a8899cd3a2fb8a 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -5982,9 +5982,7 @@ def fun(x: a): return a self.assertEqual(namespace1(), namespace1()) - # TODO(PEP 649): Used to be assertNotEqual because get_type_hints() - # would mutate the ForwardRef objects. Do we need to preserve this test? - self.assertEqual(namespace1(), namespace2()) + self.assertNotEqual(namespace1(), namespace2()) def test_forward_repr(self): self.assertEqual(repr(List['int']), "typing.List[ForwardRef('int')]") @@ -6049,9 +6047,7 @@ def cmp(o1, o2): r1 = namespace1() r2 = namespace2() self.assertIsNot(r1, r2) - self.assertEqual(r1, r2) - # TODO(PEP 649): do we need to preserve this test somehow? - # self.assertRaises(RecursionError, cmp, r1, r2) + self.assertRaises(RecursionError, cmp, r1, r2) def test_union_forward_recursion(self): ValueList = List['Value'] @@ -6350,10 +6346,10 @@ def test_deprecation_for_no_type_params_passed_to__evaluate(self): DeprecationWarning, ( "Failing to pass a value to the 'type_params' parameter " - "of 'typing._evaluate_forward_ref' is deprecated" + "of 'typing.ForwardRef._evaluate' is deprecated" ) ) as cm: - self.assertIs(typing._evaluate_forward_ref(f, globals(), {}, recursive_guard=frozenset()), int) + self.assertIs(f._evaluate(globals(), {}, recursive_guard=frozenset()), int) self.assertEqual(cm.filename, __file__) diff --git a/Lib/typing.py b/Lib/typing.py index e72130289e939b..00bedc2aabbdea 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -127,6 +127,7 @@ 'cast', 'clear_overloads', 'dataclass_transform', + 'evaluate_forward_ref', 'final', 'get_args', 'get_origin', @@ -462,7 +463,7 @@ def __repr__(self): def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=frozenset(), - format=annotations.Format.VALUE): + format=annotations.Format.VALUE, owner=None): """Evaluate all forward references in the given type t. For use of globalns and localns see the docstring for get_type_hints(). @@ -473,8 +474,9 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f _deprecation_warning_for_no_type_params_passed("typing._eval_type") type_params = () if isinstance(t, ForwardRef): - return _evaluate_forward_ref(t, globalns, localns, type_params, - recursive_guard=recursive_guard, format=format) + return evaluate_forward_ref(t, globals=globalns, locals=localns, + type_params=type_params, owner=owner, + _recursive_guard=recursive_guard, format=format) if isinstance(t, (_GenericAlias, GenericAlias, types.UnionType)): if isinstance(t, GenericAlias): args = tuple( @@ -492,7 +494,7 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f ev_args = tuple( _eval_type( a, globalns, localns, type_params, recursive_guard=recursive_guard, - format=format + format=format, owner=owner, ) for a in t.__args__ ) @@ -1023,37 +1025,54 @@ def _make_forward_ref(code, **kwargs): return forward_ref -def _evaluate_forward_ref(forward_ref, globalns, localns, type_params=_sentinel, *, - recursive_guard, format=annotations.Format.VALUE): +def evaluate_forward_ref( + forward_ref, + *, + owner=None, + globals=None, + locals=None, + type_params=None, + format=annotations.Format.VALUE, + _recursive_guard=frozenset(), +): + """Evaluate a forward reference as a type hint. + + This is similar to calling the ForwardRef.evaluate() method, + but unlike that method, evaluate_forward_ref() also: + * Recursively evaluates forward references nested within the type hint. + * Rejects certain objects that are not valid type hints. + * Replaces type hints that evaluate to None with types.NoneType. + * Supports the *FORWARDREF* and *SOURCE* formats. + + *forward_ref* must be an instance of ForwardRef. *owner*, if given, + should be the object that holds the annotations that the forward reference + derived from, such as a module, class object, or function. It is used to + infer the namespaces to use for looking up names. *globals* and *locals* + can also be explicitly given to provide the global and local namespaces. + *type_params* is a tuple of type parameters that are in scope when + evaluating the forward reference. This parameter must be provided (though + it may be an empty tuple) if *owner* is not given and the forward reference + does not already have an owner set. *format* specifies the format of the + annotation and is a member of the annoations.Format enum. + + """ if type_params is _sentinel: - _deprecation_warning_for_no_type_params_passed("typing._evaluate_forward_ref") + _deprecation_warning_for_no_type_params_passed("typing.evaluate_forward_ref") type_params = () - if forward_ref.__forward_arg__ in recursive_guard: + if format == annotations.Format.SOURCE: + return forward_ref.__forward_arg__ + if forward_ref.__forward_arg__ in _recursive_guard: return forward_ref - if globalns is None and localns is None: - globalns = localns = {} - elif globalns is None: - globalns = localns - elif localns is None: - localns = globalns - if forward_ref.__forward_module__ is not None: - globalns = getattr( - sys.modules.get(forward_ref.__forward_module__, None), '__dict__', globalns - ) - if type_params: - # "Inject" type parameters into the local namespace - # (unless they are shadowed by assignments *in* the local namespace), - # as a way of emulating annotation scopes when calling `eval()` - locals_to_pass = {param.__name__: param for param in type_params} | localns - else: - locals_to_pass = localns + try: - value = eval(forward_ref.__forward_code__, globalns, locals_to_pass) + value = forward_ref.evaluate(globals=globals, locals=locals, + type_params=type_params, owner=owner) except NameError: - if format is annotations.Format.FORWARDREF: + if format == annotations.Format.FORWARDREF: return forward_ref else: raise + type_ = _type_check( value, "Forward references must evaluate to types.", @@ -1062,11 +1081,12 @@ def _evaluate_forward_ref(forward_ref, globalns, localns, type_params=_sentinel, ) return _eval_type( type_, - globalns, - localns, + globals, + locals, type_params, - recursive_guard=(recursive_guard | {forward_ref.__forward_arg__}), + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, format=format, + owner=owner, ) @@ -2393,7 +2413,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, if isinstance(value, str): value = _make_forward_ref(value, is_argument=False, is_class=True) value = _eval_type(value, base_globals, base_locals, base.__type_params__, - format=format) + format=format, owner=obj) hints[name] = value if include_extras or format is annotations.Format.SOURCE: return hints @@ -2430,7 +2450,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, is_argument=not isinstance(obj, types.ModuleType), is_class=False, ) - hints[name] = _eval_type(value, globalns, localns, type_params, format=format) + hints[name] = _eval_type(value, globalns, localns, type_params, format=format, owner=obj) return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()} From e525f0105e6f96fadd0691b04aefab77300b7d2e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 16:56:20 -0700 Subject: [PATCH 079/105] Finish get_annotations() and extend tests --- Lib/annotations.py | 24 +++-- Lib/test/test_annotations.py | 173 ++++++++++++++++++++++------------- 2 files changed, 125 insertions(+), 72 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index 12e9b11d603aa2..30c75c164ae4db 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -400,13 +400,23 @@ def get_annotations( although if obj is a wrapped function (using functools.update_wrapper()) it is first unwrapped. """ + if eval_str and format != Format.VALUE: + raise ValueError("eval_str=True is only supported with format=Format.VALUE") + annotate = getattr(obj, "__annotate__", None) - # TODO remove format != VALUE condition - if annotate is not None and format != Format.VALUE: + if annotate is not None: ann = call_annotate_function(annotate, format, owner=obj) - elif isinstance(obj, type): + if not isinstance(ann, dict): + raise ValueError(f"{obj!r}.__annotate__ returned a non-dict") + if not eval_str: + return dict(ann) + else: + ann = None + + if isinstance(obj, type): # class - ann = getattr(obj, '__annotations__', None) + if ann is None: + ann = getattr(obj, '__annotations__', None) obj_globals = None module_name = getattr(obj, "__module__", None) @@ -418,7 +428,8 @@ def get_annotations( unwrap = obj elif isinstance(obj, types.ModuleType): # module - ann = getattr(obj, "__annotations__", None) + if ann is None: + ann = getattr(obj, "__annotations__", None) obj_globals = getattr(obj, "__dict__") obj_locals = None unwrap = None @@ -426,7 +437,8 @@ def get_annotations( # this includes types.Function, types.BuiltinFunctionType, # types.BuiltinMethodType, functools.partial, functools.singledispatch, # "class funclike" from Lib/test/test_inspect... on and on it goes. - ann = getattr(obj, "__annotations__", None) + if ann is None: + ann = getattr(obj, "__annotations__", None) obj_globals = getattr(obj, "__globals__", None) obj_locals = None unwrap = obj diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index c41891e0760faa..16e6962479360c 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -9,6 +9,12 @@ from test.test_inspect import inspect_stringized_annotations from test.test_inspect import inspect_stringized_annotations_2 +def times_three(fn): + @functools.wraps(fn) + def wrapper(a, b): + return fn(a*3, b*3) + return wrapper + class TestFormat(unittest.TestCase): def test_enum(self): @@ -82,94 +88,127 @@ def __init__(self, x: int = 0, y: str = ""): self.assertEqual(annotations.get_annotations(C()), {"x": int, "y": str}) - def test_get_annotations_with_stock_annotations(self): + def test_custom_format_eval_str(self): + def foo(): pass + with self.assertRaises(ValueError): + annotations.get_annotations(foo, format=annotations.Format.FORWARDREF, eval_str=True) + annotations.get_annotations(foo, format=annotations.Format.SOURCE, eval_str=True) + + def test_stock_annotations(self): def foo(a:int, b:str): pass - self.assertEqual(annotations.get_annotations(foo), {'a': int, 'b': str}) + for format in (annotations.Format.VALUE, annotations.Format.FORWARDREF): + with self.subTest(format=format): + self.assertEqual(annotations.get_annotations(foo, format=format), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(foo, format=annotations.Format.SOURCE), {'a': 'int', 'b': 'str'}) foo.__annotations__ = {'a': 'foo', 'b':'str'} - self.assertEqual(annotations.get_annotations(foo), {'a': 'foo', 'b': 'str'}) + for format in annotations.Format: + with self.subTest(format=format): + self.assertEqual(annotations.get_annotations(foo, format=format), {'a': 'foo', 'b': 'str'}) self.assertEqual(annotations.get_annotations(foo, eval_str=True, locals=locals()), {'a': foo, 'b': str}) self.assertEqual(annotations.get_annotations(foo, eval_str=True, globals=locals()), {'a': foo, 'b': str}) + def test_stock_annotations_in_module(self): + isa = inspect_stock_annotations + + for kwargs in [ + {}, + {'eval_str': False}, + {'format': annotations.Format.VALUE}, + {'format': annotations.Format.FORWARDREF}, + {'format': annotations.Format.VALUE, 'eval_str': False}, + {'format': annotations.Format.FORWARDREF, 'eval_str': False}, + ]: + with self.subTest(**kwargs): + self.assertEqual(annotations.get_annotations(isa, **kwargs), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.MyClass, **kwargs), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.function, **kwargs), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function2, **kwargs), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function3, **kwargs), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) + self.assertEqual(annotations.get_annotations(annotations, **kwargs), {}) # annotations module has no annotations + self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, **kwargs), {}) + self.assertEqual(annotations.get_annotations(isa.unannotated_function, **kwargs), {}) + + for kwargs in [ + {"eval_str": True}, + {"format": annotations.Format.VALUE, "eval_str": True}, + ]: + with self.subTest(**kwargs): + self.assertEqual(annotations.get_annotations(isa, **kwargs), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.MyClass, **kwargs), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.function, **kwargs), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function2, **kwargs), {'a': int, 'b': str, 'c': isa.MyClass, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function3, **kwargs), {'a': int, 'b': str, 'c': isa.MyClass}) + self.assertEqual(annotations.get_annotations(annotations, **kwargs), {}) + self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, **kwargs), {}) + self.assertEqual(annotations.get_annotations(isa.unannotated_function, **kwargs), {}) + + self.assertEqual(annotations.get_annotations(isa, format=annotations.Format.SOURCE), {'a': 'int', 'b': 'str'}) + self.assertEqual(annotations.get_annotations(isa.MyClass, format=annotations.Format.SOURCE), {'a': 'int', 'b': 'str'}) + self.assertEqual(annotations.get_annotations(isa.function, format=annotations.Format.SOURCE), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) + self.assertEqual(annotations.get_annotations(isa.function2, format=annotations.Format.SOURCE), {'a': 'int', 'b': 'str', 'c': 'MyClass', 'return': 'MyClass'}) + self.assertEqual(annotations.get_annotations(isa.function3, format=annotations.Format.SOURCE), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) + self.assertEqual(annotations.get_annotations(annotations, format=annotations.Format.SOURCE), {}) + self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, format=annotations.Format.SOURCE), {}) + self.assertEqual(annotations.get_annotations(isa.unannotated_function, format=annotations.Format.SOURCE), {}) + + def test_stock_annotations_on_wrapper(self): isa = inspect_stock_annotations - self.assertEqual(annotations.get_annotations(isa), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.MyClass), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.function), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function2), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function3), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) - self.assertEqual(annotations.get_annotations(annotations), {}) # annotations module has no annotations - self.assertEqual(annotations.get_annotations(isa.UnannotatedClass), {}) - self.assertEqual(annotations.get_annotations(isa.unannotated_function), {}) - - self.assertEqual(annotations.get_annotations(isa, eval_str=True), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.MyClass, eval_str=True), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.function, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function2, eval_str=True), {'a': int, 'b': str, 'c': isa.MyClass, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function3, eval_str=True), {'a': int, 'b': str, 'c': isa.MyClass}) - self.assertEqual(annotations.get_annotations(annotations, eval_str=True), {}) - self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, eval_str=True), {}) - self.assertEqual(annotations.get_annotations(isa.unannotated_function, eval_str=True), {}) - - self.assertEqual(annotations.get_annotations(isa, eval_str=False), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.MyClass, eval_str=False), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.function, eval_str=False), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function2, eval_str=False), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function3, eval_str=False), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) - self.assertEqual(annotations.get_annotations(annotations, eval_str=False), {}) - self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, eval_str=False), {}) - self.assertEqual(annotations.get_annotations(isa.unannotated_function, eval_str=False), {}) - - def times_three(fn): - @functools.wraps(fn) - def wrapper(a, b): - return fn(a*3, b*3) - return wrapper wrapped = times_three(isa.function) self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx')) self.assertIsNot(wrapped.__globals__, isa.function.__globals__) self.assertEqual(annotations.get_annotations(wrapped), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(wrapped, format=annotations.Format.FORWARDREF), + {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(wrapped, format=annotations.Format.SOURCE), + {'a': 'int', 'b': 'str', 'return': 'MyClass'}) self.assertEqual(annotations.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) self.assertEqual(annotations.get_annotations(wrapped, eval_str=False), {'a': int, 'b': str, 'return': isa.MyClass}) - def test_get_annotations_with_stringized_annotations(self): + def test_stringized_annotations_in_module(self): isa = inspect_stringized_annotations - self.assertEqual(annotations.get_annotations(isa), {'a': 'int', 'b': 'str'}) - self.assertEqual(annotations.get_annotations(isa.MyClass), {'a': 'int', 'b': 'str'}) - self.assertEqual(annotations.get_annotations(isa.function), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) - self.assertEqual(annotations.get_annotations(isa.function2), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'}) - self.assertEqual(annotations.get_annotations(isa.function3), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"}) - self.assertEqual(annotations.get_annotations(isa.UnannotatedClass), {}) - self.assertEqual(annotations.get_annotations(isa.unannotated_function), {}) - - self.assertEqual(annotations.get_annotations(isa, eval_str=True), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.MyClass, eval_str=True), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.function, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function2, eval_str=True), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function3, eval_str=True), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) - self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, eval_str=True), {}) - self.assertEqual(annotations.get_annotations(isa.unannotated_function, eval_str=True), {}) - - self.assertEqual(annotations.get_annotations(isa, eval_str=False), {'a': 'int', 'b': 'str'}) - self.assertEqual(annotations.get_annotations(isa.MyClass, eval_str=False), {'a': 'int', 'b': 'str'}) - self.assertEqual(annotations.get_annotations(isa.function, eval_str=False), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) - self.assertEqual(annotations.get_annotations(isa.function2, eval_str=False), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'}) - self.assertEqual(annotations.get_annotations(isa.function3, eval_str=False), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"}) - self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, eval_str=False), {}) - self.assertEqual(annotations.get_annotations(isa.unannotated_function, eval_str=False), {}) - + for kwargs in [ + {}, + {'eval_str': False}, + {'format': annotations.Format.VALUE}, + {'format': annotations.Format.FORWARDREF}, + {'format': annotations.Format.SOURCE}, + {'format': annotations.Format.VALUE, 'eval_str': False}, + {'format': annotations.Format.FORWARDREF, 'eval_str': False}, + {'format': annotations.Format.SOURCE, 'eval_str': False}, + ]: + with self.subTest(**kwargs): + self.assertEqual(annotations.get_annotations(isa, **kwargs), {'a': 'int', 'b': 'str'}) + self.assertEqual(annotations.get_annotations(isa.MyClass, **kwargs), {'a': 'int', 'b': 'str'}) + self.assertEqual(annotations.get_annotations(isa.function, **kwargs), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) + self.assertEqual(annotations.get_annotations(isa.function2, **kwargs), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'}) + self.assertEqual(annotations.get_annotations(isa.function3, **kwargs), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"}) + self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, **kwargs), {}) + self.assertEqual(annotations.get_annotations(isa.unannotated_function, **kwargs), {}) + + for kwargs in [ + {"eval_str": True}, + {"format": annotations.Format.VALUE, "eval_str": True}, + ]: + with self.subTest(**kwargs): + self.assertEqual(annotations.get_annotations(isa, **kwargs), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.MyClass, **kwargs), {'a': int, 'b': str}) + self.assertEqual(annotations.get_annotations(isa.function, **kwargs), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function2, **kwargs), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) + self.assertEqual(annotations.get_annotations(isa.function3, **kwargs), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) + self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, **kwargs), {}) + self.assertEqual(annotations.get_annotations(isa.unannotated_function, **kwargs), {}) + + def test_stringized_annotations_in_empty_module(self): isa2 = inspect_stringized_annotations_2 self.assertEqual(annotations.get_annotations(isa2), {}) self.assertEqual(annotations.get_annotations(isa2, eval_str=True), {}) self.assertEqual(annotations.get_annotations(isa2, eval_str=False), {}) - def times_three(fn): - @functools.wraps(fn) - def wrapper(a, b): - return fn(a*3, b*3) - return wrapper - + def test_stringized_annotations_on_wrapper(self): + isa = inspect_stringized_annotations wrapped = times_three(isa.function) self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx')) self.assertIsNot(wrapped.__globals__, isa.function.__globals__) @@ -177,6 +216,8 @@ def wrapper(a, b): self.assertEqual(annotations.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) self.assertEqual(annotations.get_annotations(wrapped, eval_str=False), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) + def test_stringized_annotations_on_class(self): + isa = inspect_stringized_annotations # test that local namespace lookups work self.assertEqual(annotations.get_annotations(isa.MyClassWithLocalAnnotations), {'x': 'mytype'}) self.assertEqual(annotations.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {'x': int}) From eabea7494453360deeb4609c7273674481dfb05b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 17:37:06 -0700 Subject: [PATCH 080/105] Test and fix stringifier --- Lib/annotations.py | 24 +++++++---- Lib/test/test_annotations.py | 83 ++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index 30c75c164ae4db..a9ee0cd1b1e10a 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -182,6 +182,9 @@ def __ror__(self, other): from typing import Union as _Union return _Union[other, self] + def __getitem__(self, arg): + return types.GenericAlias(self, arg) + def __repr__(self): if self.__forward_module__ is None: module_repr = "" @@ -211,6 +214,12 @@ def __init__(self, node): def _convert(self, other): if isinstance(other, Stringifier): return other.node + elif isinstance(other, slice): + return ast.Slice( + lower=self._convert(other.start) if other.start is not None else None, + upper=self._convert(other.stop) if other.stop is not None else None, + step=self._convert(other.step) if other.step is not None else None, + ) else: return ast.Name(id=repr(other)) @@ -224,25 +233,25 @@ def binop(self, other): __sub__ = _make_binop(ast.Sub()) __mul__ = _make_binop(ast.Mult()) __matmul__ = _make_binop(ast.MatMult()) - __div__ = _make_binop(ast.Div()) + __truediv__ = _make_binop(ast.Div()) __mod__ = _make_binop(ast.Mod()) __lshift__ = _make_binop(ast.LShift()) __rshift__ = _make_binop(ast.RShift()) __or__ = _make_binop(ast.BitOr()) __xor__ = _make_binop(ast.BitXor()) - __and__ = _make_binop(ast.And()) + __and__ = _make_binop(ast.BitAnd()) __floordiv__ = _make_binop(ast.FloorDiv()) __pow__ = _make_binop(ast.Pow()) def _make_unary_op(op): def unary_op(self): - return Stringifier(ast.UnaryOp(self.node, op)) + return Stringifier(ast.UnaryOp(op, self.node)) return unary_op __invert__ = _make_unary_op(ast.Invert()) - __pos__ = _make_binop(ast.UAdd()) - __neg__ = _make_binop(ast.USub()) + __pos__ = _make_unary_op(ast.UAdd()) + __neg__ = _make_unary_op(ast.USub()) def __getitem__(self, other): if isinstance(other, tuple): @@ -268,10 +277,7 @@ def __call__(self, *args, **kwargs): ) def __iter__(self): - return self - - def __next__(self): - return Stringifier(ast.Starred(self.node)) + yield Stringifier(ast.Starred(self.node)) def __repr__(self): return ast.unparse(self.node) diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index 16e6962479360c..15626830ad342f 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -60,6 +60,89 @@ def f(x: int, y: doesntexist): self.assertEqual(fwdref.evaluate(globals={"doesntexist": 1}), 1) +class TestSourceFormat(unittest.TestCase): + def test_closure(self): + def inner(arg: x): + pass + + anno = annotations.get_annotations(inner, format=annotations.Format.SOURCE) + self.assertEqual(anno, {"arg": "x"}) + + def test_function(self): + def f(x: int, y: doesntexist): + pass + + anno = annotations.get_annotations(f, format=annotations.Format.SOURCE) + self.assertEqual(anno, {"x": "int", "y": "doesntexist"}) + + def test_expressions(self): + def f( + add: a + b, + sub: a + b, + mul: a * b, + matmul: a @ b, + truediv: a / b, + mod: a % b, + lshift: a << b, + rshift: a >> b, + or_: a | b, + xor: a ^ b, + and_: a & b, + floordiv: a // b, + pow_: a ** b, + invert: ~a, + neg: -a, + pos: +a, + getitem: a[b], + getattr: a.b, + call: a(b, *c, d=e), # **kwargs are not supported + *args: *a, + ): + pass + anno = annotations.get_annotations(f, format=annotations.Format.SOURCE) + self.assertEqual(anno, { + "add": "a + b", + "sub": "a + b", + "mul": "a * b", + "matmul": "a @ b", + "truediv": "a / b", + "mod": "a % b", + "lshift": "a << b", + "rshift": "a >> b", + "or_": "a | b", + "xor": "a ^ b", + "and_": "a & b", + "floordiv": "a // b", + "pow_": "a ** b", + "invert": "~a", + "neg": "-a", + "pos": "+a", + "getitem": "a[b]", + "getattr": "a.b", + "call": "a(b, *c, d=e)", + "args": "*a", + }) + + def test_nested_expressions(self): + def f( + nested: list[Annotated[set[int], "set of ints", 4j]], + set: {a + b}, # single element because order is not guaranteed + dict: {a + b: c + d, "key": e + g}, + list: [a, b, c], + tuple: (a, b, c), + slice: (a[b:c], a[b:c:d], a[:c], a[b:], a[:], a[::d], a[b::d]), + ): pass + anno = annotations.get_annotations(f, format=annotations.Format.SOURCE) + self.assertEqual(anno, { + "nested": "list[Annotated[set[int], 'set of ints', 4j]]", + "set": "{a + b}", + "dict": "{a + b: c + d, 'key': e + g}", + "list": "[a, b, c]", + "tuple": "(a, b, c)", + "slice": "(a[b:c], a[b:c:d], a[:c], a[b:], a[:], a[::d], a[b::d])", + }) + + class TestForwardRefClass(unittest.TestCase): def test_special_attrs(self): # Forward refs provide a different introspection API. __name__ and From 2dbe8ff282ad0f8dbc6b33ff0727be0bac0036cf Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 17:40:38 -0700 Subject: [PATCH 081/105] Closure support for SOURCE --- Lib/annotations.py | 20 ++++++++++++++------ Lib/test/test_annotations.py | 2 ++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index a9ee0cd1b1e10a..2ae510162ce57a 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -344,12 +344,20 @@ def call_annotate_function(annotate, format, owner=None): return func(Format.VALUE) elif format == Format.SOURCE: globals = _StringifierDict() - func = types.FunctionType( - annotate.__code__, - globals, - # TODO: also replace the closure with stringifiers - closure=annotate.__closure__, - ) + if annotate.__closure__: + freevars = annotate.__code__.co_freevars + new_closure = [] + for i, cell in enumerate(annotate.__closure__): + if i < len(freevars): + name = freevars[i] + else: + name = "__cell__" + fwdref = Stringifier(ast.Name(id=name)) + new_closure.append(types.CellType(fwdref)) + closure = tuple(new_closure) + else: + closure = None + func = types.FunctionType(annotate.__code__, globals, closure=closure) annos = func(Format.VALUE) return { key: val if isinstance(val, str) else repr(val) diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index 15626830ad342f..06cc7895542397 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -62,6 +62,8 @@ def f(x: int, y: doesntexist): class TestSourceFormat(unittest.TestCase): def test_closure(self): + x = 0 + def inner(arg: x): pass From 2b88471b7c143ead2f25c0c2e3939d67983d014c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 19:29:19 -0700 Subject: [PATCH 082/105] More stringifier fixes --- Lib/annotations.py | 25 +++++++++++++++++++++++++ Lib/test/test_annotations.py | 12 ++++++++++++ 2 files changed, 37 insertions(+) diff --git a/Lib/annotations.py b/Lib/annotations.py index 2ae510162ce57a..1163ff80e90844 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -243,6 +243,27 @@ def binop(self, other): __floordiv__ = _make_binop(ast.FloorDiv()) __pow__ = _make_binop(ast.Pow()) + def _make_compare(op): + def compare(self, other): + return Stringifier(ast.Compare(left=self.node, ops=[op], comparators=[self._convert(other)])) + + return compare + + __lt__ = _make_compare(ast.Lt()) + __le__ = _make_compare(ast.LtE()) + __eq__ = _make_compare(ast.Eq()) + __ne__ = _make_compare(ast.NotEq()) + __gt__ = _make_compare(ast.Gt()) + __ge__ = _make_compare(ast.GtE()) + + # Doesn't work because the return type is always coerced to a bool + # __contains__ = _make_compare(ast.In()) + + # Must implement this since we set __eq__. We hash by identity so that + # stringifiers in dict keys are kept separate. + def __hash__(self): + return id(self) + def _make_unary_op(op): def unary_op(self): return Stringifier(ast.UnaryOp(op, self.node)) @@ -254,6 +275,10 @@ def unary_op(self): __neg__ = _make_unary_op(ast.USub()) def __getitem__(self, other): + # Special case, to avoid stringifying references to class-scoped variables + # as '__classdict__["x"]'. + if isinstance(self.node, ast.Name) and self.node.id == "__classdict__": + raise KeyError if isinstance(other, tuple): elts = [self._convert(elt) for elt in other] other = ast.Tuple(elts) diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index 06cc7895542397..8cc1aab1e47b2e 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -92,6 +92,12 @@ def f( and_: a & b, floordiv: a // b, pow_: a ** b, + lt: a < b, + le: a <= b, + eq: a == b, + ne: a != b, + gt: a > b, + ge: a >= b, invert: ~a, neg: -a, pos: +a, @@ -116,6 +122,12 @@ def f( "and_": "a & b", "floordiv": "a // b", "pow_": "a ** b", + "lt": "a < b", + "le": "a <= b", + "eq": "a == b", + "ne": "a != b", + "gt": "a > b", + "ge": "a >= b", "invert": "~a", "neg": "-a", "pos": "+a", From 406d3bc4cc545fe0287dfb132c514639d621f7cf Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 19:52:47 -0700 Subject: [PATCH 083/105] del temporaries --- Lib/annotations.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/annotations.py b/Lib/annotations.py index 1163ff80e90844..ad4847d8ad0803 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -243,6 +243,8 @@ def binop(self, other): __floordiv__ = _make_binop(ast.FloorDiv()) __pow__ = _make_binop(ast.Pow()) + del _make_binop + def _make_compare(op): def compare(self, other): return Stringifier(ast.Compare(left=self.node, ops=[op], comparators=[self._convert(other)])) @@ -256,6 +258,8 @@ def compare(self, other): __gt__ = _make_compare(ast.Gt()) __ge__ = _make_compare(ast.GtE()) + del _make_compare + # Doesn't work because the return type is always coerced to a bool # __contains__ = _make_compare(ast.In()) @@ -274,6 +278,8 @@ def unary_op(self): __pos__ = _make_unary_op(ast.UAdd()) __neg__ = _make_unary_op(ast.USub()) + del _make_unary_op + def __getitem__(self, other): # Special case, to avoid stringifying references to class-scoped variables # as '__classdict__["x"]'. From ad91e5acb3c5d04d3f0fce82c348ab28443fa00d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 20:30:18 -0700 Subject: [PATCH 084/105] don't look --- Lib/annotations.py | 187 ++++++++++++++++++++++++++------------------- 1 file changed, 110 insertions(+), 77 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index ad4847d8ad0803..813f638fbe4f93 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -15,24 +15,26 @@ class Format(enum.IntEnum): _Union = None _sentinel = object() +_slots = ( + "__forward_evaluated__", + "__forward_value__", + "__forward_is_argument__", + "__forward_is_class__", + "__forward_module__", + "__weakref__", + "__arg__", + "__ast_node__", + "__code__", + "__globals__", + "__owner__", + "__cell__", +) class ForwardRef: """Internal wrapper to hold a forward reference.""" - __slots__ = ( - "__forward_arg__", - "__forward_evaluated__", - "__forward_value__", - "__forward_is_argument__", - "__forward_is_class__", - "__forward_module__", - "__weakref__", - "_forward_code", - "_globals", - "_owner", - "_cell", - ) + __slots__ = _slots def __init__( self, @@ -48,16 +50,17 @@ def __init__( if not isinstance(arg, str): raise TypeError(f"Forward reference must be a string -- got {arg!r}") - self.__forward_arg__ = arg + self.__arg__ = arg self.__forward_evaluated__ = False self.__forward_value__ = None self.__forward_is_argument__ = is_argument self.__forward_is_class__ = is_class self.__forward_module__ = module - self._forward_code = None - self._globals = _globals - self._cell = _cell - self._owner = owner + self.__code__ = None + self.__ast_node__ = None + self.__globals__ = _globals + self.__cell__ = _cell + self.__owner__ = owner def __init_subclass__(cls, /, *args, **kwds): raise TypeError("Cannot subclass ForwardRef") @@ -69,9 +72,9 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None): """ if self.__forward_evaluated__: return self.__forward_value__ - if self._cell is not None: + if self.__cell__ is not None: try: - value = self._cell.cell_contents + value = self.__cell__.cell_contents except ValueError: pass else: @@ -79,12 +82,12 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None): self.__forward_value__ = value return value if owner is None: - owner = self._owner + owner = self.__owner__ if owner is None and type_params is None: raise TypeError("Either 'owner' or 'type_params' must be provided") if globals is None: - globals = self._globals + globals = self.__globals__ if globals is None: if isinstance(owner, type): module_name = getattr(owner, "__module__", None) @@ -103,14 +106,14 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None): if locals is None: locals = {} - if isinstance(self._owner, type): - locals.update(vars(self._owner)) + if isinstance(self.__owner__, type): + locals.update(vars(self.__owner__)) - if type_params is None and self._owner is not None: + if type_params is None and self.__owner__ is not None: # "Inject" type parameters into the local namespace # (unless they are shadowed by assignments *in* the local namespace), # as a way of emulating annotation scopes when calling `eval()` - type_params = getattr(self._owner, "__type_params__", None) + type_params = getattr(self.__owner__, "__type_params__", None) if type_params is not None: locals = {param.__name__: param for param in type_params} | locals @@ -136,10 +139,19 @@ def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard return typing.evaluate_forward_ref(self, globals=globalns, locals=localns, type_params=type_params, _recursive_guard=recursive_guard) + @property + def __forward_arg__(self): + if self.__arg__ is not None: + return self.__arg__ + if self.__ast_node__ is not None: + self.__arg__ = ast.unparse(self.__ast_node__) + return self.__arg__ + raise RuntimeError("Forward reference is not initialized") + @property def __forward_code__(self): - if self._forward_code is not None: - return self._forward_code + if self.__code__ is not None: + return self.__code__ arg = self.__forward_arg__ # If we do `def f(*args: *Ts)`, then we'll have `arg = '*Ts'`. # Unfortunately, this isn't a valid expression on its own, so we @@ -149,10 +161,10 @@ def __forward_code__(self): else: arg_to_compile = arg try: - self._forward_code = compile(arg_to_compile, "", "eval") + self.__code__ = compile(arg_to_compile, "", "eval") except SyntaxError: raise SyntaxError(f"Forward reference must be an expression -- got {arg!r}") - return self._forward_code + return self.__code__ def __eq__(self, other): if not isinstance(other, ForwardRef): @@ -193,39 +205,43 @@ def __repr__(self): return f"ForwardRef({self.__forward_arg__!r}{module_repr})" -class _ForwardReffer(dict): - def __init__(self, namespace, globals, owner, is_class): - super().__init__(namespace) - self.namespace = namespace - self.globals = globals - self.owner = owner - self.is_class = is_class - - def __missing__(self, key): - return ForwardRef( - key, _globals=self.globals, owner=self.owner, is_class=self.is_class - ) - - -class Stringifier: - def __init__(self, node): - self.node = node +class _Stringifier: + # Must match the slots on ForwardRef, so we can turn an instance of one into an + # instance of the other in place. + __slots__ = _slots - def _convert(self, other): - if isinstance(other, Stringifier): - return other.node + def __init__(self, node, globals=None, owner=None, is_class=False, cell=None): + assert isinstance(node, ast.AST) + self.__arg__ = None + self.__forward_evaluated__ = False + self.__forward_value__ = None + self.__forward_is_argument__ = False + self.__forward_is_class__ = is_class + self.__forward_module__ = None + self.__code__ = None + self.__ast_node__ = node + self.__globals__ = globals + self.__cell__ = cell + self.__owner__ = owner + + def __convert(self, other): + if isinstance(other, _Stringifier): + return other.__ast_node__ elif isinstance(other, slice): return ast.Slice( - lower=self._convert(other.start) if other.start is not None else None, - upper=self._convert(other.stop) if other.stop is not None else None, - step=self._convert(other.step) if other.step is not None else None, + lower=self.__convert(other.start) if other.start is not None else None, + upper=self.__convert(other.stop) if other.stop is not None else None, + step=self.__convert(other.step) if other.step is not None else None, ) else: - return ast.Name(id=repr(other)) + return ast.Constant(value=other) + + def __make_new(self, node): + return _Stringifier(node, self.__globals__, self.__owner__, self.__forward_is_class__) def _make_binop(op: ast.AST): def binop(self, other): - return Stringifier(ast.BinOp(self.node, op, self._convert(other))) + return self.__make_new(ast.BinOp(self.__ast_node__, op, self.__convert(other))) return binop @@ -247,7 +263,7 @@ def binop(self, other): def _make_compare(op): def compare(self, other): - return Stringifier(ast.Compare(left=self.node, ops=[op], comparators=[self._convert(other)])) + return self.__make_new(ast.Compare(left=self.__ast_node__, ops=[op], comparators=[self.__convert(other)])) return compare @@ -270,7 +286,7 @@ def __hash__(self): def _make_unary_op(op): def unary_op(self): - return Stringifier(ast.UnaryOp(op, self.node)) + return self.__make_new(ast.UnaryOp(op, self.__ast_node__)) return unary_op @@ -283,40 +299,53 @@ def unary_op(self): def __getitem__(self, other): # Special case, to avoid stringifying references to class-scoped variables # as '__classdict__["x"]'. - if isinstance(self.node, ast.Name) and self.node.id == "__classdict__": + if isinstance(self.__ast_node__, ast.Name) and self.__ast_node__.id == "__classdict__": raise KeyError if isinstance(other, tuple): - elts = [self._convert(elt) for elt in other] + elts = [self.__convert(elt) for elt in other] other = ast.Tuple(elts) else: - other = self._convert(other) - return Stringifier(ast.Subscript(self.node, other)) + other = self.__convert(other) + assert isinstance(other, ast.AST), repr(other) + return self.__make_new(ast.Subscript(self.__ast_node__, other)) def __getattr__(self, attr): - return Stringifier(ast.Attribute(self.node, attr)) + return self.__make_new(ast.Attribute(self.__ast_node__, attr)) def __call__(self, *args, **kwargs): - return Stringifier( + return self.__make_new( ast.Call( - self.node, - [self._convert(arg) for arg in args], + self.__ast_node__, + [self.__convert(arg) for arg in args], [ - ast.keyword(key, self._convert(value)) + ast.keyword(key, self.__convert(value)) for key, value in kwargs.items() ], ) ) def __iter__(self): - yield Stringifier(ast.Starred(self.node)) + yield self.__make_new(ast.Starred(self.__ast_node__)) def __repr__(self): - return ast.unparse(self.node) + return ast.unparse(self.__ast_node__) class _StringifierDict(dict): + def __init__(self, namespace, globals=None, owner=None, is_class=False): + super().__init__(namespace) + self.namespace = namespace + self.globals = globals + self.owner = owner + self.is_class = is_class + self.stringifiers = [] + def __missing__(self, key): - return Stringifier(ast.Name(key)) + fwdref = _Stringifier( + ast.Name(id=key), globals=self.globals, owner=self.owner, is_class=self.is_class + ) + self.stringifiers.append(fwdref) + return fwdref def call_annotate_function(annotate, format, owner=None): @@ -346,7 +375,7 @@ def call_annotate_function(annotate, format, owner=None): if format == Format.FORWARDREF: namespace = {**annotate.__builtins__, **annotate.__globals__} is_class = isinstance(owner, type) - globals = _ForwardReffer(namespace, annotate.__globals__, owner, is_class) + globals = _StringifierDict(namespace, annotate.__globals__, owner, is_class) if annotate.__closure__: freevars = annotate.__code__.co_freevars new_closure = [] @@ -358,13 +387,14 @@ def call_annotate_function(annotate, format, owner=None): name = freevars[i] else: name = "__cell__" - fwdref = ForwardRef( - name, - _cell=cell, + fwdref = _Stringifier( + ast.Name(id=name), + cell=cell, owner=owner, - _globals=annotate.__globals__, + globals=annotate.__globals__, is_class=is_class, ) + globals.stringifiers.append(fwdref) new_closure.append(types.CellType(fwdref)) else: new_closure.append(cell) @@ -372,9 +402,12 @@ def call_annotate_function(annotate, format, owner=None): else: closure = None func = types.FunctionType(annotate.__code__, globals, closure=closure) - return func(Format.VALUE) + result = func(Format.VALUE) + for obj in globals.stringifiers: + obj.__class__ = ForwardRef + return result elif format == Format.SOURCE: - globals = _StringifierDict() + globals = _StringifierDict({}) if annotate.__closure__: freevars = annotate.__code__.co_freevars new_closure = [] @@ -383,7 +416,7 @@ def call_annotate_function(annotate, format, owner=None): name = freevars[i] else: name = "__cell__" - fwdref = Stringifier(ast.Name(id=name)) + fwdref = _Stringifier(ast.Name(id=name)) new_closure.append(types.CellType(fwdref)) closure = tuple(new_closure) else: From d9a65798ece36807724b2f3b42b5073919227d04 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 20:45:36 -0700 Subject: [PATCH 085/105] but why? --- Lib/annotations.py | 74 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index 813f638fbe4f93..d7dfc142aa5c56 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -15,6 +15,10 @@ class Format(enum.IntEnum): _Union = None _sentinel = object() + +# Slots shared by ForwardRef and _Stringifier. The __forward__ names must be +# preserved for compatibility with the old typing.ForwardRef class. The remaining +# names are private. _slots = ( "__forward_evaluated__", "__forward_value__", @@ -372,7 +376,52 @@ def call_annotate_function(annotate, format, owner=None): return annotate(format) except NotImplementedError: pass - if format == Format.FORWARDREF: + if format == Format.SOURCE: + # SOURCE is implemented by calling the annotate function in a special + # environment where every name lookup results in an instance of _Stringifier. + # _Stringifier supports every dunder operation and returns a new _Stringifier. + # At the end, we get a dictionary that mostly contains _Stringifier objects (or + # possibly constants if the annotate function uses them directly). We then + # convert each of those into a string to get an approximation of the + # original source. + globals = _StringifierDict({}) + if annotate.__closure__: + freevars = annotate.__code__.co_freevars + new_closure = [] + for i, cell in enumerate(annotate.__closure__): + if i < len(freevars): + name = freevars[i] + else: + name = "__cell__" + fwdref = _Stringifier(ast.Name(id=name)) + new_closure.append(types.CellType(fwdref)) + closure = tuple(new_closure) + else: + closure = None + func = types.FunctionType(annotate.__code__, globals, closure=closure) + annos = func(Format.VALUE) + return { + key: val if isinstance(val, str) else repr(val) + for key, val in annos.items() + } + elif format == Format.FORWARDREF: + # FORWARDREF is implemented similarly to SOURCE, but there are two changes, + # at the beginning and the end of the process. + # First, while SOURCE uses an empty dictionary as the namespace, so that all + # name lookups result in _Stringifier objects, FORWARDREF uses the globals + # and builtins, so that defined names map to their real values. + # Second, instead of returning strings, we want to return either real values + # or ForwardRef objects. To do this, we keep track of all _Stringifier objects + # created while the annotation is being evaluated, and at the end we convert + # them all to ForwardRef objects by assigning to __class__. To make this + # technique work, we have to ensure that the _Stringifier and ForwardRef + # classes share the same attributes. + # We use this technique because while the annotations are being evaluated, + # we want to support all operations that the language allows, including even + # __getattr__ and __eq__, and return new _Stringifier objects so we can accurately + # reconstruct the source. But in the dictionary that we eventually return, we + # want to return objects with more user-friendly behavior, such as an __eq__ + # that returns a bool and an defined set of attributes. namespace = {**annotate.__builtins__, **annotate.__globals__} is_class = isinstance(owner, type) globals = _StringifierDict(namespace, annotate.__globals__, owner, is_class) @@ -406,28 +455,9 @@ def call_annotate_function(annotate, format, owner=None): for obj in globals.stringifiers: obj.__class__ = ForwardRef return result - elif format == Format.SOURCE: - globals = _StringifierDict({}) - if annotate.__closure__: - freevars = annotate.__code__.co_freevars - new_closure = [] - for i, cell in enumerate(annotate.__closure__): - if i < len(freevars): - name = freevars[i] - else: - name = "__cell__" - fwdref = _Stringifier(ast.Name(id=name)) - new_closure.append(types.CellType(fwdref)) - closure = tuple(new_closure) - else: - closure = None - func = types.FunctionType(annotate.__code__, globals, closure=closure) - annos = func(Format.VALUE) - return { - key: val if isinstance(val, str) else repr(val) - for key, val in annos.items() - } elif format == Format.VALUE: + # Should be impossible because __annotate__ functions must not raise + # NotImplementedError for this format. raise RuntimeError("annotate function does not support VALUE format") From 7c8bb1df5f6e2735acd587cc1977156b793ffe42 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 20:47:23 -0700 Subject: [PATCH 086/105] Update test_typing test This module overrides __annotations__ but not __annotate__. This previously made it so get_type_hints() couldn't see any annotations, but not any more. --- Lib/test/test_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index a8899cd3a2fb8a..5dc51c92263054 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6637,7 +6637,7 @@ def test_get_type_hints_from_various_objects(self): def test_get_type_hints_modules(self): ann_module_type_hints = {'f': Tuple[int, int], 'x': int, 'y': str, 'u': int | float} self.assertEqual(gth(ann_module), ann_module_type_hints) - self.assertEqual(gth(ann_module2), {}) + self.assertEqual(gth(ann_module2), {'i': int, 'j': int, 'x': float}) self.assertEqual(gth(ann_module3), {}) @skip("known bug") From 991798b87474e21517a91fd1912fbcf8ed6d12f8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 20:50:05 -0700 Subject: [PATCH 087/105] Reverse ops --- Lib/annotations.py | 22 ++++++++++++++++++++++ Lib/test/test_annotations.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/Lib/annotations.py b/Lib/annotations.py index d7dfc142aa5c56..949f6cd94f5b3b 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -265,6 +265,28 @@ def binop(self, other): del _make_binop + def _make_rbinop(op: ast.AST): + def rbinop(self, other): + return self.__make_new(ast.BinOp(self.__convert(other), op, self.__ast_node__)) + + return rbinop + + __radd__ = _make_rbinop(ast.Add()) + __rsub__ = _make_rbinop(ast.Sub()) + __rmul__ = _make_rbinop(ast.Mult()) + __rmatmul__ = _make_rbinop(ast.MatMult()) + __rtruediv__ = _make_rbinop(ast.Div()) + __rmod__ = _make_rbinop(ast.Mod()) + __rlshift__ = _make_rbinop(ast.LShift()) + __rrshift__ = _make_rbinop(ast.RShift()) + __ror__ = _make_rbinop(ast.BitOr()) + __rxor__ = _make_rbinop(ast.BitXor()) + __rand__ = _make_rbinop(ast.BitAnd()) + __rfloordiv__ = _make_rbinop(ast.FloorDiv()) + __rpow__ = _make_rbinop(ast.Pow()) + + del _make_rbinop + def _make_compare(op): def compare(self, other): return self.__make_new(ast.Compare(left=self.__ast_node__, ops=[op], comparators=[self.__convert(other)])) diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index 8cc1aab1e47b2e..bbceb37f443ce5 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -137,6 +137,41 @@ def f( "args": "*a", }) + def test_reverse_ops(self): + def f( + radd: 1 + a, + rsub: 1 - a, + rmul: 1 * a, + rmatmul: 1 @ a, + rtruediv: 1 / a, + rmod: 1 % a, + rlshift: 1 << a, + rrshift: 1 >> a, + ror: 1 | a, + rxor: 1 ^ a, + rand: 1 & a, + rfloordiv: 1 // a, + rpow: 1 ** a, + ): + pass + + anno = annotations.get_annotations(f, format=annotations.Format.SOURCE) + self.assertEqual(anno, { + "radd": "1 + a", + "rsub": "1 - a", + "rmul": "1 * a", + "rmatmul": "1 @ a", + "rtruediv": "1 / a", + "rmod": "1 % a", + "rlshift": "1 << a", + "rrshift": "1 >> a", + "ror": "1 | a", + "rxor": "1 ^ a", + "rand": "1 & a", + "rfloordiv": "1 // a", + "rpow": "1 ** a", + }) + def test_nested_expressions(self): def f( nested: list[Annotated[set[int], "set of ints", 4j]], From 0e609f83247f7a383bfc25cc866eaf8d3ca11afe Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 20:51:18 -0700 Subject: [PATCH 088/105] Reformat new module --- Lib/annotations.py | 47 +++- Lib/test/test_annotations.py | 433 ++++++++++++++++++++++++----------- 2 files changed, 338 insertions(+), 142 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index 949f6cd94f5b3b..4927c8632d2f11 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -105,7 +105,7 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None): globals = getattr(owner, "__globals__", None) if self.__forward_module__ is not None: globals = getattr( - sys.modules.get(self.__forward_module__, None), '__dict__', globals + sys.modules.get(self.__forward_module__, None), "__dict__", globals ) if locals is None: @@ -132,7 +132,9 @@ def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard import warnings if type_params is _sentinel: - typing._deprecation_warning_for_no_type_params_passed("typing.ForwardRef._evaluate") + typing._deprecation_warning_for_no_type_params_passed( + "typing.ForwardRef._evaluate" + ) type_params = () warnings._deprecated( "ForwardRef._evaluate", @@ -140,8 +142,13 @@ def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard " in Python 3.16. Use ForwardRef.evaluate() or typing.evaluate_forward_ref() instead.", remove=(3, 16), ) - return typing.evaluate_forward_ref(self, globals=globalns, locals=localns, type_params=type_params, - _recursive_guard=recursive_guard) + return typing.evaluate_forward_ref( + self, + globals=globalns, + locals=localns, + type_params=type_params, + _recursive_guard=recursive_guard, + ) @property def __forward_arg__(self): @@ -241,11 +248,15 @@ def __convert(self, other): return ast.Constant(value=other) def __make_new(self, node): - return _Stringifier(node, self.__globals__, self.__owner__, self.__forward_is_class__) + return _Stringifier( + node, self.__globals__, self.__owner__, self.__forward_is_class__ + ) def _make_binop(op: ast.AST): def binop(self, other): - return self.__make_new(ast.BinOp(self.__ast_node__, op, self.__convert(other))) + return self.__make_new( + ast.BinOp(self.__ast_node__, op, self.__convert(other)) + ) return binop @@ -267,7 +278,9 @@ def binop(self, other): def _make_rbinop(op: ast.AST): def rbinop(self, other): - return self.__make_new(ast.BinOp(self.__convert(other), op, self.__ast_node__)) + return self.__make_new( + ast.BinOp(self.__convert(other), op, self.__ast_node__) + ) return rbinop @@ -289,7 +302,13 @@ def rbinop(self, other): def _make_compare(op): def compare(self, other): - return self.__make_new(ast.Compare(left=self.__ast_node__, ops=[op], comparators=[self.__convert(other)])) + return self.__make_new( + ast.Compare( + left=self.__ast_node__, + ops=[op], + comparators=[self.__convert(other)], + ) + ) return compare @@ -325,7 +344,10 @@ def unary_op(self): def __getitem__(self, other): # Special case, to avoid stringifying references to class-scoped variables # as '__classdict__["x"]'. - if isinstance(self.__ast_node__, ast.Name) and self.__ast_node__.id == "__classdict__": + if ( + isinstance(self.__ast_node__, ast.Name) + and self.__ast_node__.id == "__classdict__" + ): raise KeyError if isinstance(other, tuple): elts = [self.__convert(elt) for elt in other] @@ -368,7 +390,10 @@ def __init__(self, namespace, globals=None, owner=None, is_class=False): def __missing__(self, key): fwdref = _Stringifier( - ast.Name(id=key), globals=self.globals, owner=self.owner, is_class=self.is_class + ast.Name(id=key), + globals=self.globals, + owner=self.owner, + is_class=self.is_class, ) self.stringifiers.append(fwdref) return fwdref @@ -546,7 +571,7 @@ def get_annotations( if isinstance(obj, type): # class if ann is None: - ann = getattr(obj, '__annotations__', None) + ann = getattr(obj, "__annotations__", None) obj_globals = None module_name = getattr(obj, "__module__", None) diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index bbceb37f443ce5..91dc0e7468be1c 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -9,10 +9,12 @@ from test.test_inspect import inspect_stringized_annotations from test.test_inspect import inspect_stringized_annotations_2 + def times_three(fn): @functools.wraps(fn) def wrapper(a, b): - return fn(a*3, b*3) + return fn(a * 3, b * 3) + return wrapper @@ -91,7 +93,7 @@ def f( xor: a ^ b, and_: a & b, floordiv: a // b, - pow_: a ** b, + pow_: a**b, lt: a < b, le: a <= b, eq: a == b, @@ -107,35 +109,39 @@ def f( *args: *a, ): pass + anno = annotations.get_annotations(f, format=annotations.Format.SOURCE) - self.assertEqual(anno, { - "add": "a + b", - "sub": "a + b", - "mul": "a * b", - "matmul": "a @ b", - "truediv": "a / b", - "mod": "a % b", - "lshift": "a << b", - "rshift": "a >> b", - "or_": "a | b", - "xor": "a ^ b", - "and_": "a & b", - "floordiv": "a // b", - "pow_": "a ** b", - "lt": "a < b", - "le": "a <= b", - "eq": "a == b", - "ne": "a != b", - "gt": "a > b", - "ge": "a >= b", - "invert": "~a", - "neg": "-a", - "pos": "+a", - "getitem": "a[b]", - "getattr": "a.b", - "call": "a(b, *c, d=e)", - "args": "*a", - }) + self.assertEqual( + anno, + { + "add": "a + b", + "sub": "a + b", + "mul": "a * b", + "matmul": "a @ b", + "truediv": "a / b", + "mod": "a % b", + "lshift": "a << b", + "rshift": "a >> b", + "or_": "a | b", + "xor": "a ^ b", + "and_": "a & b", + "floordiv": "a // b", + "pow_": "a ** b", + "lt": "a < b", + "le": "a <= b", + "eq": "a == b", + "ne": "a != b", + "gt": "a > b", + "ge": "a >= b", + "invert": "~a", + "neg": "-a", + "pos": "+a", + "getitem": "a[b]", + "getattr": "a.b", + "call": "a(b, *c, d=e)", + "args": "*a", + }, + ) def test_reverse_ops(self): def f( @@ -151,26 +157,29 @@ def f( rxor: 1 ^ a, rand: 1 & a, rfloordiv: 1 // a, - rpow: 1 ** a, + rpow: 1**a, ): pass anno = annotations.get_annotations(f, format=annotations.Format.SOURCE) - self.assertEqual(anno, { - "radd": "1 + a", - "rsub": "1 - a", - "rmul": "1 * a", - "rmatmul": "1 @ a", - "rtruediv": "1 / a", - "rmod": "1 % a", - "rlshift": "1 << a", - "rrshift": "1 >> a", - "ror": "1 | a", - "rxor": "1 ^ a", - "rand": "1 & a", - "rfloordiv": "1 // a", - "rpow": "1 ** a", - }) + self.assertEqual( + anno, + { + "radd": "1 + a", + "rsub": "1 - a", + "rmul": "1 * a", + "rmatmul": "1 @ a", + "rtruediv": "1 / a", + "rmod": "1 % a", + "rlshift": "1 << a", + "rrshift": "1 >> a", + "ror": "1 | a", + "rxor": "1 ^ a", + "rand": "1 & a", + "rfloordiv": "1 // a", + "rpow": "1 ** a", + }, + ) def test_nested_expressions(self): def f( @@ -180,16 +189,21 @@ def f( list: [a, b, c], tuple: (a, b, c), slice: (a[b:c], a[b:c:d], a[:c], a[b:], a[:], a[::d], a[b::d]), - ): pass + ): + pass + anno = annotations.get_annotations(f, format=annotations.Format.SOURCE) - self.assertEqual(anno, { - "nested": "list[Annotated[set[int], 'set of ints', 4j]]", - "set": "{a + b}", - "dict": "{a + b: c + d, 'key': e + g}", - "list": "[a, b, c]", - "tuple": "(a, b, c)", - "slice": "(a[b:c], a[b:c:d], a[:c], a[b:], a[:], a[::d], a[b::d])", - }) + self.assertEqual( + anno, + { + "nested": "list[Annotated[set[int], 'set of ints', 4j]]", + "set": "{a + b}", + "dict": "{a + b: c + d, 'key': e + g}", + "list": "[a, b, c]", + "tuple": "(a, b, c)", + "slice": "(a[b:c], a[b:c:d], a[:c], a[b:], a[:], a[::d], a[b::d])", + }, + ) class TestForwardRefClass(unittest.TestCase): @@ -221,117 +235,259 @@ def __init__(self, x: int = 0, y: str = ""): self.assertEqual(annotations.get_annotations(C()), {"x": int, "y": str}) def test_custom_format_eval_str(self): - def foo(): pass + def foo(): + pass + with self.assertRaises(ValueError): - annotations.get_annotations(foo, format=annotations.Format.FORWARDREF, eval_str=True) - annotations.get_annotations(foo, format=annotations.Format.SOURCE, eval_str=True) + annotations.get_annotations( + foo, format=annotations.Format.FORWARDREF, eval_str=True + ) + annotations.get_annotations( + foo, format=annotations.Format.SOURCE, eval_str=True + ) def test_stock_annotations(self): - def foo(a:int, b:str): pass + def foo(a: int, b: str): + pass + for format in (annotations.Format.VALUE, annotations.Format.FORWARDREF): with self.subTest(format=format): - self.assertEqual(annotations.get_annotations(foo, format=format), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(foo, format=annotations.Format.SOURCE), {'a': 'int', 'b': 'str'}) - - foo.__annotations__ = {'a': 'foo', 'b':'str'} + self.assertEqual( + annotations.get_annotations(foo, format=format), + {"a": int, "b": str}, + ) + self.assertEqual( + annotations.get_annotations(foo, format=annotations.Format.SOURCE), + {"a": "int", "b": "str"}, + ) + + foo.__annotations__ = {"a": "foo", "b": "str"} for format in annotations.Format: with self.subTest(format=format): - self.assertEqual(annotations.get_annotations(foo, format=format), {'a': 'foo', 'b': 'str'}) - - self.assertEqual(annotations.get_annotations(foo, eval_str=True, locals=locals()), {'a': foo, 'b': str}) - self.assertEqual(annotations.get_annotations(foo, eval_str=True, globals=locals()), {'a': foo, 'b': str}) + self.assertEqual( + annotations.get_annotations(foo, format=format), + {"a": "foo", "b": "str"}, + ) + + self.assertEqual( + annotations.get_annotations(foo, eval_str=True, locals=locals()), + {"a": foo, "b": str}, + ) + self.assertEqual( + annotations.get_annotations(foo, eval_str=True, globals=locals()), + {"a": foo, "b": str}, + ) def test_stock_annotations_in_module(self): isa = inspect_stock_annotations for kwargs in [ {}, - {'eval_str': False}, - {'format': annotations.Format.VALUE}, - {'format': annotations.Format.FORWARDREF}, - {'format': annotations.Format.VALUE, 'eval_str': False}, - {'format': annotations.Format.FORWARDREF, 'eval_str': False}, + {"eval_str": False}, + {"format": annotations.Format.VALUE}, + {"format": annotations.Format.FORWARDREF}, + {"format": annotations.Format.VALUE, "eval_str": False}, + {"format": annotations.Format.FORWARDREF, "eval_str": False}, ]: with self.subTest(**kwargs): - self.assertEqual(annotations.get_annotations(isa, **kwargs), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.MyClass, **kwargs), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.function, **kwargs), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function2, **kwargs), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function3, **kwargs), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) - self.assertEqual(annotations.get_annotations(annotations, **kwargs), {}) # annotations module has no annotations - self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, **kwargs), {}) - self.assertEqual(annotations.get_annotations(isa.unannotated_function, **kwargs), {}) + self.assertEqual( + annotations.get_annotations(isa, **kwargs), {"a": int, "b": str} + ) + self.assertEqual( + annotations.get_annotations(isa.MyClass, **kwargs), + {"a": int, "b": str}, + ) + self.assertEqual( + annotations.get_annotations(isa.function, **kwargs), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + annotations.get_annotations(isa.function2, **kwargs), + {"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass}, + ) + self.assertEqual( + annotations.get_annotations(isa.function3, **kwargs), + {"a": "int", "b": "str", "c": "MyClass"}, + ) + self.assertEqual( + annotations.get_annotations(annotations, **kwargs), {} + ) # annotations module has no annotations + self.assertEqual( + annotations.get_annotations(isa.UnannotatedClass, **kwargs), {} + ) + self.assertEqual( + annotations.get_annotations(isa.unannotated_function, **kwargs), {} + ) for kwargs in [ {"eval_str": True}, {"format": annotations.Format.VALUE, "eval_str": True}, ]: with self.subTest(**kwargs): - self.assertEqual(annotations.get_annotations(isa, **kwargs), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.MyClass, **kwargs), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.function, **kwargs), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function2, **kwargs), {'a': int, 'b': str, 'c': isa.MyClass, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function3, **kwargs), {'a': int, 'b': str, 'c': isa.MyClass}) + self.assertEqual( + annotations.get_annotations(isa, **kwargs), {"a": int, "b": str} + ) + self.assertEqual( + annotations.get_annotations(isa.MyClass, **kwargs), + {"a": int, "b": str}, + ) + self.assertEqual( + annotations.get_annotations(isa.function, **kwargs), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + annotations.get_annotations(isa.function2, **kwargs), + {"a": int, "b": str, "c": isa.MyClass, "return": isa.MyClass}, + ) + self.assertEqual( + annotations.get_annotations(isa.function3, **kwargs), + {"a": int, "b": str, "c": isa.MyClass}, + ) self.assertEqual(annotations.get_annotations(annotations, **kwargs), {}) - self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, **kwargs), {}) - self.assertEqual(annotations.get_annotations(isa.unannotated_function, **kwargs), {}) - - self.assertEqual(annotations.get_annotations(isa, format=annotations.Format.SOURCE), {'a': 'int', 'b': 'str'}) - self.assertEqual(annotations.get_annotations(isa.MyClass, format=annotations.Format.SOURCE), {'a': 'int', 'b': 'str'}) - self.assertEqual(annotations.get_annotations(isa.function, format=annotations.Format.SOURCE), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) - self.assertEqual(annotations.get_annotations(isa.function2, format=annotations.Format.SOURCE), {'a': 'int', 'b': 'str', 'c': 'MyClass', 'return': 'MyClass'}) - self.assertEqual(annotations.get_annotations(isa.function3, format=annotations.Format.SOURCE), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) - self.assertEqual(annotations.get_annotations(annotations, format=annotations.Format.SOURCE), {}) - self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, format=annotations.Format.SOURCE), {}) - self.assertEqual(annotations.get_annotations(isa.unannotated_function, format=annotations.Format.SOURCE), {}) + self.assertEqual( + annotations.get_annotations(isa.UnannotatedClass, **kwargs), {} + ) + self.assertEqual( + annotations.get_annotations(isa.unannotated_function, **kwargs), {} + ) + + self.assertEqual( + annotations.get_annotations(isa, format=annotations.Format.SOURCE), + {"a": "int", "b": "str"}, + ) + self.assertEqual( + annotations.get_annotations(isa.MyClass, format=annotations.Format.SOURCE), + {"a": "int", "b": "str"}, + ) + self.assertEqual( + annotations.get_annotations(isa.function, format=annotations.Format.SOURCE), + {"a": "int", "b": "str", "return": "MyClass"}, + ) + self.assertEqual( + annotations.get_annotations( + isa.function2, format=annotations.Format.SOURCE + ), + {"a": "int", "b": "str", "c": "MyClass", "return": "MyClass"}, + ) + self.assertEqual( + annotations.get_annotations( + isa.function3, format=annotations.Format.SOURCE + ), + {"a": "int", "b": "str", "c": "MyClass"}, + ) + self.assertEqual( + annotations.get_annotations(annotations, format=annotations.Format.SOURCE), + {}, + ) + self.assertEqual( + annotations.get_annotations( + isa.UnannotatedClass, format=annotations.Format.SOURCE + ), + {}, + ) + self.assertEqual( + annotations.get_annotations( + isa.unannotated_function, format=annotations.Format.SOURCE + ), + {}, + ) def test_stock_annotations_on_wrapper(self): isa = inspect_stock_annotations wrapped = times_three(isa.function) - self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx')) + self.assertEqual(wrapped(1, "x"), isa.MyClass(3, "xxx")) self.assertIsNot(wrapped.__globals__, isa.function.__globals__) - self.assertEqual(annotations.get_annotations(wrapped), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(wrapped, format=annotations.Format.FORWARDREF), - {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(wrapped, format=annotations.Format.SOURCE), - {'a': 'int', 'b': 'str', 'return': 'MyClass'}) - self.assertEqual(annotations.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(wrapped, eval_str=False), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual( + annotations.get_annotations(wrapped), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + annotations.get_annotations(wrapped, format=annotations.Format.FORWARDREF), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + annotations.get_annotations(wrapped, format=annotations.Format.SOURCE), + {"a": "int", "b": "str", "return": "MyClass"}, + ) + self.assertEqual( + annotations.get_annotations(wrapped, eval_str=True), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + annotations.get_annotations(wrapped, eval_str=False), + {"a": int, "b": str, "return": isa.MyClass}, + ) def test_stringized_annotations_in_module(self): isa = inspect_stringized_annotations for kwargs in [ {}, - {'eval_str': False}, - {'format': annotations.Format.VALUE}, - {'format': annotations.Format.FORWARDREF}, - {'format': annotations.Format.SOURCE}, - {'format': annotations.Format.VALUE, 'eval_str': False}, - {'format': annotations.Format.FORWARDREF, 'eval_str': False}, - {'format': annotations.Format.SOURCE, 'eval_str': False}, + {"eval_str": False}, + {"format": annotations.Format.VALUE}, + {"format": annotations.Format.FORWARDREF}, + {"format": annotations.Format.SOURCE}, + {"format": annotations.Format.VALUE, "eval_str": False}, + {"format": annotations.Format.FORWARDREF, "eval_str": False}, + {"format": annotations.Format.SOURCE, "eval_str": False}, ]: with self.subTest(**kwargs): - self.assertEqual(annotations.get_annotations(isa, **kwargs), {'a': 'int', 'b': 'str'}) - self.assertEqual(annotations.get_annotations(isa.MyClass, **kwargs), {'a': 'int', 'b': 'str'}) - self.assertEqual(annotations.get_annotations(isa.function, **kwargs), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) - self.assertEqual(annotations.get_annotations(isa.function2, **kwargs), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'}) - self.assertEqual(annotations.get_annotations(isa.function3, **kwargs), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"}) - self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, **kwargs), {}) - self.assertEqual(annotations.get_annotations(isa.unannotated_function, **kwargs), {}) + self.assertEqual( + annotations.get_annotations(isa, **kwargs), {"a": "int", "b": "str"} + ) + self.assertEqual( + annotations.get_annotations(isa.MyClass, **kwargs), + {"a": "int", "b": "str"}, + ) + self.assertEqual( + annotations.get_annotations(isa.function, **kwargs), + {"a": "int", "b": "str", "return": "MyClass"}, + ) + self.assertEqual( + annotations.get_annotations(isa.function2, **kwargs), + {"a": "int", "b": "'str'", "c": "MyClass", "return": "MyClass"}, + ) + self.assertEqual( + annotations.get_annotations(isa.function3, **kwargs), + {"a": "'int'", "b": "'str'", "c": "'MyClass'"}, + ) + self.assertEqual( + annotations.get_annotations(isa.UnannotatedClass, **kwargs), {} + ) + self.assertEqual( + annotations.get_annotations(isa.unannotated_function, **kwargs), {} + ) for kwargs in [ {"eval_str": True}, {"format": annotations.Format.VALUE, "eval_str": True}, ]: with self.subTest(**kwargs): - self.assertEqual(annotations.get_annotations(isa, **kwargs), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.MyClass, **kwargs), {'a': int, 'b': str}) - self.assertEqual(annotations.get_annotations(isa.function, **kwargs), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function2, **kwargs), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(isa.function3, **kwargs), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) - self.assertEqual(annotations.get_annotations(isa.UnannotatedClass, **kwargs), {}) - self.assertEqual(annotations.get_annotations(isa.unannotated_function, **kwargs), {}) + self.assertEqual( + annotations.get_annotations(isa, **kwargs), {"a": int, "b": str} + ) + self.assertEqual( + annotations.get_annotations(isa.MyClass, **kwargs), + {"a": int, "b": str}, + ) + self.assertEqual( + annotations.get_annotations(isa.function, **kwargs), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + annotations.get_annotations(isa.function2, **kwargs), + {"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass}, + ) + self.assertEqual( + annotations.get_annotations(isa.function3, **kwargs), + {"a": "int", "b": "str", "c": "MyClass"}, + ) + self.assertEqual( + annotations.get_annotations(isa.UnannotatedClass, **kwargs), {} + ) + self.assertEqual( + annotations.get_annotations(isa.unannotated_function, **kwargs), {} + ) def test_stringized_annotations_in_empty_module(self): isa2 = inspect_stringized_annotations_2 @@ -342,14 +498,29 @@ def test_stringized_annotations_in_empty_module(self): def test_stringized_annotations_on_wrapper(self): isa = inspect_stringized_annotations wrapped = times_three(isa.function) - self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx')) + self.assertEqual(wrapped(1, "x"), isa.MyClass(3, "xxx")) self.assertIsNot(wrapped.__globals__, isa.function.__globals__) - self.assertEqual(annotations.get_annotations(wrapped), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) - self.assertEqual(annotations.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) - self.assertEqual(annotations.get_annotations(wrapped, eval_str=False), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) + self.assertEqual( + annotations.get_annotations(wrapped), + {"a": "int", "b": "str", "return": "MyClass"}, + ) + self.assertEqual( + annotations.get_annotations(wrapped, eval_str=True), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + annotations.get_annotations(wrapped, eval_str=False), + {"a": "int", "b": "str", "return": "MyClass"}, + ) def test_stringized_annotations_on_class(self): isa = inspect_stringized_annotations # test that local namespace lookups work - self.assertEqual(annotations.get_annotations(isa.MyClassWithLocalAnnotations), {'x': 'mytype'}) - self.assertEqual(annotations.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {'x': int}) + self.assertEqual( + annotations.get_annotations(isa.MyClassWithLocalAnnotations), + {"x": "mytype"}, + ) + self.assertEqual( + annotations.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), + {"x": int}, + ) From 5f269c79b11b939827792019cf99d080c05227ce Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 20:52:07 -0700 Subject: [PATCH 089/105] Remove unnecessary method --- Lib/annotations.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index 4927c8632d2f11..ce46cdb8a1a8a0 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -205,9 +205,6 @@ def __ror__(self, other): from typing import Union as _Union return _Union[other, self] - def __getitem__(self, arg): - return types.GenericAlias(self, arg) - def __repr__(self): if self.__forward_module__ is None: module_repr = "" From 39f969c50b2cc7633a3563db56fc74f6e8d6ee09 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 20:55:26 -0700 Subject: [PATCH 090/105] Error on unrecognized format --- Lib/annotations.py | 2 ++ Lib/test/test_annotations.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/Lib/annotations.py b/Lib/annotations.py index ce46cdb8a1a8a0..4c324354e1966c 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -503,6 +503,8 @@ def call_annotate_function(annotate, format, owner=None): # Should be impossible because __annotate__ functions must not raise # NotImplementedError for this format. raise RuntimeError("annotate function does not support VALUE format") + else: + raise ValueError(f"Invalid format: {format!r}") def get_annotations( diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index 91dc0e7468be1c..644543e9c3da55 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -227,6 +227,37 @@ def test_builtin_type(self): self.assertEqual(annotations.get_annotations(int), {}) self.assertEqual(annotations.get_annotations(object), {}) + def test_format(self): + def f1(a: int): + pass + + def f2(a: undefined): + pass + + self.assertEqual( + annotations.get_annotations(f1, format=annotations.Format.VALUE), {"a": int} + ) + self.assertEqual(annotations.get_annotations(f1, format=1), {"a": int}) + + fwd = annotations.ForwardRef("undefined") + self.assertEqual( + annotations.get_annotations(f2, format=annotations.Format.FORWARDREF), + {"a": fwd}, + ) + self.assertEqual(annotations.get_annotations(f2, format=2), {"a": fwd}) + + self.assertEqual( + annotations.get_annotations(f1, format=annotations.Format.SOURCE), + {"a": "int"}, + ) + self.assertEqual(annotations.get_annotations(f1, format=3), {"a": "int"}) + + with self.assertRaises(ValueError): + annotations.get_annotations(f1, format=0) + + with self.assertRaises(ValueError): + annotations.get_annotations(f1, format=4) + def test_custom_object_with_annotations(self): class C: def __init__(self, x: int = 0, y: str = ""): From 223e2d5515bd166ae3cdb8a6e39509cb1e1270f4 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 2 Jun 2024 16:09:08 -0700 Subject: [PATCH 091/105] Add some tests, and explicit errors for some unsupported operations --- Lib/annotations.py | 6 ++++++ Lib/test/test_annotations.py | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/Lib/annotations.py b/Lib/annotations.py index 4c324354e1966c..8b888bab32ac91 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -375,6 +375,12 @@ def __iter__(self): def __repr__(self): return ast.unparse(self.__ast_node__) + def __bool__(self): + raise TypeError("Cannot stringify annotation that uses boolean logic") + + def __format__(self, format_spec): + raise TypeError("Cannot stringify annotation containing string formatting") + class _StringifierDict(dict): def __init__(self, namespace, globals=None, owner=None, is_class=False): diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index 644543e9c3da55..f4fd1de981c7ac 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -189,6 +189,9 @@ def f( list: [a, b, c], tuple: (a, b, c), slice: (a[b:c], a[b:c:d], a[:c], a[b:], a[:], a[::d], a[b::d]), + extended_slice: a[:, :, c:d], + unpack1: [*a], + unpack2: [*a, b, c], ): pass @@ -202,9 +205,48 @@ def f( "list": "[a, b, c]", "tuple": "(a, b, c)", "slice": "(a[b:c], a[b:c:d], a[:c], a[b:], a[:], a[::d], a[b::d])", + "extended_slice": "a[:, :, c:d]", + "unpack1": "[*a]", + "unpack2": "[*a, b, c]", }, ) + def test_unsupported_operations(self): + bool_msg = "Cannot stringify annotation that uses boolean logic" + + def f(and_: a and b): pass + with self.assertRaisesRegex(TypeError, bool_msg): + annotations.get_annotations(f, format=annotations.Format.SOURCE) + + def f(or_: a or b): pass + with self.assertRaisesRegex(TypeError, bool_msg): + annotations.get_annotations(f, format=annotations.Format.SOURCE) + + def f(if_: a if b else c): pass + with self.assertRaisesRegex(TypeError, bool_msg): + annotations.get_annotations(f, format=annotations.Format.SOURCE) + + def f(not_: not a): pass + with self.assertRaisesRegex(TypeError, bool_msg): + annotations.get_annotations(f, format=annotations.Format.SOURCE) + + def f(in_: a in b): pass + with self.assertRaisesRegex(TypeError, bool_msg): + annotations.get_annotations(f, format=annotations.Format.SOURCE) + + def f(not_in: a not in b): pass + with self.assertRaisesRegex(TypeError, bool_msg): + annotations.get_annotations(f, format=annotations.Format.SOURCE) + + format_msg = "Cannot stringify annotation containing string formatting" + def f(fstring: f"{a}"): pass + with self.assertRaisesRegex(TypeError, format_msg): + annotations.get_annotations(f, format=annotations.Format.SOURCE) + + def f(fstring_format: f"{a:02d}"): pass + with self.assertRaisesRegex(TypeError, format_msg): + annotations.get_annotations(f, format=annotations.Format.SOURCE) + class TestForwardRefClass(unittest.TestCase): def test_special_attrs(self): From 7afeccad11c66aa0ad9a9f646d251490741fd441 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 2 Jun 2024 16:21:56 -0700 Subject: [PATCH 092/105] I guess not --- Lib/annotations.py | 3 --- Lib/test/test_annotations.py | 26 -------------------------- 2 files changed, 29 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index 8b888bab32ac91..797bc327a7bed1 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -375,9 +375,6 @@ def __iter__(self): def __repr__(self): return ast.unparse(self.__ast_node__) - def __bool__(self): - raise TypeError("Cannot stringify annotation that uses boolean logic") - def __format__(self, format_spec): raise TypeError("Cannot stringify annotation containing string formatting") diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index f4fd1de981c7ac..85b2f37c1f71fb 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -212,32 +212,6 @@ def f( ) def test_unsupported_operations(self): - bool_msg = "Cannot stringify annotation that uses boolean logic" - - def f(and_: a and b): pass - with self.assertRaisesRegex(TypeError, bool_msg): - annotations.get_annotations(f, format=annotations.Format.SOURCE) - - def f(or_: a or b): pass - with self.assertRaisesRegex(TypeError, bool_msg): - annotations.get_annotations(f, format=annotations.Format.SOURCE) - - def f(if_: a if b else c): pass - with self.assertRaisesRegex(TypeError, bool_msg): - annotations.get_annotations(f, format=annotations.Format.SOURCE) - - def f(not_: not a): pass - with self.assertRaisesRegex(TypeError, bool_msg): - annotations.get_annotations(f, format=annotations.Format.SOURCE) - - def f(in_: a in b): pass - with self.assertRaisesRegex(TypeError, bool_msg): - annotations.get_annotations(f, format=annotations.Format.SOURCE) - - def f(not_in: a not in b): pass - with self.assertRaisesRegex(TypeError, bool_msg): - annotations.get_annotations(f, format=annotations.Format.SOURCE) - format_msg = "Cannot stringify annotation containing string formatting" def f(fstring: f"{a}"): pass with self.assertRaisesRegex(TypeError, format_msg): From efce54f147bc2407fa62860e246c2985b1adca53 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 2 Jun 2024 16:41:23 -0700 Subject: [PATCH 093/105] Use __annotations__ directly for VALUE --- Lib/annotations.py | 25 +++++++++---------------- Lib/test/test_annotations.py | 28 ++++++++++++++++++++++++++-- Lib/test/test_typing.py | 2 +- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/Lib/annotations.py b/Lib/annotations.py index 797bc327a7bed1..7323ecef947785 100644 --- a/Lib/annotations.py +++ b/Lib/annotations.py @@ -560,21 +560,18 @@ def get_annotations( if eval_str and format != Format.VALUE: raise ValueError("eval_str=True is only supported with format=Format.VALUE") - annotate = getattr(obj, "__annotate__", None) - if annotate is not None: - ann = call_annotate_function(annotate, format, owner=obj) - if not isinstance(ann, dict): - raise ValueError(f"{obj!r}.__annotate__ returned a non-dict") - if not eval_str: + # For VALUE format, we look at __annotations__ directly. + if format != Format.VALUE: + annotate = getattr(obj, "__annotate__", None) + if annotate is not None: + ann = call_annotate_function(annotate, format, owner=obj) + if not isinstance(ann, dict): + raise ValueError(f"{obj!r}.__annotate__ returned a non-dict") return dict(ann) - else: - ann = None + ann = getattr(obj, "__annotations__", None) if isinstance(obj, type): # class - if ann is None: - ann = getattr(obj, "__annotations__", None) - obj_globals = None module_name = getattr(obj, "__module__", None) if module_name: @@ -585,8 +582,6 @@ def get_annotations( unwrap = obj elif isinstance(obj, types.ModuleType): # module - if ann is None: - ann = getattr(obj, "__annotations__", None) obj_globals = getattr(obj, "__dict__") obj_locals = None unwrap = None @@ -594,12 +589,10 @@ def get_annotations( # this includes types.Function, types.BuiltinFunctionType, # types.BuiltinMethodType, functools.partial, functools.singledispatch, # "class funclike" from Lib/test/test_inspect... on and on it goes. - if ann is None: - ann = getattr(obj, "__annotations__", None) obj_globals = getattr(obj, "__globals__", None) obj_locals = None unwrap = obj - elif (ann := getattr(obj, "__annotations__", None)) is not None: + elif ann is not None: obj_globals = obj_locals = unwrap = None else: raise TypeError(f"{obj!r} is not a module, class, or callable.") diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index 85b2f37c1f71fb..3d376e9d68b444 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -213,11 +213,16 @@ def f( def test_unsupported_operations(self): format_msg = "Cannot stringify annotation containing string formatting" - def f(fstring: f"{a}"): pass + + def f(fstring: f"{a}"): + pass + with self.assertRaisesRegex(TypeError, format_msg): annotations.get_annotations(f, format=annotations.Format.SOURCE) - def f(fstring_format: f"{a:02d}"): pass + def f(fstring_format: f"{a:02d}"): + pass + with self.assertRaisesRegex(TypeError, format_msg): annotations.get_annotations(f, format=annotations.Format.SOURCE) @@ -571,3 +576,22 @@ def test_stringized_annotations_on_class(self): annotations.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {"x": int}, ) + + def test_modify_annotations(self): + def f(x: int): + pass + + self.assertEqual(annotations.get_annotations(f), {"x": int}) + self.assertEqual( + annotations.get_annotations(f, format=annotations.Format.FORWARDREF), + {"x": int}, + ) + + f.__annotations__["x"] = str + # The modification is reflected in VALUE (the default) + self.assertEqual(annotations.get_annotations(f), {"x": str}) + # ... but not in FORWARDREF, which uses __annotate__ + self.assertEqual( + annotations.get_annotations(f, format=annotations.Format.FORWARDREF), + {"x": int}, + ) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 5dc51c92263054..a8899cd3a2fb8a 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6637,7 +6637,7 @@ def test_get_type_hints_from_various_objects(self): def test_get_type_hints_modules(self): ann_module_type_hints = {'f': Tuple[int, int], 'x': int, 'y': str, 'u': int | float} self.assertEqual(gth(ann_module), ann_module_type_hints) - self.assertEqual(gth(ann_module2), {'i': int, 'j': int, 'x': float}) + self.assertEqual(gth(ann_module2), {}) self.assertEqual(gth(ann_module3), {}) @skip("known bug") From 3d5244ed94d5b05533272fe2715fa00e5f84f6cb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 7 Jun 2024 16:16:24 -0600 Subject: [PATCH 094/105] annotationlib --- Lib/{annotations.py => annotationlib.py} | 0 Lib/dataclasses.py | 6 +- Lib/inspect.py | 2 +- ...t_annotations.py => test_annotationlib.py} | 252 ++++++++++-------- Lib/test/test_grammar.py | 4 +- Lib/test/test_type_annotations.py | 10 +- Lib/test/test_typing.py | 32 +-- Lib/typing.py | 46 ++-- Python/stdlib_module_names.h | 2 +- 9 files changed, 188 insertions(+), 166 deletions(-) rename Lib/{annotations.py => annotationlib.py} (100%) rename Lib/test/{test_annotations.py => test_annotationlib.py} (61%) diff --git a/Lib/annotations.py b/Lib/annotationlib.py similarity index 100% rename from Lib/annotations.py rename to Lib/annotationlib.py diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index a43ee59507e228..7c59481d0caa62 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -5,7 +5,7 @@ import inspect import keyword import itertools -import annotations +import annotationlib import abc from reprlib import recursive_repr from types import GenericAlias @@ -983,8 +983,8 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, # actual default value. Pseudo-fields ClassVars and InitVars are # included, despite the fact that they're not real fields. That's # dealt with later. - cls_annotations = annotations.get_annotations(cls, - format=annotations.Format.FORWARDREF) + cls_annotations = annotationlib.get_annotations( + cls, format=annotationlib.Format.FORWARDREF) # Now find fields in our class. While doing so, validate some # things, and set the default values (as class attributes) where diff --git a/Lib/inspect.py b/Lib/inspect.py index e726edcfac0f94..78a885ea825449 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -142,7 +142,7 @@ import abc -from annotations import get_annotations +from annotationlib import get_annotations import ast import dis import collections.abc diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotationlib.py similarity index 61% rename from Lib/test/test_annotations.py rename to Lib/test/test_annotationlib.py index 3d376e9d68b444..dfe49769f2ee32 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotationlib.py @@ -1,6 +1,6 @@ """Tests for the annotations module.""" -import annotations +import annotationlib import functools import pickle import unittest @@ -20,14 +20,14 @@ def wrapper(a, b): class TestFormat(unittest.TestCase): def test_enum(self): - self.assertEqual(annotations.Format.VALUE.value, 1) - self.assertEqual(annotations.Format.VALUE, 1) + self.assertEqual(annotationlib.Format.VALUE.value, 1) + self.assertEqual(annotationlib.Format.VALUE, 1) - self.assertEqual(annotations.Format.FORWARDREF.value, 2) - self.assertEqual(annotations.Format.FORWARDREF, 2) + self.assertEqual(annotationlib.Format.FORWARDREF.value, 2) + self.assertEqual(annotationlib.Format.FORWARDREF, 2) - self.assertEqual(annotations.Format.SOURCE.value, 3) - self.assertEqual(annotations.Format.SOURCE, 3) + self.assertEqual(annotationlib.Format.SOURCE.value, 3) + self.assertEqual(annotationlib.Format.SOURCE, 3) class TestForwardRefFormat(unittest.TestCase): @@ -35,9 +35,11 @@ def test_closure(self): def inner(arg: x): pass - anno = annotations.get_annotations(inner, format=annotations.Format.FORWARDREF) + anno = annotationlib.get_annotations( + inner, format=annotationlib.Format.FORWARDREF + ) fwdref = anno["arg"] - self.assertIsInstance(fwdref, annotations.ForwardRef) + self.assertIsInstance(fwdref, annotationlib.ForwardRef) self.assertEqual(fwdref.__forward_arg__, "x") with self.assertRaises(NameError): fwdref.evaluate() @@ -45,17 +47,19 @@ def inner(arg: x): x = 1 self.assertEqual(fwdref.evaluate(), x) - anno = annotations.get_annotations(inner, format=annotations.Format.FORWARDREF) + anno = annotationlib.get_annotations( + inner, format=annotationlib.Format.FORWARDREF + ) self.assertEqual(anno["arg"], x) def test_function(self): def f(x: int, y: doesntexist): pass - anno = annotations.get_annotations(f, format=annotations.Format.FORWARDREF) + anno = annotationlib.get_annotations(f, format=annotationlib.Format.FORWARDREF) self.assertIs(anno["x"], int) fwdref = anno["y"] - self.assertIsInstance(fwdref, annotations.ForwardRef) + self.assertIsInstance(fwdref, annotationlib.ForwardRef) self.assertEqual(fwdref.__forward_arg__, "doesntexist") with self.assertRaises(NameError): fwdref.evaluate() @@ -69,14 +73,14 @@ def test_closure(self): def inner(arg: x): pass - anno = annotations.get_annotations(inner, format=annotations.Format.SOURCE) + anno = annotationlib.get_annotations(inner, format=annotationlib.Format.SOURCE) self.assertEqual(anno, {"arg": "x"}) def test_function(self): def f(x: int, y: doesntexist): pass - anno = annotations.get_annotations(f, format=annotations.Format.SOURCE) + anno = annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE) self.assertEqual(anno, {"x": "int", "y": "doesntexist"}) def test_expressions(self): @@ -110,7 +114,7 @@ def f( ): pass - anno = annotations.get_annotations(f, format=annotations.Format.SOURCE) + anno = annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE) self.assertEqual( anno, { @@ -161,7 +165,7 @@ def f( ): pass - anno = annotations.get_annotations(f, format=annotations.Format.SOURCE) + anno = annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE) self.assertEqual( anno, { @@ -195,7 +199,7 @@ def f( ): pass - anno = annotations.get_annotations(f, format=annotations.Format.SOURCE) + anno = annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE) self.assertEqual( anno, { @@ -218,13 +222,13 @@ def f(fstring: f"{a}"): pass with self.assertRaisesRegex(TypeError, format_msg): - annotations.get_annotations(f, format=annotations.Format.SOURCE) + annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE) def f(fstring_format: f"{a:02d}"): pass with self.assertRaisesRegex(TypeError, format_msg): - annotations.get_annotations(f, format=annotations.Format.SOURCE) + annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE) class TestForwardRefClass(unittest.TestCase): @@ -232,10 +236,10 @@ def test_special_attrs(self): # Forward refs provide a different introspection API. __name__ and # __qualname__ make little sense for forward refs as they can store # complex typing expressions. - fr = annotations.ForwardRef("set[Any]") + fr = annotationlib.ForwardRef("set[Any]") self.assertFalse(hasattr(fr, "__name__")) self.assertFalse(hasattr(fr, "__qualname__")) - self.assertEqual(fr.__module__, "annotations") + self.assertEqual(fr.__module__, "annotationlib") # Forward refs are currently unpicklable once they contain a code object. fr.__forward_code__ # fill the cache for proto in range(pickle.HIGHEST_PROTOCOL + 1): @@ -245,8 +249,8 @@ def test_special_attrs(self): class TestGetAnnotations(unittest.TestCase): def test_builtin_type(self): - self.assertEqual(annotations.get_annotations(int), {}) - self.assertEqual(annotations.get_annotations(object), {}) + self.assertEqual(annotationlib.get_annotations(int), {}) + self.assertEqual(annotationlib.get_annotations(object), {}) def test_format(self): def f1(a: int): @@ -256,77 +260,78 @@ def f2(a: undefined): pass self.assertEqual( - annotations.get_annotations(f1, format=annotations.Format.VALUE), {"a": int} + annotationlib.get_annotations(f1, format=annotationlib.Format.VALUE), + {"a": int}, ) - self.assertEqual(annotations.get_annotations(f1, format=1), {"a": int}) + self.assertEqual(annotationlib.get_annotations(f1, format=1), {"a": int}) - fwd = annotations.ForwardRef("undefined") + fwd = annotationlib.ForwardRef("undefined") self.assertEqual( - annotations.get_annotations(f2, format=annotations.Format.FORWARDREF), + annotationlib.get_annotations(f2, format=annotationlib.Format.FORWARDREF), {"a": fwd}, ) - self.assertEqual(annotations.get_annotations(f2, format=2), {"a": fwd}) + self.assertEqual(annotationlib.get_annotations(f2, format=2), {"a": fwd}) self.assertEqual( - annotations.get_annotations(f1, format=annotations.Format.SOURCE), + annotationlib.get_annotations(f1, format=annotationlib.Format.SOURCE), {"a": "int"}, ) - self.assertEqual(annotations.get_annotations(f1, format=3), {"a": "int"}) + self.assertEqual(annotationlib.get_annotations(f1, format=3), {"a": "int"}) with self.assertRaises(ValueError): - annotations.get_annotations(f1, format=0) + annotationlib.get_annotations(f1, format=0) with self.assertRaises(ValueError): - annotations.get_annotations(f1, format=4) + annotationlib.get_annotations(f1, format=4) def test_custom_object_with_annotations(self): class C: def __init__(self, x: int = 0, y: str = ""): self.__annotations__ = {"x": int, "y": str} - self.assertEqual(annotations.get_annotations(C()), {"x": int, "y": str}) + self.assertEqual(annotationlib.get_annotations(C()), {"x": int, "y": str}) def test_custom_format_eval_str(self): def foo(): pass with self.assertRaises(ValueError): - annotations.get_annotations( - foo, format=annotations.Format.FORWARDREF, eval_str=True + annotationlib.get_annotations( + foo, format=annotationlib.Format.FORWARDREF, eval_str=True ) - annotations.get_annotations( - foo, format=annotations.Format.SOURCE, eval_str=True + annotationlib.get_annotations( + foo, format=annotationlib.Format.SOURCE, eval_str=True ) def test_stock_annotations(self): def foo(a: int, b: str): pass - for format in (annotations.Format.VALUE, annotations.Format.FORWARDREF): + for format in (annotationlib.Format.VALUE, annotationlib.Format.FORWARDREF): with self.subTest(format=format): self.assertEqual( - annotations.get_annotations(foo, format=format), + annotationlib.get_annotations(foo, format=format), {"a": int, "b": str}, ) self.assertEqual( - annotations.get_annotations(foo, format=annotations.Format.SOURCE), + annotationlib.get_annotations(foo, format=annotationlib.Format.SOURCE), {"a": "int", "b": "str"}, ) foo.__annotations__ = {"a": "foo", "b": "str"} - for format in annotations.Format: + for format in annotationlib.Format: with self.subTest(format=format): self.assertEqual( - annotations.get_annotations(foo, format=format), + annotationlib.get_annotations(foo, format=format), {"a": "foo", "b": "str"}, ) self.assertEqual( - annotations.get_annotations(foo, eval_str=True, locals=locals()), + annotationlib.get_annotations(foo, eval_str=True, locals=locals()), {"a": foo, "b": str}, ) self.assertEqual( - annotations.get_annotations(foo, eval_str=True, globals=locals()), + annotationlib.get_annotations(foo, eval_str=True, globals=locals()), {"a": foo, "b": str}, ) @@ -336,110 +341,120 @@ def test_stock_annotations_in_module(self): for kwargs in [ {}, {"eval_str": False}, - {"format": annotations.Format.VALUE}, - {"format": annotations.Format.FORWARDREF}, - {"format": annotations.Format.VALUE, "eval_str": False}, - {"format": annotations.Format.FORWARDREF, "eval_str": False}, + {"format": annotationlib.Format.VALUE}, + {"format": annotationlib.Format.FORWARDREF}, + {"format": annotationlib.Format.VALUE, "eval_str": False}, + {"format": annotationlib.Format.FORWARDREF, "eval_str": False}, ]: with self.subTest(**kwargs): self.assertEqual( - annotations.get_annotations(isa, **kwargs), {"a": int, "b": str} + annotationlib.get_annotations(isa, **kwargs), {"a": int, "b": str} ) self.assertEqual( - annotations.get_annotations(isa.MyClass, **kwargs), + annotationlib.get_annotations(isa.MyClass, **kwargs), {"a": int, "b": str}, ) self.assertEqual( - annotations.get_annotations(isa.function, **kwargs), + annotationlib.get_annotations(isa.function, **kwargs), {"a": int, "b": str, "return": isa.MyClass}, ) self.assertEqual( - annotations.get_annotations(isa.function2, **kwargs), + annotationlib.get_annotations(isa.function2, **kwargs), {"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass}, ) self.assertEqual( - annotations.get_annotations(isa.function3, **kwargs), + annotationlib.get_annotations(isa.function3, **kwargs), {"a": "int", "b": "str", "c": "MyClass"}, ) self.assertEqual( - annotations.get_annotations(annotations, **kwargs), {} + annotationlib.get_annotations(annotationlib, **kwargs), {} ) # annotations module has no annotations self.assertEqual( - annotations.get_annotations(isa.UnannotatedClass, **kwargs), {} + annotationlib.get_annotations(isa.UnannotatedClass, **kwargs), {} ) self.assertEqual( - annotations.get_annotations(isa.unannotated_function, **kwargs), {} + annotationlib.get_annotations(isa.unannotated_function, **kwargs), + {}, ) for kwargs in [ {"eval_str": True}, - {"format": annotations.Format.VALUE, "eval_str": True}, + {"format": annotationlib.Format.VALUE, "eval_str": True}, ]: with self.subTest(**kwargs): self.assertEqual( - annotations.get_annotations(isa, **kwargs), {"a": int, "b": str} + annotationlib.get_annotations(isa, **kwargs), {"a": int, "b": str} ) self.assertEqual( - annotations.get_annotations(isa.MyClass, **kwargs), + annotationlib.get_annotations(isa.MyClass, **kwargs), {"a": int, "b": str}, ) self.assertEqual( - annotations.get_annotations(isa.function, **kwargs), + annotationlib.get_annotations(isa.function, **kwargs), {"a": int, "b": str, "return": isa.MyClass}, ) self.assertEqual( - annotations.get_annotations(isa.function2, **kwargs), + annotationlib.get_annotations(isa.function2, **kwargs), {"a": int, "b": str, "c": isa.MyClass, "return": isa.MyClass}, ) self.assertEqual( - annotations.get_annotations(isa.function3, **kwargs), + annotationlib.get_annotations(isa.function3, **kwargs), {"a": int, "b": str, "c": isa.MyClass}, ) - self.assertEqual(annotations.get_annotations(annotations, **kwargs), {}) self.assertEqual( - annotations.get_annotations(isa.UnannotatedClass, **kwargs), {} + annotationlib.get_annotations(annotationlib, **kwargs), {} ) self.assertEqual( - annotations.get_annotations(isa.unannotated_function, **kwargs), {} + annotationlib.get_annotations(isa.UnannotatedClass, **kwargs), {} + ) + self.assertEqual( + annotationlib.get_annotations(isa.unannotated_function, **kwargs), + {}, ) self.assertEqual( - annotations.get_annotations(isa, format=annotations.Format.SOURCE), + annotationlib.get_annotations(isa, format=annotationlib.Format.SOURCE), {"a": "int", "b": "str"}, ) self.assertEqual( - annotations.get_annotations(isa.MyClass, format=annotations.Format.SOURCE), + annotationlib.get_annotations( + isa.MyClass, format=annotationlib.Format.SOURCE + ), {"a": "int", "b": "str"}, ) self.assertEqual( - annotations.get_annotations(isa.function, format=annotations.Format.SOURCE), + annotationlib.get_annotations( + isa.function, format=annotationlib.Format.SOURCE + ), {"a": "int", "b": "str", "return": "MyClass"}, ) self.assertEqual( - annotations.get_annotations( - isa.function2, format=annotations.Format.SOURCE + annotationlib.get_annotations( + isa.function2, format=annotationlib.Format.SOURCE ), {"a": "int", "b": "str", "c": "MyClass", "return": "MyClass"}, ) self.assertEqual( - annotations.get_annotations( - isa.function3, format=annotations.Format.SOURCE + annotationlib.get_annotations( + isa.function3, format=annotationlib.Format.SOURCE ), {"a": "int", "b": "str", "c": "MyClass"}, ) self.assertEqual( - annotations.get_annotations(annotations, format=annotations.Format.SOURCE), + annotationlib.get_annotations( + annotationlib, format=annotationlib.Format.SOURCE + ), {}, ) self.assertEqual( - annotations.get_annotations( - isa.UnannotatedClass, format=annotations.Format.SOURCE + annotationlib.get_annotations( + isa.UnannotatedClass, format=annotationlib.Format.SOURCE ), {}, ) self.assertEqual( - annotations.get_annotations( - isa.unannotated_function, format=annotations.Format.SOURCE + annotationlib.get_annotations( + isa.unannotated_function, format=annotationlib.Format.SOURCE ), {}, ) @@ -451,23 +466,25 @@ def test_stock_annotations_on_wrapper(self): self.assertEqual(wrapped(1, "x"), isa.MyClass(3, "xxx")) self.assertIsNot(wrapped.__globals__, isa.function.__globals__) self.assertEqual( - annotations.get_annotations(wrapped), + annotationlib.get_annotations(wrapped), {"a": int, "b": str, "return": isa.MyClass}, ) self.assertEqual( - annotations.get_annotations(wrapped, format=annotations.Format.FORWARDREF), + annotationlib.get_annotations( + wrapped, format=annotationlib.Format.FORWARDREF + ), {"a": int, "b": str, "return": isa.MyClass}, ) self.assertEqual( - annotations.get_annotations(wrapped, format=annotations.Format.SOURCE), + annotationlib.get_annotations(wrapped, format=annotationlib.Format.SOURCE), {"a": "int", "b": "str", "return": "MyClass"}, ) self.assertEqual( - annotations.get_annotations(wrapped, eval_str=True), + annotationlib.get_annotations(wrapped, eval_str=True), {"a": int, "b": str, "return": isa.MyClass}, ) self.assertEqual( - annotations.get_annotations(wrapped, eval_str=False), + annotationlib.get_annotations(wrapped, eval_str=False), {"a": int, "b": str, "return": isa.MyClass}, ) @@ -476,76 +493,79 @@ def test_stringized_annotations_in_module(self): for kwargs in [ {}, {"eval_str": False}, - {"format": annotations.Format.VALUE}, - {"format": annotations.Format.FORWARDREF}, - {"format": annotations.Format.SOURCE}, - {"format": annotations.Format.VALUE, "eval_str": False}, - {"format": annotations.Format.FORWARDREF, "eval_str": False}, - {"format": annotations.Format.SOURCE, "eval_str": False}, + {"format": annotationlib.Format.VALUE}, + {"format": annotationlib.Format.FORWARDREF}, + {"format": annotationlib.Format.SOURCE}, + {"format": annotationlib.Format.VALUE, "eval_str": False}, + {"format": annotationlib.Format.FORWARDREF, "eval_str": False}, + {"format": annotationlib.Format.SOURCE, "eval_str": False}, ]: with self.subTest(**kwargs): self.assertEqual( - annotations.get_annotations(isa, **kwargs), {"a": "int", "b": "str"} + annotationlib.get_annotations(isa, **kwargs), + {"a": "int", "b": "str"}, ) self.assertEqual( - annotations.get_annotations(isa.MyClass, **kwargs), + annotationlib.get_annotations(isa.MyClass, **kwargs), {"a": "int", "b": "str"}, ) self.assertEqual( - annotations.get_annotations(isa.function, **kwargs), + annotationlib.get_annotations(isa.function, **kwargs), {"a": "int", "b": "str", "return": "MyClass"}, ) self.assertEqual( - annotations.get_annotations(isa.function2, **kwargs), + annotationlib.get_annotations(isa.function2, **kwargs), {"a": "int", "b": "'str'", "c": "MyClass", "return": "MyClass"}, ) self.assertEqual( - annotations.get_annotations(isa.function3, **kwargs), + annotationlib.get_annotations(isa.function3, **kwargs), {"a": "'int'", "b": "'str'", "c": "'MyClass'"}, ) self.assertEqual( - annotations.get_annotations(isa.UnannotatedClass, **kwargs), {} + annotationlib.get_annotations(isa.UnannotatedClass, **kwargs), {} ) self.assertEqual( - annotations.get_annotations(isa.unannotated_function, **kwargs), {} + annotationlib.get_annotations(isa.unannotated_function, **kwargs), + {}, ) for kwargs in [ {"eval_str": True}, - {"format": annotations.Format.VALUE, "eval_str": True}, + {"format": annotationlib.Format.VALUE, "eval_str": True}, ]: with self.subTest(**kwargs): self.assertEqual( - annotations.get_annotations(isa, **kwargs), {"a": int, "b": str} + annotationlib.get_annotations(isa, **kwargs), {"a": int, "b": str} ) self.assertEqual( - annotations.get_annotations(isa.MyClass, **kwargs), + annotationlib.get_annotations(isa.MyClass, **kwargs), {"a": int, "b": str}, ) self.assertEqual( - annotations.get_annotations(isa.function, **kwargs), + annotationlib.get_annotations(isa.function, **kwargs), {"a": int, "b": str, "return": isa.MyClass}, ) self.assertEqual( - annotations.get_annotations(isa.function2, **kwargs), + annotationlib.get_annotations(isa.function2, **kwargs), {"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass}, ) self.assertEqual( - annotations.get_annotations(isa.function3, **kwargs), + annotationlib.get_annotations(isa.function3, **kwargs), {"a": "int", "b": "str", "c": "MyClass"}, ) self.assertEqual( - annotations.get_annotations(isa.UnannotatedClass, **kwargs), {} + annotationlib.get_annotations(isa.UnannotatedClass, **kwargs), {} ) self.assertEqual( - annotations.get_annotations(isa.unannotated_function, **kwargs), {} + annotationlib.get_annotations(isa.unannotated_function, **kwargs), + {}, ) def test_stringized_annotations_in_empty_module(self): isa2 = inspect_stringized_annotations_2 - self.assertEqual(annotations.get_annotations(isa2), {}) - self.assertEqual(annotations.get_annotations(isa2, eval_str=True), {}) - self.assertEqual(annotations.get_annotations(isa2, eval_str=False), {}) + self.assertEqual(annotationlib.get_annotations(isa2), {}) + self.assertEqual(annotationlib.get_annotations(isa2, eval_str=True), {}) + self.assertEqual(annotationlib.get_annotations(isa2, eval_str=False), {}) def test_stringized_annotations_on_wrapper(self): isa = inspect_stringized_annotations @@ -553,15 +573,15 @@ def test_stringized_annotations_on_wrapper(self): self.assertEqual(wrapped(1, "x"), isa.MyClass(3, "xxx")) self.assertIsNot(wrapped.__globals__, isa.function.__globals__) self.assertEqual( - annotations.get_annotations(wrapped), + annotationlib.get_annotations(wrapped), {"a": "int", "b": "str", "return": "MyClass"}, ) self.assertEqual( - annotations.get_annotations(wrapped, eval_str=True), + annotationlib.get_annotations(wrapped, eval_str=True), {"a": int, "b": str, "return": isa.MyClass}, ) self.assertEqual( - annotations.get_annotations(wrapped, eval_str=False), + annotationlib.get_annotations(wrapped, eval_str=False), {"a": "int", "b": "str", "return": "MyClass"}, ) @@ -569,11 +589,13 @@ def test_stringized_annotations_on_class(self): isa = inspect_stringized_annotations # test that local namespace lookups work self.assertEqual( - annotations.get_annotations(isa.MyClassWithLocalAnnotations), + annotationlib.get_annotations(isa.MyClassWithLocalAnnotations), {"x": "mytype"}, ) self.assertEqual( - annotations.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), + annotationlib.get_annotations( + isa.MyClassWithLocalAnnotations, eval_str=True + ), {"x": int}, ) @@ -581,17 +603,17 @@ def test_modify_annotations(self): def f(x: int): pass - self.assertEqual(annotations.get_annotations(f), {"x": int}) + self.assertEqual(annotationlib.get_annotations(f), {"x": int}) self.assertEqual( - annotations.get_annotations(f, format=annotations.Format.FORWARDREF), + annotationlib.get_annotations(f, format=annotationlib.Format.FORWARDREF), {"x": int}, ) f.__annotations__["x"] = str # The modification is reflected in VALUE (the default) - self.assertEqual(annotations.get_annotations(f), {"x": str}) + self.assertEqual(annotationlib.get_annotations(f), {"x": str}) # ... but not in FORWARDREF, which uses __annotate__ self.assertEqual( - annotations.get_annotations(f, format=annotations.Format.FORWARDREF), + annotationlib.get_annotations(f, format=annotationlib.Format.FORWARDREF), {"x": int}, ) diff --git a/Lib/test/test_grammar.py b/Lib/test/test_grammar.py index 09b989a5a4f7f5..d3da673e26c9de 100644 --- a/Lib/test/test_grammar.py +++ b/Lib/test/test_grammar.py @@ -3,7 +3,7 @@ from test.support import check_syntax_error from test.support import import_helper -import annotations +import annotationlib import inspect import unittest import sys @@ -460,7 +460,7 @@ def test_var_annot_simple_exec(self): gns = {}; lns = {} exec("'docstring'\n" "x: int = 5\n", gns, lns) - self.assertEqual(lns["__annotate__"](annotations.Format.VALUE), {'x': int}) + self.assertEqual(lns["__annotate__"](annotationlib.Format.VALUE), {'x': int}) with self.assertRaises(KeyError): gns['__annotate__'] diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 8e3070b15dee96..4b432a9b0325d8 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -1,4 +1,4 @@ -import annotations +import annotationlib import textwrap import types import unittest @@ -373,12 +373,12 @@ class X: self.assertIsInstance(annotate, types.FunctionType) self.assertEqual(annotate.__name__, "__annotate__") with self.assertRaises(NotImplementedError): - annotate(annotations.Format.FORWARDREF) + annotate(annotationlib.Format.FORWARDREF) with self.assertRaises(NotImplementedError): - annotate(annotations.Format.SOURCE) + annotate(annotationlib.Format.SOURCE) with self.assertRaises(NotImplementedError): annotate(None) - self.assertEqual(annotate(annotations.Format.VALUE), {"x": int}) + self.assertEqual(annotate(annotationlib.Format.VALUE), {"x": int}) def test_comprehension_in_annotation(self): # This crashed in an earlier version of the code @@ -395,5 +395,5 @@ def f(x: int) -> int: pass f = ns["f"] self.assertIsInstance(f.__annotate__, types.FunctionType) annos = {"x": "int", "return": "int"} - self.assertEqual(f.__annotate__(annotations.Format.VALUE), annos) + self.assertEqual(f.__annotate__(annotationlib.Format.VALUE), annos) self.assertEqual(f.__annotations__, annos) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index a8899cd3a2fb8a..9f6ab045ad0512 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1,4 +1,4 @@ -import annotations +import annotationlib import contextlib import collections import collections.abc @@ -7770,9 +7770,9 @@ class Z(NamedTuple): b: "str" annos = {'a': type(None), 'b': ForwardRef("str")} self.assertEqual(Z.__annotations__, annos) - self.assertEqual(Z.__annotate__(annotations.Format.VALUE), annos) - self.assertEqual(Z.__annotate__(annotations.Format.FORWARDREF), annos) - self.assertEqual(Z.__annotate__(annotations.Format.SOURCE), {"a": "None", "b": "str"}) + self.assertEqual(Z.__annotate__(annotationlib.Format.VALUE), annos) + self.assertEqual(Z.__annotate__(annotationlib.Format.FORWARDREF), annos) + self.assertEqual(Z.__annotate__(annotationlib.Format.SOURCE), {"a": "None", "b": "str"}) def test_future_annotations(self): code = """ @@ -8113,9 +8113,9 @@ def test_basics_functional_syntax(self): self.assertEqual(Emp.__bases__, (dict,)) annos = {'name': str, 'id': int} self.assertEqual(Emp.__annotations__, annos) - self.assertEqual(Emp.__annotate__(annotations.Format.VALUE), annos) - self.assertEqual(Emp.__annotate__(annotations.Format.FORWARDREF), annos) - self.assertEqual(Emp.__annotate__(annotations.Format.SOURCE), {'name': 'str', 'id': 'int'}) + self.assertEqual(Emp.__annotate__(annotationlib.Format.VALUE), annos) + self.assertEqual(Emp.__annotate__(annotationlib.Format.FORWARDREF), annos) + self.assertEqual(Emp.__annotate__(annotationlib.Format.SOURCE), {'name': 'str', 'id': 'int'}) self.assertEqual(Emp.__total__, True) self.assertEqual(Emp.__required_keys__, {'name', 'id'}) self.assertIsInstance(Emp.__required_keys__, frozenset) @@ -8477,7 +8477,7 @@ class A[T](TypedDict): self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T])) self.assertEqual(A.__mro__, (A, Generic, dict, object)) self.assertEqual(A.__annotations__, {'a': T}) - self.assertEqual(A.__annotate__(annotations.Format.SOURCE), {'a': 'T'}) + self.assertEqual(A.__annotate__(annotationlib.Format.SOURCE), {'a': 'T'}) self.assertEqual(A.__parameters__, (T,)) self.assertEqual(A[str].__parameters__, ()) self.assertEqual(A[str].__args__, (str,)) @@ -8490,7 +8490,7 @@ class A(TypedDict, Generic[T]): self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T])) self.assertEqual(A.__mro__, (A, Generic, dict, object)) self.assertEqual(A.__annotations__, {'a': T}) - self.assertEqual(A.__annotate__(annotations.Format.SOURCE), {'a': 'T'}) + self.assertEqual(A.__annotate__(annotationlib.Format.SOURCE), {'a': 'T'}) self.assertEqual(A.__parameters__, (T,)) self.assertEqual(A[str].__parameters__, ()) self.assertEqual(A[str].__args__, (str,)) @@ -8502,7 +8502,7 @@ class A2(Generic[T], TypedDict): self.assertEqual(A2.__orig_bases__, (Generic[T], TypedDict)) self.assertEqual(A2.__mro__, (A2, Generic, dict, object)) self.assertEqual(A2.__annotations__, {'a': T}) - self.assertEqual(A2.__annotate__(annotations.Format.SOURCE), {'a': 'T'}) + self.assertEqual(A2.__annotate__(annotationlib.Format.SOURCE), {'a': 'T'}) self.assertEqual(A2.__parameters__, (T,)) self.assertEqual(A2[str].__parameters__, ()) self.assertEqual(A2[str].__args__, (str,)) @@ -8514,7 +8514,7 @@ class B(A[KT], total=False): self.assertEqual(B.__orig_bases__, (A[KT],)) self.assertEqual(B.__mro__, (B, Generic, dict, object)) self.assertEqual(B.__annotations__, {'a': T, 'b': KT}) - self.assertEqual(B.__annotate__(annotations.Format.SOURCE), {'a': 'T', 'b': 'KT'}) + self.assertEqual(B.__annotate__(annotationlib.Format.SOURCE), {'a': 'T', 'b': 'KT'}) self.assertEqual(B.__parameters__, (KT,)) self.assertEqual(B.__total__, False) self.assertEqual(B.__optional_keys__, frozenset(['b'])) @@ -8539,7 +8539,7 @@ class C(B[int]): 'b': KT, 'c': int, }) - self.assertEqual(C.__annotate__(annotations.Format.SOURCE), { + self.assertEqual(C.__annotate__(annotationlib.Format.SOURCE), { 'a': 'T', 'b': 'KT', 'c': 'int', @@ -8563,7 +8563,7 @@ class Point3D(Point2DGeneric[T], Generic[T, KT]): 'b': T, 'c': KT, }) - self.assertEqual(Point3D.__annotate__(annotations.Format.SOURCE), { + self.assertEqual(Point3D.__annotate__(annotationlib.Format.SOURCE), { 'a': 'T', 'b': 'T', 'c': 'KT', @@ -8599,7 +8599,7 @@ class WithImplicitAny(B): 'b': KT, 'c': int, }) - self.assertEqual(WithImplicitAny.__annotate__(annotations.Format.SOURCE), { + self.assertEqual(WithImplicitAny.__annotate__(annotationlib.Format.SOURCE), { 'a': 'T', 'b': 'KT', 'c': 'int', @@ -8772,7 +8772,7 @@ class Y(TypedDict): b: "int" fwdref = ForwardRef('int', module='test.test_typing') self.assertEqual(Y.__annotations__, {'a': type(None), 'b': fwdref}) - self.assertEqual(Y.__annotate__(annotations.Format.FORWARDREF), {'a': type(None), 'b': fwdref}) + self.assertEqual(Y.__annotate__(annotationlib.Format.FORWARDREF), {'a': type(None), 'b': fwdref}) # _type_check is also applied later class Z(TypedDict): @@ -8803,7 +8803,7 @@ class A(TypedDict): A.__annotations__ self.assertEqual( - A.__annotate__(annotations.Format.SOURCE), + A.__annotate__(annotationlib.Format.SOURCE), {'x': 'NotRequired[undefined]', 'y': 'ReadOnly[undefined]', 'z': 'Required[undefined]'}, ) diff --git a/Lib/typing.py b/Lib/typing.py index 00bedc2aabbdea..c11f9ee70e1015 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -19,8 +19,8 @@ """ from abc import abstractmethod, ABCMeta -import annotations -from annotations import ForwardRef +import annotationlib +from annotationlib import ForwardRef import collections from collections import defaultdict import collections.abc @@ -463,7 +463,7 @@ def __repr__(self): def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=frozenset(), - format=annotations.Format.VALUE, owner=None): + format=annotationlib.Format.VALUE, owner=None): """Evaluate all forward references in the given type t. For use of globalns and localns see the docstring for get_type_hints(). @@ -1032,7 +1032,7 @@ def evaluate_forward_ref( globals=None, locals=None, type_params=None, - format=annotations.Format.VALUE, + format=annotationlib.Format.VALUE, _recursive_guard=frozenset(), ): """Evaluate a forward reference as a type hint. @@ -1059,7 +1059,7 @@ def evaluate_forward_ref( if type_params is _sentinel: _deprecation_warning_for_no_type_params_passed("typing.evaluate_forward_ref") type_params = () - if format == annotations.Format.SOURCE: + if format == annotationlib.Format.SOURCE: return forward_ref.__forward_arg__ if forward_ref.__forward_arg__ in _recursive_guard: return forward_ref @@ -1068,7 +1068,7 @@ def evaluate_forward_ref( value = forward_ref.evaluate(globals=globals, locals=locals, type_params=type_params, owner=owner) except NameError: - if format == annotations.Format.FORWARDREF: + if format == annotationlib.Format.FORWARDREF: return forward_ref else: raise @@ -2168,7 +2168,7 @@ class _AnnotatedAlias(_NotIterable, _GenericAlias, _root=True): """Runtime representation of an annotated type. At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't' - with extra annotations. The alias behaves like a normal typing alias. + with extra annotationlib. The alias behaves like a normal typing alias. Instantiating is the same as instantiating the underlying type; binding it to types is also the same. @@ -2353,7 +2353,7 @@ def greet(name: str) -> None: def get_type_hints(obj, globalns=None, localns=None, include_extras=False, - *, format=annotations.Format.VALUE): + *, format=annotationlib.Format.VALUE): """Return type hints for an object. This is often the same as obj.__annotations__, but it handles @@ -2390,8 +2390,8 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, if isinstance(obj, type): hints = {} for base in reversed(obj.__mro__): - ann = annotations.get_annotations(base, format=format) - if format is annotations.Format.SOURCE: + ann = annotationlib.get_annotations(base, format=format) + if format is annotationlib.Format.SOURCE: hints.update(ann) continue if globalns is None: @@ -2415,14 +2415,14 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, value = _eval_type(value, base_globals, base_locals, base.__type_params__, format=format, owner=obj) hints[name] = value - if include_extras or format is annotations.Format.SOURCE: + if include_extras or format is annotationlib.Format.SOURCE: return hints else: return {k: _strip_annotations(t) for k, t in hints.items()} - hints = annotations.get_annotations(obj, format=format) + hints = annotationlib.get_annotations(obj, format=format) hints = dict(hints) - if format is annotations.Format.SOURCE: + if format is annotationlib.Format.SOURCE: return hints if globalns is None: @@ -2938,7 +2938,7 @@ def _make_eager_annotate(types): checked_types = {key: _type_check(val, f"field {key} annotation must be a type") for key, val in types.items()} def annotate(format): - if format in (annotations.Format.VALUE, annotations.Format.FORWARDREF): + if format in (annotationlib.Format.VALUE, annotationlib.Format.FORWARDREF): return checked_types else: return _convert_to_source(types) @@ -2971,7 +2971,7 @@ def __new__(cls, typename, bases, ns): annotate = _make_eager_annotate(types) elif "__annotate__" in ns: original_annotate = ns["__annotate__"] - types = annotations.call_annotate_function(original_annotate, annotations.Format.FORWARDREF) + types = annotationlib.call_annotate_function(original_annotate, annotationlib.Format.FORWARDREF) field_names = list(types) # For backward compatibility, type-check all the types at creation time @@ -2979,8 +2979,8 @@ def __new__(cls, typename, bases, ns): _type_check(typ, "field annotation must be a type") def annotate(format): - annos = annotations.call_annotate_function(original_annotate, format) - if format != annotations.Format.SOURCE: + annos = annotationlib.call_annotate_function(original_annotate, format) + if format != annotationlib.Format.SOURCE: return {key: _type_check(val, f"field {key} annotation must be a type") for key, val in annos.items()} return annos @@ -3156,8 +3156,8 @@ def __new__(cls, name, bases, ns, total=True): own_annotations = ns["__annotations__"] elif "__annotate__" in ns: own_annotate = ns["__annotate__"] - own_annotations = annotations.call_annotate_function( - own_annotate, annotations.Format.FORWARDREF, owner=tp_dict + own_annotations = annotationlib.call_annotate_function( + own_annotate, annotationlib.Format.FORWARDREF, owner=tp_dict ) else: own_annotate = None @@ -3224,16 +3224,16 @@ def __annotate__(format): base_annotate = base.__annotate__ if base_annotate is None: continue - base_annos = annotations.call_annotate_function(base.__annotate__, format, owner=base) + base_annos = annotationlib.call_annotate_function(base.__annotate__, format, owner=base) annos.update(base_annos) if own_annotate is not None: - own = annotations.call_annotate_function(own_annotate, format, owner=tp_dict) - if format != annotations.Format.SOURCE: + own = annotationlib.call_annotate_function(own_annotate, format, owner=tp_dict) + if format != annotationlib.Format.SOURCE: own = { n: _type_check(tp, msg, module=tp_dict.__module__) for n, tp in own.items() } - elif format == annotations.Format.SOURCE: + elif format == annotationlib.Format.SOURCE: own = _convert_to_source(own_annotations) else: own = own_checked_annotations diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h index db178c7ebc075c..4d595d98445a05 100644 --- a/Python/stdlib_module_names.h +++ b/Python/stdlib_module_names.h @@ -99,7 +99,7 @@ static const char* _Py_stdlib_module_names[] = { "_winapi", "_zoneinfo", "abc", -"annotations", +"annotationlib", "antigravity", "argparse", "array", From 62ed45f6df29ff735471d05bf44b9c2ab64ced4c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 11 Jun 2024 07:17:32 -0600 Subject: [PATCH 095/105] Add blurb --- .../Library/2024-06-11-07-17-25.gh-issue-119180.iH-2zy.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-06-11-07-17-25.gh-issue-119180.iH-2zy.rst diff --git a/Misc/NEWS.d/next/Library/2024-06-11-07-17-25.gh-issue-119180.iH-2zy.rst b/Misc/NEWS.d/next/Library/2024-06-11-07-17-25.gh-issue-119180.iH-2zy.rst new file mode 100644 index 00000000000000..f24d7bd6b9d26c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-06-11-07-17-25.gh-issue-119180.iH-2zy.rst @@ -0,0 +1,4 @@ +As part of implementing :pep:`649` and :pep:`749`, add a new module +``annotationlib``. Add support for unresolved forward references in +annotations to :mod:`dataclasses`, :class:`typing.TypedDict`, and +:class:`typing.NamedTuple`. From 93a932d8876dcf2bcc448d8faa6f6ae8125bd28e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 11 Jun 2024 07:17:38 -0600 Subject: [PATCH 096/105] Update functools --- Lib/functools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/functools.py b/Lib/functools.py index 52d807982e2b8e..6e0bce5694cff5 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -889,7 +889,7 @@ def register(cls, func=None): # only import typing if annotation parsing is necessary from typing import get_type_hints - from annotations import Format, ForwardRef + from annotationlib import Format, ForwardRef argname, cls = next(iter(get_type_hints(func, format=Format.FORWARDREF).items())) if not _is_valid_dispatch_type(cls): if _is_union_type(cls): From 7586ed160064d223059fd1ea4a9664edcd847b58 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 11 Jun 2024 07:36:22 -0600 Subject: [PATCH 097/105] Update Lib/typing.py Co-authored-by: Alex Waygood --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index c11f9ee70e1015..73c3c9b070eb59 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2168,7 +2168,7 @@ class _AnnotatedAlias(_NotIterable, _GenericAlias, _root=True): """Runtime representation of an annotated type. At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't' - with extra annotationlib. The alias behaves like a normal typing alias. + with extra metadata. The alias behaves like a normal typing alias. Instantiating is the same as instantiating the underlying type; binding it to types is also the same. From 064769aea3170e1f5628b4a88d750b1733bd77b7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 11 Jun 2024 13:04:20 -0600 Subject: [PATCH 098/105] Update Lib/annotationlib.py Co-authored-by: Alex Waygood --- Lib/annotationlib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 7323ecef947785..4f0a7375879554 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -157,7 +157,9 @@ def __forward_arg__(self): if self.__ast_node__ is not None: self.__arg__ = ast.unparse(self.__ast_node__) return self.__arg__ - raise RuntimeError("Forward reference is not initialized") + raise AssertionError( + "Attempted to access '__forward_arg__' on an uninitialized ForwardRef" + ) @property def __forward_code__(self): From c8510eaf32631083e39021c1ff808aa29faab3e2 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 12 Jun 2024 20:07:12 -0700 Subject: [PATCH 099/105] Review feedback --- Doc/howto/descriptor.rst | 4 +- Lib/annotationlib.py | 107 +++++++++++++++++++-------------------- Lib/typing.py | 1 + 3 files changed, 56 insertions(+), 56 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 642367089a92fc..67e981f9c57abe 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1373,8 +1373,8 @@ Using the non-data descriptor protocol, a pure Python version of The :func:`functools.update_wrapper` call adds a ``__wrapped__`` attribute that refers to the underlying function. Also it carries forward the attributes necessary to make the wrapper look like the wrapped -function: :attr:`~function.__name__`, :attr:`~function.__qualname__`, -:attr:`~function.__doc__`, and :attr:`~function.__annotations__`. +function, including :attr:`~function.__name__`, :attr:`~function.__qualname__`, +and :attr:`~function.__doc__`. .. testcode:: :hide: diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 4f0a7375879554..6cbad919a46607 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -6,6 +6,8 @@ import sys import types +__all__ = ["Format", "ForwardRef", "call_annotate_function", "get_annotations"] + class Format(enum.IntEnum): VALUE = 1 @@ -19,7 +21,7 @@ class Format(enum.IntEnum): # Slots shared by ForwardRef and _Stringifier. The __forward__ names must be # preserved for compatibility with the old typing.ForwardRef class. The remaining # names are private. -_slots = ( +_SLOTS = ( "__forward_evaluated__", "__forward_value__", "__forward_is_argument__", @@ -36,9 +38,9 @@ class Format(enum.IntEnum): class ForwardRef: - """Internal wrapper to hold a forward reference.""" + """Wrapper that holds a forward reference.""" - __slots__ = _slots + __slots__ = _SLOTS def __init__( self, @@ -87,8 +89,8 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None): return value if owner is None: owner = self.__owner__ - if owner is None and type_params is None: - raise TypeError("Either 'owner' or 'type_params' must be provided") + if type_params is None and owner is None: + raise TypeError("Either 'type_params' or 'owner' must be provided") if globals is None: globals = self.__globals__ @@ -218,7 +220,7 @@ def __repr__(self): class _Stringifier: # Must match the slots on ForwardRef, so we can turn an instance of one into an # instance of the other in place. - __slots__ = _slots + __slots__ = _SLOTS def __init__(self, node, globals=None, owner=None, is_class=False, cell=None): assert isinstance(node, ast.AST) @@ -251,6 +253,51 @@ def __make_new(self, node): node, self.__globals__, self.__owner__, self.__forward_is_class__ ) + # Must implement this since we set __eq__. We hash by identity so that + # stringifiers in dict keys are kept separate. + def __hash__(self): + return id(self) + + def __getitem__(self, other): + # Special case, to avoid stringifying references to class-scoped variables + # as '__classdict__["x"]'. + if ( + isinstance(self.__ast_node__, ast.Name) + and self.__ast_node__.id == "__classdict__" + ): + raise KeyError + if isinstance(other, tuple): + elts = [self.__convert(elt) for elt in other] + other = ast.Tuple(elts) + else: + other = self.__convert(other) + assert isinstance(other, ast.AST), repr(other) + return self.__make_new(ast.Subscript(self.__ast_node__, other)) + + def __getattr__(self, attr): + return self.__make_new(ast.Attribute(self.__ast_node__, attr)) + + def __call__(self, *args, **kwargs): + return self.__make_new( + ast.Call( + self.__ast_node__, + [self.__convert(arg) for arg in args], + [ + ast.keyword(key, self.__convert(value)) + for key, value in kwargs.items() + ], + ) + ) + + def __iter__(self): + yield self.__make_new(ast.Starred(self.__ast_node__)) + + def __repr__(self): + return ast.unparse(self.__ast_node__) + + def __format__(self, format_spec): + raise TypeError("Cannot stringify annotation containing string formatting") + def _make_binop(op: ast.AST): def binop(self, other): return self.__make_new( @@ -320,14 +367,6 @@ def compare(self, other): del _make_compare - # Doesn't work because the return type is always coerced to a bool - # __contains__ = _make_compare(ast.In()) - - # Must implement this since we set __eq__. We hash by identity so that - # stringifiers in dict keys are kept separate. - def __hash__(self): - return id(self) - def _make_unary_op(op): def unary_op(self): return self.__make_new(ast.UnaryOp(op, self.__ast_node__)) @@ -340,46 +379,6 @@ def unary_op(self): del _make_unary_op - def __getitem__(self, other): - # Special case, to avoid stringifying references to class-scoped variables - # as '__classdict__["x"]'. - if ( - isinstance(self.__ast_node__, ast.Name) - and self.__ast_node__.id == "__classdict__" - ): - raise KeyError - if isinstance(other, tuple): - elts = [self.__convert(elt) for elt in other] - other = ast.Tuple(elts) - else: - other = self.__convert(other) - assert isinstance(other, ast.AST), repr(other) - return self.__make_new(ast.Subscript(self.__ast_node__, other)) - - def __getattr__(self, attr): - return self.__make_new(ast.Attribute(self.__ast_node__, attr)) - - def __call__(self, *args, **kwargs): - return self.__make_new( - ast.Call( - self.__ast_node__, - [self.__convert(arg) for arg in args], - [ - ast.keyword(key, self.__convert(value)) - for key, value in kwargs.items() - ], - ) - ) - - def __iter__(self): - yield self.__make_new(ast.Starred(self.__ast_node__)) - - def __repr__(self): - return ast.unparse(self.__ast_node__) - - def __format__(self, format_spec): - raise TypeError("Cannot stringify annotation containing string formatting") - class _StringifierDict(dict): def __init__(self, namespace, globals=None, owner=None, is_class=False): diff --git a/Lib/typing.py b/Lib/typing.py index 73c3c9b070eb59..3db05cf7466ba0 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1039,6 +1039,7 @@ def evaluate_forward_ref( This is similar to calling the ForwardRef.evaluate() method, but unlike that method, evaluate_forward_ref() also: + * Recursively evaluates forward references nested within the type hint. * Rejects certain objects that are not valid type hints. * Replaces type hints that evaluate to None with types.NoneType. From 9fd65bf88c4a5f50c5a0a69b312dbc44d1766023 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 13 Jun 2024 08:51:44 -0700 Subject: [PATCH 100/105] Add some tests --- Lib/annotationlib.py | 24 ++++++++++++------------ Lib/test/test_annotationlib.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 6cbad919a46607..b8d61b812fd2e9 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -571,6 +571,18 @@ def get_annotations( return dict(ann) ann = getattr(obj, "__annotations__", None) + if ann is None: + return {} + + if not isinstance(ann, dict): + raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") + + if not ann: + return {} + + if not eval_str: + return dict(ann) + if isinstance(obj, type): # class obj_globals = None @@ -598,18 +610,6 @@ def get_annotations( else: raise TypeError(f"{obj!r} is not a module, class, or callable.") - if ann is None: - return {} - - if not isinstance(ann, dict): - raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") - - if not ann: - return {} - - if not eval_str: - return dict(ann) - if unwrap is not None: while True: if hasattr(unwrap, "__wrapped__"): diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index dfe49769f2ee32..5ae8551669161a 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -252,6 +252,34 @@ def test_builtin_type(self): self.assertEqual(annotationlib.get_annotations(int), {}) self.assertEqual(annotationlib.get_annotations(object), {}) + def test_custom_metaclass(self): + class Meta(type): + pass + + class C(metaclass=Meta): + x: int + + self.assertEqual(annotationlib.get_annotations(C), {"x": int}) + + def test_missing_dunder_dict(self): + class NoDict(type): + @property + def __dict__(cls): + raise AttributeError + + class C1(metaclass=NoDict): + a: int + + self.assertEqual(annotationlib.get_annotations(C1), {"a": int}) + self.assertEqual( + annotationlib.get_annotations(C1, format=annotationlib.Format.FORWARDREF), + {"a": int}, + ) + self.assertEqual( + annotationlib.get_annotations(C1, format=annotationlib.Format.SOURCE), + {"a": "int"}, + ) + def test_format(self): def f1(a: int): pass From 288fad9a77ee2a0d5d022369d59f649e758f9842 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 16 Jun 2024 16:46:03 -0700 Subject: [PATCH 101/105] unused imports --- Lib/test/test_inspect/test_inspect.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index ce573ca2223d6a..ff12e81e8b28d8 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -46,8 +46,6 @@ from test.test_inspect import inspect_fodder as mod from test.test_inspect import inspect_fodder2 as mod2 from test.test_inspect import inspect_stringized_annotations -from test.test_inspect import inspect_stringized_annotations_2 -from test.test_inspect import inspect_stringized_annotations_pep695 # Functions tested in this suite: From 2e376e0796af0156c51208bbccf2e138d3cdc844 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 16 Jun 2024 16:47:51 -0700 Subject: [PATCH 102/105] fix test_annotationlib --- Lib/annotationlib.py | 8 ++++++++ Lib/test/test_annotationlib.py | 1 + 2 files changed, 9 insertions(+) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index b8d61b812fd2e9..a79091da36b134 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -627,6 +627,14 @@ def get_annotations( if locals is None: locals = obj_locals + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + if type_params := getattr(obj, "__type_params__", ()): + if locals is None: + locals = {} + locals = {param.__name__: param for param in type_params} | locals + return_value = { key: value if not isinstance(value, str) else eval(value, globals, locals) for key, value in ann.items() diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 7da14331f599f8..ecf113e791727f 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -4,6 +4,7 @@ import functools import pickle import unittest +from typing import Unpack from test.test_inspect import inspect_stock_annotations from test.test_inspect import inspect_stringized_annotations From 2a1c9a91c684c1d49cf0743ae25917d41f18f1c9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 16 Jun 2024 17:45:28 -0700 Subject: [PATCH 103/105] Fix tests --- Lib/test/test_annotationlib.py | 11 +++++++++++ Lib/typing.py | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index ecf113e791727f..d3c0e74657f7e6 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -269,6 +269,8 @@ class NoDict(type): def __dict__(cls): raise AttributeError + b: str + class C1(metaclass=NoDict): a: int @@ -281,6 +283,15 @@ class C1(metaclass=NoDict): annotationlib.get_annotations(C1, format=annotationlib.Format.SOURCE), {"a": "int"}, ) + self.assertEqual(annotationlib.get_annotations(NoDict), {"b": str}) + self.assertEqual( + annotationlib.get_annotations(NoDict, format=annotationlib.Format.FORWARDREF), + {"b": str}, + ) + self.assertEqual( + annotationlib.get_annotations(NoDict, format=annotationlib.Format.SOURCE), + {"b": "str"}, + ) def test_format(self): def f1(a: int): diff --git a/Lib/typing.py b/Lib/typing.py index 3db05cf7466ba0..626053d8166160 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2422,7 +2422,14 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, return {k: _strip_annotations(t) for k, t in hints.items()} hints = annotationlib.get_annotations(obj, format=format) - hints = dict(hints) + if ( + not hints + and not isinstance(obj, types.ModuleType) + and not callable(obj) + and not hasattr(obj, '__annotations__') + and not hasattr(obj, '__annotate__') + ): + raise TypeError(f"{obj!r} is not a module, class, or callable.") if format is annotationlib.Format.SOURCE: return hints From 1f3a63c9172f2660dc18728a61b708f49a37eeae Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 20 Jul 2024 19:25:19 -0700 Subject: [PATCH 104/105] Reapply #120272 --- Lib/annotationlib.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index a79091da36b134..b76c24ccb4432f 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -120,8 +120,21 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None): # (unless they are shadowed by assignments *in* the local namespace), # as a way of emulating annotation scopes when calling `eval()` type_params = getattr(self.__owner__, "__type_params__", None) + + # type parameters require some special handling, + # as they exist in their own scope + # but `eval()` does not have a dedicated parameter for that scope. + # For classes, names in type parameter scopes should override + # names in the global scope (which here are called `localns`!), + # but should in turn be overridden by names in the class scope + # (which here are called `globalns`!) if type_params is not None: - locals = {param.__name__: param for param in type_params} | locals + globals, locals = dict(globals), dict(locals) + for param in type_params: + param_name = param.__name__ + if not self.__forward_is_class__ or param_name not in globals: + globals[param_name] = param + locals.pop(param_name, None) code = self.__forward_code__ value = eval(code, globals=globals, locals=locals) From 477f8c6fd438c479994717a105dc961dda6cecda Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 23 Jul 2024 13:46:56 -0700 Subject: [PATCH 105/105] Feedback from Carl --- Lib/annotationlib.py | 8 ++++---- Lib/test/test_annotationlib.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index b76c24ccb4432f..b4036ffb189c2d 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -92,6 +92,10 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None): if type_params is None and owner is None: raise TypeError("Either 'type_params' or 'owner' must be provided") + if self.__forward_module__ is not None: + globals = getattr( + sys.modules.get(self.__forward_module__, None), "__dict__", globals + ) if globals is None: globals = self.__globals__ if globals is None: @@ -105,10 +109,6 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None): globals = getattr(owner, "__dict__", None) elif callable(owner): globals = getattr(owner, "__globals__", None) - if self.__forward_module__ is not None: - globals = getattr( - sys.modules.get(self.__forward_module__, None), "__dict__", globals - ) if locals is None: locals = {} diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index d3c0e74657f7e6..e68d63c91d1a73 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -88,7 +88,7 @@ def f(x: int, y: doesntexist): def test_expressions(self): def f( add: a + b, - sub: a + b, + sub: a - b, mul: a * b, matmul: a @ b, truediv: a / b, @@ -121,7 +121,7 @@ def f( anno, { "add": "a + b", - "sub": "a + b", + "sub": "a - b", "mul": "a * b", "matmul": "a @ b", "truediv": "a / b", @@ -327,7 +327,7 @@ def f2(a: undefined): def test_custom_object_with_annotations(self): class C: - def __init__(self, x: int = 0, y: str = ""): + def __init__(self): self.__annotations__ = {"x": int, "y": str} self.assertEqual(annotationlib.get_annotations(C()), {"x": int, "y": str})