-
Notifications
You must be signed in to change notification settings - Fork 159
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
Async fixtures request wrong event loop with 0.23.0a0 #670
Comments
Thanks for giving the alpha release a try! The v0.22 release relied on marks applied to pytest Collectors (e.g. classes and modules). The logic hasn't been adjusted accordingly in the alpha release and there were no tests with mixed scopes between fixtures and tests. As a result, this problem wasn't caught. In my opinion, the expected behaviour of your first example is that async_fixture is run in a package-scoped loop and test_async_fixture is run in a function-scoped loop. When test_async_fixture is marked with |
Shouldn't |
The idea was that the loop scope for each test is determined by the scope argument to the asyncio mark of the respective test. The event_loop_policy was meant to be independent from that. It returns an event loop policy (=a factory for event loops) and applies to all tests and fixtures in its scope. Those tests and fixtures use the policy to instantiate event loops with different scopes. The policy fixture just happens to have package scope in your initial example, because pytest would not allow it to be requested by async_fixture, otherwise. In general, a user shouldn't have to touch the policy fixture at all, unless they specifically want to test with a non-default event loop or with multiple event loops. Does this idea make sense? Or do you think it would be confusing? |
Ah. I can see the logic behind that, but it wasn't actually my motivation for proposing the I think there are really three distinct problems here, and we need three solutions to them:
from pytest_asyncio import Mark # hypothetical
@pytest.fixture # scope could be set here if required
def asyncio_mark():
return Mark(scope="package") The fixture could be auto-requested, like The current setup requires something like putting pytestmark = pytest.mark.asyncio(scope="session") at the top of every test file. This isn't the end of the world, but I think in general adding boilerplate is best avoided, and it's definitely a problem with large test cases for session-scoped fixtures (e.g. tests for a database bound app). If I'm right this would still be preferable to the I'd be interested to know if I've got the right end of the stick about the motivation for deprecating Thanks for your time! |
I agree. It's not intuitive that the policy needs to be scoped at least as large as the largest fixture scope or test scope affected by the policy. I hope we can reduce errors related to this issue with good documentation. I deliberately used session scope for the policy fixtures in the how-tos. I think we should add a paragraph about this issue specifically, though. On the upside, pytest will give an appropriate error message when the policy fixture scope doesn't match. I think we can get away with that for now.
That's a really good breakdown. I wish I had it earlier :)
That's an interesting proposal. From the top of my head, I think we'd need some special code make that happen. Markers are available during collection time, whereas fixtures are only evaluated when tests are run. Pytest-asyncio currently performs some sort of "fixture preprocessing" during collection time, but it requires reaching rather deep into pytest internals. I suppose the proposed fixture would work, but it wouldn't be particularly easy to implement. I scoured the pytest bug tracker and docs if there's a way to mark packages or sessions. Apparently, it is possible to mark packages using a event_loop deprecation reasoning
Exactly, this is the biggest problem. The event_loop fixture became a magnet for all kinds of functionality that is completely unrelated. Here's one of many examples (not my code, but posting it without reference): @pytest.fixture(scope='session')
def event_loop(
use_uvloop: bool,
) -> Generator[asyncio.AbstractEventLoop, None, None]:
"""Get event loop.
Share event loop between all tests. Necessary for session scoped asyncio
fixtures.
Source: https://github.com/pytest-dev/pytest-asyncio#event_loop
"""
context: ContextManager[Any]
# Note: both of these are excluded from coverage because only one will
# execute depending on the value of --use-uvloop
if use_uvloop: # pragma: no cover
uvloop.install()
context = contextlib.nullcontext()
else: # pragma: no cover
context = mock.patch(
'uvloop.install',
side_effect=RuntimeError(
'uvloop.install() was called when --use-uvloop=False. uvloop '
'should only be used when --use-uvloop is passed to pytest.',
),
)
policy = asyncio.get_event_loop_policy()
loop = policy.new_event_loop()
with context:
yield loop
loop.close() The freedom for users to add to the implementation of the fixture leads to a lot if different failure modes, each of which requiring dedicated cleanup code in pytest-asyncio to avoid messing up subsequent tests. For example, pytest-asyncio closes the old event loop before using installing the loop provided by event_loop fixture, in order to avoid ResourceWarnings: pytest-asyncio/pytest_asyncio/plugin.py Lines 399 to 410 in a10cbde
It also installs a fixture finalizer to catch cases where a user forgot to call pytest-asyncio/pytest_asyncio/plugin.py Lines 443 to 455 in a10cbde
Counting the event_loop fixture itself, there are already three different cases where pytest-asyncio tries to close the loop. All these special cases make it extremely hard to implement meaningful changes, especially for new contributors. It would mean that users have to change all their custom event_loop implementations every time pytest-asyncio makes a change to the fixture. Changes to the fixture are required to support #127, for example.
The problem with custom code is that the problem often isn't immediately visible in the current test, especially when considering ResourceWarnings emitted when something is garbage collected. The purpose of pytest-asyncio is simply to make it more convenient to test asyncio code. This comes with the restriction that pytest-asyncio is in control of the asyncio event loop. If users require low-level control over the loop, I'm happy to provide those, but they should come as defined interfaces. |
Cross-referencing the question about package-level |
I've spent a reasonable amount of time dealing with this; I agree that anything which makes it easier to see exactly where a loop is created and torn down is a good idea.
That makes sense. I think the policy (later factory) interface is probably all one could ever require. I've been trying to think of something I couldn't do with it, but even things like running tasks on the loop or deliberately closing it (both of which probably belong in the test or a dedicated fixture if they were somehow required) could be done by overriding the factory. The case above, for instance, would be better handled IMHO just by being able to hand the right policy out to use uvloop. (Since I've inadvertently closed the issue and posted this too early I'll continue in a separate message.) |
Ah, somehow I thought the setup happened when the fixture was requested. That would be a solution, but I don't want to propose serious architecture changes.
Personally I think this would be fine. However, it appears from some testing here not to work in sub-packages. There does need to be some way of running a single event loop for a package and all its subpackages, although I think just repeating the mark would likely work fine (the 'package' scope already takes subpackages into account, it just appears that the For session scope I think a hook setting the mark dynamically might be the way to go: crudely one can do something like: # tests/conftest.py
import pytest
def pytest_collection_modifyitems(items):
marker = pytest.mark.asyncio(scope="session")
for item in (x for x in items if "Coroutine" in repr(x)):
item.add_marker(marker) (with a proper test for coroutines: I couldn't find anything quickly in inspect which seemed to work) And then the following passes: # tests/pkg/test_marks.py
import pytest
from pytest import FixtureRequest
marker = pytest.mark.asyncio(scope="session").mark
def test_sync_marks(request: FixtureRequest):
marks = list(request.node.iter_markers())
assert marker not in marks
async def test_async_marks(request: FixtureRequest):
marks = list(request.node.iter_markers())
assert marker in marks
@pytest.mark.asyncio(scope="function")
async def test_custom_mark(request: FixtureRequest):
marks = list(request.node.iter_markers())
assert marker in marks
assert pytest.mark.asyncio(scope="function").mark in marks I don't know what the behaviour should be in the third case. In any case I doubt many test suites use both session scope loops and second fixture scoped loops (I thought that wasn't supported, although I don't know): if a session scoped fixture is used it's probably the only one. So a snippet in the docs for (effectively) a session scoped hook to set the mark is just as easy as a fixture to use, if a little less intuitive. [my vim fingers keep closing this issue by mistake, sorry: not sure what I'm pressing] |
I added issue #676 for finding a simpler way to configure session-scoped loops. |
@2e0byo pytest-asyncio 0.23.0a1 should fix this issue. |
pytest-asyncio 0.22, 0.23 and 0.24 are broken in various ways, cf. * pytest-dev/pytest-asyncio#670 * pytest-dev/pytest-asyncio#718 * pytest-dev/pytest-asyncio#587 Additionally, pytest 8 cannot work with pytest-asyncio 0.21, so we need to pin pytest to 7.x too. Perhaps there is a way to make this work, but pin it to earlier versions to do this deliberately at some point.
Interesting things seem to happen with the current logic in
0.23.0a
when collecting async fixtures.Running a venv with almost nothing installed:
A single dir
tests
contains an__init__.py
and thetest_async_fixtures.py
file; pytest is invoked aspytest tests/
This fails:
Complaining:
Hmm. As a guess, is
async_fixture
implicitly requesting the wrong event loop? What happens if I mark it?No, the same as before (I have no idea if one is supposed to be able to mark fixtures like this).
What about marking the whole module explicitly?
Ah! now we get a more interesting error:
What about marking the test function with an explicit scope?
This raises the new 'Multiple asyncio event loops with different scopes' error.
Traceback
Assuming I'm using the new code correctly, it looks like there's a problem with async fixtures and policies. At any rate here's a failing test case.
The text was updated successfully, but these errors were encountered: