Skip to content

Requesting loop_scope="module" (or "session" etc.) returns incorrect event_loop if another test function requests a different loop. #950

Closed
@TimChild

Description

@TimChild

First of all, thanks for the great pytest plugin!
Also, I like the idea of being able to specify event_loop scopes explicitly and separately to the fixture scopes.

While playing with this, I kept running into issues that my "session" scoped event_loop seemed to change between tests that were running in different files, but only if certain other tests were run...

I think this example reproduces the issues in a single file. I recognize that the docs explicitly advise against doing this, but I hope it makes the problem easier to reason about.

Here's the example.

import pytest_asyncio
import asyncio
import pytest


class FakeAsyncConnection:
    def __init__(self, loop):
        self.loop = loop

    async def do_something(self):
        # Check if the current loop is the same as the one with which the
        #  connection was created
        if asyncio.get_event_loop() is not self.loop:
            raise RuntimeError(
                "This connection is being used with a different event loop!")
        return "Success"


@pytest_asyncio.fixture(scope="module", loop_scope="module")
async def async_connection():
    """Set up a async connection object with module scope."""
    event_loop = asyncio.get_event_loop()
    print(f"Setting up fixture: event_loop_id {id(event_loop)}")
    connection = FakeAsyncConnection(event_loop)
    yield connection


@pytest.mark.asyncio(loop_scope="module")
async def test_use_module_scope_loop_1(async_connection):
    """Use module loop"""
    print(f"Test using loop with id: {id(asyncio.get_event_loop())}")
    result = await async_connection.do_something()
    assert result == "Success"


@pytest.mark.asyncio(loop_scope="module")
async def test_use_module_scope_loop_2(async_connection):
    """Use module loop again"""
    print(f"Test using loop with id: {id(asyncio.get_event_loop())}")
    result = await async_connection.do_something()
    assert result == "Success"


@pytest.mark.asyncio(loop_scope="function")
async def test_use_function_scope_loop_1(async_connection):
    """Use function loop"""
    print(f"Test using loop with id: {id(asyncio.get_event_loop())}")
    with pytest.raises(RuntimeError, match="This connection is being used with a different event loop!"):
        # This should raise an error because the connection is being used with a different loop
        await async_connection.do_something()


@pytest.mark.asyncio(loop_scope="module")
async def test_use_module_scope_loop_3(async_connection):
    """Unexpectedly fail to use module scope again"""
    print(f"Test using loop with id: {id(asyncio.get_event_loop())}")
    result = await async_connection.do_something()
    assert result == "Success"

I would expect all tests to pass, however, the final test test_use_module_scope_loop_3 fails only if the test_use_function_scope_loop_1 is present. If the function scope one is commented out, the final test does pass (as expected).

The fixtures aren't obviously set up incorrectly (running with --setup-show):

SETUP    S event_loop_policy
    SETUP    M tests/test_a.py::<event_loop> (fixtures used: event_loop_policy)
    SETUP    M async_connection
        tests/test_a.py::test_use_module_scope_loop_1 (fixtures used: async_connection, event_loop_policy, request, tests/test_a.py::<event_loop>).
        tests/test_a.py::test_use_module_scope_loop_2 (fixtures used: async_connection, event_loop_policy, request, tests/test_a.py::<event_loop>).
        SETUP    F event_loop
        tests/test_a.py::test_use_function_scope_loop_1 (fixtures used: async_connection, event_loop, event_loop_policy, request).
        TEARDOWN F event_loop
        tests/test_a.py::test_use_module_scope_loop_3 (fixtures used: async_connection, event_loop_policy, request, tests/test_a.py::<event_loop>)F
    TEARDOWN M async_connection
    TEARDOWN M tests/test_a.py::<event_loop>
TEARDOWN S event_loop_policy

But for some reason I don't understand, the module scoped event loop changes for the last test.

The printed loop ids tell the same story... The fixture and first two tests all get the the same loop_id as expected. The function scope test gets a new one as expected. Then the final module scope test also gets a new loop_id (different from both previous loop_ids) unexpectedly.

Versions:
python: 3.12.7
pytest: 8.3.3
pytest-asyncio: 0.24.0

Also, my pytest settings are:

[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope="function"

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions