Coverage for tests/test_cli.py: 100%

343 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-11 09:18 +0000

1import logging 1abcdefghijklmn

2import subprocess 1abcdefghijklmn

3import sys 1abcdefghijklmn

4from pathlib import Path 1abcdefghijklmn

5from unittest.mock import patch 1abcdefghijklmn

6 

7import pytest 1abcdefghijklmn

8import uvicorn 1abcdefghijklmn

9from fastapi.testclient import TestClient 1abcdefghijklmn

10from httpx import Response 1abcdefghijklmn

11from typer.testing import CliRunner 1abcdefghijklmn

12from uvicorn.importer import import_from_string 1abcdefghijklmn

13 

14from fastapi_cli.cli import app 1abcdefghijklmn

15from fastapi_cli.utils.cli import get_uvicorn_log_config 1abcdefghijklmn

16from tests.utils import changing_dir 1abcdefghijklmn

17 

18runner = CliRunner() 1abcdefghijklmn

19 

20assets_path = Path(__file__).parent / "assets" 1abcdefghijklmn

21 

22 

23@pytest.fixture(autouse=True) 1abcdefghijklmn

24def isolate_cli_environment(monkeypatch: pytest.MonkeyPatch) -> None: 1abcdefghijklmn

25 monkeypatch.delenv("FASTAPI_ENV", raising=False) 1abcdefghijklmn

26 monkeypatch.setattr("fastapi_cli.cli.should_use_rich_logs", lambda: True) 2a Q xbR %b'bo jbndS p (bodpdLb)b*bT +b,b-bqdb U ybV .b/bq kbrdW r :bsdtdMb;b=bX ?b@b[budc Y zbZ ]b^bs lbvd0 t _bwdxdNb`b{b1 |b}b~bydd e 2 Ab3 acbcu mbzd4 v ccAdBdObdcec5 fcgchcCd6 Bb7 icjcw nbDd8 x kcEdFdPblcmc9 ncocpcGdf ! Cb# qcrcy obHd$ z scIdJdQbtcuc% vcwcxcKdg ' Db( yczcA pbLd) B AcMdNdRbBcCc* DcEcFcOdh + Eb, GcHcC qbPd- D IcQdRdSbJcKc. LcMcNcSdi j / Fb: OcPcE rbTd; F QcUdVdTbRcSc= TcUcVcWd? Gb@ WcXcG sbXd[ H YcYdZdUbZc0c] 1c2c3c0dk ^ Hb_ 4c5cI tb1d` J 6c2d3dVb7c8c{ 9c!c#c4dl | Ib} $c%cK ub5d~ L 'c6d7dWb(c)cab*c+c,c8dm n bbJbcb-c.cM vb9ddbN /c!d#dXb:c;ceb=c?c@c$dfbKbgb[c]cO wb%dhbP ^c'd(dYb_c`cib{c|c}c)d

27 

28 

29def test_dev() -> None: 1abcdefghijklmn

30 with changing_dir(assets_path): 2Q U Y 2 6 ! ' + / ? ^ | bbfb

31 with patch.object(uvicorn, "run") as mock_run: 2Q U Y 2 6 ! ' + / ? ^ | bbfb

32 result = runner.invoke(app, ["dev", "single_file_app.py"]) 2Q U Y 2 6 ! ' + / ? ^ | bbfb

33 assert result.exit_code == 0, result.output 2Q U Y 2 6 ! ' + / ? ^ | bbfb

34 assert mock_run.called 2Q U Y 2 6 ! ' + / ? ^ | bbfb

35 assert mock_run.call_args 2Q U Y 2 6 ! ' + / ? ^ | bbfb

36 assert mock_run.call_args.kwargs == { 2Q U Y 2 6 ! ' + / ? ^ | bbfb

37 "app": "single_file_app:app", 

38 "host": "127.0.0.1", 

39 "port": 8000, 

40 "reload": True, 

41 "reload_dirs": None, 

42 "workers": None, 

43 "root_path": "", 

44 "proxy_headers": True, 

45 "forwarded_allow_ips": None, 

46 "log_config": get_uvicorn_log_config(), 

47 } 

48 assert "Starting FastAPI in development mode" in result.output 2Q U Y 2 6 ! ' + / ? ^ | bbfb

49 assert "Using import string: single_file_app:app" in result.output 2Q U Y 2 6 ! ' + / ? ^ | bbfb

50 assert "Server started at http://127.0.0.1:8000" in result.output 2Q U Y 2 6 ! ' + / ? ^ | bbfb

51 assert "Documentation at http://127.0.0.1:8000/docs" in result.output 2Q U Y 2 6 ! ' + / ? ^ | bbfb

52 # a passed path is explicit, so no source hint and no pyproject nudge 

53 assert "(auto-discovered" not in result.output 2Q U Y 2 6 ! ' + / ? ^ | bbfb

54 assert "You can configure an entrypoint in pyproject.toml" not in result.output 2Q U Y 2 6 ! ' + / ? ^ | bbfb

55 # the step-by-step narration is --verbose only 

56 assert "Searching for package file structure" not in result.output 2Q U Y 2 6 ! ' + / ? ^ | bbfb

57 assert "Configuration sources:" not in result.output 2Q U Y 2 6 ! ' + / ? ^ | bbfb

58 

59 

60def _get_fastapi_env_response(*, command: str, fastapi_env: str | None) -> Response: 1abcdefghijklmn

61 module_name = "environment_app" 2R S T V W X Z 0 1 3 4 5 7 8 9 # $ % ( ) * , - . : ; = @ [ ] _ ` { } ~ abcbdbebgbhbib

62 sys.modules.pop(module_name, None) 2R S T V W X Z 0 1 3 4 5 7 8 9 # $ % ( ) * , - . : ; = @ [ ] _ ` { } ~ abcbdbebgbhbib

63 

64 try: 2R S T V W X Z 0 1 3 4 5 7 8 9 # $ % ( ) * , - . : ; = @ [ ] _ ` { } ~ abcbdbebgbhbib

65 with changing_dir(assets_path): 2R S T V W X Z 0 1 3 4 5 7 8 9 # $ % ( ) * , - . : ; = @ [ ] _ ` { } ~ abcbdbebgbhbib

66 with patch.object(uvicorn, "run") as mock_run: 2R S T V W X Z 0 1 3 4 5 7 8 9 # $ % ( ) * , - . : ; = @ [ ] _ ` { } ~ abcbdbebgbhbib

67 result = runner.invoke( 2R S T V W X Z 0 1 3 4 5 7 8 9 # $ % ( ) * , - . : ; = @ [ ] _ ` { } ~ abcbdbebgbhbib

68 app, 

69 [command, f"{module_name}.py"], 

70 env={"FASTAPI_ENV": fastapi_env}, 

71 ) 

72 

73 assert result.exit_code == 0, result.output 2R S T V W X Z 0 1 3 4 5 7 8 9 # $ % ( ) * , - . : ; = @ [ ] _ ` { } ~ abcbdbebgbhbib

74 assert mock_run.call_args 2R S T V W X Z 0 1 3 4 5 7 8 9 # $ % ( ) * , - . : ; = @ [ ] _ ` { } ~ abcbdbebgbhbib

75 imported_app = import_from_string(mock_run.call_args.kwargs["app"]) 2R S T V W X Z 0 1 3 4 5 7 8 9 # $ % ( ) * , - . : ; = @ [ ] _ ` { } ~ abcbdbebgbhbib

76 with TestClient(imported_app) as test_client: 2R S T V W X Z 0 1 3 4 5 7 8 9 # $ % ( ) * , - . : ; = @ [ ] _ ` { } ~ abcbdbebgbhbib

77 return test_client.get("/fastapi-env") 2R S T V W X Z 0 1 3 4 5 7 8 9 # $ % ( ) * , - . : ; = @ [ ] _ ` { } ~ abcbdbebgbhbib

78 finally: 

79 sys.modules.pop(module_name, None) 2R S T V W X Z 0 1 3 4 5 7 8 9 # $ % ( ) * , - . : ; = @ [ ] _ ` { } ~ abcbdbebgbhbib

80 

81 

82def test_dev_sets_fastapi_env_before_app_import() -> None: 1abcdefghijklmn

83 response = _get_fastapi_env_response(command="dev", fastapi_env=None) 2S W 0 4 8 $ ) - ; [ ` ~ dbhb

84 

85 assert response.status_code == 200 2S W 0 4 8 $ ) - ; [ ` ~ dbhb

86 assert response.json() == {"fastapi_env": "development"} 2S W 0 4 8 $ ) - ; [ ` ~ dbhb

87 

88 

89def test_dev_does_not_override_existing_fastapi_env() -> None: 1abcdefghijklmn

90 response = _get_fastapi_env_response(command="dev", fastapi_env="chicken") 2R V Z 3 7 # ( , : @ _ } cbgb

91 

92 assert response.status_code == 200 2R V Z 3 7 # ( , : @ _ } cbgb

93 assert response.json() == {"fastapi_env": "chicken"} 2R V Z 3 7 # ( , : @ _ } cbgb

94 

95 

96def test_run_does_not_set_fastapi_env() -> None: 1abcdefghijklmn

97 response = _get_fastapi_env_response(command="run", fastapi_env=None) 2T X 1 5 9 % * . = ] { abebib

98 

99 assert response.status_code == 200 2T X 1 5 9 % * . = ] { abebib

100 assert response.json() == {"fastapi_env": None} 2T X 1 5 9 % * . = ] { abebib

101 

102 

103def test_run_uses_uvicorn_default_log_config_without_rich_logs( 1abcdefghijklmn

104 monkeypatch: pytest.MonkeyPatch, 

105) -> None: 

106 monkeypatch.setattr("fastapi_cli.cli.should_use_rich_logs", lambda: False) 2*d+d,d-d.d/d:d;d=d?d@d[d]d^d

107 

108 with changing_dir(assets_path): 2*d+d,d-d.d/d:d;d=d?d@d[d]d^d

109 with patch.object(uvicorn, "run") as mock_run: 2*d+d,d-d.d/d:d;d=d?d@d[d]d^d

110 result = runner.invoke(app, ["run", "single_file_app.py"]) 2*d+d,d-d.d/d:d;d=d?d@d[d]d^d

111 assert result.exit_code == 0, result.output 2*d+d,d-d.d/d:d;d=d?d@d[d]d^d

112 assert mock_run.called 2*d+d,d-d.d/d:d;d=d?d@d[d]d^d

113 assert mock_run.call_args 2*d+d,d-d.d/d:d;d=d?d@d[d]d^d

114 

115 assert "log_config" not in mock_run.call_args.kwargs 2*d+d,d-d.d/d:d;d=d?d@d[d]d^d

116 

117 

118def test_dev_no_args_auto_discovery() -> None: 1abcdefghijklmn

119 """Test that auto-discovery works when no args and no pyproject.toml entrypoint""" 

120 with changing_dir(assets_path / "default_files" / "default_main"): 1oqsuwyACEGIKMO

121 with patch.object(uvicorn, "run") as mock_run: 1oqsuwyACEGIKMO

122 result = runner.invoke(app, ["dev"]) # No path argument 1oqsuwyACEGIKMO

123 assert result.exit_code == 0, result.output 1oqsuwyACEGIKMO

124 assert mock_run.called 1oqsuwyACEGIKMO

125 assert mock_run.call_args 1oqsuwyACEGIKMO

126 assert mock_run.call_args.kwargs["app"] == "main:app" 1oqsuwyACEGIKMO

127 assert mock_run.call_args.kwargs["host"] == "127.0.0.1" 1oqsuwyACEGIKMO

128 assert mock_run.call_args.kwargs["port"] == 8000 1oqsuwyACEGIKMO

129 assert mock_run.call_args.kwargs["reload"] is True 1oqsuwyACEGIKMO

130 assert "Starting FastAPI in development mode" in result.output 1oqsuwyACEGIKMO

131 assert "Using import string: main:app" in result.output 1oqsuwyACEGIKMO

132 # Rich can wrap the rest of the hint based on terminal and emoji width 

133 assert "(auto-discovered, use --verbose" in result.output 1oqsuwyACEGIKMO

134 # auto-discovery nudges you to pin the entrypoint 

135 assert "You can configure an entrypoint in pyproject.toml" in result.output 1oqsuwyACEGIKMO

136 assert "[tool.fastapi]" in result.output 1oqsuwyACEGIKMO

137 assert 'entrypoint = "main:app"' in result.output 1oqsuwyACEGIKMO

138 # the full breakdown stays behind --verbose 

139 assert "Configuration sources:" not in result.output 1oqsuwyACEGIKMO

140 

141 

142def test_dev_package() -> None: 1abcdefghijklmn

143 with changing_dir(assets_path): 2jbkblbmbnbobpbqbrbsbtbubvbwb

144 with patch.object(uvicorn, "run") as mock_run: 2jbkblbmbnbobpbqbrbsbtbubvbwb

145 result = runner.invoke(app, ["dev", "nested_package/package"]) 2jbkblbmbnbobpbqbrbsbtbubvbwb

146 assert result.exit_code == 0, result.output 2jbkblbmbnbobpbqbrbsbtbubvbwb

147 assert mock_run.called 2jbkblbmbnbobpbqbrbsbtbubvbwb

148 assert mock_run.call_args 2jbkblbmbnbobpbqbrbsbtbubvbwb

149 assert mock_run.call_args.kwargs == { 2jbkblbmbnbobpbqbrbsbtbubvbwb

150 "app": "nested_package.package:app", 

151 "host": "127.0.0.1", 

152 "port": 8000, 

153 "reload": True, 

154 "reload_dirs": None, 

155 "workers": None, 

156 "root_path": "", 

157 "proxy_headers": True, 

158 "forwarded_allow_ips": None, 

159 "log_config": get_uvicorn_log_config(), 

160 } 

161 assert "Starting FastAPI in development mode" in result.output 2jbkblbmbnbobpbqbrbsbtbubvbwb

162 assert "Using import string: nested_package.package:app" in result.output 2jbkblbmbnbobpbqbrbsbtbubvbwb

163 assert "Server started at http://127.0.0.1:8000" in result.output 2jbkblbmbnbobpbqbrbsbtbubvbwb

164 assert "Documentation at http://127.0.0.1:8000/docs" in result.output 2jbkblbmbnbobpbqbrbsbtbubvbwb

165 assert "(auto-discovered" not in result.output 2jbkblbmbnbobpbqbrbsbtbubvbwb

166 assert "You can configure an entrypoint in pyproject.toml" not in result.output 2jbkblbmbnbobpbqbrbsbtbubvbwb

167 # the file tree is --verbose only 

168 assert "📁 package" not in result.output 2jbkblbmbnbobpbqbrbsbtbubvbwb

169 

170 

171def test_dev_args() -> None: 1abcdefghijklmn

172 with changing_dir(assets_path): 2xbybzbAbBbCbDbEbFbGbHbIbJbKb

173 with patch.object(uvicorn, "run") as mock_run: 2xbybzbAbBbCbDbEbFbGbHbIbJbKb

174 result = runner.invoke( 2xbybzbAbBbCbDbEbFbGbHbIbJbKb

175 app, 

176 [ 

177 "dev", 

178 "single_file_app.py", 

179 "--host", 

180 "192.168.0.2", 

181 "--port", 

182 "8080", 

183 "--no-reload", 

184 "--root-path", 

185 "/api", 

186 "--app", 

187 "api", 

188 "--no-proxy-headers", 

189 ], 

190 ) 

191 assert result.exit_code == 0, result.output 2xbybzbAbBbCbDbEbFbGbHbIbJbKb

192 assert mock_run.called 2xbybzbAbBbCbDbEbFbGbHbIbJbKb

193 assert mock_run.call_args 2xbybzbAbBbCbDbEbFbGbHbIbJbKb

194 assert mock_run.call_args.kwargs == { 2xbybzbAbBbCbDbEbFbGbHbIbJbKb

195 "app": "single_file_app:api", 

196 "host": "192.168.0.2", 

197 "port": 8080, 

198 "reload": False, 

199 "reload_dirs": None, 

200 "workers": None, 

201 "root_path": "/api", 

202 "proxy_headers": False, 

203 "forwarded_allow_ips": None, 

204 "log_config": get_uvicorn_log_config(), 

205 } 

206 assert "Starting FastAPI in development mode" in result.output 2xbybzbAbBbCbDbEbFbGbHbIbJbKb

207 assert "Using import string: single_file_app:api" in result.output 2xbybzbAbBbCbDbEbFbGbHbIbJbKb

208 assert "Server started at http://192.168.0.2:8080" in result.output 2xbybzbAbBbCbDbEbFbGbHbIbJbKb

209 assert "Documentation at http://192.168.0.2:8080/docs" in result.output 2xbybzbAbBbCbDbEbFbGbHbIbJbKb

210 assert "(auto-discovered" not in result.output 2xbybzbAbBbCbDbEbFbGbHbIbJbKb

211 assert "You can configure an entrypoint in pyproject.toml" not in result.output 2xbybzbAbBbCbDbEbFbGbHbIbJbKb

212 

213 

214def test_dev_env_vars() -> None: 1abcdefghijklmn

215 with changing_dir(assets_path): 2%b.b]bacicqcycGcOcWc4c$c-c[c

216 with patch.object(uvicorn, "run") as mock_run: 2%b.b]bacicqcycGcOcWc4c$c-c[c

217 result = runner.invoke( 2%b.b]bacicqcycGcOcWc4c$c-c[c

218 app, ["dev", "single_file_app.py"], env={"PORT": "8111"} 

219 ) 

220 assert result.exit_code == 0, result.output 2%b.b]bacicqcycGcOcWc4c$c-c[c

221 assert mock_run.called 2%b.b]bacicqcycGcOcWc4c$c-c[c

222 assert mock_run.call_args 2%b.b]bacicqcycGcOcWc4c$c-c[c

223 assert mock_run.call_args.kwargs == { 2%b.b]bacicqcycGcOcWc4c$c-c[c

224 "app": "single_file_app:app", 

225 "host": "127.0.0.1", 

226 "port": 8111, 

227 "reload": True, 

228 "reload_dirs": None, 

229 "workers": None, 

230 "root_path": "", 

231 "proxy_headers": True, 

232 "forwarded_allow_ips": None, 

233 "log_config": get_uvicorn_log_config(), 

234 } 

235 assert "Starting FastAPI in development mode" in result.output 2%b.b]bacicqcycGcOcWc4c$c-c[c

236 assert "Using import string: single_file_app:app" in result.output 2%b.b]bacicqcycGcOcWc4c$c-c[c

237 assert "Server started at http://127.0.0.1:8111" in result.output 2%b.b]bacicqcycGcOcWc4c$c-c[c

238 assert "Documentation at http://127.0.0.1:8111/docs" in result.output 2%b.b]bacicqcycGcOcWc4c$c-c[c

239 

240 

241def test_dev_env_vars_and_args() -> None: 1abcdefghijklmn

242 with changing_dir(assets_path): 2'b/b^bbcjcrczcHcPcXc5c%c.c]c

243 with patch.object(uvicorn, "run") as mock_run: 2'b/b^bbcjcrczcHcPcXc5c%c.c]c

244 result = runner.invoke( 2'b/b^bbcjcrczcHcPcXc5c%c.c]c

245 app, 

246 [ 

247 "dev", 

248 "single_file_app.py", 

249 "--port", 

250 "8080", 

251 ], 

252 env={"PORT": "8111"}, 

253 ) 

254 assert result.exit_code == 0, result.output 2'b/b^bbcjcrczcHcPcXc5c%c.c]c

255 assert mock_run.called 2'b/b^bbcjcrczcHcPcXc5c%c.c]c

256 assert mock_run.call_args 2'b/b^bbcjcrczcHcPcXc5c%c.c]c

257 assert mock_run.call_args.kwargs == { 2'b/b^bbcjcrczcHcPcXc5c%c.c]c

258 "app": "single_file_app:app", 

259 "host": "127.0.0.1", 

260 "port": 8080, 

261 "reload": True, 

262 "reload_dirs": None, 

263 "workers": None, 

264 "root_path": "", 

265 "proxy_headers": True, 

266 "forwarded_allow_ips": None, 

267 "log_config": get_uvicorn_log_config(), 

268 } 

269 assert "Starting FastAPI in development mode" in result.output 2'b/b^bbcjcrczcHcPcXc5c%c.c]c

270 assert "Using import string: single_file_app:app" in result.output 2'b/b^bbcjcrczcHcPcXc5c%c.c]c

271 assert "Server started at http://127.0.0.1:8080" in result.output 2'b/b^bbcjcrczcHcPcXc5c%c.c]c

272 assert "Documentation at http://127.0.0.1:8080/docs" in result.output 2'b/b^bbcjcrczcHcPcXc5c%c.c]c

273 

274 

275def test_dev_verbose() -> None: 1abcdefghijklmn

276 """--verbose restores the full narration and the module file tree.""" 

277 with changing_dir(assets_path): 1prtvxzBDFHJLNP

278 with patch.object(uvicorn, "run") as mock_run: 1prtvxzBDFHJLNP

279 result = runner.invoke(app, ["dev", "nested_package/package", "--verbose"]) 1prtvxzBDFHJLNP

280 assert result.exit_code == 0, result.output 1prtvxzBDFHJLNP

281 assert mock_run.called 1prtvxzBDFHJLNP

282 assert "Starting FastAPI in development mode" in result.output 1prtvxzBDFHJLNP

283 assert "Searching for package file structure" in result.output 1prtvxzBDFHJLNP

284 assert "Importing from" in result.output 1prtvxzBDFHJLNP

285 assert "Importing the FastAPI app object" in result.output 1prtvxzBDFHJLNP

286 assert "from nested_package.package import app" in result.output 1prtvxzBDFHJLNP

287 assert "Using import string: nested_package.package:app" in result.output 1prtvxzBDFHJLNP

288 # module and app come from different sources here 

289 assert "Configuration sources:" in result.output 1prtvxzBDFHJLNP

290 assert "Module: path CLI argument" in result.output 1prtvxzBDFHJLNP

291 assert "App name: auto-discovery" in result.output 1prtvxzBDFHJLNP

292 # the file tree 

293 assert "📁 package" in result.output 1prtvxzBDFHJLNP

294 assert "🐍 __init__.py" in result.output 1prtvxzBDFHJLNP

295 # the source hint is folded away in verbose mode 

296 assert "(auto-discovered, use --verbose to learn more)" not in result.output 1prtvxzBDFHJLNP

297 

298 

299def test_dev_verbose_auto_discovery() -> None: 1abcdefghijklmn

300 """--verbose with auto-discovery shows the single 'import string' source.""" 

301 with changing_dir(assets_path / "default_files" / "default_main"): 2(b:b_bcckcscAcIcQcYc6c'c/c^c

302 with patch.object(uvicorn, "run") as mock_run: 2(b:b_bcckcscAcIcQcYc6c'c/c^c

303 result = runner.invoke(app, ["dev", "--verbose"]) 2(b:b_bcckcscAcIcQcYc6c'c/c^c

304 assert result.exit_code == 0, result.output 2(b:b_bcckcscAcIcQcYc6c'c/c^c

305 assert mock_run.called 2(b:b_bcckcscAcIcQcYc6c'c/c^c

306 assert "Searching for package file structure" in result.output 2(b:b_bcckcscAcIcQcYc6c'c/c^c

307 assert "Importing the FastAPI app object" in result.output 2(b:b_bcckcscAcIcQcYc6c'c/c^c

308 assert "Using import string: main:app" in result.output 2(b:b_bcckcscAcIcQcYc6c'c/c^c

309 assert "Configuration sources:" in result.output 2(b:b_bcckcscAcIcQcYc6c'c/c^c

310 # module and app share the same source (auto-discovery) 

311 assert "Import string: auto-discovery" in result.output 2(b:b_bcckcscAcIcQcYc6c'c/c^c

312 # auto-discovery still nudges to pin the entrypoint 

313 assert "You can configure an entrypoint in pyproject.toml" in result.output 2(b:b_bcckcscAcIcQcYc6c'c/c^c

314 

315 

316def test_global_verbose_enables_debug_logging() -> None: 1abcdefghijklmn

317 with changing_dir(assets_path): 2pdtdxdBdFdJdNdRdVdZd3d7d#d(d

318 with patch.object(uvicorn, "run"): 2pdtdxdBdFdJdNdRdVdZd3d7d#d(d

319 result = runner.invoke(app, ["--verbose", "dev", "single_file_app.py"]) 2pdtdxdBdFdJdNdRdVdZd3d7d#d(d

320 assert result.exit_code == 0, result.output 2pdtdxdBdFdJdNdRdVdZd3d7d#d(d

321 

322 assert logging.getLogger("fastapi_cli").level == logging.DEBUG 2pdtdxdBdFdJdNdRdVdZd3d7d#d(d

323 

324 # a run without --verbose resets the level 

325 with changing_dir(assets_path): 2pdtdxdBdFdJdNdRdVdZd3d7d#d(d

326 with patch.object(uvicorn, "run"): 2pdtdxdBdFdJdNdRdVdZd3d7d#d(d

327 result = runner.invoke(app, ["dev", "single_file_app.py"]) 2pdtdxdBdFdJdNdRdVdZd3d7d#d(d

328 assert result.exit_code == 0, result.output 2pdtdxdBdFdJdNdRdVdZd3d7d#d(d

329 

330 assert logging.getLogger("fastapi_cli").level == logging.INFO 2pdtdxdBdFdJdNdRdVdZd3d7d#d(d

331 

332 

333def test_entrypoint_mutually_exclusive_with_path() -> None: 1abcdefghijklmn

334 result = runner.invoke(app, ["dev", "mymodule.py", "--entrypoint", "other:app"]) 2KeLeMeNeOePeQeReSeTeUeVeWeXe

335 

336 assert result.exit_code == 1 2KeLeMeNeOePeQeReSeTeUeVeWeXe

337 assert ( 2KeLeMeNeOePeQeReSeTeUeVeWeXe

338 "Cannot use --entrypoint together with path or --app arguments" in result.output 

339 ) 

340 

341 

342def test_entrypoint_mutually_exclusive_with_app() -> None: 1abcdefghijklmn

343 result = runner.invoke(app, ["dev", "--app", "myapp", "--entrypoint", "other:app"]) 2YeZe0e1e2e3e4e5e6e7e8e9e!e#e

344 assert result.exit_code == 1 2YeZe0e1e2e3e4e5e6e7e8e9e!e#e

345 assert ( 2YeZe0e1e2e3e4e5e6e7e8e9e!e#e

346 "Cannot use --entrypoint together with path or --app arguments" in result.output 

347 ) 

348 

349 

350def test_run() -> None: 1abcdefghijklmn

351 with changing_dir(assets_path): 2)b;b`bdclctcBcJcRcZc7c(c:c_c

352 with patch.object(uvicorn, "run") as mock_run: 2)b;b`bdclctcBcJcRcZc7c(c:c_c

353 result = runner.invoke(app, ["run", "single_file_app.py"]) 2)b;b`bdclctcBcJcRcZc7c(c:c_c

354 assert result.exit_code == 0, result.output 2)b;b`bdclctcBcJcRcZc7c(c:c_c

355 assert mock_run.called 2)b;b`bdclctcBcJcRcZc7c(c:c_c

356 assert mock_run.call_args 2)b;b`bdclctcBcJcRcZc7c(c:c_c

357 assert mock_run.call_args.kwargs == { 2)b;b`bdclctcBcJcRcZc7c(c:c_c

358 "app": "single_file_app:app", 

359 "host": "0.0.0.0", 

360 "port": 8000, 

361 "reload": False, 

362 "reload_dirs": None, 

363 "workers": None, 

364 "root_path": "", 

365 "proxy_headers": True, 

366 "forwarded_allow_ips": None, 

367 "log_config": get_uvicorn_log_config(), 

368 } 

369 assert "Starting FastAPI in production mode" in result.output 2)b;b`bdclctcBcJcRcZc7c(c:c_c

370 assert "Using import string: single_file_app:app" in result.output 2)b;b`bdclctcBcJcRcZc7c(c:c_c

371 assert "Server started at http://0.0.0.0:8000" in result.output 2)b;b`bdclctcBcJcRcZc7c(c:c_c

372 assert "Documentation at http://0.0.0.0:8000/docs" in result.output 2)b;b`bdclctcBcJcRcZc7c(c:c_c

373 

374 

375def test_run_trust_proxy() -> None: 1abcdefghijklmn

376 with changing_dir(assets_path): 2-b[b~bhcpcxcFcNcVc3c#c,c@c}c

377 with patch.object(uvicorn, "run") as mock_run: 2-b[b~bhcpcxcFcNcVc3c#c,c@c}c

378 result = runner.invoke( 2-b[b~bhcpcxcFcNcVc3c#c,c@c}c

379 app, ["run", "single_file_app.py", "--forwarded-allow-ips", "*"] 

380 ) 

381 assert result.exit_code == 0, result.output 2-b[b~bhcpcxcFcNcVc3c#c,c@c}c

382 assert mock_run.called 2-b[b~bhcpcxcFcNcVc3c#c,c@c}c

383 assert mock_run.call_args 2-b[b~bhcpcxcFcNcVc3c#c,c@c}c

384 assert mock_run.call_args.kwargs == { 2-b[b~bhcpcxcFcNcVc3c#c,c@c}c

385 "app": "single_file_app:app", 

386 "host": "0.0.0.0", 

387 "port": 8000, 

388 "reload": False, 

389 "reload_dirs": None, 

390 "workers": None, 

391 "root_path": "", 

392 "proxy_headers": True, 

393 "forwarded_allow_ips": "*", 

394 "log_config": get_uvicorn_log_config(), 

395 } 

396 assert "Starting FastAPI in production mode" in result.output 2-b[b~bhcpcxcFcNcVc3c#c,c@c}c

397 assert "Using import string: single_file_app:app" in result.output 2-b[b~bhcpcxcFcNcVc3c#c,c@c}c

398 assert "Server started at http://0.0.0.0:8000" in result.output 2-b[b~bhcpcxcFcNcVc3c#c,c@c}c

399 assert "Documentation at http://0.0.0.0:8000/docs" in result.output 2-b[b~bhcpcxcFcNcVc3c#c,c@c}c

400 

401 

402def test_run_args() -> None: 1abcdefghijklmn

403 with changing_dir(assets_path): 2*b=b{becmcucCcKcSc0c8c)c;c`c

404 with patch.object(uvicorn, "run") as mock_run: 2*b=b{becmcucCcKcSc0c8c)c;c`c

405 result = runner.invoke( 2*b=b{becmcucCcKcSc0c8c)c;c`c

406 app, 

407 [ 

408 "run", 

409 "single_file_app.py", 

410 "--host", 

411 "192.168.0.2", 

412 "--port", 

413 "8080", 

414 "--no-reload", 

415 "--workers", 

416 "2", 

417 "--root-path", 

418 "/api", 

419 "--app", 

420 "api", 

421 "--no-proxy-headers", 

422 ], 

423 ) 

424 assert result.exit_code == 0, result.output 2*b=b{becmcucCcKcSc0c8c)c;c`c

425 assert mock_run.called 2*b=b{becmcucCcKcSc0c8c)c;c`c

426 assert mock_run.call_args 2*b=b{becmcucCcKcSc0c8c)c;c`c

427 assert mock_run.call_args.kwargs == { 2*b=b{becmcucCcKcSc0c8c)c;c`c

428 "app": "single_file_app:api", 

429 "host": "192.168.0.2", 

430 "port": 8080, 

431 "reload": False, 

432 "reload_dirs": None, 

433 "workers": 2, 

434 "root_path": "/api", 

435 "proxy_headers": False, 

436 "forwarded_allow_ips": None, 

437 "log_config": get_uvicorn_log_config(), 

438 } 

439 

440 assert "Starting FastAPI in production mode" in result.output 2*b=b{becmcucCcKcSc0c8c)c;c`c

441 assert "Using import string: single_file_app:api" in result.output 2*b=b{becmcucCcKcSc0c8c)c;c`c

442 assert "Server started at http://192.168.0.2:8080" in result.output 2*b=b{becmcucCcKcSc0c8c)c;c`c

443 assert "Documentation at http://192.168.0.2:8080/docs" in result.output 2*b=b{becmcucCcKcSc0c8c)c;c`c

444 

445 

446def test_run_env_vars() -> None: 1abcdefghijklmn

447 with changing_dir(assets_path): 2+b?b|bfcncvcDcLcTc1c9c*c=c{c

448 with patch.object(uvicorn, "run") as mock_run: 2+b?b|bfcncvcDcLcTc1c9c*c=c{c

449 result = runner.invoke( 2+b?b|bfcncvcDcLcTc1c9c*c=c{c

450 app, ["run", "single_file_app.py"], env={"PORT": "8111"} 

451 ) 

452 assert result.exit_code == 0, result.output 2+b?b|bfcncvcDcLcTc1c9c*c=c{c

453 assert mock_run.called 2+b?b|bfcncvcDcLcTc1c9c*c=c{c

454 assert mock_run.call_args 2+b?b|bfcncvcDcLcTc1c9c*c=c{c

455 assert mock_run.call_args.kwargs == { 2+b?b|bfcncvcDcLcTc1c9c*c=c{c

456 "app": "single_file_app:app", 

457 "host": "0.0.0.0", 

458 "port": 8111, 

459 "reload": False, 

460 "reload_dirs": None, 

461 "workers": None, 

462 "root_path": "", 

463 "proxy_headers": True, 

464 "forwarded_allow_ips": None, 

465 "log_config": get_uvicorn_log_config(), 

466 } 

467 assert "Starting FastAPI in production mode" in result.output 2+b?b|bfcncvcDcLcTc1c9c*c=c{c

468 assert "Using import string: single_file_app:app" in result.output 2+b?b|bfcncvcDcLcTc1c9c*c=c{c

469 assert "Server started at http://0.0.0.0:8111" in result.output 2+b?b|bfcncvcDcLcTc1c9c*c=c{c

470 assert "Documentation at http://0.0.0.0:8111/docs" in result.output 2+b?b|bfcncvcDcLcTc1c9c*c=c{c

471 

472 

473def test_run_env_vars_and_args() -> None: 1abcdefghijklmn

474 with changing_dir(assets_path): 2,b@b}bgcocwcEcMcUc2c!c+c?c|c

475 with patch.object(uvicorn, "run") as mock_run: 2,b@b}bgcocwcEcMcUc2c!c+c?c|c

476 result = runner.invoke( 2,b@b}bgcocwcEcMcUc2c!c+c?c|c

477 app, 

478 [ 

479 "run", 

480 "single_file_app.py", 

481 "--port", 

482 "8080", 

483 ], 

484 env={"PORT": "8111"}, 

485 ) 

486 assert result.exit_code == 0, result.output 2,b@b}bgcocwcEcMcUc2c!c+c?c|c

487 assert mock_run.called 2,b@b}bgcocwcEcMcUc2c!c+c?c|c

488 assert mock_run.call_args 2,b@b}bgcocwcEcMcUc2c!c+c?c|c

489 assert mock_run.call_args.kwargs == { 2,b@b}bgcocwcEcMcUc2c!c+c?c|c

490 "app": "single_file_app:app", 

491 "host": "0.0.0.0", 

492 "port": 8080, 

493 "reload": False, 

494 "reload_dirs": None, 

495 "workers": None, 

496 "root_path": "", 

497 "proxy_headers": True, 

498 "forwarded_allow_ips": None, 

499 "log_config": get_uvicorn_log_config(), 

500 } 

501 assert "Starting FastAPI in production mode" in result.output 2,b@b}bgcocwcEcMcUc2c!c+c?c|c

502 assert "Using import string: single_file_app:app" in result.output 2,b@b}bgcocwcEcMcUc2c!c+c?c|c

503 assert "Server started at http://0.0.0.0:8080" in result.output 2,b@b}bgcocwcEcMcUc2c!c+c?c|c

504 assert "Documentation at http://0.0.0.0:8080/docs" in result.output 2,b@b}bgcocwcEcMcUc2c!c+c?c|c

505 

506 

507@pytest.mark.parametrize("command", ["dev", "run"]) 1abcdefghijklmn

508@pytest.mark.parametrize( 1abcdefghijklmn

509 "public_url", 

510 [ 

511 "https://myapp.example.com", 

512 "https://myapp.example.com/", 

513 "https://myapp.example.com/subpath/", 

514 ], 

515) 

516def test_public_url_env_var(command: str, public_url: str) -> None: 1abcdefghijklmn

517 with changing_dir(assets_path): 2LbMbNbObPbQbRbSbTbUbVbWbXbYb

518 with patch.object(uvicorn, "run") as mock_run: 2LbMbNbObPbQbRbSbTbUbVbWbXbYb

519 result = runner.invoke( 2LbMbNbObPbQbRbSbTbUbVbWbXbYb

520 app, 

521 [ 

522 command, 

523 "single_file_app.py", 

524 "--host", 

525 "0.0.0.0", 

526 ], 

527 env={"FASTAPI_PUBLIC_URL": public_url}, 

528 ) 

529 assert result.exit_code == 0, result.output 2LbMbNbObPbQbRbSbTbUbVbWbXbYb

530 assert mock_run.called 2LbMbNbObPbQbRbSbTbUbVbWbXbYb

531 assert mock_run.call_args 2LbMbNbObPbQbRbSbTbUbVbWbXbYb

532 assert mock_run.call_args.kwargs == { 2LbMbNbObPbQbRbSbTbUbVbWbXbYb

533 "app": "single_file_app:app", 

534 "host": "0.0.0.0", 

535 "port": 8000, 

536 "reload": True if command == "dev" else False, 

537 "reload_dirs": None, 

538 "workers": None, 

539 "root_path": "", 

540 "proxy_headers": True, 

541 "forwarded_allow_ips": None, 

542 "log_config": get_uvicorn_log_config(), 

543 } 

544 

545 assert "Using import string: single_file_app:app" in result.output 2LbMbNbObPbQbRbSbTbUbVbWbXbYb

546 assert ( 2LbMbNbObPbQbRbSbTbUbVbWbXbYb

547 f"Starting FastAPI in {'development' if command == 'dev' else 'production'} mode" 

548 in result.output 

549 ) 

550 expected_url_base = public_url.rstrip("/") 2LbMbNbObPbQbRbSbTbUbVbWbXbYb

551 assert f"Server started at {expected_url_base}" in result.output 2LbMbNbObPbQbRbSbTbUbVbWbXbYb

552 assert f"Documentation at {expected_url_base}/docs" in result.output 2LbMbNbObPbQbRbSbTbUbVbWbXbYb

553 

554 

555def test_run_error() -> None: 1abcdefghijklmn

556 with changing_dir(assets_path): 2_d`d{d|d}d~daebecedeeefegehe

557 result = runner.invoke(app, ["run", "non_existing_file.py"]) 2_d`d{d|d}d~daebecedeeefegehe

558 assert result.exit_code == 1, result.output 2_d`d{d|d}d~daebecedeeefegehe

559 assert "Path does not exist non_existing_file.py" in result.output 2_d`d{d|d}d~daebecedeeefegehe

560 

561 

562def test_dev_help() -> None: 1abcdefghijklmn

563 result = runner.invoke(app, ["dev", "--help"]) 2Zb0b1b2b3b4b5b6b7b8b9b!b#b$b

564 assert result.exit_code == 0, result.output 2Zb0b1b2b3b4b5b6b7b8b9b!b#b$b

565 assert "Run a FastAPI app in development mode." in result.output 2Zb0b1b2b3b4b5b6b7b8b9b!b#b$b

566 assert ( 2Zb0b1b2b3b4b5b6b7b8b9b!b#b$b

567 "This is equivalent to fastapi run but with reload enabled and listening on the 127.0.0.1 address." 

568 in result.output 

569 ) 

570 assert ( 2Zb0b1b2b3b4b5b6b7b8b9b!b#b$b

571 "Otherwise, it uses the first FastAPI app found in the imported module or package." 

572 in result.output 

573 ) 

574 assert "A path to a Python file or package directory" in result.output 2Zb0b1b2b3b4b5b6b7b8b9b!b#b$b

575 assert "The host to serve on." in result.output 2Zb0b1b2b3b4b5b6b7b8b9b!b#b$b

576 assert "The port to serve on." in result.output 2Zb0b1b2b3b4b5b6b7b8b9b!b#b$b

577 assert "Enable auto-reload of the server when (code) files change." in result.output 2Zb0b1b2b3b4b5b6b7b8b9b!b#b$b

578 assert "Set reload directories explicitly" in result.output 2Zb0b1b2b3b4b5b6b7b8b9b!b#b$b

579 assert "The root path is used to tell your app" in result.output 2Zb0b1b2b3b4b5b6b7b8b9b!b#b$b

580 assert "The name of the variable that contains the FastAPI app" in result.output 2Zb0b1b2b3b4b5b6b7b8b9b!b#b$b

581 assert "Use multiple worker processes." not in result.output 2Zb0b1b2b3b4b5b6b7b8b9b!b#b$b

582 

583 

584def test_run_help() -> None: 1abcdefghijklmn

585 result = runner.invoke(app, ["run", "--help"]) 2~cadbdcdddedfdgdhdidjdkdldmd

586 assert result.exit_code == 0, result.output 2~cadbdcdddedfdgdhdidjdkdldmd

587 assert "Run a FastAPI app in production mode." in result.output 2~cadbdcdddedfdgdhdidjdkdldmd

588 assert ( 2~cadbdcdddedfdgdhdidjdkdldmd

589 "This is equivalent to fastapi dev but with reload disabled and listening on the 0.0.0.0 address." 

590 in result.output 

591 ) 

592 assert ( 2~cadbdcdddedfdgdhdidjdkdldmd

593 "Otherwise, it uses the first FastAPI app found in the imported module or package." 

594 in result.output 

595 ) 

596 assert "A path to a Python file or package directory" in result.output 2~cadbdcdddedfdgdhdidjdkdldmd

597 assert "The host to serve on." in result.output 2~cadbdcdddedfdgdhdidjdkdldmd

598 assert "The port to serve on." in result.output 2~cadbdcdddedfdgdhdidjdkdldmd

599 assert "Enable auto-reload of the server when (code) files change." in result.output 2~cadbdcdddedfdgdhdidjdkdldmd

600 assert "The root path is used to tell your app" in result.output 2~cadbdcdddedfdgdhdidjdkdldmd

601 assert "The name of the variable that contains the FastAPI app" in result.output 2~cadbdcdddedfdgdhdidjdkdldmd

602 assert "Use multiple worker processes." in result.output 2~cadbdcdddedfdgdhdidjdkdldmd

603 

604 

605def test_callback_help() -> None: 1abcdefghijklmn

606 result = runner.invoke(app, ["--help"]) 2iejekelemeneoepeqereseteueve

607 assert result.exit_code == 0, result.output 2iejekelemeneoepeqereseteueve

608 assert "FastAPI CLI - The fastapi command line app." in result.output 2iejekelemeneoepeqereseteueve

609 assert "Show the version and exit." in result.output 2iejekelemeneoepeqereseteueve

610 

611 

612def test_version() -> None: 1abcdefghijklmn

613 result = runner.invoke(app, ["--version"]) 2wexeyezeAeBeCeDeEeFeGeHeIeJe

614 assert result.exit_code == 0, result.output 2wexeyezeAeBeCeDeEeFeGeHeIeJe

615 assert "FastAPI CLI version:" in result.output 2wexeyezeAeBeCeDeEeFeGeHeIeJe

616 assert "FastAPI Cloud CLI version:" in result.output 2wexeyezeAeBeCeDeEeFeGeHeIeJe

617 

618 

619def test_dev_reload_dir() -> None: 1abcdefghijklmn

620 with changing_dir(assets_path): 2ndrdvdzdDdHdLdPdTdXd1d5d9d%d

621 with patch.object(uvicorn, "run") as mock_run: 2ndrdvdzdDdHdLdPdTdXd1d5d9d%d

622 result = runner.invoke( 2ndrdvdzdDdHdLdPdTdXd1d5d9d%d

623 app, 

624 [ 

625 "dev", 

626 "single_file_app.py", 

627 "--reload-dir", 

628 "src", 

629 "--reload-dir", 

630 "lib", 

631 ], 

632 ) 

633 assert result.exit_code == 0, result.output 2ndrdvdzdDdHdLdPdTdXd1d5d9d%d

634 assert mock_run.called 2ndrdvdzdDdHdLdPdTdXd1d5d9d%d

635 assert mock_run.call_args 2ndrdvdzdDdHdLdPdTdXd1d5d9d%d

636 # Paths are resolved to absolute paths 

637 reload_dirs = mock_run.call_args.kwargs["reload_dirs"] 2ndrdvdzdDdHdLdPdTdXd1d5d9d%d

638 assert len(reload_dirs) == 2 2ndrdvdzdDdHdLdPdTdXd1d5d9d%d

639 assert reload_dirs[0] == str((assets_path / "src").resolve()) 2ndrdvdzdDdHdLdPdTdXd1d5d9d%d

640 assert reload_dirs[1] == str((assets_path / "lib").resolve()) 2ndrdvdzdDdHdLdPdTdXd1d5d9d%d

641 

642 

643def test_dev_with_import_string() -> None: 1abcdefghijklmn

644 with changing_dir(assets_path): 2odsdwdAdEdIdMdQdUdYd2d6d!d'd

645 with patch.object(uvicorn, "run") as mock_run: 2odsdwdAdEdIdMdQdUdYd2d6d!d'd

646 result = runner.invoke(app, ["dev", "--entrypoint", "single_file_app:api"]) 2odsdwdAdEdIdMdQdUdYd2d6d!d'd

647 assert result.exit_code == 0, result.output 2odsdwdAdEdIdMdQdUdYd2d6d!d'd

648 assert mock_run.called 2odsdwdAdEdIdMdQdUdYd2d6d!d'd

649 assert mock_run.call_args 2odsdwdAdEdIdMdQdUdYd2d6d!d'd

650 assert mock_run.call_args.kwargs == { 2odsdwdAdEdIdMdQdUdYd2d6d!d'd

651 "app": "single_file_app:api", 

652 "forwarded_allow_ips": None, 

653 "host": "127.0.0.1", 

654 "port": 8000, 

655 "reload": True, 

656 "reload_dirs": None, 

657 "workers": None, 

658 "root_path": "", 

659 "proxy_headers": True, 

660 "log_config": get_uvicorn_log_config(), 

661 } 

662 assert "Using import string: single_file_app:api" in result.output 2odsdwdAdEdIdMdQdUdYd2d6d!d'd

663 # an explicit entrypoint is not auto-discovery 

664 assert "(auto-discovered" not in result.output 2odsdwdAdEdIdMdQdUdYd2d6d!d'd

665 assert "You can configure an entrypoint in pyproject.toml" not in result.output 2odsdwdAdEdIdMdQdUdYd2d6d!d'd

666 

667 

668def test_run_with_import_string() -> None: 1abcdefghijklmn

669 with changing_dir(assets_path): 2qdudydCdGdKdOdSdWd0d4d8d$d)d

670 with patch.object(uvicorn, "run") as mock_run: 2qdudydCdGdKdOdSdWd0d4d8d$d)d

671 result = runner.invoke(app, ["run", "--entrypoint", "single_file_app:app"]) 2qdudydCdGdKdOdSdWd0d4d8d$d)d

672 assert result.exit_code == 0, result.output 2qdudydCdGdKdOdSdWd0d4d8d$d)d

673 assert mock_run.called 2qdudydCdGdKdOdSdWd0d4d8d$d)d

674 assert mock_run.call_args 2qdudydCdGdKdOdSdWd0d4d8d$d)d

675 assert mock_run.call_args.kwargs == { 2qdudydCdGdKdOdSdWd0d4d8d$d)d

676 "app": "single_file_app:app", 

677 "forwarded_allow_ips": None, 

678 "host": "0.0.0.0", 

679 "port": 8000, 

680 "reload": False, 

681 "reload_dirs": None, 

682 "workers": None, 

683 "root_path": "", 

684 "proxy_headers": True, 

685 "log_config": get_uvicorn_log_config(), 

686 } 

687 assert "Using import string: single_file_app:app" in result.output 2qdudydCdGdKdOdSdWd0d4d8d$d)d

688 assert "(auto-discovered" not in result.output 2qdudydCdGdKdOdSdWd0d4d8d$d)d

689 assert "You can configure an entrypoint in pyproject.toml" not in result.output 2qdudydCdGdKdOdSdWd0d4d8d$d)d

690 

691 

692def test_script() -> None: 1abcdefghijklmn

693 result = subprocess.run( 2$e%e'e(e)e*e+e,e-e.e/e:e;e=e

694 [sys.executable, "-m", "coverage", "run", "-m", "fastapi_cli", "--help"], 

695 capture_output=True, 

696 encoding="utf-8", 

697 ) 

698 assert "Usage" in result.stdout 2$e%e'e(e)e*e+e,e-e.e/e:e;e=e