Skip to content
Closed
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dependencies = [
"uvicorn[standard] >= 0.17.6",
"rignore >= 0.5.1",
"httpx >= 0.27.0",
"rich-toolkit >= 0.19.7",
"rich-toolkit>=0.19.9",
"pydantic[email] >= 2.7.4; python_version < '3.13'",
"pydantic[email] >= 2.8.0; python_version == '3.13'",
"pydantic[email] >= 2.12.0; python_version >= '3.14'",
Expand Down
4 changes: 2 additions & 2 deletions src/fastapi_cloud_cli/commands/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import typer
from httpx import Client
from pydantic import AfterValidator, BaseModel, EmailStr, TypeAdapter, ValidationError
from rich.text import Text
from rich_toolkit import RichToolkit
from rich_toolkit.menu import Option
from rich_toolkit.progress import Progress
Expand All @@ -31,6 +30,7 @@
from fastapi_cloud_cli.utils.auth import Identity
from fastapi_cloud_cli.utils.cli import get_rich_toolkit
from fastapi_cloud_cli.utils.progress_file import ProgressFile
from fastapi_cloud_cli.utils.rich_ansi import text_from_ansi

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -518,7 +518,7 @@ def _wait_for_deployment(
time_elapsed = time.monotonic() - started_at

if log.type == "message":
progress.log(Text.from_ansi(log.message.rstrip())) # ty: ignore[unresolved-attribute]
progress.log(text_from_ansi(log.message)) # ty: ignore[unresolved-attribute]
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rich's Text.from_ansi in 14 strips new lines, in version 15 it is fine, but I don't to force either version, so we have our own tiny function that handles the newlines as we expect


if log.type == "complete":
build_complete = True
Expand Down
19 changes: 19 additions & 0 deletions src/fastapi_cloud_cli/utils/rich_ansi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import re

from rich.ansi import AnsiDecoder
from rich.text import Text

ANSI_NEWLINE_SPLIT_RE = re.compile(r"(?<=\n)")


def text_from_ansi(text: str) -> Text:
# Text.from_ansi only preserves newlines in Rich 15, in order to
# avoid forcing everyone to use Rich 15 we handle this ourselves by splitting the text on newlines,
# decoding each line separately, then join them back together with newlines.
joiner = Text("\n")
decoder = AnsiDecoder()

return joiner.join(
decoder.decode_line(line.rstrip("\n"))
for line in ANSI_NEWLINE_SPLIT_RE.split(text)
)
20 changes: 20 additions & 0 deletions tests/test_rich_ansi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from fastapi_cloud_cli.utils.rich_ansi import text_from_ansi


def test_text_from_ansi_preserves_newlines() -> None:
assert text_from_ansi("").plain == ""
assert text_from_ansi("\n").plain == "\n"
assert text_from_ansi("\n\n").plain == "\n\n"
assert text_from_ansi("Hello").plain == "Hello"
assert text_from_ansi("Hello\n").plain == "Hello\n"
assert text_from_ansi("Hello\n\n").plain == "Hello\n\n"
assert text_from_ansi("Hello\n World").plain == "Hello\n World"
assert text_from_ansi("Hello\n\n World").plain == "Hello\n\n World"
assert text_from_ansi("Hello\n World\n").plain == "Hello\n World\n"


def test_text_from_ansi_handles_carriage_returns_per_line() -> None:
assert text_from_ansi("start\rend").plain == "end"
assert text_from_ansi("start\rend\n").plain == "end\n"
assert text_from_ansi("one\rtwo\nthree").plain == "two\nthree"
assert text_from_ansi("one\ntwo\rthree\n").plain == "one\nthree\n"
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading