pytest-asyncio: Breaking change in 0.23.*
Hello! Something has been broken with the latest pytest-asyncio releases.
Consider such code:
import asyncio
import pytest
@pytest.fixture(scope='session')
def event_loop():
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture(scope='session')
async def some_async_fixture():
return asyncio.get_running_loop()
async def test_something(some_async_fixture):
assert asyncio.get_running_loop() is some_async_fixture
pytest.ini:
[pytest]
asyncio_mode = auto
This test passes with pytest-asyncio 0.21.1 but fails with 0.23.0. I am not sure if it’s ok, but if it is, IMHO it is breaking change.
About this issue
- Original URL
- State: open
- Created 7 months ago
- Reactions: 59
- Comments: 32 (8 by maintainers)
Links to this issue
Commits related to this issue
- fix tests: cap pytest_asyncio at < 0.23 due to a breaking change in 0.23.* Details: https://github.com/pytest-dev/pytest-asyncio/issues/706 — committed to userver-framework/userver by Anton3 7 months ago
- 📍💥pinned pytest-asyncio to v0.22.0 (cf. https://github.com/pytest-dev/pytest-asyncio/issues/705 and https://github.com/pytest-dev/pytest-asyncio/issues/706) — committed to Hochfrequenz/edi_energy_scraper by DeltaDaniel 7 months ago
- refactor[pytest]: Update test cases for new pytest-asyncio version [Breaking change in 0.23.*](https://github.com/pytest-dev/pytest-asyncio/issues/706) [Loop is closed before fixture teardown complet... — committed to techyangon/poly-py by waiyan13 6 months ago
- use anyio in tests and update deps — committed to stuartaccent/fastapi-boilerplate by stuartaccent 5 months ago
- fix: test suite is now fully passing using SQLite or PostgreSQL (#27) # Description This PR add changes so the test suite is fully supported by PostgreSQL (and with SQLite too): * Change `pytest... — committed to argilla-io/argilla-server by jfcalvo 5 months ago
- requirements - nail pytest-asyncio c.f. pytest-dev/pytest-asyncio#706 — committed to commonism/aiopenapi3 by commonism 5 months ago
- requirements - nail pytest-asyncio c.f. pytest-dev/pytest-asyncio#706 — committed to commonism/aiopenapi3 by commonism 5 months ago
- python311Packages.pytest-asyncio_0_21: init at 0.21.1 Version 0.23 was accidentally a breaking change: https://github.com/pytest-dev/pytest-asyncio/issues/706 — committed to dotlambda/nixpkgs by dotlambda 4 months ago
- HF-54: Downgraded `pytest-asyncio`'s last version has issue that breaks our existing tests(https://github.com/pytest-dev/pytest-asyncio/issues/706) — committed to e-gulakhmet/HomeFinancier by e-gulakhmet 4 months ago
I’m glad to see more people are experiencing this issue. Apologies, but the latest version is an absolute nightmare. After 5 hours of trying to adjust my environment I still can’t get it working. I have downgraded to
pytest-asyncio 0.21.1and will try to hold on to it as long as possible. I hope there will be some future changes to make this migration easier, but for now it’s a huge headache.@seifertm while I deeply appreciate your work, which is crucial for all python and pytest users dealing with async tests, I don’t understand why you had to change the way the event loop is set up: the previous way worked just fine.
Please consider restoring the previous behaviour.
As for use cases, a few people provided their setups and needs in #657 (and this was my comment)
@seifertm I’d like to have only one loop for all fixtures and tests, without additional decorators to all tests and fixtures. We have thousands of tests in hundreds of services where all tests and fixtures share one loop and it is crucial for them. Is there any workaround to emulate the old behaviour?
This way https://pytest-asyncio.readthedocs.io/en/v0.23.2/how-to-guides/run_session_tests_in_same_loop.html does not consider fixtures at all(
I’m afraid I’m not in a position to judge the approach: I don’t know enough about the previous design and the desired long-term design. I can see how switching from event loop to even loop policy still allows folks to switch to alternative event loop implementations (we, for example, use uvloop) but also how it makes it harder to define custom scopes, or even different scopes depending on the test or test package.
IMHO the best solution is the one that makes the end developer write as little code as possible, and only code that is strictly related to what the dev wants to do. With this in mind, overriding the
event_loopfixture was good enough: it’s 5 lines of code written in the properconftestfile. Markers instead are not good, as it’s lots of boilerplate for specifying the same setting all over a test suite: makers should better be used to specify exceptions, not rules.Sorry, but I don’t understand, I have not only session scoped fixtures but also module scoped fixtures, and they should use the same event loop as session scoped fixtures. Could you please describe how to achieve it?
We write blackbox tests for microservices using pytest and pytest-asyncio. Some session scoped fixtures for example create database connection pool, which all tests can use to check database state. Another session scoped fixture in background monitors logs of subprocesses (instances of application, that we test) and captures these logs to some list, which tests can check. These subprocesses can be started by any fixture and test as async context manager. And obviously, subprocess (asyncio.subprocess) should be started with the same loop as fixture that captures logs from it. And we have way more such examples.
I think the real problem is that the migration process is not clear. I would have expected that removing the
event_loopfixture would be enough withasyncio_mode = auto, but it’s not.Here is a minimal reproducible example (with GitHub CI!) of a real world application that I can’t migrate to 0.23 so far: https://github.com/ramnes/pytest-asyncio-706
@seifertm Any hint on how something like this should be migrated? (PR welcome on that repository.) If it’s not possible to migrate, then this would be the real issue: it wouldn’t be a breaking change but a loss of functionality. Otherwise we’ll probably have a few bits to add to the documentation here. 😃
I confirm, we use similar setup with 2 session level fixtures (one to redefine event loop, another for our own purposes), tests don’t work anymore, complain either about “The future belongs to a different loop than the one specified as the loop argument” or “Event loop is closed”.
@ramnes Thanks for the example project. I think this is the migration experience we should be aiming for.
The issue is that fixture scope is currently bound to loop scope. That means a session-scoped fixture will always be executed in a session-scoped loop, which is apparently a blocker for many users.
What does everyone think about introducing a separate
loop_scopeargument topytest_asyncio.fixture, in order to control the fixture scope and loop scope separately? For example:It says “This release is backwards-compatible with v0.21. Changes are non-breaking, unless you upgrade from v0.22.”
Here’s an example that’s similar to our tests that I don’t think can be upgraded to 0.23. Essentially, we have module-level async fixtures for setting up a relatively expensive Application, and function-scoped fixtures for small customizations to the app that are selected on a per-test basis. The function-scoped fixtures and tests need the same loop as the app because the application’s loop must be running to handle the async operations in the function-scoped fixtures and the tests themselves.
Both asserts fail because both the function-scoped fixture (expected) and the test (unexpected) run with the function_scoped event loop (
@pytest.mark.asyncio(scope="module")is ignored on the test due to the inclusion of the function-scoped fixture, which I assume is not intended? If the function-scope fixture is removed, the module-scoped event loop is used.I believe @seifertm’s proposal of an explicit
loop_scopedecorator for async fixtures would address our case just fine.If we wanted to workaround the issue today, I believe we could replace all of our function-scoped async fixtures with sync fixtures that use
app.event_loop.run_until_complete(...).What we really want is to recover the behavior of “everybody please use the module-level event loop”, so whatever path to that behavior works best for pytest-asyncio ought to be fine for us. In our case, all of our function-level async fixtures depend on our module-level async fixture, so it is conceivable that the situation is detectable, but I’m sure an explicit decorator on every fixture is easier to handle, if a bit more tedious to use. A single flag for defining a lowest or default level of event-loop creation would be simpler for us to use (e.g.
asyncio-loop-min-level = "module"would mean function-level fixtures and tests use module-level event loop by default but not session, etc.), if that’s practical.It also seems reasonable to me to expect a function-scoped fixture to run with the same loop as the function itself, so if I e.g. have
@pytest.mark.asyncio(scope="module")on a test, it makes sense to use that scope (as the default, at least) on all function-scoped async fixtures used in the test. I’m not sure if that information is readily available at the right time, though, so I’d understand if it’s not practical to implement.hi @seifertm , thank you for your reply and references.
If I got this right, pytest-asyncio 0.21.1 is perfect (
get_event_loopdeprecation aside), but you’re concerned by the mess folks do in an overriddenevent_loopfixture, you want to prevent them from shooting themselves in the foot, and would rather give them specific interfaces for fiddling with low-level stuff.I’ll say something that might sound counter-intuitive: I don’t think you should care.
I think this library should perfectly do what it does, document where and how it can be customised, and point folks to docs when they abuse certain fixtures or fiddle with the library internals.
The documentation already provides a clear example of what is needed to customise the default
event_loopfixture. If folks start adding tons of unnecessary stuff to it, it’s not this library’s fault, and it’s not something this library should handle. To the extreme that it should not be this library’s responsibility to close an unclosed event loop: if anything, it should raise an exception when it catches that the event loop hasn’t been closed.If folks think it is necessary to do all the stuff they do in their
event_loopfixtures, I think they’ll find a way to do it regardless of the constraints imposed by the library. Especially with a language like python where monkey patching is so easy.Fixing wrong customisations on behalf of devs makes your work harder, as the library starts becoming a knight fighting off dragons. And there will always be dragons.
After upgrade to v 0.23 error in teardown:
Output:
Yes, we have already downgraded to 0.21.1. I don’t think it’s gonna be a problem for us for a long time (at least until Python 3.13).
Thank you! Looking forward to the news.
The version 0.23.0 changelog is already mentioning the breaking change: https://github.com/pytest-dev/pytest-asyncio/releases/tag/v0.23.0
I went through the same, the new way to do it can be seen in this PR https://github.com/pytest-dev/pytest-asyncio/pull/662/files Would be nice to have some documentation on to how to migrate to the new way.
We also needed to downgrade to 0.21.1 to make our project work. Waiting for a new update. Thanks!
@seifertm 2 questions about this proposal:
1st question: Let’s say that we have a test with 2 async fixtures with different loop_scope. Should I expect pytest-asyncio to raise an error? Let’s say we have the following 2 fixtures:
In this case, the 2 fixtures are using 2 different loop scopes, but it should be possible to run the
random_csv_fileone with thesessionscope, right? When thinking of all different scopes (package/module/…) there seems to be some kind of compatibility/loop scope overrides that can be made, but seems like it should be investigated carefully.2nd question: Can we have some extra configuration for the
mode=autothat also allows us to define the default loop_scopes?Hello. These breaking changes are really annoying.
Anyway test is not failed and async code was executed successfully. I suggest to include gists for cases of using ORMs in tests, because there are many approaches to achieve
tearUpandtearDown, e.g.:It works as expected, but with warnings. I have seen approaches with
that causes errors if used with
parametrizefunctions. Package is a ‘must have’ if working with asyncio in my opinion. Make it easier to use, please.It was good enough but a terrible DX honestly. It took me quite a while to figure out the first time I’ve seen this problem (and thought it was a bug or very bad design). If the goal here is that we don’t need these five lines anymore, I’m all in and will just pin my dependency until we update our code. Let’s not revert to something ugly if the new approach is better.
Also, the semantic versioning meaning of 0.* versions is that they can introduce breaking changes anytime. If you don’t want to hit this kind of breaking changes in the first place, just pin your dependencies…
I did not follow exactly the issue, but I wanted to mention that 0.23 broke all our pipelines at work with some strange errors and the same with one of the open source project I maintain: https://github.com/FreeOpcUa/opcua-asyncio . The solution so far has been to revert to 0.21 everywhere
That was the original intention, yes. I can reproduce the difference between v0.21.1 and v0.23 and I agree that this is a breaking change (by accident).
With regards to the migration and as a workaround: The fundamental idea of v0.23 is that each pytest scope (session, package, module, class, and function) provides a separate event loop. You can decide for each test in which loop they run via the new
scopekwarg to theasynciomark.@tkukushkin In your specific example you want to run the test in the same loop as the fixture. The
some_async_fixturefixture hassessionscope. In order to achieve your goal, you should marktest_somethingaccordingly:See also the part about asyncio event loops in the Concepts section of the docs.
@redtomato74 I’d like to hear more about your use case for two different event loops. I suggest you open a separate issue for this.