From 83a3de4e0632d90e0d1d5a9b8859a94c9ac25f65 Mon Sep 17 00:00:00 2001 From: Ofey Chan Date: Fri, 30 Sep 2022 16:43:02 +0800 Subject: [PATCH] gh-96348: Deprecate the 3-arg signature of coroutine.throw and generator.throw (GH-96428) --- Doc/reference/datamodel.rst | 5 +++ Doc/reference/expressions.rst | 13 +++++++- Doc/whatsnew/3.12.rst | 5 +++ Lib/contextlib.py | 4 +-- Lib/test/test_asyncgen.py | 22 +++++++++---- Lib/test/test_asyncio/test_futures.py | 13 +++++--- Lib/test/test_coroutines.py | 9 +++++- Lib/test/test_generators.py | 21 ++++++++++++ Lib/test/test_types.py | 2 +- Misc/ACKS | 1 + ...2-08-31-18-46-13.gh-issue-96348.xzCoTP.rst | 2 ++ Modules/_asynciomodule.c | 8 +++++ Objects/genobject.c | 32 +++++++++++++++++-- Objects/iterobject.c | 9 ++++-- 14 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-08-31-18-46-13.gh-issue-96348.xzCoTP.rst diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 758f3aef3ee..c93269ab04b 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -2996,6 +2996,11 @@ generators, coroutines do not directly support iteration. above. If the exception is not caught in the coroutine, it propagates back to the caller. + .. versionchanged:: 3.12 + + The second signature \(type\[, value\[, traceback\]\]\) is deprecated and + may be removed in a future version of Python. + .. method:: coroutine.close() Causes the coroutine to clean itself up and exit. If the coroutine diff --git a/Doc/reference/expressions.rst b/Doc/reference/expressions.rst index 6d23e473cdc..a6ca55dafe5 100644 --- a/Doc/reference/expressions.rst +++ b/Doc/reference/expressions.rst @@ -582,6 +582,11 @@ is already executing raises a :exc:`ValueError` exception. :attr:`~BaseException.__traceback__` attribute stored in *value* may be cleared. + .. versionchanged:: 3.12 + + The second signature \(type\[, value\[, traceback\]\]\) is deprecated and + may be removed in a future version of Python. + .. index:: exception: GeneratorExit @@ -738,7 +743,8 @@ which are used to control the execution of a generator function. because there is no yield expression that could receive the value. -.. coroutinemethod:: agen.athrow(type[, value[, traceback]]) +.. coroutinemethod:: agen.athrow(value) + agen.athrow(type[, value[, traceback]]) Returns an awaitable that raises an exception of type ``type`` at the point where the asynchronous generator was paused, and returns the next value @@ -750,6 +756,11 @@ which are used to control the execution of a generator function. raises a different exception, then when the awaitable is run that exception propagates to the caller of the awaitable. + .. versionchanged:: 3.12 + + The second signature \(type\[, value\[, traceback\]\]\) is deprecated and + may be removed in a future version of Python. + .. index:: exception: GeneratorExit diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 2a31160decc..e14a2bd0133 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -182,6 +182,11 @@ Deprecated and tailor them to your needs. (Contributed by Erlend E. Aasland in :gh:`90016`.) +* The 3-arg signatures (type, value, traceback) of :meth:`~coroutine.throw`, + :meth:`~generator.throw` and :meth:`~agen.athrow` are deprecated and + may be removed in a future version of Python. Use the single-arg versions + of these functions instead. (Contributed by Ofey Chan in :gh:`89874`.) + Pending Removal in Python 3.13 ------------------------------ diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 625bb33b12d..d5822219b3e 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -152,7 +152,7 @@ class _GeneratorContextManager( # tell if we get the same exception back value = typ() try: - self.gen.throw(typ, value, traceback) + self.gen.throw(value) except StopIteration as exc: # Suppress StopIteration *unless* it's the same exception that # was passed to throw(). This prevents a StopIteration @@ -219,7 +219,7 @@ class _AsyncGeneratorContextManager( # tell if we get the same exception back value = typ() try: - await self.gen.athrow(typ, value, traceback) + await self.gen.athrow(value) except StopAsyncIteration as exc: # Suppress StopIteration *unless* it's the same exception that # was passed to throw(). This prevents a StopIteration diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py index fb22f411c2e..f6184c0cab4 100644 --- a/Lib/test/test_asyncgen.py +++ b/Lib/test/test_asyncgen.py @@ -2,6 +2,7 @@ import inspect import types import unittest import contextlib +import warnings from test.support.import_helper import import_module from test.support import gc_collect, requires_working_socket @@ -377,6 +378,13 @@ class AsyncGenTest(unittest.TestCase): self.compare_generators(sync_gen_wrapper(), async_gen_wrapper()) + def test_async_gen_3_arg_deprecation_warning(self): + async def gen(): + yield 123 + + with self.assertWarns(DeprecationWarning): + gen().athrow(GeneratorExit, GeneratorExit(), None) + def test_async_gen_api_01(self): async def gen(): yield 123 @@ -650,7 +658,7 @@ class AsyncGenAsyncioTest(unittest.TestCase): agen = agenfn() with contextlib.closing(anext(agen, "default").__await__()) as g: self.assertEqual(g.send(None), 1) - self.assertEqual(g.throw(MyError, MyError(), None), 2) + self.assertEqual(g.throw(MyError()), 2) try: g.send(None) except StopIteration as e: @@ -663,9 +671,9 @@ class AsyncGenAsyncioTest(unittest.TestCase): agen = agenfn() with contextlib.closing(anext(agen, "default").__await__()) as g: self.assertEqual(g.send(None), 1) - self.assertEqual(g.throw(MyError, MyError(), None), 2) + self.assertEqual(g.throw(MyError()), 2) with self.assertRaises(MyError): - g.throw(MyError, MyError(), None) + g.throw(MyError()) def test3(anext): agen = agenfn() @@ -692,9 +700,9 @@ class AsyncGenAsyncioTest(unittest.TestCase): agen = agenfn() with contextlib.closing(anext(agen, "default").__await__()) as g: self.assertEqual(g.send(None), 10) - self.assertEqual(g.throw(MyError, MyError(), None), 20) + self.assertEqual(g.throw(MyError()), 20) with self.assertRaisesRegex(MyError, 'val'): - g.throw(MyError, MyError('val'), None) + g.throw(MyError('val')) def test5(anext): @types.coroutine @@ -713,7 +721,7 @@ class AsyncGenAsyncioTest(unittest.TestCase): with contextlib.closing(anext(agen, "default").__await__()) as g: self.assertEqual(g.send(None), 10) with self.assertRaisesRegex(StopIteration, 'default'): - g.throw(MyError, MyError(), None) + g.throw(MyError()) def test6(anext): @types.coroutine @@ -728,7 +736,7 @@ class AsyncGenAsyncioTest(unittest.TestCase): agen = agenfn() with contextlib.closing(anext(agen, "default").__await__()) as g: with self.assertRaises(MyError): - g.throw(MyError, MyError(), None) + g.throw(MyError()) def run_test(test): with self.subTest('pure-Python anext()'): diff --git a/Lib/test/test_asyncio/test_futures.py b/Lib/test/test_asyncio/test_futures.py index f4a46ec90a1..11d42739308 100644 --- a/Lib/test/test_asyncio/test_futures.py +++ b/Lib/test/test_asyncio/test_futures.py @@ -10,6 +10,7 @@ from unittest import mock from types import GenericAlias import asyncio from asyncio import futures +import warnings from test.test_asyncio import utils as test_utils from test import support @@ -619,10 +620,14 @@ class BaseFutureTests: def test_future_iter_throw(self): fut = self._new_future(loop=self.loop) fi = iter(fut) - self.assertRaises(TypeError, fi.throw, - Exception, Exception("elephant"), 32) - self.assertRaises(TypeError, fi.throw, - Exception("elephant"), Exception("elephant")) + with self.assertWarns(DeprecationWarning): + self.assertRaises(Exception, fi.throw, Exception, Exception("zebra"), None) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + self.assertRaises(TypeError, fi.throw, + Exception, Exception("elephant"), 32) + self.assertRaises(TypeError, fi.throw, + Exception("elephant"), Exception("elephant")) self.assertRaises(TypeError, fi.throw, list) def test_future_del_collect(self): diff --git a/Lib/test/test_coroutines.py b/Lib/test/test_coroutines.py index 8fff2d47c10..9a2279d353f 100644 --- a/Lib/test/test_coroutines.py +++ b/Lib/test/test_coroutines.py @@ -709,9 +709,16 @@ class CoroutineTest(unittest.TestCase): aw = coro.__await__() next(aw) with self.assertRaises(ZeroDivisionError): - aw.throw(ZeroDivisionError, None, None) + aw.throw(ZeroDivisionError()) self.assertEqual(N, 102) + coro = foo() + aw = coro.__await__() + next(aw) + with self.assertRaises(ZeroDivisionError): + with self.assertWarns(DeprecationWarning): + aw.throw(ZeroDivisionError, ZeroDivisionError(), None) + def test_func_11(self): async def func(): pass coro = func() diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index e5aa7da1e0d..fb2d9ced063 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -342,6 +342,15 @@ class ExceptionTest(unittest.TestCase): with self.assertRaises(StopIteration): gen.throw(E) + def test_gen_3_arg_deprecation_warning(self): + def g(): + yield 42 + + gen = g() + with self.assertWarns(DeprecationWarning): + with self.assertRaises(TypeError): + gen.throw(TypeError, TypeError(24), None) + def test_stopiteration_error(self): # See also PEP 479. @@ -2113,6 +2122,12 @@ caught ValueError () >>> g.throw(ValueError("xyz")) # value only caught ValueError (xyz) +>>> import warnings +>>> warnings.filterwarnings("ignore", category=DeprecationWarning) + +# Filter DeprecationWarning: regarding the (type, val, tb) signature of throw(). +# Deprecation warnings are re-enabled below. + >>> g.throw(ValueError, ValueError(1)) # value+matching type caught ValueError (1) @@ -2181,6 +2196,12 @@ Traceback (most recent call last): ... ValueError: 7 +>>> warnings.filters.pop(0) +('ignore', None, , None, 0) + +# Re-enable DeprecationWarning: the (type, val, tb) exception representation is deprecated, +# and may be removed in a future version of Python. + Plain "raise" inside a generator should preserve the traceback (#13188). The traceback should have 3 levels: - g.throw() diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index f00da0a758d..af095632a36 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -2072,7 +2072,7 @@ class CoroutineTests(unittest.TestCase): wrapper = foo() wrapper.send(None) with self.assertRaisesRegex(Exception, 'ham'): - wrapper.throw(Exception, Exception('ham')) + wrapper.throw(Exception('ham')) # decorate foo second time foo = types.coroutine(foo) diff --git a/Misc/ACKS b/Misc/ACKS index 0edea9219d0..fc0e745e28c 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -297,6 +297,7 @@ Michael Cetrulo Dave Chambers Pascal Chambon Nicholas Chammas +Ofey Chan John Chandler Hye-Shik Chang Jeffrey Chang diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-08-31-18-46-13.gh-issue-96348.xzCoTP.rst b/Misc/NEWS.d/next/Core and Builtins/2022-08-31-18-46-13.gh-issue-96348.xzCoTP.rst new file mode 100644 index 00000000000..5d3bd17b578 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-08-31-18-46-13.gh-issue-96348.xzCoTP.rst @@ -0,0 +1,2 @@ +Emit a DeprecationWarning when :meth:`~generator.throw`, :meth:`~coroutine.throw` or :meth:`~agen.athrow` +are called with more than one argument. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 9d2f83bf6c7..5a5881b873e 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -1668,6 +1668,14 @@ FutureIter_throw(futureiterobject *self, PyObject *const *args, Py_ssize_t nargs if (!_PyArg_CheckPositional("throw", nargs, 1, 3)) { return NULL; } + if (nargs > 1) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "the (type, exc, tb) signature of throw() is deprecated, " + "use the single-arg signature instead.", + 1) < 0) { + return NULL; + } + } type = args[0]; if (nargs == 3) { diff --git a/Objects/genobject.c b/Objects/genobject.c index da4afecc69c..ad4fbed6d8d 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -418,7 +418,9 @@ PyDoc_STRVAR(throw_doc, throw(type[,value[,tb]])\n\ \n\ Raise exception in generator, return next yielded value or raise\n\ -StopIteration."); +StopIteration.\n\ +the (type, val, tb) signature is deprecated, \n\ +and may be removed in a future version of Python."); static PyObject * _gen_throw(PyGenObject *gen, int close_on_genexit, @@ -559,6 +561,14 @@ gen_throw(PyGenObject *gen, PyObject *const *args, Py_ssize_t nargs) if (!_PyArg_CheckPositional("throw", nargs, 1, 3)) { return NULL; } + if (nargs > 1) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "the (type, exc, tb) signature of throw() is deprecated, " + "use the single-arg signature instead.", + 1) < 0) { + return NULL; + } + } typ = args[0]; if (nargs == 3) { val = args[1]; @@ -1147,7 +1157,10 @@ PyDoc_STRVAR(coro_throw_doc, throw(type[,value[,traceback]])\n\ \n\ Raise exception in coroutine, return next iterated value or raise\n\ -StopIteration."); +StopIteration.\n\ +the (type, val, tb) signature is deprecated, \n\ +and may be removed in a future version of Python."); + PyDoc_STRVAR(coro_close_doc, "close() -> raise GeneratorExit inside coroutine."); @@ -1500,6 +1513,14 @@ async_gen_aclose(PyAsyncGenObject *o, PyObject *arg) static PyObject * async_gen_athrow(PyAsyncGenObject *o, PyObject *args) { + if (PyTuple_GET_SIZE(args) > 1) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "the (type, exc, tb) signature of athrow() is deprecated, " + "use the single-arg signature instead.", + 1) < 0) { + return NULL; + } + } if (async_gen_init_hooks(o)) { return NULL; } @@ -1537,7 +1558,12 @@ PyDoc_STRVAR(async_asend_doc, "asend(v) -> send 'v' in generator."); PyDoc_STRVAR(async_athrow_doc, -"athrow(typ[,val[,tb]]) -> raise exception in generator."); +"athrow(value)\n\ +athrow(type[,value[,tb]])\n\ +\n\ +raise exception in generator.\n\ +the (type, val, tb) signature is deprecated, \n\ +and may be removed in a future version of Python."); static PyMethodDef async_gen_methods[] = { {"asend", (PyCFunction)async_gen_asend, METH_O, async_asend_doc}, diff --git a/Objects/iterobject.c b/Objects/iterobject.c index 1732a037600..62c36146d64 100644 --- a/Objects/iterobject.c +++ b/Objects/iterobject.c @@ -428,8 +428,13 @@ return next yielded value or raise StopIteration."); PyDoc_STRVAR(throw_doc, -"throw(typ[,val[,tb]]) -> raise exception in the wrapped iterator,\n\ -return next yielded value or raise StopIteration."); +"throw(value)\n\ +throw(typ[,val[,tb]])\n\ +\n\ +raise exception in the wrapped iterator, return next yielded value\n\ +or raise StopIteration.\n\ +the (type, val, tb) signature is deprecated, \n\ +and may be removed in a future version of Python."); PyDoc_STRVAR(close_doc,