diff --git a/src/fastapi_cli/cli.py b/src/fastapi_cli/cli.py index a592d996..f3983fcd 100644 --- a/src/fastapi_cli/cli.py +++ b/src/fastapi_cli/cli.py @@ -20,7 +20,11 @@ from . import __version__ from .logging import setup_logging -from .utils.cli import get_rich_toolkit, get_uvicorn_log_config +from .utils.cli import ( + get_rich_toolkit, + get_uvicorn_log_config, + should_use_rich_logs, +) app = typer.Typer( rich_markup_mode="rich", context_settings={"help_option_names": ["-h", "--help"]} @@ -271,6 +275,10 @@ def _run( toolkit.print("Logs:") toolkit.print_line() + extra_uvicorn_kwargs: dict[str, Any] = ( + {"log_config": get_uvicorn_log_config()} if should_use_rich_logs() else {} + ) + uvicorn.run( app=import_string, host=host, @@ -285,7 +293,7 @@ def _run( root_path=root_path, proxy_headers=proxy_headers, forwarded_allow_ips=forwarded_allow_ips, - log_config=get_uvicorn_log_config(), + **extra_uvicorn_kwargs, ) diff --git a/src/fastapi_cli/utils/cli.py b/src/fastapi_cli/utils/cli.py index 8a02d13b..abc02a46 100644 --- a/src/fastapi_cli/utils/cli.py +++ b/src/fastapi_cli/utils/cli.py @@ -1,4 +1,5 @@ import logging +import sys from typing import Any from rich_toolkit import RichToolkit, RichToolkitTheme @@ -20,6 +21,11 @@ def formatMessage(self, record: logging.LogRecord) -> str: return result +def should_use_rich_logs() -> bool: + """Return True when stdout is a TTY and rich logs should be used, False otherwise.""" + return sys.stdout.isatty() + + def get_uvicorn_log_config() -> dict[str, Any]: return { "version": 1, diff --git a/tests/test_cli.py b/tests/test_cli.py index c29b1513..4acdfc5c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -16,6 +16,11 @@ assets_path = Path(__file__).parent / "assets" +@pytest.fixture(autouse=True) +def force_rich_logs(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("fastapi_cli.cli.should_use_rich_logs", lambda: True) + + def test_dev() -> None: with changing_dir(assets_path): with patch.object(uvicorn, "run") as mock_run: @@ -51,6 +56,21 @@ def test_dev() -> None: assert "🐍 single_file_app.py" in result.output +def test_run_uses_uvicorn_default_log_config_without_rich_logs( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr("fastapi_cli.cli.should_use_rich_logs", lambda: False) + + with changing_dir(assets_path): + with patch.object(uvicorn, "run") as mock_run: + result = runner.invoke(app, ["run", "single_file_app.py"]) + assert result.exit_code == 0, result.output + assert mock_run.called + assert mock_run.call_args + + assert "log_config" not in mock_run.call_args.kwargs + + def test_dev_no_args_auto_discovery() -> None: """Test that auto-discovery works when no args and no pyproject.toml entrypoint""" with changing_dir(assets_path / "default_files" / "default_main"): diff --git a/tests/test_utils_cli.py b/tests/test_utils_cli.py index e2cdf3ec..f10c7891 100644 --- a/tests/test_utils_cli.py +++ b/tests/test_utils_cli.py @@ -1,9 +1,15 @@ +import io import logging +import sys from logging.config import dictConfig -from pytest import LogCaptureFixture +from pytest import LogCaptureFixture, MonkeyPatch -from fastapi_cli.utils.cli import CustomFormatter, get_uvicorn_log_config +from fastapi_cli.utils.cli import ( + CustomFormatter, + get_uvicorn_log_config, + should_use_rich_logs, +) def test_get_uvicorn_config_uses_custom_formatter() -> None: @@ -14,6 +20,14 @@ def test_get_uvicorn_config_uses_custom_formatter() -> None: assert config["loggers"]["uvicorn"]["propagate"] is False +def test_should_use_rich_logs_is_false_without_tty( + monkeypatch: MonkeyPatch, +) -> None: + monkeypatch.setattr(sys, "stdout", io.StringIO()) + + assert should_use_rich_logs() is False + + def test_custom_formatter() -> None: formatter = CustomFormatter()