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
« 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
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
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
18runner = CliRunner() 1abcdefghijklmn
20assets_path = Path(__file__).parent / "assets" 1abcdefghijklmn
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
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
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
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 )
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
322 assert logging.getLogger("fastapi_cli").level == logging.DEBUG 2pdtdxdBdFdJdNdRdVdZd3d7d#d(d
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
330 assert logging.getLogger("fastapi_cli").level == logging.INFO 2pdtdxdBdFdJdNdRdVdZd3d7d#d(d
333def test_entrypoint_mutually_exclusive_with_path() -> None: 1abcdefghijklmn
334 result = runner.invoke(app, ["dev", "mymodule.py", "--entrypoint", "other:app"]) 2KeLeMeNeOePeQeReSeTeUeVeWeXe
336 assert result.exit_code == 1 2KeLeMeNeOePeQeReSeTeUeVeWeXe
337 assert ( 2KeLeMeNeOePeQeReSeTeUeVeWeXe
338 "Cannot use --entrypoint together with path or --app arguments" in result.output
339 )
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 )
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
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
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 }
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
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
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
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 }
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
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
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
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
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
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
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
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
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
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