Skip to content
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

How to fix "attached to a different loop"? #38

Closed
Archelyst opened this issue Dec 2, 2016 · 16 comments
Closed

How to fix "attached to a different loop"? #38

Archelyst opened this issue Dec 2, 2016 · 16 comments

Comments

@Archelyst
Copy link

I have a very simple app called "myapp". It uses the AsyncElasticsearch client:

from elasticsearch_async import AsyncElasticsearch

def create_app():
    app = dict()
    app['es_client'] = AsyncElasticsearch('http://index:9200/')
    app['stuff'] = Stuff(app['es_client'])
    return app

class Stuff:
    def __init__(self, es_client):
        self.es_client = es_client

    def do_async_stuff(self):
        return self.es_client.index(index='test',
                                    doc_type='test',
                                    body={'field': 'sample content'})

My question is not about AsyncElasticsearch, it just happens to be an async thing I want to work with, could be sth else like a Mongo driver or whatever.

I want to test do_async_stuff() and wrote the following conftest.py

import pytest
from myapp import create_app

@pytest.fixture(scope='session')
def app():
    return create_app()

... and test_stuff.py

import pytest

@pytest.mark.asyncio
async def test_stuff(app):
    await app['stuff'].do_async_stuff()
    assert True

When I execute the test I get an exception with the message "attached to a different loop". Digging into that matter I found that pytest-asyncio creates a new event_loop for each test case (right?). The Elasticsearch client however, takes the default loop on instantiation and sticks with it. So I tried to convince it to use the pytest-asyncio event_loop like so:

import pytest

@pytest.mark.asyncio
async def test_stuff(app, event_loop):
    app['es_client'].transport.loop = event_loop
    await app['stuff'].do_async_stuff()
    assert True

This however gives me another exception:

__________________________________ test_stuff __________________________________

app = {'es_client': <Elasticsearch([{'host': 'index', 'port': 9200, 'scheme': 'http'}])>, 'stuff': <myapp.Stuff object at 0x7ffbbaff1860>}

    @pytest.mark.asyncio
    async def test_stuff(app):
>       await app['stuff'].do_async_stuff()

test/test_stuff.py:6: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Task pending coro=<AsyncTransport.main_loop() running at /usr/local/lib/python3.5/dist-packages/elasticsearch_async/transport.py:133>>

    def __iter__(self):
        if not self.done():
            self._blocking = True
>           yield self  # This tells Task to wait for completion.
E           RuntimeError: Task <Task pending coro=<test_stuff() running at /srv/app/backend/test/test_stuff.py:6> cb=[_run_until_complete_cb() at /usr/lib/python3.5/asyncio/base_events.py:164]> got Future <Task pending coro=<AsyncTransport.main_loop() running at /usr/local/lib/python3.5/dist-packages/elasticsearch_async/transport.py:133>> attached to a different loop

How am I supposed to test this scenario?

@Tinche
Copy link
Member

Tinche commented Dec 2, 2016

Hi,

the first thing you can try is this:

@pytest.fixture
def app(event_loop):
    return create_app()

