Согласно документации 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
И это то, что терпит неудачу.
благодаря комментариям Юрия я решил эту первоначальную проблему, но не то, что вообще привело меня на этот путь. Я могу решить эту проблему с помощью:
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
@YuriiMotov это и ... _client as client
, я думаю. Затем я получаю ту же ошибку, что и во втором примере кода. это происходит при доступе к request.state.client
в первой строке прокси
Вы также должны давать manager.app
форму mock_proxy
, а не только app
. Согласно документации asgi-lifespan
это должно работать так
Manager.app не является объектом fastAPI, это state_middleware.<locals>.app_with_state()
Но согласно документации вы должны передать его в AsyncClient: async with httpx.AsyncClient(app=manager.app) as client:
просто чтобы внести ясность: какую документацию вы читаете? Приложение устарело для асинхронного клиента, вместо этого вам придется использовать транспорт. и Manager.app — это не приложение fastAPI, это промежуточное программное обеспечение состояния.
Я думаю, поскольку ваша первоначальная проблема решена, лучше задавать новые вопросы, а не вносить здесь правки. Читать становится слишком запутанно и трудно.
Вы должны уступать manager.app
от mock_proxy
, а не только app
В _client
первом контекстном менеджере async with mock_proxy as app:
установите mock_proxy
и вместо этого просто назначьте app
app = mock_proxy
(test_proxy_get_request
)
Вам также не нужен контекстный менеджер в async with _client as client:
(удалите manager.app
и используйте _client)
Я думаю, вам просто нужно удалить строку
async with mock_proxy as app:
в первом примере кода (в приспособлении_client
) и вместо этого сделать простоapp = mock_proxy