From 5aa024276396ea7511450741c5349c57d586c42f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Jan 2022 16:44:26 +0000 Subject: [PATCH 01/11] Stop using functools.lru_cache decorator on instance methods --- rich/color.py | 153 ++++++++++++++++++++++--------------------- rich/palette.py | 50 +++++++------- rich/progress_bar.py | 96 +++++++++++++-------------- rich/style.py | 79 +++++++++++----------- tests/test_bar.py | 4 +- 5 files changed, 198 insertions(+), 184 deletions(-) diff --git a/rich/color.py b/rich/color.py index f0fa026d..2a686496 100644 --- a/rich/color.py +++ b/rich/color.py @@ -1,3 +1,4 @@ +import functools import platform import re from colorsys import rgb_to_hls @@ -7,14 +8,13 @@ from typing import TYPE_CHECKING, NamedTuple, Optional, Tuple from ._palettes import EIGHT_BIT_PALETTE, STANDARD_PALETTE, WINDOWS_PALETTE from .color_triplet import ColorTriplet -from .repr import rich_repr, Result +from .repr import Result, rich_repr from .terminal_theme import DEFAULT_TERMINAL_THEME if TYPE_CHECKING: # pragma: no cover from .terminal_theme import TerminalTheme from .text import Text - WINDOWS = platform.system() == "Windows" @@ -279,8 +279,8 @@ class Color(NamedTuple): def __rich__(self) -> "Text": """Dispays the actual color if Rich printed.""" - from .text import Text from .style import Style + from .text import Text return Text.assemble( f" Tuple[str, ...]: """Get the ANSI escape codes for this color.""" - _type = self.type - if _type == ColorType.DEFAULT: - return ("39" if foreground else "49",) + return _get_ansi_codes_cached(color=self, foreground=foreground) - elif _type == ColorType.WINDOWS: - number = self.number - assert number is not None - fore, back = (30, 40) if number < 8 else (82, 92) - return (str(fore + number if foreground else back + number),) - - elif _type == ColorType.STANDARD: - number = self.number - assert number is not None - fore, back = (30, 40) if number < 8 else (82, 92) - return (str(fore + number if foreground else back + number),) - - elif _type == ColorType.EIGHT_BIT: - assert self.number is not None - return ("38" if foreground else "48", "5", str(self.number)) - - else: # self.standard == ColorStandard.TRUECOLOR: - assert self.triplet is not None - red, green, blue = self.triplet - return ("38" if foreground else "48", "2", str(red), str(green), str(blue)) - - @lru_cache(maxsize=1024) def downgrade(self, system: ColorSystem) -> "Color": """Downgrade a color system to a system with fewer colors.""" + return _downgrade_cached(color=self, system=system) - if self.type in [ColorType.DEFAULT, system]: - return self - # Convert to 8-bit color from truecolor color - if system == ColorSystem.EIGHT_BIT and self.system == ColorSystem.TRUECOLOR: - assert self.triplet is not None - red, green, blue = self.triplet.normalized - _h, l, s = rgb_to_hls(red, green, blue) - # If saturation is under 10% assume it is grayscale - if s < 0.1: - gray = round(l * 25.0) - if gray == 0: - color_number = 16 - elif gray == 25: - color_number = 231 - else: - color_number = 231 + gray - return Color(self.name, ColorType.EIGHT_BIT, number=color_number) - color_number = ( - 16 + 36 * round(red * 5.0) + 6 * round(green * 5.0) + round(blue * 5.0) - ) - return Color(self.name, ColorType.EIGHT_BIT, number=color_number) +@functools.lru_cache(maxsize=1024) +def _get_ansi_codes_cached(color: Color, foreground: bool) -> Tuple[str, ...]: + _type = color.type + if _type == ColorType.DEFAULT: + return ("39" if foreground else "49",) - # Convert to standard from truecolor or 8-bit - elif system == ColorSystem.STANDARD: - if self.system == ColorSystem.TRUECOLOR: - assert self.triplet is not None - triplet = self.triplet - else: # self.system == ColorSystem.EIGHT_BIT - assert self.number is not None - triplet = ColorTriplet(*EIGHT_BIT_PALETTE[self.number]) + elif _type == ColorType.WINDOWS: + number = color.number + assert number is not None + fore, back = (30, 40) if number < 8 else (82, 92) + return (str(fore + number if foreground else back + number),) - color_number = STANDARD_PALETTE.match(triplet) - return Color(self.name, ColorType.STANDARD, number=color_number) + elif _type == ColorType.STANDARD: + number = color.number + assert number is not None + fore, back = (30, 40) if number < 8 else (82, 92) + return (str(fore + number if foreground else back + number),) - elif system == ColorSystem.WINDOWS: - if self.system == ColorSystem.TRUECOLOR: - assert self.triplet is not None - triplet = self.triplet - else: # self.system == ColorSystem.EIGHT_BIT - assert self.number is not None - if self.number < 16: - return Color(self.name, ColorType.WINDOWS, number=self.number) - triplet = ColorTriplet(*EIGHT_BIT_PALETTE[self.number]) + elif _type == ColorType.EIGHT_BIT: + assert color.number is not None + return ("38" if foreground else "48", "5", str(color.number)) - color_number = WINDOWS_PALETTE.match(triplet) - return Color(self.name, ColorType.WINDOWS, number=color_number) + else: # color.standard == ColorStandard.TRUECOLOR: + assert color.triplet is not None + red, green, blue = color.triplet + return ("38" if foreground else "48", "2", str(red), str(green), str(blue)) - return self + +@lru_cache(maxsize=1024) +def _downgrade_cached(color: Color, system: ColorSystem) -> Color: + if color.type in [ColorType.DEFAULT, system]: + return color + # Convert to 8-bit color from truecolor color + if system == ColorSystem.EIGHT_BIT and color.system == ColorSystem.TRUECOLOR: + assert color.triplet is not None + red, green, blue = color.triplet.normalized + _h, l, s = rgb_to_hls(red, green, blue) + # If saturation is under 10% assume it is grayscale + if s < 0.1: + gray = round(l * 25.0) + if gray == 0: + color_number = 16 + elif gray == 25: + color_number = 231 + else: + color_number = 231 + gray + return Color(color.name, ColorType.EIGHT_BIT, number=color_number) + + color_number = ( + 16 + 36 * round(red * 5.0) + 6 * round(green * 5.0) + round(blue * 5.0) + ) + return Color(color.name, ColorType.EIGHT_BIT, number=color_number) + + # Convert to standard from truecolor or 8-bit + elif system == ColorSystem.STANDARD: + if color.system == ColorSystem.TRUECOLOR: + assert color.triplet is not None + triplet = color.triplet + else: # color.system == ColorSystem.EIGHT_BIT + assert color.number is not None + triplet = ColorTriplet(*EIGHT_BIT_PALETTE[color.number]) + + color_number = STANDARD_PALETTE.match(triplet) + return Color(color.name, ColorType.STANDARD, number=color_number) + + elif system == ColorSystem.WINDOWS: + if color.system == ColorSystem.TRUECOLOR: + assert color.triplet is not None + triplet = color.triplet + else: # color.system == ColorSystem.EIGHT_BIT + assert color.number is not None + if color.number < 16: + return Color(color.name, ColorType.WINDOWS, number=color.number) + triplet = ColorTriplet(*EIGHT_BIT_PALETTE[color.number]) + + color_number = WINDOWS_PALETTE.match(triplet) + return Color(color.name, ColorType.WINDOWS, number=color_number) + + return color def parse_rgb_hex(hex_color: str) -> ColorTriplet: diff --git a/rich/palette.py b/rich/palette.py index f2958794..cd55dec9 100644 --- a/rich/palette.py +++ b/rich/palette.py @@ -1,6 +1,6 @@ -from math import sqrt from functools import lru_cache -from typing import Sequence, Tuple, TYPE_CHECKING +from math import sqrt +from typing import TYPE_CHECKING, Sequence, Tuple from .color_triplet import ColorTriplet @@ -20,8 +20,8 @@ class Palette: def __rich__(self) -> "Table": from rich.color import Color from rich.style import Style - from rich.text import Text from rich.table import Table + from rich.text import Text table = Table( "index", @@ -40,8 +40,6 @@ class Palette: ) return table - # This is somewhat inefficient and needs caching - @lru_cache(maxsize=1024) def match(self, color: Tuple[int, int, int]) -> int: """Find a color from a palette that most closely matches a given color. @@ -51,30 +49,38 @@ class Palette: Returns: int: Index of closes matching color. """ - red1, green1, blue1 = color - _sqrt = sqrt - get_color = self._colors.__getitem__ + return _match_palette_cached(color=color, available_colors=tuple(self._colors)) - def get_color_distance(index: int) -> float: - """Get the distance to a color.""" - red2, green2, blue2 = get_color(index) - red_mean = (red1 + red2) // 2 - red = red1 - red2 - green = green1 - green2 - blue = blue1 - blue2 - return _sqrt( - (((512 + red_mean) * red * red) >> 8) - + 4 * green * green - + (((767 - red_mean) * blue * blue) >> 8) - ) - min_index = min(range(len(self._colors)), key=get_color_distance) - return min_index +@lru_cache(maxsize=1024) +def _match_palette_cached( + color: Tuple[int, int, int], available_colors: Tuple[Tuple[int, int, int]] +) -> int: + red1, green1, blue1 = color + _sqrt = sqrt + get_color = available_colors.__getitem__ + + def get_color_distance(index: int) -> float: + """Get the distance to a color.""" + red2, green2, blue2 = get_color(index) + red_mean = (red1 + red2) // 2 + red = red1 - red2 + green = green1 - green2 + blue = blue1 - blue2 + return _sqrt( + (((512 + red_mean) * red * red) >> 8) + + 4 * green * green + + (((767 - red_mean) * blue * blue) >> 8) + ) + + min_index = min(range(len(available_colors)), key=get_color_distance) + return min_index if __name__ == "__main__": # pragma: no cover import colorsys from typing import Iterable + from rich.color import Color from rich.console import Console, ConsoleOptions from rich.segment import Segment diff --git a/rich/progress_bar.py b/rich/progress_bar.py index 1797b5f7..ce51a860 100644 --- a/rich/progress_bar.py +++ b/rich/progress_bar.py @@ -1,5 +1,5 @@ +import functools import math -from functools import lru_cache from time import monotonic from typing import Iterable, List, Optional @@ -15,6 +15,51 @@ from .style import Style, StyleType PULSE_SIZE = 20 +@functools.lru_cache(maxsize=16) +def _get_pulse_segments( + fore_style: Style, + back_style: Style, + color_system: str, + no_color: bool, + ascii: bool = False, +) -> List[Segment]: + """Get a list of segments to render a pulse animation. + + Returns: + List[Segment]: A list of segments, one segment per character. + """ + bar = "-" if ascii else "━" + segments: List[Segment] = [] + if color_system not in ("standard", "eight_bit", "truecolor") or no_color: + segments += [Segment(bar, fore_style)] * (PULSE_SIZE // 2) + segments += [Segment(" " if no_color else bar, back_style)] * ( + PULSE_SIZE - (PULSE_SIZE // 2) + ) + return segments + + append = segments.append + fore_color = ( + fore_style.color.get_truecolor() + if fore_style.color + else ColorTriplet(255, 0, 255) + ) + back_color = ( + back_style.color.get_truecolor() if back_style.color else ColorTriplet(0, 0, 0) + ) + cos = math.cos + pi = math.pi + _Segment = Segment + _Style = Style + from_triplet = Color.from_triplet + + for index in range(PULSE_SIZE): + position = index / PULSE_SIZE + fade = 0.5 + cos((position * pi * 2)) / 2.0 + color = blend_rgb(fore_color, back_color, cross_fade=fade) + append(_Segment(bar, _Style(color=from_triplet(color)))) + return segments + + class ProgressBar(JupyterMixin): """Renders a (progress) bar. Used by rich.progress. @@ -64,53 +109,6 @@ class ProgressBar(JupyterMixin): completed = min(100, max(0.0, completed)) return completed - @lru_cache(maxsize=16) - def _get_pulse_segments( - self, - fore_style: Style, - back_style: Style, - color_system: str, - no_color: bool, - ascii: bool = False, - ) -> List[Segment]: - """Get a list of segments to render a pulse animation. - - Returns: - List[Segment]: A list of segments, one segment per character. - """ - bar = "-" if ascii else "━" - segments: List[Segment] = [] - if color_system not in ("standard", "eight_bit", "truecolor") or no_color: - segments += [Segment(bar, fore_style)] * (PULSE_SIZE // 2) - segments += [Segment(" " if no_color else bar, back_style)] * ( - PULSE_SIZE - (PULSE_SIZE // 2) - ) - return segments - - append = segments.append - fore_color = ( - fore_style.color.get_truecolor() - if fore_style.color - else ColorTriplet(255, 0, 255) - ) - back_color = ( - back_style.color.get_truecolor() - if back_style.color - else ColorTriplet(0, 0, 0) - ) - cos = math.cos - pi = math.pi - _Segment = Segment - _Style = Style - from_triplet = Color.from_triplet - - for index in range(PULSE_SIZE): - position = index / PULSE_SIZE - fade = 0.5 + cos((position * pi * 2)) / 2.0 - color = blend_rgb(fore_color, back_color, cross_fade=fade) - append(_Segment(bar, _Style(color=from_triplet(color)))) - return segments - def update(self, completed: float, total: Optional[float] = None) -> None: """Update progress with new values. @@ -139,7 +137,7 @@ class ProgressBar(JupyterMixin): fore_style = console.get_style(self.pulse_style, default="white") back_style = console.get_style(self.style, default="black") - pulse_segments = self._get_pulse_segments( + pulse_segments = _get_pulse_segments( fore_style, back_style, console.color_system, console.no_color, ascii=ascii ) segment_count = len(pulse_segments) diff --git a/rich/style.py b/rich/style.py index 0787c331..4153e277 100644 --- a/rich/style.py +++ b/rich/style.py @@ -1,15 +1,14 @@ import sys from functools import lru_cache -from marshal import loads, dumps +from marshal import dumps, loads from random import randint -from typing import Any, cast, Dict, Iterable, List, Optional, Type, Union +from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast from . import errors from .color import Color, ColorParseError, ColorSystem, blend_rgb -from .repr import rich_repr, Result +from .repr import Result, rich_repr from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme - # Style instances and style definitions are often interchangeable StyleType = Union[str, "Style"] @@ -575,42 +574,9 @@ class Style: style = Style(color=color, bgcolor=bgcolor, link=link, **attributes) return style - @lru_cache(maxsize=1024) def get_html_style(self, theme: Optional[TerminalTheme] = None) -> str: """Get a CSS style rule.""" - theme = theme or DEFAULT_TERMINAL_THEME - css: List[str] = [] - append = css.append - - color = self.color - bgcolor = self.bgcolor - if self.reverse: - color, bgcolor = bgcolor, color - if self.dim: - foreground_color = ( - theme.foreground_color if color is None else color.get_truecolor(theme) - ) - color = Color.from_triplet( - blend_rgb(foreground_color, theme.background_color, 0.5) - ) - if color is not None: - theme_color = color.get_truecolor(theme) - append(f"color: {theme_color.hex}") - append(f"text-decoration-color: {theme_color.hex}") - if bgcolor is not None: - theme_color = bgcolor.get_truecolor(theme, foreground=False) - append(f"background-color: {theme_color.hex}") - if self.bold: - append("font-weight: bold") - if self.italic: - append("font-style: italic") - if self.underline: - append("text-decoration: underline") - if self.strike: - append("text-decoration: line-through") - if self.overline: - append("text-decoration: overline") - return "; ".join(css) + return _get_html_style_cached(self, theme) @classmethod def combine(cls, styles: Iterable["Style"]) -> "Style": @@ -751,6 +717,43 @@ class Style: NULL_STYLE = Style() +@lru_cache(maxsize=1024) +def _get_html_style_cached(style: Style, theme: Optional[TerminalTheme] = None) -> str: + theme = theme or DEFAULT_TERMINAL_THEME + css: List[str] = [] + append = css.append + + color = style.color + bgcolor = style.bgcolor + if style.reverse: + color, bgcolor = bgcolor, color + if style.dim: + foreground_color = ( + theme.foreground_color if color is None else color.get_truecolor(theme) + ) + color = Color.from_triplet( + blend_rgb(foreground_color, theme.background_color, 0.5) + ) + if color is not None: + theme_color = color.get_truecolor(theme) + append(f"color: {theme_color.hex}") + append(f"text-decoration-color: {theme_color.hex}") + if bgcolor is not None: + theme_color = bgcolor.get_truecolor(theme, foreground=False) + append(f"background-color: {theme_color.hex}") + if style.bold: + append("font-weight: bold") + if style.italic: + append("font-style: italic") + if style.underline: + append("text-decoration: underline") + if style.strike: + append("text-decoration: line-through") + if style.overline: + append("text-decoration: overline") + return "; ".join(css) + + class StyleStack: """A stack of styles.""" diff --git a/tests/test_bar.py b/tests/test_bar.py index 021a8aaa..5d36a8ca 100644 --- a/tests/test_bar.py +++ b/tests/test_bar.py @@ -1,5 +1,5 @@ from rich.console import Console -from rich.progress_bar import ProgressBar +from rich.progress_bar import ProgressBar, _get_pulse_segments from rich.segment import Segment from rich.style import Style @@ -63,7 +63,7 @@ def test_pulse(): def test_get_pulse_segments(): bar = ProgressBar() - segments = bar._get_pulse_segments( + segments = _get_pulse_segments( Style.parse("red"), Style.parse("yellow"), None, False, False ) print(repr(segments)) From 95c0b168bf115e1dfb077f8442a8d4b792fcd979 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Jan 2022 16:46:36 +0000 Subject: [PATCH 02/11] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d15e5f3..15ce46de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +- Stop using `functools.lru_cache` decorator on instance methods + ## [11.0.0] - 2022-01-09 ### Added From 5957b960b5f40fa322552b9a8477b9b83743d09f Mon Sep 17 00:00:00 2001 From: Peder Bergebakken Sundt Date: Sun, 16 Jan 2022 15:04:30 +0100 Subject: [PATCH 03/11] Collapse platform details in bug reports To reduce noise. Example: https://github.com/Textualize/rich/issues/1838 --- .github/ISSUE_TEMPLATE/bug_report.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c72dc5ae..05cd2cd1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -15,6 +15,8 @@ Edit this with a clear and concise description of what the bug. Provide a minimal code example that demonstrates the issue if you can. If the issue is visual in nature, consider posting a screenshot. **Platform** +
+Click to expand What platform (Win/Linux/Mac) are you running on? What terminal software are you using? @@ -25,3 +27,5 @@ python -m rich.diagnose python -m rich._windows pip freeze | grep rich ``` + +
From 12ecab7400a33db83284c44b5da1f84124b18407 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jan 2022 13:30:43 +0000 Subject: [PATCH 04/11] Bump sphinx from 4.3.2 to 4.4.0 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.3.2 to 4.4.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.3.2...v4.4.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 716b1b3d..84985477 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ alabaster==0.7.12 -Sphinx==4.3.2 +Sphinx==4.4.0 sphinx-rtd-theme==1.0.0 sphinx-copybutton==0.4.0 From 106225f22c9546f3a5abdadb9e0d25871cfaf3f4 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 17 Jan 2022 15:40:13 +0000 Subject: [PATCH 05/11] Check for `__class__ before calling isinstance` --- rich/_inspect.py | 5 +++-- rich/pretty.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rich/_inspect.py b/rich/_inspect.py index 262695b1..7089701b 100644 --- a/rich/_inspect.py +++ b/rich/_inspect.py @@ -3,7 +3,7 @@ from __future__ import absolute_import from inspect import cleandoc, getdoc, getfile, isclass, ismodule, signature from typing import Any, Iterable, Optional, Tuple -from .console import RenderableType, Group +from .console import Group, RenderableType from .highlighter import ReprHighlighter from .jupyter import JupyterMixin from .panel import Panel @@ -206,5 +206,6 @@ class Inspect(JupyterMixin): yield items_table else: yield Text.from_markup( - f"[b cyan]{not_shown_count}[/][i] attribute(s) not shown.[/i] Run [b][magenta]inspect[/]([not b]inspect[/])[/b] for options." + f"[b cyan]{not_shown_count}[/][i] attribute(s) not shown.[/i] " + f"Run [b][magenta]inspect[/]([not b]inspect[/])[/b] for options." ) diff --git a/rich/pretty.py b/rich/pretty.py index 57e743df..45cd3a00 100644 --- a/rich/pretty.py +++ b/rich/pretty.py @@ -722,7 +722,7 @@ def traverse( pop_visited(obj_id) - elif isinstance(obj, _CONTAINERS): + elif hasattr(obj, "__class__") and isinstance(obj, _CONTAINERS): for container_type in _CONTAINERS: if isinstance(obj, container_type): obj_type = container_type From fde74c284a2620e16b64ed9b9fe651b94d93318f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 17 Jan 2022 17:53:53 +0000 Subject: [PATCH 06/11] Delete a redundant test --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0de277c2..330854f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Fix width measurement of 353 emoji variation sequences https://github.com/Textualize/rich/pull/1832 +- Workaround for strange edge case of object from Faiss with no `__class__` ## [11.0.0] - 2022-01-09 From 7a2c37f077fccf5c741a28135005457d8d18d7ce Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 18 Jan 2022 11:42:22 +0000 Subject: [PATCH 07/11] Handle exceptions thrown from isinstance --- rich/pretty.py | 41 +++++++++++++++++++++++++---------------- tests/test_inspect.py | 17 ++++++++++++++++- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/rich/pretty.py b/rich/pretty.py index 45cd3a00..f42434ef 100644 --- a/rich/pretty.py +++ b/rich/pretty.py @@ -2,7 +2,6 @@ import builtins import dataclasses import inspect import os -import re import sys from array import array from collections import Counter, UserDict, UserList, defaultdict, deque @@ -93,7 +92,7 @@ def _ipy_display_hook( 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: + if _safe_isinstance(value, JupyterRenderable) or value is None: return console = console or get_console() @@ -124,12 +123,12 @@ def _ipy_display_hook( return # Delegate rendering to IPython # certain renderables should start on a new line - if isinstance(value, ConsoleRenderable): + if _safe_isinstance(value, ConsoleRenderable): console.line() console.print( value - if isinstance(value, RichRenderable) + if _safe_isinstance(value, RichRenderable) else Pretty( value, overflow=overflow, @@ -144,6 +143,16 @@ def _ipy_display_hook( ) +def _safe_isinstance( + obj: object, class_or_tuple: Union[type, Tuple[type, ...]] +) -> bool: + """isinstance can fail in rare cases, for example types with no __class__""" + try: + return isinstance(obj, class_or_tuple) + except Exception: + return False + + def install( console: Optional["Console"] = None, overflow: "OverflowMethod" = "ignore", @@ -178,7 +187,7 @@ def install( builtins._ = None # type: ignore console.print( value - if isinstance(value, RichRenderable) + if _safe_isinstance(value, RichRenderable) else Pretty( value, overflow=overflow, @@ -355,7 +364,7 @@ _MAPPING_CONTAINERS = (dict, os._Environ, MappingProxyType, UserDict) def is_expandable(obj: Any) -> bool: """Check if an object may be expanded by pretty print.""" return ( - isinstance(obj, _CONTAINERS) + _safe_isinstance(obj, _CONTAINERS) or (is_dataclass(obj)) or (hasattr(obj, "__rich_repr__")) or _is_attr_object(obj) @@ -539,7 +548,7 @@ def traverse( """Get repr string for an object, but catch errors.""" if ( max_string is not None - and isinstance(obj, (bytes, str)) + and _safe_isinstance(obj, (bytes, str)) and len(obj) > max_string ): truncated = len(obj) - max_string @@ -565,7 +574,7 @@ def traverse( def iter_rich_args(rich_args: Any) -> Iterable[Union[Any, Tuple[str, Any]]]: for arg in rich_args: - if isinstance(arg, tuple): + if _safe_isinstance(arg, tuple): if len(arg) == 3: key, child, default = arg if default == child: @@ -622,7 +631,7 @@ def traverse( last=root, ) for last, arg in loop_last(args): - if isinstance(arg, tuple): + if _safe_isinstance(arg, tuple): key, child = arg child_node = _traverse(child, depth=depth + 1) child_node.last = last @@ -689,7 +698,7 @@ def traverse( elif ( is_dataclass(obj) - and not isinstance(obj, type) + and not _safe_isinstance(obj, type) and not fake_attributes and (_is_dataclass_repr(obj) or py_version == (3, 6)) ): @@ -722,9 +731,9 @@ def traverse( pop_visited(obj_id) - elif hasattr(obj, "__class__") and isinstance(obj, _CONTAINERS): + elif _safe_isinstance(obj, _CONTAINERS): for container_type in _CONTAINERS: - if isinstance(obj, container_type): + if _safe_isinstance(obj, container_type): obj_type = container_type break @@ -752,7 +761,7 @@ def traverse( num_items = len(obj) last_item_index = num_items - 1 - if isinstance(obj, _MAPPING_CONTAINERS): + if _safe_isinstance(obj, _MAPPING_CONTAINERS): iter_items = iter(obj.items()) if max_length is not None: iter_items = islice(iter_items, max_length) @@ -777,7 +786,7 @@ def traverse( pop_visited(obj_id) else: node = Node(value_repr=to_repr(obj), last=root) - node.is_tuple = isinstance(obj, tuple) + node.is_tuple = _safe_isinstance(obj, tuple) return node node = _traverse(_object, root=True) @@ -812,13 +821,13 @@ def pretty_repr( str: A possibly multi-line representation of the object. """ - if isinstance(_object, Node): + if _safe_isinstance(_object, Node): node = _object else: node = traverse( _object, max_length=max_length, max_string=max_string, max_depth=max_depth ) - repr_str = node.render( + repr_str: str = node.render( max_width=max_width, indent_size=indent_size, expand_all=expand_all ) return repr_str diff --git a/tests/test_inspect.py b/tests/test_inspect.py index 5a9a5761..e947a7d6 100644 --- a/tests/test_inspect.py +++ b/tests/test_inspect.py @@ -1,12 +1,12 @@ import io import sys +from unittest import mock import pytest from rich import inspect from rich.console import Console - skip_py36 = pytest.mark.skipif( sys.version_info.minor == 6 and sys.version_info.major == 3, reason="rendered differently on py3.6", @@ -260,3 +260,18 @@ def test_broken_call_attr(): result = render(foo, methods=True, width=100) print(repr(result)) assert expected == result + + +def test_inspect_swig_edge_case(): + """Issue #1838 - Edge case with Faiss library - object with empty dir()""" + + class Thing: + @property + def __class__(self): + raise AttributeError + + thing = Thing() + try: + inspect(thing) + except Exception as e: + assert False, f"Object with no __class__ shouldn't raise {e}" From 3944c46ee75267862fd74ae178c85bda983acfc7 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 18 Jan 2022 11:47:33 +0000 Subject: [PATCH 08/11] Add changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 330854f9..34ef0c3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Fix width measurement of 353 emoji variation sequences https://github.com/Textualize/rich/pull/1832 -- Workaround for strange edge case of object from Faiss with no `__class__` +- Workaround for edge case of object from Faiss with no `__class__` https://github.com/Textualize/rich/issues/1838 ## [11.0.0] - 2022-01-09 From 8c3c2e712a2b89d0084a4b77a674f45134e34937 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 18 Jan 2022 12:00:24 +0000 Subject: [PATCH 09/11] Handle case of no attr object in output --- rich/_inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rich/_inspect.py b/rich/_inspect.py index 7089701b..07b1b623 100644 --- a/rich/_inspect.py +++ b/rich/_inspect.py @@ -204,7 +204,7 @@ class Inspect(JupyterMixin): add_row(key_text, Pretty(value, highlighter=highlighter)) if items_table.row_count: yield items_table - else: + elif not_shown_count: yield Text.from_markup( f"[b cyan]{not_shown_count}[/][i] attribute(s) not shown.[/i] " f"Run [b][magenta]inspect[/]([not b]inspect[/])[/b] for options." From 902f7085edd97f88feda88f2ae95d834852c86d1 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 19 Jan 2022 10:51:12 +0000 Subject: [PATCH 10/11] Revert "Stop using functools.lru_cache decorator on instance methods" --- CHANGELOG.md | 2 +- rich/color.py | 153 +++++++++++++++++++++---------------------- rich/palette.py | 50 +++++++------- rich/progress_bar.py | 96 ++++++++++++++------------- rich/style.py | 79 +++++++++++----------- tests/test_bar.py | 4 +- 6 files changed, 185 insertions(+), 199 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b756e29d..0de277c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Stop using `functools.lru_cache` decorator on instance methods + - Fix width measurement of 353 emoji variation sequences https://github.com/Textualize/rich/pull/1832 ## [11.0.0] - 2022-01-09 diff --git a/rich/color.py b/rich/color.py index 2a686496..f0fa026d 100644 --- a/rich/color.py +++ b/rich/color.py @@ -1,4 +1,3 @@ -import functools import platform import re from colorsys import rgb_to_hls @@ -8,13 +7,14 @@ from typing import TYPE_CHECKING, NamedTuple, Optional, Tuple from ._palettes import EIGHT_BIT_PALETTE, STANDARD_PALETTE, WINDOWS_PALETTE from .color_triplet import ColorTriplet -from .repr import Result, rich_repr +from .repr import rich_repr, Result from .terminal_theme import DEFAULT_TERMINAL_THEME if TYPE_CHECKING: # pragma: no cover from .terminal_theme import TerminalTheme from .text import Text + WINDOWS = platform.system() == "Windows" @@ -279,8 +279,8 @@ class Color(NamedTuple): def __rich__(self) -> "Text": """Dispays the actual color if Rich printed.""" - from .style import Style from .text import Text + from .style import Style return Text.assemble( f" Tuple[str, ...]: """Get the ANSI escape codes for this color.""" - return _get_ansi_codes_cached(color=self, foreground=foreground) + _type = self.type + if _type == ColorType.DEFAULT: + return ("39" if foreground else "49",) + elif _type == ColorType.WINDOWS: + number = self.number + assert number is not None + fore, back = (30, 40) if number < 8 else (82, 92) + return (str(fore + number if foreground else back + number),) + + elif _type == ColorType.STANDARD: + number = self.number + assert number is not None + fore, back = (30, 40) if number < 8 else (82, 92) + return (str(fore + number if foreground else back + number),) + + elif _type == ColorType.EIGHT_BIT: + assert self.number is not None + return ("38" if foreground else "48", "5", str(self.number)) + + else: # self.standard == ColorStandard.TRUECOLOR: + assert self.triplet is not None + red, green, blue = self.triplet + return ("38" if foreground else "48", "2", str(red), str(green), str(blue)) + + @lru_cache(maxsize=1024) def downgrade(self, system: ColorSystem) -> "Color": """Downgrade a color system to a system with fewer colors.""" - return _downgrade_cached(color=self, system=system) + if self.type in [ColorType.DEFAULT, system]: + return self + # Convert to 8-bit color from truecolor color + if system == ColorSystem.EIGHT_BIT and self.system == ColorSystem.TRUECOLOR: + assert self.triplet is not None + red, green, blue = self.triplet.normalized + _h, l, s = rgb_to_hls(red, green, blue) + # If saturation is under 10% assume it is grayscale + if s < 0.1: + gray = round(l * 25.0) + if gray == 0: + color_number = 16 + elif gray == 25: + color_number = 231 + else: + color_number = 231 + gray + return Color(self.name, ColorType.EIGHT_BIT, number=color_number) -@functools.lru_cache(maxsize=1024) -def _get_ansi_codes_cached(color: Color, foreground: bool) -> Tuple[str, ...]: - _type = color.type - if _type == ColorType.DEFAULT: - return ("39" if foreground else "49",) + color_number = ( + 16 + 36 * round(red * 5.0) + 6 * round(green * 5.0) + round(blue * 5.0) + ) + return Color(self.name, ColorType.EIGHT_BIT, number=color_number) - elif _type == ColorType.WINDOWS: - number = color.number - assert number is not None - fore, back = (30, 40) if number < 8 else (82, 92) - return (str(fore + number if foreground else back + number),) + # Convert to standard from truecolor or 8-bit + elif system == ColorSystem.STANDARD: + if self.system == ColorSystem.TRUECOLOR: + assert self.triplet is not None + triplet = self.triplet + else: # self.system == ColorSystem.EIGHT_BIT + assert self.number is not None + triplet = ColorTriplet(*EIGHT_BIT_PALETTE[self.number]) - elif _type == ColorType.STANDARD: - number = color.number - assert number is not None - fore, back = (30, 40) if number < 8 else (82, 92) - return (str(fore + number if foreground else back + number),) + color_number = STANDARD_PALETTE.match(triplet) + return Color(self.name, ColorType.STANDARD, number=color_number) - elif _type == ColorType.EIGHT_BIT: - assert color.number is not None - return ("38" if foreground else "48", "5", str(color.number)) + elif system == ColorSystem.WINDOWS: + if self.system == ColorSystem.TRUECOLOR: + assert self.triplet is not None + triplet = self.triplet + else: # self.system == ColorSystem.EIGHT_BIT + assert self.number is not None + if self.number < 16: + return Color(self.name, ColorType.WINDOWS, number=self.number) + triplet = ColorTriplet(*EIGHT_BIT_PALETTE[self.number]) - else: # color.standard == ColorStandard.TRUECOLOR: - assert color.triplet is not None - red, green, blue = color.triplet - return ("38" if foreground else "48", "2", str(red), str(green), str(blue)) + color_number = WINDOWS_PALETTE.match(triplet) + return Color(self.name, ColorType.WINDOWS, number=color_number) - -@lru_cache(maxsize=1024) -def _downgrade_cached(color: Color, system: ColorSystem) -> Color: - if color.type in [ColorType.DEFAULT, system]: - return color - # Convert to 8-bit color from truecolor color - if system == ColorSystem.EIGHT_BIT and color.system == ColorSystem.TRUECOLOR: - assert color.triplet is not None - red, green, blue = color.triplet.normalized - _h, l, s = rgb_to_hls(red, green, blue) - # If saturation is under 10% assume it is grayscale - if s < 0.1: - gray = round(l * 25.0) - if gray == 0: - color_number = 16 - elif gray == 25: - color_number = 231 - else: - color_number = 231 + gray - return Color(color.name, ColorType.EIGHT_BIT, number=color_number) - - color_number = ( - 16 + 36 * round(red * 5.0) + 6 * round(green * 5.0) + round(blue * 5.0) - ) - return Color(color.name, ColorType.EIGHT_BIT, number=color_number) - - # Convert to standard from truecolor or 8-bit - elif system == ColorSystem.STANDARD: - if color.system == ColorSystem.TRUECOLOR: - assert color.triplet is not None - triplet = color.triplet - else: # color.system == ColorSystem.EIGHT_BIT - assert color.number is not None - triplet = ColorTriplet(*EIGHT_BIT_PALETTE[color.number]) - - color_number = STANDARD_PALETTE.match(triplet) - return Color(color.name, ColorType.STANDARD, number=color_number) - - elif system == ColorSystem.WINDOWS: - if color.system == ColorSystem.TRUECOLOR: - assert color.triplet is not None - triplet = color.triplet - else: # color.system == ColorSystem.EIGHT_BIT - assert color.number is not None - if color.number < 16: - return Color(color.name, ColorType.WINDOWS, number=color.number) - triplet = ColorTriplet(*EIGHT_BIT_PALETTE[color.number]) - - color_number = WINDOWS_PALETTE.match(triplet) - return Color(color.name, ColorType.WINDOWS, number=color_number) - - return color + return self def parse_rgb_hex(hex_color: str) -> ColorTriplet: diff --git a/rich/palette.py b/rich/palette.py index cd55dec9..f2958794 100644 --- a/rich/palette.py +++ b/rich/palette.py @@ -1,6 +1,6 @@ -from functools import lru_cache from math import sqrt -from typing import TYPE_CHECKING, Sequence, Tuple +from functools import lru_cache +from typing import Sequence, Tuple, TYPE_CHECKING from .color_triplet import ColorTriplet @@ -20,8 +20,8 @@ class Palette: def __rich__(self) -> "Table": from rich.color import Color from rich.style import Style - from rich.table import Table from rich.text import Text + from rich.table import Table table = Table( "index", @@ -40,6 +40,8 @@ class Palette: ) return table + # This is somewhat inefficient and needs caching + @lru_cache(maxsize=1024) def match(self, color: Tuple[int, int, int]) -> int: """Find a color from a palette that most closely matches a given color. @@ -49,38 +51,30 @@ class Palette: Returns: int: Index of closes matching color. """ - return _match_palette_cached(color=color, available_colors=tuple(self._colors)) + red1, green1, blue1 = color + _sqrt = sqrt + get_color = self._colors.__getitem__ + def get_color_distance(index: int) -> float: + """Get the distance to a color.""" + red2, green2, blue2 = get_color(index) + red_mean = (red1 + red2) // 2 + red = red1 - red2 + green = green1 - green2 + blue = blue1 - blue2 + return _sqrt( + (((512 + red_mean) * red * red) >> 8) + + 4 * green * green + + (((767 - red_mean) * blue * blue) >> 8) + ) -@lru_cache(maxsize=1024) -def _match_palette_cached( - color: Tuple[int, int, int], available_colors: Tuple[Tuple[int, int, int]] -) -> int: - red1, green1, blue1 = color - _sqrt = sqrt - get_color = available_colors.__getitem__ - - def get_color_distance(index: int) -> float: - """Get the distance to a color.""" - red2, green2, blue2 = get_color(index) - red_mean = (red1 + red2) // 2 - red = red1 - red2 - green = green1 - green2 - blue = blue1 - blue2 - return _sqrt( - (((512 + red_mean) * red * red) >> 8) - + 4 * green * green - + (((767 - red_mean) * blue * blue) >> 8) - ) - - min_index = min(range(len(available_colors)), key=get_color_distance) - return min_index + min_index = min(range(len(self._colors)), key=get_color_distance) + return min_index if __name__ == "__main__": # pragma: no cover import colorsys from typing import Iterable - from rich.color import Color from rich.console import Console, ConsoleOptions from rich.segment import Segment diff --git a/rich/progress_bar.py b/rich/progress_bar.py index ce51a860..1797b5f7 100644 --- a/rich/progress_bar.py +++ b/rich/progress_bar.py @@ -1,5 +1,5 @@ -import functools import math +from functools import lru_cache from time import monotonic from typing import Iterable, List, Optional @@ -15,51 +15,6 @@ from .style import Style, StyleType PULSE_SIZE = 20 -@functools.lru_cache(maxsize=16) -def _get_pulse_segments( - fore_style: Style, - back_style: Style, - color_system: str, - no_color: bool, - ascii: bool = False, -) -> List[Segment]: - """Get a list of segments to render a pulse animation. - - Returns: - List[Segment]: A list of segments, one segment per character. - """ - bar = "-" if ascii else "━" - segments: List[Segment] = [] - if color_system not in ("standard", "eight_bit", "truecolor") or no_color: - segments += [Segment(bar, fore_style)] * (PULSE_SIZE // 2) - segments += [Segment(" " if no_color else bar, back_style)] * ( - PULSE_SIZE - (PULSE_SIZE // 2) - ) - return segments - - append = segments.append - fore_color = ( - fore_style.color.get_truecolor() - if fore_style.color - else ColorTriplet(255, 0, 255) - ) - back_color = ( - back_style.color.get_truecolor() if back_style.color else ColorTriplet(0, 0, 0) - ) - cos = math.cos - pi = math.pi - _Segment = Segment - _Style = Style - from_triplet = Color.from_triplet - - for index in range(PULSE_SIZE): - position = index / PULSE_SIZE - fade = 0.5 + cos((position * pi * 2)) / 2.0 - color = blend_rgb(fore_color, back_color, cross_fade=fade) - append(_Segment(bar, _Style(color=from_triplet(color)))) - return segments - - class ProgressBar(JupyterMixin): """Renders a (progress) bar. Used by rich.progress. @@ -109,6 +64,53 @@ class ProgressBar(JupyterMixin): completed = min(100, max(0.0, completed)) return completed + @lru_cache(maxsize=16) + def _get_pulse_segments( + self, + fore_style: Style, + back_style: Style, + color_system: str, + no_color: bool, + ascii: bool = False, + ) -> List[Segment]: + """Get a list of segments to render a pulse animation. + + Returns: + List[Segment]: A list of segments, one segment per character. + """ + bar = "-" if ascii else "━" + segments: List[Segment] = [] + if color_system not in ("standard", "eight_bit", "truecolor") or no_color: + segments += [Segment(bar, fore_style)] * (PULSE_SIZE // 2) + segments += [Segment(" " if no_color else bar, back_style)] * ( + PULSE_SIZE - (PULSE_SIZE // 2) + ) + return segments + + append = segments.append + fore_color = ( + fore_style.color.get_truecolor() + if fore_style.color + else ColorTriplet(255, 0, 255) + ) + back_color = ( + back_style.color.get_truecolor() + if back_style.color + else ColorTriplet(0, 0, 0) + ) + cos = math.cos + pi = math.pi + _Segment = Segment + _Style = Style + from_triplet = Color.from_triplet + + for index in range(PULSE_SIZE): + position = index / PULSE_SIZE + fade = 0.5 + cos((position * pi * 2)) / 2.0 + color = blend_rgb(fore_color, back_color, cross_fade=fade) + append(_Segment(bar, _Style(color=from_triplet(color)))) + return segments + def update(self, completed: float, total: Optional[float] = None) -> None: """Update progress with new values. @@ -137,7 +139,7 @@ class ProgressBar(JupyterMixin): fore_style = console.get_style(self.pulse_style, default="white") back_style = console.get_style(self.style, default="black") - pulse_segments = _get_pulse_segments( + pulse_segments = self._get_pulse_segments( fore_style, back_style, console.color_system, console.no_color, ascii=ascii ) segment_count = len(pulse_segments) diff --git a/rich/style.py b/rich/style.py index 4153e277..0787c331 100644 --- a/rich/style.py +++ b/rich/style.py @@ -1,14 +1,15 @@ import sys from functools import lru_cache -from marshal import dumps, loads +from marshal import loads, dumps from random import randint -from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast +from typing import Any, cast, Dict, Iterable, List, Optional, Type, Union from . import errors from .color import Color, ColorParseError, ColorSystem, blend_rgb -from .repr import Result, rich_repr +from .repr import rich_repr, Result from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme + # Style instances and style definitions are often interchangeable StyleType = Union[str, "Style"] @@ -574,9 +575,42 @@ class Style: style = Style(color=color, bgcolor=bgcolor, link=link, **attributes) return style + @lru_cache(maxsize=1024) def get_html_style(self, theme: Optional[TerminalTheme] = None) -> str: """Get a CSS style rule.""" - return _get_html_style_cached(self, theme) + theme = theme or DEFAULT_TERMINAL_THEME + css: List[str] = [] + append = css.append + + color = self.color + bgcolor = self.bgcolor + if self.reverse: + color, bgcolor = bgcolor, color + if self.dim: + foreground_color = ( + theme.foreground_color if color is None else color.get_truecolor(theme) + ) + color = Color.from_triplet( + blend_rgb(foreground_color, theme.background_color, 0.5) + ) + if color is not None: + theme_color = color.get_truecolor(theme) + append(f"color: {theme_color.hex}") + append(f"text-decoration-color: {theme_color.hex}") + if bgcolor is not None: + theme_color = bgcolor.get_truecolor(theme, foreground=False) + append(f"background-color: {theme_color.hex}") + if self.bold: + append("font-weight: bold") + if self.italic: + append("font-style: italic") + if self.underline: + append("text-decoration: underline") + if self.strike: + append("text-decoration: line-through") + if self.overline: + append("text-decoration: overline") + return "; ".join(css) @classmethod def combine(cls, styles: Iterable["Style"]) -> "Style": @@ -717,43 +751,6 @@ class Style: NULL_STYLE = Style() -@lru_cache(maxsize=1024) -def _get_html_style_cached(style: Style, theme: Optional[TerminalTheme] = None) -> str: - theme = theme or DEFAULT_TERMINAL_THEME - css: List[str] = [] - append = css.append - - color = style.color - bgcolor = style.bgcolor - if style.reverse: - color, bgcolor = bgcolor, color - if style.dim: - foreground_color = ( - theme.foreground_color if color is None else color.get_truecolor(theme) - ) - color = Color.from_triplet( - blend_rgb(foreground_color, theme.background_color, 0.5) - ) - if color is not None: - theme_color = color.get_truecolor(theme) - append(f"color: {theme_color.hex}") - append(f"text-decoration-color: {theme_color.hex}") - if bgcolor is not None: - theme_color = bgcolor.get_truecolor(theme, foreground=False) - append(f"background-color: {theme_color.hex}") - if style.bold: - append("font-weight: bold") - if style.italic: - append("font-style: italic") - if style.underline: - append("text-decoration: underline") - if style.strike: - append("text-decoration: line-through") - if style.overline: - append("text-decoration: overline") - return "; ".join(css) - - class StyleStack: """A stack of styles.""" diff --git a/tests/test_bar.py b/tests/test_bar.py index 5d36a8ca..021a8aaa 100644 --- a/tests/test_bar.py +++ b/tests/test_bar.py @@ -1,5 +1,5 @@ from rich.console import Console -from rich.progress_bar import ProgressBar, _get_pulse_segments +from rich.progress_bar import ProgressBar from rich.segment import Segment from rich.style import Style @@ -63,7 +63,7 @@ def test_pulse(): def test_get_pulse_segments(): bar = ProgressBar() - segments = _get_pulse_segments( + segments = bar._get_pulse_segments( Style.parse("red"), Style.parse("yellow"), None, False, False ) print(repr(segments)) From 4d1a1a94196a55d55a4a9c060f8e1e6e81b5e23f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 19 Jan 2022 15:42:35 +0000 Subject: [PATCH 11/11] Remove unused import --- tests/test_inspect.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_inspect.py b/tests/test_inspect.py index e947a7d6..05a78b87 100644 --- a/tests/test_inspect.py +++ b/tests/test_inspect.py @@ -1,6 +1,5 @@ import io import sys -from unittest import mock import pytest