i.e. make your app fixture depend on the event loop fixture. This should make your client instance get the loop actually used in the test (even if you don't actually use the argument, the event loop will get installed as the default loop for the duration of the test). This will also make your client fixture function-scoped, but it's a good starting point.

@Archelyst
Copy link
Author

Hi Tinche, thanks for the incredibly fast response. You suggestions seems to work. However "This will also make your client fixture function-scoped" is something I'd rather like to avoid as it doesn't exactly increase the speed of the tests. What's the rationale for pytest-asyncio to create a new loop for each test? Not to share resources between tests? Maybe your suggestion is the only viable solution then.

Any idea, why my attempt to inject the loop into the client does not work? Cannot make any sense of the error message.

@Tinche
Copy link
Member

Tinche commented Dec 2, 2016

The AsyncElasticsearch instance probably grabs the loop and uses it somewhere before you can change it in the test.

Yeah, ideally tests should run in total isolation. The reason why your client fixture must be function scoped is that the event loop fixture is function scoped. You can override the event loop fixture to be session scoped though. I'm not sure this is tested but it should be possible :)

Make a conftest.py file in your tests directory, and put this in it:

@pytest.yield_fixture(scope='session')
def event_loop(request):
    """Create an instance of the default event loop for each test case."""
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()

@Archelyst
Copy link
Author

Ok, thanks for the valuable input.

smagafurov pushed a commit to smagafurov/pytest-asyncio that referenced this issue Apr 4, 2018
@juandiegopalomino
Copy link

Hi there,

So following from this issue, I'm trying to see if we could use this library to run async tests dependent on i/o in parallel. I've been trying the following example but couldn't get it to work asynchronously (total test takes 20 seconds instead of the desired 10)

import asyncio
import pytest

@pytest.yield_fixture(scope='session')
def event_loop(request):
    """Create an instance of the default event loop for each test case."""
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()


# All test coroutines will be treated as marked.
pytestmark = pytest.mark.asyncio

async def test_example1(event_loop):
    """No marker!"""
    await asyncio.sleep(10, loop=event_loop)


async def test_example2(event_loop):
    """No marker!"""
    await asyncio.sleep(10, loop=event_loop)

If anyone could give me input, I'd greatly appreciate it!

@david-shiko
Copy link

The AsyncElasticsearch instance probably grabs the loop and uses it somewhere before you can change it in the test.

Yeah, ideally tests should run in total isolation. The reason why your client fixture must be function scoped is that the event loop fixture is function scoped. You can override the event loop fixture to be session scoped though. I'm not sure this is tested but it should be possible :)

Make a conftest.py file in your tests directory, and put this in it:

@pytest.yield_fixture(scope='session')
def event_loop(request):
    """Create an instance of the default event loop for each test case."""
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()

Hello!
Can you please update an aswer to modern usage?
I came from this link

@Tinche
Copy link
Member

Tinche commented Apr 16, 2021

@david-shiko What exactly do you mean?

@david-shiko
Copy link

david-shiko commented Apr 16, 2021

  1. As I know, get.event_loop() and using loops directly is deprecated (pytest warning about it)
  2. I have a such code (I'm novice in tests) and can't figure out how to apply provided solution.
import pytest
from httpx import AsyncClient
import main
from tests import conftest


@pytest.mark.asyncio()
async def test_root():
    async with AsyncClient(app=main.app, base_url="http://test") as ac:
        response = conftest.event_loop(await ac.post("/register", json={
            "phone_number": "+7 931 964 0000",
            "full_name": "Чехов Антон Павлович"
        }))
    assert response.status_code == 201

I got an error:

RuntimeError("Task <Task pending name='Task-5' coro=<test_root() running at /home/david/PycharmProjects/work/tests/main_test.py:12> cb=[_run_until_complete_cb() at /usr/lib/python3.8/asyncio/base_events.py:184]> got Future <Future pending cb=[Protocol._on_waiter_completed()]> attached to a different loop")

@Tinche
Copy link
Member

Tinche commented Apr 16, 2021

Why do you need the conftest.event_loop?

What happens if you try this:

import pytest
from httpx import AsyncClient
import main
from tests import conftest


@pytest.mark.asyncio()
async def test_root():
    async with AsyncClient(app=main.app, base_url="http://test") as ac:
        response = await ac.post("/register", json={
            "phone_number": "+7 931 964 0000",
            "full_name": "Чехов Антон Павлович"
        })
    assert response.status_code == 201

@david-shiko
Copy link

david-shiko commented Apr 16, 2021

conftest.event_loop mentioned as a solution in docs

platform linux -- Python 3.8.5, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: /home/david/PycharmProjects/work
plugins: asyncio-0.14.0
collected 1 item                                                                                                                                                                                                                                                                                                 

tests/main_test.py 
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PDB set_trace (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /home/david/PycharmProjects/work/routers/auth.py(24)register()
-> try:
(Pdb) error
RuntimeError("Task <Task pending name='Task-5' coro=<test_root() running at /home/david/PycharmProjects/work/tests/main_test.py:10> cb=[_run_until_complete_cb() at /usr/lib/python3.8/asyncio/base_events.py:184]> got Future <Future pending cb=[Protocol._on_waiter_completed()]> attached to a different loop")
(Pdb) c

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PDB continue (IO-capturing resumed) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
F                                                                                                                                                                                                                                                                                       [100%]

==================================================================================================================================================== FAILURES ====================================================================================================================================================
___________________________________________________________________________________________________________________________________________________ test_root ____________________________________________________________________________________________________________________________________________________

    @pytest.mark.asyncio()
    async def test_root():
        async with AsyncClient(app=main.app, base_url="http://test") as ac:
>           response = await ac.post("/register", json={
                "phone_number": "+7 931 964 0000",
                "full_name": "Чехов Антон Павлович"
            })
E           assert 503 == 201

When error occurs server will always return 503 status code.

Line that causing an error:
await postgres_session.execute(statement=models.users.select().where(models.users.c.phone_number == user.phone_number))

@Tinche
Copy link
Member

Tinche commented Apr 16, 2021

Please open up a different issue for this, with a small repro case and I will look at it.

@david-shiko
Copy link

@Tinche #207 (comment). Thanks.

@sk-
Copy link

sk- commented Aug 27, 2022

@Tinche what are the implications of the suggested workaround?

Make a conftest.py file in your tests directory, and put this in it:

@pytest.yield_fixture(scope='session')
def event_loop(request):
    """Create an instance of the default event loop for each test case."""
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()

In our case we create an sqlalchemy async engine at the module level, and if we don't use this workaround tests will fail with different errors, including the dreaded attached to a different loop.

@dmelo
Copy link

dmelo commented Oct 27, 2023

Same case as @sk- described. But @pytest.yield_fixture is deprecated, so in my case, I'm using:

    @pytest_asyncio.fixture(scope='session', autouse=True)
    def event_loop(request):
        """Create an instance of the default event loop for each test case."""
        loop = asyncio.get_event_loop_policy().new_event_loop()
        yield loop
        loop.close()

Kudos to @Tinche . His post has aged well.

@n-elloco
Copy link

How to fix it with 0.23 version? In my case the latest test always failed because of "Event loop is closed"

@seifertm
Copy link
Contributor

@n-elloco Pytest-asyncio v0.23 supports running tests in different event loops. There's a loop for each level of the test suite (session, package, module, class, function). However, pytest-asyncio wrongly assumes that the scope of an async fixture is the same as the scope of the event loop in which the fixture runs. This essentially makes it impossible to have a session-scoped fixture that runs in the same loop as a function-scoped test. See #706.

If v0.23 causes trouble for you, I suggest sticking to pytest-asyncio v0.21, until the issue is resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants