Как использовать LifespanManager для тестирования обратного прокси-сервера в FastAPI (асинхронное тестирование)

Согласно документации FastAPI мне может понадобиться использовать LifespanManager. Может ли кто-нибудь показать мне пример использования LifespanManager в асинхронном тесте? Вот с таким сроком службы:

    @asynccontextmanager
    async def lifespan(_app: FastAPI):
        async with httpx.AsyncClient(base_url=env.proxy_url, transport=httpx.MockTransport(dummy_response)) as client:
            yield {'client': client}
            await client.aclose()

Я пытаюсь протестировать конечную точку под названием proxy, которая работает нормально, но мне нужны тесты для регрессии:

import pytest
import pytest_asyncio
from fastapi import FastAPI
from contextlib import asynccontextmanager
from asgi_lifespan import LifespanManager
import httpx
from httpx import Response
import importlib
import uvicorn

import proxy
import env

def dummy_response(_request):
    res = Response(200, content = "Mock response")
    res.headers['Content-Type'] = 'text/plain; charset=utf-8'
    return res

@pytest_asyncio.fixture
async def mock_proxy():
    importlib.reload(env)
    importlib.reload(proxy)

    @asynccontextmanager
    async def lifespan(_app: FastAPI):
        async with httpx.AsyncClient(base_url=env.proxy_url, transport=httpx.MockTransport(dummy_response)) as client:
            yield {'client': client}
            await client.aclose()

    app = FastAPI(lifespan=lifespan)
    app.add_route("/proxy/path", proxy.proxy)

    async with LifespanManager(app) as manager:
        yield app

@pytest_asyncio.fixture
async def _client(mock_proxy):
    async with mock_proxy as app:
        async with httpx.AsyncClient(app=app, base_url=env.proxy_url) as client:
            yield client

@pytest.mark.anyio
async def test_proxy_get_request(_client):
    async with _client as client:
        response = await client.get(f"{env.proxy_url}/proxy/path", params = {"query": "param"})
        assert response.status_code == 200

Эта попытка говорит мне

Ошибка типа: объект FastAPI не поддерживает протокол асинхронного диспетчера контекста.

редактировать:

этот код кажется довольно близким, но изменение состояния продолжительности жизни не происходит:

import pytest
import pytest_asyncio
from fastapi import FastAPI
from contextlib import asynccontextmanager
from asgi_lifespan import LifespanManager
import httpx
from httpx import Response
import importlib
import uvicorn

import proxy
import env

def dummy_response(_request):
    res = Response(200, content = "Mock response")
    res.headers['Content-Type'] = 'text/plain; charset=utf-8'
    return res

@pytest_asyncio.fixture
async def _client():
    with pytest.MonkeyPatch.context() as monkeypatch:
        monkeypatch.setenv("PROXY_URL", "http://proxy")

        importlib.reload(env)
        importlib.reload(proxy)

    @asynccontextmanager
    async def lifespan(_app: FastAPI):
        async with httpx.AsyncClient(
             base_url=env.proxy_url, 
             transport=httpx.MockTransport(dummy_response)) as client:
            yield {'client': client} # startup
            await client.aclose() # shutdown

    app = FastAPI(lifespan=lifespan)
    app.add_route("/proxy/path", proxy.proxy)

    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url=env.proxy_url) \
          as client, LifespanManager(app):
        yield client

@pytest.mark.asyncio
async def test_proxy_get_request(_client):
        response = await _client.get(f"/proxy/path", params = {"query": "param"})
        assert response.status_code == 200

=============================================== == краткая информация о тесте ========================================== ======== НЕУДАЧНЫЕ тесты/регрессия/test_proxy.py::test_proxy_get_request - AttributeError: объект «Состояние» не имеет атрибута «клиент»

... на самом деле кажется, что LifespanManager не выполняет много работы. Если я изменю последнюю часть прибора на:

    async with LifespanManager(app) as manager:
        print(manager._state, app.state._state)
        app.state = State(state=manager._state)
        transport = httpx.ASGITransport(app=app)
        print(manager._state, app.state.client)
        async with httpx.AsyncClient(transport=transport, base_url=env.proxy_url) \
              as client:
            yield client

Я получил:

