Skip to content

Commit d1e0180

Browse files
committed
🐛 Use minimal startup output outside TTY
Shortcake-Parent: 2026-07-03-avoid-fancy-logs-for-non-tty-output
1 parent d41c95b commit d1e0180

5 files changed

Lines changed: 322 additions & 113 deletions

File tree

src/fastapi_cli/cli.py

Lines changed: 82 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ def callback(
9191
"--version", help="Show the version and exit.", callback=version_callback
9292
),
9393
] = None,
94-
verbose: bool = typer.Option(False, help="Enable verbose output"),
9594
) -> None:
9695
"""
9796
FastAPI CLI - The [bold]fastapi[/bold] command line app. 😎
@@ -101,9 +100,7 @@ def callback(
101100
Read more in the docs: [link=https://fastapi.tiangolo.com/fastapi-cli/]https://fastapi.tiangolo.com/fastapi-cli/[/link].
102101
"""
103102

104-
log_level = logging.DEBUG if verbose else logging.INFO
105-
106-
setup_logging(level=log_level)
103+
setup_logging()
107104

108105

109106
def _get_module_tree(module_paths: list[Path]) -> Tree:
@@ -142,23 +139,29 @@ def _run(
142139
proxy_headers: bool = False,
143140
forwarded_allow_ips: str | None = None,
144141
public_url: str | None = None,
142+
verbose: bool = False,
145143
) -> None:
146-
with get_rich_toolkit() as toolkit:
144+
use_rich = should_use_rich_logs()
145+
# The step-by-step "how the app was found and imported" narration is a
146+
# teaching aid; show it only with --verbose so the default output stays lean
147+
show_details = use_rich and verbose
148+
with get_rich_toolkit(use_rich=use_rich) as toolkit:
147149
server_type = "development" if command == "dev" else "production"
148150

149-
toolkit.print_title(f"Starting {server_type} server 🚀", tag="FastAPI")
150-
toolkit.print_line()
151+
toolkit.print(f"Starting FastAPI in {server_type} mode", emoji="⚡️")
151152

152-
toolkit.print(
153-
"Searching for package file structure from directories with [blue]__init__.py[/blue] files"
154-
)
153+
if show_details:
154+
toolkit.print_line()
155+
toolkit.print(
156+
"Searching for package file structure from directories with [blue]__init__.py[/blue] files",
157+
emoji="🔎",
158+
)
155159

156160
if entrypoint and (path or app):
157161
toolkit.print_line()
158162
toolkit.print(
159163
"[error]Cannot use --entrypoint together with path or --app arguments"
160164
)
161-
toolkit.print_line()
162165
raise typer.Exit(code=1)
163166

164167
try:
@@ -172,8 +175,6 @@ def _run(
172175
field = ".".join(str(loc) for loc in error["loc"])
173176
toolkit.print(f" [red]•[/red] {field}: {error['msg']}")
174177

175-
toolkit.print_line()
176-
177178
raise typer.Exit(code=1) from None
178179

179180
try:
@@ -196,46 +197,58 @@ def _run(
196197

197198
module_data = import_data.module_data
198199
import_string = import_data.import_string
200+
is_auto_discovery = import_data.module_config_source == "auto-discovery"
199201

200-
toolkit.print(f"Importing from {module_data.extra_sys_path}")
201-
toolkit.print_line()
202+
if show_details:
203+
toolkit.print_line()
204+
toolkit.print(f"Importing from {module_data.extra_sys_path}", emoji="📂")
205+
toolkit.print_line()
202206

203-
if module_data.module_paths:
204-
root_tree = _get_module_tree(module_data.module_paths)
207+
if module_data.module_paths:
208+
root_tree = _get_module_tree(module_data.module_paths)
209+
210+
toolkit.print(root_tree)
205211

206-
toolkit.print(root_tree, tag="module")
207212
toolkit.print_line()
208213

209-
toolkit.print(
210-
"Importing the FastAPI app object from the module with the following code:",
211-
tag="code",
212-
)
213-
toolkit.print_line()
214-
toolkit.print(
215-
f"[underline]from [bold]{module_data.module_import_str}[/bold] import [bold]{import_data.app_name}[/bold]"
216-
)
217-
toolkit.print_line()
214+
toolkit.print(
215+
"Importing the FastAPI app object from the module with the following code:",
216+
)
217+
toolkit.print_line()
218+
toolkit.print(
219+
f"[blue]from [bold]{module_data.module_import_str}[/bold] import [bold]{import_data.app_name}[/bold]",
220+
)
221+
222+
# Outside --verbose, fold the resolution source into the import string
223+
# line and point newcomers at --verbose for the full explanation
224+
if is_auto_discovery and not verbose:
225+
app_note = " [dim](auto-discovered, use --verbose to learn more)[/]"
226+
else:
227+
app_note = ""
218228

229+
toolkit.print_line()
219230
toolkit.print(
220-
f"Using import string: [blue]{import_string}[/]",
221-
tag="app",
231+
f"Using import string: [blue]{import_string}[/]{app_note}", emoji="🐍"
222232
)
223233

224-
mod_source_desc = SOURCE_DESCRIPTIONS[import_data.module_config_source]
225-
app_source_desc = SOURCE_DESCRIPTIONS[import_data.app_name_config_source]
226-
toolkit.print_line()
227-
toolkit.print("Configuration sources:", tag="info")
228-
if mod_source_desc == app_source_desc:
229-
toolkit.print(f" • Import string: {mod_source_desc}")
230-
else:
231-
toolkit.print(f" • Module: {mod_source_desc}")
232-
toolkit.print(f" • App name: {app_source_desc}")
234+
if show_details:
235+
toolkit.print_line()
236+
mod_source_desc = SOURCE_DESCRIPTIONS[import_data.module_config_source]
237+
app_source_desc = SOURCE_DESCRIPTIONS[import_data.app_name_config_source]
238+
toolkit.print("Configuration sources:", emoji="📋")
239+
if mod_source_desc == app_source_desc:
240+
toolkit.print(f"• Import string: {mod_source_desc}")
241+
else:
242+
toolkit.print(f"• Module: {mod_source_desc}")
243+
toolkit.print(f"• App name: {app_source_desc}")
233244

234-
if import_data.module_config_source == "auto-discovery":
245+
# Nudge to pin the entrypoint whenever it was auto-discovered, so it's
246+
# explicit next time — shown in the default output, not just --verbose
247+
if use_rich and is_auto_discovery:
235248
toolkit.print_line()
236249
toolkit.print(
237250
"You can configure an entrypoint in [blue]pyproject.toml[/] for this app with:",
238-
tag="tip",
251+
emoji="💡",
239252
)
240253
toolkit.print_line()
241254
toolkit.print(
@@ -246,37 +259,31 @@ def _run(
246259
),
247260
"toml",
248261
theme="ansi_light",
249-
)
262+
),
250263
)
251264

252265
url = public_url.rstrip("/") if public_url else f"http://{host}:{port}"
253266
url_docs = f"{url}/docs"
254267

255-
toolkit.print_line()
256-
toolkit.print(
257-
f"Server started at [link={url}]{url}[/]",
258-
f"Documentation at [link={url_docs}]{url_docs}[/]",
259-
tag="server",
260-
)
261-
262-
if command == "dev":
268+
if use_rich:
263269
toolkit.print_line()
264-
toolkit.print(
265-
"Running in development mode, for production use: [bold]fastapi run[/]",
266-
tag="tip",
267-
)
270+
toolkit.print(f"Server started at [link={url}]{url}[/]", emoji="🌐")
271+
toolkit.print(f"Documentation at [link={url_docs}]{url_docs}[/]")
268272

269273
if not uvicorn:
270274
raise FastAPICLIException(
271275
"Could not import Uvicorn, try running 'pip install uvicorn'"
272276
) from None
273277

274-
toolkit.print_line()
275-
toolkit.print("Logs:")
276-
toolkit.print_line()
278+
if use_rich:
279+
toolkit.print_line()
280+
toolkit.print("Logs:", bullet=False)
281+
toolkit.print_line()
282+
else:
283+
toolkit.print("")
277284

278285
extra_uvicorn_kwargs: dict[str, Any] = (
279-
{"log_config": get_uvicorn_log_config()} if should_use_rich_logs() else {}
286+
{"log_config": get_uvicorn_log_config()} if use_rich else {}
280287
)
281288

282289
uvicorn.run(
@@ -363,6 +370,14 @@ def dev(
363370
help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything."
364371
),
365372
] = None,
373+
verbose: Annotated[
374+
bool,
375+
typer.Option(
376+
"--verbose",
377+
"-v",
378+
help="Show detailed startup output: how the [bold]FastAPI[/bold] app was discovered, imported, and configured.",
379+
),
380+
] = False,
366381
) -> Any:
367382
"""
368383
Run a [bold]FastAPI[/bold] app in [yellow]development[/yellow] mode. 🧪
@@ -402,6 +417,7 @@ def dev(
402417
proxy_headers=proxy_headers,
403418
forwarded_allow_ips=forwarded_allow_ips,
404419
public_url=os.getenv("FASTAPI_PUBLIC_URL"),
420+
verbose=verbose,
405421
)
406422

407423

@@ -471,6 +487,14 @@ def run(
471487
help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything."
472488
),
473489
] = None,
490+
verbose: Annotated[
491+
bool,
492+
typer.Option(
493+
"--verbose",
494+
"-v",
495+
help="Show detailed startup output: how the [bold]FastAPI[/bold] app was discovered, imported, and configured.",
496+
),
497+
] = False,
474498
) -> Any:
475499
"""
476500
Run a [bold]FastAPI[/bold] app in [green]production[/green] mode. 🚀
@@ -510,6 +534,7 @@ def run(
510534
proxy_headers=proxy_headers,
511535
forwarded_allow_ips=forwarded_allow_ips,
512536
public_url=os.getenv("FASTAPI_PUBLIC_URL"),
537+
verbose=verbose,
513538
)
514539

515540

0 commit comments

Comments
 (0)