Skip to content

bpo-32363: Disable Task.set_exception() and Task.set_result() #4923

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Lib/asyncio/futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,14 +239,15 @@ def set_exception(self, exception):
self._schedule_callbacks()
self._log_traceback = True

def __iter__(self):
def __await__(self):
if not self.done():
self._asyncio_future_blocking = True
yield self # This tells Task to wait for completion.
assert self.done(), "await wasn't used with future"
if not self.done():
raise RuntimeError("await wasn't used with future")
return self.result() # May raise too.

__await__ = __iter__ # make compatible with 'await' expression
__iter__ = __await__ # make compatible with 'yield from'.


# Needed for testing purposes.
Expand Down
24 changes: 17 additions & 7 deletions Lib/asyncio/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ def all_tasks(loop=None):
return {t for t in _all_tasks if futures._get_loop(t) is loop}


class Task(futures.Future):
class Task(futures._PyFuture): # Inherit Python Task implementation
# from a Python Future implementation.

"""A coroutine wrapped in a Future."""

# An important invariant maintained while a Task not done:
Expand Down Expand Up @@ -107,11 +109,17 @@ def __del__(self):
if self._source_traceback:
context['source_traceback'] = self._source_traceback
self._loop.call_exception_handler(context)
futures.Future.__del__(self)
super().__del__()

def _repr_info(self):
return base_tasks._task_repr_info(self)

def set_result(self, result):
raise RuntimeError('Task does not support set_result operation')

def set_exception(self, exception):
raise RuntimeError('Task does not support set_exception operation')

