Skip to content

Commit

Permalink
bpo-47167: Allow overriding a future compliance check in asyncio.Task (
Browse files Browse the repository at this point in the history
  • Loading branch information
asvetlov committed Apr 1, 2022
1 parent ab89ccf commit d4bb38f
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 15 deletions.
21 changes: 16 additions & 5 deletions Doc/library/asyncio-extending.rst
Expand Up @@ -48,16 +48,27 @@ For this purpose the following, *private* constructors are listed:

.. method:: Future.__init__(*, loop=None)

Create a built-in future instance.
Create a built-in future instance.

*loop* is an optional event loop instance.
*loop* is an optional event loop instance.

.. method:: Task.__init__(coro, *, loop=None, name=None, context=None)

Create a built-in task instance.
Create a built-in task instance.

*loop* is an optional event loop instance. The rest of arguments are described in
:meth:`loop.create_task` description.
*loop* is an optional event loop instance. The rest of arguments are described in
:meth:`loop.create_task` description.

.. versionchanged:: 3.11

*context* argument is added.

.. method:: Tasl._check_future(future)

Return ``True`` if *future* is attached to the same loop as the task, ``False``
otherwise.

.. versionadded:: 3.11


Task lifetime support
Expand Down
6 changes: 5 additions & 1 deletion Lib/asyncio/tasks.py
Expand Up @@ -252,6 +252,10 @@ def uncancel(self):
self._num_cancels_requested -= 1
return self._num_cancels_requested

def _check_future(self, future):
"""Return False if task and future loops are not compatible."""
return futures._get_loop(future) is self._loop

def __step(self, exc=None):
if self.done():
raise exceptions.InvalidStateError(
Expand Down Expand Up @@ -292,7 +296,7 @@ def __step(self, exc=None):
blocking = getattr(result, '_asyncio_future_blocking', None)
if blocking is not None:
# Yielded Future must come from Future.__iter__().
if futures._get_loop(result) is not self._loop:
if not self._check_future(result):
new_exc = RuntimeError(
f'Task {self!r} got Future '
f'{result!r} attached to a different loop')
Expand Down
10 changes: 9 additions & 1 deletion Lib/test/test_asyncio/test_tasks.py
Expand Up @@ -2383,7 +2383,13 @@ def add_done_callback(self, *args, **kwargs):
return super().add_done_callback(*args, **kwargs)

class Task(CommonFuture, BaseTask):
pass
def __init__(self, *args, **kwargs):
self._check_future_called = 0
super().__init__(*args, **kwargs)

def _check_future(self, future):
self._check_future_called += 1
return super()._check_future(future)

class Future(CommonFuture, BaseFuture):
pass
Expand All @@ -2409,6 +2415,8 @@ async def func():
dict(fut.calls),
{'add_done_callback': 1})

self.assertEqual(1, task._check_future_called)

# Add patched Task & Future back to the test case
cls.Task = Task
cls.Future = Future
Expand Down
@@ -0,0 +1 @@
Allow overriding a future compliance check in :class:`asyncio.Task`.
71 changes: 64 additions & 7 deletions Modules/_asynciomodule.c
Expand Up @@ -23,6 +23,7 @@ _Py_IDENTIFIER(call_soon);
_Py_IDENTIFIER(cancel);
_Py_IDENTIFIER(get_event_loop);
_Py_IDENTIFIER(throw);
_Py_IDENTIFIER(_check_future);


/* State of the _asyncio module */
Expand Down Expand Up @@ -1795,6 +1796,8 @@ class _asyncio.Task "TaskObj *" "&Task_Type"
static int task_call_step_soon(TaskObj *, PyObject *);
static PyObject * task_wakeup(TaskObj *, PyObject *);
static PyObject * task_step(TaskObj *, PyObject *);
static int task_check_future(TaskObj *, PyObject *);
static int task_check_future_exact(TaskObj *, PyObject *);

/* ----- Task._step wrapper */

Expand Down Expand Up @@ -2269,14 +2272,28 @@ Returns the remaining number of cancellation requests.
static PyObject *
_asyncio_Task_uncancel_impl(TaskObj *self)
/*[clinic end generated code: output=58184d236a817d3c input=68f81a4b90b46be2]*/
/*[clinic end generated code]*/
{
if (self->task_num_cancels_requested > 0) {
self->task_num_cancels_requested -= 1;
}
return PyLong_FromLong(self->task_num_cancels_requested);
}

/*[clinic input]
_asyncio.Task._check_future -> bool
future: object
Return False if task and future loops are not compatible.
[clinic start generated code]*/

static int
_asyncio_Task__check_future_impl(TaskObj *self, PyObject *future)
/*[clinic end generated code: output=a3bfba79295c8d57 input=3b1d6dfd6fe90aa5]*/
{
return task_check_future_exact(self, future);
}

/*[clinic input]
_asyncio.Task.get_stack
Expand Down Expand Up @@ -2502,6 +2519,7 @@ static PyMethodDef TaskType_methods[] = {
_ASYNCIO_TASK_CANCEL_METHODDEF
_ASYNCIO_TASK_CANCELLING_METHODDEF
_ASYNCIO_TASK_UNCANCEL_METHODDEF
_ASYNCIO_TASK__CHECK_FUTURE_METHODDEF
_ASYNCIO_TASK_GET_STACK_METHODDEF
_ASYNCIO_TASK_PRINT_STACK_METHODDEF
_ASYNCIO_TASK__MAKE_CANCELLED_ERROR_METHODDEF
Expand Down Expand Up @@ -2569,6 +2587,43 @@ TaskObj_dealloc(PyObject *self)
Py_TYPE(task)->tp_free(task);
}

static int
task_check_future_exact(TaskObj *task, PyObject *future)
{
int res;
if (Future_CheckExact(future) || Task_CheckExact(future)) {
FutureObj *fut = (FutureObj *)future;
res = (fut->fut_loop == task->task_loop);
} else {
PyObject *oloop = get_future_loop(future);
if (oloop == NULL) {
return -1;
}
res = (oloop == task->task_loop);
Py_DECREF(oloop);
}
return res;
}


static int
task_check_future(TaskObj *task, PyObject *future)
{
if (Task_CheckExact(task)) {
return task_check_future_exact(task, future);
} else {
PyObject * ret = _PyObject_CallMethodIdOneArg((PyObject *)task,
&PyId__check_future,
future);
if (ret == NULL) {
return -1;
}
int is_true = PyObject_IsTrue(ret);
Py_DECREF(ret);
return is_true;
}
}

static int
task_call_step_soon(TaskObj *task, PyObject *arg)
{
Expand Down Expand Up @@ -2790,7 +2845,11 @@ task_step_impl(TaskObj *task, PyObject *exc)
FutureObj *fut = (FutureObj*)result;

/* Check if `result` future is attached to a different loop */
if (fut->fut_loop != task->task_loop) {
res = task_check_future(task, result);
if (res == -1) {
goto fail;
}
if (res == 0) {
goto different_loop;
}

Expand Down Expand Up @@ -2862,15 +2921,13 @@ task_step_impl(TaskObj *task, PyObject *exc)
}

/* Check if `result` future is attached to a different loop */
PyObject *oloop = get_future_loop(result);
if (oloop == NULL) {
res = task_check_future(task, result);
if (res == -1) {
goto fail;
}
if (oloop != task->task_loop) {
Py_DECREF(oloop);
if (res == 0) {
goto different_loop;
}
Py_DECREF(oloop);

if (!blocking) {
goto yield_insteadof_yf;
Expand Down
39 changes: 38 additions & 1 deletion Modules/clinic/_asynciomodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d4bb38f

Please sign in to comment.