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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ logger.rich(
prefix=False,
)

# Rich object with custom console width
logger.rich(
"INFO",
Panel("Rich Panel with custom width", border_style="blue"),
title="Custom Width",
width=80,
)

# Temporarily raise the minimum level
logger.level_set("WARNING")
logger.info("filtered")
Expand Down
3 changes: 2 additions & 1 deletion examples/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


def create_rich_table():
table = Table(title="Sample Table")
table = Table(title="Sample Table", expand=True)
columns = [f"Column {i + 1}" for i in range(5)]
for col in columns:
table.add_column(col)
Expand Down Expand Up @@ -79,3 +79,4 @@ 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)
2 changes: 2 additions & 0 deletions src/logurich/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
LOG_LEVEL_CHOICES,
LogLevel,
LoguRich,
ctx,
global_context_configure,
global_context_set,
init_logger,
Expand All @@ -23,6 +24,7 @@
__all__ = [
"init_logger",
"logger",
"ctx",
"global_context_configure",
"global_context_set",
"propagate_loguru_to_std_logger",
Expand Down
34 changes: 24 additions & 10 deletions src/logurich/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,32 @@

from __future__ import annotations

from typing import Any
from typing import Any, Optional

from rich.console import Console, ConsoleRenderable
from rich.pretty import Pretty
from rich.table import Table
from rich.text import Text

_console = None
_console: Optional[Console] = None


def rich_to_str(*objects, ansi: bool = True, **kwargs) -> str:
def rich_to_str(
*objects: Any, ansi: bool = True, width: Optional[int] = None, **kwargs: Any
) -> str:
console = rich_get_console()
with console.capture() as capture:
console.print(*objects, **kwargs)
if ansi is True:
return capture.get()
return str(Text.from_ansi(capture.get()))
original_width = console.width
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:
Expand All @@ -34,12 +43,13 @@ def rich_format_grid(prefix: Text, data: ConsoleRenderable, real_width: int) ->


def rich_console_renderer(
prefix: str, rich_format: bool, data: Any
prefix: str, rich_format: bool, data: Any, width: Optional[int] = None
) -> list[ConsoleRenderable]:
console = rich_get_console()
rich_prefix = prefix[:-2] + "# "
pp = Text.from_markup(rich_prefix)
real_width = console.width - len(pp)
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:
Expand All @@ -62,6 +72,10 @@ def rich_console_renderer(
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
Expand Down
17 changes: 11 additions & 6 deletions src/logurich/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ def _rich_logger(
title: str = "",
prefix: bool = True,
end: str = "\n",
width: Optional[int] = None,
):
self.opt(depth=1).bind(rich_console=renderables, rich_format=prefix, end=end).log(
log_level, title
)
self.opt(depth=1).bind(
rich_console=renderables, rich_format=prefix, end=end, rich_width=width
).log(log_level, title)


_Logger.rich = partialmethod(_rich_logger)
Expand Down Expand Up @@ -246,14 +247,18 @@ def format_file(self, record: dict):
end = record["extra"].get("end", "\n")
prefix = str(Text.from_markup(record["extra"].pop("_prefix")))
rich_console = record["extra"].pop("rich_console", [])
rich_width = record["extra"].pop("rich_width", None)
list_context = record["extra"].pop("_build_list_context", [])
record["message"] = str(Text.from_markup(record["message"]))
rich_data = ""
if rich_console:
renderables = rich_console_renderer(
prefix, record["extra"].get("rich_format", True), rich_console
prefix,
record["extra"].get("rich_format", True),
rich_console,
rich_width,
)
rich_data = str(rich_to_str(*renderables, ansi=False))
rich_data = str(rich_to_str(*renderables, ansi=False, width=rich_width))
rich_data = rich_data.replace("{", " {{").replace("}", "}}")
record["message"] += "\n" + rich_data
context = str(
Expand Down Expand Up @@ -328,7 +333,7 @@ def _conf_level_by_module(conf: dict):
if levelno_ < 0:
raise ValueError(
f"The filter dict contains a module '{module}' associated to an invalid level, "
"it should be a positive interger, not: '{levelno_}'"
f"it should be a positive integer, not: '{levelno_}'"
)
level_per_module[module] = levelno_
return level_per_module
Expand Down
1 change: 1 addition & 0 deletions src/logurich/core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class LoguRich(_Logger):
title: str = "",
prefix: bool = True,
end: str = "\n",
width: Optional[int] = None,
) -> None: ...

logger: LoguRich
Expand Down
9 changes: 7 additions & 2 deletions src/logurich/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def emit(self, record: LogRecord) -> None:
list_context: list[str] = record.extra.get("_build_list_context", [])
rich_console = record.extra.get("rich_console")
rich_format = record.extra.get("rich_format")
rich_width = record.extra.get("rich_width")

try:
if record.msg:
Expand All @@ -192,7 +193,11 @@ def emit(self, record: LogRecord) -> None:
output_text.append_text(message_text)
self._console.print(output_text, end=end, highlight=False)
if rich_console:
renderable = rich_console_renderer(prefix, rich_format, rich_console)
self._console.print(*renderable, end=end, highlight=False)
renderable = rich_console_renderer(
prefix, rich_format, rich_console, rich_width
)
self._console.print(
*renderable, end=end, highlight=False, width=rich_width
)
except Exception:
self.handleError(record)
4 changes: 3 additions & 1 deletion src/logurich/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ def parse_bool_env(name: str) -> Optional[bool]:
normalized = value.strip().lower()
if normalized in {"1", "true", "yes", "on"}:
return True
return normalized not in {"0", "false", "no", "off", ""}
if normalized in {"0", "false", "no", "off", ""}:
return False
return None
67 changes: 67 additions & 0 deletions tests/test_rich.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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


def generate_random_dict(k, depth=3):
Expand Down Expand Up @@ -34,3 +35,69 @@ def test_rich(logger, buffer):
lines = buffer.getvalue().splitlines()
for line in lines:
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

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

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

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


@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
)