def get_stack(self, *, limit=None):
"""Return the list of stack frames for this task's coroutine.

Expand Down Expand Up @@ -180,7 +188,9 @@ def cancel(self):
return True

def _step(self, exc=None):
assert not self.done(), f'_step(): already done: {self!r}, {exc!r}'
if self.done():
raise futures.InvalidStateError(
f'_step(): already done: {self!r}, {exc!r}')
if self._must_cancel:
if not isinstance(exc, futures.CancelledError):
exc = futures.CancelledError()
Expand All @@ -201,15 +211,15 @@ def _step(self, exc=None):
if self._must_cancel:
# Task is cancelled right before coro stops.
self._must_cancel = False
self.set_exception(futures.CancelledError())
super().set_exception(futures.CancelledError())
else:
self.set_result(exc.value)
super().set_result(exc.value)
except futures.CancelledError:
super().cancel() # I.e., Future.cancel(self).
except Exception as exc:
self.set_exception(exc)
super().set_exception(exc)
except BaseException as exc:
self.set_exception(exc)
super().set_exception(exc)
raise
else:
blocking = getattr(result, '_asyncio_future_blocking', None)
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_asyncio/test_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,8 @@ def coro():
def test():
arg1, arg2 = coro()

self.assertRaises(AssertionError, test)
with self.assertRaisesRegex(RuntimeError, "await wasn't used"):
test()
fut.cancel()

@mock.patch('asyncio.base_events.logger')
Expand Down
93 changes: 71 additions & 22 deletions Lib/test/test_asyncio/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1332,17 +1332,23 @@ def coro():
self.assertIsNone(task._fut_waiter)
self.assertTrue(fut.cancelled())

def test_step_in_completed_task(self):
def test_task_set_methods(self):
@asyncio.coroutine
def notmuch():
return 'ko'

gen = notmuch()
task = self.new_task(self.loop, gen)
task.set_result('ok')

self.assertRaises(AssertionError, task._step)
gen.close()
with self.assertRaisesRegex(RuntimeError, 'not support set_result'):
task.set_result('ok')

with self.assertRaisesRegex(RuntimeError, 'not support set_exception'):
task.set_exception(ValueError())

self.assertEqual(
self.loop.run_until_complete(task),
'ko')

def test_step_result(self):
@asyncio.coroutine
Expand Down Expand Up @@ -2231,10 +2237,59 @@ async def func():
return cls


class SetMethodsTest:

def test_set_result_causes_invalid_state(self):
Future = type(self).Future
self.loop.call_exception_handler = exc_handler = mock.Mock()

async def foo():
await asyncio.sleep(0.1, loop=self.loop)
return 10

task = self.new_task(self.loop, foo())
Future.set_result(task, 'spam')

self.assertEqual(
self.loop.run_until_complete(task),
'spam')

exc_handler.assert_called_once()
exc = exc_handler.call_args[0][0]['exception']
with self.assertRaisesRegex(asyncio.InvalidStateError,
r'step\(\): already done'):
raise exc

def test_set_exception_causes_invalid_state(self):
class MyExc(Exception):
pass

Future = type(self).Future
self.loop.call_exception_handler = exc_handler = mock.Mock()

async def foo():
await asyncio.sleep(0.1, loop=self.loop)
return 10

task = self.new_task(self.loop, foo())
Future.set_exception(task, MyExc())

with self.assertRaises(MyExc):
self.loop.run_until_complete(task)

exc_handler.assert_called_once()
exc = exc_handler.call_args[0][0]['exception']
with self.assertRaisesRegex(asyncio.InvalidStateError,
r'step\(\): already done'):
raise exc


@unittest.skipUnless(hasattr(futures, '_CFuture') and
hasattr(tasks, '_CTask'),
'requires the C _asyncio module')
class CTask_CFuture_Tests(BaseTaskTests, test_utils.TestCase):
class CTask_CFuture_Tests(BaseTaskTests, SetMethodsTest,
test_utils.TestCase):

Task = getattr(tasks, '_CTask', None)
Future = getattr(futures, '_CFuture', None)

Expand All @@ -2245,21 +2300,16 @@ class CTask_CFuture_Tests(BaseTaskTests, test_utils.TestCase):
@add_subclass_tests
class CTask_CFuture_SubclassTests(BaseTaskTests, test_utils.TestCase):

class Task(tasks._CTask):
pass

class Future(futures._CFuture):
pass
Task = getattr(tasks, '_CTask', None)
Future = getattr(futures, '_CFuture', None)


@unittest.skipUnless(hasattr(tasks, '_CTask'),
'requires the C _asyncio module')
@add_subclass_tests
class CTaskSubclass_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):

class Task(tasks._CTask):
pass

Task = getattr(tasks, '_CTask', None)
Future = futures._PyFuture


Expand All @@ -2268,38 +2318,37 @@ class Task(tasks._CTask):
@add_subclass_tests
class PyTask_CFutureSubclass_Tests(BaseTaskTests, test_utils.TestCase):

class Future(futures._CFuture):
pass

Future = getattr(futures, '_CFuture', None)
Task = tasks._PyTask


@unittest.skipUnless(hasattr(tasks, '_CTask'),
'requires the C _asyncio module')
class CTask_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):

Task = getattr(tasks, '_CTask', None)
Future = futures._PyFuture


@unittest.skipUnless(hasattr(futures, '_CFuture'),
'requires the C _asyncio module')
class PyTask_CFuture_Tests(BaseTaskTests, test_utils.TestCase):

Task = tasks._PyTask
Future = getattr(futures, '_CFuture', None)


class PyTask_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):
class PyTask_PyFuture_Tests(BaseTaskTests, SetMethodsTest,
test_utils.TestCase):

Task = tasks._PyTask
Future = futures._PyFuture


@add_subclass_tests
class PyTask_PyFuture_SubclassTests(BaseTaskTests, test_utils.TestCase):
class Task(tasks._PyTask):
pass

class Future(futures._PyFuture):
pass
Task = tasks._PyTask
Future = futures._PyFuture


@unittest.skipUnless(hasattr(tasks, '_CTask'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Make asyncio.Task.set_exception() and set_result() raise
NotImplementedError. Task._step() and Future.__await__() raise proper
exceptions when they are in an invalid state, instead of raising an
AssertionError.
51 changes: 42 additions & 9 deletions Modules/_asynciomodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,7 @@ _asyncio_Future_exception_impl(FutureObj *self)
/*[clinic input]
_asyncio.Future.set_result

res: object
result: object
/

Mark the future done and set its result.
Expand All @@ -789,11 +789,11 @@ InvalidStateError.
[clinic start generated code]*/

static PyObject *
_asyncio_Future_set_result(FutureObj *self, PyObject *res)
/*[clinic end generated code: output=a620abfc2796bfb6 input=5b9dc180f1baa56d]*/
_asyncio_Future_set_result(FutureObj *self, PyObject *result)
/*[clinic end generated code: output=1ec2e6bcccd6f2ce input=8b75172c2a7b05f1]*/
{
ENSURE_FUTURE_ALIVE(self)
return future_set_result(self, res);
return future_set_result(self, result);
}

/*[clinic input]
Expand Down Expand Up @@ -1468,8 +1468,8 @@ FutureIter_iternext(futureiterobject *it)
Py_INCREF(fut);
return (PyObject *)fut;
}
PyErr_SetString(PyExc_AssertionError,
"yield from wasn't used with future");
PyErr_SetString(PyExc_RuntimeError,
"await wasn't used with future");
return NULL;
}

Expand Down Expand Up @@ -2232,6 +2232,39 @@ _asyncio_Task__wakeup_impl(TaskObj *self, PyObject *fut)
return task_wakeup(self, fut);
}

/*[clinic input]
_asyncio.Task.set_result

result: object
/
[clinic start generated code]*/

static PyObject *
_asyncio_Task_set_result(TaskObj *self, PyObject *result)
/*[clinic end generated code: output=1dcae308bfcba318 input=9d1a00c07be41bab]*/
{
PyErr_SetString(PyExc_RuntimeError,
"Task does not support set_result operation");
return NULL;
}

/*[clinic input]
_asyncio.Task.set_exception

exception: object
/
[clinic start generated code]*/

static PyObject *
_asyncio_Task_set_exception(TaskObj *self, PyObject *exception)
/*[clinic end generated code: output=bc377fc28067303d input=9a8f65c83dcf893a]*/
{
PyErr_SetString(PyExc_RuntimeError,
"Task doed not support set_exception operation");
return NULL;
}


static void
TaskObj_finalize(TaskObj *task)
{
Expand Down Expand Up @@ -2304,12 +2337,12 @@ static void TaskObj_dealloc(PyObject *); /* Needs Task_CheckExact */
static PyMethodDef TaskType_methods[] = {
_ASYNCIO_FUTURE_RESULT_METHODDEF
_ASYNCIO_FUTURE_EXCEPTION_METHODDEF
_ASYNCIO_FUTURE_SET_RESULT_METHODDEF
_ASYNCIO_FUTURE_SET_EXCEPTION_METHODDEF
_ASYNCIO_FUTURE_ADD_DONE_CALLBACK_METHODDEF
_ASYNCIO_FUTURE_REMOVE_DONE_CALLBACK_METHODDEF
_ASYNCIO_FUTURE_CANCELLED_METHODDEF
_ASYNCIO_FUTURE_DONE_METHODDEF
_ASYNCIO_TASK_SET_RESULT_METHODDEF
_ASYNCIO_TASK_SET_EXCEPTION_METHODDEF
_ASYNCIO_TASK_CURRENT_TASK_METHODDEF
_ASYNCIO_TASK_ALL_TASKS_METHODDEF
_ASYNCIO_TASK_CANCEL_METHODDEF
Expand Down Expand Up @@ -2461,7 +2494,7 @@ task_step_impl(TaskObj *task, PyObject *exc)
PyObject *o;

if (task->task_state != STATE_PENDING) {
PyErr_Format(PyExc_AssertionError,
PyErr_Format(asyncio_InvalidStateError,
"_step(): already done: %R %R",
task,
exc ? exc : Py_None);
Expand Down
20 changes: 18 additions & 2 deletions Modules/clinic/_asynciomodule.c.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ _asyncio_Future_exception(FutureObj *self, PyObject *Py_UNUSED(ignored))
}

PyDoc_STRVAR(_asyncio_Future_set_result__doc__,
"set_result($self, res, /)\n"
"set_result($self, result, /)\n"
"--\n"
"\n"
"Mark the future done and set its result.\n"
Expand Down Expand Up @@ -536,6 +536,22 @@ _asyncio_Task__wakeup(TaskObj *self, PyObject *const *args, Py_ssize_t nargs, Py
return return_value;
}

PyDoc_STRVAR(_asyncio_Task_set_result__doc__,
"set_result($self, result, /)\n"
"--\n"
"\n");

#define _ASYNCIO_TASK_SET_RESULT_METHODDEF \
{"set_result", (PyCFunction)_asyncio_Task_set_result, METH_O, _asyncio_Task_set_result__doc__},

PyDoc_STRVAR(_asyncio_Task_set_exception__doc__,
"set_exception($self, exception, /)\n"
"--\n"
"\n");

#define _ASYNCIO_TASK_SET_EXCEPTION_METHODDEF \
{"set_exception", (PyCFunction)_asyncio_Task_set_exception, METH_O, _asyncio_Task_set_exception__doc__},

PyDoc_STRVAR(_asyncio__get_running_loop__doc__,
"_get_running_loop($module, /)\n"
"--\n"
Expand Down Expand Up @@ -747,4 +763,4 @@ _asyncio__leave_task(PyObject *module, PyObject *const *args, Py_ssize_t nargs,
exit:
return return_value;
}
/*[clinic end generated code: output=5d100b3d74f2a0f4 input=a9049054013a1b77]*/
/*[clinic end generated code: output=616e814431893dcc input=a9049054013a1b77]*/