Coverage for tests / test_router_events.py: 100%
273 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-21 17:29 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-21 17:29 +0000
1from collections.abc import AsyncGenerator 1jkml
2from contextlib import asynccontextmanager 1jkml
4import pytest 1jkml
5from fastapi import APIRouter, FastAPI, Request 1jkml
6from fastapi.testclient import TestClient 1jkml
7from pydantic import BaseModel 1jkml
10class State(BaseModel): 1jkml
11 app_startup: bool = False 1jkml
12 app_shutdown: bool = False 1jkml
13 router_startup: bool = False 1jkml
14 router_shutdown: bool = False 1jkml
15 sub_router_startup: bool = False 1jkml
16 sub_router_shutdown: bool = False 1jkml
19@pytest.fixture 1jkml
20def state() -> State: 1jkml
21 return State() 1jkl
24@pytest.mark.filterwarnings( 1jkml
25 r"ignore:\s*on_event is deprecated, use lifespan event handlers instead.*:DeprecationWarning"
26)
27def test_router_events(state: State) -> None: 1jkml
28 app = FastAPI() 1abc
30 @app.get("/") 1abc
31 def main() -> dict[str, str]: 1abc
32 return {"message": "Hello World"} 1abc
34 @app.on_event("startup") 1abc
35 def app_startup() -> None: 1abc
36 state.app_startup = True 1abc
38 @app.on_event("shutdown") 1abc
39 def app_shutdown() -> None: 1abc
40 state.app_shutdown = True 1abc
42 router = APIRouter() 1abc
44 @router.on_event("startup") 1abc
45 def router_startup() -> None: 1abc
46 state.router_startup = True 1abc
48 @router.on_event("shutdown") 1abc
49 def router_shutdown() -> None: 1abc
50 state.router_shutdown = True 1abc
52 sub_router = APIRouter() 1abc
54 @sub_router.on_event("startup") 1abc
55 def sub_router_startup() -> None: 1abc
56 state.sub_router_startup = True 1abc
58 @sub_router.on_event("shutdown") 1abc
59 def sub_router_shutdown() -> None: 1abc
60 state.sub_router_shutdown = True 1abc
62 router.include_router(sub_router) 1abc
63 app.include_router(router) 1abc
65 assert state.app_startup is False 1abc
66 assert state.router_startup is False 1abc
67 assert state.sub_router_startup is False 1abc
68 assert state.app_shutdown is False 1abc
69 assert state.router_shutdown is False 1abc
70 assert state.sub_router_shutdown is False 1abc
71 with TestClient(app) as client: 1abc
72 assert state.app_startup is True 1abc
73 assert state.router_startup is True 1abc
74 assert state.sub_router_startup is True 1abc
75 assert state.app_shutdown is False 1abc
76 assert state.router_shutdown is False 1abc
77 assert state.sub_router_shutdown is False 1abc
78 response = client.get("/") 1abc
79 assert response.status_code == 200, response.text 1abc
80 assert response.json() == {"message": "Hello World"} 1abc
81 assert state.app_startup is True 1abc
82 assert state.router_startup is True 1abc
83 assert state.sub_router_startup is True 1abc
84 assert state.app_shutdown is True 1abc
85 assert state.router_shutdown is True 1abc
86 assert state.sub_router_shutdown is True 1abc
89def test_app_lifespan_state(state: State) -> None: 1jkml
90 @asynccontextmanager 1nop
91 async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 1nop
92 state.app_startup = True 1nop
93 yield 1nop
94 state.app_shutdown = True 1nop
96 app = FastAPI(lifespan=lifespan) 1nop
98 @app.get("/") 1nop
99 def main() -> dict[str, str]: 1nop
100 return {"message": "Hello World"} 1nop
102 assert state.app_startup is False 1nop
103 assert state.app_shutdown is False 1nop
104 with TestClient(app) as client: 1nop
105 assert state.app_startup is True 1nop
106 assert state.app_shutdown is False 1nop
107 response = client.get("/") 1nop
108 assert response.status_code == 200, response.text 1nop
109 assert response.json() == {"message": "Hello World"} 1nop
110 assert state.app_startup is True 1nop
111 assert state.app_shutdown is True 1nop
114def test_router_nested_lifespan_state(state: State) -> None: 1jkml
115 @asynccontextmanager 1def
116 async def lifespan(app: FastAPI) -> AsyncGenerator[dict[str, bool], None]: 1def
117 state.app_startup = True 1def
118 yield {"app": True} 1def
119 state.app_shutdown = True 1def
121 @asynccontextmanager 1def
122 async def router_lifespan(app: FastAPI) -> AsyncGenerator[dict[str, bool], None]: 1def
123 state.router_startup = True 1def
124 yield {"router": True} 1def
125 state.router_shutdown = True 1def
127 @asynccontextmanager 1def
128 async def subrouter_lifespan(app: FastAPI) -> AsyncGenerator[dict[str, bool], None]: 1def
129 state.sub_router_startup = True 1def
130 yield {"sub_router": True} 1def
131 state.sub_router_shutdown = True 1def
133 sub_router = APIRouter(lifespan=subrouter_lifespan) 1def
135 router = APIRouter(lifespan=router_lifespan) 1def
136 router.include_router(sub_router) 1def
138 app = FastAPI(lifespan=lifespan) 1def
139 app.include_router(router) 1def
141 @app.get("/") 1def
142 def main(request: Request) -> dict[str, str]: 1def
143 assert request.state.app 1def
144 assert request.state.router 1def
145 assert request.state.sub_router 1def
146 return {"message": "Hello World"} 1def
148 assert state.app_startup is False 1def
149 assert state.router_startup is False 1def
150 assert state.sub_router_startup is False 1def
151 assert state.app_shutdown is False 1def
152 assert state.router_shutdown is False 1def
153 assert state.sub_router_shutdown is False 1def
155 with TestClient(app) as client: 1def
156 assert state.app_startup is True 1def
157 assert state.router_startup is True 1def
158 assert state.sub_router_startup is True 1def
159 assert state.app_shutdown is False 1def
160 assert state.router_shutdown is False 1def
161 assert state.sub_router_shutdown is False 1def
162 response = client.get("/") 1def
163 assert response.status_code == 200, response.text 1def
164 assert response.json() == {"message": "Hello World"} 1def
166 assert state.app_startup is True 1def
167 assert state.router_startup is True 1def
168 assert state.sub_router_startup is True 1def
169 assert state.app_shutdown is True 1def
170 assert state.router_shutdown is True 1def
171 assert state.sub_router_shutdown is True 1def
174def test_router_nested_lifespan_state_overriding_by_parent() -> None: 1jkml
175 @asynccontextmanager 1CDE
176 async def lifespan( 1CDE
177 app: FastAPI,
178 ) -> AsyncGenerator[dict[str, str | bool], None]:
179 yield { 1CDE
180 "app_specific": True,
181 "overridden": "app",
182 }
184 @asynccontextmanager 1CDE
185 async def router_lifespan( 1CDE
186 app: FastAPI,
187 ) -> AsyncGenerator[dict[str, str | bool], None]:
188 yield { 1CDE
189 "router_specific": True,
190 "overridden": "router", # should override parent
191 }
193 router = APIRouter(lifespan=router_lifespan) 1CDE
194 app = FastAPI(lifespan=lifespan) 1CDE
195 app.include_router(router) 1CDE
197 with TestClient(app) as client: 1CDE
198 assert client.app_state == { 1CDE
199 "app_specific": True,
200 "router_specific": True,
201 "overridden": "app",
202 }
205def test_merged_no_return_lifespans_return_none() -> None: 1jkml
206 @asynccontextmanager 1FGH
207 async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 1FGH
208 yield 1FGH
210 @asynccontextmanager 1FGH
211 async def router_lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 1FGH
212 yield 1FGH
214 router = APIRouter(lifespan=router_lifespan) 1FGH
215 app = FastAPI(lifespan=lifespan) 1FGH
216 app.include_router(router) 1FGH
218 with TestClient(app) as client: 1FGH
219 assert not client.app_state 1FGH
222def test_merged_mixed_state_lifespans() -> None: 1jkml
223 @asynccontextmanager 1wxy
224 async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 1wxy
225 yield 1wxy
227 @asynccontextmanager 1wxy
228 async def router_lifespan(app: FastAPI) -> AsyncGenerator[dict[str, bool], None]: 1wxy
229 yield {"router": True} 1wxy
231 @asynccontextmanager 1wxy
232 async def sub_router_lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 1wxy
233 yield 1wxy
235 sub_router = APIRouter(lifespan=sub_router_lifespan) 1wxy
236 router = APIRouter(lifespan=router_lifespan) 1wxy
237 app = FastAPI(lifespan=lifespan) 1wxy
238 router.include_router(sub_router) 1wxy
239 app.include_router(router) 1wxy
241 with TestClient(app) as client: 1wxy
242 assert client.app_state == {"router": True} 1wxy
245@pytest.mark.filterwarnings( 1jkml
246 r"ignore:\s*on_event is deprecated, use lifespan event handlers instead.*:DeprecationWarning"
247)
248def test_router_async_shutdown_handler(state: State) -> None: 1jkml
249 """Test that async on_shutdown event handlers are called correctly, for coverage."""
250 app = FastAPI() 1zAB
252 @app.get("/") 1zAB
253 def main() -> dict[str, str]: 1zAB
254 return {"message": "Hello World"} 1zAB
256 @app.on_event("shutdown") 1zAB
257 async def app_shutdown() -> None: 1zAB
258 state.app_shutdown = True 1zAB
260 assert state.app_shutdown is False 1zAB
261 with TestClient(app) as client: 1zAB
262 assert state.app_shutdown is False 1zAB
263 response = client.get("/") 1zAB
264 assert response.status_code == 200, response.text 1zAB
265 assert state.app_shutdown is True 1zAB
268def test_router_sync_generator_lifespan(state: State) -> None: 1jkml
269 """Test that a sync generator lifespan works via _wrap_gen_lifespan_context."""
270 from collections.abc import Generator 1qrs
272 def lifespan(app: FastAPI) -> Generator[None, None, None]: 1qrs
273 state.app_startup = True 1qrs
274 yield 1qrs
275 state.app_shutdown = True 1qrs
277 app = FastAPI(lifespan=lifespan) # type: ignore[arg-type] 1qrs
279 @app.get("/") 1qrs
280 def main() -> dict[str, str]: 1qrs
281 return {"message": "Hello World"} 1qrs
283 assert state.app_startup is False 1qrs
284 assert state.app_shutdown is False 1qrs
285 with TestClient(app) as client: 1qrs
286 assert state.app_startup is True 1qrs
287 assert state.app_shutdown is False 1qrs
288 response = client.get("/") 1qrs
289 assert response.status_code == 200, response.text 1qrs
290 assert response.json() == {"message": "Hello World"} 1qrs
291 assert state.app_startup is True 1qrs
292 assert state.app_shutdown is True 1qrs
295def test_router_async_generator_lifespan(state: State) -> None: 1jkml
296 """Test that an async generator lifespan (not wrapped) works."""
298 async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 1tuv
299 state.app_startup = True 1tuv
300 yield 1tuv
301 state.app_shutdown = True 1tuv
303 app = FastAPI(lifespan=lifespan) # type: ignore[arg-type] 1tuv
305 @app.get("/") 1tuv
306 def main() -> dict[str, str]: 1tuv
307 return {"message": "Hello World"} 1tuv
309 assert state.app_startup is False 1tuv
310 assert state.app_shutdown is False 1tuv
311 with TestClient(app) as client: 1tuv
312 assert state.app_startup is True 1tuv
313 assert state.app_shutdown is False 1tuv
314 response = client.get("/") 1tuv
315 assert response.status_code == 200, response.text 1tuv
316 assert response.json() == {"message": "Hello World"} 1tuv
317 assert state.app_startup is True 1tuv
318 assert state.app_shutdown is True 1tuv
321def test_startup_shutdown_handlers_as_parameters(state: State) -> None: 1jkml
322 """Test that startup/shutdown handlers passed as parameters to FastAPI are called correctly."""
324 def app_startup() -> None: 1ghi
325 state.app_startup = True 1ghi
327 def app_shutdown() -> None: 1ghi
328 state.app_shutdown = True 1ghi
330 app = FastAPI(on_startup=[app_startup], on_shutdown=[app_shutdown]) 1ghi
332 @app.get("/") 1ghi
333 def main() -> dict[str, str]: 1ghi
334 return {"message": "Hello World"} 1ghi
336 def router_startup() -> None: 1ghi
337 state.router_startup = True 1ghi
339 def router_shutdown() -> None: 1ghi
340 state.router_shutdown = True 1ghi
342 router = APIRouter(on_startup=[router_startup], on_shutdown=[router_shutdown]) 1ghi
344 def sub_router_startup() -> None: 1ghi
345 state.sub_router_startup = True 1ghi
347 def sub_router_shutdown() -> None: 1ghi
348 state.sub_router_shutdown = True 1ghi
350 sub_router = APIRouter( 1ghi
351 on_startup=[sub_router_startup], on_shutdown=[sub_router_shutdown]
352 )
354 router.include_router(sub_router) 1ghi
355 app.include_router(router) 1ghi
357 assert state.app_startup is False 1ghi
358 assert state.router_startup is False 1ghi
359 assert state.sub_router_startup is False 1ghi
360 assert state.app_shutdown is False 1ghi
361 assert state.router_shutdown is False 1ghi
362 assert state.sub_router_shutdown is False 1ghi
363 with TestClient(app) as client: 1ghi
364 assert state.app_startup is True 1ghi
365 assert state.router_startup is True 1ghi
366 assert state.sub_router_startup is True 1ghi
367 assert state.app_shutdown is False 1ghi
368 assert state.router_shutdown is False 1ghi
369 assert state.sub_router_shutdown is False 1ghi
370 response = client.get("/") 1ghi
371 assert response.status_code == 200, response.text 1ghi
372 assert response.json() == {"message": "Hello World"} 1ghi
373 assert state.app_startup is True 1ghi
374 assert state.router_startup is True 1ghi
375 assert state.sub_router_startup is True 1ghi
376 assert state.app_shutdown is True 1ghi
377 assert state.router_shutdown is True 1ghi
378 assert state.sub_router_shutdown is True 1ghi