Skip to content
Merged
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
9 changes: 7 additions & 2 deletions examples/base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from rich.panel import Panel
from rich.table import Table
from rich.text import Text

from logurich import global_context_configure, init_logger, logger


def create_rich_table():
table = Table(title="Sample Table", expand=True)
table = Table(title="Sample Table")
columns = [f"Column {i + 1}" for i in range(5)]
for col in columns:
table.add_column(col)
Expand Down Expand Up @@ -79,4 +80,8 @@ def create_rich_table():
t = create_rich_table()
logger.rich("INFO", t, title="test")
logger.rich("INFO", t, title="test", prefix=False)
logger.rich("INFO", t, title="test", width=100)
logger.rich("INFO", t, title="[bold red]test width=50[/bold red]", width=50)
logger.rich("INFO", Text("-" * 10), title="[bold red]test width=10[/bold red]", width=10)
logger.rich("INFO", Text("-" * 10), title="[bold red]test width=5[/bold red]", width=5)
logger.rich("INFO", "-" * 10, title="[bold red]test width=10[/bold red]", width=10)
logger.rich("INFO", "-" * 10, title="[bold red]test width=5[/bold red]", width=5)
7 changes: 6 additions & 1 deletion src/logurich/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ def propagate_loguru_to_std_logger() -> None: ...
def rich_configure_console(*args: object, **kwargs: object) -> Console: ...
def rich_get_console() -> Console: ...
def rich_set_console(console: Console) -> None: ...
def rich_to_str(*objects: object, ansi: bool = True, **kwargs: object) -> str: ...
def rich_to_str(
*objects: object,
ansi: bool = True,
width: Optional[int] = None,
**kwargs: object,
) -> str: ...

__all__: Final[list[str]]
127 changes: 75 additions & 52 deletions src/logurich/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,69 +16,92 @@ def rich_to_str(
*objects: Any, ansi: bool = True, width: Optional[int] = None, **kwargs: Any
) -> str:
console = rich_get_console()
original_width = console.width
if width is not None and width < 1:
raise ValueError("width must be >= 1")
print_kwargs = dict(kwargs)
if width is not None:
console.width = width
try:
with console.capture() as capture:
console.print(*objects, **kwargs)
if ansi is True:
return capture.get()
return str(Text.from_ansi(capture.get()))
finally:
if width is not None:
console.width = original_width


def rich_format_grid(prefix: Text, data: ConsoleRenderable, real_width: int) -> Table:
print_kwargs["width"] = width
print_kwargs["no_wrap"] = True
print_kwargs["overflow"] = "ellipsis"
with console.capture() as capture:
console.print(*objects, **print_kwargs)
output = capture.get()
if ansi:
return output
return Text.from_ansi(output).plain


def rich_format_grid(
text_rich_prefix: Text, data: ConsoleRenderable, content_width: Optional[int]
) -> Table:
grid = Table.grid()
grid.add_column()
grid.add_column()
content = Text.from_ansi(rich_to_str(data, width=real_width, end=""))
grid.add_column(no_wrap=True)
grid.add_column(no_wrap=True)
content = Text.from_ansi(rich_to_str(data, width=content_width, end=""))
lines = content.split()
for line in lines:
pline = prefix.copy()
grid.add_row(pline, line)
grid.add_row(text_rich_prefix.copy(), line)
return grid


def _render_rich_item(
item: Any,
text_rich_prefix: Text,
available_width: int,
effective_width: int,
) -> ConsoleRenderable:
"""Render a single item with rich markup formatting."""
if isinstance(item, str):
content = Text.from_markup(item)
if available_width != effective_width:
return rich_format_grid(text_rich_prefix, content, effective_width)
return text_rich_prefix.copy().append(content)

if isinstance(item, ConsoleRenderable):
return rich_format_grid(text_rich_prefix, item, effective_width)

return rich_format_grid(
text_rich_prefix,
Pretty(item, max_depth=2, max_length=2),
effective_width,
)


def _render_plain_item(
item: Any,
effective_width: int,
content_width: Optional[int],
) -> ConsoleRenderable:
"""Render a single item without rich markup formatting."""
if isinstance(item, str):
return Text.from_ansi(item)

if content_width is not None:
rendered = rich_to_str(item, width=effective_width, end="")
return Text.from_ansi(rendered)

return item


def rich_console_renderer(
prefix: str, rich_format: bool, data: Any, width: Optional[int] = None
prefix: str, rich_format: bool, data: Any, content_width: Optional[int] = None
) -> list[ConsoleRenderable]:
console = rich_get_console()
rich_prefix = prefix[:-2] + "# "
pp = Text.from_markup(rich_prefix)
effective_width = width if width is not None else console.width
real_width = max(1, effective_width - len(pp))
renderable = []
for r in data:
if rich_format:
if isinstance(r, str):
item = pp.copy()
item = item.append(Text.from_markup(r))
renderable.append(item)
elif isinstance(r, Text) and len(r.split()) == 1:
item = pp.copy()
item = item.append_text(r)
renderable.append(item)
elif isinstance(r, ConsoleRenderable):
renderable.append(rich_format_grid(pp, r, real_width))
else:
renderable.append(
rich_format_grid(
pp, Pretty(r, max_depth=2, max_length=2), real_width
)
)
else:
if isinstance(r, str):
renderable.append(Text.from_ansi(r))
elif width is not None:
# Re-render with specified width
rendered = rich_to_str(r, width=width, end="")
renderable.append(Text.from_ansi(rendered))
else:
renderable.append(r)
return renderable
text_rich_prefix = Text.from_markup(rich_prefix)
available_width = max(1, console.width - len(text_rich_prefix))
effective_width = (
min(available_width, content_width)
if content_width is not None
else available_width
)

if rich_format:
return [
_render_rich_item(r, text_rich_prefix, available_width, effective_width)
for r in data
]
return [_render_plain_item(r, effective_width, content_width) for r in data]


def rich_set_console(console: Console) -> None:
Expand Down
4 changes: 1 addition & 3 deletions src/logurich/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,6 @@ def emit(self, record: LogRecord) -> None:
renderable = rich_console_renderer(
prefix, rich_format, rich_console, rich_width
)
self._console.print(
*renderable, end=end, highlight=False, width=rich_width
)
self._console.print(*renderable, end=end, highlight=False)
except Exception:
self.handleError(record)
56 changes: 8 additions & 48 deletions tests/test_rich.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import pytest
from rich.pretty import Pretty
from rich.table import Table


def generate_random_dict(k, depth=3):
Expand Down Expand Up @@ -37,67 +36,28 @@ def test_rich(logger, buffer):
assert re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d+ \| INFO", line)


@pytest.mark.parametrize(
"logger",
[{"level": "DEBUG", "enqueue": False}],
indirect=True,
)
def test_rich_with_width_parameter(logger, buffer):
"""Test that the width parameter limits the console output width."""
table = Table(title="Test Table")
table.add_column("Column A", style="cyan")
table.add_column("Column B", style="magenta")
table.add_row("Short", "Data")
table.add_row("Another", "Row")

logger.rich("INFO", table, width=80)
output = buffer.getvalue()
lines = output.splitlines()
# Verify output was produced
assert len(lines) > 0
# Verify it matches the expected log format
assert any(
re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d+ \| INFO", line)
for line in lines
)


@pytest.mark.parametrize(
"logger",
[{"level": "DEBUG", "enqueue": False}],
indirect=True,
)
def test_rich_width_affects_output(logger, buffer):
"""Test that different width values produce different output layouts."""
wide_text = "A" * 100 # Long text that will wrap differently at different widths
wide_text = " ".join(["wrap"] * 60)

# First call with narrow width
logger.rich("INFO", wide_text, width=50)
logger.rich("INFO", wide_text, width=60)
narrow_output = buffer.getvalue()

# Clear buffer and call with wide width
buffer.truncate(0)
buffer.seek(0)
logger.rich("INFO", wide_text, width=200)
logger.rich("INFO", wide_text, width=110)
wide_output = buffer.getvalue()

# Both should have content
assert len(narrow_output) > 0
assert len(wide_output) > 0

narrow_max = len(narrow_output)
wide_max = len(wide_output)

@pytest.mark.parametrize(
"logger",
[{"level": "DEBUG", "enqueue": False}],
indirect=True,
)
def test_rich_without_width_uses_default(logger, buffer):
"""Test that omitting width parameter uses default console width."""
logger.rich("INFO", "Simple text without width parameter")
output = buffer.getvalue()
lines = output.splitlines()
assert len(lines) > 0
assert any(
re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d+ \| INFO", line)
for line in lines
)
assert narrow_max > 0
assert wide_max > 0
assert narrow_max < wide_max