fastapi-cli 0.0.30__tar.gz → 0.0.32__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/PKG-INFO +5 -1
  2. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/README.md +4 -0
  3. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/pyproject.toml +1 -1
  4. fastapi_cli-0.0.32/src/fastapi_cli/__init__.py +1 -0
  5. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/src/fastapi_cli/cli.py +65 -36
  6. fastapi_cli-0.0.32/tests/assets/environment_app.py +12 -0
  7. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/test_cli.py +137 -77
  8. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/test_cli_pyproject.py +7 -7
  9. fastapi_cli-0.0.30/src/fastapi_cli/__init__.py +0 -1
  10. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/LICENSE +0 -0
  11. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/scripts/add_latest_release_date.py +0 -0
  12. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/scripts/format.sh +0 -0
  13. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/scripts/lint.sh +0 -0
  14. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/scripts/prepare_release.py +0 -0
  15. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/scripts/test-cov-html.sh +0 -0
  16. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/scripts/test.sh +0 -0
  17. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/src/fastapi_cli/__main__.py +0 -0
  18. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/src/fastapi_cli/config.py +0 -0
  19. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/src/fastapi_cli/discover.py +0 -0
  20. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/src/fastapi_cli/exceptions.py +0 -0
  21. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/src/fastapi_cli/logging.py +0 -0
  22. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/src/fastapi_cli/py.typed +0 -0
  23. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/src/fastapi_cli/utils/__init__.py +0 -0
  24. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/src/fastapi_cli/utils/cli.py +0 -0
  25. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/__init__.py +0 -0
  26. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/broken_package/mod/__init__.py +0 -0
  27. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/broken_package/mod/app.py +0 -0
  28. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/broken_package/utils.py +0 -0
  29. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_api/api.py +0 -0
  30. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_app/api.py +0 -0
  31. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_app/app.py +0 -0
  32. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
  33. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
  34. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
  35. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
  36. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
  37. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
  38. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
  39. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
  40. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
  41. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
  42. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
  43. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_main/api.py +0 -0
  44. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_main/app.py +0 -0
  45. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/default_main/main.py +0 -0
  46. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/default_files/non_default/nonstandard.py +0 -0
  47. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/nested_package/__init__.py +0 -0
  48. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/nested_package/package/__init__.py +0 -0
  49. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/nested_package/package/core/__init__.py +0 -0
  50. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/nested_package/package/core/utils.py +0 -0
  51. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/nested_package/package/mod/__init__.py +0 -0
  52. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/nested_package/package/mod/api.py +0 -0
  53. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/nested_package/package/mod/app.py +0 -0
  54. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/nested_package/package/mod/other.py +0 -0
  55. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/package/__init__.py +0 -0
  56. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/package/core/__init__.py +0 -0
  57. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/package/core/utils.py +0 -0
  58. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/package/mod/__init__.py +0 -0
  59. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/package/mod/api.py +0 -0
  60. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/package/mod/app.py +0 -0
  61. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/package/mod/other.py +0 -0
  62. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/pyproject_config/another_module.py +0 -0
  63. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/pyproject_config/my_module.py +0 -0
  64. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/pyproject_config/pyproject.toml +0 -0
  65. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/single_file_api.py +0 -0
  66. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/single_file_app.py +0 -0
  67. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/assets/single_file_other.py +0 -0
  68. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/conftest.py +0 -0
  69. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/test_discover.py +0 -0
  70. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/test_prepare_release.py +0 -0
  71. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/test_requirements.py +0 -0
  72. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/test_utils_cli.py +0 -0
  73. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/test_utils_default_dir.py +0 -0
  74. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/test_utils_default_file.py +0 -0
  75. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/test_utils_package.py +0 -0
  76. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/test_utils_single_file.py +0 -0
  77. {fastapi_cli-0.0.30 → fastapi_cli-0.0.32}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-cli
3
- Version: 0.0.30
3
+ Version: 0.0.32
4
4
  Summary: Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀
5
5
  Author-Email: =?utf-8?q?Sebasti=C3=A1n_Ram=C3=ADrez?= <tiangolo@gmail.com>
6
6
  License-Expression: MIT
@@ -121,6 +121,10 @@ By default, it will have **auto-reload** enabled, so it will automatically reloa
121
121
 