-------------------------------------------------- --- Записанная настройка стандартного вывода -------------------------------------------- --------- {'клиент': <объект httpx.AsyncClient по адресу 0x1034da240>} {} {'client': <объект httpx.AsyncClient по адресу 0x1034da240>} <объект httpx.AsyncClient по адресу 0x1034da240> =============================================== == краткая информация о тесте ========================================== ======== НЕУДАЧНЫЕ тесты/регрессия/test_proxy.py::test_proxy_get_request - AttributeError: объект «Состояние» не имеет атрибута «клиент»

Итак, приложение не получает состояние при запуске (но менеджер есть, просто по какой-то причине он не применяется к приложению). Аналогично, даже если я сам устанавливаю состояние вручную (так зачем вообще использовать LifespanManager в этот момент), состояние недоступно в запросе прокси-функции, как оно должно быть.

Причина, по которой я это делаю, заключается в том, что первая строка в прокси-сервере:

async def proxy(request: Request):
    client = request.state.client

И это то, что терпит неудачу.

редактировать 2:

благодаря комментариям Юрия я решил эту первоначальную проблему, но не то, что вообще привело меня на этот путь. Я могу решить эту проблему с помощью:

    async with LifespanManager(app) as manager:
        async with httpx.AsyncClient(transport=httpx.ASGITransport(app=manager.app), base_url=env.proxy_url) as client:
                yield client

Однако все началось, когда мой первоначальный подход к FastAPI TestClient потерпел неудачу из-за странной отмены, вызвавшей необработанные проблемы в группе задач, инициировавшую исчерпание потока, а затем попытку чтения (что, если была проблема с потоковой передачей, прокси-сервер не будет работать, и это работает). Оказывается, FastAPI не позволяет использовать TestClient для асинхронных тестов и рекомендует этот подход (подробнее см. По ссылке в начале статьи). У меня сейчас такая же проблема:

НЕУДАЧНЫЕ тесты/регрессия/test_proxy.py::test_proxy_get_request — ExceptionGroup: необработанные ошибки в TaskGroup (1 подисключение)

что вызвано той же проблемой отмены:

self = <asyncio.locks.Event object at 0x105cdade0 [unset]>

async def wait(self):
    """Block until the internal flag is true.

    If the internal flag is true on entry, return True
    immediately.  Otherwise, block until another coroutine calls
    set() to set the flag to true, then return True.
    """
    if self._value:
        return True

    fut = self._get_loop().create_future()
    self._waiters.append(fut)
    try:
      await fut

E asyncio.Exceptions.CancelledError: отменено областью отмены 105cdb8f0

Я думаю, вам просто нужно удалить строку async with mock_proxy as app: в первом примере кода (в приспособлении _client) и вместо этого сделать просто app = mock_proxy

Yurii Motov 26.04.2024 17:37

@YuriiMotov это и ... _client as client, я думаю. Затем я получаю ту же ошибку, что и во втором примере кода. это происходит при доступе к request.state.client в первой строке прокси

roberto tomás 26.04.2024 17:49

Вы также должны давать manager.app форму mock_proxy, а не только app. Согласно документации asgi-lifespan это должно работать так

Yurii Motov 26.04.2024 18:02

Manager.app не является объектом fastAPI, это state_middleware.<locals>.app_with_state()

roberto tomás 26.04.2024 18:06

Но согласно документации вы должны передать его в AsyncClient: async with httpx.AsyncClient(app=manager.app) as client:

Yurii Motov 26.04.2024 18:07

просто чтобы внести ясность: какую документацию вы читаете? Приложение устарело для асинхронного клиента, вместо этого вам придется использовать транспорт. и Manager.app — это не приложение fastAPI, это промежуточное программное обеспечение состояния.

roberto tomás 26.04.2024 18:09
pypi.org/project/asgi-lifespan
Yurii Motov 26.04.2024 18:10

Я думаю, поскольку ваша первоначальная проблема решена, лучше задавать новые вопросы, а не вносить здесь правки. Читать становится слишком запутанно и трудно.

Yurii Motov 26.04.2024 18:31
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
8
99
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий
  1. Вы должны уступать manager.app от mock_proxy, а не только app

  2. В _client первом контекстном менеджере async with mock_proxy as app: установите mock_proxy и вместо этого просто назначьте appapp = mock_proxy (test_proxy_get_request)

  3. Вам также не нужен контекстный менеджер в async with _client as client: (удалите manager.app и используйте _client)

Другие вопросы по теме

Похожие вопросы