Не работает session. Потеря контекста

Приведу несколько примеров. В одних случаях, я получаю ошибку вида:

    def __enter__(self) -> BaseTimerContext:
        task = current_task(loop=self._loop)

        if task is None:
>           raise RuntimeError('Timeout context manager should be used '
                               'inside a task')
E           RuntimeError: Timeout context manager should be used inside a task

В других случаях, всё работает корректно. Все примеры выполняются в связке с pytest (для работы требуется pytest-asyncio==0.12.0). Для того, чтобы было проще понять проблему, я формализую её как задачу с параметрами. В примерах есть несколько "параметров":

  • aiohttp.ClientSession(). Создание сессии -- первый "параметр" (A). Будем объявлять его в различных местах: в конструкторе, в теле метода
  • client_storage = AsyncClient(). Создание экземпляра класса -- второй "параметр" (B). Будем объявлять его в различных местах: вне теста, в файле с тестом (ВНУТРИ функции), в файле с тестом (ВНЕ функции).

Таким образом, у нас есть 6 различных конфигураций.

  1. A = в конструкторе; B = вне теста -> Ошибка
  2. A = в конструкторе; B = ВНУТРИ функции -> Корректная работа
  3. A = в конструкторе; B = ВНЕ функции -> Ошибка

  4. A = в теле метода; B = ВНЕ функции -> Корректная работа

  5. A = в теле метода; B = ВНУТРИ функции -> Корректная работа
  6. A = в теле метода; B = ВНЕ функции -> Корректная работа

REVIEW: Таким образом, почему-то теряется сессия, если её объявлять не там, где используешь. В чём причина. Как исправить эту проблему?

Теперь приведу рабочие примеры, чтобы стало понятнее, в чём ошибка. Буду приводить примеры также, как они расположены в списке:

1. A = в конструкторе; B = вне теста -> Ошибка

Импорт client_storage из другого module. ClientSession объявляется в конструкторе:

tests/__init__.py:

import aiohttp


class AsyncClient:
    def __init__(self):
        self.__session = aiohttp.ClientSession()  # Создание объекта сессии

    async def request(self):
        resp = await self.__session.request(method='get', url='https://yandex.ru')
        return resp


client_storage = AsyncClient()  # Создание экземпляра класса клиента

tests/test_user.py:

import pytest
import tests


@pytest.mark.asyncio
async def test_real_request():
    await tests.client_storage.request()
    print('x')

2. A = в конструкторе; B = ВНУТРИ функции -> Корректная работа

Импорт client_storage из другого module. ClientSession объявляется в конструкторе:

tests/__init__.py:

import aiohttp


class AsyncClient:
    def __init__(self):
        self.__session = aiohttp.ClientSession()  # Создание экземпляра класса

    async def request(self):
        resp = await self.__session.request(method='get', url='https://yandex.ru')
        return resp

tests/test_user.py:

import pytest
import tests


@pytest.mark.asyncio
async def test_real_request():
    client_storage = AsyncClient()  # Создание экземпляра класса клиента
    await client_storage.request()
    print('x')

3. A = в конструкторе; B = ВНЕ функции -> Ошибка

ClientSession объявляется в конструкторе. client_storage объявляется в том же module, в тесте:

tests/__init__.py:

import aiohttp


class AsyncClient:
    def __init__(self):
        self.__session = aiohttp.ClientSession()  # Создание экземпляра класса

    async def request(self):
        resp = await self.__session.request(method='get', url='https://yandex.ru')
        return resp

tests/test_user.py:

import pytest
import tests

client_storage = AsyncClient()  # Создание экземпляра класса клиента

@pytest.mark.asyncio
async def test_real_request():
    client_storage = tests.AsyncClient()
    await client_storage.request()

4. A = в методе; B = вне теста -> Корректная работа

Импорт client_storage из другого module. ClientSession объявляется в конструкторе:

tests/__init__.py:

import aiohttp


class AsyncClient:
    def __init__(self):
        self.__session = None

    async def request(self):
        self.__session = aiohttp.ClientSession()  # Создание объекта сессии
        resp = await self.__session.request(method='get', url='https://yandex.ru')
        return resp


client_storage = AsyncClient()  # Создание экземпляра класса клиента

tests/test_user.py:

import pytest
import tests


@pytest.mark.asyncio
async def test_real_request():
    await tests.client_storage.request()
    print('x')

5. A = в методе; B = ВНУТРИ функции -> Корректная работа

Импорт client_storage из другого module. ClientSession объявляется в конструкторе:

tests/__init__.py:

import aiohttp


class AsyncClient:
    def __init__(self):
        self.__session = None

    async def request(self):
        self.__session = aiohttp.ClientSession()  # Создание объекта сессии
        resp = await self.__session.request(method='get', url='https://yandex.ru')
        return resp

tests/test_user.py:

import pytest
import tests


@pytest.mark.asyncio
async def test_real_request():
    client_storage = AsyncClient()  # Создание экземпляра класса клиента
    await client_storage.request()
    print('x')

6. A = в методе; B = ВНЕ функции -> Корректная работа

ClientSession объявляется в конструкторе. client_storage объявляется в том же module, в тесте:

tests/__init__.py:

import aiohttp


class AsyncClient:
    def __init__(self):
        self.__session = None

    async def request(self):
        self.__session = aiohttp.ClientSession()  # Создание объекта сессии
        resp = await self.__session.request(method='get', url='https://yandex.ru')
        return resp

tests/test_user.py:

import pytest
import tests

client_storage = AsyncClient()  # Создание экземпляра класса клиента

@pytest.mark.asyncio
async def test_real_request():
    client_storage = tests.AsyncClient()
    await client_storage.request()

Таким образом, вопрос сводится к следующему: почему при определении ClientSession в асинхронной функции или вне её, не удаётся сделать запрос?


Если копнуть в глубь, оказывается, что под капотом в aiohttp создаётся loop. Если его создавать в асинхронном методе, то переменная _thread_id is not None. Если его создавать в других местах (там, где возникают ошибки), то она равна None. Проблема, видимо, кроется где-то в этом направлении.

Я попытался разобраться, в чём тут проблема: залез в asyncio.get_event_loop() и далее в asyncio.get_event_loop_policy(), которые вызывается внутри aiohttp и asyncio соответсвенно. Но здесь меня постигла неудача, поскольку get_event_loop_policy возвращает объект типа _UnixDefaultEventLoopPolicy, а снаружи оказывается уже объект другого типа с соверенно другим набором полей: _UnixEventLoopPolicy. Как такое происходит, я не понял.


Частичное решение подсказали в этом вопросе.

class AsyncClient:
    def __init__(self):
        self.__session = None

    async def request(self):
        if self.__session is None:
            self.__session = aiohttp.ClientSession()
        resp = await self.__session.request(method='get', url='https://yandex.ru')
        return resp

Ответы (0 шт):