Finish deprecation in asyncio.get_event_loop()
serhiy-storchaka opened this issue · 62 comments
Since 3.10 asyncio.get_event_loop() emits a deprecation warning if used outside of the event loop (see #83710). It is a time to turn a warning into error and make asyncio.get_event_loop() an alias of asyncio.get_running_loop().
But maybe we should first deprecate set_event_loop()? It will be a no-op now.
Linked PRs
I wonder a bit how much existing code will be broken by this change in Python 3.12?
If the deprecation warning in Python 3.10-11 remains unnoticed (like for me, until I ran Python with the -Werror flag), any existing code calling aysncio.get_event_loop() outside a running event loop will break.
A simple workaround that worked in my case is to call asyncio.get_event_loop_policy().get_event_loop() instead. This will return the loop already attached to the current thread, or create a new one if not existing yet. (This seems better than just calling asyncio.new_event_loop() because that creates an extra loop next to already existing loops.)
Will this workaround of using asyncio.get_event_loop_policy().get_event_loop() remain available in Python 3.12+?
I'm interesting in knowing the answer to @lschoe's question too.
A user of one of my libraries reported this deprecation warning. Is using asyncio.get_event_loop_policy().get_event_loop() a valid workaround?
Ask @asvetlov. I do not know plans about asyncio.get_event_loop_policy().get_event_loop(). It may be deprecated too: you either use the existing running loop, or explicitly create a new one.
asyncio.get_event_loop() was initially proposed to remove, but since it is use so much in the code that predates asyncio.get_running_loop(), it was decided to make it just an alias of asyncio.get_running_loop().
Thanks @serhiy-storchaka
Thanks as well. It's good to have something next to asyncio.get_running_loop() because that can be only be called from a running loop;) For lower-level usage of asyncio's event loops, accessing a loop even when it's not running seems still very useful to me. (For higher-level usage stricter, disciplined calls using asyncio.run() etc., not handling e.g. any bare asyncio.Futures is indeed advised.)
For example, I want to call loop.set_exception_handler(handler) to set my own exception handler for the event loop. To do this once and for all, it's convenient to do this before the loop is running. Not sure what the intended way to do this would be, if one can only access the loop after it started running?
@lschoe I think the intended pattern is
def loop_factory():
loop = asyncio.new_event_loop()
loop.set_exception_handler(...)
with asyncio.Runner(loop_factory=loop_factory) as runner:
runner.run(amain())or:
with asyncio.Runner() as runner:
runner.get_loop().set_exception_handler(...)
runner.run(amain())But calling asyncio.get_running_loop().set_exception_handler(...) as the first thing in your async def amain(): seems to be the best choice
Thanks, these are interesting options, indeed for Python 3.11+ which will have this new Runner class.
Using a loop_factory() does not seem preferable because asyncio.new_event_loop() is still called, which may cause problems.
If a loop is already attached to the current thread, this will create a new loop next to the existing one. However, only one of these two loops can run at the same time, so this gives a Runtime error if one tries to run the other one.
With asyncio.get_event_loop() you can join the existing loop, if any. Whether that loop is running or not doesn't matter either.
For example, one can run some code in Python's asyncio REPL (via python -m asyncio) or in Jupyter notebooks, where there is already a loop running providing top-level await, or one can run the same code from a Python script where there is no loop running yet.
I've also tried the second way you propose, calling runner.get_loop(), but this also gives a Runtime error when called from the asyncio REPL, for example.
And your third option requires a running loop again (which will not be there yet, if this code is executed upon importing and initializing a module).
To deal with such varying circumstances, global access to the event loop attached to the current thread via asyncio.get_event_loop() is very useful.
But maybe we should first deprecate set_event_loop()? It will be a no-op now.
@serhiy-storchaka set_event_loop is not a no-op it sets up the child watcher system #93896
This is a subtle issue and I would like to go slowly here (but not so slowly to miss the 3.12 feature cut-off!).
I wasn't aware of this issue and need some time to think about the consequences.
The growing asymmetry between get_event_loop() and set_event_loop() is definitely bothering me.
Should we perhaps also deprecate the behavior of Policy.get_event_loop() to sometimes create a new event loop?
See also GH-94597.
In response to @lschoe's comments (e.g. #93453 (comment)), this is indeed a reason to go slowly.
I wonder what the remaining use cases are for accessing the current thread's loop regardless of whether it's running or not -- is it just loop.set_exception_handler(), or are there other configurations as well that some folks prefer to do before the loop is running?
If so, maybe we could recommend loop = asyncio.get_event_loop_policy().get_event_loop(), which has the advantage that it works in older versions too. Maybe we can keep this and policy.new_event_loop() but deprecate set_event_loop_policy() right away? That way eventually the policy is just a singleton that holds some global state in a backwards compatible fashion.
The awkward thing is that the use case requires that we still call set_event_loop(), for the rare cases where someone wants to configure something for the current thread's loop before it is running. How can we convince users not to do that but instead configure the loop once it is running? It does seem a bit clumsy if you want to run the same code in an asyncio-enabled REPL and when using a runner.
I would very much like to believe that this is a very very small minority use case or that it's just a bad practice, but nothing I've read here convinces me of that. @lschoe can you point to real-world code that uses this pattern?
the rare cases where someone wants to configure something for the current thread's loop before it is running
This doesn't work when anything uses asyncio.run, because the first thing asyncio.run does is make a brand new event loop and the last thing it does is set the global event loop to None, which makes subsequent get_event_loop calls crash
This doesn't work when anything uses asyncio.run, because the first thing asyncio.run does is make a brand new event loop and the last thing it does is set the global event loop to None, which makes subsequent
get_event_loopcalls crash
Yeah, we need a better understanding of @lschoe's use case.
The backdrop for the use case is a bit extensive, so I'll try to extract the relevant points for your discussion as directly as possible.
The perspective is that of a (Python package) developer of code that heavily relies on asyncio and its event loop, using lots of Futures, Tasks, coroutines etc. The use of these asyncio primitives, and Futures in particular, is hidden from the end user, who will experience most code at the application level preferably as non-async code. The actual package MPyC is for secure multiparty computation, a kind of distributed computation with privacy guarantees. The parties are connected by point-to-point TCP/IP links which are used continuously for exchanging secret-shares of (intermediate) values in the computation.
The MPyC code development has a bit of history. It started in Python 2 + twisted, but it's now Python 3 + asyncio since at least five years. The major design decisions pertaining to asyncio's event loop go back to Python 3.6 (currently Python 3.8+ is required).
I'll simply sketch a few reasons for accessing the loop while it's not running.
-
Cache the loop inside application to save on
get_event_loop()calls.
The number of calls can run into the millions because Futures are used to handle individual data values, and many micro-Tasks are used as well. The need for caching the loop may be less important sinceget_event_loop()has been optimized at some point after Python 3.6. -
Configure the loop, as part of set-up code that typically runs in non-async mode (e.g., when loading modules).
Concretely, setting the loop's exception_handler with an adapted one.
By doing this on the cached loop this can be done once and for all. -
Use of
loop.run_until_complete()from the (non-running) loop.
A particular reason is thatrun_until_complete()accepts a Future as argument and returns the Future's result.
This cannot be replaced by a call toasyncio.run(); even if one wraps the given Future in a coroutine, the Future will still be attached to a different loop, and thereforeaysncio.run()will fail.
Note that this only is an issue if one already created a Future before the loop is running! But see the next reason. -
Creating Futures (attached to the not-yet running loop) from non-async code.
The Futures are hidden from the user inside lower-level constructs, and serve as placeholders which will be assigned the results of Tasks that have been set out. These Tasks correspond to the evaluation of "MPyC coroutines" of which many will be active at the same time. The non-async code we are calling from include:- top-level code in a Python script
- calls inside unit tests
- top-level code in REPL (less of an issue because of asyncio REPL for Python 3.8+)
- top-level code in Jupyter notebooks (no issue anymore since top-level await is standard in notebooks)
-
Use Futures across multiple
loop.run_until_complete()calls.
Alternate between non-async code and async code, where Futures carry part of the program's state.
A typical piece of code using mpc.run as a shorthand for loop.run_until_complete looks as follows:
x = mpc.input(secint(1)) # creates Task that will set Futures inside x (once all values are exchanged)
s = mpc.sum(x) # do some computation on x (which is a list with one entry per party)
y = mpc.output(s) # creates a Future that will hold the result of the computation
m = mpc.run(y) # m becomes a Python int, equal to the number of parties
Only at the last line we need to wait for the result, and if the above code is part of async code we would use m = await y instead.
Summarizing, we are using asyncio's event loop freely, without much concern if the loop happens to be running or not. The main goal being to accommodate mixes of non-async code and async code, and viewing the loop attached to a thread as a unique piece of real estate to do so.
Hm... If you are okay with delegating creation of the loop to something else, may I recommend using the Runner class added to 3.11? You should be able to do something like
r = asyncio.Runner()
loop = r.get_loop()
loop.set_exception_handler(exc_handler)Now you can run either futures or coroutines:
loop.run_until_complete(fut)or
r.run(coro(arg, ...))The Runner instance allows reusing the loop as many times as you want. When done, use
r.close()You can hide all this detail inside your own abstraction. There's no need to use with Runner() as r: ....
Right, thanks. The Runner class was also suggested by @graingert above. Indeed, this should give enough flexibility to set an exception handler and more generally one gets direct access to the underlying loop.
What I overlooked previously is that--unlike asyncio.run()--the loop is kept alive after r.run() is done. It is not closed until r.close() is called as you point out. This may give just enough flexibility to create and carry Futures between subsequent runs of the event loop.
I'll try and see how things work out.
... trying this out was effortless. Double checked things, and turns out that replacing
self._loop = asyncio.get_event_loop() # cache event loopwith
self._loop = asyncio.Runner().get_loop() # cache event loopdoes the job! Everything runs as before, and because the loop was already cached, Futures attached to it persist in between subsequent runs.
One small issue though: if there's a loop already running, asyncio.Runner() will still create a new loop. Would it also be possible that the existing loop is used, if any (because there's supposed to a unique loop per thread anyway)?
You probably want to copy the asyncio.mixins._LoopBoundMixin class instead
Nice, thx, that makes it work combined like this:
self._loop = asyncio.mixins._LoopBoundMixin()._get_loop()
if self._loop is None:
self._loop = asyncio.Runner().get_loop()Now it also works when a loop is already running (like in the asyncio REPL, and probably in a Jupyter notebook as well but I didn't test that for 3.11rc2).
No not like that, you need to copy the _LoopBoundMixin code and subclass it and call self._get_loop().create_future() only when the loop is running
But I actually want to create Futures attached to a (the?) not-yet running loop. At least that's what I've been doing so.
@lschoe in that case you probably want to copy the Future code and make a version that attaches the loop with the LoopBoundMixin only when a result is set. Asyncio supports duck type futures, you're not limited to asyncio.Future
I would prefer to avoid that kind of approach. But currently in 3.11 things like loop = asyncio.Runner().get_loop(); asyncio.Future(loop=loop), where loop is not running work without (deprecation) warning. So, isn't it simply allowed to attach Futures to a non-running loop?
Here's a possible solution if you want to use a Runner with a pre-existing loop. You can call Runner(loop_factory=self.get_running_loop) in that case -- this returns a Runner that gets the existing loop. However when you close that Runner it will close the loop. So just don't call close() (and don't use it as a context manager).
It would however be better if you could refactor your code so that it only uses the Runner when there is no pre-existing loop. Since you seem to have your own run() function anyway that shouldn't be hard. Just distinguish the two cases somehow when you first initialize.
An issue with refactoring along these lines is that there's also the other case that run() is not called at all. Namely, if top-level await is supported like in the asyncio REPL, ipython, and Jupyter notebooks. In that case we want to attach to a preexisting loop as well. A call like asyncio.get_event_loop() currently does the job for this case as well, so the goal is to find alternatives for this solution.
Can you please enumerate and explain your different use cases again, with examples? I feel we're going in circles trying to understand the requirements here.
There's a question in an old Discourse thread that seems related: https://discuss.python.org/t/supporting-asyncio-get-event-loop-run-until-complete-in-repls/5573/10
I realize I don't actually know enough about Jupyter's asyncio loop to be helpful there. Maybe one of the participants here can help?
Yeah, thx. I've gone through a lot of such pages in the past, looking for ways and scenarios combining async and non-async code; with or without having a running or non-running loop around (in python asyncio REPL, Jupyter, ipython).
To maneuver successfully towards solutions, it helps a lot to have access to the loop (if any) and see if it's running or not. That's the question at the top of this issue, whether such access to the loop will remain.
For the case that one wants to call loop.run_until_complete() from a running loop, like in the old Discourse thread above, I'm using a solution that works for a limited type of coroutines, namely where all awaits are used in combination with Futures. But that does not cover the latest case in the Discourse thread because that has a call asyncio.sleep(0.1).
I have a small breakthrough to report. Please read https://discuss.python.org/t/supporting-asyncio-get-event-loop-run-until-complete-in-repls/5573/13.
TL;DR: I now understand why calling run_until_complete() recursively on the same loop is a disaster, and I am proposing to allow running events in a new loop by passing a flag. @1st1 has already approved of such a design. (UPDATE: It appears @1st1 was actually thinking of the "same loop" scenario, see GH-77704.)
We could go further and fix _run_once() so that it doesn't crash when it is called recursively, and then we could even allow run_until_complete() recursively on the same loop, but I am less sure of that -- I'm not certain that there aren't other consequences, for example on the I/O multiplexing code.
Meanwhile for Python versions through 3.11, the only solution is to run the new loop in a new thread. Not terrible, you can almost do it in one line of code:
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor()
result = executor.submit(asyncio.run, coro()).result()In the future we ought to be able to write this instead (PR welcome):
result = asyncio.run(coro(), allow_recursive_loop=True)FWIW a fix for the _run_once() issue was previously proposed by Instagram: https://bugs.python.org/issue33523. It has a PR.
Going back to a different subthread, @lschoe asked:
To maneuver successfully towards solutions, it helps a lot to have access to the loop (if any) and see if it's running or not. That's the question at the top of this issue, whether such access to the loop will remain.
It's clear to me that we need to think very hard before we break that. For example when using the Runner class, in between two run() calls for the same Runner instance, there is a loop but unless you have access to the Runner instance, it is only accessible by calling asyncio.get_event_loop() or its policy equivalent (the latter is a a bit more code to write, but avoids the deprecation warning).
But do you have a use case where you want to do anything with the loop when it is not running and you don't have access to a Runner instance? Your main example seems to be setting an exception handler, which feels like if you manage the runner yourself you could do using runner.get_loop(), and if you don't manage the runner yourself, your code presumably runs only when the loop is active, so you can use get_running_loop(). The latter is the case when using e.g. python3 -m asyncio or (presumably) IPython or Jupyter.
What's a concrete scenario where neither approach works?
Well, as a replacement for this Python 3.10- code
self._loop = asyncio.get_event_loop()
self._loop.set_exception_handler(my_exception_handler)the following Python 3.11 code works:
self._loop = asyncio.events._get_running_loop() # this doesn't give a RuntimeError
if self._loop is None:
self._loop = asyncio.Runner().get_loop()
self._loop.set_exception_handler(my_exception_handler)There is no problem in this respect. Does this answer your question?
Don't use asyncio.events._get_running_loop(), it is an implementation detail. Use asyncio.get_running_loop():
try:
loop = asyncio.get_running_loop()
except RuntimeError:
...Why don't not just use new_event_loop() optionally followed by set_event_loop()? The code asyncio.Runner().get_loop() looks unnaturally to me. You create a Runner instance and immediately drops it, without using any method for which it was introduced.
What Serhiy says.
Hello from a Jupyter/IPython Maintainer!
The issue as I see it is that there can be a global event loop that is set but not running or closed, and we are losing the ability to access that loop to run it again directly if get_event_loop is removed.
For instance in ipython I can run the following:
Python 3.10.7 (v3.10.7:6cc6b13308, Sep 5 2022, 14:02:52) [Clang 13.0.0 (clang-1300.0.29.30)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.5.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: import asyncio
In [2]: asyncio.get_running_loop()
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In [2], line 1
----> 1 asyncio.get_running_loop()
RuntimeError: no running event loop
In [3]: async def test():
...: loop = asyncio.get_running_loop()
...: print(loop, id(loop))
...:
In [4]: await test()
<_UnixSelectorEventLoop running=True closed=False debug=False> 4349277568
In [5]: await test()
<_UnixSelectorEventLoop running=True closed=False debug=False> 4349277568
In [6]: asyncio.get_running_loop()
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In [6], line 1
----> 1 asyncio.get_running_loop()
RuntimeError: no running event loop
In [7]: asyncio.get_event_loop()
<ipython-input-7-6908e23590ee>:1: DeprecationWarning: There is no current event loop
asyncio.get_event_loop()
Out[7]: <_UnixSelectorEventLoop running=False closed=False debug=False>
In [8]: id(loop)
Out[8]: 4349277568We are currently replicating the get_event_loop functionality in IPython here. We are using a similar hack in jupyter_client to support both asynchronous and synchronous kernel managers with minimal code duplication.
Looking at the jupyter client, it seem that their use case can be fulfilled with re-entrant event loop. See #93462
Note that #66435 was previously rejected by @gvanrossum
We were previously using nest-asyncio for its re-entrancy, but decided to move away from it since it relies on private APIs in asyncio and was causing bugs in practice for consumers of jupyter_client.
So I am curious about what IPython actually promises and what it does to keep that promise. I can't quite follow the implementation, but from the example session I infer that there's some magic where if a top-level await is detected (syntactically?) the event loop is activated before the code is executed (and possibly the code is executed in a different way, e.g. by copying it into the body of an async function?), whereas if there's no top-level await, the loop exists (in the policy, presumably) but is not running.
That looks like magic that's totally specific to IPython (presumably inherited by Jupyter) -- the behavior of the primitive REPL invoked via python3 -m asyncio is different, the loop is always running there.
I'd say that IPython already relies on undefined behavior (supporting top-level await must be some kind of hack). Would much be lost if you had to tell the user to use a different function to get hold of IPython's event loop, e.g. ipython.get_event_loop()?
From reading some of the code snippets it also appears that there's a notion of a global event loop (comments/docstrings specifically call out that it's not per-thread) which feels like it goes against asyncio's current trends. Event loops generally don't like to be used from different threads. Or did I misread that?
@Carreau is the lead IPython developer. Matthias, might it be reasonable to follow what python3 -m asyncio does and always have a running loop?
IPython does not depend on jupyter or jupyter_client, it just so happens that both libraries require changes to accommodate the removal of get_event_loop.
In jupyter_client we explicitly have one loop per thread, but I agree it does seems like currently IPython assumes a single global loop.
So I am curious about what IPython actually promises and what it does to keep that promise. I can't quite follow the implementation, but from the example session I infer that there's some magic where if a top-level await is detected (syntactically?) the event loop is activated before the code is executed (and possibly the code is executed in a different way, e.g. by copying it into the body of an async function?), whereas if there's no top-level await, the loop exists (in the policy, presumably) but is not running.
Yes roughly, though python support code = compile(..., "exec", flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT) and coro = eval(code, {'sleep':sleep},{}) for a few years now, so no hacks necessary. We do try to compile with & w/o PyCF_ALLOW_TOP_LEVEL_AWAIT to know if it's top level await or not. If it's aync, we run_until_complete (or trio/curio equivalent), if not we just call coro.send(None) to potentially not nest loops.
might it be reasonable to follow what python3 -m asyncio does and always have a running loop
No, not easily. Because there are some folks that actually write/copy-past code in IPython CLI that start, run and stop loops/switch loop runner. And you can't nest loops officially AFAICT.
We can even change loop runner library in the middle:
In [3]: from asyncio import sleep as asleep
In [4]: from trio import sleep as tsleep
In [5]: await asleep(0)
In [6]: %autoawait trio
In [7]: await tsleep(0)
(note that this use case is likely minimal, but I'm not sure).
python -m asyncio can always start a loop because at REPL startup it knows it is in async mode. IPython will only know it later, once the user decides to use top-level await (or not).
I'd love to be able to have and option to have an always running loop, so that the tasks could run in the background while user is typing at the prompt – but the leaves some complex open questions on the meaning of an async repl as different users don't want tasks to survive between prompts. And that is probably a few month of active work as it is an issue that has been opened for ~ 4 years now on the IPython repo.
Thanks, that clarifies the context quite a bit for me. It's useful to know more about users' behavior.
Nested loops may well come, in one way or another (see above), maybe both ways.
It does sound like you would be happiest if we didn't deprecate asyncio.get_event_loop(), and if it kept the same behavior it already has (possibly creating a new loop if the stars align). (It seems you don't care much about policies?)
But I haven't heard what you would like to have if we went ahead with the plan (I'm not saying we will, I'm just exploring options). What would your fallback position be? monkey-patch asyncio.get_event_loop? Recommend users to do something else (what?). Could we provide a different API that would be almost as good for you and your users?
From the jupyter_client perspective, we would like asyncio.get_event_loop to return an event loop that is already installed (either running or not), but not create one. Our current workaround of tracking the loop we create in each thread is working for us though, so this isn't a strong requirement.
In Jupyter applications, the only time we need to interact with the policy is to set WindowsSelectorEventLoopPolicy when on Windows for compatibility with zmq and tornado.
From the
jupyter_clientperspective, we would likeasyncio.get_event_loopto return an event loop that is already installed (either running or not), but not create one.
That would indeed be a step forward, but it would also be backwards incompatible immediately (for apps still expecting the old semantics) so would itself have to be a deprecation. Possibly we'll have to change the deprecation message to indicate this.
Our current workaround of tracking the loop we create in each thread is working for us though, so this isn't a strong requirement.
Yeah, with enough dedication anyone can build their own mechanism for keeping track of a per-thread loop -- but if this is a common pattern we're better off with an API in asyncio so apps can communicate about this without having to establish bilateral contact first. Which is what asyncio.get_event_loop() was meant to be. (The "also create a new loop, sometimes" part was a mistake, intended for a smooth beginner's experience -- for that we now have asyncio.run().)
In Jupyter applications, the only time we need to interact with the policy is to set
WindowsSelectorEventLoopPolicywhen on Windows for compatibility withzmqandtornado.
That's slightly unfortunate, because the general consensus is that ProactorEventLoop is usually the better event loop. (It wasn't always, but it has steadily improved over the years.) As long as there's a legitimate need to override the default event loop (and this looks like it might still be legitimate in your case) we can't very well deprecate the whole concept of policies from asyncio, even though we'd like to (see GH-94597).
Perhaps a new API could be introduced called get_current_loop?
It would be not difficult to make get_event_loop() returning what was set by set_event_loop() and raise an error instead of creating a new event loop if it was not set or was set to None.
But then we will have a weird situation, when some code emits a Deprecation warning in 3.10-3.11, but works without warning before 3.10 and after 3.11. Should we undeprecate this case in the future bugfix releases of 3.10 and/or 3.11? It would be not so easy. We still need a Deprecation warning in 3.10-3.11 for the case when a new event loop is implicitly created. Emitting a warning with correct staсklevel is a difficult part.
#98440 is an alternate PR which only removes implicit creation of an event loop in get_event_loop(). It now removes a deprecation warning in case when there is no running event loop, but there is a current event loop (set by set_event_loop()).
I like #98440, it's the best compromise. I have asked Yury privately what he thinks. I know this is probably not a good time for him -- we can wait a month or so until he's got more time.
This is now landed in 3.12, with relevant updates to 3.11.1 and 3.10.9.
Thanks for the work, @serhiy-storchaka!
I suspect this issue has introduced a regression exhibited as prompt-toolkit/python-prompt-toolkit#1696.
@jaraco Maybe we should file a new issue for the regression?
We still need a Deprecation warning in 3.10-3.11 for the case when a new event loop is implicitly created.
Since 3.10 and 3.11 did not ship with a warning for this case, I don't think it is right to proceed with removing the behavior in 3.12. Normal policy without an exception granted is to warn in 3.12 and deprecate the behavior in 3.14.
Yeah, we went too fast here. I agree with that recommendation.
The documentation of asyncio.get_event_loop() says:
If there is no running event loop set, the function will return the result of calling
get_event_loop_policy().get_event_loop().
Which by default raises a RuntimeError, so it is rather confusing. Should the documentation be updated to explicitly say something like:
If there is no running event loop set, the function will return the result of calling
get_event_loop_policy().get_event_loop()which raises aRuntimErrorwithDefaultEventLoopPolicy.
?
Oooh, the decumentation is indeed a mess. There's no place that I can find where the behavior of DefaultEventLoopPolicy.get_event_loop() is currently documented. AbstractEventLoopPolicy is vague (just says it returns a loop and never None), DefaultEventLoopPolicy doesn't say anything specific, and BaseDefaultEventLoopPolicy is undocumented (and private, apparently, though in the light of the PEP 387 discussions it might be implicitly public). The documentation section linked to should be hyperlinked to get_event_loop_policy() and some policy class's get_event_loop() method, which should be documented.
Is it fair to say this is now a documentation issue, or is there more to be done? Should this hold up 3.12.0a4?
Is it fair to say this is now a documentation issue, or is there more to be done? Should this hold up 3.12.0a4?
Hm, #100410 isn't merged yet, even though I approved it two weeks ago. @serhiy-storchaka do you have any hesitations?