From 7fc542c88dc8a09d71006a6240943407b83229d0 Mon Sep 17 00:00:00 2001 From: Kumar Aditya <59607654+kumaraditya303@users.noreply.github.com> Date: Fri, 26 May 2023 16:53:29 +0530 Subject: GH-89091: raise `RuntimeWarning` for unawaited async generator methods (#104611) --- .../pycore_global_objects_fini_generated.h | 3 ++ Include/internal/pycore_global_strings.h | 3 ++ Include/internal/pycore_runtime_init_generated.h | 3 ++ Include/internal/pycore_unicodeobject_generated.h | 9 +++++ Include/internal/pycore_warnings.h | 1 + Lib/test/test_asyncgen.py | 38 ++++++++++++++++++++-- .../2023-05-18-12-48-39.gh-issue-89091.FDzRcW.rst | 1 + Objects/genobject.c | 26 +++++++++++++++ Python/_warnings.c | 14 ++++++++ 9 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2023-05-18-12-48-39.gh-issue-89091.FDzRcW.rst diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 5a1993e..a83f8fc 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -781,6 +781,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(a)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(abs_tol)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(access)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(aclose)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(add)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(add_done_callback)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(after_in_child)); @@ -794,7 +795,9 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(arguments)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(argv)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(as_integer_ratio)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(asend)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ast)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(athrow)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(attribute)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(authorizer_callback)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(autocommit)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 6196787..dd6a62f 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -269,6 +269,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(a) STRUCT_FOR_ID(abs_tol) STRUCT_FOR_ID(access) + STRUCT_FOR_ID(aclose) STRUCT_FOR_ID(add) STRUCT_FOR_ID(add_done_callback) STRUCT_FOR_ID(after_in_child) @@ -282,7 +283,9 @@ struct _Py_global_strings { STRUCT_FOR_ID(arguments) STRUCT_FOR_ID(argv) STRUCT_FOR_ID(as_integer_ratio) + STRUCT_FOR_ID(asend) STRUCT_FOR_ID(ast) + STRUCT_FOR_ID(athrow) STRUCT_FOR_ID(attribute) STRUCT_FOR_ID(authorizer_callback) STRUCT_FOR_ID(autocommit) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 59ec49a..d689f71 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -775,6 +775,7 @@ extern "C" { INIT_ID(a), \ INIT_ID(abs_tol), \ INIT_ID(access), \ + INIT_ID(aclose), \ INIT_ID(add), \ INIT_ID(add_done_callback), \ INIT_ID(after_in_child), \ @@ -788,7 +789,9 @@ extern "C" { INIT_ID(arguments), \ INIT_ID(argv), \ INIT_ID(as_integer_ratio), \ + INIT_ID(asend), \ INIT_ID(ast), \ + INIT_ID(athrow), \ INIT_ID(attribute), \ INIT_ID(authorizer_callback), \ INIT_ID(autocommit), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 8f8a067..db6a157 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -648,6 +648,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { string = &_Py_ID(access); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); + string = &_Py_ID(aclose); + assert(_PyUnicode_CheckConsistency(string, 1)); + _PyUnicode_InternInPlace(interp, &string); string = &_Py_ID(add); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); @@ -687,9 +690,15 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { string = &_Py_ID(as_integer_ratio); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); + string = &_Py_ID(asend); + assert(_PyUnicode_CheckConsistency(string, 1)); + _PyUnicode_InternInPlace(interp, &string); string = &_Py_ID(ast); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); + string = &_Py_ID(athrow); + assert(_PyUnicode_CheckConsistency(string, 1)); + _PyUnicode_InternInPlace(interp, &string); string = &_Py_ID(attribute); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); diff --git a/Include/internal/pycore_warnings.h b/Include/internal/pycore_warnings.h index efb4f1c..452d6b9 100644 --- a/Include/internal/pycore_warnings.h +++ b/Include/internal/pycore_warnings.h @@ -22,6 +22,7 @@ extern int _PyWarnings_InitState(PyInterpreterState *interp); PyAPI_FUNC(PyObject*) _PyWarnings_Init(void); extern void _PyErr_WarnUnawaitedCoroutine(PyObject *coro); +extern void _PyErr_WarnUnawaitedAgenMethod(PyAsyncGenObject *agen, PyObject *method); #ifdef __cplusplus } diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py index 09e4010..4f00558 100644 --- a/Lib/test/test_asyncgen.py +++ b/Lib/test/test_asyncgen.py @@ -415,8 +415,9 @@ class AsyncGenTest(unittest.TestCase): self.assertIsInstance(g.ag_frame, types.FrameType) self.assertFalse(g.ag_running) self.assertIsInstance(g.ag_code, types.CodeType) - - self.assertTrue(inspect.isawaitable(g.aclose())) + aclose = g.aclose() + self.assertTrue(inspect.isawaitable(aclose)) + aclose.close() class AsyncGenAsyncioTest(unittest.TestCase): @@ -1693,5 +1694,38 @@ class AsyncGenAsyncioTest(unittest.TestCase): self.loop.run_until_complete(run()) +class TestUnawaitedWarnings(unittest.TestCase): + def test_asend(self): + async def gen(): + yield 1 + + msg = f"coroutine method 'asend' of '{gen.__qualname__}' was never awaited" + with self.assertWarnsRegex(RuntimeWarning, msg): + g = gen() + g.asend(None) + gc_collect() + + def test_athrow(self): + async def gen(): + yield 1 + + msg = f"coroutine method 'athrow' of '{gen.__qualname__}' was never awaited" + with self.assertWarnsRegex(RuntimeWarning, msg): + g = gen() + g.athrow(RuntimeError) + gc_collect() + + def test_aclose(self): + async def gen(): + yield 1 + + msg = f"coroutine method 'aclose' of '{gen.__qualname__}' was never awaited" + with self.assertWarnsRegex(RuntimeWarning, msg): + g = gen() + g.aclose() + gc_collect() + + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-05-18-12-48-39.gh-issue-89091.FDzRcW.rst b/Misc/NEWS.d/next/Core and Builtins/2023-05-18-12-48-39.gh-issue-89091.FDzRcW.rst new file mode 100644 index 0000000..084ea70 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-05-18-12-48-39.gh-issue-89091.FDzRcW.rst @@ -0,0 +1 @@ +Raise :exc:`RuntimeWarning` for unawaited async generator methods like :meth:`~agen.asend`, :meth:`~agen.athrow` and :meth:`~agen.aclose`. Patch by Kumar Aditya. diff --git a/Objects/genobject.c b/Objects/genobject.c index 1abfc83..b40cf41 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -1735,6 +1735,10 @@ async_gen_unwrap_value(PyAsyncGenObject *gen, PyObject *result) static void async_gen_asend_dealloc(PyAsyncGenASend *o) { + if (PyObject_CallFinalizerFromDealloc((PyObject *)o)) { + return; + } + _PyObject_GC_UNTRACK((PyObject *)o); Py_CLEAR(o->ags_gen); Py_CLEAR(o->ags_sendval); @@ -1839,6 +1843,13 @@ async_gen_asend_close(PyAsyncGenASend *o, PyObject *args) Py_RETURN_NONE; } +static void +async_gen_asend_finalize(PyAsyncGenASend *o) +{ + if (o->ags_state == AWAITABLE_STATE_INIT) { + _PyErr_WarnUnawaitedAgenMethod(o->ags_gen, &_Py_ID(asend)); + } +} static PyMethodDef async_gen_asend_methods[] = { {"send", (PyCFunction)async_gen_asend_send, METH_O, send_doc}, @@ -1896,6 +1907,7 @@ PyTypeObject _PyAsyncGenASend_Type = { 0, /* tp_init */ 0, /* tp_alloc */ 0, /* tp_new */ + .tp_finalize = (destructor)async_gen_asend_finalize, }; @@ -2053,6 +2065,10 @@ _PyAsyncGenValueWrapperNew(PyThreadState *tstate, PyObject *val) static void async_gen_athrow_dealloc(PyAsyncGenAThrow *o) { + if (PyObject_CallFinalizerFromDealloc((PyObject *)o)) { + return; + } + _PyObject_GC_UNTRACK((PyObject *)o); Py_CLEAR(o->agt_gen); Py_CLEAR(o->agt_args); @@ -2256,6 +2272,15 @@ async_gen_athrow_close(PyAsyncGenAThrow *o, PyObject *args) } +static void +async_gen_athrow_finalize(PyAsyncGenAThrow *o) +{ + if (o->agt_state == AWAITABLE_STATE_INIT) { + PyObject *method = o->agt_args ? &_Py_ID(athrow) : &_Py_ID(aclose); + _PyErr_WarnUnawaitedAgenMethod(o->agt_gen, method); + } +} + static PyMethodDef async_gen_athrow_methods[] = { {"send", (PyCFunction)async_gen_athrow_send, METH_O, send_doc}, {"throw", _PyCFunction_CAST(async_gen_athrow_throw), @@ -2313,6 +2338,7 @@ PyTypeObject _PyAsyncGenAThrow_Type = { 0, /* tp_init */ 0, /* tp_alloc */ 0, /* tp_new */ + .tp_finalize = (destructor)async_gen_athrow_finalize, }; diff --git a/Python/_warnings.c b/Python/_warnings.c index dec6586..54fa5c5 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -1366,6 +1366,20 @@ exit: } void +_PyErr_WarnUnawaitedAgenMethod(PyAsyncGenObject *agen, PyObject *method) +{ + PyObject *exc = PyErr_GetRaisedException(); + if (_PyErr_WarnFormat((PyObject *)agen, PyExc_RuntimeWarning, 1, + "coroutine method %R of %R was never awaited", + method, agen->ag_qualname) < 0) + { + PyErr_WriteUnraisable((PyObject *)agen); + } + PyErr_SetRaisedException(exc); +} + + +void _PyErr_WarnUnawaitedCoroutine(PyObject *coro) { /* First, we attempt to funnel the warning through -- cgit v0.12