diff options
author | Alex Grönholm <alex.gronholm@nextday.fi> | 2018-08-08 21:06:47 (GMT) |
---|---|---|
committer | Yury Selivanov <yury@magic.io> | 2018-08-08 21:06:47 (GMT) |
commit | cca4eec3c0a67cbfeaf09182ea6c097a94891ff6 (patch) | |
tree | 0d04ad10797fa95e5e09f8b32e8aa9e0c50f6aac | |
parent | 52dee687af3671a31f63d6432de0d9ef370fd7b0 (diff) | |
download | cpython-cca4eec3c0a67cbfeaf09182ea6c097a94891ff6.zip cpython-cca4eec3c0a67cbfeaf09182ea6c097a94891ff6.tar.gz cpython-cca4eec3c0a67cbfeaf09182ea6c097a94891ff6.tar.bz2 |
bpo-34270: Make it possible to name asyncio tasks (GH-8547)
Co-authored-by: Antti Haapala <antti.haapala@anttipatterns.com>
-rw-r--r-- | Doc/library/asyncio-eventloop.rst | 8 | ||||
-rw-r--r-- | Doc/library/asyncio-task.rst | 36 | ||||
-rw-r--r-- | Doc/whatsnew/3.8.rst | 7 | ||||
-rw-r--r-- | Lib/asyncio/base_events.py | 6 | ||||
-rw-r--r-- | Lib/asyncio/base_tasks.py | 6 | ||||
-rw-r--r-- | Lib/asyncio/events.py | 2 | ||||
-rw-r--r-- | Lib/asyncio/tasks.py | 35 | ||||
-rw-r--r-- | Lib/test/test_asyncio/test_base_events.py | 28 | ||||
-rw-r--r-- | Lib/test/test_asyncio/test_tasks.py | 55 | ||||
-rw-r--r-- | Misc/ACKS | 2 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2018-07-29-11-32-56.bpo-34270.aL6P-3.rst | 8 | ||||
-rw-r--r-- | Modules/_asynciomodule.c | 60 | ||||
-rw-r--r-- | Modules/clinic/_asynciomodule.c.h | 41 |
13 files changed, 266 insertions, 28 deletions
diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 5b3d29d..1006853 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -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 diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 2b480d4..c73f36b 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index bab1e06..6e0c8b8 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -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 ------------------------ diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 78fe2a7..ee13d1a 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -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): diff --git a/Lib/asyncio/base_tasks.py b/Lib/asyncio/base_tasks.py index 3ce51f6..e2da462 100644 --- a/Lib/asyncio/base_tasks.py +++ b/Lib/asyncio/base_tasks.py @@ -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 diff --git a/Lib/asyncio/events.py b/Lib/asyncio/events.py index e4e6322..58a60a0 100644 --- a/Lib/asyncio/events.py +++ b/Lib/asyncio/events.py @@ -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. diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 72792a2..03d71d3 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -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. diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index f3ae140..e108637 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -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 diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index a5442f5..c930593 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -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') @@ -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 diff --git a/Misc/NEWS.d/next/Library/2018-07-29-11-32-56.bpo-34270.aL6P-3.rst b/Misc/NEWS.d/next/Library/2018-07-29-11-32-56.bpo-34270.aL6P-3.rst new file mode 100644 index 0000000..a66e110 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-07-29-11-32-56.bpo-34270.aL6P-3.rst @@ -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. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index ebda104..3c94848 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -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 */ }; diff --git a/Modules/clinic/_asynciomodule.c.h b/Modules/clinic/_asynciomodule.c.h index d8e1b6a..4f5326f 100644 --- a/Modules/clinic/_asynciomodule.c.h +++ b/Modules/clinic/_asynciomodule.c.h @@ -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]*/ |