Coverage for tests / test_router_events.py: 100%

274 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-12 18:15 +0000

1from collections.abc import AsyncGenerator 1jkml

2from contextlib import asynccontextmanager 1jkml

3from typing import Union 1jkml

4 

5import pytest 1jkml

6from fastapi import APIRouter, FastAPI, Request 1jkml

7from fastapi.testclient import TestClient 1jkml

8from pydantic import BaseModel 1jkml

9 

10 

11class State(BaseModel): 1jkml

12 app_startup: bool = False 1jkml

13 app_shutdown: bool = False 1jkml

14 router_startup: bool = False 1jkml

15 router_shutdown: bool = False 1jkml

16 sub_router_startup: bool = False 1jkml

17 sub_router_shutdown: bool = False 1jkml

18 

19 

20@pytest.fixture 1jkml

21def state() -> State: 1jkml

22 return State() 1jkl

23 

24 

25@pytest.mark.filterwarnings( 1jkml

26 r"ignore:\s*on_event is deprecated, use lifespan event handlers instead.*:DeprecationWarning" 

27) 

28def test_router_events(state: State) -> None: 1jkml

29 app = FastAPI() 1abc

30 

31 @app.get("/") 1abc

32 def main() -> dict[str, str]: 1abc

33 return {"message": "Hello World"} 1abc

34 

35 @app.on_event("startup") 1abc

36 def app_startup() -> None: 1abc

37 state.app_startup = True 1abc

38 

39 @app.on_event("shutdown") 1abc

40 def app_shutdown() -> None: 1abc

41 state.app_shutdown = True 1abc

42 

43 router = APIRouter() 1abc

44 

45 @router.on_event("startup") 1abc

46 def router_startup() -> None: 1abc

47 state.router_startup = True 1abc

48 

49 @router.on_event("shutdown") 1abc

50 def router_shutdown() -> None: 1abc

51 state.router_shutdown = True 1abc

52 

53 sub_router = APIRouter() 1abc

54 

55 @sub_router.on_event("startup") 1abc

56 def sub_router_startup() -> None: 1abc

57 state.sub_router_startup = True 1abc

58 

59 @sub_router.on_event("shutdown") 1abc

60 def sub_router_shutdown() -> None: 1abc

61 state.sub_router_shutdown = True 1abc

62 

63 router.include_router(sub_router) 1abc

64 app.include_router(router) 1abc

65 

66 assert state.app_startup is False 1abc

67 assert state.router_startup is False 1abc

68 assert state.sub_router_startup is False 1abc

69 assert state.app_shutdown is False 1abc

70 assert state.router_shutdown is False 1abc

71 assert state.sub_router_shutdown is False 1abc

72 with TestClient(app) as client: 1abc

73 assert state.app_startup is True 1abc

74 assert state.router_startup is True 1abc

75 assert state.sub_router_startup is True 1abc

76 assert state.app_shutdown is False 1abc

77 assert state.router_shutdown is False 1abc

78 assert state.sub_router_shutdown is False 1abc

79 response = client.get("/") 1abc

80 assert response.status_code == 200, response.text 1abc

81 assert response.json() == {"message": "Hello World"} 1abc

82 assert state.app_startup is True 1abc

83 assert state.router_startup is True 1abc

84 assert state.sub_router_startup is True 1abc

85 assert state.app_shutdown is True 1abc

86 assert state.router_shutdown is True 1abc

87 assert state.sub_router_shutdown is True 1abc

88 

89 

90def test_app_lifespan_state(state: State) -> None: 1jkml

91 @asynccontextmanager 1nop

92 async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 1nop

93 state.app_startup = True 1nop

94 yield 1nop

95 state.app_shutdown = True 1nop

96 

97 app = FastAPI(lifespan=lifespan) 1nop

98 

99 @app.get("/") 1nop

100 def main() -> dict[str, str]: 1nop

101 return {"message": "Hello World"} 1nop

102 

103 assert state.app_startup is False 1nop

104 assert state.app_shutdown is False 1nop

105 with TestClient(app) as client: 1nop

106 assert state.app_startup is True 1nop

107 assert state.app_shutdown is False 1nop

108 response = client.get("/") 1nop

109 assert response.status_code == 200, response.text 1nop

110 assert response.json() == {"message": "Hello World"} 1nop

111 assert state.app_startup is True 1nop

112 assert state.app_shutdown is True 1nop

113 

114 

115def test_router_nested_lifespan_state(state: State) -> None: 1jkml

116 @asynccontextmanager 1def

117 async def lifespan(app: FastAPI) -> AsyncGenerator[dict[str, bool], None]: 1def

118 state.app_startup = True 1def

119 yield {"app": True} 1def

120 state.app_shutdown = True 1def

121 

122 @asynccontextmanager 1def

123 async def router_lifespan(app: FastAPI) -> AsyncGenerator[dict[str, bool], None]: 1def

124 state.router_startup = True 1def

125 yield {"router": True} 1def

126 state.router_shutdown = True 1def

127 

128 @asynccontextmanager 1def

129 async def subrouter_lifespan(app: FastAPI) -> AsyncGenerator[dict[str, bool], None]: 1def

130 state.sub_router_startup = True 1def

131 yield {"sub_router": True} 1def

132 state.sub_router_shutdown = True 1def

133 

134 sub_router = APIRouter(lifespan=subrouter_lifespan) 1def

135 

136 router = APIRouter(lifespan=router_lifespan) 1def

137 router.include_router(sub_router) 1def

138 

139 app = FastAPI(lifespan=lifespan) 1def

140 app.include_router(router) 1def

141 

142 @app.get("/") 1def

143 def main(request: Request) -> dict[str, str]: 1def

144 assert request.state.app 1def

145 assert request.state.router 1def

146 assert request.state.sub_router 1def

147 return {"message": "Hello World"} 1def

148 

149 assert state.app_startup is False 1def

150 assert state.router_startup is False 1def

151 assert state.sub_router_startup is False 1def

152 assert state.app_shutdown is False 1def

153 assert state.router_shutdown is False 1def

154 assert state.sub_router_shutdown is False 1def

155 

156 with TestClient(app) as client: 1def

157 assert state.app_startup is True 1def

158 assert state.router_startup is True 1def

159 assert state.sub_router_startup is True 1def

160 assert state.app_shutdown is False 1def

161 assert state.router_shutdown is False 1def

162 assert state.sub_router_shutdown is False 1def

163 response = client.get("/") 1def

164 assert response.status_code == 200, response.text 1def

165 assert response.json() == {"message": "Hello World"} 1def

166 

167 assert state.app_startup is True 1def

168 assert state.router_startup is True 1def

169 assert state.sub_router_startup is True 1def

170 assert state.app_shutdown is True 1def

171 assert state.router_shutdown is True 1def

172 assert state.sub_router_shutdown is True 1def

173 

174 

175def test_router_nested_lifespan_state_overriding_by_parent() -> None: 1jkml

176 @asynccontextmanager 1CDE

177 async def lifespan( 1CDE

178 app: FastAPI, 

179 ) -> AsyncGenerator[dict[str, Union[str, bool]], None]: 

180 yield { 1CDE

181 "app_specific": True, 

182 "overridden": "app", 

183 } 

184 

185 @asynccontextmanager 1CDE

186 async def router_lifespan( 1CDE

187 app: FastAPI, 

188 ) -> AsyncGenerator[dict[str, Union[str, bool]], None]: 

189 yield { 1CDE

190 "router_specific": True, 

191 "overridden": "router", # should override parent 

192 } 

193 

194 router = APIRouter(lifespan=router_lifespan) 1CDE

195 app = FastAPI(lifespan=lifespan) 1CDE

196 app.include_router(router) 1CDE

197 

198 with TestClient(app) as client: 1CDE

199 assert client.app_state == { 1CDE

200 "app_specific": True, 

201 "router_specific": True, 

202 "overridden": "app", 

203 } 

204 

205 

206def test_merged_no_return_lifespans_return_none() -> None: 1jkml

207 @asynccontextmanager 1FGH

208 async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 1FGH

209 yield 1FGH

210 

211 @asynccontextmanager 1FGH

212 async def router_lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 1FGH

213 yield 1FGH

214 

215 router = APIRouter(lifespan=router_lifespan) 1FGH

216 app = FastAPI(lifespan=lifespan) 1FGH

217 app.include_router(router) 1FGH

218 

219 with TestClient(app) as client: 1FGH

220 assert not client.app_state 1FGH

221 

222 

223def test_merged_mixed_state_lifespans() -> None: 1jkml

224 @asynccontextmanager 1wxy

225 async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 1wxy

226 yield 1wxy

227 

228 @asynccontextmanager 1wxy

229 async def router_lifespan(app: FastAPI) -> AsyncGenerator[dict[str, bool], None]: 1wxy

230 yield {"router": True} 1wxy

231 

232 @asynccontextmanager 1wxy

233 async def sub_router_lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 1wxy

234 yield 1wxy

235 

236 sub_router = APIRouter(lifespan=sub_router_lifespan) 1wxy

237 router = APIRouter(lifespan=router_lifespan) 1wxy

238 app = FastAPI(lifespan=lifespan) 1wxy

239 router.include_router(sub_router) 1wxy

240 app.include_router(router) 1wxy

241 

242 with TestClient(app) as client: 1wxy

243 assert client.app_state == {"router": True} 1wxy

244 

245 

246@pytest.mark.filterwarnings( 1jkml

247 r"ignore:\s*on_event is deprecated, use lifespan event handlers instead.*:DeprecationWarning" 

248) 

249def test_router_async_shutdown_handler(state: State) -> None: 1jkml

250 """Test that async on_shutdown event handlers are called correctly, for coverage.""" 

251 app = FastAPI() 1zAB

252 

253 @app.get("/") 1zAB

254 def main() -> dict[str, str]: 1zAB

255 return {"message": "Hello World"} 1zAB

256 

257 @app.on_event("shutdown") 1zAB

258 async def app_shutdown() -> None: 1zAB

259 state.app_shutdown = True 1zAB

260 

