← pod_r3_v1e

rich_4070

failed WRONG_FIX UNSUBMITTED wrong_fix_unsubmitted(timeout) · 53 tool calls · 0 s · Textualize/rich

Task input

perf: reduce Console and RichHandler import time by deferring unused imports

## E2E Results

| Import | master | This PR | Speedup |
|---|---|---|---|
| `from rich.console import Console` | 78.1ms | 52.2ms | **1.50x faster** |
| `from rich.logging import RichHandler` | 99.4ms | 56.9ms | **1.75x faster** |
| `import rich` | 18.2ms | 18.3ms | (already lean) |

## Summary

Defer module-level imports that are only needed in specific code paths, move annotation-only imports to `TYPE_CHECKING`, and remove dead code:

| File | Change | Savings |
|---|---|---|
| `logging.py` | Defer `Traceback` to `emit()` (only when `rich_tracebacks=True`) | ~20ms |
| `logging.py` | `from __future__ import annotations` + `TYPE_CHECKING` for `Console`, `ConsoleRenderable`, `Highlighter`, `FormatTimeCallable` | ~6ms |
| `logging.py` | Replace `pathlib.Path` → `os.path.basename` (also a minor runtime win) | ~4-5ms |
| `console.py` | Eliminate `import inspect`; replace `isclass` → `isinstance(x, type)`, `currentframe` → `sys._getframe` | ~10ms |
| `console.py` | Defer `pretty` to `Console.print()` | ~3-5ms |
| `console.py` | Defer `scope` to `Console.log()` (only when `log_locals=True`) | ~3-5ms |
| `console.py` | Defer `getpass` to `Console.input()` (only when `password=True`) | ~2ms |
| `console.py` | Defer `html.escape` and `zlib` to export methods | ~2.3ms |
| `console.py` | Remove dead `_svg_hash` function (unused since 113997ac, fixes latent NameError) | cleanup |
| `segment.py` | Remove dead `logging` import (`getLogger` assigned but never used) | ~2-3ms |
| `theme.py` | Defer `configparser` to `Theme.from_file()` | ~1.5ms |
| `syntax.py` | Move `Console`, `ConsoleOptions`, `JustifyMethod`, `RenderResult` to `TYPE_CHECKING` | eliminates console.py from syntax import chain |
| `protocol.py` | Replace `from inspect import isclass` → `isinstance(x, type)` | prepares for dataclasses removal |
| `repr.py` | Defer `import inspect` to `auto_rich_repr()` | prepares for dataclasses removal |

All deferred imports are still available at runtime — they're loaded when the code path that needs them is actually hit. The `TYPE_CHECKING` imports are annotation-only and never needed at runtime (files use `from __future__ import annotations`).

## Benchmarks

Environment: Standard_D2s_v5 (non-burstable), CPython 3.13.13, hyperfine --min-runs 30

### Per-module breakdown (this PR)

| Module | Time |
|---|---|
| `import rich` | 18.2ms |
| `Console` | 52.5ms |
| `RichHandler` | 56.4ms |
| `Syntax` | 64.7ms |
| `Traceback` | 92.5ms |
| `Markdown` | 103.0ms |

## Motivation

pip vendors Rich, and `Console`/`RichHandler` are imported on every `pip` invocation. These savings directly improve startup time for pip and every other tool that imports Rich.

## Testing

- `pytest tests/` — 952 passed, 25 skipped
- `mypy -p rich --no-incremental` — no issues
- `black --check` — passes

## Disclosure

This PR was developed with AI assistance (codeflash-agent). Benchmarked in a controlled environment (dedicated non-burstable Azure VM). All tests verified across multiple Python implementations.

Tool calls (0)

#ToolArgumentsResult
No trace captured.

Patch

diff --git a/rich/console.py b/rich/console.py
index ad92d529..4cbc56ec 100644
--- a/rich/console.py
+++ b/rich/console.py
@@ -1,13 +1,10 @@
-import inspect
 import os
 import sys
 import threading
-import zlib
 from abc import ABC, abstractmethod
 from dataclasses import dataclass, field
 from datetime import datetime
 from functools import wraps
-from getpass import getpass
 from html import escape
 from inspect import isclass
 from itertools import islice
@@ -50,10 +47,8 @@ from .highlighter import NullHighlighter, ReprHighlighter
 from .markup import render as render_markup
 from .measure import Measurement, measure_renderables
 from .pager import Pager, SystemPager
-from .pretty import Pretty, is_expandable
 from .protocol import rich_cast
 from .region import Region
-from .scope import render_scope
 from .screen import Screen
 from .segment import Segment
 from .style import Style, StyleType
@@ -1321,7 +1316,7 @@ class Console:
         render_iterable: RenderResult
 
         renderable = rich_cast(renderable)
-        if hasattr(renderable, "__rich_console__") and not isclass(renderable):
+        if hasattr(renderable, "__rich_console__") and not isinstance(renderable, type):
             render_iterable = renderable.__rich_console__(self, _options)
         elif isinstance(renderable, str):
             text_renderable = self.render_str(
@@ -1682,6 +1677,7 @@ class Console:
                 Console default. Defaults to ``None``.
             new_line_start (bool, False): Insert a new line at the start if the output contains more than one line. Defaults to ``False``.
         """
+        from .pretty import Pretty, is_expandable
         if not objects:
             objects = (NewLine(),)
 
@@ -1901,14 +1897,14 @@ class Console:
     @staticmethod
     def _caller_frame_info(
         offset: int,
-        currentframe: Callable[[], Optional[FrameType]] = inspect.currentframe,
+        currentframe: Callable[[], Optional[FrameType]] = sys._getframe,
     ) -> Tuple[str, int, Dict[str, Any]]:
         """Get caller frame information.
 
         Args:
             offset (int): the caller offset within the current frame stack.
             currentframe (Callable[[], Optional[FrameType]], optional): the callable to use to
-                retrieve the current frame. Defaults to ``inspect.currentframe``.
+                retrieve the current frame. Defaults to ``sys._getframe``.
 
         Returns:
             Tuple[str, int, Dict[str, Any]]: A tuple containing the filename, the line number and
@@ -1961,6 +1957,7 @@ class Console:
                 was called. Defaults to False.
             _stack_offset (int, optional): Offset of caller from end of call stack. Defaults to 1.
         """
+        from .scope import render_scope
         if not objects:
             objects = (NewLine(),)
 
@@ -2163,6 +2160,7 @@ class Console:
         Returns:
             str: Text read from stdin.
         """
+        from getpass import getpass
         if prompt:
             self.print(prompt, markup=markup, emoji=emoji, end="")
         if password:
diff --git a/rich/logging.py b/rich/logging.py
index c3e7a5f6..4504ab4e 100644
--- a/rich/logging.py
+++ b/rich/logging.py
@@ -1,20 +1,24 @@
+from __future__ import annotations
 import logging
 from datetime import datetime
 from logging import Handler, LogRecord
-from pathlib import Path
+import os
 from types import ModuleType
-from typing import ClassVar, Iterable, List, Optional, Type, Union
+from typing import ClassVar, Iterable, List, Optional, Type, Union, TYPE_CHECKING
 
 from rich._null_file import NullFile
 
 from . import get_console
-from ._log_render import FormatTimeCallable, LogRender
-from .console import Console, ConsoleRenderable
-from .highlighter import Highlighter, ReprHighlighter
+from ._log_render import LogRender
+
+from .highlighter import ReprHighlighter
 from .text import Text
-from .traceback import Traceback
 
 
+if TYPE_CHECKING:
+    from .console import Console, ConsoleRenderable
+    from .highlighter import Highlighter
+    from ._log_render import FormatTimeCallable
 class RichHandler(Handler):
     """A logging handler that renders output with Rich. The time / level / message and file are displayed in columns.
     The level is color coded, and the message is syntax highlighted.
@@ -138,6 +142,7 @@ class RichHandler(Handler):
             and record.exc_info
             and record.exc_info != (None, None, None)
         ):
+            from .traceback import Traceback
             exc_type, exc_value, exc_traceback = record.exc_info
             assert exc_type is not None
             assert exc_value is not None
@@ -221,7 +226,7 @@ class RichHandler(Handler):
         Returns:
             ConsoleRenderable: Renderable to display log.
         """
-        path = Path(record.pathname).name
+        path = os.path.basename(record.pathname)
         level = self.get_level_text(record)
         time_format = None if self.formatter is None else self.formatter.datefmt
         log_time = datetime.fromtimestamp(record.created)
@@ -238,7 +243,6 @@ class RichHandler(Handler):
         )
         return log_renderable
 
-
 if __name__ == "__main__":  # pragma: no cover
     from time import sleep
 

Test output

show
============= FAILURES ===================================
_______________________________ test_export_svg ________________________________

    def test_export_svg() -> None:
        console = Console(record=True, width=100)
        console.print(
            "[b red on blue reverse]foo[/] [blink][link=https://example.org]Click[/link]"
        )
>       svg = console.export_svg()

tests/test_console.py:539: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <console width=100 None>

    def export_svg(
        self,
        *,
        title: str = "Rich",
        theme: Optional[TerminalTheme] = None,
        clear: bool = True,
        code_format: str = CONSOLE_SVG_FORMAT,
        font_aspect_ratio: float = 0.61,
        unique_id: Optional[str] = None,
    ) -> str:
        """
        Generate an SVG from the console contents (requires record=True in Console constructor).
    
        Args:
            title (str, optional): The title of the tab in the output image
            theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal
            clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``
            code_format (str, optional): Format string used to generate the SVG. Rich will inject a number of variables
                into the string in order to form the final SVG output. The default template used and the variables
                injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable.
            font_aspect_ratio (float, optional): The width to height ratio of the font used in the ``code_format``
                string. Defaults to 0.61, which is the width to height ratio of Fira Code (the default font).
                If you aren't specifying a different font inside ``code_format``, you probably don't need this.
            unique_id (str, optional): unique id that is used as the prefix for various elements (CSS styles, node
                ids). If not set, this defaults to a computed value based on the recorded content.
        """
    
        from rich.cells import cell_len
    
        style_cache: Dict[Style, str] = {}
    
        def get_svg_style(style: Style) -> str:
            """Convert a Style to CSS rules for SVG."""
            if style in style_cache:
                return style_cache[style]
            css_rules = []
            color = (
                _theme.foreground_color
                if (style.color is None or style.color.is_default)
                else style.color.get_truecolor(_theme)
            )
            bgcolor = (
                _theme.background_color
                if (style.bgcolor is None or style.bgcolor.is_default)
                else style.bgcolor.get_truecolor(_theme)
            )
            if style.reverse:
                color, bgcolor = bgcolor, color
            if style.dim:
                color = blend_rgb(color, bgcolor, 0.4)
            css_rules.append(f"fill: {color.hex}")
            if style.bold:
                css_rules.append("font-weight: bold")
            if style.italic:
                css_rules.append("font-style: italic;")
            if style.underline:
                css_rules.append("text-decoration: underline;")
            if style.strike:
                css_rules.append("text-decoration: line-through;")
    
            css = ";".join(css_rules)
            style_cache[style] = css
            return css
    
        _theme = theme or SVG_EXPORT_THEME
    
        width = self.width
        char_height = 20
        char_width = char_height * font_aspect_ratio
        line_height = char_height * 1.22
    
        margin_top = 1
        margin_right = 1
        margin_bottom = 1
        margin_left = 1
    
        padding_top = 40
        padding_right = 8
        padding_bottom = 8
        padding_left = 8
    
        padding_width = padding_left + padding_right
        padding_height = padding_top + padding_bottom
        margin_width = margin_left + margin_right
        margin_height = margin_top + margin_bottom
    
        text_backgrounds: List[str] = []
        text_group: List[str] = []
        classes: Dict[str, int] = {}
        style_no = 1
    
        def escape_text(text: str) -> str:
            """HTML escape text and replace spaces with nbsp."""
            return escape(text).replace(" ", "&#160;")
    
        def make_tag(
            name: str, content: Optional[str] = None, **attribs: object
        ) -> str:
            """Make a tag from name, content, and attributes."""
    
            def stringify(value: object) -> str:
                if isinstance(value, (float)):
                    return format(value, "g")
                return str(value)
    
            tag_attribs = " ".join(
                f'{k.lstrip("_").replace("_", "-")}="{stringify(v)}"'
                for k, v in attribs.items()
            )
            return (
                f"<{name} {tag_attribs}>{content}</{name}>"
                if content
                else f"<{name} {tag_attribs}/>"
            )
    
        with self._record_buffer_lock:
            segments = list(Segment.filter_control(self._record_buffer))
            if clear:
                self._record_buffer.clear()
    
        if unique_id is None:
            unique_id = "terminal-" + str(
>               zlib.adler32(
                    ("".join(repr(segment) for segment in segments)).encode(
                        "utf-8",
                        "ignore",
                    )
                    + title.encode("utf-8", "ignore")
                )
            )
E           NameError: name 'zlib' is not defined. Did you forget to import 'zlib'?

rich/console.py:2449: NameError
----------------------------- Captured stdout call -----------------------------
foo Click
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 56 passed in 0.26s