Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/fastapi_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}
Expand Down Expand Up @@ -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,
Expand All @@ -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,
)


Expand Down
6 changes: 6 additions & 0 deletions src/fastapi_cli/utils/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import sys
from typing import Any

from rich_toolkit import RichToolkit, RichToolkitTheme
Expand All @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"):
Expand Down
18 changes: 16 additions & 2 deletions tests/test_utils_cli.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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()

Expand Down
Loading