pytest-asyncio icon indicating copy to clipboard operation
pytest-asyncio copied to clipboard

0.19.0: Fixture misses attribute

Open viralmutant opened this issue 2 years ago • 5 comments

I am working with python3.10 and pytest=7.1.2 version

It seems with the latest version of pytest-asyncio=0.19.0 a regression has been introduced and this bug is back.

Using the same example testcase to reproduce the issue, I observe the same failure

AttributeError: 'async_generator' object has no attribute 'put'

Downgrading pytest-asyncio to version 0.18.3 and the error is gone.

viralmutant avatar Jul 22 '22 03:07 viralmutant

@viralmutant

pytest-asyncio=0.19.0 enforces strict mode. Projects must update their fixture markers as described.

Env: py38 + purest=7.1.2 +pytest-asyncio=0.19.0

davidandreoletti avatar Jul 22 '22 16:07 davidandreoletti

@viralmutant please show the output of the code when you run on python3.10 and pytest-asyncio==0.18.3

graingert avatar Jul 22 '22 17:07 graingert

@viralmutant please show the output of the code when you run on python3.10 and pytest-asyncio==0.18.3

Here is the output with -s

plugins: forked-1.4.0, cov-3.0.0, asyncio-0.18.3, env-0.6.2, sugar-0.9.5, requests-mock-1.9.3, pyfakefs-4.5.0, xdist-2.5.0
asyncio: mode=legacy
collecting ...
 test_bug.py ✓

/CodeBox/turbo-test/lib/python3.10/site-packages/pytest_asyncio/plugin.py:191: DeprecationWarning: The 'asyncio_mode' default value will change to 'strict' in future, please explicitly use 'asyncio_mode=strict' or 'asyncio_mode=auto' in pytest configuration file.
    config.issue_config_time_warning(LEGACY_MODE, stacklevel=2)

test_bug.py:12
  
/CodeBox/turbo/test_bug.py:12: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
    def release(self):

../turbo-test/lib/python3.10/site-packages/pytest_asyncio/plugin.py:230
  .../CodeBox/turbo-test/lib/python3.10/site-packages/pytest_asyncio/plugin.py:230: DeprecationWarning: '@pytest.fixture' is applied to <fixture fake_session, file=/CodeBox/turbo/test_bug.py, line=16> in 'legacy' mode, please replace it with '@pytest_asyncio.fixture' as a preparation for switching to 'strict' mode (or use 'auto' mode to seamlessly handle all these fixtures as asyncio-driven).
    warnings.warn(

test_bug.py::test_bug
  /CodeBox/turbo/test_bug.py:20: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
    def _fake_request(method, url, *args, **kwargs):

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html

Results (0.84s):
       1 passed

viralmutant avatar Jul 22 '22 21:07 viralmutant

Your test output shows a deprecation warning about asyncio-mode. Starting with pytest-asyncio v0.19 asyncio mode defaults to strict. The error you're seeing is likely caused by wrong fixture decorators. The reproducible example you linked suffers from that exact problem.

As @davidandreoletti already mentioned you need to update your fixture definitions if you intend to use strict mode. Strict mode will no longer evaluate async fixtures decorated witih @pytest.fixture. You need to use @pytest_asyncio.fixture.

@viralmutant Please check your fixture definitions and let us know if this fixes your issue.

seifertm avatar Jul 23 '22 06:07 seifertm

Yes, after replacing the fixture as @pytest_asyncio.fixture I didn't observe the issue with v0.19 Thanks

The confusing part is that once the version has been upgraded, it just fails and won't tell why. And while being at the earlier version, when a couple of thousand tests are running in CI pipeline, the warnings are ignored

viralmutant avatar Jul 26 '22 04:07 viralmutant

Hi @seifertm

I am trying to use async_generator with fixture in factory mode:

@pytest_asyncio.fixture
async def async_client_factory():
    async def _factory(x):
        print(x) # just to simplify case, we need x as parameter for our factory
        async with AsyncClient() as ac:
            yield ac

    return _factory

@pytest.mark.asyncio
async def test_async_generator_factory(async_client_factory):
    async_client = async_client_factory(1)
    response = await async_client.get("/test")
    assert response.status_code == status.HTTP_200_OK

But I get same error:

>       response = await async_client.get("/test")
E       AttributeError: 'async_generator' object has no attribute 'get'

package versions:

pytest-asyncio = "0.19.0"
httpx = "^0.23.0"
pytest = "^7.1.2"

kiddten avatar Aug 17 '22 17:08 kiddten

just a fixture w/o factory works well

@pytest_asyncio.fixture
async def async_client():
    async with AsyncClient() as ac:
        yield ac

@pytest.mark.asyncio
async def test_async_generator(async_client):
    response = await async_client.get("/test")
    assert response.status_code == status.HTTP_200_OK

kiddten avatar Aug 17 '22 17:08 kiddten

Interesting, thanks for the report. I'll look into it tomorrow.

seifertm avatar Aug 17 '22 18:08 seifertm

You can't use a yield inside an httpx.AsyncClient like that, you need the @contextlib.asynccontextmanager

See https://discuss.python.org/t/preventing-yield-inside-certain-context-managers/1091

graingert avatar Aug 17 '22 18:08 graingert

@graingert it does not help in case with factory

@pytest_asyncio.fixture
async def async_client_factory():
    @asynccontextmanager
    async def _factory(x):
        print(x) # just to simplify case, we need x as parameter for our factory
        async with AsyncClient() as ac:
            yield ac

    return _factory

@pytest.mark.asyncio
async def test_async_generator_factory(async_client_factory):
    async_client = async_client_factory(1)
    response = await async_client.get("/test")
    assert response.status_code == status.HTTP_200_OK

similar error

>       response = await async_client.get("/test")
E       AttributeError: '_AsyncGeneratorContextManager' object has no attribute 'get'

although it works with fixture w/o factory

@asynccontextmanager
@pytest_asyncio.fixture
async def async_client():
    async with AsyncClient() as ac:
        yield ac

@pytest.mark.asyncio
async def test_async_generator(async_client):
    response = await async_client.get("/test")
    assert response.status_code == status.HTTP_200_OK

seems something related to pytest internals

kiddten avatar Aug 18 '22 09:08 kiddten

You need to use

async with async_client_factory(1) as async_client:
    ...

graingert avatar Aug 18 '22 10:08 graingert

@kiddick I cannot see anything wrong with the behaviour of pytest-asyncio in the example you provided here. Your fixture returns the _factory async context manager. As Thomas mentioned you need to open the context manager using asnyc with …, in order to access the yielded AsyncClient.

From your reaction on the previous comment I assume the issue is resolved for you.

I initially left this issue open, because I intended to add a warning when people forgot to update their fixtures. I don't think there's a reliable way to do it, though, so I'll close this issue.

seifertm avatar Aug 25 '22 17:08 seifertm