122
122
  By default it will listen on the IP address `127.0.0.1`, which is the IP for your machine to communicate with itself alone (`localhost`).
123
123
 
124
+ Before importing your app, `fastapi dev` sets the `FASTAPI_ENV` environment variable to `development`. If `FASTAPI_ENV` is already set, its existing value is preserved. This lets app startup code choose development-friendly behavior while allowing you to provide an app-specific environment such as `staging`.
125
+
126
+ The conventional `FASTAPI_ENV` values are `development` and `production`. `fastapi run` currently leaves `FASTAPI_ENV` unchanged, so set it explicitly if your app needs to detect production mode.
127
+
124
128
  ## `fastapi run`
125
129
 
126
130
  When you run `fastapi run`, it will run on production mode by default.
@@ -76,6 +76,10 @@ By default, it will have **auto-reload** enabled, so it will automatically reloa
76
76
 
77
77
  By default it will listen on the IP address `127.0.0.1`, which is the IP for your machine to communicate with itself alone (`localhost`).
78
78
 
79
+ Before importing your app, `fastapi dev` sets the `FASTAPI_ENV` environment variable to `development`. If `FASTAPI_ENV` is already set, its existing value is preserved. This lets app startup code choose development-friendly behavior while allowing you to provide an app-specific environment such as `staging`.
80
+
81
+ The conventional `FASTAPI_ENV` values are `development` and `production`. `fastapi run` currently leaves `FASTAPI_ENV` unchanged, so set it explicitly if your app needs to detect production mode.
82
+
79
83
  ## `fastapi run`
80
84
 
81
85
  When you run `fastapi run`, it will run on production mode by default.
@@ -38,7 +38,7 @@ dependencies = [
38
38
  "rich-toolkit >= 0.14.8",
39
39
  "tomli >= 2.0.0; python_version < '3.11'",
40
40
  ]
41
- version = "0.0.30"
41
+ version = "0.0.32"
42
42
 
43
43
  [project.optional-dependencies]
44
44
  standard = [
@@ -0,0 +1 @@
1
+ __version__ = "0.0.32"
@@ -142,17 +142,19 @@ def _run(
142
142
  proxy_headers: bool = False,
143
143
  forwarded_allow_ips: str | None = None,
144
144
  public_url: str | None = None,
145
+ verbose: bool = False,
145
146
  ) -> None:
146
147
  with get_rich_toolkit() as toolkit:
147
148
  server_type = "development" if command == "dev" else "production"
148
149
 
149
150
  toolkit.print(f"Starting FastAPI in {server_type} mode", emoji="⚡️")
150
151
 
151
- toolkit.print_line()
152
- toolkit.print(
153
- "Searching for package file structure from directories with [blue]__init__.py[/blue] files",
154
- emoji="🔎",
155
- )
152
+ if verbose:
153
+ toolkit.print_line()
154
+ toolkit.print(
155
+ "Searching for package file structure from directories with [blue]__init__.py[/blue] files",
156
+ emoji="🔎",
157
+ )
156
158
 
157
159
  if entrypoint and (path or app):
158
160
  toolkit.print_line()
@@ -194,40 +196,54 @@ def _run(
194
196
 
195
197
  module_data = import_data.module_data
196
198
  import_string = import_data.import_string
199
+ is_auto_discovery = import_data.module_config_source == "auto-discovery"
197
200
 
198
- toolkit.print_line()
199
- toolkit.print(f"Importing from {module_data.extra_sys_path}", emoji="📂")
200
- toolkit.print_line()
201
+ if verbose:
202
+ toolkit.print_line()
203
+ toolkit.print(f"Importing from {module_data.extra_sys_path}", emoji="📂")
204
+ toolkit.print_line()
201
205
 
202
- if module_data.module_paths:
203
- root_tree = _get_module_tree(module_data.module_paths)
206
+ if module_data.module_paths:
207
+ root_tree = _get_module_tree(module_data.module_paths)
204
208
 
205
- toolkit.print(root_tree)
209
+ toolkit.print(root_tree)
206
210
 
207
- toolkit.print_line()
211
+ toolkit.print_line()
212
+
213
+ toolkit.print(
214
+ "Importing the FastAPI app object from the module with the following code:",
215
+ )
216
+ toolkit.print_line()
217
+ toolkit.print(
218
+ f"[blue]from [bold]{module_data.module_import_str}[/bold] import [bold]{import_data.app_name}[/bold]",
219
+ )
220
+
221
+ # Outside --verbose, fold the resolution source into the import string
222
+ # line and point newcomers at --verbose for the full explanation
223
+ if is_auto_discovery and not verbose:
224
+ app_note = " [dim](auto-discovered, use --verbose to learn more)[/]"
225
+ else:
226
+ app_note = ""
208
227
 
209
- toolkit.print(
210
- "Importing the FastAPI app object from the module with the following code:",
211
- )
212
228
  toolkit.print_line()
213
229
  toolkit.print(
214
- f"[blue]from [bold]{module_data.module_import_str}[/bold] import [bold]{import_data.app_name}[/bold]",
230
+ f"Using import string: [blue]{import_string}[/]{app_note}", emoji="🐍"
215
231
  )
216
232
 
217
- toolkit.print_line()
218
- toolkit.print(f"Using import string: [blue]{import_string}[/]", emoji="🐍")
219
-
220
- toolkit.print_line()
221
- mod_source_desc = SOURCE_DESCRIPTIONS[import_data.module_config_source]
222
- app_source_desc = SOURCE_DESCRIPTIONS[import_data.app_name_config_source]
223
- toolkit.print("Configuration sources:", emoji="📋")
224
- if mod_source_desc == app_source_desc:
225
- toolkit.print(f"• Import string: {mod_source_desc}")
226
- else:
227
- toolkit.print(f"• Module: {mod_source_desc}")
228
- toolkit.print(f"• App name: {app_source_desc}")
233
+ if verbose:
234
+ toolkit.print_line()
235
+ mod_source_desc = SOURCE_DESCRIPTIONS[import_data.module_config_source]
236
+ app_source_desc = SOURCE_DESCRIPTIONS[import_data.app_name_config_source]
237
+ toolkit.print("Configuration sources:", emoji="📋")
238
+ if mod_source_desc == app_source_desc:
239
+ toolkit.print(f"• Import string: {mod_source_desc}")
240
+ else:
241
+ toolkit.print(f"• Module: {mod_source_desc}")
242
+ toolkit.print(f"• App name: {app_source_desc}")
229
243
 
230
- if import_data.module_config_source == "auto-discovery":
244
+ # Nudge to pin the entrypoint whenever it was auto-discovered, so it's
245
+ # explicit next time — shown in the default output, not just --verbose
246
+ if is_auto_discovery:
231
247
  toolkit.print_line()
232
248
  toolkit.print(
233
249
  "You can configure an entrypoint in [blue]pyproject.toml[/] for this app with:",
@@ -252,13 +268,6 @@ def _run(
252
268
  toolkit.print(f"Server started at [link={url}]{url}[/]", emoji="🌐")
253
269
  toolkit.print(f"Documentation at [link={url_docs}]{url_docs}[/]")
254
270
 
255
- if command == "dev":
256
- toolkit.print_line()
257
- toolkit.print(
258
- "Running in development mode, for production use: [bold]fastapi run[/]",
259
- emoji="💡",
260
- )
261
-
262
271
  if not uvicorn:
263
272
  raise FastAPICLIException(
264
273
  "Could not import Uvicorn, try running 'pip install uvicorn'"
@@ -356,6 +365,14 @@ def dev(
356
365
  help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything."
357
366
  ),
358
367
  ] = None,
368
+ verbose: Annotated[
369
+ bool,
370
+ typer.Option(
371
+ "--verbose",
372
+ "-v",
373
+ help="Show detailed startup output and debug logs: how the [bold]FastAPI[/bold] app was discovered, imported, and configured.",
374
+ ),
375
+ ] = False,
359
376
  ) -> Any:
360
377
  """
361
378
  Run a [bold]FastAPI[/bold] app in [yellow]development[/yellow] mode. 🧪
@@ -382,6 +399,8 @@ def dev(
382
399
 
383
400
  Otherwise, it uses the first [bold]FastAPI[/bold] app found in the imported module or package.
384
401
  """
402
+ os.environ.setdefault("FASTAPI_ENV", "development")
403
+
385
404
  _run(
386
405
  path=path,
387
406
  host=host,
@@ -395,6 +414,7 @@ def dev(
395
414
  proxy_headers=proxy_headers,
396
415
  forwarded_allow_ips=forwarded_allow_ips,
397
416
  public_url=os.getenv("FASTAPI_PUBLIC_URL"),
417
+ verbose=verbose,
398
418
  )
399
419
 
400
420
 
@@ -464,6 +484,14 @@ def run(
464
484
  help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything."
465
485
  ),
466
486
  ] = None,
487
+ verbose: Annotated[
488
+ bool,
489
+ typer.Option(
490
+ "--verbose",
491
+ "-v",
492
+ help="Show detailed startup output and debug logs: how the [bold]FastAPI[/bold] app was discovered, imported, and configured.",
493
+ ),
494
+ ] = False,
467
495
  ) -> Any:
468
496
  """
469
497
  Run a [bold]FastAPI[/bold] app in [green]production[/green] mode. 🚀
@@ -503,6 +531,7 @@ def run(
503
531
  proxy_headers=proxy_headers,
504
532
  forwarded_allow_ips=forwarded_allow_ips,
505
533
  public_url=os.getenv("FASTAPI_PUBLIC_URL"),
534
+ verbose=verbose,
506
535
  )
507
536
 
508
537
 
@@ -0,0 +1,12 @@
1
+ import os
2
+
3
+ from fastapi import FastAPI
4
+
5
+ app = FastAPI()
6
+
7
+ fastapi_env_at_import = os.environ.get("FASTAPI_ENV")
8
+
9
+
10
+ @app.get("/fastapi-env")
11
+ def get_fastapi_env() -> dict[str, str | None]:
12
+ return {"fastapi_env": fastapi_env_at_import}
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import subprocess
2
3
  import sys
3
4
  from pathlib import Path
@@ -5,7 +6,10 @@ from unittest.mock import patch
5
6
 
6
7
  import pytest
7
8
  import uvicorn
9
+ from fastapi.testclient import TestClient
10
+ from httpx import Response
8
11
  from typer.testing import CliRunner
12
+ from uvicorn.importer import import_from_string
9
13
 
10
14
  from fastapi_cli.cli import app
11
15
  from fastapi_cli.utils.cli import get_uvicorn_log_config
@@ -17,7 +21,8 @@ assets_path = Path(__file__).parent / "assets"
17
21
 
18
22
 
19
23
  @pytest.fixture(autouse=True)
20
- def force_rich_logs(monkeypatch: pytest.MonkeyPatch) -> None:
24
+ def isolate_cli_environment(monkeypatch: pytest.MonkeyPatch) -> None:
25
+ monkeypatch.delenv("FASTAPI_ENV", raising=False)
21
26
  monkeypatch.setattr("fastapi_cli.cli.should_use_rich_logs", lambda: True)
22
27
 
23
28
 
@@ -40,20 +45,59 @@ def test_dev() -> None:
40
45
  "forwarded_allow_ips": None,
41
46
  "log_config": get_uvicorn_log_config(),
42
47
  }
43
- assert "Using import string: single_file_app:app" in result.output
44
- assert "Configuration sources:" in result.output
45
- assert "Module: path CLI argument" in result.output
46
- assert "App name: auto-discovery" in result.output
47
- assert "You can configure an entrypoint in pyproject.toml" not in result.output
48
48
  assert "Starting FastAPI in development mode" in result.output
49
+ assert "Using import string: single_file_app:app" in result.output
49
50
  assert "Server started at http://127.0.0.1:8000" in result.output
50
51
  assert "Documentation at http://127.0.0.1:8000/docs" in result.output
51
- assert (
52
- "Running in development mode, for production use: fastapi run"
53
- in result.output
54
- )
52
+ # a passed path is explicit, so no source hint and no pyproject nudge
53
+ assert "(auto-discovered" not in result.output
54
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
55
+ # the step-by-step narration is --verbose only
56
+ assert "Searching for package file structure" not in result.output
57
+ assert "Configuration sources:" not in result.output
58
+
59
+
60
+ def _get_fastapi_env_response(*, command: str, fastapi_env: str | None) -> Response:
61
+ module_name = "environment_app"
62
+ sys.modules.pop(module_name, None)
63
+
64
+ try:
65
+ with changing_dir(assets_path):
66
+ with patch.object(uvicorn, "run") as mock_run:
67
+ result = runner.invoke(
68
+ app,
69
+ [command, f"{module_name}.py"],
70
+ env={"FASTAPI_ENV": fastapi_env},
71
+ )
72
+
73
+ assert result.exit_code == 0, result.output
74
+ assert mock_run.call_args
75
+ imported_app = import_from_string(mock_run.call_args.kwargs["app"])
76
+ with TestClient(imported_app) as test_client:
77
+ return test_client.get("/fastapi-env")
78
+ finally:
79
+ sys.modules.pop(module_name, None)
80
+
55
81
 
56
- assert "🐍 single_file_app.py" in result.output
82
+ def test_dev_sets_fastapi_env_before_app_import() -> None:
83
+ response = _get_fastapi_env_response(command="dev", fastapi_env=None)
84
+
85
+ assert response.status_code == 200
86
+ assert response.json() == {"fastapi_env": "development"}
87
+
88
+
89
+ def test_dev_does_not_override_existing_fastapi_env() -> None:
90
+ response = _get_fastapi_env_response(command="dev", fastapi_env="chicken")
91
+
92
+ assert response.status_code == 200
93
+ assert response.json() == {"fastapi_env": "chicken"}
94
+
95
+
96
+ def test_run_does_not_set_fastapi_env() -> None:
97
+ response = _get_fastapi_env_response(command="run", fastapi_env=None)
98
+
99
+ assert response.status_code == 200
100
+ assert response.json() == {"fastapi_env": None}
57
101
 
58
102
 
59
103
  def test_run_uses_uvicorn_default_log_config_without_rich_logs(
@@ -83,12 +127,16 @@ def test_dev_no_args_auto_discovery() -> None:
83
127
  assert mock_run.call_args.kwargs["host"] == "127.0.0.1"
84
128
  assert mock_run.call_args.kwargs["port"] == 8000
85
129
  assert mock_run.call_args.kwargs["reload"] is True
130
+ assert "Starting FastAPI in development mode" in result.output
86
131
  assert "Using import string: main:app" in result.output
87
- assert "Configuration sources:" in result.output
88
- assert "Import string: auto-discovery" in result.output
132
+ # Rich can wrap the rest of the hint based on terminal and emoji width
133
+ assert "(auto-discovered, use --verbose" in result.output
134
+ # auto-discovery nudges you to pin the entrypoint
89
135
  assert "You can configure an entrypoint in pyproject.toml" in result.output
90
136
  assert "[tool.fastapi]" in result.output
91
137
  assert 'entrypoint = "main:app"' in result.output
138
+ # the full breakdown stays behind --verbose
139
+ assert "Configuration sources:" not in result.output
92
140
 
93
141
 
94
142
  def test_dev_package() -> None:
@@ -110,22 +158,14 @@ def test_dev_package() -> None:
110
158
  "forwarded_allow_ips": None,
111
159
  "log_config": get_uvicorn_log_config(),
112
160
  }
113
- assert "Using import string: nested_package.package:app" in result.output
114
- assert "Module: path CLI argument" in result.output
115
- assert "App name: auto-discovery" in result.output
116
- assert "You can configure an entrypoint in pyproject.toml" not in result.output
117
161
  assert "Starting FastAPI in development mode" in result.output
162
+ assert "Using import string: nested_package.package:app" in result.output
118
163
  assert "Server started at http://127.0.0.1:8000" in result.output
119
164
  assert "Documentation at http://127.0.0.1:8000/docs" in result.output
120
- assert (
121
- "Running in development mode, for production use: fastapi run"
122
- in result.output
123
- )
124
-
125
- assert "📁 package" in result.output
126
- assert "└── 🐍 __init__.py" in result.output
127
- assert "└── 📁 package" in result.output
128
- assert " └── 🐍 __init__.py" in result.output
165
+ assert "(auto-discovered" not in result.output
166
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
167
+ # the file tree is --verbose only
168
+ assert "📁 package" not in result.output
129
169
 
130
170
 
131
171
  def test_dev_args() -> None:
@@ -163,17 +203,12 @@ def test_dev_args() -> None:
163
203
  "forwarded_allow_ips": None,
164
204
  "log_config": get_uvicorn_log_config(),
165
205
  }
166
- assert "Using import string: single_file_app:api" in result.output
167
- assert "Module: path CLI argument" in result.output
168
- assert "App name: --app CLI option" in result.output
169
- assert "You can configure an entrypoint in pyproject.toml" not in result.output
170
206
  assert "Starting FastAPI in development mode" in result.output
207
+ assert "Using import string: single_file_app:api" in result.output
171
208
  assert "Server started at http://192.168.0.2:8080" in result.output
172
209
  assert "Documentation at http://192.168.0.2:8080/docs" in result.output
173
- assert (
174
- "Running in development mode, for production use: fastapi run"
175
- in result.output
176
- )
210
+ assert "(auto-discovered" not in result.output
211
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
177
212
 
178
213
 
179
214
  def test_dev_env_vars() -> None:
@@ -197,17 +232,10 @@ def test_dev_env_vars() -> None:
197
232
  "forwarded_allow_ips": None,
198
233
  "log_config": get_uvicorn_log_config(),
199
234
  }
200
- assert "Using import string: single_file_app:app" in result.output
201
- assert "Module: path CLI argument" in result.output
202
- assert "App name: auto-discovery" in result.output
203
- assert "You can configure an entrypoint in pyproject.toml" not in result.output
204
235
  assert "Starting FastAPI in development mode" in result.output
236
+ assert "Using import string: single_file_app:app" in result.output
205
237
  assert "Server started at http://127.0.0.1:8111" in result.output
206
238
  assert "Documentation at http://127.0.0.1:8111/docs" in result.output
207
- assert (
208
- "Running in development mode, for production use: fastapi run"
209
- in result.output
210
- )
211
239
 
212
240
 
213
241
  def test_dev_env_vars_and_args() -> None:
@@ -238,17 +266,68 @@ def test_dev_env_vars_and_args() -> None:
238
266
  "forwarded_allow_ips": None,
239
267
  "log_config": get_uvicorn_log_config(),
240
268
  }
241
- assert "Using import string: single_file_app:app" in result.output
242
- assert "Module: path CLI argument" in result.output
243
- assert "App name: auto-discovery" in result.output
244
- assert "You can configure an entrypoint in pyproject.toml" not in result.output
245
269
  assert "Starting FastAPI in development mode" in result.output
270
+ assert "Using import string: single_file_app:app" in result.output
246
271
  assert "Server started at http://127.0.0.1:8080" in result.output
247
272
  assert "Documentation at http://127.0.0.1:8080/docs" in result.output
248
- assert (
249
- "Running in development mode, for production use: fastapi run"
250
- in result.output
251
- )
273
+
274
+
275
+ def test_dev_verbose() -> None:
276
+ """--verbose restores the full narration and the module file tree."""
277
+ with changing_dir(assets_path):
278
+ with patch.object(uvicorn, "run") as mock_run:
279
+ result = runner.invoke(app, ["dev", "nested_package/package", "--verbose"])
280
+ assert result.exit_code == 0, result.output
281
+ assert mock_run.called
282
+ assert "Starting FastAPI in development mode" in result.output
283
+ assert "Searching for package file structure" in result.output
284
+ assert "Importing from" in result.output
285
+ assert "Importing the FastAPI app object" in result.output
286
+ assert "from nested_package.package import app" in result.output
287
+ assert "Using import string: nested_package.package:app" in result.output
288
+ # module and app come from different sources here
289
+ assert "Configuration sources:" in result.output
290
+ assert "Module: path CLI argument" in result.output
291
+ assert "App name: auto-discovery" in result.output
292
+ # the file tree
293
+ assert "📁 package" in result.output
294
+ assert "🐍 __init__.py" in result.output
295
+ # the source hint is folded away in verbose mode
296
+ assert "(auto-discovered, use --verbose to learn more)" not in result.output
297
+
298
+
299
+ def test_dev_verbose_auto_discovery() -> None:
300
+ """--verbose with auto-discovery shows the single 'import string' source."""
301
+ with changing_dir(assets_path / "default_files" / "default_main"):
302
+ with patch.object(uvicorn, "run") as mock_run:
303
+ result = runner.invoke(app, ["dev", "--verbose"])
304
+ assert result.exit_code == 0, result.output
305
+ assert mock_run.called
306
+ assert "Searching for package file structure" in result.output
307
+ assert "Importing the FastAPI app object" in result.output
308
+ assert "Using import string: main:app" in result.output
309
+ assert "Configuration sources:" in result.output
310
+ # module and app share the same source (auto-discovery)
311
+ assert "Import string: auto-discovery" in result.output
312
+ # auto-discovery still nudges to pin the entrypoint
313
+ assert "You can configure an entrypoint in pyproject.toml" in result.output
314
+
315
+
316
+ def test_global_verbose_enables_debug_logging() -> None:
317
+ with changing_dir(assets_path):
318
+ with patch.object(uvicorn, "run"):
319
+ result = runner.invoke(app, ["--verbose", "dev", "single_file_app.py"])
320
+ assert result.exit_code == 0, result.output
321
+
322
+ assert logging.getLogger("fastapi_cli").level == logging.DEBUG
323
+
324
+ # a run without --verbose resets the level
325
+ with changing_dir(assets_path):
326
+ with patch.object(uvicorn, "run"):
327
+ result = runner.invoke(app, ["dev", "single_file_app.py"])
328
+ assert result.exit_code == 0, result.output
329
+
330
+ assert logging.getLogger("fastapi_cli").level == logging.INFO
252
331
 
253
332
 
254
333
  def test_entrypoint_mutually_exclusive_with_path() -> None:
@@ -287,11 +366,8 @@ def test_run() -> None:
287
366
  "forwarded_allow_ips": None,
288
367
  "log_config": get_uvicorn_log_config(),
289
368
  }
290
- assert "Using import string: single_file_app:app" in result.output
291
- assert "Module: path CLI argument" in result.output
292
- assert "App name: auto-discovery" in result.output
293
- assert "You can configure an entrypoint in pyproject.toml" not in result.output
294
369
  assert "Starting FastAPI in production mode" in result.output
370
+ assert "Using import string: single_file_app:app" in result.output
295
371
  assert "Server started at http://0.0.0.0:8000" in result.output
296
372
  assert "Documentation at http://0.0.0.0:8000/docs" in result.output
297
373
 
@@ -317,14 +393,10 @@ def test_run_trust_proxy() -> None:
317
393
  "forwarded_allow_ips": "*",
318
394
  "log_config": get_uvicorn_log_config(),
319
395
  }
320
- assert "Using import string: single_file_app:app" in result.output
321
396
  assert "Starting FastAPI in production mode" in result.output
397
+ assert "Using import string: single_file_app:app" in result.output
322
398
  assert "Server started at http://0.0.0.0:8000" in result.output
323
399
  assert "Documentation at http://0.0.0.0:8000/docs" in result.output
324
- assert (
325
- "Running in development mode, for production use: fastapi run"
326
- not in result.output
327
- )
328
400
 
329
401
 
330
402
  def test_run_args() -> None:
@@ -365,17 +437,10 @@ def test_run_args() -> None:
365
437
  "log_config": get_uvicorn_log_config(),
366
438
  }
367
439
 
368
- assert "Using import string: single_file_app:api" in result.output
369
- assert "Module: path CLI argument" in result.output
370
- assert "App name: --app CLI option" in result.output
371
- assert "You can configure an entrypoint in pyproject.toml" not in result.output
372
440
  assert "Starting FastAPI in production mode" in result.output
441
+ assert "Using import string: single_file_app:api" in result.output
373
442
  assert "Server started at http://192.168.0.2:8080" in result.output
374
443
  assert "Documentation at http://192.168.0.2:8080/docs" in result.output
375
- assert (
376
- "Running in development mode, for production use: fastapi run"
377
- not in result.output
378
- )
379
444
 
380
445
 
381
446
  def test_run_env_vars() -> None:
@@ -399,11 +464,8 @@ def test_run_env_vars() -> None:
399
464
  "forwarded_allow_ips": None,
400
465
  "log_config": get_uvicorn_log_config(),
401
466
  }
402
- assert "Using import string: single_file_app:app" in result.output
403
- assert "Module: path CLI argument" in result.output
404
- assert "App name: auto-discovery" in result.output
405
- assert "You can configure an entrypoint in pyproject.toml" not in result.output
406
467
  assert "Starting FastAPI in production mode" in result.output
468
+ assert "Using import string: single_file_app:app" in result.output
407
469
  assert "Server started at http://0.0.0.0:8111" in result.output
408
470
  assert "Documentation at http://0.0.0.0:8111/docs" in result.output
409
471
 
@@ -436,11 +498,8 @@ def test_run_env_vars_and_args() -> None:
436
498
  "forwarded_allow_ips": None,
437
499
  "log_config": get_uvicorn_log_config(),
438
500
  }
439
- assert "Using import string: single_file_app:app" in result.output
440
- assert "Module: path CLI argument" in result.output
441
- assert "App name: auto-discovery" in result.output
442
- assert "You can configure an entrypoint in pyproject.toml" not in result.output
443
501
  assert "Starting FastAPI in production mode" in result.output
502
+ assert "Using import string: single_file_app:app" in result.output
444
503
  assert "Server started at http://0.0.0.0:8080" in result.output
445
504
  assert "Documentation at http://0.0.0.0:8080/docs" in result.output
446
505
 
@@ -601,7 +660,8 @@ def test_dev_with_import_string() -> None:
601
660
  "log_config": get_uvicorn_log_config(),
602
661
  }
603
662
  assert "Using import string: single_file_app:api" in result.output
604
- assert "Import string: --entrypoint CLI option" in result.output
663
+ # an explicit entrypoint is not auto-discovery
664
+ assert "(auto-discovered" not in result.output
605
665
  assert "You can configure an entrypoint in pyproject.toml" not in result.output
606
666
 
607
667
 
@@ -625,7 +685,7 @@ def test_run_with_import_string() -> None:
625
685
  "log_config": get_uvicorn_log_config(),
626
686
  }
627
687
  assert "Using import string: single_file_app:app" in result.output
628
- assert "Import string: --entrypoint CLI option" in result.output
688
+ assert "(auto-discovered" not in result.output
629
689
  assert "You can configure an entrypoint in pyproject.toml" not in result.output
630
690
 
631
691
 
@@ -26,8 +26,9 @@ def test_dev_with_pyproject_app_config_uses() -> None:
26
26
  assert mock_run.call_args.kwargs["reload"] is True
27
27
 
28
28
  assert "Using import string: my_module:app" in result.output
29
- assert "Configuration sources:" in result.output
30
- assert "Import string: entrypoint in pyproject.toml" in result.output
29
+ # a configured entrypoint is not auto-discovery
30
+ assert "(auto-discovered" not in result.output
31
+ assert "Configuration sources:" not in result.output
31
32
 
32
33
 
33
34
  def test_run_with_pyproject_app_config() -> None:
@@ -44,8 +45,8 @@ def test_run_with_pyproject_app_config() -> None:
44
45
  assert mock_run.call_args.kwargs["reload"] is False
45
46
 
46
47
  assert "Using import string: my_module:app" in result.output
47
- assert "Configuration sources:" in result.output
48
- assert "Import string: entrypoint in pyproject.toml" in result.output
48
+ assert "(auto-discovered" not in result.output
49
+ assert "Configuration sources:" not in result.output
49
50
 
50
51
 
51
52
  def test_cli_arg_overrides_pyproject_config() -> None:
@@ -58,9 +59,8 @@ def test_cli_arg_overrides_pyproject_config() -> None:
58
59
  assert result.exit_code == 0, result.output
59
60
 
60
61
  assert mock_run.call_args.kwargs["app"] == "another_module:app"
61
- assert "Configuration sources:" in result.output
62
- assert "Module: path CLI argument" in result.output
63
- assert "App name: auto-discovery" in result.output
62
+ assert "Using import string: another_module:app" in result.output
63
+ assert "Configuration sources:" not in result.output
64
64
 
65
65
 
66
66
  def test_pyproject_app_config_invalid_format() -> None:
@@ -1 +0,0 @@
1
- __version__ = "0.0.30"
File without changes