-
-
Notifications
You must be signed in to change notification settings - Fork 178
Ensure get_event_loop returns the running loop when called in a coroutine #355
Conversation
- get/set_event_loop is renamed to get/set_default_loop - get/set_running_loop is added - get_event_loop now uses the running loop if available, and the default loop otherwise The BaseEventLoopPolicy is updated, and adds a few features: - it does not allow to set a running loop if another one is already set - it issues warnings if the running loop doesn't correspond to the default loop A context manager is also added to AbstractEventLoopPolicy and AbstractEventLoop to set and unset the running loop in a safe manner. This might help for other event loop implementations.
@@ -507,28 +509,52 @@ def get_debug(self): | |||
def set_debug(self, enabled): | |||
raise NotImplementedError | |||
|
|||
# Running context | |||
|
|||
def _running_context(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think we need this as a separate method, this is something that users shouldn't ever call directly (even though it's a "private" name they will).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, it is probably safer!
@vxgmichel Thanks for working on this! There is a chance we can merge this before 3.5.2. Please update the PR. |
@1st1 I just removed the context manager and the warnings. Should I also revert the simplification for SleepTests and TimeoutTests? |
# Non-abstract methods | ||
|
||
def get_event_loop(self): | ||
"""Return the running event loop if any, and the default event |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first docstring line should be one-line sentence under 72 characters long (see https://www.python.org/dev/peps/pep-0257/#multi-line-docstrings). Please fix all docstrings that this PR adds/modifies.
Yes, absolutely. That code is there for a reason. I also don't think we need |
Just realized that with |
I think we need to add a new policy specifically for unittests (the majority of them), that would simply raise an exception in |
It seems like it used to be there for a reason, but not anymore since |
This reverts commit f117c75.
Both should stay the same way. Now, the way the updated |
In addition to the above: tests on Travis spit out this:
|
OK I finally got it, we want to make sure the optional loop argument is never silently ignored. It makes sense, but it might be a problem in the long run. For instance, coroutines such as @coroutine
def sleep(delay, result=None):
loop = get_event_loop()
[...] So def test_ctor_loop(self):
loop = mock.Mock()
lock = asyncio.Lock(loop=loop)
self.assertIs(lock._loop, loop)
lock = asyncio.Lock(loop=self.loop)
self.assertIs(lock._loop, self.loop)
def test_ctor_noloop(self):
asyncio.set_event_loop(self.loop)
lock = asyncio.Lock()
self.assertIs(lock._loop, self.loop) Is that a workable solution or am I missing something? |
That's debatable and out of the scope of this PR ;)
The correct solution is to create a new event loop policy for tests and update the tests to use it: class Policy:
def get_event_loop(self):
raise RuntimeError |
All right!
It turns that it's quite painful to do because of cyclic imports (a class TestCase(unittest.TestCase):
def set_event_loop(self, loop, *, cleanup=True):
assert loop is not None
+ # patch policy so get_event_loop doesn't return the running loop
+ policy = events.get_event_loop_policy()
+ policy.get_event_loop = policy.get_default_loop
+ def clean_policy():
+ try:
+ del policy.get_event_loop
+ except AttributeError:
+ pass
+ self.addCleanup(clean_policy)
# ensure that the event loop is passed explicitly in asyncio
events.set_event_loop(None)
if cleanup:
self.addCleanup(loop.close) Is it acceptable? Can you think of a better solution? |
I'd add a function like this to def disable_get_event_loop(test_case):
assert isinstance(test_case, unittest.TestCase)
policy = events.get_event_loop_policy()
if hasattr(policy, '_patched_get_event_loop'):
return
def get_event_loop():
raise RuntimeError(
'asyncio.get_event_loop() is disabled in asyncio tests')
def reset_event_loop_method():
policy.get_event_loop = old_get_event_loop
del policy._patched_get_event_loop
old_get_event_loop = policy.get_event_loop
policy.get_event_loop = old_get_event_loop
policy._patched_get_event_loop = True
test_case.addCleanup(reset_event_loop_method) And then I'd add a call to it to Also, in of the earlier messages, I asked you to remove |
I just removed |
There's no way I can follow all the conversation that happened here. Why are we now collapsing get_event_loop() and get_running_loop() into one function? This seems to defeat the purpose of verifying in the unittests that there are no implicit dependencies on get_event_loop(). Should we just change all code that needs an event loop to assume get_event_loop() never raises? (E.g. sleep().) It's possible there's a good reason, but it better be explained well, not hidden in a PR. I think if we're really changing this we should have a more public debate on the tulip list. There are a lot of words devoted to this topic in PEP 3156, and I'd hate for the PEP to become out of date like this. |
Let's assume async def coro():
loop = asyncio.get_running_loop()
queue = asyncio.Queue(loop=loop)
[...] This is good, because async def coro():
queue = asyncio.Queue()
[...] and rely on the fact that using
It does change a few things in PEP 3156. What is said in Passing an Event Loop Around Explicitly still holds, but only outside the running loop. Inside,
It is indeed a big change that needs to be discussed, but I think the idea is worth considering. Here's the list of the coroutines in asyncio that could benefit from this:
|
self._set_coroutine_wrapper(self._debug) | ||
self._thread_id = threading.get_ident() | ||
try: | ||
policy.set_running_loop(self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has to be outside of the try
statement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do that, this test (not commited yet) would fail:
def test_run_loop_inside_loop(self):
@asyncio.coroutine
def coro():
loop2 = asyncio.new_event_loop()
self.assertRaises(RuntimeError, loop2.run_forever)
self.assertFalse(loop2.is_running()) # AssertionError: True is not false
loop2.close()
loop = asyncio.new_event_loop()
loop.run_until_complete(coro())
loop.close()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm beginning to think that this is all one big misunderstanding. I never
meant get_event_loop() to return a loop that's different from the running
loop. If you have multiple loops associated with the same thread you need
to implement a new EventloopPolicy that keeps track of which one is running
(e.g. via an explicit stack, or perhaps an implicit one using a local
variable to save the event loop and restoring from that in a finally
clause).
The only thing I meant to be possible is for get_event_loop() to raise an
exception, under special circumstances (especially unit tests that are
checking that no code accidentally relies on the default loop).
For library code I think there are two cases it should support:
- No default event loop, loop must always passed in
- Default event loop == current event loop, used when no loop is passed in
For application code I think it is totally fine to assume that
get_event_loop() returns the current, running loop. An application written
with this assumption never needs to pass a loop to something it calls
(since library code should always default to using get_event_loop()).
Note all the words devoted in the PEP to "context".
In any case I think we should stop coding and start discussing the use case
you are trying to address on the tulip list, to see whether it's real or
whether you are worrying too much (or whether maybe you should just write a
custom EventloopPolicy for your use case).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gvanrossum
I actually don't have a use case. I simply noticed that asyncio, 3rd party libraries and users tend to pass the running loop explicitely to their coroutines, because "explicit is better than implicit". That, in my opinion, is unecessary and could be simplified.
Sadly, such a simplification conflicts with the PEP and the way asyncio unittesting currently works. I understand that those constraints makes it hard, and maybe impossible to implement. I orginally meant this PR as a proposal and a starting point towards this simplification.
However, this idea didn't seem to get a lot of support and I don't feel like trying to push it forward anymore. Maybe more people will come up with the same rationale later on, and I'll be happy to help at that time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm beginning to think that this is all one big misunderstanding. I never
meant get_event_loop() to return a loop that's different from the running
loop. If you have multiple loops associated with the same thread you need
to implement a new EventloopPolicy that keeps track of which one is running
(e.g. via an explicit stack, or perhaps an implicit one using a local
variable to save the event loop and restoring from that in a finally
clause).
But here's the problem: if you have multiple loops associated with the same thread, policy doesn't know which one is currently running.
That's why I like the idea of adding policy.set_running_loop
and policy.get_running_loop
functions, because it makes policies more capable and allows you to worry less about multiple running loops event with the default policy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you have multiple loops associated with the same thread, policy doesn't know which one is currently running.
Then write a new policy. The whole point of having policies is that you can write different ones. The default policy does not really cater to this use case, but a policy is a really simple object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without set_running_loop
and get_running_loop
how will the policy know precisely what's going on? All I'm saying is that get_event_loop
and set_event_loop
are kind of detached from the loop's lifecycle; policy doesn't receive any notification if a loop was stopped or run again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct me if I'm wrong but it seems to me the reason of this entire debate is as simple as:
We see this pattern emerging internally at Facebook because:
Sadly, as I said, having to pass the loop around everywhere is tiringly verbose and brittle (all it takes is for me to forget to do it once and I'm back on the default loop again). The current state of things reminds me of logging frameworks that require you to pass the logger around everywhere. Correct but brittle and inconvenient.
|
I'm not so sure about the "what is the current coroutine runner" proposal. It seems that Ben wants the user to call it and depending on the result decide what to use --
If the task is created manually, |
I'm not worried about asyncio.Task instances created by hand. If you use the "low-level" approach, you're responsible for it. I'm a little worried about code like:
It'd be helpful if For unit tests for example, I think it would be enough if |
One case where we found the current So... I'm warming up to the idea that we should stop worrying (and passing |
On Mon, Jun 6, 2016 at 12:21 PM, Łukasz Langa notifications@github.com
Yes! Or write your own event loop policy (easier than you think, it's a really --Guido van Rossum (python.org/~guido) |
This is something I did not realize about
The following example lists the different use cases:
|
Maybe. I'd be more for documenting this better though.
|
@1st1 I guess I can close this PR right? By the way, a somehow related question has been asked on stackoverflow. |
@vxgmichel Yes, let's close this PR for now. We should indeed focus on the docs; want to work on that? ;) |
@gvanrossum Guido, are you OK if I merge this PR? (as per https://groups.google.com/d/msg/python-tulip/yF9C-rFpiKk/tk5oA3GLHAAJ) |
If you're asking do I want this feature? Yes. If you're asking me is this PR perfect? I have no idea, but I trust your review. (I don't want to have to re-understand the long discussion we had here earlier.) |
Thank you. I'll spend some time testing the patch (with uvloop tests too) and will merge it soon. |
@1st1 A few month ago, @gvanrossum wrote:
This made me realize the whole def run_forever(self):
try:
if asyncio.get_event_loop() != self:
raise RuntimeError('Loop not approved by the policy')
except asyncio.NoLoopSetError:
# Now running in 'explicit' mode, used for unit-testing
pass
else:
# The loop has been approved by the policy
pass Then we can advertise in the docs that Would that work, or I am missing something? |
People may write
Without changing default loop or even with disabling it by |
@vxgmichel Please read the thread: https://groups.google.com/d/msg/python-tulip/yF9C-rFpiKk/tk5oA3GLHAAJ. The point is to make So we want to modify one single aspect of The point of modifying Since I like the design of the current PR. |
So it'll be up to the policy to make get_event_loop() return the running
loop if one is set and the other one if not, right? And there won't be a
public interface for get_running_loop().
Still it's an API change -- policies need to implement
get/set_running_loop(). What if someone has a custom policy that doesn't
implement these and doesn't inherit from AbstractEventLoopPolicy? (It could
still be "an instance of" it, using the ABC registration API.) I wonder if
this would be a PEP change.
There could be a different way to implement this, making the "running" loop
a thread-local that is returned by the global get_event_loop() function in
preference over calling the policy's event loop.
|
I like this approach more. Does this make sense to you:
This way there is no API change for policies, and it's easy to add support of this to third-patry loop implementations. I think I'll close this PR and make a new one implementing this strategy. |
Sounds good. Maybe name the new APIs get/set_running_loop() or
get/set_active_loop()? To emphasize that this is the loop used by all
coroutines. Also maybe these should be named _get_*() (i.e. an underscore
prefix) to clarify that they should only be called by event loop
implementations, not by users. Or at least for the _set_ variant.
|
I vote on underscores for both getter and setter -- this API is really private. |
This PR follows up on the issue 26969.
It is meant to ensure that
asyncio.get_event_loop
returns the running loop when called in a coroutine (or loop callbacks). This could simplify all the coroutines that includes aloop
optional argument by removing it.This is done by modifying the interface of
AbstractEventLoopPolicy
:get/set_event_loop
is renamed toget/set_default_loop
get/set_running_loop
is addedget_event_loop
now uses the running loop if available, and the default loop otherwiseBaseEventLoopPolicy
is updated, and adds a few features:A context manager is also added to
AbstractEventLoopPolicy
andAbstractEventLoop
to set and clear the running loop in a safe manner. This might help for other event loop implementations.