From 0b1d97c80cc399dbfb79aabff6711fedcb029b57 Mon Sep 17 00:00:00 2001 From: PakitoSec Date: Sun, 4 Jan 2026 20:46:25 +0100 Subject: [PATCH] feat: add width parameter to logger.rich and update related functionality --- README.md | 8 +++++ examples/base.py | 3 +- src/logurich/__init__.py | 2 ++ src/logurich/console.py | 34 ++++++++++++++------ src/logurich/core.py | 17 ++++++---- src/logurich/core.pyi | 1 + src/logurich/handler.py | 9 ++++-- src/logurich/utils.py | 4 ++- tests/test_rich.py | 67 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 125 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 169bd6b..701677a 100644 --- a/README.md +++ b/README.md @@ -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") diff --git a/examples/base.py b/examples/base.py index fd403ae..345fd7d 100644 --- a/examples/base.py +++ b/examples/base.py @@ -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) @@ -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) diff --git a/src/logurich/__init__.py b/src/logurich/__init__.py index 8528863..1476daa 100644 --- a/src/logurich/__init__.py +++ b/src/logurich/__init__.py @@ -13,6 +13,7 @@ LOG_LEVEL_CHOICES, LogLevel, LoguRich, + ctx, global_context_configure, global_context_set, init_logger, @@ -23,6 +24,7 @@ __all__ = [ "init_logger", "logger", + "ctx", "global_context_configure", "global_context_set", "propagate_loguru_to_std_logger", diff --git a/src/logurich/console.py b/src/logurich/console.py index 6f431f7..3183f04 100644 --- a/src/logurich/console.py +++ b/src/logurich/console.py @@ -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: @@ -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: @@ -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 diff --git a/src/logurich/core.py b/src/logurich/core.py index 8226c5f..f380325 100644 --- a/src/logurich/core.py +++ b/src/logurich/core.py @@ -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) @@ -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( @@ -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 diff --git a/src/logurich/core.pyi b/src/logurich/core.pyi index dbb13f0..c1568d7 100644 --- a/src/logurich/core.pyi +++ b/src/logurich/core.pyi @@ -47,6 +47,7 @@ class LoguRich(_Logger): title: str = "", prefix: bool = True, end: str = "\n", + width: Optional[int] = None, ) -> None: ... logger: LoguRich diff --git a/src/logurich/handler.py b/src/logurich/handler.py index eec3a8f..04b8e0d 100644 --- a/src/logurich/handler.py +++ b/src/logurich/handler.py @@ -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: @@ -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) diff --git a/src/logurich/utils.py b/src/logurich/utils.py index 0f8ac22..0b517fb 100644 --- a/src/logurich/utils.py +++ b/src/logurich/utils.py @@ -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 diff --git a/tests/test_rich.py b/tests/test_rich.py index 266616b..13d484c 100644 --- a/tests/test_rich.py +++ b/tests/test_rich.py @@ -4,6 +4,7 @@ import pytest from rich.pretty import Pretty +from rich.table import Table def generate_random_dict(k, depth=3): @@ -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 + )