AttributeError in pytest with asyncio after include code in fixtures

Question:

I need to test my telegram bot. To do this I need to create client user to ask my bot. I found telethon library which can do it. First I wrote a code example to ensure that authorisation and connection works and send test message to myself (imports omitted):

api_id = int(os.getenv("TELEGRAM_APP_ID"))
api_hash = os.getenv("TELEGRAM_APP_HASH")
session_str = os.getenv("TELETHON_SESSION")

async def main():
    client = TelegramClient(
        StringSession(session_str), api_id, api_hash,
        sequential_updates=True
    )
    await client.connect()
    async with client.conversation("@someuser") as conv:
        await conv.send_message('Hey, what is your name?')


if __name__ == "__main__":
    asyncio.run(main())

@someuser (me) successfully receives message. Okay, now I create a test with fixtures based on code above:

api_id = int(os.getenv("TELEGRAM_APP_ID"))
api_hash = os.getenv("TELEGRAM_APP_HASH")
session_str = os.getenv("TELETHON_SESSION")

@pytest.fixture(scope="session")
async def client():
    client = TelegramClient(
        StringSession(session_str), api_id, api_hash,
        sequential_updates=True
    )
    await client.connect()
    yield client
    await client.disconnect()


@pytest.mark.asyncio
async def test_start(client: TelegramClient):
    async with client.conversation("@someuser") as conv:
        await conv.send_message("Hey, what is your name?")

After running pytest received an error:

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

It seems client object returned from client fixture in "wrong" condition. Here is print(dir(client)):

['__aiter__', '__anext__', '__class__', '__class_getitem__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'aclose', 'ag_await', 'ag_code', 'ag_frame', 'ag_running', 'asend', 'athrow']

Where I loose "right" client object from generator in fixture?

Asked By: Andrey

||

Answers:

I would rather use anyio instead of pytest-asyncio

  • pip install anyio

Try this:

import pytest

@pytest.fixture(scope="session")
def anyio_backend():
    return "asyncio"

@pytest.fixture(scope="session")
async def conv():
    client = TelegramClient(
        StringSession(session_str), api_id, api_hash,
        sequential_updates=True
    )
    await client.connect()
    async with client.conversation("@someuser") as conv:
        yield conv

@pytest.mark.anyio
async def test_start(conv):
    await conv.send_message("Hey, what is your name?")
Answered By: Waket Zheng

Use @pytest_asyncio.fixture decorator in async fixtures according to documentation https://pypi.org/project/pytest-asyncio/#async-fixtures.

Like this:

import pytest_asyncio

@pytest_asyncio.fixture(scope="session")
async def client():
    ...
Answered By: Filip Hanes

You have two options:

  1. Set asyncio_mode to auto—see readthedocs and concepts—in which case you can omit @pytest.mark.asyncio and use @pytest.fixture for fixtures, OR
  2. Use @pytest_asyncio.fixture per Filip Hanes’ answer

Option 1 is the easy way, but option 2 is available for those who want to run some async tests using custom async libraries (such as trio).

Answered By: Joel Sullivan