bpo-34270: Make it possible to name asyncio tasks (GH-8547)

Co-authored-by: Antti Haapala <antti.haapala@anttipatterns.com>
This commit is contained in:
Alex Grönholm 2018-08-09 00:06:47 +03:00 committed by Yury Selivanov
parent 52dee687af
commit cca4eec3c0
13 changed files with 266 additions and 28 deletions

View File

@ -246,7 +246,7 @@ Futures
Tasks
-----
.. method:: AbstractEventLoop.create_task(coro)
.. method:: AbstractEventLoop.create_task(coro, \*, name=None)
Schedule the execution of a :ref:`coroutine object <coroutine>`: wrap it in
a future. Return a :class:`Task` object.
@ -255,8 +255,14 @@ Tasks
interoperability. In this case, the result type is a subclass of
:class:`Task`.
If the *name* argument is provided and not ``None``, it is set as the name
of the task using :meth:`Task.set_name`.
.. versionadded:: 3.4.2
.. versionchanged:: 3.8
Added the ``name`` parameter.
.. method:: AbstractEventLoop.set_task_factory(factory)
Set a task factory that will be used by

View File

@ -387,10 +387,13 @@ with the result.
Task
----
.. function:: create_task(coro)
.. function:: create_task(coro, \*, name=None)
Wrap a :ref:`coroutine <coroutine>` *coro* into a task and schedule
its execution. Return the task object.
its execution. Return the task object.
If *name* is not ``None``, it is set as the name of the task using
:meth:`Task.set_name`.
The task is executed in :func:`get_running_loop` context,
:exc:`RuntimeError` is raised if there is no running loop in
@ -398,7 +401,10 @@ Task
.. versionadded:: 3.7
.. class:: Task(coro, \*, loop=None)
.. versionchanged:: 3.8
Added the ``name`` parameter.
.. class:: Task(coro, \*, loop=None, name=None)
A unit for concurrent running of :ref:`coroutines <coroutine>`,
subclass of :class:`Future`.
@ -438,6 +444,9 @@ Task
.. versionchanged:: 3.7
Added support for the :mod:`contextvars` module.
.. versionchanged:: 3.8
Added the ``name`` parameter.
.. classmethod:: all_tasks(loop=None)
Return a set of all tasks for an event loop.
@ -504,6 +513,27 @@ Task
get_stack(). The file argument is an I/O stream to which the output
is written; by default output is written to sys.stderr.
.. method:: get_name()
Return the name of the task.
If no name has been explicitly assigned to the task, the default
``Task`` implementation generates a default name during instantiation.
.. versionadded:: 3.8
.. method:: set_name(value)
Set the name of the task.
The *value* argument can be any object, which is then converted to a
string.
In the default ``Task`` implementation, the name will be visible in the
:func:`repr` output of a task object.
.. versionadded:: 3.8
Example: Parallel execution of tasks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -249,6 +249,13 @@ Changes in the Python API
* ``PyGC_Head`` struct is changed completely. All code touched the
struct member should be rewritten. (See :issue:`33597`)
* Asyncio tasks can now be named, either by passing the ``name`` keyword
argument to :func:`asyncio.create_task` or
the :meth:`~asyncio.AbstractEventLoop.create_task` event loop method, or by
calling the :meth:`~asyncio.Task.set_name` method on the task object. The
task name is visible in the ``repr()`` output of :class:`asyncio.Task` and
can also be retrieved using the :meth:`~asyncio.Task.get_name` method.
CPython bytecode changes
------------------------

View File

@ -384,18 +384,20 @@ class BaseEventLoop(events.AbstractEventLoop):
"""Create a Future object attached to the loop."""
return futures.Future(loop=self)
def create_task(self, coro):
def create_task(self, coro, *, name=None):
"""Schedule a coroutine object.
Return a task object.
"""
self._check_closed()
if self._task_factory is None:
task = tasks.Task(coro, loop=self)
task = tasks.Task(coro, loop=self, name=name)
if task._source_traceback:
del task._source_traceback[-1]
else:
task = self._task_factory(self, coro)
tasks._set_task_name(task, name)
return task
def set_task_factory(self, factory):

View File

@ -12,11 +12,13 @@ def _task_repr_info(task):
# replace status
info[0] = 'cancelling'
info.insert(1, 'name=%r' % task.get_name())
coro = coroutines._format_coroutine(task._coro)
info.insert(1, f'coro=<{coro}>')
info.insert(2, f'coro=<{coro}>')
if task._fut_waiter is not None:
info.insert(2, f'wait_for={task._fut_waiter!r}')
info.insert(3, f'wait_for={task._fut_waiter!r}')
return info

View File

@ -277,7 +277,7 @@ class AbstractEventLoop:
# Method scheduling a coroutine object: create a task.
def create_task(self, coro):
def create_task(self, coro, *, name=None):
raise NotImplementedError
# Methods for interacting with threads.

View File

@ -13,6 +13,7 @@ import concurrent.futures
import contextvars
import functools
import inspect
import itertools
import types
import warnings
import weakref
@ -23,6 +24,11 @@ from . import events
from . import futures
from .coroutines import coroutine
# Helper to generate new task names
# This uses itertools.count() instead of a "+= 1" operation because the latter
# is not thread safe. See bpo-11866 for a longer explanation.
_task_name_counter = itertools.count(1).__next__
def current_task(loop=None):
"""Return a currently executed task."""
@ -48,6 +54,16 @@ def _all_tasks_compat(loop=None):
return {t for t in _all_tasks if futures._get_loop(t) is loop}
def _set_task_name(task, name):
if name is not None:
try:
set_name = task.set_name
except AttributeError:
pass
else:
set_name(name)
class Task(futures._PyFuture): # Inherit Python Task implementation
# from a Python Future implementation.
@ -94,7 +110,7 @@ class Task(futures._PyFuture): # Inherit Python Task implementation
stacklevel=2)
return _all_tasks_compat(loop)
def __init__(self, coro, *, loop=None):
def __init__(self, coro, *, loop=None, name=None):
super().__init__(loop=loop)
if self._source_traceback:
del self._source_traceback[-1]
@ -104,6 +120,11 @@ class Task(futures._PyFuture): # Inherit Python Task implementation
self._log_destroy_pending = False
raise TypeError(f"a coroutine was expected, got {coro!r}")
if name is None:
self._name = f'Task-{_task_name_counter()}'
else:
self._name = str(name)
self._must_cancel = False
self._fut_waiter = None
self._coro = coro
@ -126,6 +147,12 @@ class Task(futures._PyFuture): # Inherit Python Task implementation
def _repr_info(self):
return base_tasks._task_repr_info(self)
def get_name(self):
return self._name
def set_name(self, value):
self._name = str(value)
def set_result(self, result):
raise RuntimeError('Task does not support set_result operation')
@ -312,13 +339,15 @@ else:
Task = _CTask = _asyncio.Task
def create_task(coro):
def create_task(coro, *, name=None):
"""Schedule the execution of a coroutine object in a spawn task.
Return a Task object.
"""
loop = events.get_running_loop()
return loop.create_task(coro)
task = loop.create_task(coro)
_set_task_name(task, name)
return task
# wait() and as_completed() similar to those in PEP 3148.

View File

@ -825,6 +825,34 @@ class BaseEventLoopTests(test_utils.TestCase):
task._log_destroy_pending = False
coro.close()
def test_create_named_task_with_default_factory(self):
async def test():
pass
loop = asyncio.new_event_loop()
task = loop.create_task(test(), name='test_task')
try:
self.assertEqual(task.get_name(), 'test_task')
finally:
loop.run_until_complete(task)
loop.close()
def test_create_named_task_with_custom_factory(self):
def task_factory(loop, coro):
return asyncio.Task(coro, loop=loop)
async def test():
pass
loop = asyncio.new_event_loop()
loop.set_task_factory(task_factory)
task = loop.create_task(test(), name='test_task')
try:
self.assertEqual(task.get_name(), 'test_task')
finally:
loop.run_until_complete(task)
loop.close()
def test_run_forever_keyboard_interrupt(self):
# Python issue #22601: ensure that the temporary task created by
# run_forever() consumes the KeyboardInterrupt and so don't log

View File

@ -87,8 +87,8 @@ class BaseTaskTests:
Task = None
Future = None
def new_task(self, loop, coro):
return self.__class__.Task(coro, loop=loop)
def new_task(self, loop, coro, name='TestTask'):
return self.__class__.Task(coro, loop=loop, name=name)
def new_future(self, loop):
return self.__class__.Future(loop=loop)
@ -295,12 +295,12 @@ class BaseTaskTests:
coro = format_coroutine(coro_qualname, 'running', src,
t._source_traceback, generator=True)
self.assertEqual(repr(t),
'<Task pending %s cb=[<Dummy>()]>' % coro)
"<Task pending name='TestTask' %s cb=[<Dummy>()]>" % coro)
# test cancelling Task
t.cancel() # Does not take immediate effect!
self.assertEqual(repr(t),
'<Task cancelling %s cb=[<Dummy>()]>' % coro)
"<Task cancelling name='TestTask' %s cb=[<Dummy>()]>" % coro)
# test cancelled Task
self.assertRaises(asyncio.CancelledError,
@ -308,7 +308,7 @@ class BaseTaskTests:
coro = format_coroutine(coro_qualname, 'done', src,
t._source_traceback)
self.assertEqual(repr(t),
'<Task cancelled %s>' % coro)
"<Task cancelled name='TestTask' %s>" % coro)
# test finished Task
t = self.new_task(self.loop, notmuch())
@ -316,7 +316,36 @@ class BaseTaskTests:
coro = format_coroutine(coro_qualname, 'done', src,
t._source_traceback)
self.assertEqual(repr(t),
"<Task finished %s result='abc'>" % coro)
"<Task finished name='TestTask' %s result='abc'>" % coro)
def test_task_repr_autogenerated(self):
@asyncio.coroutine
def notmuch():
return 123
t1 = self.new_task(self.loop, notmuch(), None)
t2 = self.new_task(self.loop, notmuch(), None)
self.assertNotEqual(repr(t1), repr(t2))
match1 = re.match("^<Task pending name='Task-(\d+)'", repr(t1))
self.assertIsNotNone(match1)
match2 = re.match("^<Task pending name='Task-(\d+)'", repr(t2))
self.assertIsNotNone(match2)
# Autogenerated task names should have monotonically increasing numbers
self.assertLess(int(match1.group(1)), int(match2.group(1)))
self.loop.run_until_complete(t1)
self.loop.run_until_complete(t2)
def test_task_repr_name_not_str(self):
@asyncio.coroutine
def notmuch():
return 123
t = self.new_task(self.loop, notmuch())
t.set_name({6})
self.assertEqual(t.get_name(), '{6}')
self.loop.run_until_complete(t)
def test_task_repr_coro_decorator(self):
self.loop.set_debug(False)
@ -376,7 +405,7 @@ class BaseTaskTests:
t._source_traceback,
generator=not coroutines._DEBUG)
self.assertEqual(repr(t),
'<Task pending %s cb=[<Dummy>()]>' % coro)
"<Task pending name='TestTask' %s cb=[<Dummy>()]>" % coro)
self.loop.run_until_complete(t)
def test_task_repr_wait_for(self):
@ -2260,6 +2289,18 @@ class BaseTaskTests:
self.loop.run_until_complete(coro())
def test_bare_create_named_task(self):
async def coro_noop():
pass
async def coro():
task = asyncio.create_task(coro_noop(), name='No-op')
self.assertEqual(task.get_name(), 'No-op')
await task
self.loop.run_until_complete(coro())
def test_context_1(self):
cvar = contextvars.ContextVar('cvar', default='nope')

View File

