diff options
| author | Pradyun Gedam <pgedam@bloomberg.net> | 2022-04-22 15:46:11 +0100 |
|---|---|---|
| committer | Pradyun Gedam <pgedam@bloomberg.net> | 2022-04-22 15:50:29 +0100 |
| commit | 377d642384befe47e96e87550d9a12cbe8f13413 (patch) | |
| tree | d474fe20cb825df21cb0988dc973facc73ab02d0 | |
| parent | dfcac4add2801ea078f976b8fba3b24fa246c1ce (diff) | |
| download | pip-377d642384befe47e96e87550d9a12cbe8f13413.tar.gz | |
Upgrade rich to 12.2.0
46 files changed, 2400 insertions, 860 deletions
diff --git a/news/rich.vendor.rst b/news/rich.vendor.rst new file mode 100644 index 000000000..cc778ecef --- /dev/null +++ b/news/rich.vendor.rst @@ -0,0 +1 @@ +Upgrade rich to 12.2.0 diff --git a/src/pip/_vendor/rich/__init__.py b/src/pip/_vendor/rich/__init__.py index 50f381576..657811b5e 100644 --- a/src/pip/_vendor/rich/__init__.py +++ b/src/pip/_vendor/rich/__init__.py @@ -1,9 +1,9 @@ """Rich text and beautiful formatting in the terminal.""" import os -from typing import Callable, IO, TYPE_CHECKING, Any, Optional +from typing import Callable, IO, TYPE_CHECKING, Any, Optional, Union -from ._extension import load_ipython_extension +from ._extension import load_ipython_extension # noqa: F401 __all__ = ["get_console", "reconfigure", "print", "inspect"] @@ -73,7 +73,7 @@ def print_json( json: Optional[str] = None, *, data: Any = None, - indent: int = 2, + indent: Union[None, int, str] = 2, highlight: bool = True, skip_keys: bool = False, ensure_ascii: bool = True, diff --git a/src/pip/_vendor/rich/__main__.py b/src/pip/_vendor/rich/__main__.py index 8692d37e0..54e6d5e8a 100644 --- a/src/pip/_vendor/rich/__main__.py +++ b/src/pip/_vendor/rich/__main__.py @@ -51,7 +51,6 @@ def make_test_card() -> Table: pad_edge=False, ) color_table.add_row( - # "[bold yellow]256[/] colors or [bold green]16.7 million[/] colors [blue](if supported by your terminal)[/].", ( "✓ [bold green]4-bit color[/]\n" "✓ [bold blue]8-bit color[/]\n" @@ -226,10 +225,12 @@ if __name__ == "__main__": # pragma: no cover console.print(test_card) taken = round((process_time() - start) * 1000.0, 1) - text = console.file.getvalue() - # https://bugs.python.org/issue37871 - for line in text.splitlines(True): - print(line, end="") + c = Console(record=True) + c.print(test_card) + # c.save_svg( + # path="/Users/darrenburns/Library/Application Support/JetBrains/PyCharm2021.3/scratches/svg_export.svg", + # title="Rich can export to SVG", + # ) print(f"rendered in {pre_cache_taken}ms (cold cache)") print(f"rendered in {taken}ms (warm cache)") @@ -243,6 +244,10 @@ if __name__ == "__main__": # pragma: no cover sponsor_message.add_column(no_wrap=True) sponsor_message.add_row( + "Textualize", + "[u blue link=https://github.com/textualize]https://github.com/textualize", + ) + sponsor_message.add_row( "Buy devs a :coffee:", "[u blue link=https://ko-fi.com/textualize]https://ko-fi.com/textualize", ) @@ -250,15 +255,12 @@ if __name__ == "__main__": # pragma: no cover "Twitter", "[u blue link=https://twitter.com/willmcgugan]https://twitter.com/willmcgugan", ) - sponsor_message.add_row( - "Blog", "[u blue link=https://www.willmcgugan.com]https://www.willmcgugan.com" - ) intro_message = Text.from_markup( """\ We hope you enjoy using Rich! -Rich is maintained with :heart: by [link=https://www.textualize.io]Textualize.io[/] +Rich is maintained with [red]:heart:[/] by [link=https://www.textualize.io]Textualize.io[/] - Will McGugan""" ) diff --git a/src/pip/_vendor/rich/_inspect.py b/src/pip/_vendor/rich/_inspect.py index 262695b1c..01713e576 100644 --- a/src/pip/_vendor/rich/_inspect.py +++ b/src/pip/_vendor/rich/_inspect.py @@ -1,9 +1,10 @@ from __future__ import absolute_import +import inspect 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 @@ -97,7 +98,8 @@ class Inspect(JupyterMixin): source_filename: Optional[str] = None try: source_filename = getfile(obj) - except TypeError: + except (OSError, TypeError): + # OSError is raised if obj has no source file, e.g. when defined in REPL. pass callable_name = Text(name, style="inspect.callable") @@ -106,8 +108,17 @@ class Inspect(JupyterMixin): signature_text = self.highlighter(_signature) qualname = name or getattr(obj, "__qualname__", name) + + # If obj is a module, there may be classes (which are callable) to display + if inspect.isclass(obj): + prefix = "class" + else: + prefix = "def" + qual_signature = Text.assemble( - ("def ", "inspect.def"), (qualname, "inspect.callable"), signature_text + (f"{prefix} ", f"inspect.{prefix}"), + (qualname, "inspect.callable"), + signature_text, ) return qual_signature @@ -204,7 +215,8 @@ 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] 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/src/pip/_vendor/rich/_lru_cache.py b/src/pip/_vendor/rich/_lru_cache.py index b7bf2ce1a..10c818743 100644 --- a/src/pip/_vendor/rich/_lru_cache.py +++ b/src/pip/_vendor/rich/_lru_cache.py @@ -1,12 +1,16 @@ -from collections import OrderedDict -from typing import Dict, Generic, TypeVar - +from typing import Dict, Generic, TypeVar, TYPE_CHECKING +import sys CacheKey = TypeVar("CacheKey") CacheValue = TypeVar("CacheValue") +if sys.version_info < (3, 9): + from pip._vendor.typing_extensions import OrderedDict +else: + from collections import OrderedDict + -class LRUCache(Generic[CacheKey, CacheValue], OrderedDict): # type: ignore # https://github.com/python/mypy/issues/6904 +class LRUCache(OrderedDict[CacheKey, CacheValue]): """ A dictionary-like container that stores a given maximum items. @@ -17,18 +21,18 @@ class LRUCache(Generic[CacheKey, CacheValue], OrderedDict): # type: ignore # ht def __init__(self, cache_size: int) -> None: self.cache_size = cache_size - super(LRUCache, self).__init__() + super().__init__() def __setitem__(self, key: CacheKey, value: CacheValue) -> None: """Store a new views, potentially discarding an old value.""" if key not in self: if len(self) >= self.cache_size: self.popitem(last=False) - OrderedDict.__setitem__(self, key, value) + super().__setitem__(key, value) - def __getitem__(self: Dict[CacheKey, CacheValue], key: CacheKey) -> CacheValue: + def __getitem__(self, key: CacheKey) -> CacheValue: """Gets the item, but also makes it most recent.""" - value: CacheValue = OrderedDict.__getitem__(self, key) - OrderedDict.__delitem__(self, key) - OrderedDict.__setitem__(self, key, value) + value: CacheValue = super().__getitem__(key) + super().__delitem__(key) + super().__setitem__(key, value) return value diff --git a/src/pip/_vendor/rich/_spinners.py b/src/pip/_vendor/rich/_spinners.py index dc1db0777..d0bb1fe75 100644 --- a/src/pip/_vendor/rich/_spinners.py +++ b/src/pip/_vendor/rich/_spinners.py @@ -22,149 +22,36 @@ Spinners are from: SPINNERS = { "dots": { "interval": 80, - "frames": ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], + "frames": "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏", }, - "dots2": {"interval": 80, "frames": ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]}, + "dots2": {"interval": 80, "frames": "⣾⣽⣻⢿⡿⣟⣯⣷"}, "dots3": { "interval": 80, - "frames": ["⠋", "⠙", "⠚", "⠞", "⠖", "⠦", "⠴", "⠲", "⠳", "⠓"], + "frames": "⠋⠙⠚⠞⠖⠦⠴⠲⠳⠓", }, "dots4": { "interval": 80, - "frames": [ - "⠄", - "⠆", - "⠇", - "⠋", - "⠙", - "⠸", - "⠰", - "⠠", - "⠰", - "⠸", - "⠙", - "⠋", - "⠇", - "⠆", - ], + "frames": "⠄⠆⠇⠋⠙⠸⠰⠠⠰⠸⠙⠋⠇⠆", }, "dots5": { "interval": 80, - "frames": [ - "⠋", - "⠙", - "⠚", - "⠒", - "⠂", - "⠂", - "⠒", - "⠲", - "⠴", - "⠦", - "⠖", - "⠒", - "⠐", - "⠐", - "⠒", - "⠓", - "⠋", - ], + "frames": "⠋⠙⠚⠒⠂⠂⠒⠲⠴⠦⠖⠒⠐⠐⠒⠓⠋", }, "dots6": { "interval": 80, - "frames": [ - "⠁", - "⠉", - "⠙", - "⠚", - "⠒", - "⠂", - "⠂", - "⠒", - "⠲", - "⠴", - "⠤", - "⠄", - "⠄", - "⠤", - "⠴", - "⠲", - "⠒", - "⠂", - "⠂", - "⠒", - "⠚", - "⠙", - "⠉", - "⠁", - ], + "frames": "⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠴⠲⠒⠂⠂⠒⠚⠙⠉⠁", }, "dots7": { "interval": 80, - "frames": [ - "⠈", - "⠉", - "⠋", - "⠓", - "⠒", - "⠐", - "⠐", - "⠒", - "⠖", - "⠦", - "⠤", - "⠠", - "⠠", - "⠤", - "⠦", - "⠖", - "⠒", - "⠐", - "⠐", - "⠒", - "⠓", - "⠋", - "⠉", - "⠈", - ], + "frames": "⠈⠉⠋⠓⠒⠐⠐⠒⠖⠦⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈", }, "dots8": { "interval": 80, - "frames": [ - "⠁", - "⠁", - "⠉", - "⠙", - "⠚", - "⠒", - "⠂", - "⠂", - "⠒", - "⠲", - "⠴", - "⠤", - "⠄", - "⠄", - "⠤", - "⠠", - "⠠", - "⠤", - "⠦", - "⠖", - "⠒", - "⠐", - "⠐", - "⠒", - "⠓", - "⠋", - "⠉", - "⠈", - "⠈", - ], + "frames": "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈", }, - "dots9": {"interval": 80, "frames": ["⢹", "⢺", "⢼", "⣸", "⣇", "⡧", "⡗", "⡏"]}, - "dots10": {"interval": 80, "frames": ["⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"]}, - "dots11": {"interval": 100, "frames": ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"]}, + "dots9": {"interval": 80, "frames": "⢹⢺⢼⣸⣇⡧⡗⡏"}, + "dots10": {"interval": 80, "frames": "⢄⢂⢁⡁⡈⡐⡠"}, + "dots11": {"interval": 100, "frames": "⠁⠂⠄⡀⢀⠠⠐⠈"}, "dots12": { "interval": 80, "frames": [ @@ -228,315 +115,62 @@ SPINNERS = { }, "dots8Bit": { "interval": 80, - "frames": [ - "⠀", - "⠁", - "⠂", - "⠃", - "⠄", - "⠅", - "⠆", - "⠇", - "⡀", - "⡁", - "⡂", - "⡃", - "⡄", - "⡅", - "⡆", - "⡇", - "⠈", - "⠉", - "⠊", - "⠋", - "⠌", - "⠍", - "⠎", - "⠏", - "⡈", - "⡉", - "⡊", - "⡋", - "⡌", - "⡍", - "⡎", - "⡏", - "⠐", - "⠑", - "⠒", - "⠓", - "⠔", - "⠕", - "⠖", - "⠗", - "⡐", - "⡑", - "⡒", - "⡓", - "⡔", - "⡕", - "⡖", - "⡗", - "⠘", - "⠙", - "⠚", - "⠛", - "⠜", - "⠝", - "⠞", - "⠟", - "⡘", - "⡙", - "⡚", - "⡛", - "⡜", - "⡝", - "⡞", - "⡟", - "⠠", - "⠡", - "⠢", - "⠣", - "⠤", - "⠥", - "⠦", - "⠧", - "⡠", - "⡡", - "⡢", - "⡣", - "⡤", - "⡥", - "⡦", - "⡧", - "⠨", - "⠩", - "⠪", - "⠫", - "⠬", - "⠭", - "⠮", - "⠯", - "⡨", - "⡩", - "⡪", - "⡫", - "⡬", - "⡭", - "⡮", - "⡯", - "⠰", - "⠱", - "⠲", - "⠳", - "⠴", - "⠵", - "⠶", - "⠷", - "⡰", - "⡱", - "⡲", - "⡳", - "⡴", - "⡵", - "⡶", - "⡷", - "⠸", - "⠹", - "⠺", - "⠻", - "⠼", - "⠽", - "⠾", - "⠿", - "⡸", - "⡹", - "⡺", - "⡻", - "⡼", - "⡽", - "⡾", - "⡿", - "⢀", - "⢁", - "⢂", - "⢃", - "⢄", - "⢅", - "⢆", - "⢇", - "⣀", - "⣁", - "⣂", - "⣃", - "⣄", - "⣅", - "⣆", - "⣇", - "⢈", - "⢉", - "⢊", - "⢋", - "⢌", - "⢍", - "⢎", - "⢏", - "⣈", - "⣉", - "⣊", - "⣋", - "⣌", - "⣍", - "⣎", - "⣏", - "⢐", - "⢑", - "⢒", - "⢓", - "⢔", - "⢕", - "⢖", - "⢗", - "⣐", - "⣑", - "⣒", - "⣓", - "⣔", - "⣕", - "⣖", - "⣗", - "⢘", - "⢙", - "⢚", - "⢛", - "⢜", - "⢝", - "⢞", - "⢟", - "⣘", - "⣙", - "⣚", - "⣛", - "⣜", - "⣝", - "⣞", - "⣟", - "⢠", - "⢡", - "⢢", - "⢣", - "⢤", - "⢥", - "⢦", - "⢧", - "⣠", - "⣡", - "⣢", - "⣣", - "⣤", - "⣥", - "⣦", - "⣧", - "⢨", - "⢩", - "⢪", - "⢫", - "⢬", - "⢭", - "⢮", - "⢯", - "⣨", - "⣩", - "⣪", - "⣫", - "⣬", - "⣭", - "⣮", - "⣯", - "⢰", - "⢱", - "⢲", - "⢳", - "⢴", - "⢵", - "⢶", - "⢷", - "⣰", - "⣱", - "⣲", - "⣳", - "⣴", - "⣵", - "⣶", - "⣷", - "⢸", - "⢹", - "⢺", - "⢻", - "⢼", - "⢽", - "⢾", - "⢿", - "⣸", - "⣹", - "⣺", - "⣻", - "⣼", - "⣽", - "⣾", - "⣿", - ], + "frames": "⠀⠁⠂⠃⠄⠅⠆⠇⡀⡁⡂⡃⡄⡅⡆⡇⠈⠉⠊⠋⠌⠍⠎⠏⡈⡉⡊⡋⡌⡍⡎⡏⠐⠑⠒⠓⠔⠕⠖⠗⡐⡑⡒⡓⡔⡕⡖⡗⠘⠙⠚⠛⠜⠝⠞⠟⡘⡙" + "⡚⡛⡜⡝⡞⡟⠠⠡⠢⠣⠤⠥⠦⠧⡠⡡⡢⡣⡤⡥⡦⡧⠨⠩⠪⠫⠬⠭⠮⠯⡨⡩⡪⡫⡬⡭⡮⡯⠰⠱⠲⠳⠴⠵⠶⠷⡰⡱⡲⡳⡴⡵⡶⡷⠸⠹⠺⠻" + "⠼⠽⠾⠿⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⣀⣁⣂⣃⣄⣅⣆⣇⢈⢉⢊⢋⢌⢍⢎⢏⣈⣉⣊⣋⣌⣍⣎⣏⢐⢑⢒⢓⢔⢕⢖⢗⣐⣑⣒⣓⣔⣕" + "⣖⣗⢘⢙⢚⢛⢜⢝⢞⢟⣘⣙⣚⣛⣜⣝⣞⣟⢠⢡⢢⢣⢤⢥⢦⢧⣠⣡⣢⣣⣤⣥⣦⣧⢨⢩⢪⢫⢬⢭⢮⢯⣨⣩⣪⣫⣬⣭⣮⣯⢰⢱⢲⢳⢴⢵⢶⢷" + "⣰⣱⣲⣳⣴⣵⣶⣷⢸⢹⢺⢻⢼⢽⢾⢿⣸⣹⣺⣻⣼⣽⣾⣿", }, "line": {"interval": 130, "frames": ["-", "\\", "|", "/"]}, - "line2": {"interval": 100, "frames": ["⠂", "-", "–", "—", "–", "-"]}, - "pipe": {"interval": 100, "frames": ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"]}, + "line2": {"interval": 100, "frames": "⠂-–—–-"}, + "pipe": {"interval": 100, "frames": "┤┘┴└├┌┬┐"}, "simpleDots": {"interval": 400, "frames": [". ", ".. ", "...", " "]}, "simpleDotsScrolling": { "interval": 200, "frames": [". ", ".. ", "...", " ..", " .", " "], }, - "star": {"interval": 70, "frames": ["✶", "✸", "✹", "✺", "✹", "✷"]}, - "star2": {"interval": 80, "frames": ["+", "x", "*"]}, + "star": {"interval": 70, "frames": "✶✸✹✺✹✷"}, + "star2": {"interval": 80, "frames": "+x*"}, "flip": { "interval": 70, - "frames": ["_", "_", "_", "-", "`", "`", "'", "´", "-", "_", "_", "_"], + "frames": "___-``'´-___", }, - "hamburger": {"interval": 100, "frames": ["☱", "☲", "☴"]}, + "hamburger": {"interval": 100, "frames": "☱☲☴"}, "growVertical": { "interval": 120, - "frames": ["▁", "▃", "▄", "▅", "▆", "▇", "▆", "▅", "▄", "▃"], + "frames": "▁▃▄▅▆▇▆▅▄▃", }, "growHorizontal": { "interval": 120, - "frames": ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "▊", "▋", "▌", "▍", "▎"], + "frames": "▏▎▍▌▋▊▉▊▋▌▍▎", }, - "balloon": {"interval": 140, "frames": [" ", ".", "o", "O", "@", "*", " "]}, - "balloon2": {"interval": 120, "frames": [".", "o", "O", "°", "O", "o", "."]}, - "noise": {"interval": 100, "frames": ["▓", "▒", "░"]}, - "bounce": {"interval": 120, "frames": ["⠁", "⠂", "⠄", "⠂"]}, - "boxBounce": {"interval": 120, "frames": ["▖", "▘", "▝", "▗"]}, - "boxBounce2": {"interval": 100, "frames": ["▌", "▀", "▐", "▄"]}, - "triangle": {"interval": 50, "frames": ["◢", "◣", "◤", "◥"]}, - "arc": {"interval": 100, "frames": ["◜", "◠", "◝", "◞", "◡", "◟"]}, - "circle": {"interval": 120, "frames": ["◡", "⊙", "◠"]}, - "squareCorners": {"interval": 180, "frames": ["◰", "◳", "◲", "◱"]}, - "circleQuarters": {"interval": 120, "frames": ["◴", "◷", "◶", "◵"]}, - "circleHalves": {"interval": 50, "frames": ["◐", "◓", "◑", "◒"]}, - "squish": {"interval": 100, "frames": ["╫", "╪"]}, - "toggle": {"interval": 250, "frames": ["⊶", "⊷"]}, - "toggle2": {"interval": 80, "frames": ["▫", "▪"]}, - "toggle3": {"interval": 120, "frames": ["□", "■"]}, - "toggle4": {"interval": 100, "frames": ["■", "□", "▪", "▫"]}, - "toggle5": {"interval": 100, "frames": ["▮", "▯"]}, - "toggle6": {"interval": 300, "frames": ["ဝ", "၀"]}, - "toggle7": {"interval": 80, "frames": ["⦾", "⦿"]}, - "toggle8": {"interval": 100, "frames": ["◍", "◌"]}, - "toggle9": {"interval": 100, "frames": ["◉", "◎"]}, - "toggle10": {"interval": 100, "frames": ["㊂", "㊀", "㊁"]}, - "toggle11": {"interval": 50, "frames": ["⧇", "⧆"]}, - "toggle12": {"interval": 120, "frames": ["☗", "☖"]}, - "toggle13": {"interval": 80, "frames": ["=", "*", "-"]}, - "arrow": {"interval": 100, "frames": ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"]}, + "balloon": {"interval": 140, "frames": " .oO@* "}, + "balloon2": {"interval": 120, "frames": ".oO°Oo."}, + "noise": {"interval": 100, "frames": "▓▒░"}, + "bounce": {"interval": 120, "frames": "⠁⠂⠄⠂"}, + "boxBounce": {"interval": 120, "frames": "▖▘▝▗"}, + "boxBounce2": {"interval": 100, "frames": "▌▀▐▄"}, + "triangle": {"interval": 50, "frames": "◢◣◤◥"}, + "arc": {"interval": 100, "frames": "◜◠◝◞◡◟"}, + "circle": {"interval": 120, "frames": "◡⊙◠"}, + "squareCorners": {"interval": 180, "frames": "◰◳◲◱"}, + "circleQuarters": {"interval": 120, "frames": "◴◷◶◵"}, + "circleHalves": {"interval": 50, "frames": "◐◓◑◒"}, + "squish": {"interval": 100, "frames": "╫╪"}, + "toggle": {"interval": 250, "frames": "⊶⊷"}, + "toggle2": {"interval": 80, "frames": "▫▪"}, + "toggle3": {"interval": 120, "frames": "□■"}, + "toggle4": {"interval": 100, "frames": "■□▪▫"}, + "toggle5": {"interval": 100, "frames": "▮▯"}, + "toggle6": {"interval": 300, "frames": "ဝ၀"}, + "toggle7": {"interval": 80, "frames": "⦾⦿"}, + "toggle8": {"interval": 100, "frames": "◍◌"}, + "toggle9": {"interval": 100, "frames": "◉◎"}, + "toggle10": {"interval": 100, "frames": "㊂㊀㊁"}, + "toggle11": {"interval": 50, "frames": "⧇⧆"}, + "toggle12": {"interval": 120, "frames": "☗☖"}, + "toggle13": {"interval": 80, "frames": "=*-"}, + "arrow": {"interval": 100, "frames": "←↖↑↗→↘↓↙"}, "arrow2": { "interval": 80, "frames": ["⬆️ ", "↗️ ", "➡️ ", "↘️ ", "⬇️ ", "↙️ ", "⬅️ ", "↖️ "], @@ -769,7 +403,7 @@ SPINNERS = { "▐/|____________▌", ], }, - "dqpb": {"interval": 100, "frames": ["d", "q", "p", "b"]}, + "dqpb": {"interval": 100, "frames": "dqpb"}, "weather": { "interval": 100, "frames": [ @@ -798,7 +432,7 @@ SPINNERS = { "☀️ ", ], }, - "christmas": {"interval": 400, "frames": ["🌲", "🎄"]}, + "christmas": {"interval": 400, "frames": "🌲🎄"}, "grenade": { "interval": 80, "frames": [ @@ -819,7 +453,7 @@ SPINNERS = { ], }, "point": {"interval": 125, "frames": ["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"]}, - "layer": {"interval": 150, "frames": ["-", "=", "≡"]}, + "layer": {"interval": 150, "frames": "-=≡"}, "betaWave": { "interval": 80, "frames": [ diff --git a/src/pip/_vendor/rich/_win32_console.py b/src/pip/_vendor/rich/_win32_console.py new file mode 100644 index 000000000..d42cd7916 --- /dev/null +++ b/src/pip/_vendor/rich/_win32_console.py @@ -0,0 +1,630 @@ +"""Light wrapper around the Win32 Console API - this module should only be imported on Windows + +The API that this module wraps is documented at https://docs.microsoft.com/en-us/windows/console/console-functions +""" +import ctypes +import sys +from typing import Any + +windll: Any = None +if sys.platform == "win32": + windll = ctypes.LibraryLoader(ctypes.WinDLL) +else: + raise ImportError(f"{__name__} can only be imported on Windows") + +import time +from ctypes import Structure, byref, wintypes +from typing import IO, NamedTuple, Type, cast + +from pip._vendor.rich.color import ColorSystem +from pip._vendor.rich.style import Style + +STDOUT = -11 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 + +COORD = wintypes._COORD + + +class LegacyWindowsError(Exception): + pass + + +class WindowsCoordinates(NamedTuple): + """Coordinates in the Windows Console API are (y, x), not (x, y). + This class is intended to prevent that confusion. + Rows and columns are indexed from 0. + This class can be used in place of wintypes._COORD in arguments and argtypes. + """ + + row: int + col: int + + @classmethod + def from_param(cls, value: "WindowsCoordinates") -> COORD: + """Converts a WindowsCoordinates into a wintypes _COORD structure. + This classmethod is internally called by ctypes to perform the conversion. + + Args: + value (WindowsCoordinates): The input coordinates to convert. + + Returns: + wintypes._COORD: The converted coordinates struct. + """ + return COORD(value.col, value.row) + + +class CONSOLE_SCREEN_BUFFER_INFO(Structure): + _fields_ = [ + ("dwSize", COORD), + ("dwCursorPosition", COORD), + ("wAttributes", wintypes.WORD), + ("srWindow", wintypes.SMALL_RECT), + ("dwMaximumWindowSize", COORD), + ] + + +class CONSOLE_CURSOR_INFO(ctypes.Structure): + _fields_ = [("dwSize", wintypes.DWORD), ("bVisible", wintypes.BOOL)] + + +_GetStdHandle = windll.kernel32.GetStdHandle +_GetStdHandle.argtypes = [ + wintypes.DWORD, +] +_GetStdHandle.restype = wintypes.HANDLE + + +def GetStdHandle(handle: int = STDOUT) -> wintypes.HANDLE: + """Retrieves a handle to the specified standard device (standard input, standard output, or standard error). + + Args: + handle (int): Integer identifier for the handle. Defaults to -11 (stdout). + + Returns: + wintypes.HANDLE: The handle + """ + return cast(wintypes.HANDLE, _GetStdHandle(handle)) + + +_GetConsoleMode = windll.kernel32.GetConsoleMode +_GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD] +_GetConsoleMode.restype = wintypes.BOOL + + +def GetConsoleMode(std_handle: wintypes.HANDLE) -> int: + """Retrieves the current input mode of a console's input buffer + or the current output mode of a console screen buffer. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + + Raises: + LegacyWindowsError: If any error occurs while calling the Windows console API. + + Returns: + int: Value representing the current console mode as documented at + https://docs.microsoft.com/en-us/windows/console/getconsolemode#parameters + """ + + console_mode = wintypes.DWORD() + success = bool(_GetConsoleMode(std_handle, console_mode)) + if not success: + raise LegacyWindowsError("Unable to get legacy Windows Console Mode") + return console_mode.value + + +_FillConsoleOutputCharacterW = windll.kernel32.FillConsoleOutputCharacterW +_FillConsoleOutputCharacterW.argtypes = [ + wintypes.HANDLE, + ctypes.c_char, + wintypes.DWORD, + cast(Type[COORD], WindowsCoordinates), + ctypes.POINTER(wintypes.DWORD), +] +_FillConsoleOutputCharacterW.restype = wintypes.BOOL + + +def FillConsoleOutputCharacter( + std_handle: wintypes.HANDLE, + char: str, + length: int, + start: WindowsCoordinates, +) -> int: + """Writes a character to the console screen buffer a specified number of times, beginning at the specified coordinates. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + char (str): The character to write. Must be a string of length 1. + length (int): The number of times to write the character. + start (WindowsCoordinates): The coordinates to start writing at. + + Returns: + int: The number of characters written. + """ + character = ctypes.c_char(char.encode()) + num_characters = wintypes.DWORD(length) + num_written = wintypes.DWORD(0) + _FillConsoleOutputCharacterW( + std_handle, + character, + num_characters, + start, + byref(num_written), + ) + return num_written.value + + +_FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute +_FillConsoleOutputAttribute.argtypes = [ + wintypes.HANDLE, + wintypes.WORD, + wintypes.DWORD, + cast(Type[COORD], WindowsCoordinates), + ctypes.POINTER(wintypes.DWORD), +] +_FillConsoleOutputAttribute.restype = wintypes.BOOL + + +def FillConsoleOutputAttribute( + std_handle: wintypes.HANDLE, + attributes: int, + length: int, + start: WindowsCoordinates, +) -> int: + """Sets the character attributes for a specified number of character cells, + beginning at the specified coordinates in a screen buffer. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + attributes (int): Integer value representing the foreground and background colours of the cells. + length (int): The number of cells to set the output attribute of. + start (WindowsCoordinates): The coordinates of the first cell whose attributes are to be set. + + Returns: + int: The number of cells whose attributes were actually set. + """ + num_cells = wintypes.DWORD(length) + style_attrs = wintypes.WORD(attributes) + num_written = wintypes.DWORD(0) + _FillConsoleOutputAttribute( + std_handle, style_attrs, num_cells, start, byref(num_written) + ) + return num_written.value + + +_SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute +_SetConsoleTextAttribute.argtypes = [ + wintypes.HANDLE, + wintypes.WORD, +] +_SetConsoleTextAttribute.restype = wintypes.BOOL + + +def SetConsoleTextAttribute( + std_handle: wintypes.HANDLE, attributes: wintypes.WORD +) -> bool: + """Set the colour attributes for all text written after this function is called. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + attributes (int): Integer value representing the foreground and background colours. + + + Returns: + bool: True if the attribute was set successfully, otherwise False. + """ + return bool(_SetConsoleTextAttribute(std_handle, attributes)) + + +_GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo +_GetConsoleScreenBufferInfo.argtypes = [ + wintypes.HANDLE, + ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), +] +_GetConsoleScreenBufferInfo.restype = wintypes.BOOL + + +def GetConsoleScreenBufferInfo( + std_handle: wintypes.HANDLE, +) -> CONSOLE_SCREEN_BUFFER_INFO: + """Retrieves information about the specified console screen buffer. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + + Returns: + CONSOLE_SCREEN_BUFFER_INFO: A CONSOLE_SCREEN_BUFFER_INFO ctype struct contain information about + screen size, cursor position, colour attributes, and more.""" + console_screen_buffer_info = CONSOLE_SCREEN_BUFFER_INFO() + _GetConsoleScreenBufferInfo(std_handle, byref(console_screen_buffer_info)) + return console_screen_buffer_info + + +_SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition +_SetConsoleCursorPosition.argtypes = [ + wintypes.HANDLE, + cast(Type[COORD], WindowsCoordinates), +] +_SetConsoleCursorPosition.restype = wintypes.BOOL + + +def SetConsoleCursorPosition( + std_handle: wintypes.HANDLE, coords: WindowsCoordinates +) -> bool: + """Set the position of the cursor in the console screen + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + coords (WindowsCoordinates): The coordinates to move the cursor to. + + Returns: + bool: True if the function succeeds, otherwise False. + """ + return bool(_SetConsoleCursorPosition(std_handle, coords)) + + +_SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo +_SetConsoleCursorInfo.argtypes = [ + wintypes.HANDLE, + ctypes.POINTER(CONSOLE_CURSOR_INFO), +] +_SetConsoleCursorInfo.restype = wintypes.BOOL + + +def SetConsoleCursorInfo( + std_handle: wintypes.HANDLE, cursor_info: CONSOLE_CURSOR_INFO +) -> bool: + """Set the cursor info - used for adjusting cursor visibility and width + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + cursor_info (CONSOLE_CURSOR_INFO): CONSOLE_CURSOR_INFO ctype struct containing the new cursor info. + + Returns: + bool: True if the function succeeds, otherwise False. + """ + return bool(_SetConsoleCursorInfo(std_handle, byref(cursor_info))) + + +_SetConsoleTitle = windll.kernel32.SetConsoleTitleW +_SetConsoleTitle.argtypes = [wintypes.LPCWSTR] +_SetConsoleTitle.restype = wintypes.BOOL + + +def SetConsoleTitle(title: str) -> bool: + """Sets the title of the current console window + + Args: + title (str): The new title of the console window. + + Returns: + bool: True if the function succeeds, otherwise False. + """ + return bool(_SetConsoleTitle(title)) + + +class LegacyWindowsTerm: + """This class allows interaction with the legacy Windows Console API. It should only be used in the context + of environments where virtual terminal processing is not available. However, if it is used in a Windows environment, + the entire API should work. + + Args: + file (IO[str]): The file which the Windows Console API HANDLE is retrieved from, defaults to sys.stdout. + """ + + BRIGHT_BIT = 8 + + # Indices are ANSI color numbers, values are the corresponding Windows Console API color numbers + ANSI_TO_WINDOWS = [ + 0, # black The Windows colours are defined in wincon.h as follows: + 4, # red define FOREGROUND_BLUE 0x0001 -- 0000 0001 + 2, # green define FOREGROUND_GREEN 0x0002 -- 0000 0010 + 6, # yellow define FOREGROUND_RED 0x0004 -- 0000 0100 + 1, # blue define FOREGROUND_INTENSITY 0x0008 -- 0000 1000 + 5, # magenta define BACKGROUND_BLUE 0x0010 -- 0001 0000 + 3, # cyan define BACKGROUND_GREEN 0x0020 -- 0010 0000 + 7, # white define BACKGROUND_RED 0x0040 -- 0100 0000 + 8, # bright black (grey) define BACKGROUND_INTENSITY 0x0080 -- 1000 0000 + 12, # bright red + 10, # bright green + 14, # bright yellow + 9, # bright blue + 13, # bright magenta + 11, # bright cyan + 15, # bright white + ] + + def __init__(self, file: "IO[str]") -> None: + handle = GetStdHandle(STDOUT) + self._handle = handle + default_text = GetConsoleScreenBufferInfo(handle).wAttributes + self._default_text = default_text + + self._default_fore = default_text & 7 + self._default_back = (default_text >> 4) & 7 + self._default_attrs = self._default_fore | (self._default_back << 4) + + self._file = file + self.write = file.write + self.flush = file.flush + + @property + def cursor_position(self) -> WindowsCoordinates: + """Returns the current position of the cursor (0-based) + + Returns: + WindowsCoordinates: The current cursor position. + """ + coord: COORD = GetConsoleScreenBufferInfo(self._handle).dwCursorPosition + return WindowsCoordinates(row=cast(int, coord.Y), col=cast(int, coord.X)) + + @property + def screen_size(self) -> WindowsCoordinates: + """Returns the current size of the console screen buffer, in character columns and rows + + Returns: + WindowsCoordinates: The width and height of the screen as WindowsCoordinates. + """ + screen_size: COORD = GetConsoleScreenBufferInfo(self._handle).dwSize + return WindowsCoordinates( + row=cast(int, screen_size.Y), col=cast(int, screen_size.X) + ) + + def write_text(self, text: str) -> None: + """Write text directly to the terminal without any modification of styles + + Args: + text (str): The text to write to the console + """ + self.write(text) + self.flush() + + def write_styled(self, text: str, style: Style) -> None: + """Write styled text to the terminal. + + Args: + text (str): The text to write + style (Style): The style of the text + """ + color = style.color + bgcolor = style.bgcolor + if style.reverse: + color, bgcolor = bgcolor, color + + if color: + fore = color.downgrade(ColorSystem.WINDOWS).number + fore = fore if fore is not None else 7 # Default to ANSI 7: White + if style.bold: + fore = fore | self.BRIGHT_BIT + if style.dim: + fore = fore & ~self.BRIGHT_BIT + fore = self.ANSI_TO_WINDOWS[fore] + else: + fore = self._default_fore + + if bgcolor: + back = bgcolor.downgrade(ColorSystem.WINDOWS).number + back = back if back is not None else 0 # Default to ANSI 0: Black + back = self.ANSI_TO_WINDOWS[back] + else: + back = self._default_back + + assert fore is not None + assert back is not None + + SetConsoleTextAttribute( + self._handle, attributes=ctypes.c_ushort(fore | (back << 4)) + ) + self.write_text(text) + SetConsoleTextAttribute(self._handle, attributes=self._default_text) + + def move_cursor_to(self, new_position: WindowsCoordinates) -> None: + """Set the position of the cursor + + Args: + new_position (WindowsCoordinates): The WindowsCoordinates representing the new position of the cursor. + """ + if new_position.col < 0 or new_position.row < 0: + return + SetConsoleCursorPosition(self._handle, coords=new_position) + + def erase_line(self) -> None: + """Erase all content on the line the cursor is currently located at""" + screen_size = self.screen_size + cursor_position = self.cursor_position + cells_to_erase = screen_size.col + start_coordinates = WindowsCoordinates(row=cursor_position.row, col=0) + FillConsoleOutputCharacter( + self._handle, " ", length=cells_to_erase, start=start_coordinates + ) + FillConsoleOutputAttribute( + self._handle, + self._default_attrs, + length=cells_to_erase, + start=start_coordinates, + ) + + def erase_end_of_line(self) -> None: + """Erase all content from the cursor position to the end of that line""" + cursor_position = self.cursor_position + cells_to_erase = self.screen_size.col - cursor_position.col + FillConsoleOutputCharacter( + self._handle, " ", length=cells_to_erase, start=cursor_position + ) + FillConsoleOutputAttribute( + self._handle, + self._default_attrs, + length=cells_to_erase, + start=cursor_position, + ) + + def erase_start_of_line(self) -> None: + """Erase all content from the cursor position to the start of that line""" + row, col = self.cursor_position + start = WindowsCoordinates(row, 0) + FillConsoleOutputCharacter(self._handle, " ", length=col, start=start) + FillConsoleOutputAttribute( + self._handle, self._default_attrs, length=col, start=start + ) + + def move_cursor_up(self) -> None: + """Move the cursor up a single cell""" + cursor_position = self.cursor_position + SetConsoleCursorPosition( + self._handle, + coords=WindowsCoordinates( + row=cursor_position.row - 1, col=cursor_position.col + ), + ) + + def move_cursor_down(self) -> None: + """Move the cursor down a single cell""" + cursor_position = self.cursor_position + SetConsoleCursorPosition( + self._handle, + coords=WindowsCoordinates( + row=cursor_position.row + 1, + col=cursor_position.col, + ), + ) + + def move_cursor_forward(self) -> None: + """Move the cursor forward a single cell. Wrap to the next line if required.""" + row, col = self.cursor_position + if col == self.screen_size.col - 1: + row += 1 + col = 0 + else: + col += 1 + SetConsoleCursorPosition( + self._handle, coords=WindowsCoordinates(row=row, col=col) + ) + + def move_cursor_to_column(self, column: int) -> None: + """Move cursor to the column specified by the zero-based column index, staying on the same row + + Args: + column (int): The zero-based column index to move the cursor to. + """ + row, _ = self.cursor_position + SetConsoleCursorPosition(self._handle, coords=WindowsCoordinates(row, column)) + + def move_cursor_backward(self) -> None: + """Move the cursor backward a single cell. Wrap to the previous line if required.""" + row, col = self.cursor_position + if col == 0: + row -= 1 + col = self.screen_size.col - 1 + else: + col -= 1 + SetConsoleCursorPosition( + self._handle, coords=WindowsCoordinates(row=row, col=col) + ) + + def hide_cursor(self) -> None: + """Hide the cursor""" + invisible_cursor = CONSOLE_CURSOR_INFO(dwSize=100, bVisible=0) + SetConsoleCursorInfo(self._handle, cursor_info=invisible_cursor) + + def show_cursor(self) -> None: + """Show the cursor""" + visible_cursor = CONSOLE_CURSOR_INFO(dwSize=100, bVisible=1) + SetConsoleCursorInfo(self._handle, cursor_info=visible_cursor) + + def set_title(self, title: str) -> None: + """Set the title of the terminal window + + Args: + title (str): The new title of the console window + """ + assert len(title) < 255, "Console title must be less than 255 characters" + SetConsoleTitle(title) + + +if __name__ == "__main__": + handle = GetStdHandle() + + from pip._vendor.rich.console import Console + + console = Console() + + term = LegacyWindowsTerm(sys.stdout) + term.set_title("Win32 Console Examples") + + style = Style(color="black", bgcolor="red") + + heading = Style.parse("black on green") + + # Check colour output + console.rule("Checking colour output") + console.print("[on red]on red!") + console.print("[blue]blue!") + console.print("[yellow]yellow!") + console.print("[bold yellow]bold yellow!") + console.print("[bright_yellow]bright_yellow!") + console.print("[dim bright_yellow]dim bright_yellow!") + console.print("[italic cyan]italic cyan!") + console.print("[bold white on blue]bold white on blue!") + console.print("[reverse bold white on blue]reverse bold white on blue!") + console.print("[bold black on cyan]bold black on cyan!") + console.print("[black on green]black on green!") + console.print("[blue on green]blue on green!") + console.print("[white on black]white on black!") + console.print("[black on white]black on white!") + console.print("[#1BB152 on #DA812D]#1BB152 on #DA812D!") + + # Check cursor movement + console.rule("Checking cursor movement") + console.print() + term.move_cursor_backward() + term.move_cursor_backward() + term.write_text("went back and wrapped to prev line") + time.sleep(1) + term.move_cursor_up() + term.write_text("we go up") + time.sleep(1) + term.move_cursor_down() + term.write_text("and down") + time.sleep(1) + term.move_cursor_up() + term.move_cursor_backward() + term.move_cursor_backward() + term.write_text("we went up and back 2") + time.sleep(1) + term.move_cursor_down() + term.move_cursor_backward() + term.move_cursor_backward() + term.write_text("we went down and back 2") + time.sleep(1) + + # Check erasing of lines + term.hide_cursor() + console.print() + console.rule("Checking line erasing") + console.print("\n...Deleting to the start of the line...") + term.write_text("The red arrow shows the cursor location, and direction of erase") + time.sleep(1) + term.move_cursor_to_column(16) + term.write_styled("<", Style.parse("black on red")) + term.move_cursor_backward() + time.sleep(1) + term.erase_start_of_line() + time.sleep(1) + + console.print("\n\n...And to the end of the line...") + term.write_text("The red arrow shows the cursor location, and direction of erase") + time.sleep(1) + + term.move_cursor_to_column(16) + term.write_styled(">", Style.parse("black on red")) + time.sleep(1) + term.erase_end_of_line() + time.sleep(1) + + console.print("\n\n...Now the whole line will be erased...") + term.write_styled("I'm going to disappear!", style=Style.parse("black on cyan")) + time.sleep(1) + term.erase_line() + + term.show_cursor() + print("\n") diff --git a/src/pip/_vendor/rich/_windows.py b/src/pip/_vendor/rich/_windows.py index ca3a680d3..10fc0d7e9 100644 --- a/src/pip/_vendor/rich/_windows.py +++ b/src/pip/_vendor/rich/_windows.py @@ -14,13 +14,21 @@ class WindowsConsoleFeatures: try: import ctypes - from ctypes import LibraryLoader, wintypes + from ctypes import LibraryLoader if sys.platform == "win32": windll = LibraryLoader(ctypes.WinDLL) else: windll = None raise ImportError("Not windows") + + from pip._vendor.rich._win32_console import ( + ENABLE_VIRTUAL_TERMINAL_PROCESSING, + GetConsoleMode, + GetStdHandle, + LegacyWindowsError, + ) + except (AttributeError, ImportError, ValueError): # Fallback if we can't load the Windows DLL @@ -30,28 +38,20 @@ except (AttributeError, ImportError, ValueError): else: - STDOUT = -11 - ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 - _GetConsoleMode = windll.kernel32.GetConsoleMode - _GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD] - _GetConsoleMode.restype = wintypes.BOOL - - _GetStdHandle = windll.kernel32.GetStdHandle - _GetStdHandle.argtypes = [ - wintypes.DWORD, - ] - _GetStdHandle.restype = wintypes.HANDLE - def get_windows_console_features() -> WindowsConsoleFeatures: """Get windows console features. Returns: WindowsConsoleFeatures: An instance of WindowsConsoleFeatures. """ - handle = _GetStdHandle(STDOUT) - console_mode = wintypes.DWORD() - result = _GetConsoleMode(handle, console_mode) - vt = bool(result and console_mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) + handle = GetStdHandle() + try: + console_mode = GetConsoleMode(handle) + success = True + except LegacyWindowsError: + console_mode = 0 + success = False + vt = bool(success and console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) truecolor = False if vt: win_version = sys.getwindowsversion() diff --git a/src/pip/_vendor/rich/_windows_renderer.py b/src/pip/_vendor/rich/_windows_renderer.py new file mode 100644 index 000000000..066e585dd --- /dev/null +++ b/src/pip/_vendor/rich/_windows_renderer.py @@ -0,0 +1,53 @@ +from typing import Iterable, Sequence, Tuple, cast + +from pip._vendor.rich._win32_console import LegacyWindowsTerm, WindowsCoordinates +from pip._vendor.rich.segment import ControlCode, ControlType, Segment + + +def legacy_windows_render(buffer: Iterable[Segment], term: LegacyWindowsTerm) -> None: + """Makes appropriate Windows Console API calls based on the segments in the buffer. + + Args: + buffer (Iterable[Segment]): Iterable of Segments to convert to Win32 API calls. + term (LegacyWindowsTerm): Used to call the Windows Console API. + """ + for text, style, control in buffer: + if not control: + if style: + term.write_styled(text, style) + else: + term.write_text(text) + else: + control_codes: Sequence[ControlCode] = control + for control_code in control_codes: + control_type = control_code[0] + if control_type == ControlType.CURSOR_MOVE_TO: + _, x, y = cast(Tuple[ControlType, int, int], control_code) + term.move_cursor_to(WindowsCoordinates(row=y - 1, col=x - 1)) + elif control_type == ControlType.CARRIAGE_RETURN: + term.write_text("\r") + elif control_type == ControlType.HOME: + term.move_cursor_to(WindowsCoordinates(0, 0)) + elif control_type == ControlType.CURSOR_UP: + term.move_cursor_up() + elif control_type == ControlType.CURSOR_DOWN: + term.move_cursor_down() + elif control_type == ControlType.CURSOR_FORWARD: + term.move_cursor_forward() + elif control_type == ControlType.CURSOR_BACKWARD: + term.move_cursor_backward() + elif control_type == ControlType.CURSOR_MOVE_TO_COLUMN: + _, column = cast(Tuple[ControlType, int], control_code) + term.move_cursor_to_column(column - 1) + elif control_type == ControlType.HIDE_CURSOR: + term.hide_cursor() + elif control_type == ControlType.SHOW_CURSOR: + term.show_cursor() + elif control_type == ControlType.ERASE_IN_LINE: + _, mode = cast(Tuple[ControlType, int], control_code) + if mode == 0: + term.erase_end_of_line() + elif mode == 1: + term.erase_start_of_line() + elif mode == 2: + term.erase_line() diff --git a/src/pip/_vendor/rich/align.py b/src/pip/_vendor/rich/align.py index 4344ae141..d5abb5947 100644 --- a/src/pip/_vendor/rich/align.py +++ b/src/pip/_vendor/rich/align.py @@ -18,7 +18,6 @@ if TYPE_CHECKING: AlignMethod = Literal["left", "center", "right"] VerticalAlignMethod = Literal["top", "middle", "bottom"] -AlignValues = AlignMethod # TODO: deprecate AlignValues class Align(JupyterMixin): diff --git a/src/pip/_vendor/rich/ansi.py b/src/pip/_vendor/rich/ansi.py index 92e4772ed..d4c32cef1 100644 --- a/src/pip/_vendor/rich/ansi.py +++ b/src/pip/_vendor/rich/ansi.py @@ -1,21 +1,27 @@ -from contextlib import suppress import re -from typing import Iterable, NamedTuple +import sys +from contextlib import suppress +from typing import Iterable, NamedTuple, Optional from .color import Color from .style import Style from .text import Text -re_ansi = re.compile(r"(?:\x1b\[(.*?)m)|(?:\x1b\](.*?)\x1b\\)") -re_csi = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") +re_ansi = re.compile( + r""" +(?:\x1b\](.*?)\x1b\\)| +(?:\x1b([(@-Z\\-_]|\[[0-?]*[ -/]*[@-~])) +""", + re.VERBOSE, +) class _AnsiToken(NamedTuple): """Result of ansi tokenized string.""" plain: str = "" - sgr: str = "" - osc: str = "" + sgr: Optional[str] = "" + osc: Optional[str] = "" def _ansi_tokenize(ansi_text: str) -> Iterable[_AnsiToken]: @@ -28,20 +34,22 @@ def _ansi_tokenize(ansi_text: str) -> Iterable[_AnsiToken]: AnsiToken: A named tuple of (plain, sgr, osc) """ - def remove_csi(ansi_text: str) -> str: - """Remove unknown CSI sequences.""" - return re_csi.sub("", ansi_text) - position = 0 + sgr: Optional[str] + osc: Optional[str] for match in re_ansi.finditer(ansi_text): start, end = match.span(0) - sgr, osc = match.groups() + osc, sgr = match.groups() if start > position: - yield _AnsiToken(remove_csi(ansi_text[position:start])) - yield _AnsiToken("", sgr, osc) + yield _AnsiToken(ansi_text[position:start]) + if sgr: + if sgr.endswith("m"): + yield _AnsiToken("", sgr[1:-1], osc) + else: + yield _AnsiToken("", sgr, osc) position = end if position < len(ansi_text): - yield _AnsiToken(remove_csi(ansi_text[position:])) + yield _AnsiToken(ansi_text[position:]) SGR_STYLE_MAP = { @@ -138,20 +146,21 @@ class AnsiDecoder: text = Text() append = text.append line = line.rsplit("\r", 1)[-1] - for token in _ansi_tokenize(line): - plain_text, sgr, osc = token + for plain_text, sgr, osc in _ansi_tokenize(line): if plain_text: append(plain_text, self.style or None) - elif osc: + elif osc is not None: if osc.startswith("8;"): _params, semicolon, link = osc[2:].partition(";") if semicolon: self.style = self.style.update_link(link or None) - elif sgr: + elif sgr is not None: # Translate in to semi-colon separated codes # Ignore invalid codes, because we want to be lenient codes = [ - min(255, int(_code)) for _code in sgr.split(";") if _code.isdigit() + min(255, int(_code) if _code else 0) + for _code in sgr.split(";") + if _code.isdigit() or _code == "" ] iter_codes = iter(codes) for code in iter_codes: @@ -198,10 +207,10 @@ class AnsiDecoder: return text -if __name__ == "__main__": # pragma: no cover - import pty +if sys.platform != "win32" and __name__ == "__main__": # pragma: no cover import io import os + import pty import sys decoder = AnsiDecoder() diff --git a/src/pip/_vendor/rich/cells.py b/src/pip/_vendor/rich/cells.py index e824ea2a6..d7adf5a04 100644 --- a/src/pip/_vendor/rich/cells.py +++ b/src/pip/_vendor/rich/cells.py @@ -1,5 +1,5 @@ -from functools import lru_cache import re +from functools import lru_cache from typing import Dict, List from ._cell_widths import CELL_WIDTHS @@ -18,17 +18,14 @@ def cell_len(text: str, _cache: Dict[str, int] = LRUCache(1024 * 4)) -> int: Returns: int: Get the number of cells required to display text. """ - - 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 + 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) <= 512: + _cache[text] = total_size return total_size @@ -42,9 +39,6 @@ def get_character_cell_size(character: str) -> int: Returns: int: Number of cells (0, 1 or 2) occupied by that character. """ - if _is_single_cell_widths(character): - return 1 - return _get_codepoint_cell_size(ord(character)) @@ -119,14 +113,12 @@ def chop_cells(text: str, max_size: int, position: int = 0) -> List[str]: _get_character_cell_size = get_character_cell_size characters = [ (character, _get_character_cell_size(character)) for character in text - ][::-1] + ] total_size = position lines: List[List[str]] = [[]] append = lines[-1].append - pop = characters.pop - while characters: - character, size = pop() + for character, size in reversed(characters): if total_size + size > max_size: lines.append([character]) append = lines[-1].append @@ -134,6 +126,7 @@ def chop_cells(text: str, max_size: int, position: int = 0) -> List[str]: else: total_size += size append(character) + return ["".join(line) for line in lines] diff --git a/src/pip/_vendor/rich/color.py b/src/pip/_vendor/rich/color.py index f0fa026d6..6bca2da92 100644 --- a/src/pip/_vendor/rich/color.py +++ b/src/pip/_vendor/rich/color.py @@ -7,7 +7,7 @@ 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 @@ -61,6 +61,7 @@ ANSI_COLOR_NAMES = { "bright_cyan": 14, "bright_white": 15, "grey0": 16, + "gray0": 16, "navy_blue": 17, "dark_blue": 18, "blue3": 20, @@ -96,6 +97,7 @@ ANSI_COLOR_NAMES = { "blue_violet": 57, "orange4": 94, "grey37": 59, + "gray37": 59, "medium_purple4": 60, "slate_blue3": 62, "royal_blue1": 63, @@ -128,7 +130,9 @@ ANSI_COLOR_NAMES = { "yellow4": 106, "wheat4": 101, "grey53": 102, + "gray53": 102, "light_slate_grey": 103, + "light_slate_gray": 103, "medium_purple": 104, "light_slate_blue": 105, "dark_olive_green3": 149, @@ -155,11 +159,13 @@ ANSI_COLOR_NAMES = { "light_salmon3": 173, "rosy_brown": 138, "grey63": 139, + "gray63": 139, "medium_purple1": 141, "gold3": 178, "dark_khaki": 143, "navajo_white3": 144, "grey69": 145, + "gray69": 145, "light_steel_blue3": 146, "light_steel_blue": 147, "yellow3": 184, @@ -189,6 +195,7 @@ ANSI_COLOR_NAMES = { "light_goldenrod2": 222, "light_yellow3": 187, "grey84": 188, + "gray84": 188, "light_steel_blue1": 189, "yellow2": 190, "dark_olive_green1": 192, @@ -223,30 +230,55 @@ ANSI_COLOR_NAMES = { "wheat1": 229, "cornsilk1": 230, "grey100": 231, + "gray100": 231, "grey3": 232, + "gray3": 232, "grey7": 233, + "gray7": 233, "grey11": 234, + "gray11": 234, "grey15": 235, + "gray15": 235, "grey19": 236, + "gray19": 236, "grey23": 237, + "gray23": 237, "grey27": 238, + "gray27": 238, "grey30": 239, + "gray30": 239, "grey35": 240, + "gray35": 240, "grey39": 241, + "gray39": 241, "grey42": 242, + "gray42": 242, "grey46": 243, + "gray46": 243, "grey50": 244, + "gray50": 244, "grey54": 245, + "gray54": 245, "grey58": 246, + "gray58": 246, "grey62": 247, + "gray62": 247, "grey66": 248, + "gray66": 248, "grey70": 249, + "gray70": 249, "grey74": 250, + "gray74": 250, "grey78": 251, + "gray78": 251, "grey82": 252, + "gray82": 252, "grey85": 253, + "gray85": 253, "grey89": 254, + "gray89": 254, "grey93": 255, + "gray93": 255, } @@ -279,8 +311,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"<color {self.name!r} ({self.type.name.lower()})", @@ -569,11 +601,13 @@ if __name__ == "__main__": # pragma: no cover colors = sorted((v, k) for k, v in ANSI_COLOR_NAMES.items()) for color_number, name in colors: + if "grey" in name: + continue color_cell = Text(" " * 10, style=f"on {name}") if color_number < 16: table.add_row(color_cell, f"{color_number}", Text(f'"{name}"')) else: - color = EIGHT_BIT_PALETTE[color_number] # type: ignore + color = EIGHT_BIT_PALETTE[color_number] # type: ignore[has-type] table.add_row( color_cell, str(color_number), Text(f'"{name}"'), color.hex, color.rgb ) diff --git a/src/pip/_vendor/rich/console.py b/src/pip/_vendor/rich/console.py index 27e722760..8c305712d 100644 --- a/src/pip/_vendor/rich/console.py +++ b/src/pip/_vendor/rich/console.py @@ -1,4 +1,5 @@ import inspect +import io import os import platform import sys @@ -11,7 +12,6 @@ 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, ModuleType, TracebackType from typing import ( @@ -60,7 +60,7 @@ from .screen import Screen from .segment import Segment from .style import Style, StyleType from .styled import Styled -from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme +from .terminal_theme import DEFAULT_TERMINAL_THEME, SVG_EXPORT_THEME, TerminalTheme from .text import Text, TextType from .theme import Theme, ThemeStack @@ -82,6 +82,17 @@ class NoChange: NO_CHANGE = NoChange() +try: + _STDOUT_FILENO = sys.__stdout__.fileno() +except Exception: + _STDOUT_FILENO = 1 + +try: + _STDERR_FILENO = sys.__stderr__.fileno() +except Exception: + _STDERR_FILENO = 2 + +_STD_STREAMS = (_STDOUT_FILENO, _STDERR_FILENO) CONSOLE_HTML_FORMAT = """\ <!DOCTYPE html> @@ -104,6 +115,127 @@ body {{ </html> """ +CONSOLE_SVG_FORMAT = """\ +<svg width="{total_width}" height="{total_height}" viewBox="0 0 {total_width} {total_height}" + xmlns="http://www.w3.org/2000/svg"> + <style> + @font-face {{ + font-family: "Fira Code"; + src: local("FiraCode-Regular"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); + font-style: normal; + font-weight: 400; + }} + @font-face {{ + font-family: "Fira Code"; + src: local("FiraCode-Bold"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); + font-style: bold; + font-weight: 700; + }} + span {{ + display: inline-block; + white-space: pre; + vertical-align: top; + font-size: {font_size}px; + font-family:'Fira Code','Cascadia Code',Monaco,Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace; + }} + a {{ + text-decoration: none; + color: inherit; + }} + .blink {{ + animation: blinker 1s infinite; + }} + @keyframes blinker {{ + from {{ opacity: 1.0; }} + 50% {{ opacity: 0.3; }} + to {{ opacity: 1.0; }} + }} + #wrapper {{ + padding: {margin}px; + padding-top: 100px; + }} + #terminal {{ + position: relative; + display: flex; + flex-direction: column; + align-items: center; + background-color: {theme_background_color}; + border-radius: 14px; + outline: 1px solid #484848; + }} + #terminal:after {{ + position: absolute; + width: 100%; + height: 100%; + content: ''; + border-radius: 14px; + background: rgb(71,77,102); + background: linear-gradient(90deg, #804D69 0%, #4E4B89 100%); + transform: rotate(-4.5deg); + z-index: -1; + }} + #terminal-header {{ + position: relative; + width: 100%; + background-color: #2e2e2e; + margin-bottom: 12px; + font-weight: bold; + border-radius: 14px 14px 0 0; + color: {theme_foreground_color}; + font-size: 18px; + box-shadow: inset 0px -1px 0px 0px #4e4e4e, + inset 0px -4px 8px 0px #1a1a1a; + }} + #terminal-title-tab {{ + display: inline-block; + margin-top: 14px; + margin-left: 124px; + font-family: sans-serif; + padding: 14px 28px; + border-radius: 6px 6px 0 0; + background-color: {theme_background_color}; + box-shadow: inset 0px 1px 0px 0px #4e4e4e, + 0px -4px 4px 0px #1e1e1e, + inset 1px 0px 0px 0px #4e4e4e, + inset -1px 0px 0px 0px #4e4e4e; + }} + #terminal-traffic-lights {{ + position: absolute; + top: 24px; + left: 20px; + }} + #terminal-body {{ + line-height: {line_height}px; + padding: 14px; + }} + {stylesheet} + </style> + <foreignObject x="0" y="0" width="100%" height="100%"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <div id="wrapper"> + <div id="terminal"> + <div id='terminal-header'> + <svg id="terminal-traffic-lights" width="90" height="21" viewBox="0 0 90 21" xmlns="http://www.w3.org/2000/svg"> + <circle cx="14" cy="8" r="8" fill="#ff6159"/> + <circle cx="38" cy="8" r="8" fill="#ffbd2e"/> + <circle cx="62" cy="8" r="8" fill="#28c941"/> + </svg> + <div id="terminal-title-tab">{title}</div> + </div> + <div id='terminal-body'> + {code} + </div> + </div> + </div> + </body> + </foreignObject> +</svg> +""" + _TERM_COLORS = {"256color": ColorSystem.EIGHT_BIT, "16color": ColorSystem.STANDARD} @@ -224,6 +356,16 @@ class ConsoleOptions: options.max_height = options.height = height return options + def reset_height(self) -> "ConsoleOptions": + """Return a copy of the options with height set to ``None``. + + Returns: + ~ConsoleOptions: New console options instance. + """ + options = self.copy() + options.height = None + return options + def update_dimensions(self, width: int, height: int) -> "ConsoleOptions": """Update the width and height, and return a copy. @@ -244,7 +386,9 @@ class ConsoleOptions: class RichCast(Protocol): """An object that may be 'cast' to a console renderable.""" - def __rich__(self) -> Union["ConsoleRenderable", str]: # pragma: no cover + def __rich__( + self, + ) -> Union["ConsoleRenderable", "RichCast", str]: # pragma: no cover ... @@ -261,11 +405,9 @@ class ConsoleRenderable(Protocol): # A type that may be rendered by Console. RenderableType = Union[ConsoleRenderable, RichCast, str] - # The result of calling a __rich_console__ method. RenderResult = Iterable[Union[RenderableType, Segment]] - _null_highlighter = NullHighlighter() @@ -501,10 +643,10 @@ def group(fit: bool = True) -> Callable[..., Callable[..., Group]]: def _is_jupyter() -> bool: # pragma: no cover """Check if we're running in a Jupyter notebook.""" try: - get_ipython # type: ignore + get_ipython # type: ignore[name-defined] except NameError: return False - ipython = get_ipython() # type: ignore + ipython = get_ipython() # type: ignore[name-defined] shell = ipython.__class__.__name__ if "google.colab" in str(ipython.__class__) or shell == "ZMQInteractiveShell": return True # Jupyter notebook or qtconsole @@ -521,7 +663,6 @@ COLOR_SYSTEMS = { "windows": ColorSystem.WINDOWS, } - _COLOR_SYSTEMS_NAMES = {system: name for name, system in COLOR_SYSTEMS.items()} @@ -571,12 +712,6 @@ def detect_legacy_windows() -> bool: return WINDOWS and not get_windows_console_features().vt -if detect_legacy_windows(): # pragma: no cover - from pip._vendor.colorama import init - - init(strip=False) - - class Console: """A high level console interface. @@ -597,7 +732,7 @@ class Console: no_color (Optional[bool], optional): Enabled no color mode, or None to auto detect. Defaults to None. tab_size (int, optional): Number of spaces used to replace a tab character. Defaults to 8. record (bool, optional): Boolean to enable recording of terminal output, - required to call :meth:`export_html` and :meth:`export_text`. Defaults to False. + required to call :meth:`export_html`, :meth:`export_svg`, and :meth:`export_text`. Defaults to False. markup (bool, optional): Boolean to enable :ref:`console_markup`. Defaults to True. emoji (bool, optional): Enable emoji code. Defaults to True. emoji_variant (str, optional): Optional emoji variant, either "text" or "emoji". Defaults to None. @@ -1141,7 +1276,7 @@ class Console: Args: show (bool, optional): Set visibility of the cursor. """ - if self.is_terminal and not self.legacy_windows: + if self.is_terminal: self.control(Control.show_cursor(show)) return True return False @@ -1232,7 +1367,7 @@ class Console: renderable = rich_cast(renderable) if hasattr(renderable, "__rich_console__") and not isclass(renderable): - render_iterable = renderable.__rich_console__(self, _options) # type: ignore + render_iterable = renderable.__rich_console__(self, _options) # type: ignore[union-attr] elif isinstance(renderable, str): text_renderable = self.render_str( renderable, highlight=_options.highlight, markup=_options.markup @@ -1251,6 +1386,7 @@ class Console: f"object {render_iterable!r} is not renderable" ) _Segment = Segment + _options = _options.reset_height() for render_output in iter_render: if isinstance(render_output, _Segment): yield render_output @@ -1322,7 +1458,7 @@ class Console: highlight: Optional[bool] = None, highlighter: Optional[HighlighterType] = None, ) -> "Text": - """Convert a string to a Text instance. This is is called automatically if + """Convert a string to a Text instance. This is called automatically if you print or log a string. Args: @@ -1372,7 +1508,7 @@ class Console: def get_style( self, name: Union[str, Style], *, default: Optional[Union[Style, str]] = None ) -> Style: - """Get a Style instance by it's theme name or parse a definition. + """Get a Style instance by its theme name or parse a definition. Args: name (str): The name of a style or a style definition. @@ -1904,43 +2040,72 @@ class Console: buffer_extend(line) def _check_buffer(self) -> None: - """Check if the buffer may be rendered.""" + """Check if the buffer may be rendered. Render it if it can (e.g. Console.quiet is False) + Rendering is supported on Windows, Unix and Jupyter environments. For + legacy Windows consoles, the win32 API is called directly. + This method will also record what it renders if recording is enabled via Console.record. + """ if self.quiet: del self._buffer[:] return with self._lock: if self._buffer_index == 0: + + if self.record: + with self._record_buffer_lock: + self._record_buffer.extend(self._buffer[:]) + if self.is_jupyter: # pragma: no cover from .jupyter import display display(self._buffer, self._render_buffer(self._buffer[:])) del self._buffer[:] else: - text = self._render_buffer(self._buffer[:]) - del self._buffer[:] - if text: - try: - if WINDOWS: # pragma: no cover - # https://bugs.python.org/issue37871 - write = self.file.write - for line in text.splitlines(True): + if WINDOWS: + use_legacy_windows_render = False + if self.legacy_windows: + try: + use_legacy_windows_render = ( + self.file.fileno() in _STD_STREAMS + ) + except (ValueError, io.UnsupportedOperation): + pass + + if use_legacy_windows_render: + from pip._vendor.rich._win32_console import LegacyWindowsTerm + from pip._vendor.rich._windows_renderer import legacy_windows_render + + legacy_windows_render( + self._buffer[:], LegacyWindowsTerm(self.file) + ) + else: + # Either a non-std stream on legacy Windows, or modern Windows. + text = self._render_buffer(self._buffer[:]) + # https://bugs.python.org/issue37871 + write = self.file.write + for line in text.splitlines(True): + try: write(line) - else: - self.file.write(text) - self.file.flush() + except UnicodeEncodeError as error: + error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" + raise + else: + text = self._render_buffer(self._buffer[:]) + try: + self.file.write(text) except UnicodeEncodeError as error: error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" raise + self.file.flush() + del self._buffer[:] + def _render_buffer(self, buffer: Iterable[Segment]) -> str: """Render buffered output, and clear buffer.""" output: List[str] = [] append = output.append color_system = self._color_system legacy_windows = self.legacy_windows - if self.record: - with self._record_buffer_lock: - self._record_buffer.extend(buffer) not_terminal = not self.is_terminal if self.no_color and color_system: buffer = Segment.remove_color(buffer) @@ -1982,23 +2147,15 @@ class Console: Returns: str: Text read from stdin. """ - prompt_str = "" if prompt: - with self.capture() as capture: - self.print(prompt, markup=markup, emoji=emoji, end="") - prompt_str = capture.get() - if self.legacy_windows: - # Legacy windows doesn't like ANSI codes in getpass or input (colorama bug)? - self.file.write(prompt_str) - prompt_str = "" + self.print(prompt, markup=markup, emoji=emoji, end="") if password: - result = getpass(prompt_str, stream=stream) + result = getpass("", stream=stream) else: if stream: - self.file.write(prompt_str) result = stream.readline() else: - result = input(prompt_str) + result = input() return result def export_text(self, *, clear: bool = True, styles: bool = False) -> str: @@ -2060,8 +2217,8 @@ class Console: Args: theme (TerminalTheme, optional): TerminalTheme object containing console colors. clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``. - code_format (str, optional): Format string to render HTML, should contain {foreground} - {background} and {code}. + code_format (str, optional): Format string to render HTML. In addition to '{foreground}', + '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``. inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag. Defaults to False. @@ -2137,8 +2294,8 @@ class Console: path (str): Path to write html file. theme (TerminalTheme, optional): TerminalTheme object containing console colors. clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``. - code_format (str, optional): Format string to render HTML, should contain {foreground} - {background} and {code}. + code_format (str, optional): Format string to render HTML. In addition to '{foreground}', + '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``. inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag. Defaults to False. @@ -2153,9 +2310,173 @@ class Console: with open(path, "wt", encoding="utf-8") as write_file: write_file.write(html) + def export_svg( + self, + *, + title: str = "Rich", + theme: Optional[TerminalTheme] = None, + clear: bool = True, + code_format: str = CONSOLE_SVG_FORMAT, + ) -> str: + """Generate an SVG string from the console contents (requires record=True in Console constructor) + + Args: + title (str): The title of the tab in the output image + theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal + clear (bool, optional): Clear record buffer after exporting. Defaults to ``True`` + code_format (str): Format string used to generate the SVG. Rich will inject a number of variables + into the string in order to form the final SVG output. The default template used and the variables + injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable. + + Returns: + str: The string representation of the SVG. That is, the ``code_format`` template with content injected. + """ + assert ( + self.record + ), "To export console contents set record=True in the constructor or instance" + + _theme = theme or SVG_EXPORT_THEME + + with self._record_buffer_lock: + segments = Segment.simplify(self._record_buffer) + segments = Segment.filter_control(segments) + parts = [(text, style or Style.null()) for text, style, _ in segments] + terminal_text = Text.assemble(*parts) + lines = terminal_text.wrap(self, width=self.width, overflow="fold") + segments = self.render(lines, options=self.options) + segment_lines = list( + Segment.split_and_crop_lines( + segments, length=self.width, include_new_lines=False + ) + ) + + fragments: List[str] = [] + theme_foreground_color = _theme.foreground_color.hex + theme_background_color = _theme.background_color.hex + + theme_foreground_css = f"color: {theme_foreground_color}; text-decoration-color: {theme_foreground_color};" + theme_background_css = f"background-color: {theme_background_color};" + + theme_css = theme_foreground_css + theme_background_css + + styles: Dict[str, int] = {} + styles[theme_css] = 1 + + for line in segment_lines: + line_spans = [] + for segment in line: + text, style, _ = segment + text = escape(text) + if style: + rules = style.get_html_style(_theme) + if style.link: + text = f'<a href="{style.link}">{text}</a>' + + if style.blink or style.blink2: + text = f'<span class="blink">{text}</span>' + + # If the style doesn't contain a color, we still + # need to make sure we output the default foreground color + # from the TerminalTheme. + if not style.reverse: + foreground_css = theme_foreground_css + background_css = theme_background_css + else: + foreground_css = f"color: {theme_background_color}; text-decoration-color: {theme_background_color};" + background_css = ( + f"background-color: {theme_foreground_color};" + ) + + if style.color is None: + rules += f";{foreground_css}" + if style.bgcolor is None: + rules += f";{background_css}" + + style_number = styles.setdefault(rules, len(styles) + 1) + text = f'<span class="r{style_number}">{text}</span>' + else: + text = f'<span class="r1">{text}</span>' + line_spans.append(text) + + fragments.append(f"<div>{''.join(line_spans)}</div>") + + stylesheet_rules = [] + for style_rule, style_number in styles.items(): + if style_rule: + stylesheet_rules.append(f".r{style_number} {{{ style_rule }}}") + stylesheet = "\n".join(stylesheet_rules) + + if clear: + self._record_buffer.clear() + + # These values are the ones that I found to work well after experimentation. + # Many of them can be tweaked, but too much variation from these values could + # result in visually broken output/clipping issues. + terminal_padding = 12 + font_size = 18 + line_height = font_size + 4 + code_start_y = 60 + required_code_height = line_height * len(lines) + margin = 140 + + # Monospace fonts are generally around 0.5-0.55 width/height ratio, but I've + # added extra width to ensure that the output SVG is big enough. + monospace_font_width_scale = 0.60 + + # This works out as a good heuristic for the final size of the drawn terminal. + terminal_height = required_code_height + code_start_y + terminal_width = ( + self.width * monospace_font_width_scale * font_size + + 2 * terminal_padding + + self.width + ) + total_height = terminal_height + 2 * margin + total_width = terminal_width + 2 * margin + + rendered_code = code_format.format( + code="\n".join(fragments), + total_height=total_height, + total_width=total_width, + theme_foreground_color=theme_foreground_color, + theme_background_color=theme_background_color, + margin=margin, + font_size=font_size, + line_height=line_height, + title=title, + stylesheet=stylesheet, + ) + + return rendered_code + + def save_svg( + self, + path: str, + *, + title: str = "Rich", + theme: Optional[TerminalTheme] = None, + clear: bool = True, + code_format: str = CONSOLE_SVG_FORMAT, + ) -> None: + """Generate an SVG file from the console contents (requires record=True in Console constructor). + + Args: + path (str): The path to write the SVG to. + title (str): The title of the tab in the output image + theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal + clear (bool, optional): Clear record buffer after exporting. Defaults to ``True`` + code_format (str): Format string used to generate the SVG. Rich will inject a number of variables + into the string in order to form the final SVG output. The default template used and the variables + injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable. + """ + svg = self.export_svg( + title=title, theme=theme, clear=clear, code_format=code_format + ) + with open(path, "wt", encoding="utf-8") as write_file: + write_file.write(svg) + if __name__ == "__main__": # pragma: no cover - console = Console() + console = Console(record=True) console.log( "JSONRPC [i]request[/i]", @@ -2208,4 +2529,3 @@ if __name__ == "__main__": # pragma: no cover }, } ) - console.log("foo") diff --git a/src/pip/_vendor/rich/control.py b/src/pip/_vendor/rich/control.py index c98d0d7d9..e17b2c634 100644 --- a/src/pip/_vendor/rich/control.py +++ b/src/pip/_vendor/rich/control.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict, Iterable, List, TYPE_CHECKING, Union +from typing import Callable, Dict, Iterable, List, TYPE_CHECKING, Union from .segment import ControlCode, ControlType, Segment diff --git a/src/pip/_vendor/rich/default_styles.py b/src/pip/_vendor/rich/default_styles.py index 91ab232d3..cb7bfc192 100644 --- a/src/pip/_vendor/rich/default_styles.py +++ b/src/pip/_vendor/rich/default_styles.py @@ -2,7 +2,6 @@ from typing import Dict from .style import Style - DEFAULT_STYLES: Dict[str, Style] = { "none": Style.null(), "reset": Style( @@ -41,6 +40,7 @@ DEFAULT_STYLES: Dict[str, Style] = { "inspect.attr.dunder": Style(color="yellow", italic=True, dim=True), "inspect.callable": Style(bold=True, color="red"), "inspect.def": Style(italic=True, color="bright_cyan"), + "inspect.class": Style(italic=True, color="bright_cyan"), "inspect.error": Style(bold=True, color="red"), "inspect.equals": Style(), "inspect.help": Style(color="cyan"), diff --git a/src/pip/_vendor/rich/diagnose.py b/src/pip/_vendor/rich/diagnose.py index 38728da2a..518586ea8 100644 --- a/src/pip/_vendor/rich/diagnose.py +++ b/src/pip/_vendor/rich/diagnose.py @@ -1,6 +1,35 @@ -if __name__ == "__main__": # pragma: no cover - from pip._vendor.rich.console import Console - from pip._vendor.rich import inspect +import os +import platform + +from pip._vendor.rich import inspect +from pip._vendor.rich.console import Console, get_windows_console_features +from pip._vendor.rich.panel import Panel +from pip._vendor.rich.pretty import Pretty + +def report() -> None: # pragma: no cover + """Print a report to the terminal with debugging information""" console = Console() inspect(console) + features = get_windows_console_features() + inspect(features) + + env_names = ( + "TERM", + "COLORTERM", + "CLICOLOR", + "NO_COLOR", + "TERM_PROGRAM", + "COLUMNS", + "LINES", + "JPY_PARENT_PID", + "VSCODE_VERBOSE_LOGGING", + ) + env = {name: os.getenv(name) for name in env_names} + console.print(Panel.fit((Pretty(env)), title="[b]Environment Variables")) + + console.print(f'platform="{platform.system()}"') + + +if __name__ == "__main__": # pragma: no cover + report() diff --git a/src/pip/_vendor/rich/filesize.py b/src/pip/_vendor/rich/filesize.py index b3a0996b0..61be47510 100644 --- a/src/pip/_vendor/rich/filesize.py +++ b/src/pip/_vendor/rich/filesize.py @@ -13,7 +13,7 @@ See Also: __all__ = ["decimal"] -from typing import Iterable, List, Tuple, Optional +from typing import Iterable, List, Optional, Tuple def _to_str( @@ -30,7 +30,7 @@ def _to_str( return "{:,} bytes".format(size) for i, suffix in enumerate(suffixes, 2): # noqa: B007 - unit = base ** i + unit = base**i if size < unit: break return "{:,.{precision}f}{separator}{}".format( @@ -44,7 +44,7 @@ def _to_str( def pick_unit_and_suffix(size: int, suffixes: List[str], base: int) -> Tuple[int, str]: """Pick a suffix and base for the given size.""" for i, suffix in enumerate(suffixes): - unit = base ** i + unit = base**i if size < unit * base: break return unit, suffix diff --git a/src/pip/_vendor/rich/highlighter.py b/src/pip/_vendor/rich/highlighter.py index 8afdd017b..7bee4167e 100644 --- a/src/pip/_vendor/rich/highlighter.py +++ b/src/pip/_vendor/rich/highlighter.py @@ -1,7 +1,8 @@ +import re from abc import ABC, abstractmethod from typing import List, Union -from .text import Text +from .text import Span, Text def _combine_regex(*regexes: str) -> str: @@ -81,22 +82,22 @@ class ReprHighlighter(RegexHighlighter): base_style = "repr." highlights = [ - r"(?P<tag_start>\<)(?P<tag_name>[\w\-\.\:]*)(?P<tag_contents>[\w\W]*?)(?P<tag_end>\>)", - r"(?P<attrib_name>[\w_]{1,50})=(?P<attrib_value>\"?[\w_]+\"?)?", - r"(?P<brace>[\{\[\(\)\]\}])", + r"(?P<tag_start><)(?P<tag_name>[-\w.:|]*)(?P<tag_contents>[\w\W]*?)(?P<tag_end>>)", + r'(?P<attrib_name>[\w_]{1,50})=(?P<attrib_value>"?[\w_]+"?)?', + r"(?P<brace>[][{}()])", _combine_regex( r"(?P<ipv4>[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})", r"(?P<ipv6>([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})", r"(?P<eui64>(?:[0-9A-Fa-f]{1,2}-){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\.){3}[0-9A-Fa-f]{4})", r"(?P<eui48>(?:[0-9A-Fa-f]{1,2}-){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4})", - r"(?P<call>[\w\.]*?)\(", + r"(?P<uuid>[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})", + r"(?P<call>[\w.]*?)\(", r"\b(?P<bool_true>True)\b|\b(?P<bool_false>False)\b|\b(?P<none>None)\b", r"(?P<ellipsis>\.\.\.)", - r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[\-\+]?\d+?)?\b|0x[0-9a-fA-F]*)", - r"(?P<path>\B(\/[\w\.\-\_\+]+)*\/)(?P<filename>[\w\.\-\_\+]*)?", - r"(?<![\\\w])(?P<str>b?\'\'\'.*?(?<!\\)\'\'\'|b?\'.*?(?<!\\)\'|b?\"\"\".*?(?<!\\)\"\"\"|b?\".*?(?<!\\)\")", - r"(?P<uuid>[a-fA-F0-9]{8}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{12})", - r"(?P<url>(file|https|http|ws|wss):\/\/[0-9a-zA-Z\$\-\_\+\!`\(\)\,\.\?\/\;\:\&\=\%\#]*)", + r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[-+]?\d+?)?\b|0x[0-9a-fA-F]*)", + r"(?P<path>\B(/[-\w._+]+)*\/)(?P<filename>[-\w._+]*)?", + r"(?<![\\\w])(?P<str>b?'''.*?(?<!\\)'''|b?'.*?(?<!\\)'|b?\"\"\".*?(?<!\\)\"\"\"|b?\".*?(?<!\\)\")", + r"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#]*)", ), ] @@ -104,17 +105,39 @@ class ReprHighlighter(RegexHighlighter): class JSONHighlighter(RegexHighlighter): """Highlights JSON""" + # Captures the start and end of JSON strings, handling escaped quotes + JSON_STR = r"(?<![\\\w])(?P<str>b?\".*?(?<!\\)\")" + JSON_WHITESPACE = {" ", "\n", "\r", "\t"} + base_style = "json." highlights = [ _combine_regex( r"(?P<brace>[\{\[\(\)\]\}])", r"\b(?P<bool_true>true)\b|\b(?P<bool_false>false)\b|\b(?P<null>null)\b", r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[\-\+]?\d+?)?\b|0x[0-9a-fA-F]*)", - r"(?<![\\\w])(?P<str>b?\".*?(?<!\\)\")", + JSON_STR, ), - r"(?<![\\\w])(?P<key>b?\".*?(?<!\\)\")\:", ] + def highlight(self, text: Text) -> None: + super().highlight(text) + + # Additional work to handle highlighting JSON keys + plain = text.plain + append = text.spans.append + whitespace = self.JSON_WHITESPACE + for match in re.finditer(self.JSON_STR, plain): + start, end = match.span() + cursor = end + while cursor < len(plain): + char = plain[cursor] + cursor += 1 + if char == ":": + append(Span(start, end, "json.key")) + elif char in whitespace: + continue + break + if __name__ == "__main__": # pragma: no cover from .console import Console @@ -145,3 +168,6 @@ if __name__ == "__main__": # pragma: no cover console.print( "127.0.1.1 bar 192.168.1.4 2001:0db8:85a3:0000:0000:8a2e:0370:7334 foo" ) + import json + + console.print_json(json.dumps(obj={"name": "apple", "count": 1}), indent=None) diff --git a/src/pip/_vendor/rich/jupyter.py b/src/pip/_vendor/rich/jupyter.py index bedf5cb19..2ff4acca1 100644 --- a/src/pip/_vendor/rich/jupyter.py +++ b/src/pip/_vendor/rich/jupyter.py @@ -1,9 +1,12 @@ -from typing import Any, Dict, Iterable, List +from typing import TYPE_CHECKING, Any, Dict, Iterable, List from . import get_console from .segment import Segment from .terminal_theme import DEFAULT_TERMINAL_THEME +if TYPE_CHECKING: + from pip._vendor.rich.console import ConsoleRenderable + 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> """ @@ -33,10 +36,13 @@ class JupyterMixin: __slots__ = () def _repr_mimebundle_( - self, include: Iterable[str], exclude: Iterable[str], **kwargs: Any + self: "ConsoleRenderable", + include: Iterable[str], + exclude: Iterable[str], + **kwargs: Any, ) -> Dict[str, str]: console = get_console() - segments = list(console.render(self, console.options)) # type: ignore + segments = list(console.render(self, console.options)) html = _render_segments(segments) text = console._render_buffer(segments) data = {"text/plain": text, "text/html": html} @@ -63,7 +69,7 @@ def _render_segments(segments: Iterable[Segment]) -> str: rule = style.get_html_style(theme) text = f'<span style="{rule}">{text}</span>' if rule else text if style.link: - text = f'<a href="{style.link}">{text}</a>' + text = f'<a href="{style.link}" target="_blank">{text}</a>' append_fragment(text) code = "".join(fragments) diff --git a/src/pip/_vendor/rich/layout.py b/src/pip/_vendor/rich/layout.py index 22a4c5478..1d704652e 100644 --- a/src/pip/_vendor/rich/layout.py +++ b/src/pip/_vendor/rich/layout.py @@ -73,6 +73,7 @@ class _Placeholder: style=self.style, title=self.highlighter(title), border_style="blue", + height=height, ) @@ -299,7 +300,7 @@ class Layout: self._children.extend(_layouts) def split_row(self, *layouts: Union["Layout", RenderableType]) -> None: - """Split the layout in tow a row (Layouts side by side). + """Split the layout in to a row (layouts side by side). Args: *layouts (Layout): Positional arguments should be (sub) Layout instances. diff --git a/src/pip/_vendor/rich/logging.py b/src/pip/_vendor/rich/logging.py index 002f1f7bf..58188fd8a 100644 --- a/src/pip/_vendor/rich/logging.py +++ b/src/pip/_vendor/rich/logging.py @@ -2,7 +2,8 @@ import logging from datetime import datetime from logging import Handler, LogRecord from pathlib import Path -from typing import ClassVar, List, Optional, Type, Union +from types import ModuleType +from typing import ClassVar, List, Optional, Iterable, Type, Union from . import get_console from ._log_render import LogRender, FormatTimeCallable @@ -37,10 +38,12 @@ class RichHandler(Handler): tracebacks_theme (str, optional): Override pygments theme used in traceback. tracebacks_word_wrap (bool, optional): Enable word wrapping of long tracebacks lines. Defaults to True. tracebacks_show_locals (bool, optional): Enable display of locals in tracebacks. Defaults to False. + tracebacks_suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback. locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. Defaults to 10. locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80. log_time_format (Union[str, TimeFormatterCallable], optional): If ``log_time`` is enabled, either string for strftime or callable that formats the time. Defaults to "[%x %X] ". + keywords (List[str], optional): List of words to highlight instead of ``RichHandler.KEYWORDS``. """ KEYWORDS: ClassVar[Optional[List[str]]] = [ @@ -73,9 +76,11 @@ class RichHandler(Handler): tracebacks_theme: Optional[str] = None, tracebacks_word_wrap: bool = True, tracebacks_show_locals: bool = False, + tracebacks_suppress: Iterable[Union[str, ModuleType]] = (), locals_max_length: int = 10, locals_max_string: int = 80, log_time_format: Union[str, FormatTimeCallable] = "[%x %X]", + keywords: Optional[List[str]] = None, ) -> None: super().__init__(level=level) self.console = console or get_console() @@ -96,8 +101,10 @@ class RichHandler(Handler): self.tracebacks_theme = tracebacks_theme self.tracebacks_word_wrap = tracebacks_word_wrap self.tracebacks_show_locals = tracebacks_show_locals + self.tracebacks_suppress = tracebacks_suppress self.locals_max_length = locals_max_length self.locals_max_string = locals_max_string + self.keywords = keywords def get_level_text(self, record: LogRecord) -> Text: """Get the level name from the record. @@ -137,6 +144,7 @@ class RichHandler(Handler): show_locals=self.tracebacks_show_locals, locals_max_length=self.locals_max_length, locals_max_string=self.locals_max_string, + suppress=self.tracebacks_suppress, ) message = record.getMessage() if self.formatter: @@ -171,8 +179,12 @@ class RichHandler(Handler): if highlighter: message_text = highlighter(message_text) - if self.KEYWORDS: - message_text.highlight_words(self.KEYWORDS, "logging.keyword") + if self.keywords is None: + self.keywords = self.KEYWORDS + + if self.keywords: + message_text.highlight_words(self.keywords, "logging.keyword") + return message_text def render( diff --git a/src/pip/_vendor/rich/markup.py b/src/pip/_vendor/rich/markup.py index 619540202..b7150c1c5 100644 --- a/src/pip/_vendor/rich/markup.py +++ b/src/pip/_vendor/rich/markup.py @@ -1,21 +1,20 @@ +import re from ast import literal_eval from operator import attrgetter -import re from typing import Callable, Iterable, List, Match, NamedTuple, Optional, Tuple, Union +from ._emoji_replace import _emoji_replace +from .emoji import EmojiVariant from .errors import MarkupError from .style import Style from .text import Span, Text -from .emoji import EmojiVariant -from ._emoji_replace import _emoji_replace - RE_TAGS = re.compile( - r"""((\\*)\[([a-z#\/@].*?)\])""", + r"""((\\*)\[([a-z#/@][^[]*?)])""", re.VERBOSE, ) -RE_HANDLER = re.compile(r"^([\w\.]*?)(\(.*?\))?$") +RE_HANDLER = re.compile(r"^([\w.]*?)(\(.*?\))?$") class Tag(NamedTuple): @@ -146,6 +145,8 @@ def render( for position, plain_text, tag in _parse(markup): if plain_text is not None: + # Handle open brace escapes, where the brace is not part of a tag. + plain_text = plain_text.replace("\\[", "[") append(emoji_replace(plain_text) if emoji else plain_text) elif tag is not None: if tag.name.startswith("/"): # Closing tag @@ -233,8 +234,8 @@ if __name__ == "__main__": # pragma: no cover ":warning-emoji: [bold red blink] DANGER![/]", ] - from pip._vendor.rich.table import Table from pip._vendor.rich import print + from pip._vendor.rich.table import Table grid = Table("Markup", "Result", padding=(0, 1)) diff --git a/src/pip/_vendor/rich/measure.py b/src/pip/_vendor/rich/measure.py index aea238df9..e12787c8b 100644 --- a/src/pip/_vendor/rich/measure.py +++ b/src/pip/_vendor/rich/measure.py @@ -1,5 +1,5 @@ from operator import itemgetter -from typing import Callable, Iterable, NamedTuple, Optional, TYPE_CHECKING +from typing import TYPE_CHECKING, Callable, Iterable, NamedTuple, Optional from . import errors from .protocol import is_renderable, rich_cast @@ -96,7 +96,9 @@ class Measurement(NamedTuple): if _max_width < 1: return Measurement(0, 0) if isinstance(renderable, str): - renderable = console.render_str(renderable, markup=options.markup) + renderable = console.render_str( + renderable, markup=options.markup, highlight=False + ) renderable = rich_cast(renderable) if is_renderable(renderable): get_console_width: Optional[ diff --git a/src/pip/_vendor/rich/pager.py b/src/pip/_vendor/rich/pager.py index dbfb973e3..a3f7aa62a 100644 --- a/src/pip/_vendor/rich/pager.py +++ b/src/pip/_vendor/rich/pager.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Any, Callable +from typing import Any class Pager(ABC): diff --git a/src/pip/_vendor/rich/panel.py b/src/pip/_vendor/rich/panel.py index 151fe5f01..fc2807c31 100644 --- a/src/pip/_vendor/rich/panel.py +++ b/src/pip/_vendor/rich/panel.py @@ -1,14 +1,13 @@ -from typing import Optional, TYPE_CHECKING - -from .box import Box, ROUNDED +from typing import TYPE_CHECKING, Optional from .align import AlignMethod +from .box import ROUNDED, Box from .jupyter import JupyterMixin from .measure import Measurement, measure_renderables from .padding import Padding, PaddingDimensions +from .segment import Segment from .style import StyleType from .text import Text, TextType -from .segment import Segment if TYPE_CHECKING: from .console import Console, ConsoleOptions, RenderableType, RenderResult @@ -183,7 +182,7 @@ class Panel(JupyterMixin): else: title_text.align(self.title_align, width - 4, character=box.top) yield Segment(box.top_left + box.top, border_style) - yield from console.render(title_text) + yield from console.render(title_text, child_options.update_width(width - 4)) yield Segment(box.top + box.top_right, border_style) yield new_line @@ -202,7 +201,9 @@ class Panel(JupyterMixin): else: subtitle_text.align(self.subtitle_align, width - 4, character=box.bottom) yield Segment(box.bottom_left + box.bottom, border_style) - yield from console.render(subtitle_text) + yield from console.render( + subtitle_text, child_options.update_width(width - 4) + ) yield Segment(box.bottom + box.bottom_right, border_style) yield new_line @@ -235,8 +236,8 @@ if __name__ == "__main__": # pragma: no cover c = Console() + from .box import DOUBLE, ROUNDED from .padding import Padding - from .box import ROUNDED, DOUBLE p = Panel( "Hello, World!", diff --git a/src/pip/_vendor/rich/pretty.py b/src/pip/_vendor/rich/pretty.py index 606ee3382..95f3d34f9 100644 --- a/src/pip/_vendor/rich/pretty.py +++ b/src/pip/_vendor/rich/pretty.py @@ -1,8 +1,8 @@ import builtins +import collections import dataclasses import inspect import os -import re import sys from array import array from collections import Counter, UserDict, UserList, defaultdict, deque @@ -29,8 +29,7 @@ from pip._vendor.rich.repr import RichReprResult try: import attr as _attr_module except ImportError: # pragma: no cover - _attr_module = None # type: ignore - + _attr_module = None # type: ignore[assignment] from . import get_console from ._loop import loop_last @@ -80,6 +79,29 @@ def _is_dataclass_repr(obj: object) -> bool: return False +_dummy_namedtuple = collections.namedtuple("_dummy_namedtuple", []) + + +def _has_default_namedtuple_repr(obj: object) -> bool: + """Check if an instance of namedtuple contains the default repr + + Args: + obj (object): A namedtuple + + Returns: + bool: True if the default repr is used, False if there's a custom repr. + """ + obj_file = None + try: + obj_file = inspect.getfile(obj.__repr__) + except (OSError, TypeError): + # OSError handles case where object is defined in __main__ scope, e.g. REPL - no filename available. + # TypeError trapped defensively, in case of object without filename slips through. + pass + default_repr_file = inspect.getfile(_dummy_namedtuple.__repr__) + return obj_file == default_repr_file + + def _ipy_display_hook( value: Any, console: Optional["Console"] = None, @@ -93,7 +115,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 +146,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 +166,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", @@ -175,10 +207,10 @@ def install( """Replacement sys.displayhook which prettifies objects with Rich.""" if value is not None: assert console is not None - builtins._ = None # type: ignore + builtins._ = None # type: ignore[attr-defined] console.print( value - if isinstance(value, RichRenderable) + if _safe_isinstance(value, RichRenderable) else Pretty( value, overflow=overflow, @@ -189,13 +221,13 @@ def install( ), crop=crop, ) - builtins._ = value # type: ignore + builtins._ = value # type: ignore[attr-defined] try: # pragma: no cover - ip = get_ipython() # type: ignore + ip = get_ipython() # type: ignore[name-defined] from IPython.core.formatters import BaseFormatter - class RichFormatter(BaseFormatter): # type: ignore + class RichFormatter(BaseFormatter): # type: ignore[misc] pprint: bool = True def __call__(self, value: Any) -> Any: @@ -314,6 +346,7 @@ class Pretty(JupyterMixin): indent_size=self.indent_size, max_length=self.max_length, max_string=self.max_string, + expand_all=self.expand_all, ) text_width = ( max(cell_len(line) for line in pretty_str.splitlines()) if pretty_str else 0 @@ -355,7 +388,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) @@ -373,6 +406,7 @@ class Node: empty: str = "" last: bool = False is_tuple: bool = False + is_namedtuple: bool = False children: Optional[List["Node"]] = None key_separator = ": " separator: str = ", " @@ -387,7 +421,7 @@ class Node: elif self.children is not None: if self.children: yield self.open_brace - if self.is_tuple and len(self.children) == 1: + if self.is_tuple and not self.is_namedtuple and len(self.children) == 1: yield from self.children[0].iter_tokens() yield "," else: @@ -514,6 +548,25 @@ class _Line: ) +def _is_namedtuple(obj: Any) -> bool: + """Checks if an object is most likely a namedtuple. It is possible + to craft an object that passes this check and isn't a namedtuple, but + there is only a minuscule chance of this happening unintentionally. + + Args: + obj (Any): The object to test + + Returns: + bool: True if the object is a namedtuple. False otherwise. + """ + try: + fields = getattr(obj, "_fields", None) + except Exception: + # Being very defensive - if we cannot get the attr then its not a namedtuple + return False + return isinstance(obj, tuple) and isinstance(fields, tuple) + + def traverse( _object: Any, max_length: Optional[int] = None, @@ -539,7 +592,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 +618,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 +675,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 +742,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)) ): @@ -721,10 +774,28 @@ def traverse( append(child_node) pop_visited(obj_id) - - elif isinstance(obj, _CONTAINERS): + elif _is_namedtuple(obj) and _has_default_namedtuple_repr(obj): + if reached_max_depth: + node = Node(value_repr="...") + else: + children = [] + class_name = obj.__class__.__name__ + node = Node( + open_brace=f"{class_name}(", + close_brace=")", + children=children, + empty=f"{class_name}()", + ) + append = children.append + for last, (key, value) in loop_last(obj._asdict().items()): + child_node = _traverse(value, depth=depth + 1) + child_node.key_repr = key + child_node.last = last + child_node.key_separator = "=" + append(child_node) + 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 +823,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) @@ -770,14 +841,15 @@ def traverse( child_node.last = index == last_item_index append(child_node) if max_length is not None and num_items > max_length: - append(Node(value_repr=f"... +{num_items-max_length}", last=True)) + append(Node(value_repr=f"... +{num_items - max_length}", last=True)) else: node = Node(empty=empty, children=[], last=root) 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) + node.is_namedtuple = _is_namedtuple(obj) return node node = _traverse(_object, root=True) @@ -812,13 +884,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 @@ -868,6 +940,15 @@ if __name__ == "__main__": # pragma: no cover 1 / 0 return "this will fail" + from typing import NamedTuple + + class StockKeepingUnit(NamedTuple): + name: str + description: str + price: float + category: str + reviews: List[str] + d = defaultdict(int) d["foo"] = 5 data = { @@ -894,9 +975,16 @@ if __name__ == "__main__": # pragma: no cover ] ), "atomic": (False, True, None), + "namedtuple": StockKeepingUnit( + "Sparkling British Spring Water", + "Carbonated spring water", + 0.9, + "water", + ["its amazing!", "its terrible!"], + ), "Broken": BrokenRepr(), } - data["foo"].append(data) # type: ignore + data["foo"].append(data) # type: ignore[attr-defined] from pip._vendor.rich import print diff --git a/src/pip/_vendor/rich/progress.py b/src/pip/_vendor/rich/progress.py index 1f670db43..5140eda63 100644 --- a/src/pip/_vendor/rich/progress.py +++ b/src/pip/_vendor/rich/progress.py @@ -1,28 +1,44 @@ +import io +import sys +import typing +import warnings from abc import ABC, abstractmethod from collections import deque from collections.abc import Sized from dataclasses import dataclass, field from datetime import timedelta +from io import RawIOBase, UnsupportedOperation from math import ceil +from mmap import mmap +from os import PathLike, stat from threading import Event, RLock, Thread from types import TracebackType from typing import ( Any, + BinaryIO, Callable, + ContextManager, Deque, Dict, + Generic, Iterable, List, NamedTuple, NewType, Optional, Sequence, + TextIO, Tuple, Type, TypeVar, Union, ) +if sys.version_info >= (3, 8): + from typing import Literal +else: + from pip._vendor.typing_extensions import Literal # pragma: no cover + from . import filesize, get_console from .console import Console, JustifyMethod, RenderableType, Group from .highlighter import Highlighter @@ -41,6 +57,9 @@ ProgressType = TypeVar("ProgressType") GetTimeCallable = Callable[[], float] +_I = typing.TypeVar("_I", TextIO, BinaryIO) + + class _TrackThread(Thread): """A thread to periodically update progress.""" @@ -149,6 +168,320 @@ def track( ) +class _Reader(RawIOBase, BinaryIO): + """A reader that tracks progress while it's being read from.""" + + def __init__( + self, + handle: BinaryIO, + progress: "Progress", + task: TaskID, + close_handle: bool = True, + ) -> None: + self.handle = handle + self.progress = progress + self.task = task + self.close_handle = close_handle + self._closed = False + + def __enter__(self) -> "_Reader": + self.handle.__enter__() + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + self.close() + + def __iter__(self) -> BinaryIO: + return self + + def __next__(self) -> bytes: + line = next(self.handle) + self.progress.advance(self.task, advance=len(line)) + return line + + @property + def closed(self) -> bool: + return self._closed + + def fileno(self) -> int: + return self.handle.fileno() + + def isatty(self) -> bool: + return self.handle.isatty() + + def readable(self) -> bool: + return self.handle.readable() + + def seekable(self) -> bool: + return self.handle.seekable() + + def writable(self) -> bool: + return False + + def read(self, size: int = -1) -> bytes: + block = self.handle.read(size) + self.progress.advance(self.task, advance=len(block)) + return block + + def readinto(self, b: Union[bytearray, memoryview, mmap]): # type: ignore[no-untyped-def, override] + n = self.handle.readinto(b) # type: ignore[attr-defined] + self.progress.advance(self.task, advance=n) + return n + + def readline(self, size: int = -1) -> bytes: # type: ignore[override] + line = self.handle.readline(size) + self.progress.advance(self.task, advance=len(line)) + return line + + def readlines(self, hint: int = -1) -> List[bytes]: + lines = self.handle.readlines(hint) + self.progress.advance(self.task, advance=sum(map(len, lines))) + return lines + + def close(self) -> None: + if self.close_handle: + self.handle.close() + self._closed = True + + def seek(self, offset: int, whence: int = 0) -> int: + pos = self.handle.seek(offset, whence) + self.progress.update(self.task, completed=pos) + return pos + + def tell(self) -> int: + return self.handle.tell() + + def write(self, s: Any) -> int: + raise UnsupportedOperation("write") + + +class _ReadContext(ContextManager[_I], Generic[_I]): + """A utility class to handle a context for both a reader and a progress.""" + + def __init__(self, progress: "Progress", reader: _I) -> None: + self.progress = progress + self.reader: _I = reader + + def __enter__(self) -> _I: + self.progress.start() + return self.reader.__enter__() + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + self.progress.stop() + self.reader.__exit__(exc_type, exc_val, exc_tb) + + +def wrap_file( + file: BinaryIO, + total: int, + *, + description: str = "Reading...", + auto_refresh: bool = True, + console: Optional[Console] = None, + transient: bool = False, + get_time: Optional[Callable[[], float]] = None, + refresh_per_second: float = 10, + style: StyleType = "bar.back", + complete_style: StyleType = "bar.complete", + finished_style: StyleType = "bar.finished", + pulse_style: StyleType = "bar.pulse", + disable: bool = False, +) -> ContextManager[BinaryIO]: + """Read bytes from a file while tracking progress. + + Args: + file (Union[str, PathLike[str], BinaryIO]): The path to the file to read, or a file-like object in binary mode. + total (int): Total number of bytes to read. + description (str, optional): Description of task show next to progress bar. Defaults to "Reading". + auto_refresh (bool, optional): Automatic refresh, disable to force a refresh after each iteration. Default is True. + transient: (bool, optional): Clear the progress on exit. Defaults to False. + console (Console, optional): Console to write to. Default creates internal Console instance. + refresh_per_second (float): Number of times per second to refresh the progress information. Defaults to 10. + style (StyleType, optional): Style for the bar background. Defaults to "bar.back". + complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete". + finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done". + pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse". + disable (bool, optional): Disable display of progress. + Returns: + ContextManager[BinaryIO]: A context manager yielding a progress reader. + + """ + + columns: List["ProgressColumn"] = ( + [TextColumn("[progress.description]{task.description}")] if description else [] + ) + columns.extend( + ( + BarColumn( + style=style, + complete_style=complete_style, + finished_style=finished_style, + pulse_style=pulse_style, + ), + DownloadColumn(), + TimeRemainingColumn(), + ) + ) + progress = Progress( + *columns, + auto_refresh=auto_refresh, + console=console, + transient=transient, + get_time=get_time, + refresh_per_second=refresh_per_second or 10, + disable=disable, + ) + + reader = progress.wrap_file(file, total=total, description=description) + return _ReadContext(progress, reader) + + +@typing.overload +def open( + file: Union[str, "PathLike[str]", bytes], + mode: Union[Literal["rt"], Literal["r"]], + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + *, + total: Optional[int] = None, + description: str = "Reading...", + auto_refresh: bool = True, + console: Optional[Console] = None, + transient: bool = False, + get_time: Optional[Callable[[], float]] = None, + refresh_per_second: float = 10, + style: StyleType = "bar.back", + complete_style: StyleType = "bar.complete", + finished_style: StyleType = "bar.finished", + pulse_style: StyleType = "bar.pulse", + disable: bool = False, +) -> ContextManager[TextIO]: + pass + + +@typing.overload +def open( + file: Union[str, "PathLike[str]", bytes], + mode: Literal["rb"], + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + *, + total: Optional[int] = None, + description: str = "Reading...", + auto_refresh: bool = True, + console: Optional[Console] = None, + transient: bool = False, + get_time: Optional[Callable[[], float]] = None, + refresh_per_second: float = 10, + style: StyleType = "bar.back", + complete_style: StyleType = "bar.complete", + finished_style: StyleType = "bar.finished", + pulse_style: StyleType = "bar.pulse", + disable: bool = False, +) -> ContextManager[BinaryIO]: + pass + + +def open( + file: Union[str, "PathLike[str]", bytes], + mode: Union[Literal["rb"], Literal["rt"], Literal["r"]] = "r", + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + *, + total: Optional[int] = None, + description: str = "Reading...", + auto_refresh: bool = True, + console: Optional[Console] = None, + transient: bool = False, + get_time: Optional[Callable[[], float]] = None, + refresh_per_second: float = 10, + style: StyleType = "bar.back", + complete_style: StyleType = "bar.complete", + finished_style: StyleType = "bar.finished", + pulse_style: StyleType = "bar.pulse", + disable: bool = False, +) -> Union[ContextManager[BinaryIO], ContextManager[TextIO]]: + """Read bytes from a file while tracking progress. + + Args: + path (Union[str, PathLike[str], BinaryIO]): The path to the file to read, or a file-like object in binary mode. + mode (str): The mode to use to open the file. Only supports "r", "rb" or "rt". + buffering (int): The buffering strategy to use, see :func:`io.open`. + encoding (str, optional): The encoding to use when reading in text mode, see :func:`io.open`. + errors (str, optional): The error handling strategy for decoding errors, see :func:`io.open`. + newline (str, optional): The strategy for handling newlines in text mode, see :func:`io.open` + total: (int, optional): Total number of bytes to read. Must be provided if reading from a file handle. Default for a path is os.stat(file).st_size. + description (str, optional): Description of task show next to progress bar. Defaults to "Reading". + auto_refresh (bool, optional): Automatic refresh, disable to force a refresh after each iteration. Default is True. + transient: (bool, optional): Clear the progress on exit. Defaults to False. + console (Console, optional): Console to write to. Default creates internal Console instance. + refresh_per_second (float): Number of times per second to refresh the progress information. Defaults to 10. + style (StyleType, optional): Style for the bar background. Defaults to "bar.back". + complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete". + finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done". + pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse". + disable (bool, optional): Disable display of progress. + encoding (str, optional): The encoding to use when reading in text mode. + + Returns: + ContextManager[BinaryIO]: A context manager yielding a progress reader. + + """ + + columns: List["ProgressColumn"] = ( + [TextColumn("[progress.description]{task.description}")] if description else [] + ) + columns.extend( + ( + BarColumn( + style=style, + complete_style=complete_style, + finished_style=finished_style, + pulse_style=pulse_style, + ), + DownloadColumn(), + TimeRemainingColumn(), + ) + ) + progress = Progress( + *columns, + auto_refresh=auto_refresh, + console=console, + transient=transient, + get_time=get_time, + refresh_per_second=refresh_per_second or 10, + disable=disable, + ) + + reader = progress.open( + file, + mode=mode, + buffering=buffering, + encoding=encoding, + errors=errors, + newline=newline, + total=total, + description=description, + ) + return _ReadContext(progress, reader) # type: ignore[return-value, type-var] + + class ProgressColumn(ABC): """Base class for a widget to use in progress display.""" @@ -343,18 +676,48 @@ class TimeElapsedColumn(ProgressColumn): class TimeRemainingColumn(ProgressColumn): - """Renders estimated time remaining.""" + """Renders estimated time remaining. + + Args: + compact (bool, optional): Render MM:SS when time remaining is less than an hour. Defaults to False. + elapsed_when_finished (bool, optional): Render time elapsed when the task is finished. Defaults to False. + """ # Only refresh twice a second to prevent jitter max_refresh = 0.5 + def __init__( + self, + compact: bool = False, + elapsed_when_finished: bool = False, + table_column: Optional[Column] = None, + ): + self.compact = compact + self.elapsed_when_finished = elapsed_when_finished + super().__init__(table_column=table_column) + def render(self, task: "Task") -> Text: """Show time remaining.""" - remaining = task.time_remaining - if remaining is None: - return Text("-:--:--", style="progress.remaining") - remaining_delta = timedelta(seconds=int(remaining)) - return Text(str(remaining_delta), style="progress.remaining") + if self.elapsed_when_finished and task.finished: + task_time = task.finished_time + style = "progress.elapsed" + else: + task_time = task.time_remaining + style = "progress.remaining" + + if task_time is None: + return Text("--:--" if self.compact else "-:--:--", style=style) + + # Based on https://github.com/tqdm/tqdm/blob/master/tqdm/std.py + minutes, seconds = divmod(int(task_time), 60) + hours, minutes = divmod(minutes, 60) + + if self.compact and not hours: + formatted = f"{minutes:02d}:{seconds:02d}" + else: + formatted = f"{hours:d}:{minutes:02d}:{seconds:02d}" + + return Text(formatted, style=style) class FileSizeColumn(ProgressColumn): @@ -375,6 +738,33 @@ class TotalFileSizeColumn(ProgressColumn): return Text(data_size, style="progress.filesize.total") +class MofNCompleteColumn(ProgressColumn): + """Renders completed count/total, e.g. ' 10/1000'. + + Best for bounded tasks with int quantities. + + Space pads the completed count so that progress length does not change as task progresses + past powers of 10. + + Args: + separator (str, optional): Text to separate completed and total values. Defaults to "/". + """ + + def __init__(self, separator: str = "/", table_column: Optional[Column] = None): + self.separator = separator + super().__init__(table_column=table_column) + + def render(self, task: "Task") -> Text: + """Show completed/total.""" + completed = int(task.completed) + total = int(task.total) + total_width = len(str(total)) + return Text( + f"{completed:{total_width}d}{self.separator}{total}", + style="progress.download", + ) + + class DownloadColumn(ProgressColumn): """Renders file size downloaded and total, e.g. '0.5/2.3 GB'. @@ -400,7 +790,9 @@ class DownloadColumn(ProgressColumn): ) else: unit, suffix = filesize.pick_unit_and_suffix( - total, ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"], 1000 + total, + ["bytes", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"], + 1000, ) completed_ratio = completed / unit total_ratio = total / unit @@ -475,7 +867,7 @@ class Task: """Optional[float]: The last speed for a finished task.""" _progress: Deque[ProgressSample] = field( - default_factory=deque, init=False, repr=False + default_factory=lambda: deque(maxlen=1000), init=False, repr=False ) _lock: RLock = field(repr=False, default_factory=RLock) @@ -588,12 +980,7 @@ class Progress(JupyterMixin): refresh_per_second is None or refresh_per_second > 0 ), "refresh_per_second must be > 0" self._lock = RLock() - self.columns = columns or ( - TextColumn("[progress.description]{task.description}"), - BarColumn(), - TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), - TimeRemainingColumn(), - ) + self.columns = columns or self.get_default_columns() self.speed_estimate_period = speed_estimate_period self.disable = disable @@ -613,6 +1000,37 @@ class Progress(JupyterMixin): self.print = self.console.print self.log = self.console.log + @classmethod + def get_default_columns(cls) -> Tuple[ProgressColumn, ...]: + """Get the default columns used for a new Progress instance: + - a text column for the description (TextColumn) + - the bar itself (BarColumn) + - a text column showing completion percentage (TextColumn) + - an estimated-time-remaining column (TimeRemainingColumn) + If the Progress instance is created without passing a columns argument, + the default columns defined here will be used. + + You can also create a Progress instance using custom columns before + and/or after the defaults, as in this example: + + progress = Progress( + SpinnerColumn(), + *Progress.default_columns(), + "Elapsed:", + TimeElapsedColumn(), + ) + + This code shows the creation of a Progress display, containing + a spinner to the left, the default columns, and a labeled elapsed + time column. + """ + return ( + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), + TimeRemainingColumn(), + ) + @property def console(self) -> Console: return self.live.console @@ -709,6 +1127,157 @@ class Progress(JupyterMixin): advance(task_id, 1) refresh() + def wrap_file( + self, + file: BinaryIO, + total: Optional[int] = None, + *, + task_id: Optional[TaskID] = None, + description: str = "Reading...", + ) -> BinaryIO: + """Track progress file reading from a binary file. + + Args: + file (BinaryIO): A file-like object opened in binary mode. + total (int, optional): Total number of bytes to read. This must be provided unless a task with a total is also given. + task_id (TaskID): Task to track. Default is new task. + description (str, optional): Description of task, if new task is created. + + Returns: + BinaryIO: A readable file-like object in binary mode. + + Raises: + ValueError: When no total value can be extracted from the arguments or the task. + """ + # attempt to recover the total from the task + total_bytes: Optional[float] = None + if total is not None: + total_bytes = total + elif task_id is not None: + with self._lock: + total_bytes = self._tasks[task_id].total + if total_bytes is None: + raise ValueError( + f"unable to get the total number of bytes, please specify 'total'" + ) + + # update total of task or create new task + if task_id is None: + task_id = self.add_task(description, total=total_bytes) + else: + self.update(task_id, total=total_bytes) + + return _Reader(file, self, task_id, close_handle=False) + + @typing.overload + def open( + self, + file: Union[str, "PathLike[str]", bytes], + mode: Literal["rb"], + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + *, + total: Optional[int] = None, + task_id: Optional[TaskID] = None, + description: str = "Reading...", + ) -> BinaryIO: + pass + + @typing.overload + def open( + self, + file: Union[str, "PathLike[str]", bytes], + mode: Union[Literal["r"], Literal["rt"]], + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + *, + total: Optional[int] = None, + task_id: Optional[TaskID] = None, + description: str = "Reading...", + ) -> TextIO: + pass + + def open( + self, + file: Union[str, "PathLike[str]", bytes], + mode: Union[Literal["rb"], Literal["rt"], Literal["r"]] = "r", + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + *, + total: Optional[int] = None, + task_id: Optional[TaskID] = None, + description: str = "Reading...", + ) -> Union[BinaryIO, TextIO]: + """Track progress while reading from a binary file. + + Args: + path (Union[str, PathLike[str]]): The path to the file to read. + mode (str): The mode to use to open the file. Only supports "r", "rb" or "rt". + buffering (int): The buffering strategy to use, see :func:`io.open`. + encoding (str, optional): The encoding to use when reading in text mode, see :func:`io.open`. + errors (str, optional): The error handling strategy for decoding errors, see :func:`io.open`. + newline (str, optional): The strategy for handling newlines in text mode, see :func:`io.open`. + total (int, optional): Total number of bytes to read. If none given, os.stat(path).st_size is used. + task_id (TaskID): Task to track. Default is new task. + description (str, optional): Description of task, if new task is created. + + Returns: + BinaryIO: A readable file-like object in binary mode. + + Raises: + ValueError: When an invalid mode is given. + """ + # normalize the mode (always rb, rt) + _mode = "".join(sorted(mode, reverse=False)) + if _mode not in ("br", "rt", "r"): + raise ValueError("invalid mode {!r}".format(mode)) + + # patch buffering to provide the same behaviour as the builtin `open` + line_buffering = buffering == 1 + if _mode == "br" and buffering == 1: + warnings.warn( + "line buffering (buffering=1) isn't supported in binary mode, the default buffer size will be used", + RuntimeWarning, + ) + buffering = -1 + elif _mode == "rt" or _mode == "r": + if buffering == 0: + raise ValueError("can't have unbuffered text I/O") + elif buffering == 1: + buffering = -1 + + # attempt to get the total with `os.stat` + if total is None: + total = stat(file).st_size + + # update total of task or create new task + if task_id is None: + task_id = self.add_task(description, total=total) + else: + self.update(task_id, total=total) + + # open the file in binary mode, + handle = io.open(file, "rb", buffering=buffering) + reader = _Reader(handle, self, task_id, close_handle=True) + + # wrap the reader in a `TextIOWrapper` if text mode + if mode == "r" or mode == "rt": + return io.TextIOWrapper( + reader, + encoding=encoding, + errors=errors, + newline=newline, + line_buffering=line_buffering, + ) + + return reader + def start_task(self, task_id: TaskID) -> None: """Start a task. @@ -787,8 +1356,6 @@ class Progress(JupyterMixin): popleft = _progress.popleft while _progress and _progress[0].timestamp < old_sample_time: popleft() - while len(_progress) > 1000: - popleft() if update_completed > 0: _progress.append(ProgressSample(current_time, update_completed)) if task.completed >= task.total and task.finished_time is None: @@ -1015,10 +1582,7 @@ if __name__ == "__main__": # pragma: no coverage with Progress( SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - BarColumn(), - TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), - TimeRemainingColumn(), + *Progress.get_default_columns(), TimeElapsedColumn(), console=console, transient=True, diff --git a/src/pip/_vendor/rich/prompt.py b/src/pip/_vendor/rich/prompt.py index b2cea2b52..2bd0a7724 100644 --- a/src/pip/_vendor/rich/prompt.py +++ b/src/pip/_vendor/rich/prompt.py @@ -228,14 +228,14 @@ class PromptBase(Generic[PromptType]): """ value = value.strip() try: - return_value = self.response_type(value) + return_value: PromptType = self.response_type(value) except ValueError: raise InvalidResponse(self.validate_error_message) if self.choices is not None and not self.check_choice(value): raise InvalidResponse(self.illegal_choice_message) - return return_value # type: ignore + return return_value def on_validate_error(self, value: str, error: InvalidResponse) -> None: """Called to handle validation error. diff --git a/src/pip/_vendor/rich/protocol.py b/src/pip/_vendor/rich/protocol.py index 624805211..12ab23713 100644 --- a/src/pip/_vendor/rich/protocol.py +++ b/src/pip/_vendor/rich/protocol.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, cast, Set, TYPE_CHECKING +from typing import Any, cast, Set, TYPE_CHECKING from inspect import isclass if TYPE_CHECKING: diff --git a/src/pip/_vendor/rich/repr.py b/src/pip/_vendor/rich/repr.py index 17147fd4b..36966e70f 100644 --- a/src/pip/_vendor/rich/repr.py +++ b/src/pip/_vendor/rich/repr.py @@ -1,5 +1,6 @@ from functools import partial import inspect +import sys from typing import ( Any, @@ -27,28 +28,28 @@ class ReprError(Exception): @overload -def auto(cls: Optional[T]) -> T: +def auto(cls: Optional[Type[T]]) -> Type[T]: ... @overload -def auto(*, angular: bool = False) -> Callable[[T], T]: +def auto(*, angular: bool = False) -> Callable[[Type[T]], Type[T]]: ... def auto( - cls: Optional[T] = None, *, angular: Optional[bool] = None -) -> Union[T, Callable[[T], T]]: + cls: Optional[Type[T]] = None, *, angular: Optional[bool] = None +) -> Union[Type[T], Callable[[Type[T]], Type[T]]]: """Class decorator to create __repr__ from __rich_repr__""" def do_replace(cls: Type[T], angular: Optional[bool] = None) -> Type[T]: - def auto_repr(self: Type[T]) -> str: + def auto_repr(self: T) -> str: """Create repr string from __rich_repr__""" repr_str: List[str] = [] append = repr_str.append - angular = getattr(self.__rich_repr__, "angular", False) # type: ignore - for arg in self.__rich_repr__(): # type: ignore + angular: bool = getattr(self.__rich_repr__, "angular", False) # type: ignore[attr-defined] + for arg in self.__rich_repr__(): # type: ignore[attr-defined] if isinstance(arg, tuple): if len(arg) == 1: append(repr(arg[0])) @@ -70,7 +71,7 @@ def auto( def auto_rich_repr(self: Type[T]) -> Result: """Auto generate __rich_rep__ from signature of __init__""" try: - signature = inspect.signature(self.__init__) ## type: ignore + signature = inspect.signature(self.__init__) for name, param in signature.parameters.items(): if param.kind == param.POSITIONAL_ONLY: yield getattr(self, name) @@ -89,33 +90,33 @@ def auto( if not hasattr(cls, "__rich_repr__"): auto_rich_repr.__doc__ = "Build a rich repr" - cls.__rich_repr__ = auto_rich_repr # type: ignore + cls.__rich_repr__ = auto_rich_repr # type: ignore[attr-defined] auto_repr.__doc__ = "Return repr(self)" - cls.__repr__ = auto_repr # type: ignore + cls.__repr__ = auto_repr # type: ignore[assignment] if angular is not None: - cls.__rich_repr__.angular = angular # type: ignore + cls.__rich_repr__.angular = angular # type: ignore[attr-defined] return cls if cls is None: - return partial(do_replace, angular=angular) # type: ignore + return partial(do_replace, angular=angular) else: - return do_replace(cls, angular=angular) # type: ignore + return do_replace(cls, angular=angular) @overload -def rich_repr(cls: Optional[T]) -> T: +def rich_repr(cls: Optional[Type[T]]) -> Type[T]: ... @overload -def rich_repr(*, angular: bool = False) -> Callable[[T], T]: +def rich_repr(*, angular: bool = False) -> Callable[[Type[T]], Type[T]]: ... def rich_repr( - cls: Optional[T] = None, *, angular: bool = False -) -> Union[T, Callable[[T], T]]: + cls: Optional[Type[T]] = None, *, angular: bool = False +) -> Union[Type[T], Callable[[Type[T]], Type[T]]]: if cls is None: return auto(angular=angular) else: @@ -143,7 +144,7 @@ if __name__ == "__main__": console.print(foo, width=30) console.rule("Angular repr") - Foo.__rich_repr__.angular = True # type: ignore + Foo.__rich_repr__.angular = True # type: ignore[attr-defined] console.print(foo) diff --git a/src/pip/_vendor/rich/segment.py b/src/pip/_vendor/rich/segment.py index 94ca73076..adb3dd3a9 100644 --- a/src/pip/_vendor/rich/segment.py +++ b/src/pip/_vendor/rich/segment.py @@ -64,15 +64,25 @@ class Segment(NamedTuple): Args: text (str): A piece of text. style (:class:`~rich.style.Style`, optional): An optional style to apply to the text. - control (Tuple[ControlCode..], optional): Optional sequence of control codes. + control (Tuple[ControlCode], optional): Optional sequence of control codes. + + Attributes: + cell_length (int): The cell length of this Segment. """ - text: str = "" - """Raw text.""" + text: str style: Optional[Style] = None - """An optional style.""" control: Optional[Sequence[ControlCode]] = None - """Optional sequence of control codes.""" + + @property + def cell_length(self) -> int: + """The number of terminal cells required to display self.text. + + Returns: + int: A number of cells. + """ + text, _style, control = self + return 0 if control else cell_len(text) def __rich_repr__(self) -> Result: yield self.text @@ -88,18 +98,13 @@ class Segment(NamedTuple): return bool(self.text) @property - def cell_length(self) -> int: - """Get cell length of segment.""" - return 0 if self.control else cell_len(self.text) - - @property def is_control(self) -> bool: """Check if the segment contains control codes.""" return self.control is not None @classmethod @lru_cache(1024 * 16) - def _split_cells(cls, segment: "Segment", cut: int) -> Tuple["Segment", "Segment"]: # type: ignore + def _split_cells(cls, segment: "Segment", cut: int) -> Tuple["Segment", "Segment"]: text, style, control = segment _Segment = Segment @@ -135,6 +140,8 @@ class Segment(NamedTuple): _Segment(" " + text[pos:], style, control), ) + raise AssertionError("Will never reach here") + def split_cells(self, cut: int) -> Tuple["Segment", "Segment"]: """Split segment in to two segments at the specified column. @@ -682,39 +689,35 @@ class SegmentLines: yield from line -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover + from pip._vendor.rich.console import Console + from pip._vendor.rich.syntax import Syntax + from pip._vendor.rich.text import Text - if __name__ == "__main__": # pragma: no cover - from pip._vendor.rich.console import Console - from pip._vendor.rich.syntax import Syntax - from pip._vendor.rich.text import Text + code = """from rich.console import Console +console = Console() +text = Text.from_markup("Hello, [bold magenta]World[/]!") +console.print(text)""" - code = """from rich.console import Console - console = Console() text = Text.from_markup("Hello, [bold magenta]World[/]!") - console.print(text)""" - text = Text.from_markup("Hello, [bold magenta]World[/]!") - - console = Console() + console = Console() - console.rule("rich.Segment") - console.print( - "A Segment is the last step in the Rich render process before generating text with ANSI codes." - ) - console.print("\nConsider the following code:\n") - console.print(Syntax(code, "python", line_numbers=True)) - console.print() - console.print( - "When you call [b]print()[/b], Rich [i]renders[/i] the object in to the the following:\n" - ) - fragments = list(console.render(text)) - console.print(fragments) - console.print() - console.print( - "The Segments are then processed to produce the following output:\n" - ) - console.print(text) - console.print( - "\nYou will only need to know this if you are implementing your own Rich renderables." - ) + console.rule("rich.Segment") + console.print( + "A Segment is the last step in the Rich render process before generating text with ANSI codes." + ) + console.print("\nConsider the following code:\n") + console.print(Syntax(code, "python", line_numbers=True)) + console.print() + console.print( + "When you call [b]print()[/b], Rich [i]renders[/i] the object in to the the following:\n" + ) + fragments = list(console.render(text)) + console.print(fragments) + console.print() + console.print("The Segments are then processed to produce the following output:\n") + console.print(text) + console.print( + "\nYou will only need to know this if you are implementing your own Rich renderables." + ) diff --git a/src/pip/_vendor/rich/syntax.py b/src/pip/_vendor/rich/syntax.py index 58cc1037f..089ebfcd1 100644 --- a/src/pip/_vendor/rich/syntax.py +++ b/src/pip/_vendor/rich/syntax.py @@ -1,6 +1,5 @@ import os.path import platform -from pip._vendor.rich.containers import Lines import textwrap from abc import ABC, abstractmethod from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Type, Union @@ -23,6 +22,8 @@ from pip._vendor.pygments.token import ( ) from pip._vendor.pygments.util import ClassNotFound +from pip._vendor.rich.containers import Lines + from ._loop import loop_first from .color import Color, blend_rgb from .console import Console, ConsoleOptions, JustifyMethod, RenderResult @@ -200,7 +201,8 @@ class Syntax(JupyterMixin): dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False. line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False. start_line (int, optional): Starting number for line numbers. Defaults to 1. - line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render. + line_range (Tuple[int | None, int | None], optional): If given should be a tuple of the start and end line to render. + A value of None in the tuple indicates the range is open in that direction. highlight_lines (Set[int]): A set of line numbers to highlight. code_width: Width of code to render (not including line numbers), or ``None`` to use all available width. tab_size (int, optional): Size of tabs. Defaults to 4. @@ -233,7 +235,7 @@ class Syntax(JupyterMixin): dedent: bool = False, line_numbers: bool = False, start_line: int = 1, - line_range: Optional[Tuple[int, int]] = None, + line_range: Optional[Tuple[Optional[int], Optional[int]]] = None, highlight_lines: Optional[Set[int]] = None, code_width: Optional[int] = None, tab_size: int = 4, @@ -264,6 +266,7 @@ class Syntax(JupyterMixin): cls, path: str, encoding: str = "utf-8", + lexer: Optional[Union[Lexer, str]] = None, theme: Union[str, SyntaxTheme] = DEFAULT_THEME, dedent: bool = False, line_numbers: bool = False, @@ -281,6 +284,7 @@ class Syntax(JupyterMixin): Args: path (str): Path to file to highlight. encoding (str): Encoding of file. + lexer (str | Lexer, optional): Lexer to use. If None, lexer will be auto-detected from path/file content. theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "emacs". dedent (bool, optional): Enable stripping of initial whitespace. Defaults to True. line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False. @@ -299,26 +303,12 @@ class Syntax(JupyterMixin): with open(path, "rt", encoding=encoding) as code_file: code = code_file.read() - lexer = None - lexer_name = "default" - try: - _, ext = os.path.splitext(path) - if ext: - extension = ext.lstrip(".").lower() - lexer = get_lexer_by_name(extension) - lexer_name = lexer.name - except ClassNotFound: - pass - - if lexer is None: - try: - lexer_name = guess_lexer_for_filename(path, code).name - except ClassNotFound: - pass + if not lexer: + lexer = cls.guess_lexer(path, code=code) return cls( code, - lexer_name, + lexer, theme=theme, dedent=dedent, line_numbers=line_numbers, @@ -332,6 +322,48 @@ class Syntax(JupyterMixin): indent_guides=indent_guides, ) + @classmethod + def guess_lexer(cls, path: str, code: Optional[str] = None) -> str: + """Guess the alias of the Pygments lexer to use based on a path and an optional string of code. + If code is supplied, it will use a combination of the code and the filename to determine the + best lexer to use. For example, if the file is ``index.html`` and the file contains Django + templating syntax, then "html+django" will be returned. If the file is ``index.html``, and no + templating language is used, the "html" lexer will be used. If no string of code + is supplied, the lexer will be chosen based on the file extension.. + + Args: + path (AnyStr): The path to the file containing the code you wish to know the lexer for. + code (str, optional): Optional string of code that will be used as a fallback if no lexer + is found for the supplied path. + + Returns: + str: The name of the Pygments lexer that best matches the supplied path/code. + """ + lexer: Optional[Lexer] = None + lexer_name = "default" + if code: + try: + lexer = guess_lexer_for_filename(path, code) + except ClassNotFound: + pass + + if not lexer: + try: + _, ext = os.path.splitext(path) + if ext: + extension = ext.lstrip(".").lower() + lexer = get_lexer_by_name(extension) + except ClassNotFound: + pass + + if lexer: + if lexer.aliases: + lexer_name = lexer.aliases[0] + else: + lexer_name = lexer.name + + return lexer_name + def _get_base_style(self) -> Style: """Get the base style.""" default_style = self._theme.get_background_style() + self.background_style @@ -369,7 +401,9 @@ class Syntax(JupyterMixin): return None def highlight( - self, code: str, line_range: Optional[Tuple[int, int]] = None + self, + code: str, + line_range: Optional[Tuple[Optional[int], Optional[int]]] = None, ) -> Text: """Highlight code and return a Text instance. @@ -417,7 +451,7 @@ class Syntax(JupyterMixin): """Convert tokens to spans.""" tokens = iter(line_tokenize()) line_no = 0 - _line_start = line_start - 1 + _line_start = line_start - 1 if line_start else 0 # Skip over tokens until line start while line_no < _line_start: @@ -430,7 +464,7 @@ class Syntax(JupyterMixin): yield (token, _get_theme_style(token_type)) if token.endswith("\n"): line_no += 1 - if line_no >= line_end: + if line_end and line_no >= line_end: break text.append_tokens(tokens_to_spans()) @@ -513,11 +547,6 @@ class Syntax(JupyterMixin): else self.code_width ) - line_offset = 0 - if self.line_range: - start_line, end_line = self.line_range - line_offset = max(0, start_line - 1) - ends_on_nl = self.code.endswith("\n") code = self.code if ends_on_nl else self.code + "\n" code = textwrap.dedent(code) if self.dedent else code @@ -550,7 +579,7 @@ class Syntax(JupyterMixin): else: syntax_lines = console.render_lines( text, - options.update(width=code_width, height=None), + options.update(width=code_width, height=None, justify="left"), style=self.background_style, pad=True, new_lines=True, @@ -559,6 +588,10 @@ class Syntax(JupyterMixin): yield from syntax_line return + start_line, end_line = self.line_range or (None, None) + line_offset = 0 + if start_line: + line_offset = max(0, start_line - 1) lines: Union[List[Text], Lines] = text.split("\n", allow_blank=ends_on_nl) if self.line_range: lines = lines[line_offset:end_line] @@ -591,7 +624,7 @@ class Syntax(JupyterMixin): if self.word_wrap: wrapped_lines = console.render_lines( line, - render_options.update(height=None), + render_options.update(height=None, justify="left"), style=background_style, pad=not transparent_background, ) @@ -702,7 +735,7 @@ if __name__ == "__main__": # pragma: no cover parser.add_argument( "-x", "--lexer", - default="default", + default=None, dest="lexer_name", help="Lexer name", ) @@ -726,6 +759,7 @@ if __name__ == "__main__": # pragma: no cover else: syntax = Syntax.from_path( args.path, + 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 da4386085..bafb86a13 100644 --- a/src/pip/_vendor/rich/table.py +++ b/src/pip/_vendor/rich/table.py @@ -37,7 +37,35 @@ if TYPE_CHECKING: @dataclass class Column: - """Defines a column in a table.""" + """Defines a column within a ~Table. + + Args: + title (Union[str, Text], optional): The title of the table rendered at the top. Defaults to None. + caption (Union[str, Text], optional): The table caption rendered below. Defaults to None. + width (int, optional): The width in characters of the table, or ``None`` to automatically fit. Defaults to None. + min_width (Optional[int], optional): The minimum width of the table, or ``None`` for no minimum. Defaults to None. + box (box.Box, optional): One of the constants in box.py used to draw the edges (see :ref:`appendix_box`), or ``None`` for no box lines. Defaults to box.HEAVY_HEAD. + safe_box (Optional[bool], optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True. + padding (PaddingDimensions, optional): Padding for cells (top, right, bottom, left). Defaults to (0, 1). + collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to False. + pad_edge (bool, optional): Enable padding of edge cells. Defaults to True. + expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False. + show_header (bool, optional): Show a header row. Defaults to True. + show_footer (bool, optional): Show a footer row. Defaults to False. + show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True. + show_lines (bool, optional): Draw lines between every row. Defaults to False. + leading (bool, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0. + style (Union[str, Style], optional): Default style for the table. Defaults to "none". + row_styles (List[Union, str], optional): Optional list of row styles, if more than one style is given then the styles will alternate. Defaults to None. + header_style (Union[str, Style], optional): Style of the header. Defaults to "table.header". + footer_style (Union[str, Style], optional): Style of the footer. Defaults to "table.footer". + border_style (Union[str, Style], optional): Style of the border. Defaults to None. + title_style (Union[str, Style], optional): Style of the title. Defaults to None. + caption_style (Union[str, Style], optional): Style of the caption. Defaults to None. + title_justify (str, optional): Justify method for title. Defaults to "center". + caption_justify (str, optional): Justify method for caption. Defaults to "center". + highlight (bool, optional): Highlight cell contents (if str). Defaults to False. + """ header: "RenderableType" = "" """RenderableType: Renderable for the header (typically a string)""" diff --git a/src/pip/_vendor/rich/tabulate.py b/src/pip/_vendor/rich/tabulate.py deleted file mode 100644 index 6889f2d33..000000000 --- a/src/pip/_vendor/rich/tabulate.py +++ /dev/null @@ -1,51 +0,0 @@ -from collections.abc import Mapping -from typing import Any, Optional -import warnings - -from pip._vendor.rich.console import JustifyMethod - -from . import box -from .highlighter import ReprHighlighter -from .pretty import Pretty -from .table import Table - - -def tabulate_mapping( - mapping: "Mapping[Any, Any]", - title: Optional[str] = None, - caption: Optional[str] = None, - title_justify: Optional[JustifyMethod] = None, - caption_justify: Optional[JustifyMethod] = None, -) -> Table: - """Generate a simple table from a mapping. - - Args: - mapping (Mapping): A mapping object (e.g. a dict); - title (str, optional): Optional title to be displayed over the table. - caption (str, optional): Optional caption to be displayed below the table. - title_justify (str, optional): Justify method for title. Defaults to None. - caption_justify (str, optional): Justify method for caption. Defaults to None. - - Returns: - Table: A table instance which may be rendered by the Console. - """ - warnings.warn("tabulate_mapping will be deprecated in Rich v11", DeprecationWarning) - table = Table( - show_header=False, - title=title, - caption=caption, - box=box.ROUNDED, - border_style="blue", - ) - table.title = title - table.caption = caption - if title_justify is not None: - table.title_justify = title_justify - if caption_justify is not None: - table.caption_justify = caption_justify - highlighter = ReprHighlighter() - for key, value in mapping.items(): - table.add_row( - Pretty(key, highlighter=highlighter), Pretty(value, highlighter=highlighter) - ) - return table diff --git a/src/pip/_vendor/rich/terminal_theme.py b/src/pip/_vendor/rich/terminal_theme.py index 801ac0b7b..ace8e93de 100644 --- a/src/pip/_vendor/rich/terminal_theme.py +++ b/src/pip/_vendor/rich/terminal_theme.py @@ -53,3 +53,101 @@ DEFAULT_TERMINAL_THEME = TerminalTheme( (255, 255, 255), ], ) + +SVG_EXPORT_THEME = TerminalTheme( + (12, 12, 12), + (242, 242, 242), + [ + (12, 12, 12), + (205, 49, 49), + (13, 188, 121), + (229, 229, 16), + (36, 114, 200), + (188, 63, 188), + (17, 168, 205), + (229, 229, 229), + ], + [ + (102, 102, 102), + (241, 76, 76), + (35, 209, 139), + (245, 245, 67), + (59, 142, 234), + (214, 112, 214), + (41, 184, 219), + (229, 229, 229), + ], +) + +MONOKAI = TerminalTheme( + (12, 12, 12), + (217, 217, 217), + [ + (26, 26, 26), + (244, 0, 95), + (152, 224, 36), + (253, 151, 31), + (157, 101, 255), + (244, 0, 95), + (88, 209, 235), + (196, 197, 181), + (98, 94, 76), + ], + [ + (244, 0, 95), + (152, 224, 36), + (224, 213, 97), + (157, 101, 255), + (244, 0, 95), + (88, 209, 235), + (246, 246, 239), + ], +) +DIMMED_MONOKAI = TerminalTheme( + (25, 25, 25), + (185, 188, 186), + [ + (58, 61, 67), + (190, 63, 72), + (135, 154, 59), + (197, 166, 53), + (79, 118, 161), + (133, 92, 141), + (87, 143, 164), + (185, 188, 186), + (136, 137, 135), + ], + [ + (251, 0, 31), + (15, 114, 47), + (196, 112, 51), + (24, 109, 227), + (251, 0, 103), + (46, 112, 109), + (253, 255, 185), + ], +) +NIGHT_OWLISH = TerminalTheme( + (255, 255, 255), + (64, 63, 83), + [ + (1, 22, 39), + (211, 66, 62), + (42, 162, 152), + (218, 170, 1), + (72, 118, 214), + (64, 63, 83), + (8, 145, 106), + (122, 129, 129), + (122, 129, 129), + ], + [ + (247, 110, 110), + (73, 208, 197), + (218, 194, 107), + (92, 167, 228), + (105, 112, 152), + (0, 201, 144), + (152, 159, 177), + ], +) diff --git a/src/pip/_vendor/rich/text.py b/src/pip/_vendor/rich/text.py index ea12c09d7..9bddbb26d 100644 --- a/src/pip/_vendor/rich/text.py +++ b/src/pip/_vendor/rich/text.py @@ -253,6 +253,7 @@ class Text(JupyterMixin): emoji_variant: Optional[EmojiVariant] = None, justify: Optional["JustifyMethod"] = None, overflow: Optional["OverflowMethod"] = None, + end: str = "\n", ) -> "Text": """Create Text instance from markup. @@ -261,6 +262,7 @@ class Text(JupyterMixin): emoji (bool, optional): Also render emoji code. Defaults to True. justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None. overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None. + end (str, optional): Character to end text with. Defaults to "\\\\n". Returns: Text: A Text instance with markup rendered. @@ -270,6 +272,7 @@ class Text(JupyterMixin): rendered_text = render(text, style, emoji=emoji, emoji_variant=emoji_variant) rendered_text.justify = justify rendered_text.overflow = overflow + rendered_text.end = end return rendered_text @classmethod diff --git a/src/pip/_vendor/rich/traceback.py b/src/pip/_vendor/rich/traceback.py index 66a39ebab..b14cb71a5 100644 --- a/src/pip/_vendor/rich/traceback.py +++ b/src/pip/_vendor/rich/traceback.py @@ -12,9 +12,10 @@ from pip._vendor.pygments.lexers import guess_lexer_for_filename from pip._vendor.pygments.token import Comment, Keyword, Name, Number, Operator, String from pip._vendor.pygments.token import Text as TextToken from pip._vendor.pygments.token import Token +from pip._vendor.pygments.util import ClassNotFound from . import pretty -from ._loop import loop_first, loop_last +from ._loop import loop_last from .columns import Columns from .console import Console, ConsoleOptions, ConsoleRenderable, RenderResult, group from .constrain import Constrain @@ -130,7 +131,7 @@ def install( try: # pragma: no cover # if within ipython, use customized traceback - ip = get_ipython() # type: ignore + ip = get_ipython() # type: ignore[name-defined] ipy_excepthook_closure(ip) return sys.excepthook except Exception: @@ -390,9 +391,8 @@ class Traceback: exc_type = cause.__class__ exc_value = cause traceback = cause.__traceback__ - if traceback: - is_cause = True - continue + is_cause = True + continue cause = exc_value.__context__ if ( @@ -403,9 +403,8 @@ class Traceback: exc_type = cause.__class__ exc_value = cause traceback = cause.__traceback__ - if traceback: - is_cause = False - continue + is_cause = False + continue # No cover, code is reached but coverage doesn't recognize it. break # pragma: no cover @@ -523,10 +522,10 @@ class Traceback: first_line = code[:new_line_index] if new_line_index != -1 else code if first_line.startswith("#!") and "python" in first_line.lower(): return "python" - lexer_name = ( - cls.LEXERS.get(ext) or guess_lexer_for_filename(filename, code).name - ) - return lexer_name + try: + return cls.LEXERS.get(ext) or guess_lexer_for_filename(filename, code).name + except ClassNotFound: + return "text" @group() def _render_stack(self, stack: Stack) -> RenderResult: @@ -671,7 +670,7 @@ if __name__ == "__main__": # pragma: no cover try: foo(0) except: - slfkjsldkfj # type: ignore + slfkjsldkfj # type: ignore[name-defined] except: console.print_exception(show_locals=True) diff --git a/src/pip/_vendor/rich/tree.py b/src/pip/_vendor/rich/tree.py index c5ec27da9..92561a5b6 100644 --- a/src/pip/_vendor/rich/tree.py +++ b/src/pip/_vendor/rich/tree.py @@ -136,6 +136,7 @@ class Tree(JupyterMixin): highlight=self.highlight, height=None, ), + pad=options.justify is not None, ) if not (depth == 0 and self.hide_root): @@ -214,9 +215,9 @@ if __name__ == "__main__": # pragma: no cover code = """\ class Segment(NamedTuple): - text: str = "" - style: Optional[Style] = None - is_control: bool = False + text: str = "" + style: Optional[Style] = None + is_control: bool = False """ syntax = Syntax(code, "python", theme="monokai", line_numbers=True) @@ -224,7 +225,7 @@ class Segment(NamedTuple): """\ ### example.md > Hello, World! -> +> > Markdown _all_ the things """ ) @@ -246,4 +247,5 @@ class Segment(NamedTuple): containers_node.add(Group("📄 [b magenta]Table", table)) console = Console() + console.print(root) diff --git a/src/pip/_vendor/urllib3/_version.py b/src/pip/_vendor/urllib3/_version.py index fa8979d73..d905b6975 100644 --- a/src/pip/_vendor/urllib3/_version.py +++ b/src/pip/_vendor/urllib3/_version.py @@ -1,2 +1,2 @@ # This file is protected via CODEOWNERS -__version__ = "1.26.8" +__version__ = "1.26.9" diff --git a/src/pip/_vendor/urllib3/connection.py b/src/pip/_vendor/urllib3/connection.py index 4d92ac6d2..7bf395bda 100644 --- a/src/pip/_vendor/urllib3/connection.py +++ b/src/pip/_vendor/urllib3/connection.py @@ -355,17 +355,15 @@ class HTTPSConnection(HTTPConnection): def connect(self): # Add certificate verification - conn = self._new_conn() + self.sock = conn = self._new_conn() hostname = self.host tls_in_tls = False if self._is_using_tunnel(): if self.tls_in_tls_required: - conn = self._connect_tls_proxy(hostname, conn) + self.sock = conn = self._connect_tls_proxy(hostname, conn) tls_in_tls = True - self.sock = conn - # Calls self._set_hostport(), so self.host is # self._tunnel_host below. self._tunnel() diff --git a/src/pip/_vendor/urllib3/poolmanager.py b/src/pip/_vendor/urllib3/poolmanager.py index 3a31a285b..ca4ec3411 100644 --- a/src/pip/_vendor/urllib3/poolmanager.py +++ b/src/pip/_vendor/urllib3/poolmanager.py @@ -34,6 +34,7 @@ SSL_KEYWORDS = ( "ca_cert_dir", "ssl_context", "key_password", + "server_hostname", ) # All known keyword arguments that could be provided to the pool manager, its diff --git a/src/pip/_vendor/urllib3/util/ssl_match_hostname.py b/src/pip/_vendor/urllib3/util/ssl_match_hostname.py index a4b4a569c..1dd950c48 100644 --- a/src/pip/_vendor/urllib3/util/ssl_match_hostname.py +++ b/src/pip/_vendor/urllib3/util/ssl_match_hostname.py @@ -112,11 +112,9 @@ def match_hostname(cert, hostname): try: # Divergence from upstream: ipaddress can't handle byte str host_ip = ipaddress.ip_address(_to_unicode(hostname)) - except ValueError: - # Not an IP address (common case) - host_ip = None - except UnicodeError: - # Divergence from upstream: Have to deal with ipaddress not taking + except (UnicodeError, ValueError): + # ValueError: Not an IP address (common case) + # UnicodeError: Divergence from upstream: Have to deal with ipaddress not taking # byte strings. addresses should be all ascii, so we consider it not # an ipaddress in this case host_ip = None @@ -124,7 +122,7 @@ def match_hostname(cert, hostname): # Divergence from upstream: Make ipaddress library optional if ipaddress is None: host_ip = None - else: + else: # Defensive raise dnsnames = [] san = cert.get("subjectAltName", ()) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 525f6d80e..7134851f6 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -12,8 +12,8 @@ requests==2.27.1 certifi==2021.10.08 chardet==4.0.0 idna==3.3 - urllib3==1.26.8 -rich==11.0.0 + urllib3==1.26.9 +rich==12.2.0 pygments==2.11.2 typing_extensions==4.0.1 resolvelib==0.8.1 diff --git a/tools/vendoring/patches/urllib3-disable-brotli.patch b/tools/vendoring/patches/urllib3-disable-brotli.patch new file mode 100644 index 000000000..1058ac479 --- /dev/null +++ b/tools/vendoring/patches/urllib3-disable-brotli.patch @@ -0,0 +1,39 @@ +diff --git a/src/pip/_vendor/urllib3/response.py b/src/pip/_vendor/urllib3/response.py +index fdb50ddb2..db259d6ce 100644 +--- a/src/pip/_vendor/urllib3/response.py ++++ b/src/pip/_vendor/urllib3/response.py +@@ -7,13 +7,7 @@ + from socket import error as SocketError + from socket import timeout as SocketTimeout + +-try: +- try: +- import brotlicffi as brotli +- except ImportError: +- import brotli +-except ImportError: +- brotli = None ++brotli = None + + from ._collections import HTTPHeaderDict + from .connection import BaseSSLError, HTTPException +diff --git a/src/pip/_vendor/urllib3/util/request.py b/src/pip/_vendor/urllib3/util/request.py +index b574b081e..330766ef4 100644 +--- a/src/pip/_vendor/urllib3/util/request.py ++++ b/src/pip/_vendor/urllib3/util/request.py +@@ -13,15 +13,6 @@ + SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"]) + + ACCEPT_ENCODING = "gzip,deflate" +-try: +- try: +- import brotlicffi as _unused_module_brotli # noqa: F401 +- except ImportError: +- import brotli as _unused_module_brotli # noqa: F401 +-except ImportError: +- pass +-else: +- ACCEPT_ENCODING += ",br" + + _FAILEDTELL = object() + diff --git a/tools/vendoring/patches/urllib3.patch b/tools/vendoring/patches/urllib3.patch index 747b81e1d..3ca7226fa 100644 --- a/tools/vendoring/patches/urllib3.patch +++ b/tools/vendoring/patches/urllib3.patch @@ -27,37 +27,3 @@ index c43146279..4cded53f6 100644 + pyopenssl.inject_into_urllib3() except ImportError: pass - -diff --git a/src/pip/_vendor/urllib3/response.py b/src/pip/_vendor/urllib3/response.py -index 38693f4fc..776e49dd2 100644 ---- a/src/pip/_vendor/urllib3/response.py -+++ b/src/pip/_vendor/urllib3/response.py -@@ -7,10 +7,7 @@ from contextlib import contextmanager - from socket import error as SocketError - from socket import timeout as SocketTimeout - --try: -- import brotli --except ImportError: -- brotli = None -+brotli = None - - from ._collections import HTTPHeaderDict - from .connection import BaseSSLError, HTTPException -diff --git a/src/pip/_vendor/urllib3/util/request.py b/src/pip/_vendor/urllib3/util/request.py -index 25103383e..330766ef4 100644 ---- a/src/pip/_vendor/urllib3/util/request.py -+++ b/src/pip/_vendor/urllib3/util/request.py -@@ -13,12 +13,6 @@ SKIP_HEADER = "@@@SKIP_HEADER@@@" - SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"]) - - ACCEPT_ENCODING = "gzip,deflate" --try: -- import brotli as _unused_module_brotli # noqa: F401 --except ImportError: -- pass --else: -- ACCEPT_ENCODING += ",br" - - _FAILEDTELL = object() - |
