1.1.0: Session fixture with `asyncio_default_fixture_loop_scope=function` yields `You tried to access the function scoped fixture _function_scoped_runner with a session scoped request object`
Opened this issue · 10 comments
Hit this in the wild while trying to run fakeredis's test suite.
Trivial reproducer:
import pytest_asyncio
@pytest_asyncio.fixture(scope="session")
def foo():
pass
def test_foo(foo):
pass$ pytest -o asyncio_default_fixture_loop_scope=function test_foo.py -vv
========================================================= test session starts =========================================================
platform linux -- Python 3.13.5, pytest-8.4.1, pluggy-1.6.0 -- /tmp/fakeredis-py/.venv/bin/python3
cachedir: .pytest_cache
hypothesis profile 'default'
rootdir: /tmp
plugins: asyncio-1.1.0, hypothesis-6.136.6
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function
collected 1 item
test_foo.py::test_foo ERROR [100%]
=============================================================== ERRORS ================================================================
_____________________________________________________ ERROR at setup of test_foo ______________________________________________________
ScopeMismatch: You tried to access the function scoped fixture _function_scoped_runner with a session scoped request object. Requesting fixture stack:
test_foo.py:4: def foo()
Requested fixture:
pytest-asyncio/pytest_asyncio/plugin.py:774: def _scoped_runner(event_loop_policy, request: 'FixtureRequest') -> 'Iterator[Runner]'
======================================================= short test summary info =======================================================
ERROR test_foo.py::test_foo - Failed: ScopeMismatch: You tried to access the function scoped fixture _function_scoped_runner with a session scoped request object. Requesting fixture stack:
test_foo.py:4: def foo()
Requested fixture:
pytest-asyncio/pytest_asyncio/plugin.py:774: def _scoped_runner(event_loop_policy, request: 'FixtureRequest') -> 'Iterator[Runner]'
========================================================== 1 error in 0.12s ===========================================================Reproduced on 92962d1.
I think the issue here is that the invocation scope is set to "session" (such that fixture called once per session), but the loop scope is set to "function" via the config (meaning the loop is reset once per function). The loop scope can't be narrower than the invocation scope, hence the error. Setting loop_scope="session" resolved the issue for me.
By the way, I was able to reproduce the issue using an async test and fixture (the example you gave uses a sync test and fixture, not sure if that's what you intended).
import pytest
import pytest_asyncio
@pytest_asyncio.fixture(scope="session")
async def foo():
pass
@pytest.mark.asyncio
async def test_foo(foo):
passI'm sorry but I'm only a third party here — it looked like a regression in pytest-asyncio (it worked with the earlier versions), so I've reduced what fakeredis was doing and reported it here. Should I pass it on to fakeredis that it's not valid?
If my bisection was correct, this was introduced in 8b8e6e9. I'm not a maintainer, so can't say if this was intended.
Should I pass it on to fakeredis that it's not valid?
With the caveat that I'm not familiar with fakeredis, if your reproducer reflects what they are doing, it seems invalid. They shouldn't be using pytest_asyncio decorators on synchronous tests. If the test is supposed to be asynchronous, the loop scope can't be narrower than the invocation scope.
Thanks for the report!
I think the issue here is that the invocation scope is set to "session" (such that fixture called once per session), but the loop scope is set to "function" via the config (meaning the loop is reset once per function). The loop scope can't be narrower than the invocation scope, hence the error. Setting loop_scope="session" resolved the issue for me.
Exactly, this is the issue!
I want to point out that the reproducer succeeds with the deprecated configuration option asyncio_default_fixture_loop_scope=None, but fails with asyncio_default_fixture_loop_scope=function (as configured in the reproducer) . The former case is equivalent to
@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def foo():
passwhereas the latter case is equivalent to
@pytest_asyncio.fixture(scope="session", loop_scope="function")
async def foo():
passWhen spelling it out, it becomes visible that the second version wants to cache the fixture result for the whole session, but tries to use a fresh loop every function. This is not possible and pytest complains about it.
That said, I cannot find any difference in behavior between pytest-asyncio v1.0.0 and v1.1.0. Also pytest-asyncio v0.24, which is used by fakeredis, behaves the same way.
From my point of view, it's neither a bug nor a regression. As mentioned in a recent discussion this specific constellation should give a more descriptive error message, though.
How can pytest-asyncio help out here?
That said, I cannot find any difference in behavior between pytest-asyncio v1.0.0 and v1.1.0. Also pytest-asyncio v0.24, which is used by fakeredis, behaves the same way.
Hm, but my original reduced example and fakeredis's test suite works with v1.0.0 but fails with v1.1.0.
I'll file the problem with fakeredis now.
Hm, but my original reduced example and fakeredis's test suite works with v1.0.0 but fails with v1.1.0.
You're right. I accidentally ran tjkuson's version, not your original reproducer. I can confirm that it used to work with v1.0, but not with v1.1.
Preferably, this should be fixed on the fakeredis side. Using the pytest_asyncio.fixture decorator on sync functions doesn't make sense as it doesn't give any advantage over pytest.fixture.
If there's an easy way to address this in pytest-asyncio, we should still do it…
Hmm, sync functions were probably result of me reducing it, sorry. The original happens with async functions.
Filed cunla/fakeredis-py#403.
You're right. I accidentally ran tjkuson's version, not your original reproducer. I can confirm that it used to work with v1.0, but not with v1.1.
To be clear, the bisection I performed was on the original reproducer, not the updated async one I posted. Sorry for any confusion I caused! I think the commit I linked previously changed what happens when sync tests are decorated with pytest_asyncio decorators.
@tjkuson No worries. I really appreciate you helping out, also with the recent PRs!
I filed cunla/fakeredis-py#404 which should fix the issue with fakeredis-py, specifically.
At the same time, pytest-asyncio minor version upgrades shouldn't break existing test suites. However, providing a fix for this issue probably needs some additional thought.
Currently, I'm leaning towards not providing a fix at all, but raising a warning that transitions to an error in the next major version.