@ -573,6 +573,7 @@ Elliot Gorokhovsky
Hans de Graaff
Tim Graham
Kim Gräsman
Alex Grönholm
Nathaniel Gray
Eddy De Greef
Duane Griffin
@ -594,6 +595,7 @@ Michael Guravage
Lars Gustäbel
Thomas Güttler
Jonas H.
Antti Haapala
Joseph Hackman
Barry Haddow
Philipp Hagemeister

View File

@ -0,0 +1,8 @@
The default asyncio task class now always has a name which can be get or set
using two new methods (:meth:`~asyncio.Task.get_name()` and
:meth:`~asyncio.Task.set_name`) and is visible in the :func:`repr` output. An
initial name can also be set using the new ``name`` keyword argument to
:func:`asyncio.create_task` or the
:meth:`~asyncio.AbstractEventLoop.create_task` method of the event loop.
If no initial name is set, the default Task implementation generates a name
like ``Task-1`` using a monotonic counter.

View File

@ -37,6 +37,8 @@ static PyObject *context_kwname;
static PyObject *cached_running_holder;
static volatile uint64_t cached_running_holder_tsid;
/* Counter for autogenerated Task names */
static uint64_t task_name_counter = 0;
/* WeakSet containing all alive tasks. */
static PyObject *all_tasks;
@ -78,6 +80,7 @@ typedef struct {
FutureObj_HEAD(task)
PyObject *task_fut_waiter;
PyObject *task_coro;
PyObject *task_name;
PyContext *task_context;
int task_must_cancel;
int task_log_destroy_pending;
@ -1934,13 +1937,15 @@ _asyncio.Task.__init__
coro: object
*
loop: object = None
name: object = None
A coroutine wrapped in a Future.
[clinic start generated code]*/
static int
_asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop)
/*[clinic end generated code: output=9f24774c2287fc2f input=8d132974b049593e]*/
_asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
PyObject *name)
/*[clinic end generated code: output=88b12b83d570df50 input=352a3137fe60091d]*/
{
if (future_init((FutureObj*)self, loop)) {
return -1;
@ -1969,6 +1974,18 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop)
Py_INCREF(coro);
Py_XSETREF(self->task_coro, coro);
if (name == Py_None) {
name = PyUnicode_FromFormat("Task-%" PRIu64, ++task_name_counter);
} else if (!PyUnicode_Check(name)) {
name = PyObject_Str(name);
} else {
Py_INCREF(name);
}
Py_XSETREF(self->task_name, name);
if (self->task_name == NULL) {
return -1;
}
if (task_call_step_soon(self, NULL)) {
return -1;
}
@ -1981,6 +1998,7 @@ TaskObj_clear(TaskObj *task)
(void)FutureObj_clear((FutureObj*) task);
Py_CLEAR(task->task_context);
Py_CLEAR(task->task_coro);
Py_CLEAR(task->task_name);
Py_CLEAR(task->task_fut_waiter);
return 0;
}
@ -1990,6 +2008,7 @@ TaskObj_traverse(TaskObj *task, visitproc visit, void *arg)
{
Py_VISIT(task->task_context);
Py_VISIT(task->task_coro);
Py_VISIT(task->task_name);
Py_VISIT(task->task_fut_waiter);
(void)FutureObj_traverse((FutureObj*) task, visit, arg);
return 0;
@ -2297,6 +2316,41 @@ _asyncio_Task_set_exception(TaskObj *self, PyObject *exception)
return NULL;
}
/*[clinic input]
_asyncio.Task.get_name
[clinic start generated code]*/
static PyObject *
_asyncio_Task_get_name_impl(TaskObj *self)
/*[clinic end generated code: output=0ecf1570c3b37a8f input=a4a6595d12f4f0f8]*/
{
if (self->task_name) {
Py_INCREF(self->task_name);
return self->task_name;
}
Py_RETURN_NONE;
}
/*[clinic input]
_asyncio.Task.set_name
value: object
/
[clinic start generated code]*/
static PyObject *
_asyncio_Task_set_name(TaskObj *self, PyObject *value)
/*[clinic end generated code: output=138a8d51e32057d6 input=a8359b6e65f8fd31]*/
{
PyObject *name = PyObject_Str(value);
if (name == NULL) {
return NULL;
}
Py_XSETREF(self->task_name, name);
Py_RETURN_NONE;
}
static void
TaskObj_finalize(TaskObj *task)
@ -2382,6 +2436,8 @@ static PyMethodDef TaskType_methods[] = {
_ASYNCIO_TASK_GET_STACK_METHODDEF
_ASYNCIO_TASK_PRINT_STACK_METHODDEF
_ASYNCIO_TASK__REPR_INFO_METHODDEF
_ASYNCIO_TASK_GET_NAME_METHODDEF
_ASYNCIO_TASK_SET_NAME_METHODDEF
{NULL, NULL} /* Sentinel */
};

View File

@ -253,28 +253,30 @@ _asyncio_Future__repr_info(FutureObj *self, PyObject *Py_UNUSED(ignored))
}
PyDoc_STRVAR(_asyncio_Task___init____doc__,
"Task(coro, *, loop=None)\n"
"Task(coro, *, loop=None, name=None)\n"
"--\n"
"\n"
"A coroutine wrapped in a Future.");
static int
_asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop);
_asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
PyObject *name);
static int
_asyncio_Task___init__(PyObject *self, PyObject *args, PyObject *kwargs)
{
int return_value = -1;
static const char * const _keywords[] = {"coro", "loop", NULL};
static _PyArg_Parser _parser = {"O|$O:Task", _keywords, 0};
static const char * const _keywords[] = {"coro", "loop", "name", NULL};
static _PyArg_Parser _parser = {"O|$OO:Task", _keywords, 0};
PyObject *coro;
PyObject *loop = Py_None;
PyObject *name = Py_None;
if (!_PyArg_ParseTupleAndKeywordsFast(args, kwargs, &_parser,
&coro, &loop)) {
&coro, &loop, &name)) {
goto exit;
}
return_value = _asyncio_Task___init___impl((TaskObj *)self, coro, loop);
return_value = _asyncio_Task___init___impl((TaskObj *)self, coro, loop, name);
exit:
return return_value;
@ -500,6 +502,31 @@ PyDoc_STRVAR(_asyncio_Task_set_exception__doc__,
#define _ASYNCIO_TASK_SET_EXCEPTION_METHODDEF \
{"set_exception", (PyCFunction)_asyncio_Task_set_exception, METH_O, _asyncio_Task_set_exception__doc__},
PyDoc_STRVAR(_asyncio_Task_get_name__doc__,
"get_name($self, /)\n"
"--\n"
"\n");
#define _ASYNCIO_TASK_GET_NAME_METHODDEF \
{"get_name", (PyCFunction)_asyncio_Task_get_name, METH_NOARGS, _asyncio_Task_get_name__doc__},
static PyObject *
_asyncio_Task_get_name_impl(TaskObj *self);
static PyObject *
_asyncio_Task_get_name(TaskObj *self, PyObject *Py_UNUSED(ignored))
{
return _asyncio_Task_get_name_impl(self);
}
PyDoc_STRVAR(_asyncio_Task_set_name__doc__,
"set_name($self, value, /)\n"
"--\n"
"\n");
#define _ASYNCIO_TASK_SET_NAME_METHODDEF \
{"set_name", (PyCFunction)_asyncio_Task_set_name, METH_O, _asyncio_Task_set_name__doc__},
PyDoc_STRVAR(_asyncio__get_running_loop__doc__,
"_get_running_loop($module, /)\n"
"--\n"
@ -711,4 +738,4 @@ _asyncio__leave_task(PyObject *module, PyObject *const *args, Py_ssize_t nargs,
exit:
return return_value;
}
/*[clinic end generated code: output=b6148b0134e7a819 input=a9049054013a1b77]*/
/*[clinic end generated code: output=67da879c9f841505 input=a9049054013a1b77]*/