bpo-32250: Implement asyncio.current_task() and asyncio.all_tasks()#4799
bpo-32250: Implement asyncio.current_task() and asyncio.all_tasks()#4799asvetlov merged 28 commits intopython:masterfrom
Conversation
|
|
||
| from . import base_futures | ||
| from . import coroutines | ||
| from .events import get_running_loop |
There was a problem hiding this comment.
Import the module, not the function.
| By default all tasks for the current event loop are returned. | ||
| """ | ||
| if loop is None: | ||
| loop = events.get_event_loop() |
There was a problem hiding this comment.
You should keep get_event_loop() here for backwards compatibility. So that code that calls Task.all_tasks() before a loop is created continues to work in 3.7 (although it will raise a warning).
asyncio.all_tasks() should use get_running_loop().
There was a problem hiding this comment.
Not sure about asyncio.all_tasks() -- calling the function outside of coroutine context might make sense.
RuntimeError from asyncio.current_task() is more reasonable but I not sure again.
There was a problem hiding this comment.
Not sure about asyncio.all_tasks() -- calling the function outside of coroutine context might make sense.
Then pass a loop object explicitly. The problem with get_event_loop() is that it can create a new loop object implicitly. Or it may fail at creating it -- that function is weird.
|
Overall it looks good. I think we should keep using a weak-mapping for
|
|
Implementation is finished (I hope), test coverage added. If everything is ok -- I'll start documenting the feature. P.S. |
|
|
||
|
|
||
| # WeakKeyDictionary of {Task: EventLoop} containing all tasks alive. | ||
| _all_tasks = weakref.WeakKeyDictionary() |
There was a problem hiding this comment.
Why weakref.WeakKeyDictionary and not weakref.WeakValueDictionary?
There was a problem hiding this comment.
A task should be weak, on task garbage collection an item from _all_tasks should disappear. asyncio on master branch uses WeakSet of tasks.
But task._loop is a private property, custom task implementation can not expose it. That's why storing a loop along with task itself is required.
Mapping like {loop: WeakSet[task]} is not an option: there is no signal for loop deletion and _all_tasks is the singleton. In case of many loops creations/deletions (unit tests) the singleton will blow up with closed loops. I want to avoid it.
There was a problem hiding this comment.
Alright. This deserves to be put in a comment.
| current_task = _current_tasks.get(loop) | ||
| if current_task is not None: | ||
| raise RuntimeError(f"Entering into task {task!r} " | ||
| f"when other task {current_task!r} is executed.") |
There was a problem hiding this comment.
Cannot enter into task {task!r} while another
task {current_task!r} is being executed.
| current_task = _current_tasks.get(loop) | ||
| if current_task is not task: | ||
| raise RuntimeError(f"Leaving task {task!r} " | ||
| f"is not current {current_task!r}.") |
There was a problem hiding this comment.
Leaving task {task!r} does not match
the current task {current_task!r}.
| from . import events | ||
| from . import futures | ||
| from .coroutines import coroutine | ||
| from .base_tasks import (all_tasks, current_task, _register_task, |
There was a problem hiding this comment.
Please import the module, not functions from it.
There was a problem hiding this comment.
In this case base_tasks should have __all__ = ('all_tasks', 'current_task') to export names into asyncio namespace. _register_task cannot be exposed by base_tasks, see comment below.
| "use asyncio.current_task() instead", | ||
| PendingDeprecationWarning, | ||
| stacklevel=2) | ||
| return current_task(loop) |
There was a problem hiding this comment.
So we want to use asyncio.get_running_loop() in asyncio.current_task(). And we want to keep using asyncio.get_event_loop() in Task.current_task() to maintain backwards compat.
@classmethod
def current_task(cls, loop=None):
if loop is None:
loop = events.get_event_loop()
warnings.warn(..)
return current_task(loop)|
|
||
| def current_task(loop=None): | ||
| if loop is None: | ||
| loop = events.get_event_loop() |
There was a problem hiding this comment.
Use events.get_running_loop() here -- we want to use the new API here. It doesn't make sense to call asyncio.current_task() and accidentally create a new event loop because get_event_loop() thinks so.
| _PyTask = Task | ||
| _py_register_task = _register_task | ||
| _py_enter_task = _enter_task | ||
| _py_leave_task = _leave_task |
There was a problem hiding this comment.
You should do this logic in base_tasks.py.
There was a problem hiding this comment.
Not so easy: asyncio._asyncio imports asyncio.base_tasks. C Accelerators becomes available only in asyncio.tasks, pushing them into base_tasks leads to circular imports.
That's why I've decided to have both _py_register_task and _c_register_task in the same asyncio.tasks module.
There was a problem hiding this comment.
Then define all these functions (and _all_tasks and _current_tasks mappings) in _asynciomodule.c and try to import them in tasks.py. If that fails, define them in Python. Let's not touch base_tasks.py at all in this PR.
There was a problem hiding this comment.
If we define _all_tasks in _asynciomodule.c -- C Accelerator becomes mandatory.
Let's define _all_tasks and _current_tasks in base_tasks.py but move _register_task and family into tasks.py.
There was a problem hiding this comment.
C Accelerator becomes mandatory.
No, why?
_asynciomodule.c:
// defines _all_tasks, _current_tasksasyncio/tasks.py:
try:
from _asyncio import _all_tasks, _current_tasks
except ImportError:
_all_tasks = ...
_current_tasks = ...There was a problem hiding this comment.
I actually wanted to make a separate PR
I can make that PR myself now.
There was a problem hiding this comment.
Please do.
I'm going to sleep soon anyway.
There was a problem hiding this comment.
Please do.
Almost there.
Thus asyncio.current_loop() should be in base_tasks.py.
Yeah... I get what you're saying now. OK, let it be in base_tasks.py. I'm not blocking you from working on this PR, right?
There was a problem hiding this comment.
Yes, you are not blocking.
| Task = _CTask = _asyncio.Task | ||
| _register_task = _c_register_task = _asyncio._register_task | ||
| _enter_task = _c_enter_task = _asyncio._enter_task | ||
| _leave_task = _c_leave_task = _asyncio._leave_task |
There was a problem hiding this comment.
You should do this logic in base_tasks.py.
|
|
||
| static PyObject * | ||
| _asyncio__enter_task_impl(PyObject *module, PyObject *loop, PyObject *task) | ||
| /*[clinic end generated code: output=a22611c858035b73 input=de1b06dca70d8737]*/ |
There was a problem hiding this comment.
I prefer not to use _impl functions in C code. Better to implement int enter_task(loop, task) function and use it in Task implementation directly. Then you call that function in _asyncio__enter_task_impl and return NULL/None based on its return value. This way you also don't need to decref None in Task code, and the code is overall cleaner.
Absolutely, a separate PR is the way to go. |
|
Do you agree with using |
|
overall LGTM. Please apply this diff to fix tests in refleak mode: diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py
index 31d4ed649c..f6a1ac6f31 100644
--- a/Lib/test/test_asyncio/test_tasks.py
+++ b/Lib/test/test_asyncio/test_tasks.py
@@ -2202,6 +2202,8 @@ class BaseTaskIntrospectionTests:
self.assertEqual(asyncio.all_tasks(loop), set())
self._register_task(loop, task)
self.assertEqual(asyncio.all_tasks(loop), {task})
+ self._unregister_task(loop, task)
+ self.assertEqual(asyncio.all_tasks(loop), set())
def test__enter_task(self):
task = mock.Mock()
@@ -2209,6 +2211,7 @@ class BaseTaskIntrospectionTests:
self.assertIsNone(asyncio.current_task(loop))
self._enter_task(loop, task)
self.assertIs(asyncio.current_task(loop), task)
+ self._leave_task(loop, task)
def test__enter_task_failure(self):
task1 = mock.Mock()
@@ -2218,6 +2221,7 @@ class BaseTaskIntrospectionTests:
with self.assertRaises(RuntimeError):
self._enter_task(loop, task2)
self.assertIs(asyncio.current_task(loop), task1)
+ self._leave_task(loop, task1)
def test__leave_task(self):
task = mock.Mock()
@@ -2234,6 +2238,7 @@ class BaseTaskIntrospectionTests:
with self.assertRaises(RuntimeError):
self._leave_task(loop, task2)
self.assertIs(asyncio.current_task(loop), task1)
+ self._leave_task(loop, task1)
def test__leave_task_failure2(self):
task = mock.Mock()
@@ -2246,13 +2251,13 @@ class BaseTaskIntrospectionTests:
task = mock.Mock()
loop = mock.Mock()
self._register_task(loop, task)
- self._unregister_task(loop, task)
+ asyncio._unregister_task(loop, task)
self.assertEqual(asyncio.all_tasks(loop), set())
def test__unregister_task_not_registered(self):
task = mock.Mock()
loop = mock.Mock()
- self._unregister_task(loop, task)
+ asyncio._unregister_task(loop, task)
self.assertEqual(asyncio.all_tasks(loop), set()) |
|
|
||
| .. function:: current_task(loop=None): | ||
|
|
||
| Return a currently executed task or ``None`` if no task is running. |
There was a problem hiding this comment.
Please change to "Return the current running task or None, if no task is running."
| Return a currently executed task or ``None`` if no task is running. | ||
|
|
||
| If *loop* is ``None`` :func:`get_running_loop` is used for gettung | ||
| current loop. |
There was a problem hiding this comment.
gettung -> getting
Please change to "If loop is None, :func:get_running_loop is used to get the current loop."
| .. function:: all_tasks(loop=None): | ||
|
|
||
| Return a set of tasks created for the *loop* (the set can be empty | ||
| if there is no task exists). |
There was a problem hiding this comment.
Change to: "Return a set of :class:Task objects created for the loop."
I don't think "the set can be empty if there is no task exists" is needed, I think people will figure it out.
|
This is ready to be merged. Just please fix two doc nits. |
| enter_task(PyObject *loop, PyObject *task) | ||
| { | ||
| PyObject *item; | ||
| item = PyDict_GetItem(current_tasks, loop); |
There was a problem hiding this comment.
Maybe we can further optimize this by using _PyDict_GetItem_KnownHash and _PyDict_SetItem_KnownHash?
|
Notes are fixed. |
|
I think it's ok for loop to be a positional parameter for our new functions. The reason it was a keyword-only parameter was that those functions were methods on Task and that's the convention for methods. |
| { | ||
| PyObject *item; | ||
| item = PyDict_GetItem(current_tasks, loop); | ||
| long hash; |
| { | ||
| PyObject *item; | ||
| item = PyDict_GetItem(current_tasks, loop); | ||
| long hash; |
| item = PyDict_GetItem(current_tasks, loop); | ||
| long hash; | ||
| hash = PyObject_Hash(loop); | ||
| if (hash == NULL) { |
| item = PyDict_GetItem(current_tasks, loop); | ||
| long hash; | ||
| hash = PyObject_Hash(loop); | ||
| if (hash == NULL) { |
|
Looks like there's some compile/sphinx issue in the updated documentation. Otherwise LGTM. Let's merge this. |
Second version.
Proof of concept, no tests, C Accelerator is not updated.
@1st1 @bdarnell please take a look
https://bugs.python.org/issue32250