261 assert state.app_shutdown is False 1zAB

262 with TestClient(app) as client: 1zAB

263 assert state.app_shutdown is False 1zAB

264 response = client.get("/") 1zAB

265 assert response.status_code == 200, response.text 1zAB

266 assert state.app_shutdown is True 1zAB

267 

268 

269def test_router_sync_generator_lifespan(state: State) -> None: 1jkml

270 """Test that a sync generator lifespan works via _wrap_gen_lifespan_context.""" 

271 from collections.abc import Generator 1qrs

272 

273 def lifespan(app: FastAPI) -> Generator[None, None, None]: 1qrs

274 state.app_startup = True 1qrs

275 yield 1qrs

276 state.app_shutdown = True 1qrs

277 

278 app = FastAPI(lifespan=lifespan) # type: ignore[arg-type] 1qrs

279 

280 @app.get("/") 1qrs

281 def main() -> dict[str, str]: 1qrs

282 return {"message": "Hello World"} 1qrs

283 

284 assert state.app_startup is False 1qrs

285 assert state.app_shutdown is False 1qrs

286 with TestClient(app) as client: 1qrs

287 assert state.app_startup is True 1qrs

288 assert state.app_shutdown is False 1qrs

289 response = client.get("/") 1qrs

290 assert response.status_code == 200, response.text 1qrs

291 assert response.json() == {"message": "Hello World"} 1qrs

292 assert state.app_startup is True 1qrs

293 assert state.app_shutdown is True 1qrs

294 

295 

296def test_router_async_generator_lifespan(state: State) -> None: 1jkml

297 """Test that an async generator lifespan (not wrapped) works.""" 

298 

299 async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 1tuv

300 state.app_startup = True 1tuv

301 yield 1tuv

302 state.app_shutdown = True 1tuv

303 

304 app = FastAPI(lifespan=lifespan) # type: ignore[arg-type] 1tuv

305 

306 @app.get("/") 1tuv

307 def main() -> dict[str, str]: 1tuv

308 return {"message": "Hello World"} 1tuv

309 

310 assert state.app_startup is False 1tuv

311 assert state.app_shutdown is False 1tuv

312 with TestClient(app) as client: 1tuv

313 assert state.app_startup is True 1tuv

314 assert state.app_shutdown is False 1tuv

315 response = client.get("/") 1tuv

316 assert response.status_code == 200, response.text 1tuv

317 assert response.json() == {"message": "Hello World"} 1tuv

318 assert state.app_startup is True 1tuv

319 assert state.app_shutdown is True 1tuv

320 

321 

322def test_startup_shutdown_handlers_as_parameters(state: State) -> None: 1jkml

323 """Test that startup/shutdown handlers passed as parameters to FastAPI are called correctly.""" 

324 

325 def app_startup() -> None: 1ghi

326 state.app_startup = True 1ghi

327 

328 def app_shutdown() -> None: 1ghi

329 state.app_shutdown = True 1ghi

330 

331 app = FastAPI(on_startup=[app_startup], on_shutdown=[app_shutdown]) 1ghi

332 

333 @app.get("/") 1ghi

334 def main() -> dict[str, str]: 1ghi

335 return {"message": "Hello World"} 1ghi

336 

337 def router_startup() -> None: 1ghi

338 state.router_startup = True 1ghi

339 

340 def router_shutdown() -> None: 1ghi

341 state.router_shutdown = True 1ghi

342 

343 router = APIRouter(on_startup=[router_startup], on_shutdown=[router_shutdown]) 1ghi

344 

345 def sub_router_startup() -> None: 1ghi

346 state.sub_router_startup = True 1ghi

347 

348 def sub_router_shutdown() -> None: 1ghi

349 state.sub_router_shutdown = True 1ghi

350 

351 sub_router = APIRouter( 1ghi

352 on_startup=[sub_router_startup], on_shutdown=[sub_router_shutdown] 

353 ) 

354 

355 router.include_router(sub_router) 1ghi

356 app.include_router(router) 1ghi

357 

358 assert state.app_startup is False 1ghi

359 assert state.router_startup is False 1ghi

360 assert state.sub_router_startup is False 1ghi

361 assert state.app_shutdown is False 1ghi

362 assert state.router_shutdown is False 1ghi

363 assert state.sub_router_shutdown is False 1ghi

364 with TestClient(app) as client: 1ghi

365 assert state.app_startup is True 1ghi

366 assert state.router_startup is True 1ghi

367 assert state.sub_router_startup is True 1ghi

368 assert state.app_shutdown is False 1ghi

369 assert state.router_shutdown is False 1ghi

370 assert state.sub_router_shutdown is False 1ghi

371 response = client.get("/") 1ghi

372 assert response.status_code == 200, response.text 1ghi

373 assert response.json() == {"message": "Hello World"} 1ghi

374 assert state.app_startup is True 1ghi

375 assert state.router_startup is True 1ghi

376 assert state.sub_router_startup is True 1ghi

377 assert state.app_shutdown is True 1ghi

378 assert state.router_shutdown is True 1ghi

379 assert state.sub_router_shutdown is True 1ghi