diff --git a/examples/base.py b/examples/base.py index 345fd7d..e6ffec0 100644 --- a/examples/base.py +++ b/examples/base.py @@ -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) @@ -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) diff --git a/src/logurich/__init__.pyi b/src/logurich/__init__.pyi index 0f93e08..0ceb257 100644 --- a/src/logurich/__init__.pyi +++ b/src/logurich/__init__.pyi @@ -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]] diff --git a/src/logurich/console.py b/src/logurich/console.py index 3183f04..f411dcf 100644 --- a/src/logurich/console.py +++ b/src/logurich/console.py @@ -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: diff --git a/src/logurich/handler.py b/src/logurich/handler.py index 04b8e0d..a32b36a 100644 --- a/src/logurich/handler.py +++ b/src/logurich/handler.py @@ -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) diff --git a/tests/test_rich.py b/tests/test_rich.py index 13d484c..66134f1 100644 --- a/tests/test_rich.py +++ b/tests/test_rich.py @@ -4,7 +4,6 @@ import pytest from rich.pretty import Pretty -from rich.table import Table def generate_random_dict(k, depth=3): @@ -37,31 +36,6 @@ 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}], @@ -69,35 +43,21 @@ def test_rich_with_width_parameter(logger, buffer): ) 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