Skip to content

Commit

Permalink
feat: only print when terminal is TTY enabled (#3219)
Browse files Browse the repository at this point in the history
  • Loading branch information
cofin authored and provinzkraut committed Mar 18, 2024
1 parent 615da3e commit c1c9f53
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 20 deletions.
25 changes: 19 additions & 6 deletions litestar/cli/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ def from_env(cls, app_path: str | None, app_dir: Path | None = None) -> Litestar
if app_path and getenv("LITESTAR_APP") is None:
os.environ["LITESTAR_APP"] = app_path
if app_path:
if not quiet_console:
console.print(f"Using {app_name} from env: [bright_blue]{app_path!r}")
if not quiet_console and isatty():
console.print(f"Using {app_name} app from env: [bright_blue]{app_path!r}")
loaded_app = _load_app_from_path(app_path)
else:
loaded_app = _autodiscover_app(cwd)
Expand Down Expand Up @@ -303,6 +303,8 @@ def _autodiscovery_paths(base_dir: Path, arbitrary: bool = True) -> Generator[Pa


def _autodiscover_app(cwd: Path) -> LoadedApp:
app_name = getenv("LITESTAR_APP_NAME") or "Litestar"
quiet_console = getenv("LITESTAR_QUIET_CONSOLE") or False
for file_path in _autodiscovery_paths(cwd):
import_path = _path_to_dotted_path(file_path.relative_to(cwd))
module = importlib.import_module(import_path)
Expand All @@ -314,13 +316,15 @@ def _autodiscover_app(cwd: Path) -> LoadedApp:
if isinstance(value, Litestar):
app_string = f"{import_path}:{attr}"
os.environ["LITESTAR_APP"] = app_string
console.print(f"Using Litestar app from [bright_blue]{app_string}")
if not quiet_console and isatty():
console.print(f"Using {app_name} app from [bright_blue]{app_string}")
return LoadedApp(app=value, app_path=app_string, is_factory=False)

if hasattr(module, "create_app"):
app_string = f"{import_path}:create_app"
os.environ["LITESTAR_APP"] = app_string
console.print(f"Using Litestar factory [bright_blue]{app_string}")
if not quiet_console and isatty():
console.print(f"Using {app_name} factory from [bright_blue]{app_string}")
return LoadedApp(app=module.create_app(), app_path=app_string, is_factory=True)

for attr, value in module.__dict__.items():
Expand All @@ -334,10 +338,11 @@ def _autodiscover_app(cwd: Path) -> LoadedApp:
if return_annotation in ("Litestar", Litestar):
app_string = f"{import_path}:{attr}"
os.environ["LITESTAR_APP"] = app_string
console.print(f"Using Litestar factory [bright_blue]{app_string}")
if not quiet_console and sys.stdout.isatty():
console.print(f"Using {app_name} factory from [bright_blue]{app_string}")

Check warning on line 342 in litestar/cli/_utils.py

View check run for this annotation

Codecov / codecov/patch

litestar/cli/_utils.py#L342

Added line #L342 was not covered by tests
return LoadedApp(app=value(), app_path=f"{app_string}", is_factory=True)

raise LitestarCLIException("Could not find a Litestar app or factory")
raise LitestarCLIException(f"Could not find {app_name} instance or factory")


def _format_is_enabled(value: Any) -> str:
Expand Down Expand Up @@ -544,3 +549,11 @@ def remove_default_schema_routes(
else openapi_config.openapi_controller.path
)
return remove_routes_with_patterns(routes, (schema_path,))


def isatty() -> bool:
"""Detect if a terminal is TTY enabled.
This is a convenience wrapper around the built in system methods. This allows for easier testing of TTY/non-TTY modes.
"""
return sys.stdout.isatty()
3 changes: 2 additions & 1 deletion litestar/cli/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
LitestarEnv,
console,
create_ssl_files,
isatty,
remove_default_schema_routes,
remove_routes_with_patterns,
show_app_info,
Expand Down Expand Up @@ -253,7 +254,7 @@ def run_command(
else validate_ssl_file_paths(ssl_certfile, ssl_keyfile)
)

if not quiet_console:
if not quiet_console and isatty():
console.rule("[yellow]Starting server process", align="left")
show_app_info(app)
with _server_lifespan(app):
Expand Down
84 changes: 71 additions & 13 deletions tests/unit/test_cli/test_core_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from litestar import __version__ as litestar_version
from litestar.cli import _utils
from litestar.cli.commands import core
from litestar.cli.main import litestar_group as cli_command
from litestar.exceptions import LitestarWarning

Expand Down Expand Up @@ -57,8 +58,11 @@ def mock_show_app_info(mocker: MockerFixture) -> MagicMock:
(False, None, None, None, 2),
],
)
@pytest.mark.parametrize("tty_enabled", [True, False])
@pytest.mark.parametrize("quiet_console", [True, False])
def test_run_command(
mock_show_app_info: MagicMock,
mocker: MockerFixture,
runner: CliRunner,
monkeypatch: MonkeyPatch,
reload: Optional[bool],
Expand All @@ -74,10 +78,17 @@ def test_run_command(
custom_app_file: Optional[Path],
create_app_file: CreateAppFileFixture,
set_in_env: bool,
tty_enabled: bool,
quiet_console: bool,
mock_subprocess_run: MagicMock,
mock_uvicorn_run: MagicMock,
tmp_project_dir: Path,
) -> None:
monkeypatch.delenv("LITESTAR_QUIET_CONSOLE", raising=False)
if quiet_console:
monkeypatch.setenv("LITESTAR_QUIET_CONSOLE", "true")
mocker.patch.object(core, "isatty", return_value=tty_enabled)
mocker.patch.object(_utils, "isatty", return_value=tty_enabled)
args = []
if custom_app_file:
args.extend(["--app", f"{custom_app_file.stem}:app"])
Expand Down Expand Up @@ -194,9 +205,14 @@ def test_run_command(
ssl_keyfile=None,
)

mock_show_app_info.assert_called_once()
if tty_enabled and not quiet_console:
mock_show_app_info.assert_called_once()
else:
mock_show_app_info.assert_not_called()


@pytest.mark.parametrize("quiet_console", [True, False])
@pytest.mark.parametrize("tty_enabled", [True, False])
@pytest.mark.parametrize(
"file_name,file_content,factory_name",
[
Expand All @@ -213,12 +229,20 @@ def test_run_command_with_autodiscover_app_factory(
file_content: str,
factory_name: str,
patch_autodiscovery_paths: Callable[[List[str]], None],
tty_enabled: bool,
quiet_console: bool,
create_app_file: CreateAppFileFixture,
mocker: MockerFixture,
monkeypatch: MonkeyPatch,
) -> None:
monkeypatch.delenv("LITESTAR_QUIET_CONSOLE", raising=False)
if quiet_console:
monkeypatch.setenv("LITESTAR_QUIET_CONSOLE", "true")
mocker.patch.object(core, "isatty", return_value=tty_enabled)
mocker.patch.object(_utils, "isatty", return_value=tty_enabled)
patch_autodiscovery_paths([file_name])
path = create_app_file(file_name, content=file_content)
result = runner.invoke(cli_command, "run")

assert result.exception is None
assert result.exit_code == 0

Expand All @@ -232,11 +256,28 @@ def test_run_command_with_autodiscover_app_factory(
ssl_certfile=None,
ssl_keyfile=None,
)
if tty_enabled and not quiet_console:
assert len(result.output) > 0
else:
assert len(result.output) == 0


@pytest.mark.parametrize("quiet_console", [True, False])
@pytest.mark.parametrize("tty_enabled", [True, False])
def test_run_command_with_app_factory(
runner: CliRunner, mock_uvicorn_run: MagicMock, create_app_file: CreateAppFileFixture
runner: CliRunner,
mock_uvicorn_run: MagicMock,
create_app_file: CreateAppFileFixture,
tty_enabled: bool,
quiet_console: bool,
mocker: MockerFixture,
monkeypatch: MonkeyPatch,
) -> None:
monkeypatch.delenv("LITESTAR_QUIET_CONSOLE", raising=False)
if quiet_console:
monkeypatch.setenv("LITESTAR_QUIET_CONSOLE", "true")
mocker.patch.object(core, "isatty", return_value=tty_enabled)
mocker.patch.object(_utils, "isatty", return_value=tty_enabled)
path = create_app_file("_create_app_with_path.py", content=CREATE_APP_FILE_CONTENT)
app_path = f"{path.stem}:create_app"
result = runner.invoke(cli_command, ["--app", app_path, "run"])
Expand All @@ -254,6 +295,10 @@ def test_run_command_with_app_factory(
ssl_certfile=None,
ssl_keyfile=None,
)
if tty_enabled and not quiet_console:
assert len(result.output) > 0
else:
assert len(result.output) == 0


@pytest.mark.parametrize(
Expand Down Expand Up @@ -390,9 +435,15 @@ def test_run_command_debug(

@pytest.mark.usefixtures("mock_uvicorn_run", "unset_env")
def test_run_command_quiet_console(
app_file: Path, runner: CliRunner, monkeypatch: MonkeyPatch, create_app_file: CreateAppFileFixture
app_file: Path,
mocker: MockerFixture,
runner: CliRunner,
monkeypatch: MonkeyPatch,
create_app_file: CreateAppFileFixture,
) -> None:
console = Console(file=io.StringIO())
mocker.patch.object(core, "isatty", return_value=True)
mocker.patch.object(_utils, "isatty", return_value=True)
console = Console(file=io.StringIO(), force_interactive=True)
monkeypatch.setattr(_utils, "console", console)

path = create_app_file("_create_app_with_path.py", content=CREATE_APP_FILE_CONTENT)
Expand All @@ -401,26 +452,33 @@ def test_run_command_quiet_console(
result = runner.invoke(cli_command, ["--app", app_path, "run"])
assert result.exit_code == 0
normal_output = console.file.getvalue() # type: ignore[attr-defined]
assert "Using Litestar from env:" in normal_output
assert "Using Litestar app from env:" in normal_output
assert "Starting server process" in result.stdout
del result
console = Console(file=io.StringIO())
console = Console(file=io.StringIO(), force_interactive=True)
monkeypatch.setattr(_utils, "console", console)
monkeypatch.setenv("LITESTAR_QUIET_CONSOLE", "1")
assert os.getenv("LITESTAR_QUIET_CONSOLE") == "1"
result = runner.invoke(cli_command, ["--app", app_path, "run"])
assert result.exit_code == 0
quiet_output = console.file.getvalue() # type: ignore[attr-defined]
assert "Starting server process" not in result.stdout
assert "Using Litestar from env:" not in quiet_output
assert "Using Litestar app from env:" not in quiet_output
console.clear()


@pytest.mark.usefixtures("mock_uvicorn_run", "unset_env")
def test_run_command_custom_app_name(
app_file: Path, runner: CliRunner, monkeypatch: MonkeyPatch, create_app_file: CreateAppFileFixture
app_file: Path,
runner: CliRunner,
monkeypatch: MonkeyPatch,
create_app_file: CreateAppFileFixture,
mocker: MockerFixture,
) -> None:
console = Console(file=io.StringIO())
mocker.patch.object(core, "isatty", return_value=True)
mocker.patch.object(_utils, "isatty", return_value=True)

console = Console(file=io.StringIO(), force_interactive=True)
monkeypatch.setattr(_utils, "console", console)

path = create_app_file("_create_app_with_path.py", content=CREATE_APP_FILE_CONTENT)
Expand All @@ -429,15 +487,15 @@ def test_run_command_custom_app_name(
result = runner.invoke(cli_command, ["--app", app_path, "run"])
assert result.exit_code == 0
_output = console.file.getvalue() # type: ignore[attr-defined]
assert "Using Litestar from env:" in _output
console = Console(file=io.StringIO())
assert "Using Litestar app from env:" in _output
console = Console(file=io.StringIO(), force_interactive=True)
monkeypatch.setattr(_utils, "console", console)
monkeypatch.setenv("LITESTAR_APP_NAME", "My Stuff")
assert os.getenv("LITESTAR_APP_NAME") == "My Stuff"
result = runner.invoke(cli_command, ["--app", app_path, "run"])
assert result.exit_code == 0
_output = console.file.getvalue() # type: ignore[attr-defined]
assert "Using My Stuff from env:" in _output
assert "Using My Stuff app from env:" in _output


@pytest.mark.usefixtures("mock_uvicorn_run", "unset_env")
Expand Down

0 comments on commit c1c9f53

Please sign in to comment.