-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create AsyncIOLoop from existing asyncio event loop #2324
Comments
Shouldn't I'm definitely open to suggestions here. I'm not at all familiar with testing practices in the asyncio world (and only barely familiar with pytest). Part of the problem is that AsyncTestCase does two different things: it sets up a clean-slate environment for the tests (the new IOLoop), and it provides utilities for the tests to use at runtime (setting up the HTTPServer, etc). One way to look at this problem is that the former part of AsyncTestCase is in conflict with pytest-asyncio, and we should decouple them so that you can use the other parts of AsyncTestCase while leaving the event loop setup to pytest-asyncio. |
That does work for the first test, but all subsequent tests fail. As a note – that is how we were had it set up pre-Tornado 5. But it required overriding pytest's default behavior (which is to create a new loop for each test via I think the simplest solution would be to add something like I was trying to figure out how something like that might work but I got hung up on the |
But this is exactly what When |
This improves compatibility with asyncio test frameworks like pytest-asyncio. Fixes tornadoweb#2324
That seems reasonable. I did some more playing around, and I was able to simplify our setup quite a bit. It turns out that pytest-asyncio's event loop fixture doesn't call And this even works without f47d639, since it doesn't depend on |
This improves compatibility with asyncio test frameworks like pytest-asyncio. Fixes tornadoweb#2324
@adamrothman In case you're still following this issue: This interaction between AsyncTestCase and pytest-asyncio has become problematic with changes made in Python 3.10 and 3.12 (See #3223 (comment)). We're probably going to have to effectively revert #2327. I notice that now that the standard library has its own counterpart to Tornado's AsyncTestCase (IsolatedAsyncioTestCase), they never did anything like we did here to support pytest-asyncio. Is there a different solution now? I've barely ever used pytest so I'm not sure I understand the issue here (our test case here is artificial; we don't have an actual pytest-asyncio example to work with). If you want to lobby for the continued existence of this feature, could you provide a minimal complete example where the issue applies? |
Thanks for following up! The projects I was working on that prompted me to open this issue are long dead and I don’t write much Python anymore since switching to Go. Feel free to pursue whatever solution makes the most sense for Tornado and it’s users. |
Creating a new
AsyncIOLoop
always creates a new underlying asyncio event loop. I think this makes sense in most cases but it would be very helpful (especially for testing, read on) to be able to pass an existing asyncio event loop for Tornado to wrap.We use pytest with the pytest-asyncio plugin to run our test suite for several Tornado applications. After upgrading one of these apps to Tornado 5 and improving our test hygiene to use a new event loop for each test, our test suite broke.
Our unit tests are simple because they don't interact with or depend on
tornado.testing.AsyncTestCase
at all. Our integration tests do (because they make requests against the application), however, and this is where I ran into trouble. When I ran the tests, I would get a ton of errors due toTask
s gettingFuture
s attached to a different loop, like this:pytest-asyncio creates a new event loop for each async test that it runs (which is a good thing). Previously we were overriding this behavior to always return the "main" loop via
asyncio.get_event_loop()
and explicitly not close the loop between tests. This worked under Tornado 4 because we were usingAsyncIOMainLoop
, and Tornado 4 did not close the underlying asyncio event loop in such a configuration. But this override has always felt kind of gross to me, so I resolved to remove it. After all, there's no reason it should be necessary; we are careful to pass the testing event loop to classes that require it so they don't grab the main loop by default. Plus, Tornado 5 does close the underlying loop when closingAsyncIOMainLoop
.But making this change borked our tests.
We have some setup code that is shared between the unit and integration tests. This setup is handled by pytest, so a number of async singletons (e.g. DB connectors) get configured to use the event loop created by pytest-asyncio for each test.
tornado.testing.AsyncTestCase
also creates a loop for each test – anIOLoop
– by callingget_new_ioloop
as part ofsetUp
. This is still a good thing – but turned out to be the cause of my sadness. During the integration tests, our application logic is run by Tornado's loop, but per the above, the async singletons use pytest-asyncio's loop. When async code running on one loop tries to interact with async code on the other, people weep in the streets.Being able to override
AsyncTestCase.get_new_ioloop
goes most of the way towards solving this issue. It's not difficult to expose pytest-asyncio's event loop to this function – the problem is that I can't use this existing asyncio event loop to instantiate the Tornado IOLoopget_new_ioloop
is expected to return.For now I'm working around this limitation by overriding pytest-asyncio's default behavior to first create an
AsyncIOLoop
and then using the underlying.asyncio_loop
. This feels wrong, though. pytest shouldn't need to know about what's happening "downstream" as long as it implements a compatible test interface likeunittest.TestCase
.Mine is just one (long-winded, sorry) case but I can imagine there are other situations in which users would want to provide an existing event loop to Tornado. What do you think @bdarnell?
The text was updated successfully, but these errors were encountered: