diff options
author | Pradyun Gedam <pradyunsg@users.noreply.github.com> | 2022-01-21 15:50:17 +0000 |
---|---|---|
committer | Pradyun Gedam <pradyunsg@users.noreply.github.com> | 2022-01-25 08:51:14 +0000 |
commit | 1c61502ce126283132b1f47f3fb48276bfcefd95 (patch) | |
tree | 0e1c99da1b759656ee50eb2f3d83d1e26ac92280 /src/pip/_vendor/rich | |
parent | d706df7e101b175e786c08aef4b08648d84cdfd0 (diff) | |
download | pip-1c61502ce126283132b1f47f3fb48276bfcefd95.tar.gz |
Upgrade rich to 11.0.0
Diffstat (limited to 'src/pip/_vendor/rich')
-rw-r--r-- | src/pip/_vendor/rich/__main__.py | 27 | ||||
-rw-r--r-- | src/pip/_vendor/rich/_windows.py | 5 | ||||
-rw-r--r-- | src/pip/_vendor/rich/cells.py | 40 | ||||
-rw-r--r-- | src/pip/_vendor/rich/console.py | 54 | ||||
-rw-r--r-- | src/pip/_vendor/rich/default_styles.py | 24 | ||||
-rw-r--r-- | src/pip/_vendor/rich/file_proxy.py | 2 | ||||
-rw-r--r-- | src/pip/_vendor/rich/jupyter.py | 12 | ||||
-rw-r--r-- | src/pip/_vendor/rich/live.py | 60 | ||||
-rw-r--r-- | src/pip/_vendor/rich/live_render.py | 10 | ||||
-rw-r--r-- | src/pip/_vendor/rich/logging.py | 11 | ||||
-rw-r--r-- | src/pip/_vendor/rich/markup.py | 2 | ||||
-rw-r--r-- | src/pip/_vendor/rich/padding.py | 10 | ||||
-rw-r--r-- | src/pip/_vendor/rich/pager.py | 2 | ||||
-rw-r--r-- | src/pip/_vendor/rich/pretty.py | 316 | ||||
-rw-r--r-- | src/pip/_vendor/rich/segment.py | 155 | ||||
-rw-r--r-- | src/pip/_vendor/rich/syntax.py | 42 | ||||
-rw-r--r-- | src/pip/_vendor/rich/table.py | 195 | ||||
-rw-r--r-- | src/pip/_vendor/rich/text.py | 138 | ||||
-rw-r--r-- | src/pip/_vendor/rich/traceback.py | 24 |
19 files changed, 740 insertions, 389 deletions
diff --git a/src/pip/_vendor/rich/__main__.py b/src/pip/_vendor/rich/__main__.py index 60d03efba..8692d37e0 100644 --- a/src/pip/_vendor/rich/__main__.py +++ b/src/pip/_vendor/rich/__main__.py @@ -4,13 +4,7 @@ from time import process_time from pip._vendor.rich import box from pip._vendor.rich.color import Color -from pip._vendor.rich.console import ( - Console, - ConsoleOptions, - Group, - RenderResult, - RenderableType, -) +from pip._vendor.rich.console import Console, ConsoleOptions, Group, RenderableType, RenderResult from pip._vendor.rich.markdown import Markdown from pip._vendor.rich.measure import Measurement from pip._vendor.rich.pretty import Pretty @@ -222,7 +216,10 @@ if __name__ == "__main__": # pragma: no cover test_card = make_test_card() # Print once to warm cache + start = process_time() console.print(test_card) + pre_cache_taken = round((process_time() - start) * 1000.0, 1) + console.file = io.StringIO() start = process_time() @@ -234,7 +231,8 @@ if __name__ == "__main__": # pragma: no cover for line in text.splitlines(True): print(line, end="") - print(f"rendered in {taken}ms") + print(f"rendered in {pre_cache_taken}ms (cold cache)") + print(f"rendered in {taken}ms (warm cache)") from pip._vendor.rich.panel import Panel @@ -243,13 +241,10 @@ if __name__ == "__main__": # pragma: no cover sponsor_message = Table.grid(padding=1) sponsor_message.add_column(style="green", justify="right") sponsor_message.add_column(no_wrap=True) + sponsor_message.add_row( - "Sponsor me", - "[u blue link=https://github.com/sponsors/willmcgugan]https://github.com/sponsors/willmcgugan", - ) - sponsor_message.add_row( - "Buy me a :coffee:", - "[u blue link=https://ko-fi.com/willmcgugan]https://ko-fi.com/willmcgugan", + "Buy devs a :coffee:", + "[u blue link=https://ko-fi.com/textualize]https://ko-fi.com/textualize", ) sponsor_message.add_row( "Twitter", @@ -261,9 +256,9 @@ if __name__ == "__main__": # pragma: no cover intro_message = Text.from_markup( """\ -It takes a lot of time to develop Rich and to provide support. +We hope you enjoy using Rich! -Consider supporting my work via Github Sponsors (ask your company / organization), or buy me a coffee to say thanks. +Rich is maintained with :heart: by [link=https://www.textualize.io]Textualize.io[/] - Will McGugan""" ) diff --git a/src/pip/_vendor/rich/_windows.py b/src/pip/_vendor/rich/_windows.py index 0fdbc4539..ca3a680d3 100644 --- a/src/pip/_vendor/rich/_windows.py +++ b/src/pip/_vendor/rich/_windows.py @@ -1,5 +1,4 @@ import sys - from dataclasses import dataclass @@ -15,8 +14,7 @@ class WindowsConsoleFeatures: try: import ctypes - from ctypes import wintypes - from ctypes import LibraryLoader + from ctypes import LibraryLoader, wintypes if sys.platform == "win32": windll = LibraryLoader(ctypes.WinDLL) @@ -30,7 +28,6 @@ except (AttributeError, ImportError, ValueError): features = WindowsConsoleFeatures() return features - else: STDOUT = -11 diff --git a/src/pip/_vendor/rich/cells.py b/src/pip/_vendor/rich/cells.py index b02f4b868..e824ea2a6 100644 --- a/src/pip/_vendor/rich/cells.py +++ b/src/pip/_vendor/rich/cells.py @@ -1,9 +1,13 @@ from functools import lru_cache +import re from typing import Dict, List from ._cell_widths import CELL_WIDTHS from ._lru_cache import LRUCache +# Regex to match sequence of the most common character ranges +_is_single_cell_widths = re.compile("^[\u0020-\u006f\u00a0\u02ff\u0370-\u0482]*$").match + def cell_len(text: str, _cache: Dict[str, int] = LRUCache(1024 * 4)) -> int: """Get the number of cells required to display text. @@ -12,16 +16,19 @@ def cell_len(text: str, _cache: Dict[str, int] = LRUCache(1024 * 4)) -> int: text (str): Text to display. Returns: - int: Number of cells required to display the text. + int: Get the number of cells required to display text. """ - cached_result = _cache.get(text, None) - if cached_result is not None: - return cached_result - - _get_size = get_character_cell_size - total_size = sum(_get_size(character) for character in text) - if len(text) <= 64: - _cache[text] = total_size + + if _is_single_cell_widths(text): + return len(text) + else: + cached_result = _cache.get(text, None) + if cached_result is not None: + return cached_result + _get_size = get_character_cell_size + total_size = sum(_get_size(character) for character in text) + if len(text) <= 64: + _cache[text] = total_size return total_size @@ -35,12 +42,10 @@ def get_character_cell_size(character: str) -> int: Returns: int: Number of cells (0, 1 or 2) occupied by that character. """ - - codepoint = ord(character) - if 127 > codepoint > 31: - # Shortcut for ascii + if _is_single_cell_widths(character): return 1 - return _get_codepoint_cell_size(codepoint) + + return _get_codepoint_cell_size(ord(character)) @lru_cache(maxsize=4096) @@ -74,6 +79,13 @@ def _get_codepoint_cell_size(codepoint: int) -> int: def set_cell_size(text: str, total: int) -> str: """Set the length of a string to fit within given number of cells.""" + + if _is_single_cell_widths(text): + size = len(text) + if size < total: + return text + " " * (total - size) + return text[:total] + if not total: return "" cell_size = cell_len(text) diff --git a/src/pip/_vendor/rich/console.py b/src/pip/_vendor/rich/console.py index ee07d7ad7..27e722760 100644 --- a/src/pip/_vendor/rich/console.py +++ b/src/pip/_vendor/rich/console.py @@ -1,7 +1,6 @@ import inspect import os import platform -import shutil import sys import threading from abc import ABC, abstractmethod @@ -12,8 +11,9 @@ from getpass import getpass from html import escape from inspect import isclass from itertools import islice +from threading import RLock from time import monotonic -from types import FrameType, TracebackType, ModuleType +from types import FrameType, ModuleType, TracebackType from typing import ( IO, TYPE_CHECKING, @@ -25,7 +25,6 @@ from typing import ( Mapping, NamedTuple, Optional, - Set, TextIO, Tuple, Type, @@ -212,6 +211,19 @@ class ConsoleOptions: options.min_width = options.max_width = max(0, width) return options + def update_height(self, height: int) -> "ConsoleOptions": + """Update the height, and return a copy. + + Args: + height (int): New height + + Returns: + ~ConsoleOptions: New Console options instance. + """ + options = self.copy() + options.max_height = options.height = height + return options + def update_dimensions(self, width: int, height: int) -> "ConsoleOptions": """Update the width and height, and return a copy. @@ -224,8 +236,7 @@ class ConsoleOptions: """ options = self.copy() options.min_width = options.max_width = max(0, width) - options.height = height - options.max_height = height + options.height = options.max_height = height return options @@ -247,11 +258,12 @@ class ConsoleRenderable(Protocol): ... +# A type that may be rendered by Console. RenderableType = Union[ConsoleRenderable, RichCast, str] -"""A type that may be rendered by Console.""" + +# The result of calling a __rich_console__ method. RenderResult = Iterable[Union[RenderableType, Segment]] -"""The result of calling a __rich_console__ method.""" _null_highlighter = NullHighlighter() @@ -464,9 +476,6 @@ class Group: yield from self.renderables -RenderGroup = Group # TODO: deprecate at some point - - def group(fit: bool = True) -> Callable[..., Callable[..., Group]]: """A decorator that turns an iterable of renderables in to a group. @@ -477,7 +486,7 @@ def group(fit: bool = True) -> Callable[..., Callable[..., Group]]: def decorator( method: Callable[..., Iterable[RenderableType]] ) -> Callable[..., Group]: - """Convert a method that returns an iterable of renderables in to a RenderGroup.""" + """Convert a method that returns an iterable of renderables in to a Group.""" @wraps(method) def _replace(*args: Any, **kwargs: Any) -> Group: @@ -489,9 +498,6 @@ def group(fit: bool = True) -> Callable[..., Callable[..., Group]]: return decorator -render_group = group - - def _is_jupyter() -> bool: # pragma: no cover """Check if we're running in a Jupyter notebook.""" try: @@ -813,12 +819,13 @@ class Console: Args: hook (RenderHook): Render hook instance. """ - - self._render_hooks.append(hook) + with self._lock: + self._render_hooks.append(hook) def pop_render_hook(self) -> None: """Pop the last renderhook from the stack.""" - self._render_hooks.pop() + with self._lock: + self._render_hooks.pop() def __enter__(self) -> "Console": """Own context manager to enter buffer context.""" @@ -1495,9 +1502,8 @@ class Console: control_codes (str): Control codes, such as those that may move the cursor. """ if not self.is_dumb_terminal: - for _control in control: - self._buffer.append(_control.segment) - self._check_buffer() + with self: + self._buffer.extend(_control.segment for _control in control) def out( self, @@ -1579,7 +1585,7 @@ class Console: if overflow is None: overflow = "ignore" crop = False - + render_hooks = self._render_hooks[:] with self: renderables = self._collect_renderables( objects, @@ -1590,7 +1596,7 @@ class Console: markup=markup, highlight=highlight, ) - for hook in self._render_hooks: + for hook in render_hooks: renderables = hook.process_renderables(renderables) render_options = self.options.update( justify=justify, @@ -1847,6 +1853,8 @@ class Console: if not objects: objects = (NewLine(),) + render_hooks = self._render_hooks[:] + with self: renderables = self._collect_renderables( objects, @@ -1881,7 +1889,7 @@ class Console: link_path=link_path, ) ] - for hook in self._render_hooks: + for hook in render_hooks: renderables = hook.process_renderables(renderables) new_segments: List[Segment] = [] extend = new_segments.extend diff --git a/src/pip/_vendor/rich/default_styles.py b/src/pip/_vendor/rich/default_styles.py index 355f9df85..91ab232d3 100644 --- a/src/pip/_vendor/rich/default_styles.py +++ b/src/pip/_vendor/rich/default_styles.py @@ -157,3 +157,27 @@ DEFAULT_STYLES: Dict[str, Style] = { "markdown.link": Style(color="bright_blue"), "markdown.link_url": Style(color="blue"), } + + +if __name__ == "__main__": # pragma: no cover + import argparse + import io + + from pip._vendor.rich.console import Console + from pip._vendor.rich.table import Table + from pip._vendor.rich.text import Text + + parser = argparse.ArgumentParser() + parser.add_argument("--html", action="store_true", help="Export as HTML table") + args = parser.parse_args() + html: bool = args.html + console = Console(record=True, width=70, file=io.StringIO()) if html else Console() + + table = Table("Name", "Styling") + + for style_name, style in DEFAULT_STYLES.items(): + table.add_row(Text(style_name, style=style), str(style)) + + console.print(table) + if html: + print(console.export_html(inline_styles=True)) diff --git a/src/pip/_vendor/rich/file_proxy.py b/src/pip/_vendor/rich/file_proxy.py index 99a6922cb..3ec593a5a 100644 --- a/src/pip/_vendor/rich/file_proxy.py +++ b/src/pip/_vendor/rich/file_proxy.py @@ -44,7 +44,7 @@ class FileProxy(io.TextIOBase): output = Text("\n").join( self.__ansi_decoder.decode_line(line) for line in lines ) - console.print(output, markup=False, emoji=False, highlight=False) + console.print(output) return len(text) def flush(self) -> None: diff --git a/src/pip/_vendor/rich/jupyter.py b/src/pip/_vendor/rich/jupyter.py index 7cdcc9cab..bedf5cb19 100644 --- a/src/pip/_vendor/rich/jupyter.py +++ b/src/pip/_vendor/rich/jupyter.py @@ -4,7 +4,6 @@ from . import get_console from .segment import Segment from .terminal_theme import DEFAULT_TERMINAL_THEME - JUPYTER_HTML_FORMAT = """\ <pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace">{code}</pre> """ @@ -75,11 +74,16 @@ def _render_segments(segments: Iterable[Segment]) -> str: def display(segments: Iterable[Segment], text: str) -> None: """Render segments to Jupyter.""" - from IPython.display import display as ipython_display - html = _render_segments(segments) jupyter_renderable = JupyterRenderable(html, text) - ipython_display(jupyter_renderable) + try: + from IPython.display import display as ipython_display + + ipython_display(jupyter_renderable) + except ModuleNotFoundError: + # Handle the case where the Console has force_jupyter=True, + # but IPython is not installed. + pass def print(*args: Any, **kwargs: Any) -> None: diff --git a/src/pip/_vendor/rich/live.py b/src/pip/_vendor/rich/live.py index 8097f7d2e..6db5b605f 100644 --- a/src/pip/_vendor/rich/live.py +++ b/src/pip/_vendor/rich/live.py @@ -130,35 +130,29 @@ class Live(JupyterMixin, RenderHook): return self.console.clear_live() self._started = False - try: - if self.auto_refresh and self._refresh_thread is not None: - self._refresh_thread.stop() - # allow it to fully render on the last even if overflow - self.vertical_overflow = "visible" - if not self._alt_screen and not self.console.is_jupyter: - self.refresh() - - finally: - self._disable_redirect_io() - self.console.pop_render_hook() - if not self._alt_screen and self.console.is_terminal: - self.console.line() - self.console.show_cursor(True) - if self._alt_screen: - self.console.set_alt_screen(False) - - if self._refresh_thread is not None: - self._refresh_thread.join() - self._refresh_thread = None - if self.transient and not self._alt_screen: - self.console.control(self._live_render.restore_cursor()) - if self.ipy_widget is not None: # pragma: no cover - if self.transient: - self.ipy_widget.close() - else: - # jupyter last refresh must occur after console pop render hook - # i am not sure why this is needed - self.refresh() + + if self.auto_refresh and self._refresh_thread is not None: + self._refresh_thread.stop() + self._refresh_thread = None + # allow it to fully render on the last even if overflow + self.vertical_overflow = "visible" + with self.console: + try: + if not self._alt_screen and not self.console.is_jupyter: + self.refresh() + finally: + self._disable_redirect_io() + self.console.pop_render_hook() + if not self._alt_screen and self.console.is_terminal: + self.console.line() + self.console.show_cursor(True) + if self._alt_screen: + self.console.set_alt_screen(False) + + if self.transient and not self._alt_screen: + self.console.control(self._live_render.restore_cursor()) + if self.ipy_widget is not None and self.transient: + self.ipy_widget.close() # pragma: no cover def __enter__(self) -> "Live": self.start(refresh=self._renderable is not None) @@ -174,7 +168,7 @@ class Live(JupyterMixin, RenderHook): def _enable_redirect_io(self) -> None: """Enable redirecting of stdout / stderr.""" - if self.console.is_terminal: + if self.console.is_terminal or self.console.is_jupyter: if self._redirect_stdout and not isinstance(sys.stdout, FileProxy): self._restore_stdout = sys.stdout sys.stdout = cast("TextIO", FileProxy(self.console, sys.stdout)) @@ -255,11 +249,7 @@ class Live(JupyterMixin, RenderHook): if self._alt_screen else self._live_render.position_cursor() ) - renderables = [ - reset, - *renderables, - self._live_render, - ] + renderables = [reset, *renderables, self._live_render] elif ( not self._started and not self.transient ): # if it is finished render the final output for files or dumb_terminals diff --git a/src/pip/_vendor/rich/live_render.py b/src/pip/_vendor/rich/live_render.py index b02fd3176..b90fbf7f3 100644 --- a/src/pip/_vendor/rich/live_render.py +++ b/src/pip/_vendor/rich/live_render.py @@ -84,16 +84,15 @@ class LiveRender: ) -> RenderResult: renderable = self.renderable - _Segment = Segment style = console.get_style(self.style) lines = console.render_lines(renderable, options, style=style, pad=False) - shape = _Segment.get_shape(lines) + shape = Segment.get_shape(lines) _, height = shape if height > options.size.height: if self.vertical_overflow == "crop": lines = lines[: options.size.height] - shape = _Segment.get_shape(lines) + shape = Segment.get_shape(lines) elif self.vertical_overflow == "ellipsis": lines = lines[: (options.size.height - 1)] overflow_text = Text( @@ -104,10 +103,11 @@ class LiveRender: style="live.ellipsis", ) lines.append(list(console.render(overflow_text))) - shape = _Segment.get_shape(lines) + shape = Segment.get_shape(lines) self._shape = shape + new_line = Segment.line() for last, line in loop_last(lines): yield from line if not last: - yield _Segment.line() + yield new_line diff --git a/src/pip/_vendor/rich/logging.py b/src/pip/_vendor/rich/logging.py index 47ca7d42d..002f1f7bf 100644 --- a/src/pip/_vendor/rich/logging.py +++ b/src/pip/_vendor/rich/logging.py @@ -164,12 +164,13 @@ class RichHandler(Handler): Returns: ConsoleRenderable: Renderable to display log message. """ - use_markup = ( - getattr(record, "markup") if hasattr(record, "markup") else self.markup - ) + use_markup = getattr(record, "markup", self.markup) message_text = Text.from_markup(message) if use_markup else Text(message) - if self.highlighter: - message_text = self.highlighter(message_text) + + highlighter = getattr(record, "highlighter", self.highlighter) + if highlighter: + message_text = highlighter(message_text) + if self.KEYWORDS: message_text.highlight_words(self.KEYWORDS, "logging.keyword") return message_text diff --git a/src/pip/_vendor/rich/markup.py b/src/pip/_vendor/rich/markup.py index ff861a354..619540202 100644 --- a/src/pip/_vendor/rich/markup.py +++ b/src/pip/_vendor/rich/markup.py @@ -47,7 +47,7 @@ _EscapeSubMethod = Callable[[_ReSubCallable, str], str] # Sub method of a compi def escape( - markup: str, _escape: _EscapeSubMethod = re.compile(r"(\\*)(\[[a-z#\/].*?\])").sub + markup: str, _escape: _EscapeSubMethod = re.compile(r"(\\*)(\[[a-z#\/@].*?\])").sub ) -> str: """Escapes text so that it won't be interpreted as markup. diff --git a/src/pip/_vendor/rich/padding.py b/src/pip/_vendor/rich/padding.py index f024e95e0..1b2204f59 100644 --- a/src/pip/_vendor/rich/padding.py +++ b/src/pip/_vendor/rich/padding.py @@ -89,11 +89,13 @@ class Padding(JupyterMixin): + self.right, options.max_width, ) + render_options = options.update_width(width - self.left - self.right) + if render_options.height is not None: + render_options = render_options.update_height( + height=render_options.height - self.top - self.bottom + ) lines = console.render_lines( - self.renderable, - options.update_width(width - self.left - self.right), - style=style, - pad=True, + self.renderable, render_options, style=style, pad=True ) _Segment = Segment diff --git a/src/pip/_vendor/rich/pager.py b/src/pip/_vendor/rich/pager.py index ea9bdf08f..dbfb973e3 100644 --- a/src/pip/_vendor/rich/pager.py +++ b/src/pip/_vendor/rich/pager.py @@ -17,7 +17,7 @@ class Pager(ABC): class SystemPager(Pager): """Uses the pager installed on the system.""" - def _pager(self, content: str) -> Any: + def _pager(self, content: str) -> Any: # pragma: no cover return __import__("pydoc").pager(content) def show(self, content: str) -> None: diff --git a/src/pip/_vendor/rich/pretty.py b/src/pip/_vendor/rich/pretty.py index cfce487af..606ee3382 100644 --- a/src/pip/_vendor/rich/pretty.py +++ b/src/pip/_vendor/rich/pretty.py @@ -1,28 +1,30 @@ import builtins +import dataclasses +import inspect import os -from pip._vendor.rich.repr import RichReprResult +import re import sys from array import array -from collections import Counter, defaultdict, deque, UserDict, UserList -import dataclasses +from collections import Counter, UserDict, UserList, defaultdict, deque from dataclasses import dataclass, fields, is_dataclass from inspect import isclass from itertools import islice -import re +from types import MappingProxyType from typing import ( - DefaultDict, TYPE_CHECKING, Any, Callable, + DefaultDict, Dict, Iterable, List, Optional, Set, - Union, Tuple, + Union, ) -from types import MappingProxyType + +from pip._vendor.rich.repr import RichReprResult try: import attr as _attr_module @@ -30,7 +32,6 @@ except ImportError: # pragma: no cover _attr_module = None # type: ignore -from .highlighter import ReprHighlighter from . import get_console from ._loop import loop_last from ._pick import pick_bool @@ -51,9 +52,6 @@ if TYPE_CHECKING: RenderResult, ) -# Matches Jupyter's special methods -_re_jupyter_repr = re.compile(f"^_repr_.+_$") - def _is_attr_object(obj: Any) -> bool: """Check if an object was created with attrs module.""" @@ -78,10 +76,74 @@ def _is_dataclass_repr(obj: object) -> bool: # Catching all exceptions in case something is missing on a non CPython implementation try: return obj.__repr__.__code__.co_filename == dataclasses.__file__ - except Exception: + except Exception: # pragma: no coverage return False +def _ipy_display_hook( + value: Any, + console: Optional["Console"] = None, + overflow: "OverflowMethod" = "ignore", + crop: bool = False, + indent_guides: bool = False, + max_length: Optional[int] = None, + max_string: Optional[int] = None, + expand_all: bool = False, +) -> None: + from .console import ConsoleRenderable # needed here to prevent circular import + + # always skip rich generated jupyter renderables or None values + if isinstance(value, JupyterRenderable) or value is None: + return + + console = console or get_console() + if console.is_jupyter: + # Delegate rendering to IPython if the object (and IPython) supports it + # https://ipython.readthedocs.io/en/stable/config/integrating.html#rich-display + ipython_repr_methods = [ + "_repr_html_", + "_repr_markdown_", + "_repr_json_", + "_repr_latex_", + "_repr_jpeg_", + "_repr_png_", + "_repr_svg_", + "_repr_mimebundle_", + ] + for repr_method in ipython_repr_methods: + method = getattr(value, repr_method, None) + if inspect.ismethod(method): + # Calling the method ourselves isn't ideal. The interface for the `_repr_*_` methods + # specifies that if they return None, then they should not be rendered + # by the notebook. + try: + repr_result = method() + except Exception: + continue # If the method raises, treat it as if it doesn't exist, try any others + if repr_result is not None: + return # Delegate rendering to IPython + + # certain renderables should start on a new line + if isinstance(value, ConsoleRenderable): + console.line() + + console.print( + value + if isinstance(value, RichRenderable) + else Pretty( + value, + overflow=overflow, + indent_guides=indent_guides, + max_length=max_length, + max_string=max_string, + expand_all=expand_all, + margin=12, + ), + crop=crop, + new_line_start=True, + ) + + def install( console: Optional["Console"] = None, overflow: "OverflowMethod" = "ignore", @@ -106,8 +168,6 @@ def install( """ from pip._vendor.rich import get_console - from .console import ConsoleRenderable # needed here to prevent circular import - console = console or get_console() assert console is not None @@ -131,37 +191,6 @@ def install( ) builtins._ = value # type: ignore - def ipy_display_hook(value: Any) -> None: # pragma: no cover - assert console is not None - # always skip rich generated jupyter renderables or None values - if isinstance(value, JupyterRenderable) or value is None: - return - # on jupyter rich display, if using one of the special representations don't use rich - if console.is_jupyter and any( - _re_jupyter_repr.match(attr) for attr in dir(value) - ): - return - - # certain renderables should start on a new line - if isinstance(value, ConsoleRenderable): - console.line() - - console.print( - value - if isinstance(value, RichRenderable) - else Pretty( - value, - overflow=overflow, - indent_guides=indent_guides, - max_length=max_length, - max_string=max_string, - expand_all=expand_all, - margin=12, - ), - crop=crop, - new_line_start=True, - ) - try: # pragma: no cover ip = get_ipython() # type: ignore from IPython.core.formatters import BaseFormatter @@ -171,7 +200,15 @@ def install( def __call__(self, value: Any) -> Any: if self.pprint: - return ipy_display_hook(value) + return _ipy_display_hook( + value, + console=get_console(), + overflow=overflow, + indent_guides=indent_guides, + max_length=max_length, + max_string=max_string, + expand_all=expand_all, + ) else: return repr(value) @@ -196,6 +233,7 @@ class Pretty(JupyterMixin): max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. Defaults to None. max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None. + max_depth (int, optional): Maximum depth of nested data structures, or None for no maximum. Defaults to None. expand_all (bool, optional): Expand all containers. Defaults to False. margin (int, optional): Subtrace a margin from width to force containers to expand earlier. Defaults to 0. insert_line (bool, optional): Insert a new line if the output has multiple new lines. Defaults to False. @@ -213,6 +251,7 @@ class Pretty(JupyterMixin): indent_guides: bool = False, max_length: Optional[int] = None, max_string: Optional[int] = None, + max_depth: Optional[int] = None, expand_all: bool = False, margin: int = 0, insert_line: bool = False, @@ -226,6 +265,7 @@ class Pretty(JupyterMixin): self.indent_guides = indent_guides self.max_length = max_length self.max_string = max_string + self.max_depth = max_depth self.expand_all = expand_all self.margin = margin self.insert_line = insert_line @@ -239,6 +279,7 @@ class Pretty(JupyterMixin): indent_size=self.indent_size, max_length=self.max_length, max_string=self.max_string, + max_depth=self.max_depth, expand_all=self.expand_all, ) pretty_text = Text( @@ -474,7 +515,10 @@ class _Line: def traverse( - _object: Any, max_length: Optional[int] = None, max_string: Optional[int] = None + _object: Any, + max_length: Optional[int] = None, + max_string: Optional[int] = None, + max_depth: Optional[int] = None, ) -> Node: """Traverse object and generate a tree. @@ -484,6 +528,8 @@ def traverse( Defaults to None. max_string (int, optional): Maximum length of string before truncating, or None to disable truncating. Defaults to None. + max_depth (int, optional): Maximum depth of data structures, or None for no maximum. + Defaults to None. Returns: Node: The root of a tree structure which can be used to render a pretty repr. @@ -509,11 +555,13 @@ def traverse( push_visited = visited_ids.add pop_visited = visited_ids.remove - def _traverse(obj: Any, root: bool = False) -> Node: + def _traverse(obj: Any, root: bool = False, depth: int = 0) -> Node: """Walk the object depth first.""" + obj_type = type(obj) py_version = (sys.version_info.major, sys.version_info.minor) children: List[Node] + reached_max_depth = max_depth is not None and depth >= max_depth def iter_rich_args(rich_args: Any) -> Iterable[Union[Any, Tuple[str, Any]]]: for arg in rich_args: @@ -554,33 +602,37 @@ def traverse( if args: children = [] append = children.append - if angular: - node = Node( - open_brace=f"<{class_name} ", - close_brace=">", - children=children, - last=root, - separator=" ", - ) + + if reached_max_depth: + node = Node(value_repr=f"...") else: - node = Node( - open_brace=f"{class_name}(", - close_brace=")", - children=children, - last=root, - ) - for last, arg in loop_last(args): - if isinstance(arg, tuple): - key, child = arg - child_node = _traverse(child) - child_node.last = last - child_node.key_repr = key - child_node.key_separator = "=" - append(child_node) + if angular: + node = Node( + open_brace=f"<{class_name} ", + close_brace=">", + children=children, + last=root, + separator=" ", + ) else: - child_node = _traverse(arg) - child_node.last = last - append(child_node) + node = Node( + open_brace=f"{class_name}(", + close_brace=")", + children=children, + last=root, + ) + for last, arg in loop_last(args): + if isinstance(arg, tuple): + key, child = arg + child_node = _traverse(child, depth=depth + 1) + child_node.last = last + child_node.key_repr = key + child_node.key_separator = "=" + append(child_node) + else: + child_node = _traverse(arg, depth=depth + 1) + child_node.last = last + append(child_node) else: node = Node( value_repr=f"<{class_name}>" if angular else f"{class_name}()", @@ -593,40 +645,43 @@ def traverse( attr_fields = _get_attr_fields(obj) if attr_fields: - node = Node( - open_brace=f"{obj.__class__.__name__}(", - close_brace=")", - children=children, - last=root, - ) + if reached_max_depth: + node = Node(value_repr=f"...") + else: + node = Node( + open_brace=f"{obj.__class__.__name__}(", + close_brace=")", + children=children, + last=root, + ) - def iter_attrs() -> Iterable[ - Tuple[str, Any, Optional[Callable[[Any], str]]] - ]: - """Iterate over attr fields and values.""" - for attr in attr_fields: - if attr.repr: - try: - value = getattr(obj, attr.name) - except Exception as error: - # Can happen, albeit rarely - yield (attr.name, error, None) - else: - yield ( - attr.name, - value, - attr.repr if callable(attr.repr) else None, - ) - - for last, (name, value, repr_callable) in loop_last(iter_attrs()): - if repr_callable: - child_node = Node(value_repr=str(repr_callable(value))) - else: - child_node = _traverse(value) - child_node.last = last - child_node.key_repr = name - child_node.key_separator = "=" - append(child_node) + def iter_attrs() -> Iterable[ + Tuple[str, Any, Optional[Callable[[Any], str]]] + ]: + """Iterate over attr fields and values.""" + for attr in attr_fields: + if attr.repr: + try: + value = getattr(obj, attr.name) + except Exception as error: + # Can happen, albeit rarely + yield (attr.name, error, None) + else: + yield ( + attr.name, + value, + attr.repr if callable(attr.repr) else None, + ) + + for last, (name, value, repr_callable) in loop_last(iter_attrs()): + if repr_callable: + child_node = Node(value_repr=str(repr_callable(value))) + else: + child_node = _traverse(value, depth=depth + 1) + child_node.last = last + child_node.key_repr = name + child_node.key_separator = "=" + append(child_node) else: node = Node( value_repr=f"{obj.__class__.__name__}()", children=[], last=root @@ -646,21 +701,26 @@ def traverse( children = [] append = children.append - node = Node( - open_brace=f"{obj.__class__.__name__}(", - close_brace=")", - children=children, - last=root, - ) + if reached_max_depth: + node = Node(value_repr=f"...") + else: + node = Node( + open_brace=f"{obj.__class__.__name__}(", + close_brace=")", + children=children, + last=root, + ) - for last, field in loop_last(field for field in fields(obj) if field.repr): - child_node = _traverse(getattr(obj, field.name)) - child_node.key_repr = field.name - child_node.last = last - child_node.key_separator = "=" - append(child_node) + for last, field in loop_last( + field for field in fields(obj) if field.repr + ): + child_node = _traverse(getattr(obj, field.name), depth=depth + 1) + child_node.key_repr = field.name + child_node.last = last + child_node.key_separator = "=" + append(child_node) - pop_visited(obj_id) + pop_visited(obj_id) elif isinstance(obj, _CONTAINERS): for container_type in _CONTAINERS: @@ -676,7 +736,9 @@ def traverse( open_brace, close_brace, empty = _BRACES[obj_type](obj) - if obj_type.__repr__ != type(obj).__repr__: + if reached_max_depth: + node = Node(value_repr=f"...", last=root) + elif obj_type.__repr__ != type(obj).__repr__: node = Node(value_repr=to_repr(obj), last=root) elif obj: children = [] @@ -695,7 +757,7 @@ def traverse( if max_length is not None: iter_items = islice(iter_items, max_length) for index, (key, child) in enumerate(iter_items): - child_node = _traverse(child) + child_node = _traverse(child, depth=depth + 1) child_node.key_repr = to_repr(key) child_node.last = index == last_item_index append(child_node) @@ -704,7 +766,7 @@ def traverse( if max_length is not None: iter_values = islice(iter_values, max_length) for index, child in enumerate(iter_values): - child_node = _traverse(child) + child_node = _traverse(child, depth=depth + 1) child_node.last = index == last_item_index append(child_node) if max_length is not None and num_items > max_length: @@ -729,6 +791,7 @@ def pretty_repr( indent_size: int = 4, max_length: Optional[int] = None, max_string: Optional[int] = None, + max_depth: Optional[int] = None, expand_all: bool = False, ) -> str: """Prettify repr string by expanding on to new lines to fit within a given width. @@ -741,6 +804,8 @@ def pretty_repr( Defaults to None. max_string (int, optional): Maximum length of string before truncating, or None to disable truncating. Defaults to None. + max_depth (int, optional): Maximum depth of nested data structure, or None for no depth. + Defaults to None. expand_all (bool, optional): Expand all containers regardless of available width. Defaults to False. Returns: @@ -750,7 +815,9 @@ def pretty_repr( if isinstance(_object, Node): node = _object else: - node = traverse(_object, max_length=max_length, max_string=max_string) + node = traverse( + _object, max_length=max_length, max_string=max_string, max_depth=max_depth + ) repr_str = node.render( max_width=max_width, indent_size=indent_size, expand_all=expand_all ) @@ -764,6 +831,7 @@ def pprint( indent_guides: bool = True, max_length: Optional[int] = None, max_string: Optional[int] = None, + max_depth: Optional[int] = None, expand_all: bool = False, ) -> None: """A convenience function for pretty printing. @@ -774,6 +842,7 @@ def pprint( max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. Defaults to None. max_string (int, optional): Maximum length of strings before truncating, or None to disable. Defaults to None. + max_depth (int, optional): Maximum depth for nested data structures, or None for unlimited depth. Defaults to None. indent_guides (bool, optional): Enable indentation guides. Defaults to True. expand_all (bool, optional): Expand all containers. Defaults to False. """ @@ -783,6 +852,7 @@ def pprint( _object, max_length=max_length, max_string=max_string, + max_depth=max_depth, indent_guides=indent_guides, expand_all=expand_all, overflow="ignore", diff --git a/src/pip/_vendor/rich/segment.py b/src/pip/_vendor/rich/segment.py index 869b61010..94ca73076 100644 --- a/src/pip/_vendor/rich/segment.py +++ b/src/pip/_vendor/rich/segment.py @@ -12,10 +12,16 @@ from typing import ( Optional, Sequence, Tuple, + Type, Union, ) -from .cells import cell_len, get_character_cell_size, set_cell_size +from .cells import ( + _is_single_cell_widths, + cell_len, + get_character_cell_size, + set_cell_size, +) from .repr import Result, rich_repr from .style import Style @@ -97,19 +103,14 @@ class Segment(NamedTuple): text, style, control = segment _Segment = Segment - if cut >= segment.cell_length: - return segment, _Segment("", style, control) - if len(text) == segment.cell_length: - # Fast path with all 1 cell characters - return ( - _Segment(text[:cut], style, control), - _Segment(text[cut:], style, control), - ) + cell_length = segment.cell_length + if cut >= cell_length: + return segment, _Segment("", style, control) cell_size = get_character_cell_size - pos = int((cut / segment.cell_length) * len(text)) + pos = int((cut / cell_length) * len(text)) before = text[:pos] cell_pos = cell_len(before) @@ -143,6 +144,17 @@ class Segment(NamedTuple): Returns: Tuple[Segment, Segment]: Two segments. """ + text, style, control = self + + if _is_single_cell_widths(text): + # Fast path with all 1 cell characters + if cut >= len(text): + return self, Segment("", style, control) + return ( + Segment(text[:cut], style, control), + Segment(text[cut:], style, control), + ) + return self._split_cells(self, cut) @classmethod @@ -341,7 +353,8 @@ class Segment(NamedTuple): Returns: int: The length of the line. """ - return sum(segment.cell_length for segment in line) + _cell_len = cell_len + return sum(_cell_len(segment.text) for segment in line) @classmethod def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]: @@ -372,34 +385,117 @@ class Segment(NamedTuple): lines (List[List[Segment]]): A list of lines. width (int): Desired width. height (int, optional): Desired height or None for no change. - style (Style, optional): Style of any padding added. Defaults to None. + style (Style, optional): Style of any padding added. new_lines (bool, optional): Padded lines should include "\n". Defaults to False. Returns: - List[List[Segment]]: New list of lines that fits width x height. + List[List[Segment]]: New list of lines. """ - if height is None: - height = len(lines) - shaped_lines: List[List[Segment]] = [] - pad_line = ( - [Segment(" " * width, style), Segment("\n")] - if new_lines - else [Segment(" " * width, style)] + _height = height or len(lines) + + blank = ( + [cls(" " * width + "\n", style)] if new_lines else [cls(" " * width, style)] ) - append = shaped_lines.append adjust_line_length = cls.adjust_line_length - line: Optional[List[Segment]] - iter_lines = iter(lines) - for _ in range(height): - line = next(iter_lines, None) - if line is None: - append(pad_line) - else: - append(adjust_line_length(line, width, style=style)) + shaped_lines = lines[:_height] + shaped_lines[:] = [ + adjust_line_length(line, width, style=style) for line in lines + ] + if len(shaped_lines) < _height: + shaped_lines.extend([blank] * (_height - len(shaped_lines))) return shaped_lines @classmethod + def align_top( + cls: Type["Segment"], + lines: List[List["Segment"]], + width: int, + height: int, + style: Style, + new_lines: bool = False, + ) -> List[List["Segment"]]: + """Aligns lines to top (adds extra lines to bottom as required). + + Args: + lines (List[List[Segment]]): A list of lines. + width (int): Desired width. + height (int, optional): Desired height or None for no change. + style (Style): Style of any padding added. + new_lines (bool, optional): Padded lines should include "\n". Defaults to False. + + Returns: + List[List[Segment]]: New list of lines. + """ + extra_lines = height - len(lines) + if not extra_lines: + return lines[:] + lines = lines[:height] + blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style) + lines = lines + [[blank]] * extra_lines + return lines + + @classmethod + def align_bottom( + cls: Type["Segment"], + lines: List[List["Segment"]], + width: int, + height: int, + style: Style, + new_lines: bool = False, + ) -> List[List["Segment"]]: + """Aligns render to bottom (adds extra lines above as required). + + Args: + lines (List[List[Segment]]): A list of lines. + width (int): Desired width. + height (int, optional): Desired height or None for no change. + style (Style): Style of any padding added. Defaults to None. + new_lines (bool, optional): Padded lines should include "\n". Defaults to False. + + Returns: + List[List[Segment]]: New list of lines. + """ + extra_lines = height - len(lines) + if not extra_lines: + return lines[:] + lines = lines[:height] + blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style) + lines = [[blank]] * extra_lines + lines + return lines + + @classmethod + def align_middle( + cls: Type["Segment"], + lines: List[List["Segment"]], + width: int, + height: int, + style: Style, + new_lines: bool = False, + ) -> List[List["Segment"]]: + """Aligns lines to middle (adds extra lines to above and below as required). + + Args: + lines (List[List[Segment]]): A list of lines. + width (int): Desired width. + height (int, optional): Desired height or None for no change. + style (Style): Style of any padding added. + new_lines (bool, optional): Padded lines should include "\n". Defaults to False. + + Returns: + List[List[Segment]]: New list of lines. + """ + extra_lines = height - len(lines) + if not extra_lines: + return lines[:] + lines = lines[:height] + blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style) + top_lines = extra_lines // 2 + bottom_lines = extra_lines - top_lines + lines = [[blank]] * top_lines + lines + [[blank]] * bottom_lines + return lines + + @classmethod def simplify(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]: """Simplify an iterable of segments by combining contiguous segments with the same style. @@ -492,6 +588,7 @@ class Segment(NamedTuple): """ split_segments: List["Segment"] = [] add_segment = split_segments.append + iter_cuts = iter(cuts) while True: diff --git a/src/pip/_vendor/rich/syntax.py b/src/pip/_vendor/rich/syntax.py index 254963e91..58cc1037f 100644 --- a/src/pip/_vendor/rich/syntax.py +++ b/src/pip/_vendor/rich/syntax.py @@ -5,6 +5,7 @@ import textwrap from abc import ABC, abstractmethod from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Type, Union +from pip._vendor.pygments.lexer import Lexer from pip._vendor.pygments.lexers import get_lexer_by_name, guess_lexer_for_filename from pip._vendor.pygments.style import Style as PygmentsStyle from pip._vendor.pygments.styles import get_style_by_name @@ -194,7 +195,7 @@ class Syntax(JupyterMixin): Args: code (str): Code to highlight. - lexer_name (str): Lexer to use (see https://pygments.org/docs/lexers/) + lexer (Lexer | str): Lexer to use (see https://pygments.org/docs/lexers/) theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "monokai". dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False. line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False. @@ -226,7 +227,7 @@ class Syntax(JupyterMixin): def __init__( self, code: str, - lexer_name: str, + lexer: Union[Lexer, str], *, theme: Union[str, SyntaxTheme] = DEFAULT_THEME, dedent: bool = False, @@ -241,7 +242,7 @@ class Syntax(JupyterMixin): indent_guides: bool = False, ) -> None: self.code = code - self.lexer_name = lexer_name + self._lexer = lexer self.dedent = dedent self.line_numbers = line_numbers self.start_line = start_line @@ -348,6 +349,25 @@ class Syntax(JupyterMixin): style = self._theme.get_style_for_token(token_type) return style.color + @property + def lexer(self) -> Optional[Lexer]: + """The lexer for this syntax, or None if no lexer was found. + + Tries to find the lexer by name if a string was passed to the constructor. + """ + + if isinstance(self._lexer, Lexer): + return self._lexer + try: + return get_lexer_by_name( + self._lexer, + stripnl=False, + ensurenl=True, + tabsize=self.tab_size, + ) + except ClassNotFound: + return None + def highlight( self, code: str, line_range: Optional[Tuple[int, int]] = None ) -> Text: @@ -373,14 +393,10 @@ class Syntax(JupyterMixin): no_wrap=not self.word_wrap, ) _get_theme_style = self._theme.get_style_for_token - try: - lexer = get_lexer_by_name( - self.lexer_name, - stripnl=False, - ensurenl=True, - tabsize=self.tab_size, - ) - except ClassNotFound: + + lexer = self.lexer + + if lexer is None: text.append(code) else: if line_range: @@ -390,6 +406,8 @@ class Syntax(JupyterMixin): def line_tokenize() -> Iterable[Tuple[Any, str]]: """Split tokens to one per line.""" + assert lexer + for token_type, token in lexer.get_tokens(code): while token: line_token, new_line, token = token.partition("\n") @@ -698,7 +716,7 @@ if __name__ == "__main__": # pragma: no cover code = sys.stdin.read() syntax = Syntax( code=code, - lexer_name=args.lexer_name, + lexer=args.lexer_name, line_numbers=args.line_numbers, word_wrap=args.word_wrap, theme=args.theme, diff --git a/src/pip/_vendor/rich/table.py b/src/pip/_vendor/rich/table.py index 554ff4013..da4386085 100644 --- a/src/pip/_vendor/rich/table.py +++ b/src/pip/_vendor/rich/table.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field, replace from typing import ( - Dict, TYPE_CHECKING, + Dict, Iterable, List, NamedTuple, @@ -15,6 +15,7 @@ from . import box, errors from ._loop import loop_first_last, loop_last from ._pick import pick_bool from ._ratio import ratio_distribute, ratio_reduce +from .align import VerticalAlignMethod from .jupyter import JupyterMixin from .measure import Measurement from .padding import Padding, PaddingDimensions @@ -56,6 +57,9 @@ class Column: justify: "JustifyMethod" = "left" """str: How to justify text within the column ("left", "center", "right", or "full")""" + vertical: "VerticalAlignMethod" = "top" + """str: How to vertically align content ("top", "middle", or "bottom")""" + overflow: "OverflowMethod" = "ellipsis" """str: Overflow method.""" @@ -112,6 +116,8 @@ class _Cell(NamedTuple): """Style to apply to cell.""" renderable: "RenderableType" """Cell renderable.""" + vertical: VerticalAlignMethod + """Cell vertical alignment.""" class Table(JupyterMixin): @@ -335,6 +341,7 @@ class Table(JupyterMixin): footer_style: Optional[StyleType] = None, style: Optional[StyleType] = None, justify: "JustifyMethod" = "left", + vertical: "VerticalAlignMethod" = "top", overflow: "OverflowMethod" = "ellipsis", width: Optional[int] = None, min_width: Optional[int] = None, @@ -353,6 +360,7 @@ class Table(JupyterMixin): footer_style (Union[str, Style], optional): Style for the footer, or None for default. Defaults to None. style (Union[str, Style], optional): Style for the column cells, or None for default. Defaults to None. justify (JustifyMethod, optional): Alignment for cells. Defaults to "left". + vertical (VerticalAlignMethod, optional): Vertical alignment, one of "top", "middle", or "bottom". Defaults to "top". overflow (OverflowMethod): Overflow method: "crop", "fold", "ellipsis". Defaults to "ellipsis". width (int, optional): Desired width of column in characters, or None to fit to contents. Defaults to None. min_width (Optional[int], optional): Minimum width of column, or ``None`` for no minimum. Defaults to None. @@ -369,6 +377,7 @@ class Table(JupyterMixin): footer_style=footer_style or "", style=style or "", justify=justify, + vertical=vertical, overflow=overflow, width=width, min_width=min_width, @@ -636,10 +645,18 @@ class Table(JupyterMixin): if any_padding: _Padding = Padding for first, last, (style, renderable) in loop_first_last(raw_cells): - yield _Cell(style, _Padding(renderable, get_padding(first, last))) + yield _Cell( + style, + _Padding(renderable, get_padding(first, last)), + getattr(renderable, "vertical", None) or column.vertical, + ) else: for (style, renderable) in raw_cells: - yield _Cell(style, renderable) + yield _Cell( + style, + renderable, + getattr(renderable, "vertical", None) or column.vertical, + ) def _get_padding_width(self, column_index: int) -> int: """Get extra width from padding.""" @@ -770,18 +787,45 @@ class Table(JupyterMixin): overflow=column.overflow, height=None, ) - cell_style = table_style + row_style + get_style(cell.style) lines = console.render_lines( - cell.renderable, render_options, style=cell_style + cell.renderable, + render_options, + style=get_style(cell.style) + row_style, ) max_height = max(max_height, len(lines)) cells.append(lines) + row_height = max(len(cell) for cell in cells) + + def align_cell( + cell: List[List[Segment]], + vertical: "VerticalAlignMethod", + width: int, + style: Style, + ) -> List[List[Segment]]: + if header_row: + vertical = "bottom" + elif footer_row: + vertical = "top" + + if vertical == "top": + return _Segment.align_top(cell, width, row_height, style) + elif vertical == "middle": + return _Segment.align_middle(cell, width, row_height, style) + return _Segment.align_bottom(cell, width, row_height, style) + cells[:] = [ _Segment.set_shape( - _cell, width, max_height, style=table_style + row_style + align_cell( + cell, + _cell.vertical, + width, + get_style(_cell.style) + row_style, + ), + width, + max_height, ) - for width, _cell in zip(widths, cells) + for width, _cell, cell, column in zip(widths, row_cell, cells, columns) ] if _box: @@ -848,72 +892,77 @@ if __name__ == "__main__": # pragma: no cover from pip._vendor.rich.highlighter import ReprHighlighter from pip._vendor.rich.table import Table as Table - table = Table( - title="Star Wars Movies", - caption="Rich example table", - caption_justify="right", - ) + from ._timer import timer + + with timer("Table render"): + table = Table( + title="Star Wars Movies", + caption="Rich example table", + caption_justify="right", + ) - table.add_column("Released", header_style="bright_cyan", style="cyan", no_wrap=True) - table.add_column("Title", style="magenta") - table.add_column("Box Office", justify="right", style="green") + table.add_column( + "Released", header_style="bright_cyan", style="cyan", no_wrap=True + ) + table.add_column("Title", style="magenta") + table.add_column("Box Office", justify="right", style="green") - table.add_row( - "Dec 20, 2019", - "Star Wars: The Rise of Skywalker", - "$952,110,690", - ) - table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347") - table.add_row( - "Dec 15, 2017", - "Star Wars Ep. V111: The Last Jedi", - "$1,332,539,889", - style="on black", - end_section=True, - ) - table.add_row( - "Dec 16, 2016", - "Rogue One: A Star Wars Story", - "$1,332,439,889", - ) + table.add_row( + "Dec 20, 2019", + "Star Wars: The Rise of Skywalker", + "$952,110,690", + ) + table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347") + table.add_row( + "Dec 15, 2017", + "Star Wars Ep. V111: The Last Jedi", + "$1,332,539,889", + style="on black", + end_section=True, + ) + table.add_row( + "Dec 16, 2016", + "Rogue One: A Star Wars Story", + "$1,332,439,889", + ) - def header(text: str) -> None: - console.print() - console.rule(highlight(text)) - console.print() - - console = Console() - highlight = ReprHighlighter() - header("Example Table") - console.print(table, justify="center") - - table.expand = True - header("expand=True") - console.print(table) - - table.width = 50 - header("width=50") - - console.print(table, justify="center") - - table.width = None - table.expand = False - table.row_styles = ["dim", "none"] - header("row_styles=['dim', 'none']") - - console.print(table, justify="center") - - table.width = None - table.expand = False - table.row_styles = ["dim", "none"] - table.leading = 1 - header("leading=1, row_styles=['dim', 'none']") - console.print(table, justify="center") - - table.width = None - table.expand = False - table.row_styles = ["dim", "none"] - table.show_lines = True - table.leading = 0 - header("show_lines=True, row_styles=['dim', 'none']") - console.print(table, justify="center") + def header(text: str) -> None: + console.print() + console.rule(highlight(text)) + console.print() + + console = Console() + highlight = ReprHighlighter() + header("Example Table") + console.print(table, justify="center") + + table.expand = True + header("expand=True") + console.print(table) + + table.width = 50 + header("width=50") + + console.print(table, justify="center") + + table.width = None + table.expand = False + table.row_styles = ["dim", "none"] + header("row_styles=['dim', 'none']") + + console.print(table, justify="center") + + table.width = None + table.expand = False + table.row_styles = ["dim", "none"] + table.leading = 1 + header("leading=1, row_styles=['dim', 'none']") + console.print(table, justify="center") + + table.width = None + table.expand = False + table.row_styles = ["dim", "none"] + table.show_lines = True + table.leading = 0 + header("show_lines=True, row_styles=['dim', 'none']") + console.print(table, justify="center") diff --git a/src/pip/_vendor/rich/text.py b/src/pip/_vendor/rich/text.py index 5c9742d7f..ea12c09d7 100644 --- a/src/pip/_vendor/rich/text.py +++ b/src/pip/_vendor/rich/text.py @@ -1,7 +1,7 @@ import re from functools import partial, reduce from math import gcd -from operator import attrgetter, itemgetter +from operator import itemgetter from pip._vendor.rich.emoji import EmojiVariant from typing import ( TYPE_CHECKING, @@ -213,6 +213,36 @@ class Text(JupyterMixin): """Get the number of cells required to render this text.""" return cell_len(self.plain) + @property + def markup(self) -> str: + """Get console markup to render this Text. + + Returns: + str: A string potentially creating markup tags. + """ + from .markup import escape + + output: List[str] = [] + + plain = self.plain + markup_spans = [ + (0, False, self.style), + *((span.start, False, span.style) for span in self._spans), + *((span.end, True, span.style) for span in self._spans), + (len(plain), True, self.style), + ] + markup_spans.sort(key=itemgetter(0, 1)) + position = 0 + append = output.append + for offset, closing, style in markup_spans: + if offset > position: + append(escape(plain[position:offset])) + position = offset + if style: + append(f"[/{style}]" if closing else f"[{style}]") + markup = "".join(output) + return markup + @classmethod def from_markup( cls, @@ -243,6 +273,44 @@ class Text(JupyterMixin): return rendered_text @classmethod + def from_ansi( + cls, + text: str, + *, + style: Union[str, Style] = "", + justify: Optional["JustifyMethod"] = None, + overflow: Optional["OverflowMethod"] = None, + no_wrap: Optional[bool] = None, + end: str = "\n", + tab_size: Optional[int] = 8, + ) -> "Text": + """Create a Text object from a string containing ANSI escape codes. + + Args: + text (str): A string containing escape codes. + style (Union[str, Style], optional): Base style for text. Defaults to "". + justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None. + overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None. + no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None. + end (str, optional): Character to end text with. Defaults to "\\\\n". + tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8. + """ + from .ansi import AnsiDecoder + + joiner = Text( + "\n", + justify=justify, + overflow=overflow, + no_wrap=no_wrap, + end=end, + tab_size=tab_size, + style=style, + ) + decoder = AnsiDecoder() + result = joiner.join(line for line in decoder.decode(text)) + return result + + @classmethod def styled( cls, text: str, @@ -965,6 +1033,7 @@ class Text(JupyterMixin): Lines: New RichText instances between offsets. """ _offsets = list(offsets) + if not _offsets: return Lines([self.copy()]) @@ -988,33 +1057,49 @@ class Text(JupyterMixin): ) if not self._spans: return new_lines - order = {span: span_index for span_index, span in enumerate(self._spans)} - span_stack = sorted(self._spans, key=attrgetter("start"), reverse=True) - pop = span_stack.pop - push = span_stack.append + _line_appends = [line._spans.append for line in new_lines._lines] + line_count = len(line_ranges) _Span = Span - get_order = order.__getitem__ - - for line, (start, end) in zip(new_lines, line_ranges): - if not span_stack: - break - append_span = line._spans.append - position = len(span_stack) - 1 - while span_stack[position].start < end: - span = pop(position) - add_span, remaining_span = span.split(end) - if remaining_span: - push(remaining_span) - order[remaining_span] = order[span] - span_start, span_end, span_style = add_span - line_span = _Span(span_start - start, span_end - start, span_style) - order[line_span] = order[span] - append_span(line_span) - position -= 1 - if position < 0 or not span_stack: - break # pragma: no cover - line._spans.sort(key=get_order) + + for span_start, span_end, style in self._spans: + + lower_bound = 0 + upper_bound = line_count + start_line_no = (lower_bound + upper_bound) // 2 + + while True: + line_start, line_end = line_ranges[start_line_no] + if span_start < line_start: + upper_bound = start_line_no - 1 + elif span_start > line_end: + lower_bound = start_line_no + 1 + else: + break + start_line_no = (lower_bound + upper_bound) // 2 + + if span_end < line_end: + end_line_no = start_line_no + else: + end_line_no = lower_bound = start_line_no + upper_bound = line_count + + while True: + line_start, line_end = line_ranges[end_line_no] + if span_end < line_start: + upper_bound = end_line_no - 1 + elif span_end > line_end: + lower_bound = end_line_no + 1 + else: + break + end_line_no = (lower_bound + upper_bound) // 2 + + for line_no in range(start_line_no, end_line_no + 1): + line_start, line_end = line_ranges[line_no] + new_start = max(0, span_start - line_start) + new_end = min(span_end - line_start, line_end - line_start) + if new_end > new_start: + _line_appends[line_no](_Span(new_start, new_end, style)) return new_lines @@ -1179,6 +1264,7 @@ if __name__ == "__main__": # pragma: no cover text.highlight_words(["ipsum"], "italic") console = Console() + console.rule("justify='left'") console.print(text, style="red") console.print() diff --git a/src/pip/_vendor/rich/traceback.py b/src/pip/_vendor/rich/traceback.py index dbb9f1f70..66a39ebab 100644 --- a/src/pip/_vendor/rich/traceback.py +++ b/src/pip/_vendor/rich/traceback.py @@ -16,13 +16,7 @@ from pip._vendor.pygments.token import Token from . import pretty from ._loop import loop_first, loop_last from .columns import Columns -from .console import ( - Console, - ConsoleOptions, - ConsoleRenderable, - RenderResult, - group, -) +from .console import Console, ConsoleOptions, ConsoleRenderable, RenderResult, group from .constrain import Constrain from .highlighter import RegexHighlighter, ReprHighlighter from .panel import Panel @@ -135,15 +129,15 @@ def install( ) try: # pragma: no cover - # if wihin ipython, use customized traceback + # if within ipython, use customized traceback ip = get_ipython() # type: ignore ipy_excepthook_closure(ip) - return sys.excepthook # type: ignore # more strict signature that mypy can't interpret + return sys.excepthook except Exception: # otherwise use default system hook old_excepthook = sys.excepthook sys.excepthook = excepthook - return old_excepthook # type: ignore # more strict signature that mypy can't interpret + return old_excepthook @dataclass @@ -246,6 +240,9 @@ class Traceback: self.suppress: Sequence[str] = [] for suppress_entity in suppress: if not isinstance(suppress_entity, str): + assert ( + suppress_entity.__file__ is not None + ), f"{suppress_entity!r} must be a module with '__file__' attribute" path = os.path.dirname(suppress_entity.__file__) else: path = suppress_entity @@ -440,7 +437,8 @@ class Traceback: "scope.equals": token_style(Operator), "scope.key": token_style(Name), "scope.key.special": token_style(Name.Constant) + Style(dim=True), - } + }, + inherit=False, ) highlighter = ReprHighlighter() @@ -450,7 +448,7 @@ class Traceback: self._render_stack(stack), title="[traceback.title]Traceback [dim](most recent call last)", style=background_style, - border_style="traceback.border.syntax_error", + border_style="traceback.border", expand=True, padding=(0, 1), ) @@ -463,7 +461,7 @@ class Traceback: Panel( self._render_syntax_error(stack.syntax_error), style=background_style, - border_style="traceback.border", + border_style="traceback.border.syntax_error", expand=True, padding=(0, 1), width=self.width, |