Cleaned up the directories

This commit is contained in:
ComputerTech312 2024-02-19 15:34:25 +01:00
parent f708506d68
commit a683fcffea
1340 changed files with 554582 additions and 6840 deletions

View file

@ -0,0 +1,177 @@
"""Rich text and beautiful formatting in the terminal."""
import os
from typing import IO, TYPE_CHECKING, Any, Callable, Optional, Union
from ._extension import load_ipython_extension # noqa: F401
__all__ = ["get_console", "reconfigure", "print", "inspect", "print_json"]
if TYPE_CHECKING:
from .console import Console
# Global console used by alternative print
_console: Optional["Console"] = None
try:
_IMPORT_CWD = os.path.abspath(os.getcwd())
except FileNotFoundError:
# Can happen if the cwd has been deleted
_IMPORT_CWD = ""
def get_console() -> "Console":
"""Get a global :class:`~rich.console.Console` instance. This function is used when Rich requires a Console,
and hasn't been explicitly given one.
Returns:
Console: A console instance.
"""
global _console
if _console is None:
from .console import Console
_console = Console()
return _console
def reconfigure(*args: Any, **kwargs: Any) -> None:
"""Reconfigures the global console by replacing it with another.
Args:
*args (Any): Positional arguments for the replacement :class:`~rich.console.Console`.
**kwargs (Any): Keyword arguments for the replacement :class:`~rich.console.Console`.
"""
from pip._vendor.rich.console import Console
new_console = Console(*args, **kwargs)
_console = get_console()
_console.__dict__ = new_console.__dict__
def print(
*objects: Any,
sep: str = " ",
end: str = "\n",
file: Optional[IO[str]] = None,
flush: bool = False,
) -> None:
r"""Print object(s) supplied via positional arguments.
This function has an identical signature to the built-in print.
For more advanced features, see the :class:`~rich.console.Console` class.
Args:
sep (str, optional): Separator between printed objects. Defaults to " ".
end (str, optional): Character to write at end of output. Defaults to "\\n".
file (IO[str], optional): File to write to, or None for stdout. Defaults to None.
flush (bool, optional): Has no effect as Rich always flushes output. Defaults to False.
"""
from .console import Console
write_console = get_console() if file is None else Console(file=file)
return write_console.print(*objects, sep=sep, end=end)
def print_json(
json: Optional[str] = None,
*,
data: Any = None,
indent: Union[None, int, str] = 2,
highlight: bool = True,
skip_keys: bool = False,
ensure_ascii: bool = False,
check_circular: bool = True,
allow_nan: bool = True,
default: Optional[Callable[[Any], Any]] = None,
sort_keys: bool = False,
) -> None:
"""Pretty prints JSON. Output will be valid JSON.
Args:
json (str): A string containing JSON.
data (Any): If json is not supplied, then encode this data.
indent (int, optional): Number of spaces to indent. Defaults to 2.
highlight (bool, optional): Enable highlighting of output: Defaults to True.
skip_keys (bool, optional): Skip keys not of a basic type. Defaults to False.
ensure_ascii (bool, optional): Escape all non-ascii characters. Defaults to False.
check_circular (bool, optional): Check for circular references. Defaults to True.
allow_nan (bool, optional): Allow NaN and Infinity values. Defaults to True.
default (Callable, optional): A callable that converts values that can not be encoded
in to something that can be JSON encoded. Defaults to None.
sort_keys (bool, optional): Sort dictionary keys. Defaults to False.
"""
get_console().print_json(
json,
data=data,
indent=indent,
highlight=highlight,
skip_keys=skip_keys,
ensure_ascii=ensure_ascii,
check_circular=check_circular,
allow_nan=allow_nan,
default=default,
sort_keys=sort_keys,
)
def inspect(
obj: Any,
*,
console: Optional["Console"] = None,
title: Optional[str] = None,
help: bool = False,
methods: bool = False,
docs: bool = True,
private: bool = False,
dunder: bool = False,
sort: bool = True,
all: bool = False,
value: bool = True,
) -> None:
"""Inspect any Python object.
* inspect(<OBJECT>) to see summarized info.
* inspect(<OBJECT>, methods=True) to see methods.
* inspect(<OBJECT>, help=True) to see full (non-abbreviated) help.
* inspect(<OBJECT>, private=True) to see private attributes (single underscore).
* inspect(<OBJECT>, dunder=True) to see attributes beginning with double underscore.
* inspect(<OBJECT>, all=True) to see all attributes.
Args:
obj (Any): An object to inspect.
title (str, optional): Title to display over inspect result, or None use type. Defaults to None.
help (bool, optional): Show full help text rather than just first paragraph. Defaults to False.
methods (bool, optional): Enable inspection of callables. Defaults to False.
docs (bool, optional): Also render doc strings. Defaults to True.
private (bool, optional): Show private attributes (beginning with underscore). Defaults to False.
dunder (bool, optional): Show attributes starting with double underscore. Defaults to False.
sort (bool, optional): Sort attributes alphabetically. Defaults to True.
all (bool, optional): Show all attributes. Defaults to False.
value (bool, optional): Pretty print value. Defaults to True.
"""
_console = console or get_console()
from pip._vendor.rich._inspect import Inspect
# Special case for inspect(inspect)
is_inspect = obj is inspect
_inspect = Inspect(
obj,
title=title,
help=is_inspect or help,
methods=is_inspect or methods,
docs=is_inspect or docs,
private=private,
dunder=dunder,
sort=sort,
all=all,
value=value,
)
_console.print(_inspect)
if __name__ == "__main__": # pragma: no cover
print("Hello, **World**")

View file

@ -0,0 +1,274 @@
import colorsys
import io
from time import process_time
from pip._vendor.rich import box
from pip._vendor.rich.color import Color
from pip._vendor.rich.console import Console, ConsoleOptions, Group, RenderableType, RenderResult
from pip._vendor.rich.markdown import Markdown
from pip._vendor.rich.measure import Measurement
from pip._vendor.rich.pretty import Pretty
from pip._vendor.rich.segment import Segment
from pip._vendor.rich.style import Style
from pip._vendor.rich.syntax import Syntax
from pip._vendor.rich.table import Table
from pip._vendor.rich.text import Text
class ColorBox:
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
for y in range(0, 5):
for x in range(options.max_width):
h = x / options.max_width
l = 0.1 + ((y / 5) * 0.7)
r1, g1, b1 = colorsys.hls_to_rgb(h, l, 1.0)
r2, g2, b2 = colorsys.hls_to_rgb(h, l + 0.7 / 10, 1.0)
bgcolor = Color.from_rgb(r1 * 255, g1 * 255, b1 * 255)
color = Color.from_rgb(r2 * 255, g2 * 255, b2 * 255)
yield Segment("", Style(color=color, bgcolor=bgcolor))
yield Segment.line()
def __rich_measure__(
self, console: "Console", options: ConsoleOptions
) -> Measurement:
return Measurement(1, options.max_width)
def make_test_card() -> Table:
"""Get a renderable that demonstrates a number of features."""
table = Table.grid(padding=1, pad_edge=True)
table.title = "Rich features"
table.add_column("Feature", no_wrap=True, justify="center", style="bold red")
table.add_column("Demonstration")
color_table = Table(
box=None,
expand=False,
show_header=False,
show_edge=False,
pad_edge=False,
)
color_table.add_row(
(
"✓ [bold green]4-bit color[/]\n"
"✓ [bold blue]8-bit color[/]\n"
"✓ [bold magenta]Truecolor (16.7 million)[/]\n"
"✓ [bold yellow]Dumb terminals[/]\n"
"✓ [bold cyan]Automatic color conversion"
),
ColorBox(),
)
table.add_row("Colors", color_table)
table.add_row(
"Styles",
"All ansi styles: [bold]bold[/], [dim]dim[/], [italic]italic[/italic], [underline]underline[/], [strike]strikethrough[/], [reverse]reverse[/], and even [blink]blink[/].",
)
lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque in metus sed sapien ultricies pretium a at justo. Maecenas luctus velit et auctor maximus."
lorem_table = Table.grid(padding=1, collapse_padding=True)
lorem_table.pad_edge = False
lorem_table.add_row(
Text(lorem, justify="left", style="green"),
Text(lorem, justify="center", style="yellow"),
Text(lorem, justify="right", style="blue"),
Text(lorem, justify="full", style="red"),
)
table.add_row(
"Text",
Group(
Text.from_markup(
"""Word wrap text. Justify [green]left[/], [yellow]center[/], [blue]right[/] or [red]full[/].\n"""
),
lorem_table,
),
)
def comparison(renderable1: RenderableType, renderable2: RenderableType) -> Table:
table = Table(show_header=False, pad_edge=False, box=None, expand=True)
table.add_column("1", ratio=1)
table.add_column("2", ratio=1)
table.add_row(renderable1, renderable2)
return table
table.add_row(
"Asian\nlanguage\nsupport",
":flag_for_china: 该库支持中文,日文和韩文文本!\n:flag_for_japan: ライブラリは中国語、日本語、韓国語のテキストをサポートしています\n:flag_for_south_korea: 이 라이브러리는 중국어, 일본어 및 한국어 텍스트를 지원합니다",
)
markup_example = (
"[bold magenta]Rich[/] supports a simple [i]bbcode[/i]-like [b]markup[/b] for [yellow]color[/], [underline]style[/], and emoji! "
":+1: :apple: :ant: :bear: :baguette_bread: :bus: "
)
table.add_row("Markup", markup_example)
example_table = Table(
show_edge=False,
show_header=True,
expand=False,
row_styles=["none", "dim"],
box=box.SIMPLE,
)
example_table.add_column("[green]Date", style="green", no_wrap=True)
example_table.add_column("[blue]Title", style="blue")
example_table.add_column(
"[cyan]Production Budget",
style="cyan",
justify="right",
no_wrap=True,
)
example_table.add_column(
"[magenta]Box Office",
style="magenta",
justify="right",
no_wrap=True,
)
example_table.add_row(
"Dec 20, 2019",
"Star Wars: The Rise of Skywalker",
"$275,000,000",
"$375,126,118",
)
example_table.add_row(
"May 25, 2018",
"[b]Solo[/]: A Star Wars Story",
"$275,000,000",
"$393,151,347",
)
example_table.add_row(
"Dec 15, 2017",
"Star Wars Ep. VIII: The Last Jedi",
"$262,000,000",
"[bold]$1,332,539,889[/bold]",
)
example_table.add_row(
"May 19, 1999",
"Star Wars Ep. [b]I[/b]: [i]The phantom Menace",
"$115,000,000",
"$1,027,044,677",
)
table.add_row("Tables", example_table)
code = '''\
def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
"""Iterate and generate a tuple with a flag for last value."""
iter_values = iter(values)
try:
previous_value = next(iter_values)
except StopIteration:
return
for value in iter_values:
yield False, previous_value
previous_value = value
yield True, previous_value'''
pretty_data = {
"foo": [
3.1427,
(
"Paul Atreides",
"Vladimir Harkonnen",
"Thufir Hawat",
),
],
"atomic": (False, True, None),
}
table.add_row(
"Syntax\nhighlighting\n&\npretty\nprinting",
comparison(
Syntax(code, "python3", line_numbers=True, indent_guides=True),
Pretty(pretty_data, indent_guides=True),
),
)
markdown_example = """\
# Markdown
Supports much of the *markdown* __syntax__!
- Headers
- Basic formatting: **bold**, *italic*, `code`
- Block quotes
- Lists, and more...
"""
table.add_row(
"Markdown", comparison("[cyan]" + markdown_example, Markdown(markdown_example))
)
table.add_row(
"+more!",
"""Progress bars, columns, styled logging handler, tracebacks, etc...""",
)
return table
if __name__ == "__main__": # pragma: no cover
console = Console(
file=io.StringIO(),
force_terminal=True,
)
test_card = make_test_card()
# Print once to warm cache
start = process_time()
console.print(test_card)
pre_cache_taken = round((process_time() - start) * 1000.0, 1)
console.file = io.StringIO()
start = process_time()
console.print(test_card)
taken = round((process_time() - start) * 1000.0, 1)
c = Console(record=True)
c.print(test_card)
print(f"rendered in {pre_cache_taken}ms (cold cache)")
print(f"rendered in {taken}ms (warm cache)")
from pip._vendor.rich.panel import Panel
console = Console()
sponsor_message = Table.grid(padding=1)
sponsor_message.add_column(style="green", justify="right")
sponsor_message.add_column(no_wrap=True)
sponsor_message.add_row(
"Textualize",
"[u blue link=https://github.com/textualize]https://github.com/textualize",
)
sponsor_message.add_row(
"Twitter",
"[u blue link=https://twitter.com/willmcgugan]https://twitter.com/willmcgugan",
)
intro_message = Text.from_markup(
"""\
We hope you enjoy using Rich!
Rich is maintained with [red]:heart:[/] by [link=https://www.textualize.io]Textualize.io[/]
- Will McGugan"""
)
message = Table.grid(padding=2)
message.add_column()
message.add_column(no_wrap=True)
message.add_row(intro_message, sponsor_message)
console.print(
Panel.fit(
message,
box=box.ROUNDED,
padding=(1, 2),
title="[b red]Thanks for trying out Rich!",
border_style="bright_blue",
),
justify="center",
)

View file

@ -0,0 +1,451 @@
# Auto generated by make_terminal_widths.py
CELL_WIDTHS = [
(0, 0, 0),
(1, 31, -1),
(127, 159, -1),
(768, 879, 0),
(1155, 1161, 0),
(1425, 1469, 0),
(1471, 1471, 0),
(1473, 1474, 0),
(1476, 1477, 0),
(1479, 1479, 0),
(1552, 1562, 0),
(1611, 1631, 0),
(1648, 1648, 0),
(1750, 1756, 0),
(1759, 1764, 0),
(1767, 1768, 0),
(1770, 1773, 0),
(1809, 1809, 0),
(1840, 1866, 0),
(1958, 1968, 0),
(2027, 2035, 0),
(2045, 2045, 0),
(2070, 2073, 0),
(2075, 2083, 0),
(2085, 2087, 0),
(2089, 2093, 0),
(2137, 2139, 0),
(2259, 2273, 0),
(2275, 2306, 0),
(2362, 2362, 0),
(2364, 2364, 0),
(2369, 2376, 0),
(2381, 2381, 0),
(2385, 2391, 0),
(2402, 2403, 0),
(2433, 2433, 0),
(2492, 2492, 0),
(2497, 2500, 0),
(2509, 2509, 0),
(2530, 2531, 0),
(2558, 2558, 0),
(2561, 2562, 0),
(2620, 2620, 0),
(2625, 2626, 0),
(2631, 2632, 0),
(2635, 2637, 0),
(2641, 2641, 0),
(2672, 2673, 0),
(2677, 2677, 0),
(2689, 2690, 0),
(2748, 2748, 0),
(2753, 2757, 0),
(2759, 2760, 0),
(2765, 2765, 0),
(2786, 2787, 0),
(2810, 2815, 0),
(2817, 2817, 0),
(2876, 2876, 0),
(2879, 2879, 0),
(2881, 2884, 0),
(2893, 2893, 0),
(2901, 2902, 0),
(2914, 2915, 0),
(2946, 2946, 0),
(3008, 3008, 0),
(3021, 3021, 0),
(3072, 3072, 0),
(3076, 3076, 0),
(3134, 3136, 0),
(3142, 3144, 0),
(3146, 3149, 0),
(3157, 3158, 0),
(3170, 3171, 0),
(3201, 3201, 0),
(3260, 3260, 0),
(3263, 3263, 0),
(3270, 3270, 0),
(3276, 3277, 0),
(3298, 3299, 0),
(3328, 3329, 0),
(3387, 3388, 0),
(3393, 3396, 0),
(3405, 3405, 0),
(3426, 3427, 0),
(3457, 3457, 0),
(3530, 3530, 0),
(3538, 3540, 0),
(3542, 3542, 0),
(3633, 3633, 0),
(3636, 3642, 0),
(3655, 3662, 0),
(3761, 3761, 0),
(3764, 3772, 0),
(3784, 3789, 0),
(3864, 3865, 0),
(3893, 3893, 0),
(3895, 3895, 0),
(3897, 3897, 0),
(3953, 3966, 0),
(3968, 3972, 0),
(3974, 3975, 0),
(3981, 3991, 0),
(3993, 4028, 0),
(4038, 4038, 0),
(4141, 4144, 0),
(4146, 4151, 0),
(4153, 4154, 0),
(4157, 4158, 0),
(4184, 4185, 0),
(4190, 4192, 0),
(4209, 4212, 0),
(4226, 4226, 0),
(4229, 4230, 0),
(4237, 4237, 0),
(4253, 4253, 0),
(4352, 4447, 2),
(4957, 4959, 0),
(5906, 5908, 0),
(5938, 5940, 0),
(5970, 5971, 0),
(6002, 6003, 0),
(6068, 6069, 0),
(6071, 6077, 0),
(6086, 6086, 0),
(6089, 6099, 0),
(6109, 6109, 0),
(6155, 6157, 0),
(6277, 6278, 0),
(6313, 6313, 0),
(6432, 6434, 0),
(6439, 6440, 0),
(6450, 6450, 0),
(6457, 6459, 0),
(6679, 6680, 0),
(6683, 6683, 0),
(6742, 6742, 0),
(6744, 6750, 0),
(6752, 6752, 0),
(6754, 6754, 0),
(6757, 6764, 0),
(6771, 6780, 0),
(6783, 6783, 0),
(6832, 6848, 0),
(6912, 6915, 0),
(6964, 6964, 0),
(6966, 6970, 0),
(6972, 6972, 0),
(6978, 6978, 0),
(7019, 7027, 0),
(7040, 7041, 0),
(7074, 7077, 0),
(7080, 7081, 0),
(7083, 7085, 0),
(7142, 7142, 0),
(7144, 7145, 0),
(7149, 7149, 0),
(7151, 7153, 0),
(7212, 7219, 0),
(7222, 7223, 0),
(7376, 7378, 0),
(7380, 7392, 0),
(7394, 7400, 0),
(7405, 7405, 0),
(7412, 7412, 0),
(7416, 7417, 0),
(7616, 7673, 0),
(7675, 7679, 0),
(8203, 8207, 0),
(8232, 8238, 0),
(8288, 8291, 0),
(8400, 8432, 0),
(8986, 8987, 2),
(9001, 9002, 2),
(9193, 9196, 2),
(9200, 9200, 2),
(9203, 9203, 2),
(9725, 9726, 2),
(9748, 9749, 2),
(9800, 9811, 2),
(9855, 9855, 2),
(9875, 9875, 2),
(9889, 9889, 2),
(9898, 9899, 2),
(9917, 9918, 2),
(9924, 9925, 2),
(9934, 9934, 2),
(9940, 9940, 2),
(9962, 9962, 2),
(9970, 9971, 2),
(9973, 9973, 2),
(9978, 9978, 2),
(9981, 9981, 2),
(9989, 9989, 2),
(9994, 9995, 2),
(10024, 10024, 2),
(10060, 10060, 2),
(10062, 10062, 2),
(10067, 10069, 2),
(10071, 10071, 2),
(10133, 10135, 2),
(10160, 10160, 2),
(10175, 10175, 2),
(11035, 11036, 2),
(11088, 11088, 2),
(11093, 11093, 2),
(11503, 11505, 0),
(11647, 11647, 0),
(11744, 11775, 0),
(11904, 11929, 2),
(11931, 12019, 2),
(12032, 12245, 2),
(12272, 12283, 2),
(12288, 12329, 2),
(12330, 12333, 0),
(12334, 12350, 2),
(12353, 12438, 2),
(12441, 12442, 0),
(12443, 12543, 2),
(12549, 12591, 2),
(12593, 12686, 2),
(12688, 12771, 2),
(12784, 12830, 2),
(12832, 12871, 2),
(12880, 19903, 2),
(19968, 42124, 2),
(42128, 42182, 2),
(42607, 42610, 0),
(42612, 42621, 0),
(42654, 42655, 0),
(42736, 42737, 0),
(43010, 43010, 0),
(43014, 43014, 0),
(43019, 43019, 0),
(43045, 43046, 0),
(43052, 43052, 0),
(43204, 43205, 0),
(43232, 43249, 0),
(43263, 43263, 0),
(43302, 43309, 0),
(43335, 43345, 0),
(43360, 43388, 2),
(43392, 43394, 0),
(43443, 43443, 0),
(43446, 43449, 0),
(43452, 43453, 0),
(43493, 43493, 0),
(43561, 43566, 0),
(43569, 43570, 0),
(43573, 43574, 0),
(43587, 43587, 0),
(43596, 43596, 0),
(43644, 43644, 0),
(43696, 43696, 0),
(43698, 43700, 0),
(43703, 43704, 0),
(43710, 43711, 0),
(43713, 43713, 0),
(43756, 43757, 0),
(43766, 43766, 0),
(44005, 44005, 0),
(44008, 44008, 0),
(44013, 44013, 0),
(44032, 55203, 2),
(63744, 64255, 2),
(64286, 64286, 0),
(65024, 65039, 0),
(65040, 65049, 2),
(65056, 65071, 0),
(65072, 65106, 2),
(65108, 65126, 2),
(65128, 65131, 2),
(65281, 65376, 2),
(65504, 65510, 2),
(66045, 66045, 0),
(66272, 66272, 0),
(66422, 66426, 0),
(68097, 68099, 0),
(68101, 68102, 0),
(68108, 68111, 0),
(68152, 68154, 0),
(68159, 68159, 0),
(68325, 68326, 0),
(68900, 68903, 0),
(69291, 69292, 0),
(69446, 69456, 0),
(69633, 69633, 0),
(69688, 69702, 0),
(69759, 69761, 0),
(69811, 69814, 0),
(69817, 69818, 0),
(69888, 69890, 0),
(69927, 69931, 0),
(69933, 69940, 0),
(70003, 70003, 0),
(70016, 70017, 0),
(70070, 70078, 0),
(70089, 70092, 0),
(70095, 70095, 0),
(70191, 70193, 0),
(70196, 70196, 0),
(70198, 70199, 0),
(70206, 70206, 0),
(70367, 70367, 0),
(70371, 70378, 0),
(70400, 70401, 0),
(70459, 70460, 0),
(70464, 70464, 0),
(70502, 70508, 0),
(70512, 70516, 0),
(70712, 70719, 0),
(70722, 70724, 0),
(70726, 70726, 0),
(70750, 70750, 0),
(70835, 70840, 0),
(70842, 70842, 0),
(70847, 70848, 0),
(70850, 70851, 0),
(71090, 71093, 0),
(71100, 71101, 0),
(71103, 71104, 0),
(71132, 71133, 0),
(71219, 71226, 0),
(71229, 71229, 0),
(71231, 71232, 0),
(71339, 71339, 0),
(71341, 71341, 0),
(71344, 71349, 0),
(71351, 71351, 0),
(71453, 71455, 0),
(71458, 71461, 0),
(71463, 71467, 0),
(71727, 71735, 0),
(71737, 71738, 0),
(71995, 71996, 0),
(71998, 71998, 0),
(72003, 72003, 0),
(72148, 72151, 0),
(72154, 72155, 0),
(72160, 72160, 0),
(72193, 72202, 0),
(72243, 72248, 0),
(72251, 72254, 0),
(72263, 72263, 0),
(72273, 72278, 0),
(72281, 72283, 0),
(72330, 72342, 0),
(72344, 72345, 0),
(72752, 72758, 0),
(72760, 72765, 0),
(72767, 72767, 0),
(72850, 72871, 0),
(72874, 72880, 0),
(72882, 72883, 0),
(72885, 72886, 0),
(73009, 73014, 0),
(73018, 73018, 0),
(73020, 73021, 0),
(73023, 73029, 0),
(73031, 73031, 0),
(73104, 73105, 0),
(73109, 73109, 0),
(73111, 73111, 0),
(73459, 73460, 0),
(92912, 92916, 0),
(92976, 92982, 0),
(94031, 94031, 0),
(94095, 94098, 0),
(94176, 94179, 2),
(94180, 94180, 0),
(94192, 94193, 2),
(94208, 100343, 2),
(100352, 101589, 2),
(101632, 101640, 2),
(110592, 110878, 2),
(110928, 110930, 2),
(110948, 110951, 2),
(110960, 111355, 2),
(113821, 113822, 0),
(119143, 119145, 0),
(119163, 119170, 0),
(119173, 119179, 0),
(119210, 119213, 0),
(119362, 119364, 0),
(121344, 121398, 0),
(121403, 121452, 0),
(121461, 121461, 0),
(121476, 121476, 0),
(121499, 121503, 0),
(121505, 121519, 0),
(122880, 122886, 0),
(122888, 122904, 0),
(122907, 122913, 0),
(122915, 122916, 0),
(122918, 122922, 0),
(123184, 123190, 0),
(123628, 123631, 0),
(125136, 125142, 0),
(125252, 125258, 0),
(126980, 126980, 2),
(127183, 127183, 2),
(127374, 127374, 2),
(127377, 127386, 2),
(127488, 127490, 2),
(127504, 127547, 2),
(127552, 127560, 2),
(127568, 127569, 2),
(127584, 127589, 2),
(127744, 127776, 2),
(127789, 127797, 2),
(127799, 127868, 2),
(127870, 127891, 2),
(127904, 127946, 2),
(127951, 127955, 2),
(127968, 127984, 2),
(127988, 127988, 2),
(127992, 128062, 2),
(128064, 128064, 2),
(128066, 128252, 2),
(128255, 128317, 2),
(128331, 128334, 2),
(128336, 128359, 2),
(128378, 128378, 2),
(128405, 128406, 2),
(128420, 128420, 2),
(128507, 128591, 2),
(128640, 128709, 2),
(128716, 128716, 2),
(128720, 128722, 2),
(128725, 128727, 2),
(128747, 128748, 2),
(128756, 128764, 2),
(128992, 129003, 2),
(129292, 129338, 2),
(129340, 129349, 2),
(129351, 129400, 2),
(129402, 129483, 2),
(129485, 129535, 2),
(129648, 129652, 2),
(129656, 129658, 2),
(129664, 129670, 2),
(129680, 129704, 2),
(129712, 129718, 2),
(129728, 129730, 2),
(129744, 129750, 2),
(131072, 196605, 2),
(196608, 262141, 2),
(917760, 917999, 0),
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
from typing import Callable, Match, Optional
import re
from ._emoji_codes import EMOJI
_ReStringMatch = Match[str] # regex match object
_ReSubCallable = Callable[[_ReStringMatch], str] # Callable invoked by re.sub
_EmojiSubMethod = Callable[[_ReSubCallable, str], str] # Sub method of a compiled re
def _emoji_replace(
text: str,
default_variant: Optional[str] = None,
_emoji_sub: _EmojiSubMethod = re.compile(r"(:(\S*?)(?:(?:\-)(emoji|text))?:)").sub,
) -> str:
"""Replace emoji code in text."""
get_emoji = EMOJI.__getitem__
variants = {"text": "\uFE0E", "emoji": "\uFE0F"}
get_variant = variants.get
default_variant_code = variants.get(default_variant, "") if default_variant else ""
def do_replace(match: Match[str]) -> str:
emoji_code, emoji_name, variant = match.groups()
try:
return get_emoji(emoji_name.lower()) + get_variant(
variant, default_variant_code
)
except KeyError:
return emoji_code
return _emoji_sub(do_replace, text)

View file

@ -0,0 +1,76 @@
CONSOLE_HTML_FORMAT = """\
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<style>
{stylesheet}
body {{
color: {foreground};
background-color: {background};
}}
</style>
</head>
<html>
<body>
<pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><code>{code}</code></pre>
</body>
</html>
"""
CONSOLE_SVG_FORMAT = """\
<svg class="rich-terminal" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<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;
}}
.{unique_id}-matrix {{
font-family: Fira Code, monospace;
font-size: {char_height}px;
line-height: {line_height}px;
font-variant-east-asian: full-width;
}}
.{unique_id}-title {{
font-size: 18px;
font-weight: bold;
font-family: arial;
}}
{styles}
</style>
<defs>
<clipPath id="{unique_id}-clip-terminal">
<rect x="0" y="0" width="{terminal_width}" height="{terminal_height}" />
</clipPath>
{lines}
</defs>
{chrome}
<g transform="translate({terminal_x}, {terminal_y})" clip-path="url(#{unique_id}-clip-terminal)">
{backgrounds}
<g class="{unique_id}-matrix">
{matrix}
</g>
</g>
</svg>
"""
_SVG_FONT_FAMILY = "Rich Fira Code"
_SVG_CLASSES_PREFIX = "rich-svg"

View file

@ -0,0 +1,10 @@
from typing import Any
def load_ipython_extension(ip: Any) -> None: # pragma: no cover
# prevent circular import
from pip._vendor.rich.pretty import install
from pip._vendor.rich.traceback import install as tr_install
install()
tr_install()

View file

@ -0,0 +1,24 @@
from __future__ import annotations
from typing import IO, Callable
def get_fileno(file_like: IO[str]) -> int | None:
"""Get fileno() from a file, accounting for poorly implemented file-like objects.
Args:
file_like (IO): A file-like object.
Returns:
int | None: The result of fileno if available, or None if operation failed.
"""
fileno: Callable[[], int] | None = getattr(file_like, "fileno", None)
if fileno is not None:
try:
return fileno()
except Exception:
# `fileno` is documented as potentially raising a OSError
# Alas, from the issues, there are so many poorly implemented file-like objects,
# that `fileno()` can raise just about anything.
return None
return None

View file

@ -0,0 +1,270 @@
from __future__ import absolute_import
import inspect
from inspect import cleandoc, getdoc, getfile, isclass, ismodule, signature
from typing import Any, Collection, Iterable, Optional, Tuple, Type, Union
from .console import Group, RenderableType
from .control import escape_control_codes
from .highlighter import ReprHighlighter
from .jupyter import JupyterMixin
from .panel import Panel
from .pretty import Pretty
from .table import Table
from .text import Text, TextType
def _first_paragraph(doc: str) -> str:
"""Get the first paragraph from a docstring."""
paragraph, _, _ = doc.partition("\n\n")
return paragraph
class Inspect(JupyterMixin):
"""A renderable to inspect any Python Object.
Args:
obj (Any): An object to inspect.
title (str, optional): Title to display over inspect result, or None use type. Defaults to None.
help (bool, optional): Show full help text rather than just first paragraph. Defaults to False.
methods (bool, optional): Enable inspection of callables. Defaults to False.
docs (bool, optional): Also render doc strings. Defaults to True.
private (bool, optional): Show private attributes (beginning with underscore). Defaults to False.
dunder (bool, optional): Show attributes starting with double underscore. Defaults to False.
sort (bool, optional): Sort attributes alphabetically. Defaults to True.
all (bool, optional): Show all attributes. Defaults to False.
value (bool, optional): Pretty print value of object. Defaults to True.
"""
def __init__(
self,
obj: Any,
*,
title: Optional[TextType] = None,
help: bool = False,
methods: bool = False,
docs: bool = True,
private: bool = False,
dunder: bool = False,
sort: bool = True,
all: bool = True,
value: bool = True,
) -> None:
self.highlighter = ReprHighlighter()
self.obj = obj
self.title = title or self._make_title(obj)
if all:
methods = private = dunder = True
self.help = help
self.methods = methods
self.docs = docs or help
self.private = private or dunder
self.dunder = dunder
self.sort = sort
self.value = value
def _make_title(self, obj: Any) -> Text:
"""Make a default title."""
title_str = (
str(obj)
if (isclass(obj) or callable(obj) or ismodule(obj))
else str(type(obj))
)
title_text = self.highlighter(title_str)
return title_text
def __rich__(self) -> Panel:
return Panel.fit(
Group(*self._render()),
title=self.title,
border_style="scope.border",
padding=(0, 1),
)
def _get_signature(self, name: str, obj: Any) -> Optional[Text]:
"""Get a signature for a callable."""
try:
_signature = str(signature(obj)) + ":"
except ValueError:
_signature = "(...)"
except TypeError:
return None
source_filename: Optional[str] = None
try:
source_filename = getfile(obj)
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")
if source_filename:
callable_name.stylize(f"link file://{source_filename}")
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"
elif inspect.iscoroutinefunction(obj):
prefix = "async def"
else:
prefix = "def"
qual_signature = Text.assemble(
(f"{prefix} ", f"inspect.{prefix.replace(' ', '_')}"),
(qualname, "inspect.callable"),
signature_text,
)
return qual_signature
def _render(self) -> Iterable[RenderableType]:
"""Render object."""
def sort_items(item: Tuple[str, Any]) -> Tuple[bool, str]:
key, (_error, value) = item
return (callable(value), key.strip("_").lower())
def safe_getattr(attr_name: str) -> Tuple[Any, Any]:
"""Get attribute or any exception."""
try:
return (None, getattr(obj, attr_name))
except Exception as error:
return (error, None)
obj = self.obj
keys = dir(obj)
total_items = len(keys)
if not self.dunder:
keys = [key for key in keys if not key.startswith("__")]
if not self.private:
keys = [key for key in keys if not key.startswith("_")]
not_shown_count = total_items - len(keys)
items = [(key, safe_getattr(key)) for key in keys]
if self.sort:
items.sort(key=sort_items)
items_table = Table.grid(padding=(0, 1), expand=False)
items_table.add_column(justify="right")
add_row = items_table.add_row
highlighter = self.highlighter
if callable(obj):
signature = self._get_signature("", obj)
if signature is not None:
yield signature
yield ""
if self.docs:
_doc = self._get_formatted_doc(obj)
if _doc is not None:
doc_text = Text(_doc, style="inspect.help")
doc_text = highlighter(doc_text)
yield doc_text
yield ""
if self.value and not (isclass(obj) or callable(obj) or ismodule(obj)):
yield Panel(
Pretty(obj, indent_guides=True, max_length=10, max_string=60),
border_style="inspect.value.border",
)
yield ""
for key, (error, value) in items:
key_text = Text.assemble(
(
key,
"inspect.attr.dunder" if key.startswith("__") else "inspect.attr",
),
(" =", "inspect.equals"),
)
if error is not None:
warning = key_text.copy()
warning.stylize("inspect.error")
add_row(warning, highlighter(repr(error)))
continue
if callable(value):
if not self.methods:
continue
_signature_text = self._get_signature(key, value)
if _signature_text is None:
add_row(key_text, Pretty(value, highlighter=highlighter))
else:
if self.docs:
docs = self._get_formatted_doc(value)
if docs is not None:
_signature_text.append("\n" if "\n" in docs else " ")
doc = highlighter(docs)
doc.stylize("inspect.doc")
_signature_text.append(doc)
add_row(key_text, _signature_text)
else:
add_row(key_text, Pretty(value, highlighter=highlighter))
if items_table.row_count:
yield items_table
elif not_shown_count:
yield Text.from_markup(
f"[b cyan]{not_shown_count}[/][i] attribute(s) not shown.[/i] "
f"Run [b][magenta]inspect[/]([not b]inspect[/])[/b] for options."
)
def _get_formatted_doc(self, object_: Any) -> Optional[str]:
"""
Extract the docstring of an object, process it and returns it.
The processing consists in cleaning up the doctring's indentation,
taking only its 1st paragraph if `self.help` is not True,
and escape its control codes.
Args:
object_ (Any): the object to get the docstring from.
Returns:
Optional[str]: the processed docstring, or None if no docstring was found.
"""
docs = getdoc(object_)
if docs is None:
return None
docs = cleandoc(docs).strip()
if not self.help:
docs = _first_paragraph(docs)
return escape_control_codes(docs)
def get_object_types_mro(obj: Union[object, Type[Any]]) -> Tuple[type, ...]:
"""Returns the MRO of an object's class, or of the object itself if it's a class."""
if not hasattr(obj, "__mro__"):
# N.B. we cannot use `if type(obj) is type` here because it doesn't work with
# some types of classes, such as the ones that use abc.ABCMeta.
obj = type(obj)
return getattr(obj, "__mro__", ())
def get_object_types_mro_as_strings(obj: object) -> Collection[str]:
"""
Returns the MRO of an object's class as full qualified names, or of the object itself if it's a class.
Examples:
`object_types_mro_as_strings(JSONDecoder)` will return `['json.decoder.JSONDecoder', 'builtins.object']`
"""
return [
f'{getattr(type_, "__module__", "")}.{getattr(type_, "__qualname__", "")}'
for type_ in get_object_types_mro(obj)
]
def is_object_one_of_types(
obj: object, fully_qualified_types_names: Collection[str]
) -> bool:
"""
Returns `True` if the given object's class (or the object itself, if it's a class) has one of the
fully qualified names in its MRO.
"""
for type_name in get_object_types_mro_as_strings(obj):
if type_name in fully_qualified_types_names:
return True
return False

View file

@ -0,0 +1,94 @@
from datetime import datetime
from typing import Iterable, List, Optional, TYPE_CHECKING, Union, Callable
from .text import Text, TextType
if TYPE_CHECKING:
from .console import Console, ConsoleRenderable, RenderableType
from .table import Table
FormatTimeCallable = Callable[[datetime], Text]
class LogRender:
def __init__(
self,
show_time: bool = True,
show_level: bool = False,
show_path: bool = True,
time_format: Union[str, FormatTimeCallable] = "[%x %X]",
omit_repeated_times: bool = True,
level_width: Optional[int] = 8,
) -> None:
self.show_time = show_time
self.show_level = show_level
self.show_path = show_path
self.time_format = time_format
self.omit_repeated_times = omit_repeated_times
self.level_width = level_width
self._last_time: Optional[Text] = None
def __call__(
self,
console: "Console",
renderables: Iterable["ConsoleRenderable"],
log_time: Optional[datetime] = None,
time_format: Optional[Union[str, FormatTimeCallable]] = None,
level: TextType = "",
path: Optional[str] = None,
line_no: Optional[int] = None,
link_path: Optional[str] = None,
) -> "Table":
from .containers import Renderables
from .table import Table
output = Table.grid(padding=(0, 1))
output.expand = True
if self.show_time:
output.add_column(style="log.time")
if self.show_level:
output.add_column(style="log.level", width=self.level_width)
output.add_column(ratio=1, style="log.message", overflow="fold")
if self.show_path and path:
output.add_column(style="log.path")
row: List["RenderableType"] = []
if self.show_time:
log_time = log_time or console.get_datetime()
time_format = time_format or self.time_format
if callable(time_format):
log_time_display = time_format(log_time)
else:
log_time_display = Text(log_time.strftime(time_format))
if log_time_display == self._last_time and self.omit_repeated_times:
row.append(Text(" " * len(log_time_display)))
else:
row.append(log_time_display)
self._last_time = log_time_display
if self.show_level:
row.append(level)
row.append(Renderables(renderables))
if self.show_path and path:
path_text = Text()
path_text.append(
path, style=f"link file://{link_path}" if link_path else ""
)
if line_no:
path_text.append(":")
path_text.append(
f"{line_no}",
style=f"link file://{link_path}#{line_no}" if link_path else "",
)
row.append(path_text)
output.add_row(*row)
return output
if __name__ == "__main__": # pragma: no cover
from pip._vendor.rich.console import Console
c = Console()
c.print("[on blue]Hello", justify="right")
c.log("[on blue]hello", justify="right")

View file

@ -0,0 +1,43 @@
from typing import Iterable, Tuple, TypeVar
T = TypeVar("T")
def loop_first(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
"""Iterate and generate a tuple with a flag for first value."""
iter_values = iter(values)
try:
value = next(iter_values)
except StopIteration:
return
yield True, value
for value in iter_values:
yield False, value
def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
"""Iterate and generate a tuple with a flag for last value."""
iter_values = iter(values)
try:
previous_value = next(iter_values)
except StopIteration:
return
for value in iter_values:
yield False, previous_value
previous_value = value
yield True, previous_value
def loop_first_last(values: Iterable[T]) -> Iterable[Tuple[bool, bool, T]]:
"""Iterate and generate a tuple with a flag for first and last value."""
iter_values = iter(values)
try:
previous_value = next(iter_values)
except StopIteration:
return
first = True
for value in iter_values:
yield first, False, previous_value
first = False
previous_value = value
yield first, True, previous_value

View file

@ -0,0 +1,69 @@
from types import TracebackType
from typing import IO, Iterable, Iterator, List, Optional, Type
class NullFile(IO[str]):
def close(self) -> None:
pass
def isatty(self) -> bool:
return False
def read(self, __n: int = 1) -> str:
return ""
def readable(self) -> bool:
return False
def readline(self, __limit: int = 1) -> str:
return ""
def readlines(self, __hint: int = 1) -> List[str]:
return []
def seek(self, __offset: int, __whence: int = 1) -> int:
return 0
def seekable(self) -> bool:
return False
def tell(self) -> int:
return 0
def truncate(self, __size: Optional[int] = 1) -> int:
return 0
def writable(self) -> bool:
return False
def writelines(self, __lines: Iterable[str]) -> None:
pass
def __next__(self) -> str:
return ""
def __iter__(self) -> Iterator[str]:
return iter([""])
def __enter__(self) -> IO[str]:
pass
def __exit__(
self,
__t: Optional[Type[BaseException]],
__value: Optional[BaseException],
__traceback: Optional[TracebackType],
) -> None:
pass
def write(self, text: str) -> int:
return 0
def flush(self) -> None:
pass
def fileno(self) -> int:
return -1
NULL_FILE = NullFile()

View file

@ -0,0 +1,309 @@
from .palette import Palette
# Taken from https://en.wikipedia.org/wiki/ANSI_escape_code (Windows 10 column)
WINDOWS_PALETTE = Palette(
[
(12, 12, 12),
(197, 15, 31),
(19, 161, 14),
(193, 156, 0),
(0, 55, 218),
(136, 23, 152),
(58, 150, 221),
(204, 204, 204),
(118, 118, 118),
(231, 72, 86),
(22, 198, 12),
(249, 241, 165),
(59, 120, 255),
(180, 0, 158),
(97, 214, 214),
(242, 242, 242),
]
)
# # The standard ansi colors (including bright variants)
STANDARD_PALETTE = Palette(
[
(0, 0, 0),
(170, 0, 0),
(0, 170, 0),
(170, 85, 0),
(0, 0, 170),
(170, 0, 170),
(0, 170, 170),
(170, 170, 170),
(85, 85, 85),
(255, 85, 85),
(85, 255, 85),
(255, 255, 85),
(85, 85, 255),
(255, 85, 255),
(85, 255, 255),
(255, 255, 255),
]
)
# The 256 color palette
EIGHT_BIT_PALETTE = Palette(
[
(0, 0, 0),
(128, 0, 0),
(0, 128, 0),
(128, 128, 0),
(0, 0, 128),
(128, 0, 128),
(0, 128, 128),
(192, 192, 192),
(128, 128, 128),
(255, 0, 0),
(0, 255, 0),
(255, 255, 0),
(0, 0, 255),
(255, 0, 255),
(0, 255, 255),
(255, 255, 255),
(0, 0, 0),
(0, 0, 95),
(0, 0, 135),
(0, 0, 175),
(0, 0, 215),
(0, 0, 255),
(0, 95, 0),
(0, 95, 95),
(0, 95, 135),
(0, 95, 175),
(0, 95, 215),
(0, 95, 255),
(0, 135, 0),
(0, 135, 95),
(0, 135, 135),
(0, 135, 175),
(0, 135, 215),
(0, 135, 255),
(0, 175, 0),
(0, 175, 95),
(0, 175, 135),
(0, 175, 175),
(0, 175, 215),
(0, 175, 255),
(0, 215, 0),
(0, 215, 95),
(0, 215, 135),
(0, 215, 175),
(0, 215, 215),
(0, 215, 255),
(0, 255, 0),
(0, 255, 95),
(0, 255, 135),
(0, 255, 175),
(0, 255, 215),
(0, 255, 255),
(95, 0, 0),
(95, 0, 95),
(95, 0, 135),
(95, 0, 175),
(95, 0, 215),
(95, 0, 255),
(95, 95, 0),
(95, 95, 95),
(95, 95, 135),
(95, 95, 175),
(95, 95, 215),
(95, 95, 255),
(95, 135, 0),
(95, 135, 95),
(95, 135, 135),
(95, 135, 175),
(95, 135, 215),
(95, 135, 255),
(95, 175, 0),
(95, 175, 95),
(95, 175, 135),
(95, 175, 175),
(95, 175, 215),
(95, 175, 255),
(95, 215, 0),
(95, 215, 95),
(95, 215, 135),
(95, 215, 175),
(95, 215, 215),
(95, 215, 255),
(95, 255, 0),
(95, 255, 95),
(95, 255, 135),
(95, 255, 175),
(95, 255, 215),
(95, 255, 255),
(135, 0, 0),
(135, 0, 95),
(135, 0, 135),
(135, 0, 175),
(135, 0, 215),
(135, 0, 255),
(135, 95, 0),
(135, 95, 95),
(135, 95, 135),
(135, 95, 175),
(135, 95, 215),
(135, 95, 255),
(135, 135, 0),
(135, 135, 95),
(135, 135, 135),
(135, 135, 175),
(135, 135, 215),
(135, 135, 255),
(135, 175, 0),
(135, 175, 95),
(135, 175, 135),
(135, 175, 175),
(135, 175, 215),
(135, 175, 255),
(135, 215, 0),
(135, 215, 95),
(135, 215, 135),
(135, 215, 175),
(135, 215, 215),
(135, 215, 255),
(135, 255, 0),
(135, 255, 95),
(135, 255, 135),
(135, 255, 175),
(135, 255, 215),
(135, 255, 255),
(175, 0, 0),
(175, 0, 95),
(175, 0, 135),
(175, 0, 175),
(175, 0, 215),
(175, 0, 255),
(175, 95, 0),
(175, 95, 95),
(175, 95, 135),
(175, 95, 175),
(175, 95, 215),
(175, 95, 255),
(175, 135, 0),
(175, 135, 95),
(175, 135, 135),
(175, 135, 175),
(175, 135, 215),
(175, 135, 255),
(175, 175, 0),
(175, 175, 95),
(175, 175, 135),
(175, 175, 175),
(175, 175, 215),
(175, 175, 255),
(175, 215, 0),
(175, 215, 95),
(175, 215, 135),
(175, 215, 175),
(175, 215, 215),
(175, 215, 255),
(175, 255, 0),
(175, 255, 95),
(175, 255, 135),
(175, 255, 175),
(175, 255, 215),
(175, 255, 255),
(215, 0, 0),
(215, 0, 95),
(215, 0, 135),
(215, 0, 175),
(215, 0, 215),
(215, 0, 255),
(215, 95, 0),
(215, 95, 95),
(215, 95, 135),
(215, 95, 175),
(215, 95, 215),
(215, 95, 255),
(215, 135, 0),
(215, 135, 95),
(215, 135, 135),
(215, 135, 175),
(215, 135, 215),
(215, 135, 255),
(215, 175, 0),
(215, 175, 95),
(215, 175, 135),
(215, 175, 175),
(215, 175, 215),
(215, 175, 255),
(215, 215, 0),
(215, 215, 95),
(215, 215, 135),
(215, 215, 175),
(215, 215, 215),
(215, 215, 255),
(215, 255, 0),
(215, 255, 95),
(215, 255, 135),
(215, 255, 175),
(215, 255, 215),
(215, 255, 255),
(255, 0, 0),
(255, 0, 95),
(255, 0, 135),
(255, 0, 175),
(255, 0, 215),
(255, 0, 255),
(255, 95, 0),
(255, 95, 95),
(255, 95, 135),
(255, 95, 175),
(255, 95, 215),
(255, 95, 255),
(255, 135, 0),
(255, 135, 95),
(255, 135, 135),
(255, 135, 175),
(255, 135, 215),
(255, 135, 255),
(255, 175, 0),
(255, 175, 95),
(255, 175, 135),
(255, 175, 175),
(255, 175, 215),
(255, 175, 255),
(255, 215, 0),
(255, 215, 95),
(255, 215, 135),
(255, 215, 175),
(255, 215, 215),
(255, 215, 255),
(255, 255, 0),
(255, 255, 95),
(255, 255, 135),
(255, 255, 175),
(255, 255, 215),
(255, 255, 255),
(8, 8, 8),
(18, 18, 18),
(28, 28, 28),
(38, 38, 38),
(48, 48, 48),
(58, 58, 58),
(68, 68, 68),
(78, 78, 78),
(88, 88, 88),
(98, 98, 98),
(108, 108, 108),
(118, 118, 118),
(128, 128, 128),
(138, 138, 138),
(148, 148, 148),
(158, 158, 158),
(168, 168, 168),
(178, 178, 178),
(188, 188, 188),
(198, 198, 198),
(208, 208, 208),
(218, 218, 218),
(228, 228, 228),
(238, 238, 238),
]
)

View file

@ -0,0 +1,17 @@
from typing import Optional
def pick_bool(*values: Optional[bool]) -> bool:
"""Pick the first non-none bool or return the last value.
Args:
*values (bool): Any number of boolean or None values.
Returns:
bool: First non-none boolean.
"""
assert values, "1 or more values required"
for value in values:
if value is not None:
return value
return bool(value)

View file

@ -0,0 +1,160 @@
import sys
from fractions import Fraction
from math import ceil
from typing import cast, List, Optional, Sequence
if sys.version_info >= (3, 8):
from typing import Protocol
else:
from pip._vendor.typing_extensions import Protocol # pragma: no cover
class Edge(Protocol):
"""Any object that defines an edge (such as Layout)."""
size: Optional[int] = None
ratio: int = 1
minimum_size: int = 1
def ratio_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
"""Divide total space to satisfy size, ratio, and minimum_size, constraints.
The returned list of integers should add up to total in most cases, unless it is
impossible to satisfy all the constraints. For instance, if there are two edges
with a minimum size of 20 each and `total` is 30 then the returned list will be
greater than total. In practice, this would mean that a Layout object would
clip the rows that would overflow the screen height.
Args:
total (int): Total number of characters.
edges (List[Edge]): Edges within total space.
Returns:
List[int]: Number of characters for each edge.
"""
# Size of edge or None for yet to be determined
sizes = [(edge.size or None) for edge in edges]
_Fraction = Fraction
# While any edges haven't been calculated
while None in sizes:
# Get flexible edges and index to map these back on to sizes list
flexible_edges = [
(index, edge)
for index, (size, edge) in enumerate(zip(sizes, edges))
if size is None
]
# Remaining space in total
remaining = total - sum(size or 0 for size in sizes)
if remaining <= 0:
# No room for flexible edges
return [
((edge.minimum_size or 1) if size is None else size)
for size, edge in zip(sizes, edges)
]
# Calculate number of characters in a ratio portion
portion = _Fraction(
remaining, sum((edge.ratio or 1) for _, edge in flexible_edges)
)
# If any edges will be less than their minimum, replace size with the minimum
for index, edge in flexible_edges:
if portion * edge.ratio <= edge.minimum_size:
sizes[index] = edge.minimum_size
# New fixed size will invalidate calculations, so we need to repeat the process
break
else:
# Distribute flexible space and compensate for rounding error
# Since edge sizes can only be integers we need to add the remainder
# to the following line
remainder = _Fraction(0)
for index, edge in flexible_edges:
size, remainder = divmod(portion * edge.ratio + remainder, 1)
sizes[index] = size
break
# Sizes now contains integers only
return cast(List[int], sizes)
def ratio_reduce(
total: int, ratios: List[int], maximums: List[int], values: List[int]
) -> List[int]:
"""Divide an integer total in to parts based on ratios.
Args:
total (int): The total to divide.
ratios (List[int]): A list of integer ratios.
maximums (List[int]): List of maximums values for each slot.
values (List[int]): List of values
Returns:
List[int]: A list of integers guaranteed to sum to total.
"""
ratios = [ratio if _max else 0 for ratio, _max in zip(ratios, maximums)]
total_ratio = sum(ratios)
if not total_ratio:
return values[:]
total_remaining = total
result: List[int] = []
append = result.append
for ratio, maximum, value in zip(ratios, maximums, values):
if ratio and total_ratio > 0:
distributed = min(maximum, round(ratio * total_remaining / total_ratio))
append(value - distributed)
total_remaining -= distributed
total_ratio -= ratio
else:
append(value)
return result
def ratio_distribute(
total: int, ratios: List[int], minimums: Optional[List[int]] = None
) -> List[int]:
"""Distribute an integer total in to parts based on ratios.
Args:
total (int): The total to divide.
ratios (List[int]): A list of integer ratios.
minimums (List[int]): List of minimum values for each slot.
Returns:
List[int]: A list of integers guaranteed to sum to total.
"""
if minimums:
ratios = [ratio if _min else 0 for ratio, _min in zip(ratios, minimums)]
total_ratio = sum(ratios)
assert total_ratio > 0, "Sum of ratios must be > 0"
total_remaining = total
distributed_total: List[int] = []
append = distributed_total.append
if minimums is None:
_minimums = [0] * len(ratios)
else:
_minimums = minimums
for ratio, minimum in zip(ratios, _minimums):
if total_ratio > 0:
distributed = max(minimum, ceil(ratio * total_remaining / total_ratio))
else:
distributed = total_remaining
append(distributed)
total_ratio -= ratio
total_remaining -= distributed
return distributed_total
if __name__ == "__main__":
from dataclasses import dataclass
@dataclass
class E:
size: Optional[int] = None
ratio: int = 1
minimum_size: int = 1
resolved = ratio_resolve(110, [E(None, 1, 1), E(None, 1, 1), E(None, 1, 1)])
print(sum(resolved))

View file

@ -0,0 +1,482 @@
"""
Spinners are from:
* cli-spinners:
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
"""
SPINNERS = {
"dots": {
"interval": 80,
"frames": "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏",
},
"dots2": {"interval": 80, "frames": "⣾⣽⣻⢿⡿⣟⣯⣷"},
"dots3": {
"interval": 80,
"frames": "⠋⠙⠚⠞⠖⠦⠴⠲⠳⠓",
},
"dots4": {
"interval": 80,
"frames": "⠄⠆⠇⠋⠙⠸⠰⠠⠰⠸⠙⠋⠇⠆",
},
"dots5": {
"interval": 80,
"frames": "⠋⠙⠚⠒⠂⠂⠒⠲⠴⠦⠖⠒⠐⠐⠒⠓⠋",
},
"dots6": {
"interval": 80,
"frames": "⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠴⠲⠒⠂⠂⠒⠚⠙⠉⠁",
},
"dots7": {
"interval": 80,
"frames": "⠈⠉⠋⠓⠒⠐⠐⠒⠖⠦⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈",
},
"dots8": {
"interval": 80,
"frames": "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈",
},
"dots9": {"interval": 80, "frames": "⢹⢺⢼⣸⣇⡧⡗⡏"},
"dots10": {"interval": 80, "frames": "⢄⢂⢁⡁⡈⡐⡠"},
"dots11": {"interval": 100, "frames": "⠁⠂⠄⡀⢀⠠⠐⠈"},
"dots12": {
"interval": 80,
"frames": [
"⢀⠀",
"⡀⠀",
"⠄⠀",
"⢂⠀",
"⡂⠀",
"⠅⠀",
"⢃⠀",
"⡃⠀",
"⠍⠀",
"⢋⠀",
"⡋⠀",
"⠍⠁",
"⢋⠁",
"⡋⠁",
"⠍⠉",
"⠋⠉",
"⠋⠉",
"⠉⠙",
"⠉⠙",
"⠉⠩",
"⠈⢙",
"⠈⡙",
"⢈⠩",
"⡀⢙",
"⠄⡙",
"⢂⠩",
"⡂⢘",
"⠅⡘",
"⢃⠨",
"⡃⢐",
"⠍⡐",
"⢋⠠",
"⡋⢀",
"⠍⡁",
"⢋⠁",
"⡋⠁",
"⠍⠉",
"⠋⠉",
"⠋⠉",
"⠉⠙",
"⠉⠙",
"⠉⠩",
"⠈⢙",
"⠈⡙",
"⠈⠩",
"⠀⢙",
"⠀⡙",
"⠀⠩",
"⠀⢘",
"⠀⡘",
"⠀⠨",
"⠀⢐",
"⠀⡐",
"⠀⠠",
"⠀⢀",
"⠀⡀",
],
},
"dots8Bit": {
"interval": 80,
"frames": "⠀⠁⠂⠃⠄⠅⠆⠇⡀⡁⡂⡃⡄⡅⡆⡇⠈⠉⠊⠋⠌⠍⠎⠏⡈⡉⡊⡋⡌⡍⡎⡏⠐⠑⠒⠓⠔⠕⠖⠗⡐⡑⡒⡓⡔⡕⡖⡗⠘⠙⠚⠛⠜⠝⠞⠟⡘⡙"
"⡚⡛⡜⡝⡞⡟⠠⠡⠢⠣⠤⠥⠦⠧⡠⡡⡢⡣⡤⡥⡦⡧⠨⠩⠪⠫⠬⠭⠮⠯⡨⡩⡪⡫⡬⡭⡮⡯⠰⠱⠲⠳⠴⠵⠶⠷⡰⡱⡲⡳⡴⡵⡶⡷⠸⠹⠺⠻"
"⠼⠽⠾⠿⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⣀⣁⣂⣃⣄⣅⣆⣇⢈⢉⢊⢋⢌⢍⢎⢏⣈⣉⣊⣋⣌⣍⣎⣏⢐⢑⢒⢓⢔⢕⢖⢗⣐⣑⣒⣓⣔⣕"
"⣖⣗⢘⢙⢚⢛⢜⢝⢞⢟⣘⣙⣚⣛⣜⣝⣞⣟⢠⢡⢢⢣⢤⢥⢦⢧⣠⣡⣢⣣⣤⣥⣦⣧⢨⢩⢪⢫⢬⢭⢮⢯⣨⣩⣪⣫⣬⣭⣮⣯⢰⢱⢲⢳⢴⢵⢶⢷"
"⣰⣱⣲⣳⣴⣵⣶⣷⢸⢹⢺⢻⢼⢽⢾⢿⣸⣹⣺⣻⣼⣽⣾⣿",
},
"line": {"interval": 130, "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*"},
"flip": {
"interval": 70,
"frames": "___-``'´-___",
},
"hamburger": {"interval": 100, "frames": "☱☲☴"},
"growVertical": {
"interval": 120,
"frames": "▁▃▄▅▆▇▆▅▄▃",
},
"growHorizontal": {
"interval": 120,
"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": ["⬆️ ", "↗️ ", "➡️ ", "↘️ ", "⬇️ ", "↙️ ", "⬅️ ", "↖️ "],
},
"arrow3": {
"interval": 120,
"frames": ["▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸"],
},
"bouncingBar": {
"interval": 80,
"frames": [
"[ ]",
"[= ]",
"[== ]",
"[=== ]",
"[ ===]",
"[ ==]",
"[ =]",
"[ ]",
"[ =]",
"[ ==]",
"[ ===]",
"[====]",
"[=== ]",
"[== ]",
"[= ]",
],
},
"bouncingBall": {
"interval": 80,
"frames": [
"( ● )",
"( ● )",
"( ● )",
"( ● )",
"( ●)",
"( ● )",
"( ● )",
"( ● )",
"( ● )",
"(● )",
],
},
"smiley": {"interval": 200, "frames": ["😄 ", "😝 "]},
"monkey": {"interval": 300, "frames": ["🙈 ", "🙈 ", "🙉 ", "🙊 "]},
"hearts": {"interval": 100, "frames": ["💛 ", "💙 ", "💜 ", "💚 ", "❤️ "]},
"clock": {
"interval": 100,
"frames": [
"🕛 ",
"🕐 ",
"🕑 ",
"🕒 ",
"🕓 ",
"🕔 ",
"🕕 ",
"🕖 ",
"🕗 ",
"🕘 ",
"🕙 ",
"🕚 ",
],
},
"earth": {"interval": 180, "frames": ["🌍 ", "🌎 ", "🌏 "]},
"material": {
"interval": 17,
"frames": [
"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"███████▁▁▁▁▁▁▁▁▁▁▁▁▁",
"████████▁▁▁▁▁▁▁▁▁▁▁▁",
"█████████▁▁▁▁▁▁▁▁▁▁▁",
"█████████▁▁▁▁▁▁▁▁▁▁▁",
"██████████▁▁▁▁▁▁▁▁▁▁",
"███████████▁▁▁▁▁▁▁▁▁",
"█████████████▁▁▁▁▁▁▁",
"██████████████▁▁▁▁▁▁",
"██████████████▁▁▁▁▁▁",
"▁██████████████▁▁▁▁▁",
"▁██████████████▁▁▁▁▁",
"▁██████████████▁▁▁▁▁",
"▁▁██████████████▁▁▁▁",
"▁▁▁██████████████▁▁▁",
"▁▁▁▁█████████████▁▁▁",
"▁▁▁▁██████████████▁▁",
"▁▁▁▁██████████████▁▁",
"▁▁▁▁▁██████████████▁",
"▁▁▁▁▁██████████████▁",
"▁▁▁▁▁██████████████▁",
"▁▁▁▁▁▁██████████████",
"▁▁▁▁▁▁██████████████",
"▁▁▁▁▁▁▁█████████████",
"▁▁▁▁▁▁▁█████████████",
"▁▁▁▁▁▁▁▁████████████",
"▁▁▁▁▁▁▁▁████████████",
"▁▁▁▁▁▁▁▁▁███████████",
"▁▁▁▁▁▁▁▁▁███████████",
"▁▁▁▁▁▁▁▁▁▁██████████",
"▁▁▁▁▁▁▁▁▁▁██████████",
"▁▁▁▁▁▁▁▁▁▁▁▁████████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
"██████▁▁▁▁▁▁▁▁▁▁▁▁▁█",
"████████▁▁▁▁▁▁▁▁▁▁▁▁",
"█████████▁▁▁▁▁▁▁▁▁▁▁",
"█████████▁▁▁▁▁▁▁▁▁▁▁",
"█████████▁▁▁▁▁▁▁▁▁▁▁",
"█████████▁▁▁▁▁▁▁▁▁▁▁",
"███████████▁▁▁▁▁▁▁▁▁",
"████████████▁▁▁▁▁▁▁▁",
"████████████▁▁▁▁▁▁▁▁",
"██████████████▁▁▁▁▁▁",
"██████████████▁▁▁▁▁▁",
"▁██████████████▁▁▁▁▁",
"▁██████████████▁▁▁▁▁",
"▁▁▁█████████████▁▁▁▁",
"▁▁▁▁▁████████████▁▁▁",
"▁▁▁▁▁████████████▁▁▁",
"▁▁▁▁▁▁███████████▁▁▁",
"▁▁▁▁▁▁▁▁█████████▁▁▁",
"▁▁▁▁▁▁▁▁█████████▁▁▁",
"▁▁▁▁▁▁▁▁▁█████████▁▁",
"▁▁▁▁▁▁▁▁▁█████████▁▁",
"▁▁▁▁▁▁▁▁▁▁█████████▁",
"▁▁▁▁▁▁▁▁▁▁▁████████▁",
"▁▁▁▁▁▁▁▁▁▁▁████████▁",
"▁▁▁▁▁▁▁▁▁▁▁▁███████▁",
"▁▁▁▁▁▁▁▁▁▁▁▁███████▁",
"▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
],
},
"moon": {
"interval": 80,
"frames": ["🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "],
},
"runner": {"interval": 140, "frames": ["🚶 ", "🏃 "]},
"pong": {
"interval": 80,
"frames": [
"▐⠂ ▌",
"▐⠈ ▌",
"▐ ⠂ ▌",
"▐ ⠠ ▌",
"▐ ⡀ ▌",
"▐ ⠠ ▌",
"▐ ⠂ ▌",
"▐ ⠈ ▌",
"▐ ⠂ ▌",
"▐ ⠠ ▌",
"▐ ⡀ ▌",
"▐ ⠠ ▌",
"▐ ⠂ ▌",
"▐ ⠈ ▌",
"▐ ⠂▌",
"▐ ⠠▌",
"▐ ⡀▌",
"▐ ⠠ ▌",
"▐ ⠂ ▌",
"▐ ⠈ ▌",
"▐ ⠂ ▌",
"▐ ⠠ ▌",
"▐ ⡀ ▌",
"▐ ⠠ ▌",
"▐ ⠂ ▌",
"▐ ⠈ ▌",
"▐ ⠂ ▌",
"▐ ⠠ ▌",
"▐ ⡀ ▌",
"▐⠠ ▌",
],
},
"shark": {
"interval": 120,
"frames": [
"▐|\\____________▌",
"▐_|\\___________▌",
"▐__|\\__________▌",
"▐___|\\_________▌",
"▐____|\\________▌",
"▐_____|\\_______▌",
"▐______|\\______▌",
"▐_______|\\_____▌",
"▐________|\\____▌",
"▐_________|\\___▌",
"▐__________|\\__▌",
"▐___________|\\_▌",
"▐____________|\\",
"▐____________/|▌",
"▐___________/|_▌",
"▐__________/|__▌",
"▐_________/|___▌",
"▐________/|____▌",
"▐_______/|_____▌",
"▐______/|______▌",
"▐_____/|_______▌",
"▐____/|________▌",
"▐___/|_________▌",
"▐__/|__________▌",
"▐_/|___________▌",
"▐/|____________▌",
],
},
"dqpb": {"interval": 100, "frames": "dqpb"},
"weather": {
"interval": 100,
"frames": [
"☀️ ",
"☀️ ",
"☀️ ",
"🌤 ",
"⛅️ ",
"🌥 ",
"☁️ ",
"🌧 ",
"🌨 ",
"🌧 ",
"🌨 ",
"🌧 ",
"🌨 ",
"",
"🌨 ",
"🌧 ",
"🌨 ",
"☁️ ",
"🌥 ",
"⛅️ ",
"🌤 ",
"☀️ ",
"☀️ ",
],
},
"christmas": {"interval": 400, "frames": "🌲🎄"},
"grenade": {
"interval": 80,
"frames": [
"، ",
" ",
" ´ ",
"",
"",
"",
" |",
" ",
"",
"",
" ",
" ",
" ",
" ",
],
},
"point": {"interval": 125, "frames": ["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"]},
"layer": {"interval": 150, "frames": "-=≡"},
"betaWave": {
"interval": 80,
"frames": [
"ρββββββ",
"βρβββββ",
"ββρββββ",
"βββρβββ",
"ββββρββ",
"βββββρβ",
"ββββββρ",
],
},
"aesthetic": {
"interval": 80,
"frames": [
"▰▱▱▱▱▱▱",
"▰▰▱▱▱▱▱",
"▰▰▰▱▱▱▱",
"▰▰▰▰▱▱▱",
"▰▰▰▰▰▱▱",
"▰▰▰▰▰▰▱",
"▰▰▰▰▰▰▰",
"▰▱▱▱▱▱▱",
],
},
}

View file

@ -0,0 +1,16 @@
from typing import List, TypeVar
T = TypeVar("T")
class Stack(List[T]):
"""A small shim over builtin list."""
@property
def top(self) -> T:
"""Get top of stack."""
return self[-1]
def push(self, item: T) -> None:
"""Push an item on to the stack (append in stack nomenclature)."""
self.append(item)

View file

@ -0,0 +1,19 @@
"""
Timer context manager, only used in debug.
"""
from time import time
import contextlib
from typing import Generator
@contextlib.contextmanager
def timer(subject: str = "time") -> Generator[None, None, None]:
"""print the elapsed time. (only used in debugging)"""
start = time()
yield
elapsed = time() - start
elapsed_ms = elapsed * 1000
print(f"{subject} elapsed {elapsed_ms:.1f}ms")

View file

@ -0,0 +1,662 @@
"""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))
_GetConsoleCursorInfo = windll.kernel32.GetConsoleCursorInfo
_GetConsoleCursorInfo.argtypes = [
wintypes.HANDLE,
ctypes.POINTER(CONSOLE_CURSOR_INFO),
]
_GetConsoleCursorInfo.restype = wintypes.BOOL
def GetConsoleCursorInfo(
std_handle: wintypes.HANDLE, cursor_info: CONSOLE_CURSOR_INFO
) -> bool:
"""Get the cursor info - used to get 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 that receives information
about the console's cursor.
Returns:
bool: True if the function succeeds, otherwise False.
"""
return bool(_GetConsoleCursorInfo(std_handle, byref(cursor_info)))
_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"""
current_cursor_size = self._get_cursor_size()
invisible_cursor = CONSOLE_CURSOR_INFO(dwSize=current_cursor_size, bVisible=0)
SetConsoleCursorInfo(self._handle, cursor_info=invisible_cursor)
def show_cursor(self) -> None:
"""Show the cursor"""
current_cursor_size = self._get_cursor_size()
visible_cursor = CONSOLE_CURSOR_INFO(dwSize=current_cursor_size, 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)
def _get_cursor_size(self) -> int:
"""Get the percentage of the character cell that is filled by the cursor"""
cursor_info = CONSOLE_CURSOR_INFO()
GetConsoleCursorInfo(self._handle, cursor_info=cursor_info)
return int(cursor_info.dwSize)
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")

View file

@ -0,0 +1,72 @@
import sys
from dataclasses import dataclass
@dataclass
class WindowsConsoleFeatures:
"""Windows features available."""
vt: bool = False
"""The console supports VT codes."""
truecolor: bool = False
"""The console supports truecolor."""
try:
import ctypes
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
def get_windows_console_features() -> WindowsConsoleFeatures:
features = WindowsConsoleFeatures()
return features
else:
def get_windows_console_features() -> WindowsConsoleFeatures:
"""Get windows console features.
Returns:
WindowsConsoleFeatures: An instance of WindowsConsoleFeatures.
"""
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()
truecolor = win_version.major > 10 or (
win_version.major == 10 and win_version.build >= 15063
)
features = WindowsConsoleFeatures(vt=vt, truecolor=truecolor)
return features
if __name__ == "__main__":
import platform
features = get_windows_console_features()
from pip._vendor.rich import print
print(f'platform="{platform.system()}"')
print(repr(features))

View file

@ -0,0 +1,56 @@
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()
elif control_type == ControlType.SET_WINDOW_TITLE:
_, title = cast(Tuple[ControlType, str], control_code)
term.set_title(title)

View file

@ -0,0 +1,56 @@
import re
from typing import Iterable, List, Tuple
from ._loop import loop_last
from .cells import cell_len, chop_cells
re_word = re.compile(r"\s*\S+\s*")
def words(text: str) -> Iterable[Tuple[int, int, str]]:
position = 0
word_match = re_word.match(text, position)
while word_match is not None:
start, end = word_match.span()
word = word_match.group(0)
yield start, end, word
word_match = re_word.match(text, end)
def divide_line(text: str, width: int, fold: bool = True) -> List[int]:
divides: List[int] = []
append = divides.append
line_position = 0
_cell_len = cell_len
for start, _end, word in words(text):
word_length = _cell_len(word.rstrip())
if line_position + word_length > width:
if word_length > width:
if fold:
chopped_words = chop_cells(word, max_size=width, position=0)
for last, line in loop_last(chopped_words):
if start:
append(start)
if last:
line_position = _cell_len(line)
else:
start += len(line)
else:
if start:
append(start)
line_position = _cell_len(word)
elif line_position and start:
append(start)
line_position = _cell_len(word)
else:
line_position += _cell_len(word)
return divides
if __name__ == "__main__": # pragma: no cover
from .console import Console
console = Console(width=10)
console.print("12345 abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ 12345")
print(chop_cells("abcdefghijklmnopqrstuvwxyz", 10, position=2))

View file

@ -0,0 +1,33 @@
from abc import ABC
class RichRenderable(ABC):
"""An abstract base class for Rich renderables.
Note that there is no need to extend this class, the intended use is to check if an
object supports the Rich renderable protocol. For example::
if isinstance(my_object, RichRenderable):
console.print(my_object)
"""
@classmethod
def __subclasshook__(cls, other: type) -> bool:
"""Check if this class supports the rich render protocol."""
return hasattr(other, "__rich_console__") or hasattr(other, "__rich__")
if __name__ == "__main__": # pragma: no cover
from pip._vendor.rich.text import Text
t = Text()
print(isinstance(Text, RichRenderable))
print(isinstance(t, RichRenderable))
class Foo:
pass
f = Foo()
print(isinstance(f, RichRenderable))
print(isinstance("", RichRenderable))

View file

@ -0,0 +1,311 @@
import sys
from itertools import chain
from typing import TYPE_CHECKING, Iterable, Optional
if sys.version_info >= (3, 8):
from typing import Literal
else:
from pip._vendor.typing_extensions import Literal # pragma: no cover
from .constrain import Constrain
from .jupyter import JupyterMixin
from .measure import Measurement
from .segment import Segment
from .style import StyleType
if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderableType, RenderResult
AlignMethod = Literal["left", "center", "right"]
VerticalAlignMethod = Literal["top", "middle", "bottom"]
class Align(JupyterMixin):
"""Align a renderable by adding spaces if necessary.
Args:
renderable (RenderableType): A console renderable.
align (AlignMethod): One of "left", "center", or "right""
style (StyleType, optional): An optional style to apply to the background.
vertical (Optional[VerticalAlginMethod], optional): Optional vertical align, one of "top", "middle", or "bottom". Defaults to None.
pad (bool, optional): Pad the right with spaces. Defaults to True.
width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None.
height (int, optional): Set height of align renderable, or None to fit to contents. Defaults to None.
Raises:
ValueError: if ``align`` is not one of the expected values.
"""
def __init__(
self,
renderable: "RenderableType",
align: AlignMethod = "left",
style: Optional[StyleType] = None,
*,
vertical: Optional[VerticalAlignMethod] = None,
pad: bool = True,
width: Optional[int] = None,
height: Optional[int] = None,
) -> None:
if align not in ("left", "center", "right"):
raise ValueError(
f'invalid value for align, expected "left", "center", or "right" (not {align!r})'
)
if vertical is not None and vertical not in ("top", "middle", "bottom"):
raise ValueError(
f'invalid value for vertical, expected "top", "middle", or "bottom" (not {vertical!r})'
)
self.renderable = renderable
self.align = align
self.style = style
self.vertical = vertical
self.pad = pad
self.width = width
self.height = height
def __repr__(self) -> str:
return f"Align({self.renderable!r}, {self.align!r})"
@classmethod
def left(
cls,
renderable: "RenderableType",
style: Optional[StyleType] = None,
*,
vertical: Optional[VerticalAlignMethod] = None,
pad: bool = True,
width: Optional[int] = None,
height: Optional[int] = None,
) -> "Align":
"""Align a renderable to the left."""
return cls(
renderable,
"left",
style=style,
vertical=vertical,
pad=pad,
width=width,
height=height,
)
@classmethod
def center(
cls,
renderable: "RenderableType",
style: Optional[StyleType] = None,
*,
vertical: Optional[VerticalAlignMethod] = None,
pad: bool = True,
width: Optional[int] = None,
height: Optional[int] = None,
) -> "Align":
"""Align a renderable to the center."""
return cls(
renderable,
"center",
style=style,
vertical=vertical,
pad=pad,
width=width,
height=height,
)
@classmethod
def right(
cls,
renderable: "RenderableType",
style: Optional[StyleType] = None,
*,
vertical: Optional[VerticalAlignMethod] = None,
pad: bool = True,
width: Optional[int] = None,
height: Optional[int] = None,
) -> "Align":
"""Align a renderable to the right."""
return cls(
renderable,
"right",
style=style,
vertical=vertical,
pad=pad,
width=width,
height=height,
)
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
align = self.align
width = console.measure(self.renderable, options=options).maximum
rendered = console.render(
Constrain(
self.renderable, width if self.width is None else min(width, self.width)
),
options.update(height=None),
)
lines = list(Segment.split_lines(rendered))
width, height = Segment.get_shape(lines)
lines = Segment.set_shape(lines, width, height)
new_line = Segment.line()
excess_space = options.max_width - width
style = console.get_style(self.style) if self.style is not None else None
def generate_segments() -> Iterable[Segment]:
if excess_space <= 0:
# Exact fit
for line in lines:
yield from line
yield new_line
elif align == "left":
# Pad on the right
pad = Segment(" " * excess_space, style) if self.pad else None
for line in lines:
yield from line
if pad:
yield pad
yield new_line
elif align == "center":
# Pad left and right
left = excess_space // 2
pad = Segment(" " * left, style)
pad_right = (
Segment(" " * (excess_space - left), style) if self.pad else None
)
for line in lines:
if left:
yield pad
yield from line
if pad_right:
yield pad_right
yield new_line
elif align == "right":
# Padding on left
pad = Segment(" " * excess_space, style)
for line in lines:
yield pad
yield from line
yield new_line
blank_line = (
Segment(f"{' ' * (self.width or options.max_width)}\n", style)
if self.pad
else Segment("\n")
)
def blank_lines(count: int) -> Iterable[Segment]:
if count > 0:
for _ in range(count):
yield blank_line
vertical_height = self.height or options.height
iter_segments: Iterable[Segment]
if self.vertical and vertical_height is not None:
if self.vertical == "top":
bottom_space = vertical_height - height
iter_segments = chain(generate_segments(), blank_lines(bottom_space))
elif self.vertical == "middle":
top_space = (vertical_height - height) // 2
bottom_space = vertical_height - top_space - height
iter_segments = chain(
blank_lines(top_space),
generate_segments(),
blank_lines(bottom_space),
)
else: # self.vertical == "bottom":
top_space = vertical_height - height
iter_segments = chain(blank_lines(top_space), generate_segments())
else:
iter_segments = generate_segments()
if self.style:
style = console.get_style(self.style)
iter_segments = Segment.apply_style(iter_segments, style)
yield from iter_segments
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
measurement = Measurement.get(console, options, self.renderable)
return measurement
class VerticalCenter(JupyterMixin):
"""Vertically aligns a renderable.
Warn:
This class is deprecated and may be removed in a future version. Use Align class with
`vertical="middle"`.
Args:
renderable (RenderableType): A renderable object.
"""
def __init__(
self,
renderable: "RenderableType",
style: Optional[StyleType] = None,
) -> None:
self.renderable = renderable
self.style = style
def __repr__(self) -> str:
return f"VerticalCenter({self.renderable!r})"
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
style = console.get_style(self.style) if self.style is not None else None
lines = console.render_lines(
self.renderable, options.update(height=None), pad=False
)
width, _height = Segment.get_shape(lines)
new_line = Segment.line()
height = options.height or options.size.height
top_space = (height - len(lines)) // 2
bottom_space = height - top_space - len(lines)
blank_line = Segment(f"{' ' * width}", style)
def blank_lines(count: int) -> Iterable[Segment]:
for _ in range(count):
yield blank_line
yield new_line
if top_space > 0:
yield from blank_lines(top_space)
for line in lines:
yield from line
yield new_line
if bottom_space > 0:
yield from blank_lines(bottom_space)
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
measurement = Measurement.get(console, options, self.renderable)
return measurement
if __name__ == "__main__": # pragma: no cover
from pip._vendor.rich.console import Console, Group
from pip._vendor.rich.highlighter import ReprHighlighter
from pip._vendor.rich.panel import Panel
highlighter = ReprHighlighter()
console = Console()
panel = Panel(
Group(
Align.left(highlighter("align='left'")),
Align.center(highlighter("align='center'")),
Align.right(highlighter("align='right'")),
),
width=60,
style="on dark_blue",
title="Align",
)
console.print(
Align.center(panel, vertical="middle", style="on red", height=console.height)
)

View file

@ -0,0 +1,240 @@
import re
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\](.*?)\x1b\\)|
(?:\x1b([(@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))
""",
re.VERBOSE,
)
class _AnsiToken(NamedTuple):
"""Result of ansi tokenized string."""
plain: str = ""
sgr: Optional[str] = ""
osc: Optional[str] = ""
def _ansi_tokenize(ansi_text: str) -> Iterable[_AnsiToken]:
"""Tokenize a string in to plain text and ANSI codes.
Args:
ansi_text (str): A String containing ANSI codes.
Yields:
AnsiToken: A named tuple of (plain, sgr, osc)
"""
position = 0
sgr: Optional[str]
osc: Optional[str]
for match in re_ansi.finditer(ansi_text):
start, end = match.span(0)
osc, sgr = match.groups()
if start > position:
yield _AnsiToken(ansi_text[position:start])
if sgr:
if sgr == "(":
position = end + 1
continue
if sgr.endswith("m"):
yield _AnsiToken("", sgr[1:-1], osc)
else:
yield _AnsiToken("", sgr, osc)
position = end
if position < len(ansi_text):
yield _AnsiToken(ansi_text[position:])
SGR_STYLE_MAP = {
1: "bold",
2: "dim",
3: "italic",
4: "underline",
5: "blink",
6: "blink2",
7: "reverse",
8: "conceal",
9: "strike",
21: "underline2",
22: "not dim not bold",
23: "not italic",
24: "not underline",
25: "not blink",
26: "not blink2",
27: "not reverse",
28: "not conceal",
29: "not strike",
30: "color(0)",
31: "color(1)",
32: "color(2)",
33: "color(3)",
34: "color(4)",
35: "color(5)",
36: "color(6)",
37: "color(7)",
39: "default",
40: "on color(0)",
41: "on color(1)",
42: "on color(2)",
43: "on color(3)",
44: "on color(4)",
45: "on color(5)",
46: "on color(6)",
47: "on color(7)",
49: "on default",
51: "frame",
52: "encircle",
53: "overline",
54: "not frame not encircle",
55: "not overline",
90: "color(8)",
91: "color(9)",
92: "color(10)",
93: "color(11)",
94: "color(12)",
95: "color(13)",
96: "color(14)",
97: "color(15)",
100: "on color(8)",
101: "on color(9)",
102: "on color(10)",
103: "on color(11)",
104: "on color(12)",
105: "on color(13)",
106: "on color(14)",
107: "on color(15)",
}
class AnsiDecoder:
"""Translate ANSI code in to styled Text."""
def __init__(self) -> None:
self.style = Style.null()
def decode(self, terminal_text: str) -> Iterable[Text]:
"""Decode ANSI codes in an iterable of lines.
Args:
lines (Iterable[str]): An iterable of lines of terminal output.
Yields:
Text: Marked up Text.
"""
for line in terminal_text.splitlines():
yield self.decode_line(line)
def decode_line(self, line: str) -> Text:
"""Decode a line containing ansi codes.
Args:
line (str): A line of terminal output.
Returns:
Text: A Text instance marked up according to ansi codes.
"""
from_ansi = Color.from_ansi
from_rgb = Color.from_rgb
_Style = Style
text = Text()
append = text.append
line = line.rsplit("\r", 1)[-1]
for plain_text, sgr, osc in _ansi_tokenize(line):
if plain_text:
append(plain_text, self.style or None)
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 is not None:
# Translate in to semi-colon separated codes
# Ignore invalid codes, because we want to be lenient
codes = [
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:
if code == 0:
# reset
self.style = _Style.null()
elif code in SGR_STYLE_MAP:
# styles
self.style += _Style.parse(SGR_STYLE_MAP[code])
elif code == 38:
#  Foreground
with suppress(StopIteration):
color_type = next(iter_codes)
if color_type == 5:
self.style += _Style.from_color(
from_ansi(next(iter_codes))
)
elif color_type == 2:
self.style += _Style.from_color(
from_rgb(
next(iter_codes),
next(iter_codes),
next(iter_codes),
)
)
elif code == 48:
# Background
with suppress(StopIteration):
color_type = next(iter_codes)
if color_type == 5:
self.style += _Style.from_color(
None, from_ansi(next(iter_codes))
)
elif color_type == 2:
self.style += _Style.from_color(
None,
from_rgb(
next(iter_codes),
next(iter_codes),
next(iter_codes),
),
)
return text
if sys.platform != "win32" and __name__ == "__main__": # pragma: no cover
import io
import os
import pty
import sys
decoder = AnsiDecoder()
stdout = io.BytesIO()
def read(fd: int) -> bytes:
data = os.read(fd, 1024)
stdout.write(data)
return data
pty.spawn(sys.argv[1:], read)
from .console import Console
console = Console(record=True)
stdout_result = stdout.getvalue().decode("utf-8")
print(stdout_result)
for line in decoder.decode(stdout_result):
console.print(line)
console.save_html("stdout.html")

View file

@ -0,0 +1,94 @@
from typing import Optional, Union
from .color import Color
from .console import Console, ConsoleOptions, RenderResult
from .jupyter import JupyterMixin
from .measure import Measurement
from .segment import Segment
from .style import Style
# There are left-aligned characters for 1/8 to 7/8, but
# the right-aligned characters exist only for 1/8 and 4/8.
BEGIN_BLOCK_ELEMENTS = ["", "", "", "", "", "", "", ""]
END_BLOCK_ELEMENTS = [" ", "", "", "", "", "", "", ""]
FULL_BLOCK = ""
class Bar(JupyterMixin):
"""Renders a solid block bar.
Args:
size (float): Value for the end of the bar.
begin (float): Begin point (between 0 and size, inclusive).
end (float): End point (between 0 and size, inclusive).
width (int, optional): Width of the bar, or ``None`` for maximum width. Defaults to None.
color (Union[Color, str], optional): Color of the bar. Defaults to "default".
bgcolor (Union[Color, str], optional): Color of bar background. Defaults to "default".
"""
def __init__(
self,
size: float,
begin: float,
end: float,
*,
width: Optional[int] = None,
color: Union[Color, str] = "default",
bgcolor: Union[Color, str] = "default",
):
self.size = size
self.begin = max(begin, 0)
self.end = min(end, size)
self.width = width
self.style = Style(color=color, bgcolor=bgcolor)
def __repr__(self) -> str:
return f"Bar({self.size}, {self.begin}, {self.end})"
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
width = min(
self.width if self.width is not None else options.max_width,
options.max_width,
)
if self.begin >= self.end:
yield Segment(" " * width, self.style)
yield Segment.line()
return
prefix_complete_eights = int(width * 8 * self.begin / self.size)
prefix_bar_count = prefix_complete_eights // 8
prefix_eights_count = prefix_complete_eights % 8
body_complete_eights = int(width * 8 * self.end / self.size)
body_bar_count = body_complete_eights // 8
body_eights_count = body_complete_eights % 8
# When start and end fall into the same cell, we ideally should render
# a symbol that's "center-aligned", but there is no good symbol in Unicode.
# In this case, we fall back to right-aligned block symbol for simplicity.
prefix = " " * prefix_bar_count
if prefix_eights_count:
prefix += BEGIN_BLOCK_ELEMENTS[prefix_eights_count]
body = FULL_BLOCK * body_bar_count
if body_eights_count:
body += END_BLOCK_ELEMENTS[body_eights_count]
suffix = " " * (width - len(body))
yield Segment(prefix + body[len(prefix) :] + suffix, self.style)
yield Segment.line()
def __rich_measure__(
self, console: Console, options: ConsoleOptions
) -> Measurement:
return (
Measurement(self.width, self.width)
if self.width is not None
else Measurement(4, options.max_width)
)

View file

@ -0,0 +1,517 @@
import sys
from typing import TYPE_CHECKING, Iterable, List
if sys.version_info >= (3, 8):
from typing import Literal
else:
from pip._vendor.typing_extensions import Literal # pragma: no cover
from ._loop import loop_last
if TYPE_CHECKING:
from pip._vendor.rich.console import ConsoleOptions
class Box:
"""Defines characters to render boxes.
top
head
head_row
mid
row
foot_row
foot
bottom
Args:
box (str): Characters making up box.
ascii (bool, optional): True if this box uses ascii characters only. Default is False.
"""
def __init__(self, box: str, *, ascii: bool = False) -> None:
self._box = box
self.ascii = ascii
line1, line2, line3, line4, line5, line6, line7, line8 = box.splitlines()
# top
self.top_left, self.top, self.top_divider, self.top_right = iter(line1)
# head
self.head_left, _, self.head_vertical, self.head_right = iter(line2)
# head_row
(
self.head_row_left,
self.head_row_horizontal,
self.head_row_cross,
self.head_row_right,
) = iter(line3)
# mid
self.mid_left, _, self.mid_vertical, self.mid_right = iter(line4)
# row
self.row_left, self.row_horizontal, self.row_cross, self.row_right = iter(line5)
# foot_row
(
self.foot_row_left,
self.foot_row_horizontal,
self.foot_row_cross,
self.foot_row_right,
) = iter(line6)
# foot
self.foot_left, _, self.foot_vertical, self.foot_right = iter(line7)
# bottom
self.bottom_left, self.bottom, self.bottom_divider, self.bottom_right = iter(
line8
)
def __repr__(self) -> str:
return "Box(...)"
def __str__(self) -> str:
return self._box
def substitute(self, options: "ConsoleOptions", safe: bool = True) -> "Box":
"""Substitute this box for another if it won't render due to platform issues.
Args:
options (ConsoleOptions): Console options used in rendering.
safe (bool, optional): Substitute this for another Box if there are known problems
displaying on the platform (currently only relevant on Windows). Default is True.
Returns:
Box: A different Box or the same Box.
"""
box = self
if options.legacy_windows and safe:
box = LEGACY_WINDOWS_SUBSTITUTIONS.get(box, box)
if options.ascii_only and not box.ascii:
box = ASCII
return box
def get_plain_headed_box(self) -> "Box":
"""If this box uses special characters for the borders of the header, then
return the equivalent box that does not.
Returns:
Box: The most similar Box that doesn't use header-specific box characters.
If the current Box already satisfies this criterion, then it's returned.
"""
return PLAIN_HEADED_SUBSTITUTIONS.get(self, self)
def get_top(self, widths: Iterable[int]) -> str:
"""Get the top of a simple box.
Args:
widths (List[int]): Widths of columns.
Returns:
str: A string of box characters.
"""
parts: List[str] = []
append = parts.append
append(self.top_left)
for last, width in loop_last(widths):
append(self.top * width)
if not last:
append(self.top_divider)
append(self.top_right)
return "".join(parts)
def get_row(
self,
widths: Iterable[int],
level: Literal["head", "row", "foot", "mid"] = "row",
edge: bool = True,
) -> str:
"""Get the top of a simple box.
Args:
width (List[int]): Widths of columns.
Returns:
str: A string of box characters.
"""
if level == "head":
left = self.head_row_left
horizontal = self.head_row_horizontal
cross = self.head_row_cross
right = self.head_row_right
elif level == "row":
left = self.row_left
horizontal = self.row_horizontal
cross = self.row_cross
right = self.row_right
elif level == "mid":
left = self.mid_left
horizontal = " "
cross = self.mid_vertical
right = self.mid_right
elif level == "foot":
left = self.foot_row_left
horizontal = self.foot_row_horizontal
cross = self.foot_row_cross
right = self.foot_row_right
else:
raise ValueError("level must be 'head', 'row' or 'foot'")
parts: List[str] = []
append = parts.append
if edge:
append(left)
for last, width in loop_last(widths):
append(horizontal * width)
if not last:
append(cross)
if edge:
append(right)
return "".join(parts)
def get_bottom(self, widths: Iterable[int]) -> str:
"""Get the bottom of a simple box.
Args:
widths (List[int]): Widths of columns.
Returns:
str: A string of box characters.
"""
parts: List[str] = []
append = parts.append
append(self.bottom_left)
for last, width in loop_last(widths):
append(self.bottom * width)
if not last:
append(self.bottom_divider)
append(self.bottom_right)
return "".join(parts)
ASCII: Box = Box(
"""\
+--+
| ||
|-+|
| ||
|-+|
|-+|
| ||
+--+
""",
ascii=True,
)
ASCII2: Box = Box(
"""\
+-++
| ||
+-++
| ||
+-++
+-++
| ||
+-++
""",
ascii=True,
)
ASCII_DOUBLE_HEAD: Box = Box(
"""\
+-++
| ||
+=++
| ||
+-++
+-++
| ||
+-++
""",
ascii=True,
)
SQUARE: Box = Box(
"""\
"""
)
SQUARE_DOUBLE_HEAD: Box = Box(
"""\
"""
)
MINIMAL: Box = Box(
"""\
"""
)
MINIMAL_HEAVY_HEAD: Box = Box(
"""\
"""
)
MINIMAL_DOUBLE_HEAD: Box = Box(
"""\
"""
)
SIMPLE: Box = Box(
"""\
"""
)
SIMPLE_HEAD: Box = Box(
"""\
"""
)
SIMPLE_HEAVY: Box = Box(
"""\
"""
)
HORIZONTALS: Box = Box(
"""\
"""
)
ROUNDED: Box = Box(
"""\
"""
)
HEAVY: Box = Box(
"""\
"""
)
HEAVY_EDGE: Box = Box(
"""\
"""
)
HEAVY_HEAD: Box = Box(
"""\
"""
)
DOUBLE: Box = Box(
"""\
"""
)
DOUBLE_EDGE: Box = Box(
"""\
"""
)
MARKDOWN: Box = Box(
"""\
| ||
|-||
| ||
|-||
|-||
| ||
""",
ascii=True,
)
# Map Boxes that don't render with raster fonts on to equivalent that do
LEGACY_WINDOWS_SUBSTITUTIONS = {
ROUNDED: SQUARE,
MINIMAL_HEAVY_HEAD: MINIMAL,
SIMPLE_HEAVY: SIMPLE,
HEAVY: SQUARE,
HEAVY_EDGE: SQUARE,
HEAVY_HEAD: SQUARE,
}
# Map headed boxes to their headerless equivalents
PLAIN_HEADED_SUBSTITUTIONS = {
HEAVY_HEAD: SQUARE,
SQUARE_DOUBLE_HEAD: SQUARE,
MINIMAL_DOUBLE_HEAD: MINIMAL,
MINIMAL_HEAVY_HEAD: MINIMAL,
ASCII_DOUBLE_HEAD: ASCII2,
}
if __name__ == "__main__": # pragma: no cover
from pip._vendor.rich.columns import Columns
from pip._vendor.rich.panel import Panel
from . import box as box
from .console import Console
from .table import Table
from .text import Text
console = Console(record=True)
BOXES = [
"ASCII",
"ASCII2",
"ASCII_DOUBLE_HEAD",
"SQUARE",
"SQUARE_DOUBLE_HEAD",
"MINIMAL",
"MINIMAL_HEAVY_HEAD",
"MINIMAL_DOUBLE_HEAD",
"SIMPLE",
"SIMPLE_HEAD",
"SIMPLE_HEAVY",
"HORIZONTALS",
"ROUNDED",
"HEAVY",
"HEAVY_EDGE",
"HEAVY_HEAD",
"DOUBLE",
"DOUBLE_EDGE",
"MARKDOWN",
]
console.print(Panel("[bold green]Box Constants", style="green"), justify="center")
console.print()
columns = Columns(expand=True, padding=2)
for box_name in sorted(BOXES):
table = Table(
show_footer=True, style="dim", border_style="not dim", expand=True
)
table.add_column("Header 1", "Footer 1")
table.add_column("Header 2", "Footer 2")
table.add_row("Cell", "Cell")
table.add_row("Cell", "Cell")
table.box = getattr(box, box_name)
table.title = Text(f"box.{box_name}", style="magenta")
columns.add_renderable(table)
console.print(columns)
# console.save_svg("box.svg")

View file

@ -0,0 +1,154 @@
import re
from functools import lru_cache
from typing import Callable, List
from ._cell_widths import CELL_WIDTHS
# Regex to match sequence of the most common character ranges
_is_single_cell_widths = re.compile("^[\u0020-\u006f\u00a0\u02ff\u0370-\u0482]*$").match
@lru_cache(4096)
def cached_cell_len(text: str) -> int:
"""Get the number of cells required to display text.
This method always caches, which may use up a lot of memory. It is recommended to use
`cell_len` over this method.
Args:
text (str): Text to display.
Returns:
int: Get the number of cells required to display text.
"""
_get_size = get_character_cell_size
total_size = sum(_get_size(character) for character in text)
return total_size
def cell_len(text: str, _cell_len: Callable[[str], int] = cached_cell_len) -> int:
"""Get the number of cells required to display text.
Args:
text (str): Text to display.
Returns:
int: Get the number of cells required to display text.
"""
if len(text) < 512:
return _cell_len(text)
_get_size = get_character_cell_size
total_size = sum(_get_size(character) for character in text)
return total_size
@lru_cache(maxsize=4096)
def get_character_cell_size(character: str) -> int:
"""Get the cell size of a character.
Args:
character (str): A single character.
Returns:
int: Number of cells (0, 1 or 2) occupied by that character.
"""
return _get_codepoint_cell_size(ord(character))
@lru_cache(maxsize=4096)
def _get_codepoint_cell_size(codepoint: int) -> int:
"""Get the cell size of a character.
Args:
codepoint (int): Codepoint of a character.
Returns:
int: Number of cells (0, 1 or 2) occupied by that character.
"""
_table = CELL_WIDTHS
lower_bound = 0
upper_bound = len(_table) - 1
index = (lower_bound + upper_bound) // 2
while True:
start, end, width = _table[index]
if codepoint < start:
upper_bound = index - 1
elif codepoint > end:
lower_bound = index + 1
else:
return 0 if width == -1 else width
if upper_bound < lower_bound:
break
index = (lower_bound + upper_bound) // 2
return 1
def set_cell_size(text: str, total: int) -> str:
"""Set the length of a string to fit within given number of cells."""
if _is_single_cell_widths(text):
size = len(text)
if size < total:
return text + " " * (total - size)
return text[:total]
if total <= 0:
return ""
cell_size = cell_len(text)
if cell_size == total:
return text
if cell_size < total:
return text + " " * (total - cell_size)
start = 0
end = len(text)
# Binary search until we find the right size
while True:
pos = (start + end) // 2
before = text[: pos + 1]
before_len = cell_len(before)
if before_len == total + 1 and cell_len(before[-1]) == 2:
return before[:-1] + " "
if before_len == total:
return before
if before_len > total:
end = pos
else:
start = pos
# TODO: This is inefficient
# TODO: This might not work with CWJ type characters
def chop_cells(text: str, max_size: int, position: int = 0) -> List[str]:
"""Break text in to equal (cell) length strings, returning the characters in reverse
order"""
_get_character_cell_size = get_character_cell_size
characters = [
(character, _get_character_cell_size(character)) for character in text
]
total_size = position
lines: List[List[str]] = [[]]
append = lines[-1].append
for character, size in reversed(characters):
if total_size + size > max_size:
lines.append([character])
append = lines[-1].append
total_size = size
else:
total_size += size
append(character)
return ["".join(line) for line in lines]
if __name__ == "__main__": # pragma: no cover
print(get_character_cell_size("😽"))
for line in chop_cells("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", 8):
print(line)
for n in range(80, 1, -1):
print(set_cell_size("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", n) + "|")
print("x" * n)

View file

@ -0,0 +1,622 @@
import platform
import re
from colorsys import rgb_to_hls
from enum import IntEnum
from functools import lru_cache
from typing import TYPE_CHECKING, NamedTuple, Optional, Tuple
from ._palettes import EIGHT_BIT_PALETTE, STANDARD_PALETTE, WINDOWS_PALETTE
from .color_triplet import ColorTriplet
from .repr import Result, rich_repr
from .terminal_theme import DEFAULT_TERMINAL_THEME
if TYPE_CHECKING: # pragma: no cover
from .terminal_theme import TerminalTheme
from .text import Text
WINDOWS = platform.system() == "Windows"
class ColorSystem(IntEnum):
"""One of the 3 color system supported by terminals."""
STANDARD = 1
EIGHT_BIT = 2
TRUECOLOR = 3
WINDOWS = 4
def __repr__(self) -> str:
return f"ColorSystem.{self.name}"
def __str__(self) -> str:
return repr(self)
class ColorType(IntEnum):
"""Type of color stored in Color class."""
DEFAULT = 0
STANDARD = 1
EIGHT_BIT = 2
TRUECOLOR = 3
WINDOWS = 4
def __repr__(self) -> str:
return f"ColorType.{self.name}"
ANSI_COLOR_NAMES = {
"black": 0,
"red": 1,
"green": 2,
"yellow": 3,
"blue": 4,
"magenta": 5,
"cyan": 6,
"white": 7,
"bright_black": 8,
"bright_red": 9,
"bright_green": 10,
"bright_yellow": 11,
"bright_blue": 12,
"bright_magenta": 13,
"bright_cyan": 14,
"bright_white": 15,
"grey0": 16,
"gray0": 16,
"navy_blue": 17,
"dark_blue": 18,
"blue3": 20,
"blue1": 21,
"dark_green": 22,
"deep_sky_blue4": 25,
"dodger_blue3": 26,
"dodger_blue2": 27,
"green4": 28,
"spring_green4": 29,
"turquoise4": 30,
"deep_sky_blue3": 32,
"dodger_blue1": 33,
"green3": 40,
"spring_green3": 41,
"dark_cyan": 36,
"light_sea_green": 37,
"deep_sky_blue2": 38,
"deep_sky_blue1": 39,
"spring_green2": 47,
"cyan3": 43,
"dark_turquoise": 44,
"turquoise2": 45,
"green1": 46,
"spring_green1": 48,
"medium_spring_green": 49,
"cyan2": 50,
"cyan1": 51,
"dark_red": 88,
"deep_pink4": 125,
"purple4": 55,
"purple3": 56,
"blue_violet": 57,
"orange4": 94,
"grey37": 59,
"gray37": 59,
"medium_purple4": 60,
"slate_blue3": 62,
"royal_blue1": 63,
"chartreuse4": 64,
"dark_sea_green4": 71,
"pale_turquoise4": 66,
"steel_blue": 67,
"steel_blue3": 68,
"cornflower_blue": 69,
"chartreuse3": 76,
"cadet_blue": 73,
"sky_blue3": 74,
"steel_blue1": 81,
"pale_green3": 114,
"sea_green3": 78,
"aquamarine3": 79,
"medium_turquoise": 80,
"chartreuse2": 112,
"sea_green2": 83,
"sea_green1": 85,
"aquamarine1": 122,
"dark_slate_gray2": 87,
"dark_magenta": 91,
"dark_violet": 128,
"purple": 129,
"light_pink4": 95,
"plum4": 96,
"medium_purple3": 98,
"slate_blue1": 99,
"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,
"dark_sea_green": 108,
"light_sky_blue3": 110,
"sky_blue2": 111,
"dark_sea_green3": 150,
"dark_slate_gray3": 116,
"sky_blue1": 117,
"chartreuse1": 118,
"light_green": 120,
"pale_green1": 156,
"dark_slate_gray1": 123,
"red3": 160,
"medium_violet_red": 126,
"magenta3": 164,
"dark_orange3": 166,
"indian_red": 167,
"hot_pink3": 168,
"medium_orchid3": 133,
"medium_orchid": 134,
"medium_purple2": 140,
"dark_goldenrod": 136,
"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,
"dark_sea_green2": 157,
"light_cyan3": 152,
"light_sky_blue1": 153,
"green_yellow": 154,
"dark_olive_green2": 155,
"dark_sea_green1": 193,
"pale_turquoise1": 159,
"deep_pink3": 162,
"magenta2": 200,
"hot_pink2": 169,
"orchid": 170,
"medium_orchid1": 207,
"orange3": 172,
"light_pink3": 174,
"pink3": 175,
"plum3": 176,
"violet": 177,
"light_goldenrod3": 179,
"tan": 180,
"misty_rose3": 181,
"thistle3": 182,
"plum2": 183,
"khaki3": 185,
"light_goldenrod2": 222,
"light_yellow3": 187,
"grey84": 188,
"gray84": 188,
"light_steel_blue1": 189,
"yellow2": 190,
"dark_olive_green1": 192,
"honeydew2": 194,
"light_cyan1": 195,
"red1": 196,
"deep_pink2": 197,
"deep_pink1": 199,
"magenta1": 201,
"orange_red1": 202,
"indian_red1": 204,
"hot_pink": 206,
"dark_orange": 208,
"salmon1": 209,
"light_coral": 210,
"pale_violet_red1": 211,
"orchid2": 212,
"orchid1": 213,
"orange1": 214,
"sandy_brown": 215,
"light_salmon1": 216,
"light_pink1": 217,
"pink1": 218,
"plum1": 219,
"gold1": 220,
"navajo_white1": 223,
"misty_rose1": 224,
"thistle1": 225,
"yellow1": 226,
"light_goldenrod1": 227,
"khaki1": 228,
"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,
}
class ColorParseError(Exception):
"""The color could not be parsed."""
RE_COLOR = re.compile(
r"""^
\#([0-9a-f]{6})$|
color\(([0-9]{1,3})\)$|
rgb\(([\d\s,]+)\)$
""",
re.VERBOSE,
)
@rich_repr
class Color(NamedTuple):
"""Terminal color definition."""
name: str
"""The name of the color (typically the input to Color.parse)."""
type: ColorType
"""The type of the color."""
number: Optional[int] = None
"""The color number, if a standard color, or None."""
triplet: Optional[ColorTriplet] = None
"""A triplet of color components, if an RGB color."""
def __rich__(self) -> "Text":
"""Displays the actual color if Rich printed."""
from .style import Style
from .text import Text
return Text.assemble(
f"<color {self.name!r} ({self.type.name.lower()})",
("", Style(color=self)),
" >",
)
def __rich_repr__(self) -> Result:
yield self.name
yield self.type
yield "number", self.number, None
yield "triplet", self.triplet, None
@property
def system(self) -> ColorSystem:
"""Get the native color system for this color."""
if self.type == ColorType.DEFAULT:
return ColorSystem.STANDARD
return ColorSystem(int(self.type))
@property
def is_system_defined(self) -> bool:
"""Check if the color is ultimately defined by the system."""
return self.system not in (ColorSystem.EIGHT_BIT, ColorSystem.TRUECOLOR)
@property
def is_default(self) -> bool:
"""Check if the color is a default color."""
return self.type == ColorType.DEFAULT
def get_truecolor(
self, theme: Optional["TerminalTheme"] = None, foreground: bool = True
) -> ColorTriplet:
"""Get an equivalent color triplet for this color.
Args:
theme (TerminalTheme, optional): Optional terminal theme, or None to use default. Defaults to None.
foreground (bool, optional): True for a foreground color, or False for background. Defaults to True.
Returns:
ColorTriplet: A color triplet containing RGB components.
"""
if theme is None:
theme = DEFAULT_TERMINAL_THEME
if self.type == ColorType.TRUECOLOR:
assert self.triplet is not None
return self.triplet
elif self.type == ColorType.EIGHT_BIT:
assert self.number is not None
return EIGHT_BIT_PALETTE[self.number]
elif self.type == ColorType.STANDARD:
assert self.number is not None
return theme.ansi_colors[self.number]
elif self.type == ColorType.WINDOWS:
assert self.number is not None
return WINDOWS_PALETTE[self.number]
else: # self.type == ColorType.DEFAULT:
assert self.number is None
return theme.foreground_color if foreground else theme.background_color
@classmethod
def from_ansi(cls, number: int) -> "Color":
"""Create a Color number from it's 8-bit ansi number.
Args:
number (int): A number between 0-255 inclusive.
Returns:
Color: A new Color instance.
"""
return cls(
name=f"color({number})",
type=(ColorType.STANDARD if number < 16 else ColorType.EIGHT_BIT),
number=number,
)
@classmethod
def from_triplet(cls, triplet: "ColorTriplet") -> "Color":
"""Create a truecolor RGB color from a triplet of values.
Args:
triplet (ColorTriplet): A color triplet containing red, green and blue components.
Returns:
Color: A new color object.
"""
return cls(name=triplet.hex, type=ColorType.TRUECOLOR, triplet=triplet)
@classmethod
def from_rgb(cls, red: float, green: float, blue: float) -> "Color":
"""Create a truecolor from three color components in the range(0->255).
Args:
red (float): Red component in range 0-255.
green (float): Green component in range 0-255.
blue (float): Blue component in range 0-255.
Returns:
Color: A new color object.
"""
return cls.from_triplet(ColorTriplet(int(red), int(green), int(blue)))
@classmethod
def default(cls) -> "Color":
"""Get a Color instance representing the default color.
Returns:
Color: Default color.
"""
return cls(name="default", type=ColorType.DEFAULT)
@classmethod
@lru_cache(maxsize=1024)
def parse(cls, color: str) -> "Color":
"""Parse a color definition."""
original_color = color
color = color.lower().strip()
if color == "default":
return cls(color, type=ColorType.DEFAULT)
color_number = ANSI_COLOR_NAMES.get(color)
if color_number is not None:
return cls(
color,
type=(ColorType.STANDARD if color_number < 16 else ColorType.EIGHT_BIT),
number=color_number,
)
color_match = RE_COLOR.match(color)
if color_match is None:
raise ColorParseError(f"{original_color!r} is not a valid color")
color_24, color_8, color_rgb = color_match.groups()
if color_24:
triplet = ColorTriplet(
int(color_24[0:2], 16), int(color_24[2:4], 16), int(color_24[4:6], 16)
)
return cls(color, ColorType.TRUECOLOR, triplet=triplet)
elif color_8:
number = int(color_8)
if number > 255:
raise ColorParseError(f"color number must be <= 255 in {color!r}")
return cls(
color,
type=(ColorType.STANDARD if number < 16 else ColorType.EIGHT_BIT),
number=number,
)
else: # color_rgb:
components = color_rgb.split(",")
if len(components) != 3:
raise ColorParseError(
f"expected three components in {original_color!r}"
)
red, green, blue = components
triplet = ColorTriplet(int(red), int(green), int(blue))
if not all(component <= 255 for component in triplet):
raise ColorParseError(
f"color components must be <= 255 in {original_color!r}"
)
return cls(color, ColorType.TRUECOLOR, triplet=triplet)
@lru_cache(maxsize=1024)
def get_ansi_codes(self, foreground: bool = True) -> Tuple[str, ...]:
"""Get the ANSI escape codes for this color."""
_type = self.type
if _type == ColorType.DEFAULT:
return ("39" if foreground else "49",)
elif _type == ColorType.WINDOWS:
number = self.number
assert number is not None
fore, back = (30, 40) if number < 8 else (82, 92)
return (str(fore + number if foreground else back + number),)
elif _type == ColorType.STANDARD:
number = self.number
assert number is not None
fore, back = (30, 40) if number < 8 else (82, 92)
return (str(fore + number if foreground else back + number),)
elif _type == ColorType.EIGHT_BIT:
assert self.number is not None
return ("38" if foreground else "48", "5", str(self.number))
else: # self.standard == ColorStandard.TRUECOLOR:
assert self.triplet is not None
red, green, blue = self.triplet
return ("38" if foreground else "48", "2", str(red), str(green), str(blue))
@lru_cache(maxsize=1024)
def downgrade(self, system: ColorSystem) -> "Color":
"""Downgrade a color system to a system with fewer colors."""
if self.type in (ColorType.DEFAULT, system):
return self
# Convert to 8-bit color from truecolor color
if system == ColorSystem.EIGHT_BIT and self.system == ColorSystem.TRUECOLOR:
assert self.triplet is not None
_h, l, s = rgb_to_hls(*self.triplet.normalized)
# If saturation is under 15% assume it is grayscale
if s < 0.15:
gray = round(l * 25.0)
if gray == 0:
color_number = 16
elif gray == 25:
color_number = 231
else:
color_number = 231 + gray
return Color(self.name, ColorType.EIGHT_BIT, number=color_number)
red, green, blue = self.triplet
six_red = red / 95 if red < 95 else 1 + (red - 95) / 40
six_green = green / 95 if green < 95 else 1 + (green - 95) / 40
six_blue = blue / 95 if blue < 95 else 1 + (blue - 95) / 40
color_number = (
16 + 36 * round(six_red) + 6 * round(six_green) + round(six_blue)
)
return Color(self.name, ColorType.EIGHT_BIT, number=color_number)
# Convert to standard from truecolor or 8-bit
elif system == ColorSystem.STANDARD:
if self.system == ColorSystem.TRUECOLOR:
assert self.triplet is not None
triplet = self.triplet
else: # self.system == ColorSystem.EIGHT_BIT
assert self.number is not None
triplet = ColorTriplet(*EIGHT_BIT_PALETTE[self.number])
color_number = STANDARD_PALETTE.match(triplet)
return Color(self.name, ColorType.STANDARD, number=color_number)
elif system == ColorSystem.WINDOWS:
if self.system == ColorSystem.TRUECOLOR:
assert self.triplet is not None
triplet = self.triplet
else: # self.system == ColorSystem.EIGHT_BIT
assert self.number is not None
if self.number < 16:
return Color(self.name, ColorType.WINDOWS, number=self.number)
triplet = ColorTriplet(*EIGHT_BIT_PALETTE[self.number])
color_number = WINDOWS_PALETTE.match(triplet)
return Color(self.name, ColorType.WINDOWS, number=color_number)
return self
def parse_rgb_hex(hex_color: str) -> ColorTriplet:
"""Parse six hex characters in to RGB triplet."""
assert len(hex_color) == 6, "must be 6 characters"
color = ColorTriplet(
int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
)
return color
def blend_rgb(
color1: ColorTriplet, color2: ColorTriplet, cross_fade: float = 0.5
) -> ColorTriplet:
"""Blend one RGB color in to another."""
r1, g1, b1 = color1
r2, g2, b2 = color2
new_color = ColorTriplet(
int(r1 + (r2 - r1) * cross_fade),
int(g1 + (g2 - g1) * cross_fade),
int(b1 + (b2 - b1) * cross_fade),
)
return new_color
if __name__ == "__main__": # pragma: no cover
from .console import Console
from .table import Table
from .text import Text
console = Console()
table = Table(show_footer=False, show_edge=True)
table.add_column("Color", width=10, overflow="ellipsis")
table.add_column("Number", justify="right", style="yellow")
table.add_column("Name", style="green")
table.add_column("Hex", style="blue")
table.add_column("RGB", style="magenta")
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[has-type]
table.add_row(
color_cell, str(color_number), Text(f'"{name}"'), color.hex, color.rgb
)
console.print(table)

View file

@ -0,0 +1,38 @@
from typing import NamedTuple, Tuple
class ColorTriplet(NamedTuple):
"""The red, green, and blue components of a color."""
red: int
"""Red component in 0 to 255 range."""
green: int
"""Green component in 0 to 255 range."""
blue: int
"""Blue component in 0 to 255 range."""
@property
def hex(self) -> str:
"""get the color triplet in CSS style."""
red, green, blue = self
return f"#{red:02x}{green:02x}{blue:02x}"
@property
def rgb(self) -> str:
"""The color in RGB format.
Returns:
str: An rgb color, e.g. ``"rgb(100,23,255)"``.
"""
red, green, blue = self
return f"rgb({red},{green},{blue})"
@property
def normalized(self) -> Tuple[float, float, float]:
"""Convert components into floats between 0 and 1.
Returns:
Tuple[float, float, float]: A tuple of three normalized colour components.
"""
red, green, blue = self
return red / 255.0, green / 255.0, blue / 255.0

View file

@ -0,0 +1,187 @@
from collections import defaultdict
from itertools import chain
from operator import itemgetter
from typing import Dict, Iterable, List, Optional, Tuple
from .align import Align, AlignMethod
from .console import Console, ConsoleOptions, RenderableType, RenderResult
from .constrain import Constrain
from .measure import Measurement
from .padding import Padding, PaddingDimensions
from .table import Table
from .text import TextType
from .jupyter import JupyterMixin
class Columns(JupyterMixin):
"""Display renderables in neat columns.
Args:
renderables (Iterable[RenderableType]): Any number of Rich renderables (including str).
width (int, optional): The desired width of the columns, or None to auto detect. Defaults to None.
padding (PaddingDimensions, optional): Optional padding around cells. Defaults to (0, 1).
expand (bool, optional): Expand columns to full width. Defaults to False.
equal (bool, optional): Arrange in to equal sized columns. Defaults to False.
column_first (bool, optional): Align items from top to bottom (rather than left to right). Defaults to False.
right_to_left (bool, optional): Start column from right hand side. Defaults to False.
align (str, optional): Align value ("left", "right", or "center") or None for default. Defaults to None.
title (TextType, optional): Optional title for Columns.
"""
def __init__(
self,
renderables: Optional[Iterable[RenderableType]] = None,
padding: PaddingDimensions = (0, 1),
*,
width: Optional[int] = None,
expand: bool = False,
equal: bool = False,
column_first: bool = False,
right_to_left: bool = False,
align: Optional[AlignMethod] = None,
title: Optional[TextType] = None,
) -> None:
self.renderables = list(renderables or [])
self.width = width
self.padding = padding
self.expand = expand
self.equal = equal
self.column_first = column_first
self.right_to_left = right_to_left
self.align: Optional[AlignMethod] = align
self.title = title
def add_renderable(self, renderable: RenderableType) -> None:
"""Add a renderable to the columns.
Args:
renderable (RenderableType): Any renderable object.
"""
self.renderables.append(renderable)
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
render_str = console.render_str
renderables = [
render_str(renderable) if isinstance(renderable, str) else renderable
for renderable in self.renderables
]
if not renderables:
return
_top, right, _bottom, left = Padding.unpack(self.padding)
width_padding = max(left, right)
max_width = options.max_width
widths: Dict[int, int] = defaultdict(int)
column_count = len(renderables)
get_measurement = Measurement.get
renderable_widths = [
get_measurement(console, options, renderable).maximum
for renderable in renderables
]
if self.equal:
renderable_widths = [max(renderable_widths)] * len(renderable_widths)
def iter_renderables(
column_count: int,
) -> Iterable[Tuple[int, Optional[RenderableType]]]:
item_count = len(renderables)
if self.column_first:
width_renderables = list(zip(renderable_widths, renderables))
column_lengths: List[int] = [item_count // column_count] * column_count
for col_no in range(item_count % column_count):
column_lengths[col_no] += 1
row_count = (item_count + column_count - 1) // column_count
cells = [[-1] * column_count for _ in range(row_count)]
row = col = 0
for index in range(item_count):
cells[row][col] = index
column_lengths[col] -= 1
if column_lengths[col]:
row += 1
else:
col += 1
row = 0
for index in chain.from_iterable(cells):
if index == -1:
break
yield width_renderables[index]
else:
yield from zip(renderable_widths, renderables)
# Pad odd elements with spaces
if item_count % column_count:
for _ in range(column_count - (item_count % column_count)):
yield 0, None
table = Table.grid(padding=self.padding, collapse_padding=True, pad_edge=False)
table.expand = self.expand
table.title = self.title
if self.width is not None:
column_count = (max_width) // (self.width + width_padding)
for _ in range(column_count):
table.add_column(width=self.width)
else:
while column_count > 1:
widths.clear()
column_no = 0
for renderable_width, _ in iter_renderables(column_count):
widths[column_no] = max(widths[column_no], renderable_width)
total_width = sum(widths.values()) + width_padding * (
len(widths) - 1
)
if total_width > max_width:
column_count = len(widths) - 1
break
else:
column_no = (column_no + 1) % column_count
else:
break
get_renderable = itemgetter(1)
_renderables = [
get_renderable(_renderable)
for _renderable in iter_renderables(column_count)
]
if self.equal:
_renderables = [
None
if renderable is None
else Constrain(renderable, renderable_widths[0])
for renderable in _renderables
]
if self.align:
align = self.align
_Align = Align
_renderables = [
None if renderable is None else _Align(renderable, align)
for renderable in _renderables
]
right_to_left = self.right_to_left
add_row = table.add_row
for start in range(0, len(_renderables), column_count):
row = _renderables[start : start + column_count]
if right_to_left:
row = row[::-1]
add_row(*row)
yield table
if __name__ == "__main__": # pragma: no cover
import os
console = Console()
files = [f"{i} {s}" for i, s in enumerate(sorted(os.listdir()))]
columns = Columns(files, padding=(0, 1), expand=False, equal=False)
console.print(columns)
console.rule()
columns.column_first = True
console.print(columns)
columns.right_to_left = True
console.rule()
console.print(columns)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,37 @@
from typing import Optional, TYPE_CHECKING
from .jupyter import JupyterMixin
from .measure import Measurement
if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderableType, RenderResult
class Constrain(JupyterMixin):
"""Constrain the width of a renderable to a given number of characters.
Args:
renderable (RenderableType): A renderable object.
width (int, optional): The maximum width (in characters) to render. Defaults to 80.
"""
def __init__(self, renderable: "RenderableType", width: Optional[int] = 80) -> None:
self.renderable = renderable
self.width = width
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
if self.width is None:
yield self.renderable
else:
child_options = options.update_width(min(self.width, options.max_width))
yield from console.render(self.renderable, child_options)
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
if self.width is not None:
options = options.update_width(self.width)
measurement = Measurement.get(console, options, self.renderable)
return measurement

View file

@ -0,0 +1,167 @@
from itertools import zip_longest
from typing import (
Iterator,
Iterable,
List,
Optional,
Union,
overload,
TypeVar,
TYPE_CHECKING,
)
if TYPE_CHECKING:
from .console import (
Console,
ConsoleOptions,
JustifyMethod,
OverflowMethod,
RenderResult,
RenderableType,
)
from .text import Text
from .cells import cell_len
from .measure import Measurement
T = TypeVar("T")
class Renderables:
"""A list subclass which renders its contents to the console."""
def __init__(
self, renderables: Optional[Iterable["RenderableType"]] = None
) -> None:
self._renderables: List["RenderableType"] = (
list(renderables) if renderables is not None else []
)
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
"""Console render method to insert line-breaks."""
yield from self._renderables
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
dimensions = [
Measurement.get(console, options, renderable)
for renderable in self._renderables
]
if not dimensions:
return Measurement(1, 1)
_min = max(dimension.minimum for dimension in dimensions)
_max = max(dimension.maximum for dimension in dimensions)
return Measurement(_min, _max)
def append(self, renderable: "RenderableType") -> None:
self._renderables.append(renderable)
def __iter__(self) -> Iterable["RenderableType"]:
return iter(self._renderables)
class Lines:
"""A list subclass which can render to the console."""
def __init__(self, lines: Iterable["Text"] = ()) -> None:
self._lines: List["Text"] = list(lines)
def __repr__(self) -> str:
return f"Lines({self._lines!r})"
def __iter__(self) -> Iterator["Text"]:
return iter(self._lines)
@overload
def __getitem__(self, index: int) -> "Text":
...
@overload
def __getitem__(self, index: slice) -> List["Text"]:
...
def __getitem__(self, index: Union[slice, int]) -> Union["Text", List["Text"]]:
return self._lines[index]
def __setitem__(self, index: int, value: "Text") -> "Lines":
self._lines[index] = value
return self
def __len__(self) -> int:
return self._lines.__len__()
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
"""Console render method to insert line-breaks."""
yield from self._lines
def append(self, line: "Text") -> None:
self._lines.append(line)
def extend(self, lines: Iterable["Text"]) -> None:
self._lines.extend(lines)
def pop(self, index: int = -1) -> "Text":
return self._lines.pop(index)
def justify(
self,
console: "Console",
width: int,
justify: "JustifyMethod" = "left",
overflow: "OverflowMethod" = "fold",
) -> None:
"""Justify and overflow text to a given width.
Args:
console (Console): Console instance.
width (int): Number of characters per line.
justify (str, optional): Default justify method for text: "left", "center", "full" or "right". Defaults to "left".
overflow (str, optional): Default overflow for text: "crop", "fold", or "ellipsis". Defaults to "fold".
"""
from .text import Text
if justify == "left":
for line in self._lines:
line.truncate(width, overflow=overflow, pad=True)
elif justify == "center":
for line in self._lines:
line.rstrip()
line.truncate(width, overflow=overflow)
line.pad_left((width - cell_len(line.plain)) // 2)
line.pad_right(width - cell_len(line.plain))
elif justify == "right":
for line in self._lines:
line.rstrip()
line.truncate(width, overflow=overflow)
line.pad_left(width - cell_len(line.plain))
elif justify == "full":
for line_index, line in enumerate(self._lines):
if line_index == len(self._lines) - 1:
break
words = line.split(" ")
words_size = sum(cell_len(word.plain) for word in words)
num_spaces = len(words) - 1
spaces = [1 for _ in range(num_spaces)]
index = 0
if spaces:
while words_size + num_spaces < width:
spaces[len(spaces) - index - 1] += 1
num_spaces += 1
index = (index + 1) % len(spaces)
tokens: List[Text] = []
for index, (word, next_word) in enumerate(
zip_longest(words, words[1:])
):
tokens.append(word)
if index < len(spaces):
style = word.get_style_at_offset(console, -1)
next_style = next_word.get_style_at_offset(console, 0)
space_style = style if style == next_style else line.style
tokens.append(Text(" " * spaces[index], style=space_style))
self[line_index] = Text("").join(tokens)

View file

@ -0,0 +1,225 @@
import sys
import time
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Union
if sys.version_info >= (3, 8):
from typing import Final
else:
from pip._vendor.typing_extensions import Final # pragma: no cover
from .segment import ControlCode, ControlType, Segment
if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderResult
STRIP_CONTROL_CODES: Final = [
7, # Bell
8, # Backspace
11, # Vertical tab
12, # Form feed
13, # Carriage return
]
_CONTROL_STRIP_TRANSLATE: Final = {
_codepoint: None for _codepoint in STRIP_CONTROL_CODES
}
CONTROL_ESCAPE: Final = {
7: "\\a",
8: "\\b",
11: "\\v",
12: "\\f",
13: "\\r",
}
CONTROL_CODES_FORMAT: Dict[int, Callable[..., str]] = {
ControlType.BELL: lambda: "\x07",
ControlType.CARRIAGE_RETURN: lambda: "\r",
ControlType.HOME: lambda: "\x1b[H",
ControlType.CLEAR: lambda: "\x1b[2J",
ControlType.ENABLE_ALT_SCREEN: lambda: "\x1b[?1049h",
ControlType.DISABLE_ALT_SCREEN: lambda: "\x1b[?1049l",
ControlType.SHOW_CURSOR: lambda: "\x1b[?25h",
ControlType.HIDE_CURSOR: lambda: "\x1b[?25l",
ControlType.CURSOR_UP: lambda param: f"\x1b[{param}A",
ControlType.CURSOR_DOWN: lambda param: f"\x1b[{param}B",
ControlType.CURSOR_FORWARD: lambda param: f"\x1b[{param}C",
ControlType.CURSOR_BACKWARD: lambda param: f"\x1b[{param}D",
ControlType.CURSOR_MOVE_TO_COLUMN: lambda param: f"\x1b[{param+1}G",
ControlType.ERASE_IN_LINE: lambda param: f"\x1b[{param}K",
ControlType.CURSOR_MOVE_TO: lambda x, y: f"\x1b[{y+1};{x+1}H",
ControlType.SET_WINDOW_TITLE: lambda title: f"\x1b]0;{title}\x07",
}
class Control:
"""A renderable that inserts a control code (non printable but may move cursor).
Args:
*codes (str): Positional arguments are either a :class:`~rich.segment.ControlType` enum or a
tuple of ControlType and an integer parameter
"""
__slots__ = ["segment"]
def __init__(self, *codes: Union[ControlType, ControlCode]) -> None:
control_codes: List[ControlCode] = [
(code,) if isinstance(code, ControlType) else code for code in codes
]
_format_map = CONTROL_CODES_FORMAT
rendered_codes = "".join(
_format_map[code](*parameters) for code, *parameters in control_codes
)
self.segment = Segment(rendered_codes, None, control_codes)
@classmethod
def bell(cls) -> "Control":
"""Ring the 'bell'."""
return cls(ControlType.BELL)
@classmethod
def home(cls) -> "Control":
"""Move cursor to 'home' position."""
return cls(ControlType.HOME)
@classmethod
def move(cls, x: int = 0, y: int = 0) -> "Control":
"""Move cursor relative to current position.
Args:
x (int): X offset.
y (int): Y offset.
Returns:
~Control: Control object.
"""
def get_codes() -> Iterable[ControlCode]:
control = ControlType
if x:
yield (
control.CURSOR_FORWARD if x > 0 else control.CURSOR_BACKWARD,
abs(x),
)
if y:
yield (
control.CURSOR_DOWN if y > 0 else control.CURSOR_UP,
abs(y),
)
control = cls(*get_codes())
return control
@classmethod
def move_to_column(cls, x: int, y: int = 0) -> "Control":
"""Move to the given column, optionally add offset to row.
Returns:
x (int): absolute x (column)
y (int): optional y offset (row)
Returns:
~Control: Control object.
"""
return (
cls(
(ControlType.CURSOR_MOVE_TO_COLUMN, x),
(
ControlType.CURSOR_DOWN if y > 0 else ControlType.CURSOR_UP,
abs(y),
),
)
if y
else cls((ControlType.CURSOR_MOVE_TO_COLUMN, x))
)
@classmethod
def move_to(cls, x: int, y: int) -> "Control":
"""Move cursor to absolute position.
Args:
x (int): x offset (column)
y (int): y offset (row)
Returns:
~Control: Control object.
"""
return cls((ControlType.CURSOR_MOVE_TO, x, y))
@classmethod
def clear(cls) -> "Control":
"""Clear the screen."""
return cls(ControlType.CLEAR)
@classmethod
def show_cursor(cls, show: bool) -> "Control":
"""Show or hide the cursor."""
return cls(ControlType.SHOW_CURSOR if show else ControlType.HIDE_CURSOR)
@classmethod
def alt_screen(cls, enable: bool) -> "Control":
"""Enable or disable alt screen."""
if enable:
return cls(ControlType.ENABLE_ALT_SCREEN, ControlType.HOME)
else:
return cls(ControlType.DISABLE_ALT_SCREEN)
@classmethod
def title(cls, title: str) -> "Control":
"""Set the terminal window title
Args:
title (str): The new terminal window title
"""
return cls((ControlType.SET_WINDOW_TITLE, title))
def __str__(self) -> str:
return self.segment.text
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
if self.segment.text:
yield self.segment
def strip_control_codes(
text: str, _translate_table: Dict[int, None] = _CONTROL_STRIP_TRANSLATE
) -> str:
"""Remove control codes from text.
Args:
text (str): A string possibly contain control codes.
Returns:
str: String with control codes removed.
"""
return text.translate(_translate_table)
def escape_control_codes(
text: str,
_translate_table: Dict[int, str] = CONTROL_ESCAPE,
) -> str:
"""Replace control codes with their "escaped" equivalent in the given text.
(e.g. "\b" becomes "\\b")
Args:
text (str): A string possibly containing control codes.
Returns:
str: String with control codes replaced with their escaped version.
"""
return text.translate(_translate_table)
if __name__ == "__main__": # pragma: no cover
from pip._vendor.rich.console import Console
console = Console()
console.print("Look at the title of your terminal window ^")
# console.print(Control((ControlType.SET_WINDOW_TITLE, "Hello, world!")))
for i in range(10):
console.set_window_title("🚀 Loading" + "." * i)
time.sleep(0.5)

View file

@ -0,0 +1,190 @@
from typing import Dict
from .style import Style
DEFAULT_STYLES: Dict[str, Style] = {
"none": Style.null(),
"reset": Style(
color="default",
bgcolor="default",
dim=False,
bold=False,
italic=False,
underline=False,
blink=False,
blink2=False,
reverse=False,
conceal=False,
strike=False,
),
"dim": Style(dim=True),
"bright": Style(dim=False),
"bold": Style(bold=True),
"strong": Style(bold=True),
"code": Style(reverse=True, bold=True),
"italic": Style(italic=True),
"emphasize": Style(italic=True),
"underline": Style(underline=True),
"blink": Style(blink=True),
"blink2": Style(blink2=True),
"reverse": Style(reverse=True),
"strike": Style(strike=True),
"black": Style(color="black"),
"red": Style(color="red"),
"green": Style(color="green"),
"yellow": Style(color="yellow"),
"magenta": Style(color="magenta"),
"cyan": Style(color="cyan"),
"white": Style(color="white"),
"inspect.attr": Style(color="yellow", italic=True),
"inspect.attr.dunder": Style(color="yellow", italic=True, dim=True),
"inspect.callable": Style(bold=True, color="red"),
"inspect.async_def": Style(italic=True, color="bright_cyan"),
"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"),
"inspect.doc": Style(dim=True),
"inspect.value.border": Style(color="green"),
"live.ellipsis": Style(bold=True, color="red"),
"layout.tree.row": Style(dim=False, color="red"),
"layout.tree.column": Style(dim=False, color="blue"),
"logging.keyword": Style(bold=True, color="yellow"),
"logging.level.notset": Style(dim=True),
"logging.level.debug": Style(color="green"),
"logging.level.info": Style(color="blue"),
"logging.level.warning": Style(color="red"),
"logging.level.error": Style(color="red", bold=True),
"logging.level.critical": Style(color="red", bold=True, reverse=True),
"log.level": Style.null(),
"log.time": Style(color="cyan", dim=True),
"log.message": Style.null(),
"log.path": Style(dim=True),
"repr.ellipsis": Style(color="yellow"),
"repr.indent": Style(color="green", dim=True),
"repr.error": Style(color="red", bold=True),
"repr.str": Style(color="green", italic=False, bold=False),
"repr.brace": Style(bold=True),
"repr.comma": Style(bold=True),
"repr.ipv4": Style(bold=True, color="bright_green"),
"repr.ipv6": Style(bold=True, color="bright_green"),
"repr.eui48": Style(bold=True, color="bright_green"),
"repr.eui64": Style(bold=True, color="bright_green"),
"repr.tag_start": Style(bold=True),
"repr.tag_name": Style(color="bright_magenta", bold=True),
"repr.tag_contents": Style(color="default"),
"repr.tag_end": Style(bold=True),
"repr.attrib_name": Style(color="yellow", italic=False),
"repr.attrib_equal": Style(bold=True),
"repr.attrib_value": Style(color="magenta", italic=False),
"repr.number": Style(color="cyan", bold=True, italic=False),
"repr.number_complex": Style(color="cyan", bold=True, italic=False), # same
"repr.bool_true": Style(color="bright_green", italic=True),
"repr.bool_false": Style(color="bright_red", italic=True),
"repr.none": Style(color="magenta", italic=True),
"repr.url": Style(underline=True, color="bright_blue", italic=False, bold=False),
"repr.uuid": Style(color="bright_yellow", bold=False),
"repr.call": Style(color="magenta", bold=True),
"repr.path": Style(color="magenta"),
"repr.filename": Style(color="bright_magenta"),
"rule.line": Style(color="bright_green"),
"rule.text": Style.null(),
"json.brace": Style(bold=True),
"json.bool_true": Style(color="bright_green", italic=True),
"json.bool_false": Style(color="bright_red", italic=True),
"json.null": Style(color="magenta", italic=True),
"json.number": Style(color="cyan", bold=True, italic=False),
"json.str": Style(color="green", italic=False, bold=False),
"json.key": Style(color="blue", bold=True),
"prompt": Style.null(),
"prompt.choices": Style(color="magenta", bold=True),
"prompt.default": Style(color="cyan", bold=True),
"prompt.invalid": Style(color="red"),
"prompt.invalid.choice": Style(color="red"),
"pretty": Style.null(),
"scope.border": Style(color="blue"),
"scope.key": Style(color="yellow", italic=True),
"scope.key.special": Style(color="yellow", italic=True, dim=True),
"scope.equals": Style(color="red"),
"table.header": Style(bold=True),
"table.footer": Style(bold=True),
"table.cell": Style.null(),
"table.title": Style(italic=True),
"table.caption": Style(italic=True, dim=True),
"traceback.error": Style(color="red", italic=True),
"traceback.border.syntax_error": Style(color="bright_red"),
"traceback.border": Style(color="red"),
"traceback.text": Style.null(),
"traceback.title": Style(color="red", bold=True),
"traceback.exc_type": Style(color="bright_red", bold=True),
"traceback.exc_value": Style.null(),
"traceback.offset": Style(color="bright_red", bold=True),
"bar.back": Style(color="grey23"),
"bar.complete": Style(color="rgb(249,38,114)"),
"bar.finished": Style(color="rgb(114,156,31)"),
"bar.pulse": Style(color="rgb(249,38,114)"),
"progress.description": Style.null(),
"progress.filesize": Style(color="green"),
"progress.filesize.total": Style(color="green"),
"progress.download": Style(color="green"),
"progress.elapsed": Style(color="yellow"),
"progress.percentage": Style(color="magenta"),
"progress.remaining": Style(color="cyan"),
"progress.data.speed": Style(color="red"),
"progress.spinner": Style(color="green"),
"status.spinner": Style(color="green"),
"tree": Style(),
"tree.line": Style(),
"markdown.paragraph": Style(),
"markdown.text": Style(),
"markdown.em": Style(italic=True),
"markdown.emph": Style(italic=True), # For commonmark backwards compatibility
"markdown.strong": Style(bold=True),
"markdown.code": Style(bold=True, color="cyan", bgcolor="black"),
"markdown.code_block": Style(color="cyan", bgcolor="black"),
"markdown.block_quote": Style(color="magenta"),
"markdown.list": Style(color="cyan"),
"markdown.item": Style(),
"markdown.item.bullet": Style(color="yellow", bold=True),
"markdown.item.number": Style(color="yellow", bold=True),
"markdown.hr": Style(color="yellow"),
"markdown.h1.border": Style(),
"markdown.h1": Style(bold=True),
"markdown.h2": Style(bold=True, underline=True),
"markdown.h3": Style(bold=True),
"markdown.h4": Style(bold=True, dim=True),
"markdown.h5": Style(underline=True),
"markdown.h6": Style(italic=True),
"markdown.h7": Style(italic=True, dim=True),
"markdown.link": Style(color="bright_blue"),
"markdown.link_url": Style(color="blue", underline=True),
"markdown.s": Style(strike=True),
"iso8601.date": Style(color="blue"),
"iso8601.time": Style(color="magenta"),
"iso8601.timezone": Style(color="yellow"),
}
if __name__ == "__main__": # pragma: no cover
import argparse
import io
from pip._vendor.rich.console import Console
from pip._vendor.rich.table import Table
from pip._vendor.rich.text import Text
parser = argparse.ArgumentParser()
parser.add_argument("--html", action="store_true", help="Export as HTML table")
args = parser.parse_args()
html: bool = args.html
console = Console(record=True, width=70, file=io.StringIO()) if html else Console()
table = Table("Name", "Styling")
for style_name, style in DEFAULT_STYLES.items():
table.add_row(Text(style_name, style=style), str(style))
console.print(table)
if html:
print(console.export_html(inline_styles=True))

View file

@ -0,0 +1,37 @@
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",
"JUPYTER_COLUMNS",
"JUPYTER_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()

View file

@ -0,0 +1,96 @@
import sys
from typing import TYPE_CHECKING, Optional, Union
from .jupyter import JupyterMixin
from .segment import Segment
from .style import Style
from ._emoji_codes import EMOJI
from ._emoji_replace import _emoji_replace
if sys.version_info >= (3, 8):
from typing import Literal
else:
from pip._vendor.typing_extensions import Literal # pragma: no cover
if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderResult
EmojiVariant = Literal["emoji", "text"]
class NoEmoji(Exception):
"""No emoji by that name."""
class Emoji(JupyterMixin):
__slots__ = ["name", "style", "_char", "variant"]
VARIANTS = {"text": "\uFE0E", "emoji": "\uFE0F"}
def __init__(
self,
name: str,
style: Union[str, Style] = "none",
variant: Optional[EmojiVariant] = None,
) -> None:
"""A single emoji character.
Args:
name (str): Name of emoji.
style (Union[str, Style], optional): Optional style. Defaults to None.
Raises:
NoEmoji: If the emoji doesn't exist.
"""
self.name = name
self.style = style
self.variant = variant
try:
self._char = EMOJI[name]
except KeyError:
raise NoEmoji(f"No emoji called {name!r}")
if variant is not None:
self._char += self.VARIANTS.get(variant, "")
@classmethod
def replace(cls, text: str) -> str:
"""Replace emoji markup with corresponding unicode characters.
Args:
text (str): A string with emojis codes, e.g. "Hello :smiley:!"
Returns:
str: A string with emoji codes replaces with actual emoji.
"""
return _emoji_replace(text)
def __repr__(self) -> str:
return f"<emoji {self.name!r}>"
def __str__(self) -> str:
return self._char
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
yield Segment(self._char, console.get_style(self.style))
if __name__ == "__main__": # pragma: no cover
import sys
from pip._vendor.rich.columns import Columns
from pip._vendor.rich.console import Console
console = Console(record=True)
columns = Columns(
(f":{name}: {name}" for name in sorted(EMOJI.keys()) if "\u200D" not in name),
column_first=True,
)
console.print(columns)
if len(sys.argv) > 1:
console.save_html(sys.argv[1])

View file

@ -0,0 +1,34 @@
class ConsoleError(Exception):
"""An error in console operation."""
class StyleError(Exception):
"""An error in styles."""
class StyleSyntaxError(ConsoleError):
"""Style was badly formatted."""
class MissingStyle(StyleError):
"""No such style."""
class StyleStackError(ConsoleError):
"""Style stack is invalid."""
class NotRenderableError(ConsoleError):
"""Object is not renderable."""
class MarkupError(ConsoleError):
"""Markup was badly formatted."""
class LiveError(ConsoleError):
"""Error related to Live display."""
class NoAltScreen(ConsoleError):
"""Alt screen mode was required."""

View file

@ -0,0 +1,57 @@
import io
from typing import IO, TYPE_CHECKING, Any, List
from .ansi import AnsiDecoder
from .text import Text
if TYPE_CHECKING:
from .console import Console
class FileProxy(io.TextIOBase):
"""Wraps a file (e.g. sys.stdout) and redirects writes to a console."""
def __init__(self, console: "Console", file: IO[str]) -> None:
self.__console = console
self.__file = file
self.__buffer: List[str] = []
self.__ansi_decoder = AnsiDecoder()
@property
def rich_proxied_file(self) -> IO[str]:
"""Get proxied file."""
return self.__file
def __getattr__(self, name: str) -> Any:
return getattr(self.__file, name)
def write(self, text: str) -> int:
if not isinstance(text, str):
raise TypeError(f"write() argument must be str, not {type(text).__name__}")
buffer = self.__buffer
lines: List[str] = []
while text:
line, new_line, text = text.partition("\n")
if new_line:
lines.append("".join(buffer) + line)
buffer.clear()
else:
buffer.append(line)
break
if lines:
console = self.__console
with console:
output = Text("\n").join(
self.__ansi_decoder.decode_line(line) for line in lines
)
console.print(output)
return len(text)
def flush(self) -> None:
output = "".join(self.__buffer)
if output:
self.__console.print(output)
del self.__buffer[:]
def fileno(self) -> int:
return self.__file.fileno()

View file

@ -0,0 +1,89 @@
# coding: utf-8
"""Functions for reporting filesizes. Borrowed from https://github.com/PyFilesystem/pyfilesystem2
The functions declared in this module should cover the different
use cases needed to generate a string representation of a file size
using several different units. Since there are many standards regarding
file size units, three different functions have been implemented.
See Also:
* `Wikipedia: Binary prefix <https://en.wikipedia.org/wiki/Binary_prefix>`_
"""
__all__ = ["decimal"]
from typing import Iterable, List, Optional, Tuple
def _to_str(
size: int,
suffixes: Iterable[str],
base: int,
*,
precision: Optional[int] = 1,
separator: Optional[str] = " ",
) -> str:
if size == 1:
return "1 byte"
elif size < base:
return "{:,} bytes".format(size)
for i, suffix in enumerate(suffixes, 2): # noqa: B007
unit = base**i
if size < unit:
break
return "{:,.{precision}f}{separator}{}".format(
(base * size / unit),
suffix,
precision=precision,
separator=separator,
)
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
if size < unit * base:
break
return unit, suffix
def decimal(
size: int,
*,
precision: Optional[int] = 1,
separator: Optional[str] = " ",
) -> str:
"""Convert a filesize in to a string (powers of 1000, SI prefixes).
In this convention, ``1000 B = 1 kB``.
This is typically the format used to advertise the storage
capacity of USB flash drives and the like (*256 MB* meaning
actually a storage capacity of more than *256 000 000 B*),
or used by **Mac OS X** since v10.6 to report file sizes.
Arguments:
int (size): A file size.
int (precision): The number of decimal places to include (default = 1).
str (separator): The string to separate the value from the units (default = " ").
Returns:
`str`: A string containing a abbreviated file size and units.
Example:
>>> filesize.decimal(30000)
'30.0 kB'
>>> filesize.decimal(30000, precision=2, separator="")
'30.00kB'
"""
return _to_str(
size,
("kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"),
1000,
precision=precision,
separator=separator,
)

View file

@ -0,0 +1,232 @@
import re
from abc import ABC, abstractmethod
from typing import List, Union
from .text import Span, Text
def _combine_regex(*regexes: str) -> str:
"""Combine a number of regexes in to a single regex.
Returns:
str: New regex with all regexes ORed together.
"""
return "|".join(regexes)
class Highlighter(ABC):
"""Abstract base class for highlighters."""
def __call__(self, text: Union[str, Text]) -> Text:
"""Highlight a str or Text instance.
Args:
text (Union[str, ~Text]): Text to highlight.
Raises:
TypeError: If not called with text or str.
Returns:
Text: A test instance with highlighting applied.
"""
if isinstance(text, str):
highlight_text = Text(text)
elif isinstance(text, Text):
highlight_text = text.copy()
else:
raise TypeError(f"str or Text instance required, not {text!r}")
self.highlight(highlight_text)
return highlight_text
@abstractmethod
def highlight(self, text: Text) -> None:
"""Apply highlighting in place to text.
Args:
text (~Text): A text object highlight.
"""
class NullHighlighter(Highlighter):
"""A highlighter object that doesn't highlight.
May be used to disable highlighting entirely.
"""
def highlight(self, text: Text) -> None:
"""Nothing to do"""
class RegexHighlighter(Highlighter):
"""Applies highlighting from a list of regular expressions."""
highlights: List[str] = []
base_style: str = ""
def highlight(self, text: Text) -> None:
"""Highlight :class:`rich.text.Text` using regular expressions.
Args:
text (~Text): Text to highlighted.
"""
highlight_regex = text.highlight_regex
for re_highlight in self.highlights:
highlight_regex(re_highlight, style_prefix=self.base_style)
class ReprHighlighter(RegexHighlighter):
"""Highlights the text typically produced from ``__repr__`` methods."""
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>[][{}()])",
_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<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_complex>(?<!\w)(?:\-?[0-9]+\.?[0-9]*(?:e[-+]?\d+?)?)(?:[-+](?:[0-9]+\.?[0-9]*(?:e[-+]?\d+)?))?j)",
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$_+!`(),.?/;:&=%#]*)",
),
]
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]*)",
JSON_STR,
),
]
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
class ISO8601Highlighter(RegexHighlighter):
"""Highlights the ISO8601 date time strings.
Regex reference: https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s07.html
"""
base_style = "iso8601."
highlights = [
#
# Dates
#
# Calendar month (e.g. 2008-08). The hyphen is required
r"^(?P<year>[0-9]{4})-(?P<month>1[0-2]|0[1-9])$",
# Calendar date w/o hyphens (e.g. 20080830)
r"^(?P<date>(?P<year>[0-9]{4})(?P<month>1[0-2]|0[1-9])(?P<day>3[01]|0[1-9]|[12][0-9]))$",
# Ordinal date (e.g. 2008-243). The hyphen is optional
r"^(?P<date>(?P<year>[0-9]{4})-?(?P<day>36[0-6]|3[0-5][0-9]|[12][0-9]{2}|0[1-9][0-9]|00[1-9]))$",
#
# Weeks
#
# Week of the year (e.g., 2008-W35). The hyphen is optional
r"^(?P<date>(?P<year>[0-9]{4})-?W(?P<week>5[0-3]|[1-4][0-9]|0[1-9]))$",
# Week date (e.g., 2008-W35-6). The hyphens are optional
r"^(?P<date>(?P<year>[0-9]{4})-?W(?P<week>5[0-3]|[1-4][0-9]|0[1-9])-?(?P<day>[1-7]))$",
#
# Times
#
# Hours and minutes (e.g., 17:21). The colon is optional
r"^(?P<time>(?P<hour>2[0-3]|[01][0-9]):?(?P<minute>[0-5][0-9]))$",
# Hours, minutes, and seconds w/o colons (e.g., 172159)
r"^(?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))$",
# Time zone designator (e.g., Z, +07 or +07:00). The colons and the minutes are optional
r"^(?P<timezone>(Z|[+-](?:2[0-3]|[01][0-9])(?::?(?:[0-5][0-9]))?))$",
# Hours, minutes, and seconds with time zone designator (e.g., 17:21:59+07:00).
# All the colons are optional. The minutes in the time zone designator are also optional
r"^(?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9])(?::?(?:[0-5][0-9]))?)$",
#
# Date and Time
#
# Calendar date with hours, minutes, and seconds (e.g., 2008-08-30 17:21:59 or 20080830 172159).
# A space is required between the date and the time. The hyphens and colons are optional.
# This regex matches dates and times that specify some hyphens or colons but omit others.
# This does not follow ISO 8601
r"^(?P<date>(?P<year>[0-9]{4})(?P<hyphen>-)?(?P<month>1[0-2]|0[1-9])(?(hyphen)-)(?P<day>3[01]|0[1-9]|[12][0-9])) (?P<time>(?P<hour>2[0-3]|[01][0-9])(?(hyphen):)(?P<minute>[0-5][0-9])(?(hyphen):)(?P<second>[0-5][0-9]))$",
#
# XML Schema dates and times
#
# Date, with optional time zone (e.g., 2008-08-30 or 2008-08-30+07:00).
# Hyphens are required. This is the XML Schema 'date' type
r"^(?P<date>(?P<year>-?(?:[1-9][0-9]*)?[0-9]{4})-(?P<month>1[0-2]|0[1-9])-(?P<day>3[01]|0[1-9]|[12][0-9]))(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
# Time, with optional fractional seconds and time zone (e.g., 01:45:36 or 01:45:36.123+07:00).
# There is no limit on the number of digits for the fractional seconds. This is the XML Schema 'time' type
r"^(?P<time>(?P<hour>2[0-3]|[01][0-9]):(?P<minute>[0-5][0-9]):(?P<second>[0-5][0-9])(?P<frac>\.[0-9]+)?)(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
# Date and time, with optional fractional seconds and time zone (e.g., 2008-08-30T01:45:36 or 2008-08-30T01:45:36.123Z).
# This is the XML Schema 'dateTime' type
r"^(?P<date>(?P<year>-?(?:[1-9][0-9]*)?[0-9]{4})-(?P<month>1[0-2]|0[1-9])-(?P<day>3[01]|0[1-9]|[12][0-9]))T(?P<time>(?P<hour>2[0-3]|[01][0-9]):(?P<minute>[0-5][0-9]):(?P<second>[0-5][0-9])(?P<ms>\.[0-9]+)?)(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
]
if __name__ == "__main__": # pragma: no cover
from .console import Console
console = Console()
console.print("[bold green]hello world![/bold green]")
console.print("'[bold green]hello world![/bold green]'")
console.print(" /foo")
console.print("/foo/")
console.print("/foo/bar")
console.print("foo/bar/baz")
console.print("/foo/bar/baz?foo=bar+egg&egg=baz")
console.print("/foo/bar/baz/")
console.print("/foo/bar/baz/egg")
console.print("/foo/bar/baz/egg.py")
console.print("/foo/bar/baz/egg.py word")
console.print(" /foo/bar/baz/egg.py word")
console.print("foo /foo/bar/baz/egg.py word")
console.print("foo /foo/bar/ba._++z/egg+.py word")
console.print("https://example.org?foo=bar#header")
console.print(1234567.34)
console.print(1 / 2)
console.print(-1 / 123123123123)
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)

View file

@ -0,0 +1,140 @@
from pathlib import Path
from json import loads, dumps
from typing import Any, Callable, Optional, Union
from .text import Text
from .highlighter import JSONHighlighter, NullHighlighter
class JSON:
"""A renderable which pretty prints JSON.
Args:
json (str): JSON encoded data.
indent (Union[None, int, str], optional): Number of characters to indent by. Defaults to 2.
highlight (bool, optional): Enable highlighting. Defaults to True.
skip_keys (bool, optional): Skip keys not of a basic type. Defaults to False.
ensure_ascii (bool, optional): Escape all non-ascii characters. Defaults to False.
check_circular (bool, optional): Check for circular references. Defaults to True.
allow_nan (bool, optional): Allow NaN and Infinity values. Defaults to True.
default (Callable, optional): A callable that converts values that can not be encoded
in to something that can be JSON encoded. Defaults to None.
sort_keys (bool, optional): Sort dictionary keys. Defaults to False.
"""
def __init__(
self,
json: str,
indent: Union[None, int, str] = 2,
highlight: bool = True,
skip_keys: bool = False,
ensure_ascii: bool = False,
check_circular: bool = True,
allow_nan: bool = True,
default: Optional[Callable[[Any], Any]] = None,
sort_keys: bool = False,
) -> None:
data = loads(json)
json = dumps(
data,
indent=indent,
skipkeys=skip_keys,
ensure_ascii=ensure_ascii,
check_circular=check_circular,
allow_nan=allow_nan,
default=default,
sort_keys=sort_keys,
)
highlighter = JSONHighlighter() if highlight else NullHighlighter()
self.text = highlighter(json)
self.text.no_wrap = True
self.text.overflow = None
@classmethod
def from_data(
cls,
data: Any,
indent: Union[None, int, str] = 2,
highlight: bool = True,
skip_keys: bool = False,
ensure_ascii: bool = False,
check_circular: bool = True,
allow_nan: bool = True,
default: Optional[Callable[[Any], Any]] = None,
sort_keys: bool = False,
) -> "JSON":
"""Encodes a JSON object from arbitrary data.
Args:
data (Any): An object that may be encoded in to JSON
indent (Union[None, int, str], optional): Number of characters to indent by. Defaults to 2.
highlight (bool, optional): Enable highlighting. Defaults to True.
default (Callable, optional): Optional callable which will be called for objects that cannot be serialized. Defaults to None.
skip_keys (bool, optional): Skip keys not of a basic type. Defaults to False.
ensure_ascii (bool, optional): Escape all non-ascii characters. Defaults to False.
check_circular (bool, optional): Check for circular references. Defaults to True.
allow_nan (bool, optional): Allow NaN and Infinity values. Defaults to True.
default (Callable, optional): A callable that converts values that can not be encoded
in to something that can be JSON encoded. Defaults to None.
sort_keys (bool, optional): Sort dictionary keys. Defaults to False.
Returns:
JSON: New JSON object from the given data.
"""
json_instance: "JSON" = cls.__new__(cls)
json = dumps(
data,
indent=indent,
skipkeys=skip_keys,
ensure_ascii=ensure_ascii,
check_circular=check_circular,
allow_nan=allow_nan,
default=default,
sort_keys=sort_keys,
)
highlighter = JSONHighlighter() if highlight else NullHighlighter()
json_instance.text = highlighter(json)
json_instance.text.no_wrap = True
json_instance.text.overflow = None
return json_instance
def __rich__(self) -> Text:
return self.text
if __name__ == "__main__":
import argparse
import sys
parser = argparse.ArgumentParser(description="Pretty print json")
parser.add_argument(
"path",
metavar="PATH",
help="path to file, or - for stdin",
)
parser.add_argument(
"-i",
"--indent",
metavar="SPACES",
type=int,
help="Number of spaces in an indent",
default=2,
)
args = parser.parse_args()
from pip._vendor.rich.console import Console
console = Console()
error_console = Console(stderr=True)
try:
if args.path == "-":
json_data = sys.stdin.read()
else:
json_data = Path(args.path).read_text()
except Exception as error:
error_console.print(f"Unable to read {args.path!r}; {error}")
sys.exit(-1)
console.print(JSON(json_data, indent=args.indent), soft_wrap=True)

View file

@ -0,0 +1,101 @@
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Sequence
if TYPE_CHECKING:
from pip._vendor.rich.console import ConsoleRenderable
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>
"""
class JupyterRenderable:
"""A shim to write html to Jupyter notebook."""
def __init__(self, html: str, text: str) -> None:
self.html = html
self.text = text
def _repr_mimebundle_(
self, include: Sequence[str], exclude: Sequence[str], **kwargs: Any
) -> Dict[str, str]:
data = {"text/plain": self.text, "text/html": self.html}
if include:
data = {k: v for (k, v) in data.items() if k in include}
if exclude:
data = {k: v for (k, v) in data.items() if k not in exclude}
return data
class JupyterMixin:
"""Add to an Rich renderable to make it render in Jupyter notebook."""
__slots__ = ()
def _repr_mimebundle_(
self: "ConsoleRenderable",
include: Sequence[str],
exclude: Sequence[str],
**kwargs: Any,
) -> Dict[str, str]:
console = get_console()
segments = list(console.render(self, console.options))
html = _render_segments(segments)
text = console._render_buffer(segments)
data = {"text/plain": text, "text/html": html}
if include:
data = {k: v for (k, v) in data.items() if k in include}
if exclude:
data = {k: v for (k, v) in data.items() if k not in exclude}
return data
def _render_segments(segments: Iterable[Segment]) -> str:
def escape(text: str) -> str:
"""Escape html."""
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
fragments: List[str] = []
append_fragment = fragments.append
theme = DEFAULT_TERMINAL_THEME
for text, style, control in Segment.simplify(segments):
if control:
continue
text = escape(text)
if style:
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}" target="_blank">{text}</a>'
append_fragment(text)
code = "".join(fragments)
html = JUPYTER_HTML_FORMAT.format(code=code)
return html
def display(segments: Iterable[Segment], text: str) -> None:
"""Render segments to Jupyter."""
html = _render_segments(segments)
jupyter_renderable = JupyterRenderable(html, text)
try:
from IPython.display import display as ipython_display
ipython_display(jupyter_renderable)
except ModuleNotFoundError:
# Handle the case where the Console has force_jupyter=True,
# but IPython is not installed.
pass
def print(*args: Any, **kwargs: Any) -> None:
"""Proxy for Console print."""
console = get_console()
return console.print(*args, **kwargs)

View file

@ -0,0 +1,443 @@
from abc import ABC, abstractmethod
from itertools import islice
from operator import itemgetter
from threading import RLock
from typing import (
TYPE_CHECKING,
Dict,
Iterable,
List,
NamedTuple,
Optional,
Sequence,
Tuple,
Union,
)
from ._ratio import ratio_resolve
from .align import Align
from .console import Console, ConsoleOptions, RenderableType, RenderResult
from .highlighter import ReprHighlighter
from .panel import Panel
from .pretty import Pretty
from .region import Region
from .repr import Result, rich_repr
from .segment import Segment
from .style import StyleType
if TYPE_CHECKING:
from pip._vendor.rich.tree import Tree
class LayoutRender(NamedTuple):
"""An individual layout render."""
region: Region
render: List[List[Segment]]
RegionMap = Dict["Layout", Region]
RenderMap = Dict["Layout", LayoutRender]
class LayoutError(Exception):
"""Layout related error."""
class NoSplitter(LayoutError):
"""Requested splitter does not exist."""
class _Placeholder:
"""An internal renderable used as a Layout placeholder."""
highlighter = ReprHighlighter()
def __init__(self, layout: "Layout", style: StyleType = "") -> None:
self.layout = layout
self.style = style
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
width = options.max_width
height = options.height or options.size.height
layout = self.layout
title = (
f"{layout.name!r} ({width} x {height})"
if layout.name
else f"({width} x {height})"
)
yield Panel(
Align.center(Pretty(layout), vertical="middle"),
style=self.style,
title=self.highlighter(title),
border_style="blue",
height=height,
)
class Splitter(ABC):
"""Base class for a splitter."""
name: str = ""
@abstractmethod
def get_tree_icon(self) -> str:
"""Get the icon (emoji) used in layout.tree"""
@abstractmethod
def divide(
self, children: Sequence["Layout"], region: Region
) -> Iterable[Tuple["Layout", Region]]:
"""Divide a region amongst several child layouts.
Args:
children (Sequence(Layout)): A number of child layouts.
region (Region): A rectangular region to divide.
"""
class RowSplitter(Splitter):
"""Split a layout region in to rows."""
name = "row"
def get_tree_icon(self) -> str:
return "[layout.tree.row]⬌"
def divide(
self, children: Sequence["Layout"], region: Region
) -> Iterable[Tuple["Layout", Region]]:
x, y, width, height = region
render_widths = ratio_resolve(width, children)
offset = 0
_Region = Region
for child, child_width in zip(children, render_widths):
yield child, _Region(x + offset, y, child_width, height)
offset += child_width
class ColumnSplitter(Splitter):
"""Split a layout region in to columns."""
name = "column"
def get_tree_icon(self) -> str:
return "[layout.tree.column]⬍"
def divide(
self, children: Sequence["Layout"], region: Region
) -> Iterable[Tuple["Layout", Region]]:
x, y, width, height = region
render_heights = ratio_resolve(height, children)
offset = 0
_Region = Region
for child, child_height in zip(children, render_heights):
yield child, _Region(x, y + offset, width, child_height)
offset += child_height
@rich_repr
class Layout:
"""A renderable to divide a fixed height in to rows or columns.
Args:
renderable (RenderableType, optional): Renderable content, or None for placeholder. Defaults to None.
name (str, optional): Optional identifier for Layout. Defaults to None.
size (int, optional): Optional fixed size of layout. Defaults to None.
minimum_size (int, optional): Minimum size of layout. Defaults to 1.
ratio (int, optional): Optional ratio for flexible layout. Defaults to 1.
visible (bool, optional): Visibility of layout. Defaults to True.
"""
splitters = {"row": RowSplitter, "column": ColumnSplitter}
def __init__(
self,
renderable: Optional[RenderableType] = None,
*,
name: Optional[str] = None,
size: Optional[int] = None,
minimum_size: int = 1,
ratio: int = 1,
visible: bool = True,
) -> None:
self._renderable = renderable or _Placeholder(self)
self.size = size
self.minimum_size = minimum_size
self.ratio = ratio
self.name = name
self.visible = visible
self.splitter: Splitter = self.splitters["column"]()
self._children: List[Layout] = []
self._render_map: RenderMap = {}
self._lock = RLock()
def __rich_repr__(self) -> Result:
yield "name", self.name, None
yield "size", self.size, None
yield "minimum_size", self.minimum_size, 1
yield "ratio", self.ratio, 1
@property
def renderable(self) -> RenderableType:
"""Layout renderable."""
return self if self._children else self._renderable
@property
def children(self) -> List["Layout"]:
"""Gets (visible) layout children."""
return [child for child in self._children if child.visible]
@property
def map(self) -> RenderMap:
"""Get a map of the last render."""
return self._render_map
def get(self, name: str) -> Optional["Layout"]:
"""Get a named layout, or None if it doesn't exist.
Args:
name (str): Name of layout.
Returns:
Optional[Layout]: Layout instance or None if no layout was found.
"""
if self.name == name:
return self
else:
for child in self._children:
named_layout = child.get(name)
if named_layout is not None:
return named_layout
return None
def __getitem__(self, name: str) -> "Layout":
layout = self.get(name)
if layout is None:
raise KeyError(f"No layout with name {name!r}")
return layout
@property
def tree(self) -> "Tree":
"""Get a tree renderable to show layout structure."""
from pip._vendor.rich.styled import Styled
from pip._vendor.rich.table import Table
from pip._vendor.rich.tree import Tree
def summary(layout: "Layout") -> Table:
icon = layout.splitter.get_tree_icon()
table = Table.grid(padding=(0, 1, 0, 0))
text: RenderableType = (
Pretty(layout) if layout.visible else Styled(Pretty(layout), "dim")
)
table.add_row(icon, text)
_summary = table
return _summary
layout = self
tree = Tree(
summary(layout),
guide_style=f"layout.tree.{layout.splitter.name}",
highlight=True,
)
def recurse(tree: "Tree", layout: "Layout") -> None:
for child in layout._children:
recurse(
tree.add(
summary(child),
guide_style=f"layout.tree.{child.splitter.name}",
),
child,
)
recurse(tree, self)
return tree
def split(
self,
*layouts: Union["Layout", RenderableType],
splitter: Union[Splitter, str] = "column",
) -> None:
"""Split the layout in to multiple sub-layouts.
Args:
*layouts (Layout): Positional arguments should be (sub) Layout instances.
splitter (Union[Splitter, str]): Splitter instance or name of splitter.
"""
_layouts = [
layout if isinstance(layout, Layout) else Layout(layout)
for layout in layouts
]
try:
self.splitter = (
splitter
if isinstance(splitter, Splitter)
else self.splitters[splitter]()
)
except KeyError:
raise NoSplitter(f"No splitter called {splitter!r}")
self._children[:] = _layouts
def add_split(self, *layouts: Union["Layout", RenderableType]) -> None:
"""Add a new layout(s) to existing split.
Args:
*layouts (Union[Layout, RenderableType]): Positional arguments should be renderables or (sub) Layout instances.
"""
_layouts = (
layout if isinstance(layout, Layout) else Layout(layout)
for layout in layouts
)
self._children.extend(_layouts)
def split_row(self, *layouts: Union["Layout", RenderableType]) -> None:
"""Split the layout in to a row (layouts side by side).
Args:
*layouts (Layout): Positional arguments should be (sub) Layout instances.
"""
self.split(*layouts, splitter="row")
def split_column(self, *layouts: Union["Layout", RenderableType]) -> None:
"""Split the layout in to a column (layouts stacked on top of each other).
Args:
*layouts (Layout): Positional arguments should be (sub) Layout instances.
"""
self.split(*layouts, splitter="column")
def unsplit(self) -> None:
"""Reset splits to initial state."""
del self._children[:]
def update(self, renderable: RenderableType) -> None:
"""Update renderable.
Args:
renderable (RenderableType): New renderable object.
"""
with self._lock:
self._renderable = renderable
def refresh_screen(self, console: "Console", layout_name: str) -> None:
"""Refresh a sub-layout.
Args:
console (Console): Console instance where Layout is to be rendered.
layout_name (str): Name of layout.
"""
with self._lock:
layout = self[layout_name]
region, _lines = self._render_map[layout]
(x, y, width, height) = region
lines = console.render_lines(
layout, console.options.update_dimensions(width, height)
)
self._render_map[layout] = LayoutRender(region, lines)
console.update_screen_lines(lines, x, y)
def _make_region_map(self, width: int, height: int) -> RegionMap:
"""Create a dict that maps layout on to Region."""
stack: List[Tuple[Layout, Region]] = [(self, Region(0, 0, width, height))]
push = stack.append
pop = stack.pop
layout_regions: List[Tuple[Layout, Region]] = []
append_layout_region = layout_regions.append
while stack:
append_layout_region(pop())
layout, region = layout_regions[-1]
children = layout.children
if children:
for child_and_region in layout.splitter.divide(children, region):
push(child_and_region)
region_map = {
layout: region
for layout, region in sorted(layout_regions, key=itemgetter(1))
}
return region_map
def render(self, console: Console, options: ConsoleOptions) -> RenderMap:
"""Render the sub_layouts.
Args:
console (Console): Console instance.
options (ConsoleOptions): Console options.
Returns:
RenderMap: A dict that maps Layout on to a tuple of Region, lines
"""
render_width = options.max_width
render_height = options.height or console.height
region_map = self._make_region_map(render_width, render_height)
layout_regions = [
(layout, region)
for layout, region in region_map.items()
if not layout.children
]
render_map: Dict["Layout", "LayoutRender"] = {}
render_lines = console.render_lines
update_dimensions = options.update_dimensions
for layout, region in layout_regions:
lines = render_lines(
layout.renderable, update_dimensions(region.width, region.height)
)
render_map[layout] = LayoutRender(region, lines)
return render_map
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
with self._lock:
width = options.max_width or console.width
height = options.height or console.height
render_map = self.render(console, options.update_dimensions(width, height))
self._render_map = render_map
layout_lines: List[List[Segment]] = [[] for _ in range(height)]
_islice = islice
for (region, lines) in render_map.values():
_x, y, _layout_width, layout_height = region
for row, line in zip(
_islice(layout_lines, y, y + layout_height), lines
):
row.extend(line)
new_line = Segment.line()
for layout_row in layout_lines:
yield from layout_row
yield new_line
if __name__ == "__main__":
from pip._vendor.rich.console import Console
console = Console()
layout = Layout()
layout.split_column(
Layout(name="header", size=3),
Layout(ratio=1, name="main"),
Layout(size=10, name="footer"),
)
layout["main"].split_row(Layout(name="side"), Layout(name="body", ratio=2))
layout["body"].split_row(Layout(name="content", ratio=2), Layout(name="s2"))
layout["s2"].split_column(
Layout(name="top"), Layout(name="middle"), Layout(name="bottom")
)
layout["side"].split_column(Layout(layout.tree, name="left1"), Layout(name="left2"))
layout["content"].update("foo")
console.print(layout)

View file

@ -0,0 +1,375 @@
import sys
from threading import Event, RLock, Thread
from types import TracebackType
from typing import IO, Any, Callable, List, Optional, TextIO, Type, cast
from . import get_console
from .console import Console, ConsoleRenderable, RenderableType, RenderHook
from .control import Control
from .file_proxy import FileProxy
from .jupyter import JupyterMixin
from .live_render import LiveRender, VerticalOverflowMethod
from .screen import Screen
from .text import Text
class _RefreshThread(Thread):
"""A thread that calls refresh() at regular intervals."""
def __init__(self, live: "Live", refresh_per_second: float) -> None:
self.live = live
self.refresh_per_second = refresh_per_second
self.done = Event()
super().__init__(daemon=True)
def stop(self) -> None:
self.done.set()
def run(self) -> None:
while not self.done.wait(1 / self.refresh_per_second):
with self.live._lock:
if not self.done.is_set():
self.live.refresh()
class Live(JupyterMixin, RenderHook):
"""Renders an auto-updating live display of any given renderable.
Args:
renderable (RenderableType, optional): The renderable to live display. Defaults to displaying nothing.
console (Console, optional): Optional Console instance. Default will an internal Console instance writing to stdout.
screen (bool, optional): Enable alternate screen mode. Defaults to False.
auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()` or `update()` with refresh flag. Defaults to True
refresh_per_second (float, optional): Number of times per second to refresh the live display. Defaults to 4.
transient (bool, optional): Clear the renderable on exit (has no effect when screen=True). Defaults to False.
redirect_stdout (bool, optional): Enable redirection of stdout, so ``print`` may be used. Defaults to True.
redirect_stderr (bool, optional): Enable redirection of stderr. Defaults to True.
vertical_overflow (VerticalOverflowMethod, optional): How to handle renderable when it is too tall for the console. Defaults to "ellipsis".
get_renderable (Callable[[], RenderableType], optional): Optional callable to get renderable. Defaults to None.
"""
def __init__(
self,
renderable: Optional[RenderableType] = None,
*,
console: Optional[Console] = None,
screen: bool = False,
auto_refresh: bool = True,
refresh_per_second: float = 4,
transient: bool = False,
redirect_stdout: bool = True,
redirect_stderr: bool = True,
vertical_overflow: VerticalOverflowMethod = "ellipsis",
get_renderable: Optional[Callable[[], RenderableType]] = None,
) -> None:
assert refresh_per_second > 0, "refresh_per_second must be > 0"
self._renderable = renderable
self.console = console if console is not None else get_console()
self._screen = screen
self._alt_screen = False
self._redirect_stdout = redirect_stdout
self._redirect_stderr = redirect_stderr
self._restore_stdout: Optional[IO[str]] = None
self._restore_stderr: Optional[IO[str]] = None
self._lock = RLock()
self.ipy_widget: Optional[Any] = None
self.auto_refresh = auto_refresh
self._started: bool = False
self.transient = True if screen else transient
self._refresh_thread: Optional[_RefreshThread] = None
self.refresh_per_second = refresh_per_second
self.vertical_overflow = vertical_overflow
self._get_renderable = get_renderable
self._live_render = LiveRender(
self.get_renderable(), vertical_overflow=vertical_overflow
)
@property
def is_started(self) -> bool:
"""Check if live display has been started."""
return self._started
def get_renderable(self) -> RenderableType:
renderable = (
self._get_renderable()
if self._get_renderable is not None
else self._renderable
)
return renderable or ""
def start(self, refresh: bool = False) -> None:
"""Start live rendering display.
Args:
refresh (bool, optional): Also refresh. Defaults to False.
"""
with self._lock:
if self._started:
return
self.console.set_live(self)
self._started = True
if self._screen:
self._alt_screen = self.console.set_alt_screen(True)
self.console.show_cursor(False)
self._enable_redirect_io()
self.console.push_render_hook(self)
if refresh:
try:
self.refresh()
except Exception:
# If refresh fails, we want to stop the redirection of sys.stderr,
# so the error stacktrace is properly displayed in the terminal.
# (or, if the code that calls Rich captures the exception and wants to display something,
# let this be displayed in the terminal).
self.stop()
raise
if self.auto_refresh:
self._refresh_thread = _RefreshThread(self, self.refresh_per_second)
self._refresh_thread.start()
def stop(self) -> None:
"""Stop live rendering display."""
with self._lock:
if not self._started:
return
self.console.clear_live()
self._started = False
if self.auto_refresh and self._refresh_thread is not None:
self._refresh_thread.stop()
self._refresh_thread = None
# allow it to fully render on the last even if overflow
self.vertical_overflow = "visible"
with self.console:
try:
if not self._alt_screen and not self.console.is_jupyter:
self.refresh()
finally:
self._disable_redirect_io()
self.console.pop_render_hook()
if not self._alt_screen and self.console.is_terminal:
self.console.line()
self.console.show_cursor(True)
if self._alt_screen:
self.console.set_alt_screen(False)
if self.transient and not self._alt_screen:
self.console.control(self._live_render.restore_cursor())
if self.ipy_widget is not None and self.transient:
self.ipy_widget.close() # pragma: no cover
def __enter__(self) -> "Live":
self.start(refresh=self._renderable is not None)
return self
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
self.stop()
def _enable_redirect_io(self) -> None:
"""Enable redirecting of stdout / stderr."""
if self.console.is_terminal or self.console.is_jupyter:
if self._redirect_stdout and not isinstance(sys.stdout, FileProxy):
self._restore_stdout = sys.stdout
sys.stdout = cast("TextIO", FileProxy(self.console, sys.stdout))
if self._redirect_stderr and not isinstance(sys.stderr, FileProxy):
self._restore_stderr = sys.stderr
sys.stderr = cast("TextIO", FileProxy(self.console, sys.stderr))
def _disable_redirect_io(self) -> None:
"""Disable redirecting of stdout / stderr."""
if self._restore_stdout:
sys.stdout = cast("TextIO", self._restore_stdout)
self._restore_stdout = None
if self._restore_stderr:
sys.stderr = cast("TextIO", self._restore_stderr)
self._restore_stderr = None
@property
def renderable(self) -> RenderableType:
"""Get the renderable that is being displayed
Returns:
RenderableType: Displayed renderable.
"""
renderable = self.get_renderable()
return Screen(renderable) if self._alt_screen else renderable
def update(self, renderable: RenderableType, *, refresh: bool = False) -> None:
"""Update the renderable that is being displayed
Args:
renderable (RenderableType): New renderable to use.
refresh (bool, optional): Refresh the display. Defaults to False.
"""
if isinstance(renderable, str):
renderable = self.console.render_str(renderable)
with self._lock:
self._renderable = renderable
if refresh:
self.refresh()
def refresh(self) -> None:
"""Update the display of the Live Render."""
with self._lock:
self._live_render.set_renderable(self.renderable)
if self.console.is_jupyter: # pragma: no cover
try:
from IPython.display import display
from ipywidgets import Output
except ImportError:
import warnings
warnings.warn('install "ipywidgets" for Jupyter support')
else:
if self.ipy_widget is None:
self.ipy_widget = Output()
display(self.ipy_widget)
with self.ipy_widget:
self.ipy_widget.clear_output(wait=True)
self.console.print(self._live_render.renderable)
elif self.console.is_terminal and not self.console.is_dumb_terminal:
with self.console:
self.console.print(Control())
elif (
not self._started and not self.transient
): # if it is finished allow files or dumb-terminals to see final result
with self.console:
self.console.print(Control())
def process_renderables(
self, renderables: List[ConsoleRenderable]
) -> List[ConsoleRenderable]:
"""Process renderables to restore cursor and display progress."""
self._live_render.vertical_overflow = self.vertical_overflow
if self.console.is_interactive:
# lock needs acquiring as user can modify live_render renderable at any time unlike in Progress.
with self._lock:
reset = (
Control.home()
if self._alt_screen
else self._live_render.position_cursor()
)
renderables = [reset, *renderables, self._live_render]
elif (
not self._started and not self.transient
): # if it is finished render the final output for files or dumb_terminals
renderables = [*renderables, self._live_render]
return renderables
if __name__ == "__main__": # pragma: no cover
import random
import time
from itertools import cycle
from typing import Dict, List, Tuple
from .align import Align
from .console import Console
from .live import Live as Live
from .panel import Panel
from .rule import Rule
from .syntax import Syntax
from .table import Table
console = Console()
syntax = Syntax(
'''def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
"""Iterate and generate a tuple with a flag for last value."""
iter_values = iter(values)
try:
previous_value = next(iter_values)
except StopIteration:
return
for value in iter_values:
yield False, previous_value
previous_value = value
yield True, previous_value''',
"python",
line_numbers=True,
)
table = Table("foo", "bar", "baz")
table.add_row("1", "2", "3")
progress_renderables = [
"You can make the terminal shorter and taller to see the live table hide"
"Text may be printed while the progress bars are rendering.",
Panel("In fact, [i]any[/i] renderable will work"),
"Such as [magenta]tables[/]...",
table,
"Pretty printed structures...",
{"type": "example", "text": "Pretty printed"},
"Syntax...",
syntax,
Rule("Give it a try!"),
]
examples = cycle(progress_renderables)
exchanges = [
"SGD",
"MYR",
"EUR",
"USD",
"AUD",
"JPY",
"CNH",
"HKD",
"CAD",
"INR",
"DKK",
"GBP",
"RUB",
"NZD",
"MXN",
"IDR",
"TWD",
"THB",
"VND",
]
with Live(console=console) as live_table:
exchange_rate_dict: Dict[Tuple[str, str], float] = {}
for index in range(100):
select_exchange = exchanges[index % len(exchanges)]
for exchange in exchanges:
if exchange == select_exchange:
continue
time.sleep(0.4)
if random.randint(0, 10) < 1:
console.log(next(examples))
exchange_rate_dict[(select_exchange, exchange)] = 200 / (
(random.random() * 320) + 1
)
if len(exchange_rate_dict) > len(exchanges) - 1:
exchange_rate_dict.pop(list(exchange_rate_dict.keys())[0])
table = Table(title="Exchange Rates")
table.add_column("Source Currency")
table.add_column("Destination Currency")
table.add_column("Exchange Rate")
for ((source, dest), exchange_rate) in exchange_rate_dict.items():
table.add_row(
source,
dest,
Text(
f"{exchange_rate:.4f}",
style="red" if exchange_rate < 1.0 else "green",
),
)
live_table.update(Align.center(table))

View file

@ -0,0 +1,113 @@
import sys
from typing import Optional, Tuple
if sys.version_info >= (3, 8):
from typing import Literal
else:
from pip._vendor.typing_extensions import Literal # pragma: no cover
from ._loop import loop_last
from .console import Console, ConsoleOptions, RenderableType, RenderResult
from .control import Control
from .segment import ControlType, Segment
from .style import StyleType
from .text import Text
VerticalOverflowMethod = Literal["crop", "ellipsis", "visible"]
class LiveRender:
"""Creates a renderable that may be updated.
Args:
renderable (RenderableType): Any renderable object.
style (StyleType, optional): An optional style to apply to the renderable. Defaults to "".
"""
def __init__(
self,
renderable: RenderableType,
style: StyleType = "",
vertical_overflow: VerticalOverflowMethod = "ellipsis",
) -> None:
self.renderable = renderable
self.style = style
self.vertical_overflow = vertical_overflow
self._shape: Optional[Tuple[int, int]] = None
def set_renderable(self, renderable: RenderableType) -> None:
"""Set a new renderable.
Args:
renderable (RenderableType): Any renderable object, including str.
"""
self.renderable = renderable
def position_cursor(self) -> Control:
"""Get control codes to move cursor to beginning of live render.
Returns:
Control: A control instance that may be printed.
"""
if self._shape is not None:
_, height = self._shape
return Control(
ControlType.CARRIAGE_RETURN,
(ControlType.ERASE_IN_LINE, 2),
*(
(
(ControlType.CURSOR_UP, 1),
(ControlType.ERASE_IN_LINE, 2),
)
* (height - 1)
)
)
return Control()
def restore_cursor(self) -> Control:
"""Get control codes to clear the render and restore the cursor to its previous position.
Returns:
Control: A Control instance that may be printed.
"""
if self._shape is not None:
_, height = self._shape
return Control(
ControlType.CARRIAGE_RETURN,
*((ControlType.CURSOR_UP, 1), (ControlType.ERASE_IN_LINE, 2)) * height
)
return Control()
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
renderable = self.renderable
style = console.get_style(self.style)
lines = console.render_lines(renderable, options, style=style, pad=False)
shape = Segment.get_shape(lines)
_, height = shape
if height > options.size.height:
if self.vertical_overflow == "crop":
lines = lines[: options.size.height]
shape = Segment.get_shape(lines)
elif self.vertical_overflow == "ellipsis":
lines = lines[: (options.size.height - 1)]
overflow_text = Text(
"...",
overflow="crop",
justify="center",
end="",
style="live.ellipsis",
)
lines.append(list(console.render(overflow_text)))
shape = Segment.get_shape(lines)
self._shape = shape
new_line = Segment.line()
for last, line in loop_last(lines):
yield from line
if not last:
yield new_line

View file

@ -0,0 +1,289 @@
import logging
from datetime import datetime
from logging import Handler, LogRecord
from pathlib import Path
from types import ModuleType
from typing import ClassVar, Iterable, List, Optional, Type, Union
from pip._vendor.rich._null_file import NullFile
from . import get_console
from ._log_render import FormatTimeCallable, LogRender
from .console import Console, ConsoleRenderable
from .highlighter import Highlighter, ReprHighlighter
from .text import Text
from .traceback import Traceback
class RichHandler(Handler):
"""A logging handler that renders output with Rich. The time / level / message and file are displayed in columns.
The level is color coded, and the message is syntax highlighted.
Note:
Be careful when enabling console markup in log messages if you have configured logging for libraries not
under your control. If a dependency writes messages containing square brackets, it may not produce the intended output.
Args:
level (Union[int, str], optional): Log level. Defaults to logging.NOTSET.
console (:class:`~rich.console.Console`, optional): Optional console instance to write logs.
Default will use a global console instance writing to stdout.
show_time (bool, optional): Show a column for the time. Defaults to True.
omit_repeated_times (bool, optional): Omit repetition of the same time. Defaults to True.
show_level (bool, optional): Show a column for the level. Defaults to True.
show_path (bool, optional): Show the path to the original log call. Defaults to True.
enable_link_path (bool, optional): Enable terminal link of path column to file. Defaults to True.
highlighter (Highlighter, optional): Highlighter to style log messages, or None to use ReprHighlighter. Defaults to None.
markup (bool, optional): Enable console markup in log messages. Defaults to False.
rich_tracebacks (bool, optional): Enable rich tracebacks with syntax highlighting and formatting. Defaults to False.
tracebacks_width (Optional[int], optional): Number of characters used to render tracebacks, or None for full width. Defaults to None.
tracebacks_extra_lines (int, optional): Additional lines of code to render tracebacks, or None for full width. Defaults to None.
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]]] = [
"GET",
"POST",
"HEAD",
"PUT",
"DELETE",
"OPTIONS",
"TRACE",
"PATCH",
]
HIGHLIGHTER_CLASS: ClassVar[Type[Highlighter]] = ReprHighlighter
def __init__(
self,
level: Union[int, str] = logging.NOTSET,
console: Optional[Console] = None,
*,
show_time: bool = True,
omit_repeated_times: bool = True,
show_level: bool = True,
show_path: bool = True,
enable_link_path: bool = True,
highlighter: Optional[Highlighter] = None,
markup: bool = False,
rich_tracebacks: bool = False,
tracebacks_width: Optional[int] = None,
tracebacks_extra_lines: int = 3,
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()
self.highlighter = highlighter or self.HIGHLIGHTER_CLASS()
self._log_render = LogRender(
show_time=show_time,
show_level=show_level,
show_path=show_path,
time_format=log_time_format,
omit_repeated_times=omit_repeated_times,
level_width=None,
)
self.enable_link_path = enable_link_path
self.markup = markup
self.rich_tracebacks = rich_tracebacks
self.tracebacks_width = tracebacks_width
self.tracebacks_extra_lines = tracebacks_extra_lines
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.
Args:
record (LogRecord): LogRecord instance.
Returns:
Text: A tuple of the style and level name.
"""
level_name = record.levelname
level_text = Text.styled(
level_name.ljust(8), f"logging.level.{level_name.lower()}"
)
return level_text
def emit(self, record: LogRecord) -> None:
"""Invoked by logging."""
message = self.format(record)
traceback = None
if (
self.rich_tracebacks
and record.exc_info
and record.exc_info != (None, None, None)
):
exc_type, exc_value, exc_traceback = record.exc_info
assert exc_type is not None
assert exc_value is not None
traceback = Traceback.from_exception(
exc_type,
exc_value,
exc_traceback,
width=self.tracebacks_width,
extra_lines=self.tracebacks_extra_lines,
theme=self.tracebacks_theme,
word_wrap=self.tracebacks_word_wrap,
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:
record.message = record.getMessage()
formatter = self.formatter
if hasattr(formatter, "usesTime") and formatter.usesTime():
record.asctime = formatter.formatTime(record, formatter.datefmt)
message = formatter.formatMessage(record)
message_renderable = self.render_message(record, message)
log_renderable = self.render(
record=record, traceback=traceback, message_renderable=message_renderable
)
if isinstance(self.console.file, NullFile):
# Handles pythonw, where stdout/stderr are null, and we return NullFile
# instance from Console.file. In this case, we still want to make a log record
# even though we won't be writing anything to a file.
self.handleError(record)
else:
try:
self.console.print(log_renderable)
except Exception:
self.handleError(record)
def render_message(self, record: LogRecord, message: str) -> "ConsoleRenderable":
"""Render message text in to Text.
Args:
record (LogRecord): logging Record.
message (str): String containing log message.
Returns:
ConsoleRenderable: Renderable to display log message.
"""
use_markup = getattr(record, "markup", self.markup)
message_text = Text.from_markup(message) if use_markup else Text(message)
highlighter = getattr(record, "highlighter", self.highlighter)
if highlighter:
message_text = highlighter(message_text)
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(
self,
*,
record: LogRecord,
traceback: Optional[Traceback],
message_renderable: "ConsoleRenderable",
) -> "ConsoleRenderable":
"""Render log for display.
Args:
record (LogRecord): logging Record.
traceback (Optional[Traceback]): Traceback instance or None for no Traceback.
message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents.
Returns:
ConsoleRenderable: Renderable to display log.
"""
path = Path(record.pathname).name
level = self.get_level_text(record)
time_format = None if self.formatter is None else self.formatter.datefmt
log_time = datetime.fromtimestamp(record.created)
log_renderable = self._log_render(
self.console,
[message_renderable] if not traceback else [message_renderable, traceback],
log_time=log_time,
time_format=time_format,
level=level,
path=path,
line_no=record.lineno,
link_path=record.pathname if self.enable_link_path else None,
)
return log_renderable
if __name__ == "__main__": # pragma: no cover
from time import sleep
FORMAT = "%(message)s"
# FORMAT = "%(asctime)-15s - %(levelname)s - %(message)s"
logging.basicConfig(
level="NOTSET",
format=FORMAT,
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True, tracebacks_show_locals=True)],
)
log = logging.getLogger("rich")
log.info("Server starting...")
log.info("Listening on http://127.0.0.1:8080")
sleep(1)
log.info("GET /index.html 200 1298")
log.info("GET /imgs/backgrounds/back1.jpg 200 54386")
log.info("GET /css/styles.css 200 54386")
log.warning("GET /favicon.ico 404 242")
sleep(1)
log.debug(
"JSONRPC request\n--> %r\n<-- %r",
{
"version": "1.1",
"method": "confirmFruitPurchase",
"params": [["apple", "orange", "mangoes", "pomelo"], 1.123],
"id": "194521489",
},
{"version": "1.1", "result": True, "error": None, "id": "194521489"},
)
log.debug(
"Loading configuration file /adasd/asdasd/qeqwe/qwrqwrqwr/sdgsdgsdg/werwerwer/dfgerert/ertertert/ertetert/werwerwer"
)
log.error("Unable to find 'pomelo' in database!")
log.info("POST /jsonrpc/ 200 65532")
log.info("POST /admin/ 401 42234")
log.warning("password was rejected for admin site.")
def divide() -> None:
number = 1
divisor = 0
foos = ["foo"] * 100
log.debug("in divide")
try:
number / divisor
except:
log.exception("An error of some kind occurred!")
divide()
sleep(1)
log.critical("Out of memory!")
log.info("Server exited with code=-1")
log.info("[bold]EXITING...[/bold]", extra=dict(markup=True))

View file

@ -0,0 +1,246 @@
import re
from ast import literal_eval
from operator import attrgetter
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
RE_TAGS = re.compile(
r"""((\\*)\[([a-z#/@][^[]*?)])""",
re.VERBOSE,
)
RE_HANDLER = re.compile(r"^([\w.]*?)(\(.*?\))?$")
class Tag(NamedTuple):
"""A tag in console markup."""
name: str
"""The tag name. e.g. 'bold'."""
parameters: Optional[str]
"""Any additional parameters after the name."""
def __str__(self) -> str:
return (
self.name if self.parameters is None else f"{self.name} {self.parameters}"
)
@property
def markup(self) -> str:
"""Get the string representation of this tag."""
return (
f"[{self.name}]"
if self.parameters is None
else f"[{self.name}={self.parameters}]"
)
_ReStringMatch = Match[str] # regex match object
_ReSubCallable = Callable[[_ReStringMatch], str] # Callable invoked by re.sub
_EscapeSubMethod = Callable[[_ReSubCallable, str], str] # Sub method of a compiled re
def escape(
markup: str,
_escape: _EscapeSubMethod = re.compile(r"(\\*)(\[[a-z#/@][^[]*?])").sub,
) -> str:
"""Escapes text so that it won't be interpreted as markup.
Args:
markup (str): Content to be inserted in to markup.
Returns:
str: Markup with square brackets escaped.
"""
def escape_backslashes(match: Match[str]) -> str:
"""Called by re.sub replace matches."""
backslashes, text = match.groups()
return f"{backslashes}{backslashes}\\{text}"
markup = _escape(escape_backslashes, markup)
return markup
def _parse(markup: str) -> Iterable[Tuple[int, Optional[str], Optional[Tag]]]:
"""Parse markup in to an iterable of tuples of (position, text, tag).
Args:
markup (str): A string containing console markup
"""
position = 0
_divmod = divmod
_Tag = Tag
for match in RE_TAGS.finditer(markup):
full_text, escapes, tag_text = match.groups()
start, end = match.span()
if start > position:
yield start, markup[position:start], None
if escapes:
backslashes, escaped = _divmod(len(escapes), 2)
if backslashes:
# Literal backslashes
yield start, "\\" * backslashes, None
start += backslashes * 2
if escaped:
# Escape of tag
yield start, full_text[len(escapes) :], None
position = end
continue
text, equals, parameters = tag_text.partition("=")
yield start, None, _Tag(text, parameters if equals else None)
position = end
if position < len(markup):
yield position, markup[position:], None
def render(
markup: str,
style: Union[str, Style] = "",
emoji: bool = True,
emoji_variant: Optional[EmojiVariant] = None,
) -> Text:
"""Render console markup in to a Text instance.
Args:
markup (str): A string containing console markup.
emoji (bool, optional): Also render emoji code. Defaults to True.
Raises:
MarkupError: If there is a syntax error in the markup.
Returns:
Text: A test instance.
"""
emoji_replace = _emoji_replace
if "[" not in markup:
return Text(
emoji_replace(markup, default_variant=emoji_variant) if emoji else markup,
style=style,
)
text = Text(style=style)
append = text.append
normalize = Style.normalize
style_stack: List[Tuple[int, Tag]] = []
pop = style_stack.pop
spans: List[Span] = []
append_span = spans.append
_Span = Span
_Tag = Tag
def pop_style(style_name: str) -> Tuple[int, Tag]:
"""Pop tag matching given style name."""
for index, (_, tag) in enumerate(reversed(style_stack), 1):
if tag.name == style_name:
return pop(-index)
raise KeyError(style_name)
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
style_name = tag.name[1:].strip()
if style_name: # explicit close
style_name = normalize(style_name)
try:
start, open_tag = pop_style(style_name)
except KeyError:
raise MarkupError(
f"closing tag '{tag.markup}' at position {position} doesn't match any open tag"
) from None
else: # implicit close
try:
start, open_tag = pop()
except IndexError:
raise MarkupError(
f"closing tag '[/]' at position {position} has nothing to close"
) from None
if open_tag.name.startswith("@"):
if open_tag.parameters:
handler_name = ""
parameters = open_tag.parameters.strip()
handler_match = RE_HANDLER.match(parameters)
if handler_match is not None:
handler_name, match_parameters = handler_match.groups()
parameters = (
"()" if match_parameters is None else match_parameters
)
try:
meta_params = literal_eval(parameters)
except SyntaxError as error:
raise MarkupError(
f"error parsing {parameters!r} in {open_tag.parameters!r}; {error.msg}"
)
except Exception as error:
raise MarkupError(
f"error parsing {open_tag.parameters!r}; {error}"
) from None
if handler_name:
meta_params = (
handler_name,
meta_params
if isinstance(meta_params, tuple)
else (meta_params,),
)
else:
meta_params = ()
append_span(
_Span(
start, len(text), Style(meta={open_tag.name: meta_params})
)
)
else:
append_span(_Span(start, len(text), str(open_tag)))
else: # Opening tag
normalized_tag = _Tag(normalize(tag.name), tag.parameters)
style_stack.append((len(text), normalized_tag))
text_length = len(text)
while style_stack:
start, tag = style_stack.pop()
style = str(tag)
if style:
append_span(_Span(start, text_length, style))
text.spans = sorted(spans[::-1], key=attrgetter("start"))
return text
if __name__ == "__main__": # pragma: no cover
MARKUP = [
"[red]Hello World[/red]",
"[magenta]Hello [b]World[/b]",
"[bold]Bold[italic] bold and italic [/bold]italic[/italic]",
"Click [link=https://www.willmcgugan.com]here[/link] to visit my Blog",
":warning-emoji: [bold red blink] DANGER![/]",
]
from pip._vendor.rich import print
from pip._vendor.rich.table import Table
grid = Table("Markup", "Result", padding=(0, 1))
for markup in MARKUP:
grid.add_row(Text(markup), markup)
print(grid)

View file

@ -0,0 +1,151 @@
from operator import itemgetter
from typing import TYPE_CHECKING, Callable, NamedTuple, Optional, Sequence
from . import errors
from .protocol import is_renderable, rich_cast
if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderableType
class Measurement(NamedTuple):
"""Stores the minimum and maximum widths (in characters) required to render an object."""
minimum: int
"""Minimum number of cells required to render."""
maximum: int
"""Maximum number of cells required to render."""
@property
def span(self) -> int:
"""Get difference between maximum and minimum."""
return self.maximum - self.minimum
def normalize(self) -> "Measurement":
"""Get measurement that ensures that minimum <= maximum and minimum >= 0
Returns:
Measurement: A normalized measurement.
"""
minimum, maximum = self
minimum = min(max(0, minimum), maximum)
return Measurement(max(0, minimum), max(0, max(minimum, maximum)))
def with_maximum(self, width: int) -> "Measurement":
"""Get a RenderableWith where the widths are <= width.
Args:
width (int): Maximum desired width.
Returns:
Measurement: New Measurement object.
"""
minimum, maximum = self
return Measurement(min(minimum, width), min(maximum, width))
def with_minimum(self, width: int) -> "Measurement":
"""Get a RenderableWith where the widths are >= width.
Args:
width (int): Minimum desired width.
Returns:
Measurement: New Measurement object.
"""
minimum, maximum = self
width = max(0, width)
return Measurement(max(minimum, width), max(maximum, width))
def clamp(
self, min_width: Optional[int] = None, max_width: Optional[int] = None
) -> "Measurement":
"""Clamp a measurement within the specified range.
Args:
min_width (int): Minimum desired width, or ``None`` for no minimum. Defaults to None.
max_width (int): Maximum desired width, or ``None`` for no maximum. Defaults to None.
Returns:
Measurement: New Measurement object.
"""
measurement = self
if min_width is not None:
measurement = measurement.with_minimum(min_width)
if max_width is not None:
measurement = measurement.with_maximum(max_width)
return measurement
@classmethod
def get(
cls, console: "Console", options: "ConsoleOptions", renderable: "RenderableType"
) -> "Measurement":
"""Get a measurement for a renderable.
Args:
console (~rich.console.Console): Console instance.
options (~rich.console.ConsoleOptions): Console options.
renderable (RenderableType): An object that may be rendered with Rich.
Raises:
errors.NotRenderableError: If the object is not renderable.
Returns:
Measurement: Measurement object containing range of character widths required to render the object.
"""
_max_width = options.max_width
if _max_width < 1:
return Measurement(0, 0)
if isinstance(renderable, str):
renderable = console.render_str(
renderable, markup=options.markup, highlight=False
)
renderable = rich_cast(renderable)
if is_renderable(renderable):
get_console_width: Optional[
Callable[["Console", "ConsoleOptions"], "Measurement"]
] = getattr(renderable, "__rich_measure__", None)
if get_console_width is not None:
render_width = (
get_console_width(console, options)
.normalize()
.with_maximum(_max_width)
)
if render_width.maximum < 1:
return Measurement(0, 0)
return render_width.normalize()
else:
return Measurement(0, _max_width)
else:
raise errors.NotRenderableError(
f"Unable to get render width for {renderable!r}; "
"a str, Segment, or object with __rich_console__ method is required"
)
def measure_renderables(
console: "Console",
options: "ConsoleOptions",
renderables: Sequence["RenderableType"],
) -> "Measurement":
"""Get a measurement that would fit a number of renderables.
Args:
console (~rich.console.Console): Console instance.
options (~rich.console.ConsoleOptions): Console options.
renderables (Iterable[RenderableType]): One or more renderable objects.
Returns:
Measurement: Measurement object containing range of character widths required to
contain all given renderables.
"""
if not renderables:
return Measurement(0, 0)
get_measurement = Measurement.get
measurements = [
get_measurement(console, options, renderable) for renderable in renderables
]
measured_width = Measurement(
max(measurements, key=itemgetter(0)).minimum,
max(measurements, key=itemgetter(1)).maximum,
)
return measured_width

View file

@ -0,0 +1,141 @@
from typing import cast, List, Optional, Tuple, TYPE_CHECKING, Union
if TYPE_CHECKING:
from .console import (
Console,
ConsoleOptions,
RenderableType,
RenderResult,
)
from .jupyter import JupyterMixin
from .measure import Measurement
from .style import Style
from .segment import Segment
PaddingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]]
class Padding(JupyterMixin):
"""Draw space around content.
Example:
>>> print(Padding("Hello", (2, 4), style="on blue"))
Args:
renderable (RenderableType): String or other renderable.
pad (Union[int, Tuple[int]]): Padding for top, right, bottom, and left borders.
May be specified with 1, 2, or 4 integers (CSS style).
style (Union[str, Style], optional): Style for padding characters. Defaults to "none".
expand (bool, optional): Expand padding to fit available width. Defaults to True.
"""
def __init__(
self,
renderable: "RenderableType",
pad: "PaddingDimensions" = (0, 0, 0, 0),
*,
style: Union[str, Style] = "none",
expand: bool = True,
):
self.renderable = renderable
self.top, self.right, self.bottom, self.left = self.unpack(pad)
self.style = style
self.expand = expand
@classmethod
def indent(cls, renderable: "RenderableType", level: int) -> "Padding":
"""Make padding instance to render an indent.
Args:
renderable (RenderableType): String or other renderable.
level (int): Number of characters to indent.
Returns:
Padding: A Padding instance.
"""
return Padding(renderable, pad=(0, 0, 0, level), expand=False)
@staticmethod
def unpack(pad: "PaddingDimensions") -> Tuple[int, int, int, int]:
"""Unpack padding specified in CSS style."""
if isinstance(pad, int):
return (pad, pad, pad, pad)
if len(pad) == 1:
_pad = pad[0]
return (_pad, _pad, _pad, _pad)
if len(pad) == 2:
pad_top, pad_right = cast(Tuple[int, int], pad)
return (pad_top, pad_right, pad_top, pad_right)
if len(pad) == 4:
top, right, bottom, left = cast(Tuple[int, int, int, int], pad)
return (top, right, bottom, left)
raise ValueError(f"1, 2 or 4 integers required for padding; {len(pad)} given")
def __repr__(self) -> str:
return f"Padding({self.renderable!r}, ({self.top},{self.right},{self.bottom},{self.left}))"
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
style = console.get_style(self.style)
if self.expand:
width = options.max_width
else:
width = min(
Measurement.get(console, options, self.renderable).maximum
+ self.left
+ self.right,
options.max_width,
)
render_options = options.update_width(width - self.left - self.right)
if render_options.height is not None:
render_options = render_options.update_height(
height=render_options.height - self.top - self.bottom
)
lines = console.render_lines(
self.renderable, render_options, style=style, pad=True
)
_Segment = Segment
left = _Segment(" " * self.left, style) if self.left else None
right = (
[_Segment(f'{" " * self.right}', style), _Segment.line()]
if self.right
else [_Segment.line()]
)
blank_line: Optional[List[Segment]] = None
if self.top:
blank_line = [_Segment(f'{" " * width}\n', style)]
yield from blank_line * self.top
if left:
for line in lines:
yield left
yield from line
yield from right
else:
for line in lines:
yield from line
yield from right
if self.bottom:
blank_line = blank_line or [_Segment(f'{" " * width}\n', style)]
yield from blank_line * self.bottom
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
max_width = options.max_width
extra_width = self.left + self.right
if max_width - extra_width < 1:
return Measurement(max_width, max_width)
measure_min, measure_max = Measurement.get(console, options, self.renderable)
measurement = Measurement(measure_min + extra_width, measure_max + extra_width)
measurement = measurement.with_maximum(max_width)
return measurement
if __name__ == "__main__": # pragma: no cover
from pip._vendor.rich import print
print(Padding("Hello, World", (2, 4), style="on blue"))

View file

@ -0,0 +1,34 @@
from abc import ABC, abstractmethod
from typing import Any
class Pager(ABC):
"""Base class for a pager."""
@abstractmethod
def show(self, content: str) -> None:
"""Show content in pager.
Args:
content (str): Content to be displayed.
"""
class SystemPager(Pager):
"""Uses the pager installed on the system."""
def _pager(self, content: str) -> Any: #  pragma: no cover
return __import__("pydoc").pager(content)
def show(self, content: str) -> None:
"""Use the same pager used by pydoc."""
self._pager(content)
if __name__ == "__main__": # pragma: no cover
from .__main__ import make_test_card
from .console import Console
console = Console()
with console.pager(styles=True):
console.print(make_test_card())

View file

@ -0,0 +1,100 @@
from math import sqrt
from functools import lru_cache
from typing import Sequence, Tuple, TYPE_CHECKING
from .color_triplet import ColorTriplet
if TYPE_CHECKING:
from pip._vendor.rich.table import Table
class Palette:
"""A palette of available colors."""
def __init__(self, colors: Sequence[Tuple[int, int, int]]):
self._colors = colors
def __getitem__(self, number: int) -> ColorTriplet:
return ColorTriplet(*self._colors[number])
def __rich__(self) -> "Table":
from pip._vendor.rich.color import Color
from pip._vendor.rich.style import Style
from pip._vendor.rich.text import Text
from pip._vendor.rich.table import Table
table = Table(
"index",
"RGB",
"Color",
title="Palette",
caption=f"{len(self._colors)} colors",
highlight=True,
caption_justify="right",
)
for index, color in enumerate(self._colors):
table.add_row(
str(index),
repr(color),
Text(" " * 16, style=Style(bgcolor=Color.from_rgb(*color))),
)
return table
# This is somewhat inefficient and needs caching
@lru_cache(maxsize=1024)
def match(self, color: Tuple[int, int, int]) -> int:
"""Find a color from a palette that most closely matches a given color.
Args:
color (Tuple[int, int, int]): RGB components in range 0 > 255.
Returns:
int: Index of closes matching color.
"""
red1, green1, blue1 = color
_sqrt = sqrt
get_color = self._colors.__getitem__
def get_color_distance(index: int) -> float:
"""Get the distance to a color."""
red2, green2, blue2 = get_color(index)
red_mean = (red1 + red2) // 2
red = red1 - red2
green = green1 - green2
blue = blue1 - blue2
return _sqrt(
(((512 + red_mean) * red * red) >> 8)
+ 4 * green * green
+ (((767 - red_mean) * blue * blue) >> 8)
)
min_index = min(range(len(self._colors)), key=get_color_distance)
return min_index
if __name__ == "__main__": # pragma: no cover
import colorsys
from typing import Iterable
from pip._vendor.rich.color import Color
from pip._vendor.rich.console import Console, ConsoleOptions
from pip._vendor.rich.segment import Segment
from pip._vendor.rich.style import Style
class ColorBox:
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> Iterable[Segment]:
height = console.size.height - 3
for y in range(0, height):
for x in range(options.max_width):
h = x / options.max_width
l = y / (height + 1)
r1, g1, b1 = colorsys.hls_to_rgb(h, l, 1.0)
r2, g2, b2 = colorsys.hls_to_rgb(h, l + (1 / height / 2), 1.0)
bgcolor = Color.from_rgb(r1 * 255, g1 * 255, b1 * 255)
color = Color.from_rgb(r2 * 255, g2 * 255, b2 * 255)
yield Segment("", Style(color=color, bgcolor=bgcolor))
yield Segment.line()
console = Console()
console.print(ColorBox())

View file

@ -0,0 +1,308 @@
from typing import TYPE_CHECKING, Optional
from .align import AlignMethod
from .box import ROUNDED, Box
from .cells import cell_len
from .jupyter import JupyterMixin
from .measure import Measurement, measure_renderables
from .padding import Padding, PaddingDimensions
from .segment import Segment
from .style import Style, StyleType
from .text import Text, TextType
if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderableType, RenderResult
class Panel(JupyterMixin):
"""A console renderable that draws a border around its contents.
Example:
>>> console.print(Panel("Hello, World!"))
Args:
renderable (RenderableType): A console renderable object.
box (Box, optional): A Box instance that defines the look of the border (see :ref:`appendix_box`.
Defaults to box.ROUNDED.
safe_box (bool, optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
expand (bool, optional): If True the panel will stretch to fill the console
width, otherwise it will be sized to fit the contents. Defaults to True.
style (str, optional): The style of the panel (border and contents). Defaults to "none".
border_style (str, optional): The style of the border. Defaults to "none".
width (Optional[int], optional): Optional width of panel. Defaults to None to auto-detect.
height (Optional[int], optional): Optional height of panel. Defaults to None to auto-detect.
padding (Optional[PaddingDimensions]): Optional padding around renderable. Defaults to 0.
highlight (bool, optional): Enable automatic highlighting of panel title (if str). Defaults to False.
"""
def __init__(
self,
renderable: "RenderableType",
box: Box = ROUNDED,
*,
title: Optional[TextType] = None,
title_align: AlignMethod = "center",
subtitle: Optional[TextType] = None,
subtitle_align: AlignMethod = "center",
safe_box: Optional[bool] = None,
expand: bool = True,
style: StyleType = "none",
border_style: StyleType = "none",
width: Optional[int] = None,
height: Optional[int] = None,
padding: PaddingDimensions = (0, 1),
highlight: bool = False,
) -> None:
self.renderable = renderable
self.box = box
self.title = title
self.title_align: AlignMethod = title_align
self.subtitle = subtitle
self.subtitle_align = subtitle_align
self.safe_box = safe_box
self.expand = expand
self.style = style
self.border_style = border_style
self.width = width
self.height = height
self.padding = padding
self.highlight = highlight
@classmethod
def fit(
cls,
renderable: "RenderableType",
box: Box = ROUNDED,
*,
title: Optional[TextType] = None,
title_align: AlignMethod = "center",
subtitle: Optional[TextType] = None,
subtitle_align: AlignMethod = "center",
safe_box: Optional[bool] = None,
style: StyleType = "none",
border_style: StyleType = "none",
width: Optional[int] = None,
padding: PaddingDimensions = (0, 1),
) -> "Panel":
"""An alternative constructor that sets expand=False."""
return cls(
renderable,
box,
title=title,
title_align=title_align,
subtitle=subtitle,
subtitle_align=subtitle_align,
safe_box=safe_box,
style=style,
border_style=border_style,
width=width,
padding=padding,
expand=False,
)
@property
def _title(self) -> Optional[Text]:
if self.title:
title_text = (
Text.from_markup(self.title)
if isinstance(self.title, str)
else self.title.copy()
)
title_text.end = ""
title_text.plain = title_text.plain.replace("\n", " ")
title_text.no_wrap = True
title_text.expand_tabs()
title_text.pad(1)
return title_text
return None
@property
def _subtitle(self) -> Optional[Text]:
if self.subtitle:
subtitle_text = (
Text.from_markup(self.subtitle)
if isinstance(self.subtitle, str)
else self.subtitle.copy()
)
subtitle_text.end = ""
subtitle_text.plain = subtitle_text.plain.replace("\n", " ")
subtitle_text.no_wrap = True
subtitle_text.expand_tabs()
subtitle_text.pad(1)
return subtitle_text
return None
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
_padding = Padding.unpack(self.padding)
renderable = (
Padding(self.renderable, _padding) if any(_padding) else self.renderable
)
style = console.get_style(self.style)
border_style = style + console.get_style(self.border_style)
width = (
options.max_width
if self.width is None
else min(options.max_width, self.width)
)
safe_box: bool = console.safe_box if self.safe_box is None else self.safe_box
box = self.box.substitute(options, safe=safe_box)
def align_text(
text: Text, width: int, align: str, character: str, style: Style
) -> Text:
"""Gets new aligned text.
Args:
text (Text): Title or subtitle text.
width (int): Desired width.
align (str): Alignment.
character (str): Character for alignment.
style (Style): Border style
Returns:
Text: New text instance
"""
text = text.copy()
text.truncate(width)
excess_space = width - cell_len(text.plain)
if excess_space:
if align == "left":
return Text.assemble(
text,
(character * excess_space, style),
no_wrap=True,
end="",
)
elif align == "center":
left = excess_space // 2
return Text.assemble(
(character * left, style),
text,
(character * (excess_space - left), style),
no_wrap=True,
end="",
)
else:
return Text.assemble(
(character * excess_space, style),
text,
no_wrap=True,
end="",
)
return text
title_text = self._title
if title_text is not None:
title_text.stylize_before(border_style)
child_width = (
width - 2
if self.expand
else console.measure(
renderable, options=options.update_width(width - 2)
).maximum
)
child_height = self.height or options.height or None
if child_height:
child_height -= 2
if title_text is not None:
child_width = min(
options.max_width - 2, max(child_width, title_text.cell_len + 2)
)
width = child_width + 2
child_options = options.update(
width=child_width, height=child_height, highlight=self.highlight
)
lines = console.render_lines(renderable, child_options, style=style)
line_start = Segment(box.mid_left, border_style)
line_end = Segment(f"{box.mid_right}", border_style)
new_line = Segment.line()
if title_text is None or width <= 4:
yield Segment(box.get_top([width - 2]), border_style)
else:
title_text = align_text(
title_text,
width - 4,
self.title_align,
box.top,
border_style,
)
yield Segment(box.top_left + box.top, border_style)
yield from console.render(title_text, child_options.update_width(width - 4))
yield Segment(box.top + box.top_right, border_style)
yield new_line
for line in lines:
yield line_start
yield from line
yield line_end
yield new_line
subtitle_text = self._subtitle
if subtitle_text is not None:
subtitle_text.stylize_before(border_style)
if subtitle_text is None or width <= 4:
yield Segment(box.get_bottom([width - 2]), border_style)
else:
subtitle_text = align_text(
subtitle_text,
width - 4,
self.subtitle_align,
box.bottom,
border_style,
)
yield Segment(box.bottom_left + box.bottom, border_style)
yield from console.render(
subtitle_text, child_options.update_width(width - 4)
)
yield Segment(box.bottom + box.bottom_right, border_style)
yield new_line
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
_title = self._title
_, right, _, left = Padding.unpack(self.padding)
padding = left + right
renderables = [self.renderable, _title] if _title else [self.renderable]
if self.width is None:
width = (
measure_renderables(
console,
options.update_width(options.max_width - padding - 2),
renderables,
).maximum
+ padding
+ 2
)
else:
width = self.width
return Measurement(width, width)
if __name__ == "__main__": # pragma: no cover
from .console import Console
c = Console()
from .box import DOUBLE, ROUNDED
from .padding import Padding
p = Panel(
"Hello, World!",
title="rich.Panel",
style="white on blue",
box=DOUBLE,
padding=1,
)
c.print()
c.print(p)

View file

@ -0,0 +1,994 @@
import builtins
import collections
import dataclasses
import inspect
import os
import sys
from array import array
from collections import Counter, UserDict, UserList, defaultdict, deque
from dataclasses import dataclass, fields, is_dataclass
from inspect import isclass
from itertools import islice
from types import MappingProxyType
from typing import (
TYPE_CHECKING,
Any,
Callable,
DefaultDict,
Dict,
Iterable,
List,
Optional,
Sequence,
Set,
Tuple,
Union,
)
from pip._vendor.rich.repr import RichReprResult
try:
import attr as _attr_module
_has_attrs = hasattr(_attr_module, "ib")
except ImportError: # pragma: no cover
_has_attrs = False
from . import get_console
from ._loop import loop_last
from ._pick import pick_bool
from .abc import RichRenderable
from .cells import cell_len
from .highlighter import ReprHighlighter
from .jupyter import JupyterMixin, JupyterRenderable
from .measure import Measurement
from .text import Text
if TYPE_CHECKING:
from .console import (
Console,
ConsoleOptions,
HighlighterType,
JustifyMethod,
OverflowMethod,
RenderResult,
)
def _is_attr_object(obj: Any) -> bool:
"""Check if an object was created with attrs module."""
return _has_attrs and _attr_module.has(type(obj))
def _get_attr_fields(obj: Any) -> Sequence["_attr_module.Attribute[Any]"]:
"""Get fields for an attrs object."""
return _attr_module.fields(type(obj)) if _has_attrs else []
def _is_dataclass_repr(obj: object) -> bool:
"""Check if an instance of a dataclass contains the default repr.
Args:
obj (object): A dataclass instance.
Returns:
bool: True if the default repr is used, False if there is a custom repr.
"""
# Digging in to a lot of internals here
# Catching all exceptions in case something is missing on a non CPython implementation
try:
return obj.__repr__.__code__.co_filename == dataclasses.__file__
except Exception: # pragma: no coverage
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,
overflow: "OverflowMethod" = "ignore",
crop: bool = False,
indent_guides: bool = False,
max_length: Optional[int] = None,
max_string: Optional[int] = None,
max_depth: Optional[int] = None,
expand_all: bool = False,
) -> Union[str, None]:
# needed here to prevent circular import:
from .console import ConsoleRenderable
# always skip rich generated jupyter renderables or None values
if _safe_isinstance(value, JupyterRenderable) or value is None:
return None
console = console or get_console()
with console.capture() as capture:
# certain renderables should start on a new line
if _safe_isinstance(value, ConsoleRenderable):
console.line()
console.print(
value
if _safe_isinstance(value, RichRenderable)
else Pretty(
value,
overflow=overflow,
indent_guides=indent_guides,
max_length=max_length,
max_string=max_string,
max_depth=max_depth,
expand_all=expand_all,
margin=12,
),
crop=crop,
new_line_start=True,
end="",
)
# strip trailing newline, not usually part of a text repr
# I'm not sure if this should be prevented at a lower level
return capture.get().rstrip("\n")
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",
crop: bool = False,
indent_guides: bool = False,
max_length: Optional[int] = None,
max_string: Optional[int] = None,
max_depth: Optional[int] = None,
expand_all: bool = False,
) -> None:
"""Install automatic pretty printing in the Python REPL.
Args:
console (Console, optional): Console instance or ``None`` to use global console. Defaults to None.
overflow (Optional[OverflowMethod], optional): Overflow method. Defaults to "ignore".
crop (Optional[bool], optional): Enable cropping of long lines. Defaults to False.
indent_guides (bool, optional): Enable indentation guides. Defaults to False.
max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to None.
max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None.
max_depth (int, optional): Maximum depth of nested data structures, or None for no maximum. Defaults to None.
expand_all (bool, optional): Expand all containers. Defaults to False.
max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
"""
from pip._vendor.rich import get_console
console = console or get_console()
assert console is not None
def display_hook(value: Any) -> None:
"""Replacement sys.displayhook which prettifies objects with Rich."""
if value is not None:
assert console is not None
builtins._ = None # type: ignore[attr-defined]
console.print(
value
if _safe_isinstance(value, RichRenderable)
else Pretty(
value,
overflow=overflow,
indent_guides=indent_guides,
max_length=max_length,
max_string=max_string,
max_depth=max_depth,
expand_all=expand_all,
),
crop=crop,
)
builtins._ = value # type: ignore[attr-defined]
if "get_ipython" in globals():
ip = get_ipython() # type: ignore[name-defined]
from IPython.core.formatters import BaseFormatter
class RichFormatter(BaseFormatter): # type: ignore[misc]
pprint: bool = True
def __call__(self, value: Any) -> Any:
if self.pprint:
return _ipy_display_hook(
value,
console=get_console(),
overflow=overflow,
indent_guides=indent_guides,
max_length=max_length,
max_string=max_string,
max_depth=max_depth,
expand_all=expand_all,
)
else:
return repr(value)
# replace plain text formatter with rich formatter
rich_formatter = RichFormatter()
ip.display_formatter.formatters["text/plain"] = rich_formatter
else:
sys.displayhook = display_hook
class Pretty(JupyterMixin):
"""A rich renderable that pretty prints an object.
Args:
_object (Any): An object to pretty print.
highlighter (HighlighterType, optional): Highlighter object to apply to result, or None for ReprHighlighter. Defaults to None.
indent_size (int, optional): Number of spaces in indent. Defaults to 4.
justify (JustifyMethod, optional): Justify method, or None for default. Defaults to None.
overflow (OverflowMethod, optional): Overflow method, or None for default. Defaults to None.
no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to False.
indent_guides (bool, optional): Enable indentation guides. Defaults to False.
max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to None.
max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None.
max_depth (int, optional): Maximum depth of nested data structures, or None for no maximum. Defaults to None.
expand_all (bool, optional): Expand all containers. Defaults to False.
margin (int, optional): Subtrace a margin from width to force containers to expand earlier. Defaults to 0.
insert_line (bool, optional): Insert a new line if the output has multiple new lines. Defaults to False.
"""
def __init__(
self,
_object: Any,
highlighter: Optional["HighlighterType"] = None,
*,
indent_size: int = 4,
justify: Optional["JustifyMethod"] = None,
overflow: Optional["OverflowMethod"] = None,
no_wrap: Optional[bool] = False,
indent_guides: bool = False,
max_length: Optional[int] = None,
max_string: Optional[int] = None,
max_depth: Optional[int] = None,
expand_all: bool = False,
margin: int = 0,
insert_line: bool = False,
) -> None:
self._object = _object
self.highlighter = highlighter or ReprHighlighter()
self.indent_size = indent_size
self.justify: Optional["JustifyMethod"] = justify
self.overflow: Optional["OverflowMethod"] = overflow
self.no_wrap = no_wrap
self.indent_guides = indent_guides
self.max_length = max_length
self.max_string = max_string
self.max_depth = max_depth
self.expand_all = expand_all
self.margin = margin
self.insert_line = insert_line
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
pretty_str = pretty_repr(
self._object,
max_width=options.max_width - self.margin,
indent_size=self.indent_size,
max_length=self.max_length,
max_string=self.max_string,
max_depth=self.max_depth,
expand_all=self.expand_all,
)
pretty_text = Text.from_ansi(
pretty_str,
justify=self.justify or options.justify,
overflow=self.overflow or options.overflow,
no_wrap=pick_bool(self.no_wrap, options.no_wrap),
style="pretty",
)
pretty_text = (
self.highlighter(pretty_text)
if pretty_text
else Text(
f"{type(self._object)}.__repr__ returned empty string",
style="dim italic",
)
)
if self.indent_guides and not options.ascii_only:
pretty_text = pretty_text.with_indent_guides(
self.indent_size, style="repr.indent"
)
if self.insert_line and "\n" in pretty_text:
yield ""
yield pretty_text
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
pretty_str = pretty_repr(
self._object,
max_width=options.max_width,
indent_size=self.indent_size,
max_length=self.max_length,
max_string=self.max_string,
max_depth=self.max_depth,
expand_all=self.expand_all,
)
text_width = (
max(cell_len(line) for line in pretty_str.splitlines()) if pretty_str else 0
)
return Measurement(text_width, text_width)
def _get_braces_for_defaultdict(_object: DefaultDict[Any, Any]) -> Tuple[str, str, str]:
return (
f"defaultdict({_object.default_factory!r}, {{",
"})",
f"defaultdict({_object.default_factory!r}, {{}})",
)
def _get_braces_for_array(_object: "array[Any]") -> Tuple[str, str, str]:
return (f"array({_object.typecode!r}, [", "])", f"array({_object.typecode!r})")
_BRACES: Dict[type, Callable[[Any], Tuple[str, str, str]]] = {
os._Environ: lambda _object: ("environ({", "})", "environ({})"),
array: _get_braces_for_array,
defaultdict: _get_braces_for_defaultdict,
Counter: lambda _object: ("Counter({", "})", "Counter()"),
deque: lambda _object: ("deque([", "])", "deque()"),
dict: lambda _object: ("{", "}", "{}"),
UserDict: lambda _object: ("{", "}", "{}"),
frozenset: lambda _object: ("frozenset({", "})", "frozenset()"),
list: lambda _object: ("[", "]", "[]"),
UserList: lambda _object: ("[", "]", "[]"),
set: lambda _object: ("{", "}", "set()"),
tuple: lambda _object: ("(", ")", "()"),
MappingProxyType: lambda _object: ("mappingproxy({", "})", "mappingproxy({})"),
}
_CONTAINERS = tuple(_BRACES.keys())
_MAPPING_CONTAINERS = (dict, os._Environ, MappingProxyType, UserDict)
def is_expandable(obj: Any) -> bool:
"""Check if an object may be expanded by pretty print."""
return (
_safe_isinstance(obj, _CONTAINERS)
or (is_dataclass(obj))
or (hasattr(obj, "__rich_repr__"))
or _is_attr_object(obj)
) and not isclass(obj)
@dataclass
class Node:
"""A node in a repr tree. May be atomic or a container."""
key_repr: str = ""
value_repr: str = ""
open_brace: str = ""
close_brace: str = ""
empty: str = ""
last: bool = False
is_tuple: bool = False
is_namedtuple: bool = False
children: Optional[List["Node"]] = None
key_separator: str = ": "
separator: str = ", "
def iter_tokens(self) -> Iterable[str]:
"""Generate tokens for this node."""
if self.key_repr:
yield self.key_repr
yield self.key_separator
if self.value_repr:
yield self.value_repr
elif self.children is not None:
if self.children:
yield self.open_brace
if self.is_tuple and not self.is_namedtuple and len(self.children) == 1:
yield from self.children[0].iter_tokens()
yield ","
else:
for child in self.children:
yield from child.iter_tokens()
if not child.last:
yield self.separator
yield self.close_brace
else:
yield self.empty
def check_length(self, start_length: int, max_length: int) -> bool:
"""Check the length fits within a limit.
Args:
start_length (int): Starting length of the line (indent, prefix, suffix).
max_length (int): Maximum length.
Returns:
bool: True if the node can be rendered within max length, otherwise False.
"""
total_length = start_length
for token in self.iter_tokens():
total_length += cell_len(token)
if total_length > max_length:
return False
return True
def __str__(self) -> str:
repr_text = "".join(self.iter_tokens())
return repr_text
def render(
self, max_width: int = 80, indent_size: int = 4, expand_all: bool = False
) -> str:
"""Render the node to a pretty repr.
Args:
max_width (int, optional): Maximum width of the repr. Defaults to 80.
indent_size (int, optional): Size of indents. Defaults to 4.
expand_all (bool, optional): Expand all levels. Defaults to False.
Returns:
str: A repr string of the original object.
"""
lines = [_Line(node=self, is_root=True)]
line_no = 0
while line_no < len(lines):
line = lines[line_no]
if line.expandable and not line.expanded:
if expand_all or not line.check_length(max_width):
lines[line_no : line_no + 1] = line.expand(indent_size)
line_no += 1
repr_str = "\n".join(str(line) for line in lines)
return repr_str
@dataclass
class _Line:
"""A line in repr output."""
parent: Optional["_Line"] = None
is_root: bool = False
node: Optional[Node] = None
text: str = ""
suffix: str = ""
whitespace: str = ""
expanded: bool = False
last: bool = False
@property
def expandable(self) -> bool:
"""Check if the line may be expanded."""
return bool(self.node is not None and self.node.children)
def check_length(self, max_length: int) -> bool:
"""Check this line fits within a given number of cells."""
start_length = (
len(self.whitespace) + cell_len(self.text) + cell_len(self.suffix)
)
assert self.node is not None
return self.node.check_length(start_length, max_length)
def expand(self, indent_size: int) -> Iterable["_Line"]:
"""Expand this line by adding children on their own line."""
node = self.node
assert node is not None
whitespace = self.whitespace
assert node.children
if node.key_repr:
new_line = yield _Line(
text=f"{node.key_repr}{node.key_separator}{node.open_brace}",
whitespace=whitespace,
)
else:
new_line = yield _Line(text=node.open_brace, whitespace=whitespace)
child_whitespace = self.whitespace + " " * indent_size
tuple_of_one = node.is_tuple and len(node.children) == 1
for last, child in loop_last(node.children):
separator = "," if tuple_of_one else node.separator
line = _Line(
parent=new_line,
node=child,
whitespace=child_whitespace,
suffix=separator,
last=last and not tuple_of_one,
)
yield line
yield _Line(
text=node.close_brace,
whitespace=whitespace,
suffix=self.suffix,
last=self.last,
)
def __str__(self) -> str:
if self.last:
return f"{self.whitespace}{self.text}{self.node or ''}"
else:
return (
f"{self.whitespace}{self.text}{self.node or ''}{self.suffix.rstrip()}"
)
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,
max_string: Optional[int] = None,
max_depth: Optional[int] = None,
) -> Node:
"""Traverse object and generate a tree.
Args:
_object (Any): Object to be traversed.
max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to None.
max_string (int, optional): Maximum length of string before truncating, or None to disable truncating.
Defaults to None.
max_depth (int, optional): Maximum depth of data structures, or None for no maximum.
Defaults to None.
Returns:
Node: The root of a tree structure which can be used to render a pretty repr.
"""
def to_repr(obj: Any) -> str:
"""Get repr string for an object, but catch errors."""
if (
max_string is not None
and _safe_isinstance(obj, (bytes, str))
and len(obj) > max_string
):
truncated = len(obj) - max_string
obj_repr = f"{obj[:max_string]!r}+{truncated}"
else:
try:
obj_repr = repr(obj)
except Exception as error:
obj_repr = f"<repr-error {str(error)!r}>"
return obj_repr
visited_ids: Set[int] = set()
push_visited = visited_ids.add
pop_visited = visited_ids.remove
def _traverse(obj: Any, root: bool = False, depth: int = 0) -> Node:
"""Walk the object depth first."""
obj_id = id(obj)
if obj_id in visited_ids:
# Recursion detected
return Node(value_repr="...")
obj_type = type(obj)
children: List[Node]
reached_max_depth = max_depth is not None and depth >= max_depth
def iter_rich_args(rich_args: Any) -> Iterable[Union[Any, Tuple[str, Any]]]:
for arg in rich_args:
if _safe_isinstance(arg, tuple):
if len(arg) == 3:
key, child, default = arg
if default == child:
continue
yield key, child
elif len(arg) == 2:
key, child = arg
yield key, child
elif len(arg) == 1:
yield arg[0]
else:
yield arg
try:
fake_attributes = hasattr(
obj, "awehoi234_wdfjwljet234_234wdfoijsdfmmnxpi492"
)
except Exception:
fake_attributes = False
rich_repr_result: Optional[RichReprResult] = None
if not fake_attributes:
try:
if hasattr(obj, "__rich_repr__") and not isclass(obj):
rich_repr_result = obj.__rich_repr__()
except Exception:
pass
if rich_repr_result is not None:
push_visited(obj_id)
angular = getattr(obj.__rich_repr__, "angular", False)
args = list(iter_rich_args(rich_repr_result))
class_name = obj.__class__.__name__
if args:
children = []
append = children.append
if reached_max_depth:
if angular:
node = Node(value_repr=f"<{class_name}...>")
else:
node = Node(value_repr=f"{class_name}(...)")
else:
if angular:
node = Node(
open_brace=f"<{class_name} ",
close_brace=">",
children=children,
last=root,
separator=" ",
)
else:
node = Node(
open_brace=f"{class_name}(",
close_brace=")",
children=children,
last=root,
)
for last, arg in loop_last(args):
if _safe_isinstance(arg, tuple):
key, child = arg
child_node = _traverse(child, depth=depth + 1)
child_node.last = last
child_node.key_repr = key
child_node.key_separator = "="
append(child_node)
else:
child_node = _traverse(arg, depth=depth + 1)
child_node.last = last
append(child_node)
else:
node = Node(
value_repr=f"<{class_name}>" if angular else f"{class_name}()",
children=[],
last=root,
)
pop_visited(obj_id)
elif _is_attr_object(obj) and not fake_attributes:
push_visited(obj_id)
children = []
append = children.append
attr_fields = _get_attr_fields(obj)
if attr_fields:
if reached_max_depth:
node = Node(value_repr=f"{obj.__class__.__name__}(...)")
else:
node = Node(
open_brace=f"{obj.__class__.__name__}(",
close_brace=")",
children=children,
last=root,
)
def iter_attrs() -> Iterable[
Tuple[str, Any, Optional[Callable[[Any], str]]]
]:
"""Iterate over attr fields and values."""
for attr in attr_fields:
if attr.repr:
try:
value = getattr(obj, attr.name)
except Exception as error:
# Can happen, albeit rarely
yield (attr.name, error, None)
else:
yield (
attr.name,
value,
attr.repr if callable(attr.repr) else None,
)
for last, (name, value, repr_callable) in loop_last(iter_attrs()):
if repr_callable:
child_node = Node(value_repr=str(repr_callable(value)))
else:
child_node = _traverse(value, depth=depth + 1)
child_node.last = last
child_node.key_repr = name
child_node.key_separator = "="
append(child_node)
else:
node = Node(
value_repr=f"{obj.__class__.__name__}()", children=[], last=root
)
pop_visited(obj_id)
elif (
is_dataclass(obj)
and not _safe_isinstance(obj, type)
and not fake_attributes
and _is_dataclass_repr(obj)
):
push_visited(obj_id)
children = []
append = children.append
if reached_max_depth:
node = Node(value_repr=f"{obj.__class__.__name__}(...)")
else:
node = Node(
open_brace=f"{obj.__class__.__name__}(",
close_brace=")",
children=children,
last=root,
empty=f"{obj.__class__.__name__}()",
)
for last, field in loop_last(
field for field in fields(obj) if field.repr
):
child_node = _traverse(getattr(obj, field.name), depth=depth + 1)
child_node.key_repr = field.name
child_node.last = last
child_node.key_separator = "="
append(child_node)
pop_visited(obj_id)
elif _is_namedtuple(obj) and _has_default_namedtuple_repr(obj):
push_visited(obj_id)
class_name = obj.__class__.__name__
if reached_max_depth:
# If we've reached the max depth, we still show the class name, but not its contents
node = Node(
value_repr=f"{class_name}(...)",
)
else:
children = []
append = children.append
node = Node(
open_brace=f"{class_name}(",
close_brace=")",
children=children,
empty=f"{class_name}()",
)
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)
pop_visited(obj_id)
elif _safe_isinstance(obj, _CONTAINERS):
for container_type in _CONTAINERS:
if _safe_isinstance(obj, container_type):
obj_type = container_type
break
push_visited(obj_id)
open_brace, close_brace, empty = _BRACES[obj_type](obj)
if reached_max_depth:
node = Node(value_repr=f"{open_brace}...{close_brace}")
elif obj_type.__repr__ != type(obj).__repr__:
node = Node(value_repr=to_repr(obj), last=root)
elif obj:
children = []
node = Node(
open_brace=open_brace,
close_brace=close_brace,
children=children,
last=root,
)
append = children.append
num_items = len(obj)
last_item_index = num_items - 1
if _safe_isinstance(obj, _MAPPING_CONTAINERS):
iter_items = iter(obj.items())
if max_length is not None:
iter_items = islice(iter_items, max_length)
for index, (key, child) in enumerate(iter_items):
child_node = _traverse(child, depth=depth + 1)
child_node.key_repr = to_repr(key)
child_node.last = index == last_item_index
append(child_node)
else:
iter_values = iter(obj)
if max_length is not None:
iter_values = islice(iter_values, max_length)
for index, child in enumerate(iter_values):
child_node = _traverse(child, depth=depth + 1)
child_node.last = index == last_item_index
append(child_node)
if max_length is not None and num_items > max_length:
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 = _safe_isinstance(obj, tuple)
node.is_namedtuple = _is_namedtuple(obj)
return node
node = _traverse(_object, root=True)
return node
def pretty_repr(
_object: Any,
*,
max_width: int = 80,
indent_size: int = 4,
max_length: Optional[int] = None,
max_string: Optional[int] = None,
max_depth: Optional[int] = None,
expand_all: bool = False,
) -> str:
"""Prettify repr string by expanding on to new lines to fit within a given width.
Args:
_object (Any): Object to repr.
max_width (int, optional): Desired maximum width of repr string. Defaults to 80.
indent_size (int, optional): Number of spaces to indent. Defaults to 4.
max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to None.
max_string (int, optional): Maximum length of string before truncating, or None to disable truncating.
Defaults to None.
max_depth (int, optional): Maximum depth of nested data structure, or None for no depth.
Defaults to None.
expand_all (bool, optional): Expand all containers regardless of available width. Defaults to False.
Returns:
str: A possibly multi-line representation of the object.
"""
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: str = node.render(
max_width=max_width, indent_size=indent_size, expand_all=expand_all
)
return repr_str
def pprint(
_object: Any,
*,
console: Optional["Console"] = None,
indent_guides: bool = True,
max_length: Optional[int] = None,
max_string: Optional[int] = None,
max_depth: Optional[int] = None,
expand_all: bool = False,
) -> None:
"""A convenience function for pretty printing.
Args:
_object (Any): Object to pretty print.
console (Console, optional): Console instance, or None to use default. Defaults to None.
max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to None.
max_string (int, optional): Maximum length of strings before truncating, or None to disable. Defaults to None.
max_depth (int, optional): Maximum depth for nested data structures, or None for unlimited depth. Defaults to None.
indent_guides (bool, optional): Enable indentation guides. Defaults to True.
expand_all (bool, optional): Expand all containers. Defaults to False.
"""
_console = get_console() if console is None else console
_console.print(
Pretty(
_object,
max_length=max_length,
max_string=max_string,
max_depth=max_depth,
indent_guides=indent_guides,
expand_all=expand_all,
overflow="ignore",
),
soft_wrap=True,
)
if __name__ == "__main__": # pragma: no cover
class BrokenRepr:
def __repr__(self) -> str:
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 = {
"foo": [
1,
"Hello World!",
100.123,
323.232,
432324.0,
{5, 6, 7, (1, 2, 3, 4), 8},
],
"bar": frozenset({1, 2, 3}),
"defaultdict": defaultdict(
list, {"crumble": ["apple", "rhubarb", "butter", "sugar", "flour"]}
),
"counter": Counter(
[
"apple",
"orange",
"pear",
"kumquat",
"kumquat",
"durian" * 100,
]
),
"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[attr-defined]
from pip._vendor.rich import print
# print(Pretty(data, indent_guides=True, max_string=20))
class Thing:
def __repr__(self) -> str:
return "Hello\x1b[38;5;239m World!"
print(Pretty(Thing()))

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,224 @@
import math
from functools import lru_cache
from time import monotonic
from typing import Iterable, List, Optional
from .color import Color, blend_rgb
from .color_triplet import ColorTriplet
from .console import Console, ConsoleOptions, RenderResult
from .jupyter import JupyterMixin
from .measure import Measurement
from .segment import Segment
from .style import Style, StyleType
# Number of characters before 'pulse' animation repeats
PULSE_SIZE = 20
class ProgressBar(JupyterMixin):
"""Renders a (progress) bar. Used by rich.progress.
Args:
total (float, optional): Number of steps in the bar. Defaults to 100. Set to None to render a pulsing animation.
completed (float, optional): Number of steps completed. Defaults to 0.
width (int, optional): Width of the bar, or ``None`` for maximum width. Defaults to None.
pulse (bool, optional): Enable pulse effect. Defaults to False. Will pulse if a None total was passed.
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.finished".
pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
animation_time (Optional[float], optional): Time in seconds to use for animation, or None to use system time.
"""
def __init__(
self,
total: Optional[float] = 100.0,
completed: float = 0,
width: Optional[int] = None,
pulse: bool = False,
style: StyleType = "bar.back",
complete_style: StyleType = "bar.complete",
finished_style: StyleType = "bar.finished",
pulse_style: StyleType = "bar.pulse",
animation_time: Optional[float] = None,
):
self.total = total
self.completed = completed
self.width = width
self.pulse = pulse
self.style = style
self.complete_style = complete_style
self.finished_style = finished_style
self.pulse_style = pulse_style
self.animation_time = animation_time
self._pulse_segments: Optional[List[Segment]] = None
def __repr__(self) -> str:
return f"<Bar {self.completed!r} of {self.total!r}>"
@property
def percentage_completed(self) -> Optional[float]:
"""Calculate percentage complete."""
if self.total is None:
return None
completed = (self.completed / self.total) * 100.0
completed = min(100, max(0.0, completed))
return completed
@lru_cache(maxsize=16)
def _get_pulse_segments(
self,
fore_style: Style,
back_style: Style,
color_system: str,
no_color: bool,
ascii: bool = False,
) -> List[Segment]:
"""Get a list of segments to render a pulse animation.
Returns:
List[Segment]: A list of segments, one segment per character.
"""
bar = "-" if ascii else ""
segments: List[Segment] = []
if color_system not in ("standard", "eight_bit", "truecolor") or no_color:
segments += [Segment(bar, fore_style)] * (PULSE_SIZE // 2)
segments += [Segment(" " if no_color else bar, back_style)] * (
PULSE_SIZE - (PULSE_SIZE // 2)
)
return segments
append = segments.append
fore_color = (
fore_style.color.get_truecolor()
if fore_style.color
else ColorTriplet(255, 0, 255)
)
back_color = (
back_style.color.get_truecolor()
if back_style.color
else ColorTriplet(0, 0, 0)
)
cos = math.cos
pi = math.pi
_Segment = Segment
_Style = Style
from_triplet = Color.from_triplet
for index in range(PULSE_SIZE):
position = index / PULSE_SIZE
fade = 0.5 + cos((position * pi * 2)) / 2.0
color = blend_rgb(fore_color, back_color, cross_fade=fade)
append(_Segment(bar, _Style(color=from_triplet(color))))
return segments
def update(self, completed: float, total: Optional[float] = None) -> None:
"""Update progress with new values.
Args:
completed (float): Number of steps completed.
total (float, optional): Total number of steps, or ``None`` to not change. Defaults to None.
"""
self.completed = completed
self.total = total if total is not None else self.total
def _render_pulse(
self, console: Console, width: int, ascii: bool = False
) -> Iterable[Segment]:
"""Renders the pulse animation.
Args:
console (Console): Console instance.
width (int): Width in characters of pulse animation.
Returns:
RenderResult: [description]
Yields:
Iterator[Segment]: Segments to render pulse
"""
fore_style = console.get_style(self.pulse_style, default="white")
back_style = console.get_style(self.style, default="black")
pulse_segments = self._get_pulse_segments(
fore_style, back_style, console.color_system, console.no_color, ascii=ascii
)
segment_count = len(pulse_segments)
current_time = (
monotonic() if self.animation_time is None else self.animation_time
)
segments = pulse_segments * (int(width / segment_count) + 2)
offset = int(-current_time * 15) % segment_count
segments = segments[offset : offset + width]
yield from segments
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
width = min(self.width or options.max_width, options.max_width)
ascii = options.legacy_windows or options.ascii_only
should_pulse = self.pulse or self.total is None
if should_pulse:
yield from self._render_pulse(console, width, ascii=ascii)
return
completed: Optional[float] = (
min(self.total, max(0, self.completed)) if self.total is not None else None
)
bar = "-" if ascii else ""
half_bar_right = " " if ascii else ""
half_bar_left = " " if ascii else ""
complete_halves = (
int(width * 2 * completed / self.total)
if self.total and completed is not None
else width * 2
)
bar_count = complete_halves // 2
half_bar_count = complete_halves % 2
style = console.get_style(self.style)
is_finished = self.total is None or self.completed >= self.total
complete_style = console.get_style(
self.finished_style if is_finished else self.complete_style
)
_Segment = Segment
if bar_count:
yield _Segment(bar * bar_count, complete_style)
if half_bar_count:
yield _Segment(half_bar_right * half_bar_count, complete_style)
if not console.no_color:
remaining_bars = width - bar_count - half_bar_count
if remaining_bars and console.color_system is not None:
if not half_bar_count and bar_count:
yield _Segment(half_bar_left, style)
remaining_bars -= 1
if remaining_bars:
yield _Segment(bar * remaining_bars, style)
def __rich_measure__(
self, console: Console, options: ConsoleOptions
) -> Measurement:
return (
Measurement(self.width, self.width)
if self.width is not None
else Measurement(4, options.max_width)
)
if __name__ == "__main__": # pragma: no cover
console = Console()
bar = ProgressBar(width=50, total=100)
import time
console.show_cursor(False)
for n in range(0, 101, 1):
bar.update(n)
console.print(bar)
console.file.write("\r")
time.sleep(0.05)
console.show_cursor(True)
console.print()

View file

@ -0,0 +1,376 @@
from typing import Any, Generic, List, Optional, TextIO, TypeVar, Union, overload
from . import get_console
from .console import Console
from .text import Text, TextType
PromptType = TypeVar("PromptType")
DefaultType = TypeVar("DefaultType")
class PromptError(Exception):
"""Exception base class for prompt related errors."""
class InvalidResponse(PromptError):
"""Exception to indicate a response was invalid. Raise this within process_response() to indicate an error
and provide an error message.
Args:
message (Union[str, Text]): Error message.
"""
def __init__(self, message: TextType) -> None:
self.message = message
def __rich__(self) -> TextType:
return self.message
class PromptBase(Generic[PromptType]):
"""Ask the user for input until a valid response is received. This is the base class, see one of
the concrete classes for examples.
Args:
prompt (TextType, optional): Prompt text. Defaults to "".
console (Console, optional): A Console instance or None to use global console. Defaults to None.
password (bool, optional): Enable password input. Defaults to False.
choices (List[str], optional): A list of valid choices. Defaults to None.
show_default (bool, optional): Show default in prompt. Defaults to True.
show_choices (bool, optional): Show choices in prompt. Defaults to True.
"""
response_type: type = str
validate_error_message = "[prompt.invalid]Please enter a valid value"
illegal_choice_message = (
"[prompt.invalid.choice]Please select one of the available options"
)
prompt_suffix = ": "
choices: Optional[List[str]] = None
def __init__(
self,
prompt: TextType = "",
*,
console: Optional[Console] = None,
password: bool = False,
choices: Optional[List[str]] = None,
show_default: bool = True,
show_choices: bool = True,
) -> None:
self.console = console or get_console()
self.prompt = (
Text.from_markup(prompt, style="prompt")
if isinstance(prompt, str)
else prompt
)
self.password = password
if choices is not None:
self.choices = choices
self.show_default = show_default
self.show_choices = show_choices
@classmethod
@overload
def ask(
cls,
prompt: TextType = "",
*,
console: Optional[Console] = None,
password: bool = False,
choices: Optional[List[str]] = None,
show_default: bool = True,
show_choices: bool = True,
default: DefaultType,
stream: Optional[TextIO] = None,
) -> Union[DefaultType, PromptType]:
...
@classmethod
@overload
def ask(
cls,
prompt: TextType = "",
*,
console: Optional[Console] = None,
password: bool = False,
choices: Optional[List[str]] = None,
show_default: bool = True,
show_choices: bool = True,
stream: Optional[TextIO] = None,
) -> PromptType:
...
@classmethod
def ask(
cls,
prompt: TextType = "",
*,
console: Optional[Console] = None,
password: bool = False,
choices: Optional[List[str]] = None,
show_default: bool = True,
show_choices: bool = True,
default: Any = ...,
stream: Optional[TextIO] = None,
) -> Any:
"""Shortcut to construct and run a prompt loop and return the result.
Example:
>>> filename = Prompt.ask("Enter a filename")
Args:
prompt (TextType, optional): Prompt text. Defaults to "".
console (Console, optional): A Console instance or None to use global console. Defaults to None.
password (bool, optional): Enable password input. Defaults to False.
choices (List[str], optional): A list of valid choices. Defaults to None.
show_default (bool, optional): Show default in prompt. Defaults to True.
show_choices (bool, optional): Show choices in prompt. Defaults to True.
stream (TextIO, optional): Optional text file open for reading to get input. Defaults to None.
"""
_prompt = cls(
prompt,
console=console,
password=password,
choices=choices,
show_default=show_default,
show_choices=show_choices,
)
return _prompt(default=default, stream=stream)
def render_default(self, default: DefaultType) -> Text:
"""Turn the supplied default in to a Text instance.
Args:
default (DefaultType): Default value.
Returns:
Text: Text containing rendering of default value.
"""
return Text(f"({default})", "prompt.default")
def make_prompt(self, default: DefaultType) -> Text:
"""Make prompt text.
Args:
default (DefaultType): Default value.
Returns:
Text: Text to display in prompt.
"""
prompt = self.prompt.copy()
prompt.end = ""
if self.show_choices and self.choices:
_choices = "/".join(self.choices)
choices = f"[{_choices}]"
prompt.append(" ")
prompt.append(choices, "prompt.choices")
if (
default != ...
and self.show_default
and isinstance(default, (str, self.response_type))
):
prompt.append(" ")
_default = self.render_default(default)
prompt.append(_default)
prompt.append(self.prompt_suffix)
return prompt
@classmethod
def get_input(
cls,
console: Console,
prompt: TextType,
password: bool,
stream: Optional[TextIO] = None,
) -> str:
"""Get input from user.
Args:
console (Console): Console instance.
prompt (TextType): Prompt text.
password (bool): Enable password entry.
Returns:
str: String from user.
"""
return console.input(prompt, password=password, stream=stream)
def check_choice(self, value: str) -> bool:
"""Check value is in the list of valid choices.
Args:
value (str): Value entered by user.
Returns:
bool: True if choice was valid, otherwise False.
"""
assert self.choices is not None
return value.strip() in self.choices
def process_response(self, value: str) -> PromptType:
"""Process response from user, convert to prompt type.
Args:
value (str): String typed by user.
Raises:
InvalidResponse: If ``value`` is invalid.
Returns:
PromptType: The value to be returned from ask method.
"""
value = value.strip()
try:
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
def on_validate_error(self, value: str, error: InvalidResponse) -> None:
"""Called to handle validation error.
Args:
value (str): String entered by user.
error (InvalidResponse): Exception instance the initiated the error.
"""
self.console.print(error)
def pre_prompt(self) -> None:
"""Hook to display something before the prompt."""
@overload
def __call__(self, *, stream: Optional[TextIO] = None) -> PromptType:
...
@overload
def __call__(
self, *, default: DefaultType, stream: Optional[TextIO] = None
) -> Union[PromptType, DefaultType]:
...
def __call__(self, *, default: Any = ..., stream: Optional[TextIO] = None) -> Any:
"""Run the prompt loop.
Args:
default (Any, optional): Optional default value.
Returns:
PromptType: Processed value.
"""
while True:
self.pre_prompt()
prompt = self.make_prompt(default)
value = self.get_input(self.console, prompt, self.password, stream=stream)
if value == "" and default != ...:
return default
try:
return_value = self.process_response(value)
except InvalidResponse as error:
self.on_validate_error(value, error)
continue
else:
return return_value
class Prompt(PromptBase[str]):
"""A prompt that returns a str.
Example:
>>> name = Prompt.ask("Enter your name")
"""
response_type = str
class IntPrompt(PromptBase[int]):
"""A prompt that returns an integer.
Example:
>>> burrito_count = IntPrompt.ask("How many burritos do you want to order")
"""
response_type = int
validate_error_message = "[prompt.invalid]Please enter a valid integer number"
class FloatPrompt(PromptBase[int]):
"""A prompt that returns a float.
Example:
>>> temperature = FloatPrompt.ask("Enter desired temperature")
"""
response_type = float
validate_error_message = "[prompt.invalid]Please enter a number"
class Confirm(PromptBase[bool]):
"""A yes / no confirmation prompt.
Example:
>>> if Confirm.ask("Continue"):
run_job()
"""
response_type = bool
validate_error_message = "[prompt.invalid]Please enter Y or N"
choices: List[str] = ["y", "n"]
def render_default(self, default: DefaultType) -> Text:
"""Render the default as (y) or (n) rather than True/False."""
yes, no = self.choices
return Text(f"({yes})" if default else f"({no})", style="prompt.default")
def process_response(self, value: str) -> bool:
"""Convert choices to a bool."""
value = value.strip().lower()
if value not in self.choices:
raise InvalidResponse(self.validate_error_message)
return value == self.choices[0]
if __name__ == "__main__": # pragma: no cover
from pip._vendor.rich import print
if Confirm.ask("Run [i]prompt[/i] tests?", default=True):
while True:
result = IntPrompt.ask(
":rocket: Enter a number between [b]1[/b] and [b]10[/b]", default=5
)
if result >= 1 and result <= 10:
break
print(":pile_of_poo: [prompt.invalid]Number must be between 1 and 10")
print(f"number={result}")
while True:
password = Prompt.ask(
"Please enter a password [cyan](must be at least 5 characters)",
password=True,
)
if len(password) >= 5:
break
print("[prompt.invalid]password too short")
print(f"password={password!r}")
fruit = Prompt.ask("Enter a fruit", choices=["apple", "orange", "pear"])
print(f"fruit={fruit!r}")
else:
print("[b]OK :loudly_crying_face:")

View file

@ -0,0 +1,42 @@
from typing import Any, cast, Set, TYPE_CHECKING
from inspect import isclass
if TYPE_CHECKING:
from pip._vendor.rich.console import RenderableType
_GIBBERISH = """aihwerij235234ljsdnp34ksodfipwoe234234jlskjdf"""
def is_renderable(check_object: Any) -> bool:
"""Check if an object may be rendered by Rich."""
return (
isinstance(check_object, str)
or hasattr(check_object, "__rich__")
or hasattr(check_object, "__rich_console__")
)
def rich_cast(renderable: object) -> "RenderableType":
"""Cast an object to a renderable by calling __rich__ if present.
Args:
renderable (object): A potentially renderable object
Returns:
object: The result of recursively calling __rich__.
"""
from pip._vendor.rich.console import RenderableType
rich_visited_set: Set[type] = set() # Prevent potential infinite loop
while hasattr(renderable, "__rich__") and not isclass(renderable):
# Detect object which claim to have all the attributes
if hasattr(renderable, _GIBBERISH):
return repr(renderable)
cast_method = getattr(renderable, "__rich__")
renderable = cast_method()
renderable_type = type(renderable)
if renderable_type in rich_visited_set:
break
rich_visited_set.add(renderable_type)
return cast(RenderableType, renderable)

View file

@ -0,0 +1,10 @@
from typing import NamedTuple
class Region(NamedTuple):
"""Defines a rectangular region of the screen."""
x: int
y: int
width: int
height: int

View file

@ -0,0 +1,149 @@
import inspect
from functools import partial
from typing import (
Any,
Callable,
Iterable,
List,
Optional,
Tuple,
Type,
TypeVar,
Union,
overload,
)
T = TypeVar("T")
Result = Iterable[Union[Any, Tuple[Any], Tuple[str, Any], Tuple[str, Any, Any]]]
RichReprResult = Result
class ReprError(Exception):
"""An error occurred when attempting to build a repr."""
@overload
def auto(cls: Optional[Type[T]]) -> Type[T]:
...
@overload
def auto(*, angular: bool = False) -> Callable[[Type[T]], Type[T]]:
...
def auto(
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: T) -> str:
"""Create repr string from __rich_repr__"""
repr_str: List[str] = []
append = repr_str.append
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]))
else:
key, value, *default = arg
if key is None:
append(repr(value))
else:
if default and default[0] == value:
continue
append(f"{key}={value!r}")
else:
append(repr(arg))
if angular:
return f"<{self.__class__.__name__} {' '.join(repr_str)}>"
else:
return f"{self.__class__.__name__}({', '.join(repr_str)})"
def auto_rich_repr(self: Type[T]) -> Result:
"""Auto generate __rich_rep__ from signature of __init__"""
try:
signature = inspect.signature(self.__init__)
for name, param in signature.parameters.items():
if param.kind == param.POSITIONAL_ONLY:
yield getattr(self, name)
elif param.kind in (
param.POSITIONAL_OR_KEYWORD,
param.KEYWORD_ONLY,
):
if param.default == param.empty:
yield getattr(self, param.name)
else:
yield param.name, getattr(self, param.name), param.default
except Exception as error:
raise ReprError(
f"Failed to auto generate __rich_repr__; {error}"
) from None
if not hasattr(cls, "__rich_repr__"):
auto_rich_repr.__doc__ = "Build a rich repr"
cls.__rich_repr__ = auto_rich_repr # type: ignore[attr-defined]
auto_repr.__doc__ = "Return repr(self)"
cls.__repr__ = auto_repr # type: ignore[assignment]
if angular is not None:
cls.__rich_repr__.angular = angular # type: ignore[attr-defined]
return cls
if cls is None:
return partial(do_replace, angular=angular)
else:
return do_replace(cls, angular=angular)
@overload
def rich_repr(cls: Optional[Type[T]]) -> Type[T]:
...
@overload
def rich_repr(*, angular: bool = False) -> Callable[[Type[T]], Type[T]]:
...
def rich_repr(
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:
return auto(cls)
if __name__ == "__main__":
@auto
class Foo:
def __rich_repr__(self) -> Result:
yield "foo"
yield "bar", {"shopping": ["eggs", "ham", "pineapple"]}
yield "buy", "hand sanitizer"
foo = Foo()
from pip._vendor.rich.console import Console
console = Console()
console.rule("Standard repr")
console.print(foo)
console.print(foo, width=60)
console.print(foo, width=30)
console.rule("Angular repr")
Foo.__rich_repr__.angular = True # type: ignore[attr-defined]
console.print(foo)
console.print(foo, width=60)
console.print(foo, width=30)

View file

@ -0,0 +1,130 @@
from typing import Union
from .align import AlignMethod
from .cells import cell_len, set_cell_size
from .console import Console, ConsoleOptions, RenderResult
from .jupyter import JupyterMixin
from .measure import Measurement
from .style import Style
from .text import Text
class Rule(JupyterMixin):
"""A console renderable to draw a horizontal rule (line).
Args:
title (Union[str, Text], optional): Text to render in the rule. Defaults to "".
characters (str, optional): Character(s) used to draw the line. Defaults to "".
style (StyleType, optional): Style of Rule. Defaults to "rule.line".
end (str, optional): Character at end of Rule. defaults to "\\\\n"
align (str, optional): How to align the title, one of "left", "center", or "right". Defaults to "center".
"""
def __init__(
self,
title: Union[str, Text] = "",
*,
characters: str = "",
style: Union[str, Style] = "rule.line",
end: str = "\n",
align: AlignMethod = "center",
) -> None:
if cell_len(characters) < 1:
raise ValueError(
"'characters' argument must have a cell width of at least 1"
)
if align not in ("left", "center", "right"):
raise ValueError(
f'invalid value for align, expected "left", "center", "right" (not {align!r})'
)
self.title = title
self.characters = characters
self.style = style
self.end = end
self.align = align
def __repr__(self) -> str:
return f"Rule({self.title!r}, {self.characters!r})"
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
width = options.max_width
characters = (
"-"
if (options.ascii_only and not self.characters.isascii())
else self.characters
)
chars_len = cell_len(characters)
if not self.title:
yield self._rule_line(chars_len, width)
return
if isinstance(self.title, Text):
title_text = self.title
else:
title_text = console.render_str(self.title, style="rule.text")
title_text.plain = title_text.plain.replace("\n", " ")
title_text.expand_tabs()
required_space = 4 if self.align == "center" else 2
truncate_width = max(0, width - required_space)
if not truncate_width:
yield self._rule_line(chars_len, width)
return
rule_text = Text(end=self.end)
if self.align == "center":
title_text.truncate(truncate_width, overflow="ellipsis")
side_width = (width - cell_len(title_text.plain)) // 2
left = Text(characters * (side_width // chars_len + 1))
left.truncate(side_width - 1)
right_length = width - cell_len(left.plain) - cell_len(title_text.plain)
right = Text(characters * (side_width // chars_len + 1))
right.truncate(right_length)
rule_text.append(left.plain + " ", self.style)
rule_text.append(title_text)
rule_text.append(" " + right.plain, self.style)
elif self.align == "left":
title_text.truncate(truncate_width, overflow="ellipsis")
rule_text.append(title_text)
rule_text.append(" ")
rule_text.append(characters * (width - rule_text.cell_len), self.style)
elif self.align == "right":
title_text.truncate(truncate_width, overflow="ellipsis")
rule_text.append(characters * (width - title_text.cell_len - 1), self.style)
rule_text.append(" ")
rule_text.append(title_text)
rule_text.plain = set_cell_size(rule_text.plain, width)
yield rule_text
def _rule_line(self, chars_len: int, width: int) -> Text:
rule_text = Text(self.characters * ((width // chars_len) + 1), self.style)
rule_text.truncate(width)
rule_text.plain = set_cell_size(rule_text.plain, width)
return rule_text
def __rich_measure__(
self, console: Console, options: ConsoleOptions
) -> Measurement:
return Measurement(1, 1)
if __name__ == "__main__": # pragma: no cover
import sys
from pip._vendor.rich.console import Console
try:
text = sys.argv[1]
except IndexError:
text = "Hello, World"
console = Console()
console.print(Rule(title=text))
console = Console()
console.print(Rule("foo"), width=4)

View file

@ -0,0 +1,86 @@
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Optional, Tuple
from .highlighter import ReprHighlighter
from .panel import Panel
from .pretty import Pretty
from .table import Table
from .text import Text, TextType
if TYPE_CHECKING:
from .console import ConsoleRenderable
def render_scope(
scope: "Mapping[str, Any]",
*,
title: Optional[TextType] = None,
sort_keys: bool = True,
indent_guides: bool = False,
max_length: Optional[int] = None,
max_string: Optional[int] = None,
) -> "ConsoleRenderable":
"""Render python variables in a given scope.
Args:
scope (Mapping): A mapping containing variable names and values.
title (str, optional): Optional title. Defaults to None.
sort_keys (bool, optional): Enable sorting of items. Defaults to True.
indent_guides (bool, optional): Enable indentation guides. Defaults to False.
max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to None.
max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None.
Returns:
ConsoleRenderable: A renderable object.
"""
highlighter = ReprHighlighter()
items_table = Table.grid(padding=(0, 1), expand=False)
items_table.add_column(justify="right")
def sort_items(item: Tuple[str, Any]) -> Tuple[bool, str]:
"""Sort special variables first, then alphabetically."""
key, _ = item
return (not key.startswith("__"), key.lower())
items = sorted(scope.items(), key=sort_items) if sort_keys else scope.items()
for key, value in items:
key_text = Text.assemble(
(key, "scope.key.special" if key.startswith("__") else "scope.key"),
(" =", "scope.equals"),
)
items_table.add_row(
key_text,
Pretty(
value,
highlighter=highlighter,
indent_guides=indent_guides,
max_length=max_length,
max_string=max_string,
),
)
return Panel.fit(
items_table,
title=title,
border_style="scope.border",
padding=(0, 1),
)
if __name__ == "__main__": # pragma: no cover
from pip._vendor.rich import print
print()
def test(foo: float, bar: float) -> None:
list_of_things = [1, 2, 3, None, 4, True, False, "Hello World"]
dict_of_things = {
"version": "1.1",
"method": "confirmFruitPurchase",
"params": [["apple", "orange", "mangoes", "pomelo"], 1.123],
"id": "194521489",
}
print(render_scope(locals(), title="[i]locals", sort_keys=False))
test(20.3423, 3.1427)
print()

View file

@ -0,0 +1,54 @@
from typing import Optional, TYPE_CHECKING
from .segment import Segment
from .style import StyleType
from ._loop import loop_last
if TYPE_CHECKING:
from .console import (
Console,
ConsoleOptions,
RenderResult,
RenderableType,
Group,
)
class Screen:
"""A renderable that fills the terminal screen and crops excess.
Args:
renderable (RenderableType): Child renderable.
style (StyleType, optional): Optional background style. Defaults to None.
"""
renderable: "RenderableType"
def __init__(
self,
*renderables: "RenderableType",
style: Optional[StyleType] = None,
application_mode: bool = False,
) -> None:
from pip._vendor.rich.console import Group
self.renderable = Group(*renderables)
self.style = style
self.application_mode = application_mode
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
width, height = options.size
style = console.get_style(self.style) if self.style else None
render_options = options.update(width=width, height=height)
lines = console.render_lines(
self.renderable or "", render_options, style=style, pad=True
)
lines = Segment.set_shape(lines, width, height, style=style)
new_line = Segment("\n\r") if self.application_mode else Segment.line()
for last, line in loop_last(lines):
yield from line
if not last:
yield new_line

View file

@ -0,0 +1,739 @@
from enum import IntEnum
from functools import lru_cache
from itertools import filterfalse
from logging import getLogger
from operator import attrgetter
from typing import (
TYPE_CHECKING,
Dict,
Iterable,
List,
NamedTuple,
Optional,
Sequence,
Tuple,
Type,
Union,
)
from .cells import (
_is_single_cell_widths,
cached_cell_len,
cell_len,
get_character_cell_size,
set_cell_size,
)
from .repr import Result, rich_repr
from .style import Style
if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderResult
log = getLogger("rich")
class ControlType(IntEnum):
"""Non-printable control codes which typically translate to ANSI codes."""
BELL = 1
CARRIAGE_RETURN = 2
HOME = 3
CLEAR = 4
SHOW_CURSOR = 5
HIDE_CURSOR = 6
ENABLE_ALT_SCREEN = 7
DISABLE_ALT_SCREEN = 8
CURSOR_UP = 9
CURSOR_DOWN = 10
CURSOR_FORWARD = 11
CURSOR_BACKWARD = 12
CURSOR_MOVE_TO_COLUMN = 13
CURSOR_MOVE_TO = 14
ERASE_IN_LINE = 15
SET_WINDOW_TITLE = 16
ControlCode = Union[
Tuple[ControlType],
Tuple[ControlType, Union[int, str]],
Tuple[ControlType, int, int],
]
@rich_repr()
class Segment(NamedTuple):
"""A piece of text with associated style. Segments are produced by the Console render process and
are ultimately converted in to strings to be written to the terminal.
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.
Attributes:
cell_length (int): The cell length of this Segment.
"""
text: str
style: Optional[Style] = None
control: Optional[Sequence[ControlCode]] = None
@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
if self.control is None:
if self.style is not None:
yield self.style
else:
yield self.style
yield self.control
def __bool__(self) -> bool:
"""Check if the segment contains text."""
return bool(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"]:
text, style, control = segment
_Segment = Segment
cell_length = segment.cell_length
if cut >= cell_length:
return segment, _Segment("", style, control)
cell_size = get_character_cell_size
pos = int((cut / cell_length) * (len(text) - 1))
before = text[:pos]
cell_pos = cell_len(before)
if cell_pos == cut:
return (
_Segment(before, style, control),
_Segment(text[pos:], style, control),
)
while pos < len(text):
char = text[pos]
pos += 1
cell_pos += cell_size(char)
before = text[:pos]
if cell_pos == cut:
return (
_Segment(before, style, control),
_Segment(text[pos:], style, control),
)
if cell_pos > cut:
return (
_Segment(before[: pos - 1] + " ", style, control),
_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.
If the cut point falls in the middle of a 2-cell wide character then it is replaced
by two spaces, to preserve the display width of the parent segment.
Returns:
Tuple[Segment, Segment]: Two segments.
"""
text, style, control = self
if _is_single_cell_widths(text):
# Fast path with all 1 cell characters
if cut >= len(text):
return self, Segment("", style, control)
return (
Segment(text[:cut], style, control),
Segment(text[cut:], style, control),
)
return self._split_cells(self, cut)
@classmethod
def line(cls) -> "Segment":
"""Make a new line segment."""
return cls("\n")
@classmethod
def apply_style(
cls,
segments: Iterable["Segment"],
style: Optional[Style] = None,
post_style: Optional[Style] = None,
) -> Iterable["Segment"]:
"""Apply style(s) to an iterable of segments.
Returns an iterable of segments where the style is replaced by ``style + segment.style + post_style``.
Args:
segments (Iterable[Segment]): Segments to process.
style (Style, optional): Base style. Defaults to None.
post_style (Style, optional): Style to apply on top of segment style. Defaults to None.
Returns:
Iterable[Segments]: A new iterable of segments (possibly the same iterable).
"""
result_segments = segments
if style:
apply = style.__add__
result_segments = (
cls(text, None if control else apply(_style), control)
for text, _style, control in result_segments
)
if post_style:
result_segments = (
cls(
text,
(
None
if control
else (_style + post_style if _style else post_style)
),
control,
)
for text, _style, control in result_segments
)
return result_segments
@classmethod
def filter_control(
cls, segments: Iterable["Segment"], is_control: bool = False
) -> Iterable["Segment"]:
"""Filter segments by ``is_control`` attribute.
Args:
segments (Iterable[Segment]): An iterable of Segment instances.
is_control (bool, optional): is_control flag to match in search.
Returns:
Iterable[Segment]: And iterable of Segment instances.
"""
if is_control:
return filter(attrgetter("control"), segments)
else:
return filterfalse(attrgetter("control"), segments)
@classmethod
def split_lines(cls, segments: Iterable["Segment"]) -> Iterable[List["Segment"]]:
"""Split a sequence of segments in to a list of lines.
Args:
segments (Iterable[Segment]): Segments potentially containing line feeds.
Yields:
Iterable[List[Segment]]: Iterable of segment lists, one per line.
"""
line: List[Segment] = []
append = line.append
for segment in segments:
if "\n" in segment.text and not segment.control:
text, style, _ = segment
while text:
_text, new_line, text = text.partition("\n")
if _text:
append(cls(_text, style))
if new_line:
yield line
line = []
append = line.append
else:
append(segment)
if line:
yield line
@classmethod
def split_and_crop_lines(
cls,
segments: Iterable["Segment"],
length: int,
style: Optional[Style] = None,
pad: bool = True,
include_new_lines: bool = True,
) -> Iterable[List["Segment"]]:
"""Split segments in to lines, and crop lines greater than a given length.
Args:
segments (Iterable[Segment]): An iterable of segments, probably
generated from console.render.
length (int): Desired line length.
style (Style, optional): Style to use for any padding.
pad (bool): Enable padding of lines that are less than `length`.
Returns:
Iterable[List[Segment]]: An iterable of lines of segments.
"""
line: List[Segment] = []
append = line.append
adjust_line_length = cls.adjust_line_length
new_line_segment = cls("\n")
for segment in segments:
if "\n" in segment.text and not segment.control:
text, segment_style, _ = segment
while text:
_text, new_line, text = text.partition("\n")
if _text:
append(cls(_text, segment_style))
if new_line:
cropped_line = adjust_line_length(
line, length, style=style, pad=pad
)
if include_new_lines:
cropped_line.append(new_line_segment)
yield cropped_line
line.clear()
else:
append(segment)
if line:
yield adjust_line_length(line, length, style=style, pad=pad)
@classmethod
def adjust_line_length(
cls,
line: List["Segment"],
length: int,
style: Optional[Style] = None,
pad: bool = True,
) -> List["Segment"]:
"""Adjust a line to a given width (cropping or padding as required).
Args:
segments (Iterable[Segment]): A list of segments in a single line.
length (int): The desired width of the line.
style (Style, optional): The style of padding if used (space on the end). Defaults to None.
pad (bool, optional): Pad lines with spaces if they are shorter than `length`. Defaults to True.
Returns:
List[Segment]: A line of segments with the desired length.
"""
line_length = sum(segment.cell_length for segment in line)
new_line: List[Segment]
if line_length < length:
if pad:
new_line = line + [cls(" " * (length - line_length), style)]
else:
new_line = line[:]
elif line_length > length:
new_line = []
append = new_line.append
line_length = 0
for segment in line:
segment_length = segment.cell_length
if line_length + segment_length < length or segment.control:
append(segment)
line_length += segment_length
else:
text, segment_style, _ = segment
text = set_cell_size(text, length - line_length)
append(cls(text, segment_style))
break
else:
new_line = line[:]
return new_line
@classmethod
def get_line_length(cls, line: List["Segment"]) -> int:
"""Get the length of list of segments.
Args:
line (List[Segment]): A line encoded as a list of Segments (assumes no '\\\\n' characters),
Returns:
int: The length of the line.
"""
_cell_len = cell_len
return sum(_cell_len(text) for text, style, control in line if not control)
@classmethod
def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]:
"""Get the shape (enclosing rectangle) of a list of lines.
Args:
lines (List[List[Segment]]): A list of lines (no '\\\\n' characters).
Returns:
Tuple[int, int]: Width and height in characters.
"""
get_line_length = cls.get_line_length
max_width = max(get_line_length(line) for line in lines) if lines else 0
return (max_width, len(lines))
@classmethod
def set_shape(
cls,
lines: List[List["Segment"]],
width: int,
height: Optional[int] = None,
style: Optional[Style] = None,
new_lines: bool = False,
) -> List[List["Segment"]]:
"""Set the shape of a list of lines (enclosing rectangle).
Args:
lines (List[List[Segment]]): A list of lines.
width (int): Desired width.
height (int, optional): Desired height or None for no change.
style (Style, optional): Style of any padding added.
new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
Returns:
List[List[Segment]]: New list of lines.
"""
_height = height or len(lines)
blank = (
[cls(" " * width + "\n", style)] if new_lines else [cls(" " * width, style)]
)
adjust_line_length = cls.adjust_line_length
shaped_lines = lines[:_height]
shaped_lines[:] = [
adjust_line_length(line, width, style=style) for line in lines
]
if len(shaped_lines) < _height:
shaped_lines.extend([blank] * (_height - len(shaped_lines)))
return shaped_lines
@classmethod
def align_top(
cls: Type["Segment"],
lines: List[List["Segment"]],
width: int,
height: int,
style: Style,
new_lines: bool = False,
) -> List[List["Segment"]]:
"""Aligns lines to top (adds extra lines to bottom as required).
Args:
lines (List[List[Segment]]): A list of lines.
width (int): Desired width.
height (int, optional): Desired height or None for no change.
style (Style): Style of any padding added.
new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
Returns:
List[List[Segment]]: New list of lines.
"""
extra_lines = height - len(lines)
if not extra_lines:
return lines[:]
lines = lines[:height]
blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style)
lines = lines + [[blank]] * extra_lines
return lines
@classmethod
def align_bottom(
cls: Type["Segment"],
lines: List[List["Segment"]],
width: int,
height: int,
style: Style,
new_lines: bool = False,
) -> List[List["Segment"]]:
"""Aligns render to bottom (adds extra lines above as required).
Args:
lines (List[List[Segment]]): A list of lines.
width (int): Desired width.
height (int, optional): Desired height or None for no change.
style (Style): Style of any padding added. Defaults to None.
new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
Returns:
List[List[Segment]]: New list of lines.
"""
extra_lines = height - len(lines)
if not extra_lines:
return lines[:]
lines = lines[:height]
blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style)
lines = [[blank]] * extra_lines + lines
return lines
@classmethod
def align_middle(
cls: Type["Segment"],
lines: List[List["Segment"]],
width: int,
height: int,
style: Style,
new_lines: bool = False,
) -> List[List["Segment"]]:
"""Aligns lines to middle (adds extra lines to above and below as required).
Args:
lines (List[List[Segment]]): A list of lines.
width (int): Desired width.
height (int, optional): Desired height or None for no change.
style (Style): Style of any padding added.
new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
Returns:
List[List[Segment]]: New list of lines.
"""
extra_lines = height - len(lines)
if not extra_lines:
return lines[:]
lines = lines[:height]
blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style)
top_lines = extra_lines // 2
bottom_lines = extra_lines - top_lines
lines = [[blank]] * top_lines + lines + [[blank]] * bottom_lines
return lines
@classmethod
def simplify(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
"""Simplify an iterable of segments by combining contiguous segments with the same style.
Args:
segments (Iterable[Segment]): An iterable of segments.
Returns:
Iterable[Segment]: A possibly smaller iterable of segments that will render the same way.
"""
iter_segments = iter(segments)
try:
last_segment = next(iter_segments)
except StopIteration:
return
_Segment = Segment
for segment in iter_segments:
if last_segment.style == segment.style and not segment.control:
last_segment = _Segment(
last_segment.text + segment.text, last_segment.style
)
else:
yield last_segment
last_segment = segment
yield last_segment
@classmethod
def strip_links(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
"""Remove all links from an iterable of styles.
Args:
segments (Iterable[Segment]): An iterable segments.
Yields:
Segment: Segments with link removed.
"""
for segment in segments:
if segment.control or segment.style is None:
yield segment
else:
text, style, _control = segment
yield cls(text, style.update_link(None) if style else None)
@classmethod
def strip_styles(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
"""Remove all styles from an iterable of segments.
Args:
segments (Iterable[Segment]): An iterable segments.
Yields:
Segment: Segments with styles replace with None
"""
for text, _style, control in segments:
yield cls(text, None, control)
@classmethod
def remove_color(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
"""Remove all color from an iterable of segments.
Args:
segments (Iterable[Segment]): An iterable segments.
Yields:
Segment: Segments with colorless style.
"""
cache: Dict[Style, Style] = {}
for text, style, control in segments:
if style:
colorless_style = cache.get(style)
if colorless_style is None:
colorless_style = style.without_color
cache[style] = colorless_style
yield cls(text, colorless_style, control)
else:
yield cls(text, None, control)
@classmethod
def divide(
cls, segments: Iterable["Segment"], cuts: Iterable[int]
) -> Iterable[List["Segment"]]:
"""Divides an iterable of segments in to portions.
Args:
cuts (Iterable[int]): Cell positions where to divide.
Yields:
[Iterable[List[Segment]]]: An iterable of Segments in List.
"""
split_segments: List["Segment"] = []
add_segment = split_segments.append
iter_cuts = iter(cuts)
while True:
cut = next(iter_cuts, -1)
if cut == -1:
return []
if cut != 0:
break
yield []
pos = 0
segments_clear = split_segments.clear
segments_copy = split_segments.copy
_cell_len = cached_cell_len
for segment in segments:
text, _style, control = segment
while text:
end_pos = pos if control else pos + _cell_len(text)
if end_pos < cut:
add_segment(segment)
pos = end_pos
break
if end_pos == cut:
add_segment(segment)
yield segments_copy()
segments_clear()
pos = end_pos
cut = next(iter_cuts, -1)
if cut == -1:
if split_segments:
yield segments_copy()
return
break
else:
before, segment = segment.split_cells(cut - pos)
text, _style, control = segment
add_segment(before)
yield segments_copy()
segments_clear()
pos = cut
cut = next(iter_cuts, -1)
if cut == -1:
if split_segments:
yield segments_copy()
return
yield segments_copy()
class Segments:
"""A simple renderable to render an iterable of segments. This class may be useful if
you want to print segments outside of a __rich_console__ method.
Args:
segments (Iterable[Segment]): An iterable of segments.
new_lines (bool, optional): Add new lines between segments. Defaults to False.
"""
def __init__(self, segments: Iterable[Segment], new_lines: bool = False) -> None:
self.segments = list(segments)
self.new_lines = new_lines
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
if self.new_lines:
line = Segment.line()
for segment in self.segments:
yield segment
yield line
else:
yield from self.segments
class SegmentLines:
def __init__(self, lines: Iterable[List[Segment]], new_lines: bool = False) -> None:
"""A simple renderable containing a number of lines of segments. May be used as an intermediate
in rendering process.
Args:
lines (Iterable[List[Segment]]): Lists of segments forming lines.
new_lines (bool, optional): Insert new lines after each line. Defaults to False.
"""
self.lines = list(lines)
self.new_lines = new_lines
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
if self.new_lines:
new_line = Segment.line()
for line in self.lines:
yield from line
yield new_line
else:
for line in self.lines:
yield from line
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)"""
text = Text.from_markup("Hello, [bold magenta]World[/]!")
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 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."
)

View file

@ -0,0 +1,137 @@
from typing import cast, List, Optional, TYPE_CHECKING, Union
from ._spinners import SPINNERS
from .measure import Measurement
from .table import Table
from .text import Text
if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderResult, RenderableType
from .style import StyleType
class Spinner:
"""A spinner animation.
Args:
name (str): Name of spinner (run python -m rich.spinner).
text (RenderableType, optional): A renderable to display at the right of the spinner (str or Text typically). Defaults to "".
style (StyleType, optional): Style for spinner animation. Defaults to None.
speed (float, optional): Speed factor for animation. Defaults to 1.0.
Raises:
KeyError: If name isn't one of the supported spinner animations.
"""
def __init__(
self,
name: str,
text: "RenderableType" = "",
*,
style: Optional["StyleType"] = None,
speed: float = 1.0,
) -> None:
try:
spinner = SPINNERS[name]
except KeyError:
raise KeyError(f"no spinner called {name!r}")
self.text: "Union[RenderableType, Text]" = (
Text.from_markup(text) if isinstance(text, str) else text
)
self.frames = cast(List[str], spinner["frames"])[:]
self.interval = cast(float, spinner["interval"])
self.start_time: Optional[float] = None
self.style = style
self.speed = speed
self.frame_no_offset: float = 0.0
self._update_speed = 0.0
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
yield self.render(console.get_time())
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
text = self.render(0)
return Measurement.get(console, options, text)
def render(self, time: float) -> "RenderableType":
"""Render the spinner for a given time.
Args:
time (float): Time in seconds.
Returns:
RenderableType: A renderable containing animation frame.
"""
if self.start_time is None:
self.start_time = time
frame_no = ((time - self.start_time) * self.speed) / (
self.interval / 1000.0
) + self.frame_no_offset
frame = Text(
self.frames[int(frame_no) % len(self.frames)], style=self.style or ""
)
if self._update_speed:
self.frame_no_offset = frame_no
self.start_time = time
self.speed = self._update_speed
self._update_speed = 0.0
if not self.text:
return frame
elif isinstance(self.text, (str, Text)):
return Text.assemble(frame, " ", self.text)
else:
table = Table.grid(padding=1)
table.add_row(frame, self.text)
return table
def update(
self,
*,
text: "RenderableType" = "",
style: Optional["StyleType"] = None,
speed: Optional[float] = None,
) -> None:
"""Updates attributes of a spinner after it has been started.
Args:
text (RenderableType, optional): A renderable to display at the right of the spinner (str or Text typically). Defaults to "".
style (StyleType, optional): Style for spinner animation. Defaults to None.
speed (float, optional): Speed factor for animation. Defaults to None.
"""
if text:
self.text = Text.from_markup(text) if isinstance(text, str) else text
if style:
self.style = style
if speed:
self._update_speed = speed
if __name__ == "__main__": # pragma: no cover
from time import sleep
from .columns import Columns
from .panel import Panel
from .live import Live
all_spinners = Columns(
[
Spinner(spinner_name, text=Text(repr(spinner_name), style="green"))
for spinner_name in sorted(SPINNERS.keys())
],
column_first=True,
expand=True,
)
with Live(
Panel(all_spinners, title="Spinners", border_style="blue"),
refresh_per_second=20,
) as live:
while True:
sleep(0.1)

View file

@ -0,0 +1,132 @@
from types import TracebackType
from typing import Optional, Type
from .console import Console, RenderableType
from .jupyter import JupyterMixin
from .live import Live
from .spinner import Spinner
from .style import StyleType
class Status(JupyterMixin):
"""Displays a status indicator with a 'spinner' animation.
Args:
status (RenderableType): A status renderable (str or Text typically).
console (Console, optional): Console instance to use, or None for global console. Defaults to None.
spinner (str, optional): Name of spinner animation (see python -m rich.spinner). Defaults to "dots".
spinner_style (StyleType, optional): Style of spinner. Defaults to "status.spinner".
speed (float, optional): Speed factor for spinner animation. Defaults to 1.0.
refresh_per_second (float, optional): Number of refreshes per second. Defaults to 12.5.
"""
def __init__(
self,
status: RenderableType,
*,
console: Optional[Console] = None,
spinner: str = "dots",
spinner_style: StyleType = "status.spinner",
speed: float = 1.0,
refresh_per_second: float = 12.5,
):
self.status = status
self.spinner_style = spinner_style
self.speed = speed
self._spinner = Spinner(spinner, text=status, style=spinner_style, speed=speed)
self._live = Live(
self.renderable,
console=console,
refresh_per_second=refresh_per_second,
transient=True,
)
@property
def renderable(self) -> Spinner:
return self._spinner
@property
def console(self) -> "Console":
"""Get the Console used by the Status objects."""
return self._live.console
def update(
self,
status: Optional[RenderableType] = None,
*,
spinner: Optional[str] = None,
spinner_style: Optional[StyleType] = None,
speed: Optional[float] = None,
) -> None:
"""Update status.
Args:
status (Optional[RenderableType], optional): New status renderable or None for no change. Defaults to None.
spinner (Optional[str], optional): New spinner or None for no change. Defaults to None.
spinner_style (Optional[StyleType], optional): New spinner style or None for no change. Defaults to None.
speed (Optional[float], optional): Speed factor for spinner animation or None for no change. Defaults to None.
"""
if status is not None:
self.status = status
if spinner_style is not None:
self.spinner_style = spinner_style
if speed is not None:
self.speed = speed
if spinner is not None:
self._spinner = Spinner(
spinner, text=self.status, style=self.spinner_style, speed=self.speed
)
self._live.update(self.renderable, refresh=True)
else:
self._spinner.update(
text=self.status, style=self.spinner_style, speed=self.speed
)
def start(self) -> None:
"""Start the status animation."""
self._live.start()
def stop(self) -> None:
"""Stop the spinner animation."""
self._live.stop()
def __rich__(self) -> RenderableType:
return self.renderable
def __enter__(self) -> "Status":
self.start()
return self
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
self.stop()
if __name__ == "__main__": # pragma: no cover
from time import sleep
from .console import Console
console = Console()
with console.status("[magenta]Covid detector booting up") as status:
sleep(3)
console.log("Importing advanced AI")
sleep(3)
console.log("Advanced Covid AI Ready")
sleep(3)
status.update(status="[bold blue] Scanning for Covid", spinner="earth")
sleep(3)
console.log("Found 10,000,000,000 copies of Covid32.exe")
sleep(3)
status.update(
status="[bold red]Moving Covid32.exe to Trash",
spinner="bouncingBall",
spinner_style="yellow",
)
sleep(5)
console.print("[bold green]Covid deleted successfully")

View file

@ -0,0 +1,796 @@
import sys
from functools import lru_cache
from marshal import dumps, loads
from random import randint
from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast
from . import errors
from .color import Color, ColorParseError, ColorSystem, blend_rgb
from .repr import Result, rich_repr
from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme
# Style instances and style definitions are often interchangeable
StyleType = Union[str, "Style"]
class _Bit:
"""A descriptor to get/set a style attribute bit."""
__slots__ = ["bit"]
def __init__(self, bit_no: int) -> None:
self.bit = 1 << bit_no
def __get__(self, obj: "Style", objtype: Type["Style"]) -> Optional[bool]:
if obj._set_attributes & self.bit:
return obj._attributes & self.bit != 0
return None
@rich_repr
class Style:
"""A terminal style.
A terminal style consists of a color (`color`), a background color (`bgcolor`), and a number of attributes, such
as bold, italic etc. The attributes have 3 states: they can either be on
(``True``), off (``False``), or not set (``None``).
Args:
color (Union[Color, str], optional): Color of terminal text. Defaults to None.
bgcolor (Union[Color, str], optional): Color of terminal background. Defaults to None.
bold (bool, optional): Enable bold text. Defaults to None.
dim (bool, optional): Enable dim text. Defaults to None.
italic (bool, optional): Enable italic text. Defaults to None.
underline (bool, optional): Enable underlined text. Defaults to None.
blink (bool, optional): Enabled blinking text. Defaults to None.
blink2 (bool, optional): Enable fast blinking text. Defaults to None.
reverse (bool, optional): Enabled reverse text. Defaults to None.
conceal (bool, optional): Enable concealed text. Defaults to None.
strike (bool, optional): Enable strikethrough text. Defaults to None.
underline2 (bool, optional): Enable doubly underlined text. Defaults to None.
frame (bool, optional): Enable framed text. Defaults to None.
encircle (bool, optional): Enable encircled text. Defaults to None.
overline (bool, optional): Enable overlined text. Defaults to None.
link (str, link): Link URL. Defaults to None.
"""
_color: Optional[Color]
_bgcolor: Optional[Color]
_attributes: int
_set_attributes: int
_hash: Optional[int]
_null: bool
_meta: Optional[bytes]
__slots__ = [
"_color",
"_bgcolor",
"_attributes",
"_set_attributes",
"_link",
"_link_id",
"_ansi",
"_style_definition",
"_hash",
"_null",
"_meta",
]
# maps bits on to SGR parameter
_style_map = {
0: "1",
1: "2",
2: "3",
3: "4",
4: "5",
5: "6",
6: "7",
7: "8",
8: "9",
9: "21",
10: "51",
11: "52",
12: "53",
}
STYLE_ATTRIBUTES = {
"dim": "dim",
"d": "dim",
"bold": "bold",
"b": "bold",
"italic": "italic",
"i": "italic",
"underline": "underline",
"u": "underline",
"blink": "blink",
"blink2": "blink2",
"reverse": "reverse",
"r": "reverse",
"conceal": "conceal",
"c": "conceal",
"strike": "strike",
"s": "strike",
"underline2": "underline2",
"uu": "underline2",
"frame": "frame",
"encircle": "encircle",
"overline": "overline",
"o": "overline",
}
def __init__(
self,
*,
color: Optional[Union[Color, str]] = None,
bgcolor: Optional[Union[Color, str]] = None,
bold: Optional[bool] = None,
dim: Optional[bool] = None,
italic: Optional[bool] = None,
underline: Optional[bool] = None,
blink: Optional[bool] = None,
blink2: Optional[bool] = None,
reverse: Optional[bool] = None,
conceal: Optional[bool] = None,
strike: Optional[bool] = None,
underline2: Optional[bool] = None,
frame: Optional[bool] = None,
encircle: Optional[bool] = None,
overline: Optional[bool] = None,
link: Optional[str] = None,
meta: Optional[Dict[str, Any]] = None,
):
self._ansi: Optional[str] = None
self._style_definition: Optional[str] = None
def _make_color(color: Union[Color, str]) -> Color:
return color if isinstance(color, Color) else Color.parse(color)
self._color = None if color is None else _make_color(color)
self._bgcolor = None if bgcolor is None else _make_color(bgcolor)
self._set_attributes = sum(
(
bold is not None,
dim is not None and 2,
italic is not None and 4,
underline is not None and 8,
blink is not None and 16,
blink2 is not None and 32,
reverse is not None and 64,
conceal is not None and 128,
strike is not None and 256,
underline2 is not None and 512,
frame is not None and 1024,
encircle is not None and 2048,
overline is not None and 4096,
)
)
self._attributes = (
sum(
(
bold and 1 or 0,
dim and 2 or 0,
italic and 4 or 0,
underline and 8 or 0,
blink and 16 or 0,
blink2 and 32 or 0,
reverse and 64 or 0,
conceal and 128 or 0,
strike and 256 or 0,
underline2 and 512 or 0,
frame and 1024 or 0,
encircle and 2048 or 0,
overline and 4096 or 0,
)
)
if self._set_attributes
else 0
)
self._link = link
self._meta = None if meta is None else dumps(meta)
self._link_id = (
f"{randint(0, 999999)}{hash(self._meta)}" if (link or meta) else ""
)
self._hash: Optional[int] = None
self._null = not (self._set_attributes or color or bgcolor or link or meta)
@classmethod
def null(cls) -> "Style":
"""Create an 'null' style, equivalent to Style(), but more performant."""
return NULL_STYLE
@classmethod
def from_color(
cls, color: Optional[Color] = None, bgcolor: Optional[Color] = None
) -> "Style":
"""Create a new style with colors and no attributes.
Returns:
color (Optional[Color]): A (foreground) color, or None for no color. Defaults to None.
bgcolor (Optional[Color]): A (background) color, or None for no color. Defaults to None.
"""
style: Style = cls.__new__(Style)
style._ansi = None
style._style_definition = None
style._color = color
style._bgcolor = bgcolor
style._set_attributes = 0
style._attributes = 0
style._link = None
style._link_id = ""
style._meta = None
style._null = not (color or bgcolor)
style._hash = None
return style
@classmethod
def from_meta(cls, meta: Optional[Dict[str, Any]]) -> "Style":
"""Create a new style with meta data.
Returns:
meta (Optional[Dict[str, Any]]): A dictionary of meta data. Defaults to None.
"""
style: Style = cls.__new__(Style)
style._ansi = None
style._style_definition = None
style._color = None
style._bgcolor = None
style._set_attributes = 0
style._attributes = 0
style._link = None
style._meta = dumps(meta)
style._link_id = f"{randint(0, 999999)}{hash(style._meta)}"
style._hash = None
style._null = not (meta)
return style
@classmethod
def on(cls, meta: Optional[Dict[str, Any]] = None, **handlers: Any) -> "Style":
"""Create a blank style with meta information.
Example:
style = Style.on(click=self.on_click)
Args:
meta (Optional[Dict[str, Any]], optional): An optional dict of meta information.
**handlers (Any): Keyword arguments are translated in to handlers.
Returns:
Style: A Style with meta information attached.
"""
meta = {} if meta is None else meta
meta.update({f"@{key}": value for key, value in handlers.items()})
return cls.from_meta(meta)
bold = _Bit(0)
dim = _Bit(1)
italic = _Bit(2)
underline = _Bit(3)
blink = _Bit(4)
blink2 = _Bit(5)
reverse = _Bit(6)
conceal = _Bit(7)
strike = _Bit(8)
underline2 = _Bit(9)
frame = _Bit(10)
encircle = _Bit(11)
overline = _Bit(12)
@property
def link_id(self) -> str:
"""Get a link id, used in ansi code for links."""
return self._link_id
def __str__(self) -> str:
"""Re-generate style definition from attributes."""
if self._style_definition is None:
attributes: List[str] = []
append = attributes.append
bits = self._set_attributes
if bits & 0b0000000001111:
if bits & 1:
append("bold" if self.bold else "not bold")
if bits & (1 << 1):
append("dim" if self.dim else "not dim")
if bits & (1 << 2):
append("italic" if self.italic else "not italic")
if bits & (1 << 3):
append("underline" if self.underline else "not underline")
if bits & 0b0000111110000:
if bits & (1 << 4):
append("blink" if self.blink else "not blink")
if bits & (1 << 5):
append("blink2" if self.blink2 else "not blink2")
if bits & (1 << 6):
append("reverse" if self.reverse else "not reverse")
if bits & (1 << 7):
append("conceal" if self.conceal else "not conceal")
if bits & (1 << 8):
append("strike" if self.strike else "not strike")
if bits & 0b1111000000000:
if bits & (1 << 9):
append("underline2" if self.underline2 else "not underline2")
if bits & (1 << 10):
append("frame" if self.frame else "not frame")
if bits & (1 << 11):
append("encircle" if self.encircle else "not encircle")
if bits & (1 << 12):
append("overline" if self.overline else "not overline")
if self._color is not None:
append(self._color.name)
if self._bgcolor is not None:
append("on")
append(self._bgcolor.name)
if self._link:
append("link")
append(self._link)
self._style_definition = " ".join(attributes) or "none"
return self._style_definition
def __bool__(self) -> bool:
"""A Style is false if it has no attributes, colors, or links."""
return not self._null
def _make_ansi_codes(self, color_system: ColorSystem) -> str:
"""Generate ANSI codes for this style.
Args:
color_system (ColorSystem): Color system.
Returns:
str: String containing codes.
"""
if self._ansi is None:
sgr: List[str] = []
append = sgr.append
_style_map = self._style_map
attributes = self._attributes & self._set_attributes
if attributes:
if attributes & 1:
append(_style_map[0])
if attributes & 2:
append(_style_map[1])
if attributes & 4:
append(_style_map[2])
if attributes & 8:
append(_style_map[3])
if attributes & 0b0000111110000:
for bit in range(4, 9):
if attributes & (1 << bit):
append(_style_map[bit])
if attributes & 0b1111000000000:
for bit in range(9, 13):
if attributes & (1 << bit):
append(_style_map[bit])
if self._color is not None:
sgr.extend(self._color.downgrade(color_system).get_ansi_codes())
if self._bgcolor is not None:
sgr.extend(
self._bgcolor.downgrade(color_system).get_ansi_codes(
foreground=False
)
)
self._ansi = ";".join(sgr)
return self._ansi
@classmethod
@lru_cache(maxsize=1024)
def normalize(cls, style: str) -> str:
"""Normalize a style definition so that styles with the same effect have the same string
representation.
Args:
style (str): A style definition.
Returns:
str: Normal form of style definition.
"""
try:
return str(cls.parse(style))
except errors.StyleSyntaxError:
return style.strip().lower()
@classmethod
def pick_first(cls, *values: Optional[StyleType]) -> StyleType:
"""Pick first non-None style."""
for value in values:
if value is not None:
return value
raise ValueError("expected at least one non-None style")
def __rich_repr__(self) -> Result:
yield "color", self.color, None
yield "bgcolor", self.bgcolor, None
yield "bold", self.bold, None,
yield "dim", self.dim, None,
yield "italic", self.italic, None
yield "underline", self.underline, None,
yield "blink", self.blink, None
yield "blink2", self.blink2, None
yield "reverse", self.reverse, None
yield "conceal", self.conceal, None
yield "strike", self.strike, None
yield "underline2", self.underline2, None
yield "frame", self.frame, None
yield "encircle", self.encircle, None
yield "link", self.link, None
if self._meta:
yield "meta", self.meta
def __eq__(self, other: Any) -> bool:
if not isinstance(other, Style):
return NotImplemented
return self.__hash__() == other.__hash__()
def __ne__(self, other: Any) -> bool:
if not isinstance(other, Style):
return NotImplemented
return self.__hash__() != other.__hash__()
def __hash__(self) -> int:
if self._hash is not None:
return self._hash
self._hash = hash(
(
self._color,
self._bgcolor,
self._attributes,
self._set_attributes,
self._link,
self._meta,
)
)
return self._hash
@property
def color(self) -> Optional[Color]:
"""The foreground color or None if it is not set."""
return self._color
@property
def bgcolor(self) -> Optional[Color]:
"""The background color or None if it is not set."""
return self._bgcolor
@property
def link(self) -> Optional[str]:
"""Link text, if set."""
return self._link
@property
def transparent_background(self) -> bool:
"""Check if the style specified a transparent background."""
return self.bgcolor is None or self.bgcolor.is_default
@property
def background_style(self) -> "Style":
"""A Style with background only."""
return Style(bgcolor=self.bgcolor)
@property
def meta(self) -> Dict[str, Any]:
"""Get meta information (can not be changed after construction)."""
return {} if self._meta is None else cast(Dict[str, Any], loads(self._meta))
@property
def without_color(self) -> "Style":
"""Get a copy of the style with color removed."""
if self._null:
return NULL_STYLE
style: Style = self.__new__(Style)
style._ansi = None
style._style_definition = None
style._color = None
style._bgcolor = None
style._attributes = self._attributes
style._set_attributes = self._set_attributes
style._link = self._link
style._link_id = f"{randint(0, 999999)}" if self._link else ""
style._null = False
style._meta = None
style._hash = None
return style
@classmethod
@lru_cache(maxsize=4096)
def parse(cls, style_definition: str) -> "Style":
"""Parse a style definition.
Args:
style_definition (str): A string containing a style.
Raises:
errors.StyleSyntaxError: If the style definition syntax is invalid.
Returns:
`Style`: A Style instance.
"""
if style_definition.strip() == "none" or not style_definition:
return cls.null()
STYLE_ATTRIBUTES = cls.STYLE_ATTRIBUTES
color: Optional[str] = None
bgcolor: Optional[str] = None
attributes: Dict[str, Optional[Any]] = {}
link: Optional[str] = None
words = iter(style_definition.split())
for original_word in words:
word = original_word.lower()
if word == "on":
word = next(words, "")
if not word:
raise errors.StyleSyntaxError("color expected after 'on'")
try:
Color.parse(word) is None
except ColorParseError as error:
raise errors.StyleSyntaxError(
f"unable to parse {word!r} as background color; {error}"
) from None
bgcolor = word
elif word == "not":
word = next(words, "")
attribute = STYLE_ATTRIBUTES.get(word)
if attribute is None:
raise errors.StyleSyntaxError(
f"expected style attribute after 'not', found {word!r}"
)
attributes[attribute] = False
elif word == "link":
word = next(words, "")
if not word:
raise errors.StyleSyntaxError("URL expected after 'link'")
link = word
elif word in STYLE_ATTRIBUTES:
attributes[STYLE_ATTRIBUTES[word]] = True
else:
try:
Color.parse(word)
except ColorParseError as error:
raise errors.StyleSyntaxError(
f"unable to parse {word!r} as color; {error}"
) from None
color = word
style = Style(color=color, bgcolor=bgcolor, link=link, **attributes)
return style
@lru_cache(maxsize=1024)
def get_html_style(self, theme: Optional[TerminalTheme] = None) -> str:
"""Get a CSS style rule."""
theme = theme or DEFAULT_TERMINAL_THEME
css: List[str] = []
append = css.append
color = self.color
bgcolor = self.bgcolor
if self.reverse:
color, bgcolor = bgcolor, color
if self.dim:
foreground_color = (
theme.foreground_color if color is None else color.get_truecolor(theme)
)
color = Color.from_triplet(
blend_rgb(foreground_color, theme.background_color, 0.5)
)
if color is not None:
theme_color = color.get_truecolor(theme)
append(f"color: {theme_color.hex}")
append(f"text-decoration-color: {theme_color.hex}")
if bgcolor is not None:
theme_color = bgcolor.get_truecolor(theme, foreground=False)
append(f"background-color: {theme_color.hex}")
if self.bold:
append("font-weight: bold")
if self.italic:
append("font-style: italic")
if self.underline:
append("text-decoration: underline")
if self.strike:
append("text-decoration: line-through")
if self.overline:
append("text-decoration: overline")
return "; ".join(css)
@classmethod
def combine(cls, styles: Iterable["Style"]) -> "Style":
"""Combine styles and get result.
Args:
styles (Iterable[Style]): Styles to combine.
Returns:
Style: A new style instance.
"""
iter_styles = iter(styles)
return sum(iter_styles, next(iter_styles))
@classmethod
def chain(cls, *styles: "Style") -> "Style":
"""Combine styles from positional argument in to a single style.
Args:
*styles (Iterable[Style]): Styles to combine.
Returns:
Style: A new style instance.
"""
iter_styles = iter(styles)
return sum(iter_styles, next(iter_styles))
def copy(self) -> "Style":
"""Get a copy of this style.
Returns:
Style: A new Style instance with identical attributes.
"""
if self._null:
return NULL_STYLE
style: Style = self.__new__(Style)
style._ansi = self._ansi
style._style_definition = self._style_definition
style._color = self._color
style._bgcolor = self._bgcolor
style._attributes = self._attributes
style._set_attributes = self._set_attributes
style._link = self._link
style._link_id = f"{randint(0, 999999)}" if self._link else ""
style._hash = self._hash
style._null = False
style._meta = self._meta
return style
@lru_cache(maxsize=128)
def clear_meta_and_links(self) -> "Style":
"""Get a copy of this style with link and meta information removed.
Returns:
Style: New style object.
"""
if self._null:
return NULL_STYLE
style: Style = self.__new__(Style)
style._ansi = self._ansi
style._style_definition = self._style_definition
style._color = self._color
style._bgcolor = self._bgcolor
style._attributes = self._attributes
style._set_attributes = self._set_attributes
style._link = None
style._link_id = ""
style._hash = self._hash
style._null = False
style._meta = None
return style
def update_link(self, link: Optional[str] = None) -> "Style":
"""Get a copy with a different value for link.
Args:
link (str, optional): New value for link. Defaults to None.
Returns:
Style: A new Style instance.
"""
style: Style = self.__new__(Style)
style._ansi = self._ansi
style._style_definition = self._style_definition
style._color = self._color
style._bgcolor = self._bgcolor
style._attributes = self._attributes
style._set_attributes = self._set_attributes
style._link = link
style._link_id = f"{randint(0, 999999)}" if link else ""
style._hash = None
style._null = False
style._meta = self._meta
return style
def render(
self,
text: str = "",
*,
color_system: Optional[ColorSystem] = ColorSystem.TRUECOLOR,
legacy_windows: bool = False,
) -> str:
"""Render the ANSI codes for the style.
Args:
text (str, optional): A string to style. Defaults to "".
color_system (Optional[ColorSystem], optional): Color system to render to. Defaults to ColorSystem.TRUECOLOR.
Returns:
str: A string containing ANSI style codes.
"""
if not text or color_system is None:
return text
attrs = self._ansi or self._make_ansi_codes(color_system)
rendered = f"\x1b[{attrs}m{text}\x1b[0m" if attrs else text
if self._link and not legacy_windows:
rendered = (
f"\x1b]8;id={self._link_id};{self._link}\x1b\\{rendered}\x1b]8;;\x1b\\"
)
return rendered
def test(self, text: Optional[str] = None) -> None:
"""Write text with style directly to terminal.
This method is for testing purposes only.
Args:
text (Optional[str], optional): Text to style or None for style name.
"""
text = text or str(self)
sys.stdout.write(f"{self.render(text)}\n")
@lru_cache(maxsize=1024)
def _add(self, style: Optional["Style"]) -> "Style":
if style is None or style._null:
return self
if self._null:
return style
new_style: Style = self.__new__(Style)
new_style._ansi = None
new_style._style_definition = None
new_style._color = style._color or self._color
new_style._bgcolor = style._bgcolor or self._bgcolor
new_style._attributes = (self._attributes & ~style._set_attributes) | (
style._attributes & style._set_attributes
)
new_style._set_attributes = self._set_attributes | style._set_attributes
new_style._link = style._link or self._link
new_style._link_id = style._link_id or self._link_id
new_style._null = style._null
if self._meta and style._meta:
new_style._meta = dumps({**self.meta, **style.meta})
else:
new_style._meta = self._meta or style._meta
new_style._hash = None
return new_style
def __add__(self, style: Optional["Style"]) -> "Style":
combined_style = self._add(style)
return combined_style.copy() if combined_style.link else combined_style
NULL_STYLE = Style()
class StyleStack:
"""A stack of styles."""
__slots__ = ["_stack"]
def __init__(self, default_style: "Style") -> None:
self._stack: List[Style] = [default_style]
def __repr__(self) -> str:
return f"<stylestack {self._stack!r}>"
@property
def current(self) -> Style:
"""Get the Style at the top of the stack."""
return self._stack[-1]
def push(self, style: Style) -> None:
"""Push a new style on to the stack.
Args:
style (Style): New style to combine with current style.
"""
self._stack.append(self._stack[-1] + style)
def pop(self) -> Style:
"""Pop last style and discard.
Returns:
Style: New current style (also available as stack.current)
"""
self._stack.pop()
return self._stack[-1]

View file

@ -0,0 +1,42 @@
from typing import TYPE_CHECKING
from .measure import Measurement
from .segment import Segment
from .style import StyleType
if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderResult, RenderableType
class Styled:
"""Apply a style to a renderable.
Args:
renderable (RenderableType): Any renderable.
style (StyleType): A style to apply across the entire renderable.
"""
def __init__(self, renderable: "RenderableType", style: "StyleType") -> None:
self.renderable = renderable
self.style = style
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
style = console.get_style(self.style)
rendered_segments = console.render(self.renderable, options)
segments = Segment.apply_style(rendered_segments, style)
return segments
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
return Measurement.get(console, options, self.renderable)
if __name__ == "__main__": # pragma: no cover
from pip._vendor.rich import print
from pip._vendor.rich.panel import Panel
panel = Styled(Panel("hello"), "on blue")
print(panel)

View file

@ -0,0 +1,948 @@
import os.path
import platform
import re
import sys
import textwrap
from abc import ABC, abstractmethod
from pathlib import Path
from typing import (
Any,
Dict,
Iterable,
List,
NamedTuple,
Optional,
Sequence,
Set,
Tuple,
Type,
Union,
)
from pip._vendor.pygments.lexer import Lexer
from pip._vendor.pygments.lexers import get_lexer_by_name, guess_lexer_for_filename
from pip._vendor.pygments.style import Style as PygmentsStyle
from pip._vendor.pygments.styles import get_style_by_name
from pip._vendor.pygments.token import (
Comment,
Error,
Generic,
Keyword,
Name,
Number,
Operator,
String,
Token,
Whitespace,
)
from pip._vendor.pygments.util import ClassNotFound
from pip._vendor.rich.containers import Lines
from pip._vendor.rich.padding import Padding, PaddingDimensions
from ._loop import loop_first
from .cells import cell_len
from .color import Color, blend_rgb
from .console import Console, ConsoleOptions, JustifyMethod, RenderResult
from .jupyter import JupyterMixin
from .measure import Measurement
from .segment import Segment, Segments
from .style import Style, StyleType
from .text import Text
TokenType = Tuple[str, ...]
WINDOWS = platform.system() == "Windows"
DEFAULT_THEME = "monokai"
# The following styles are based on https://github.com/pygments/pygments/blob/master/pygments/formatters/terminal.py
# A few modifications were made
ANSI_LIGHT: Dict[TokenType, Style] = {
Token: Style(),
Whitespace: Style(color="white"),
Comment: Style(dim=True),
Comment.Preproc: Style(color="cyan"),
Keyword: Style(color="blue"),
Keyword.Type: Style(color="cyan"),
Operator.Word: Style(color="magenta"),
Name.Builtin: Style(color="cyan"),
Name.Function: Style(color="green"),
Name.Namespace: Style(color="cyan", underline=True),
Name.Class: Style(color="green", underline=True),
Name.Exception: Style(color="cyan"),
Name.Decorator: Style(color="magenta", bold=True),
Name.Variable: Style(color="red"),
Name.Constant: Style(color="red"),
Name.Attribute: Style(color="cyan"),
Name.Tag: Style(color="bright_blue"),
String: Style(color="yellow"),
Number: Style(color="blue"),
Generic.Deleted: Style(color="bright_red"),
Generic.Inserted: Style(color="green"),
Generic.Heading: Style(bold=True),
Generic.Subheading: Style(color="magenta", bold=True),
Generic.Prompt: Style(bold=True),
Generic.Error: Style(color="bright_red"),
Error: Style(color="red", underline=True),
}
ANSI_DARK: Dict[TokenType, Style] = {
Token: Style(),
Whitespace: Style(color="bright_black"),
Comment: Style(dim=True),
Comment.Preproc: Style(color="bright_cyan"),
Keyword: Style(color="bright_blue"),
Keyword.Type: Style(color="bright_cyan"),
Operator.Word: Style(color="bright_magenta"),
Name.Builtin: Style(color="bright_cyan"),
Name.Function: Style(color="bright_green"),
Name.Namespace: Style(color="bright_cyan", underline=True),
Name.Class: Style(color="bright_green", underline=True),
Name.Exception: Style(color="bright_cyan"),
Name.Decorator: Style(color="bright_magenta", bold=True),
Name.Variable: Style(color="bright_red"),
Name.Constant: Style(color="bright_red"),
Name.Attribute: Style(color="bright_cyan"),
Name.Tag: Style(color="bright_blue"),
String: Style(color="yellow"),
Number: Style(color="bright_blue"),
Generic.Deleted: Style(color="bright_red"),
Generic.Inserted: Style(color="bright_green"),
Generic.Heading: Style(bold=True),
Generic.Subheading: Style(color="bright_magenta", bold=True),
Generic.Prompt: Style(bold=True),
Generic.Error: Style(color="bright_red"),
Error: Style(color="red", underline=True),
}
RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK}
NUMBERS_COLUMN_DEFAULT_PADDING = 2
class SyntaxTheme(ABC):
"""Base class for a syntax theme."""
@abstractmethod
def get_style_for_token(self, token_type: TokenType) -> Style:
"""Get a style for a given Pygments token."""
raise NotImplementedError # pragma: no cover
@abstractmethod
def get_background_style(self) -> Style:
"""Get the background color."""
raise NotImplementedError # pragma: no cover
class PygmentsSyntaxTheme(SyntaxTheme):
"""Syntax theme that delegates to Pygments theme."""
def __init__(self, theme: Union[str, Type[PygmentsStyle]]) -> None:
self._style_cache: Dict[TokenType, Style] = {}
if isinstance(theme, str):
try:
self._pygments_style_class = get_style_by_name(theme)
except ClassNotFound:
self._pygments_style_class = get_style_by_name("default")
else:
self._pygments_style_class = theme
self._background_color = self._pygments_style_class.background_color
self._background_style = Style(bgcolor=self._background_color)
def get_style_for_token(self, token_type: TokenType) -> Style:
"""Get a style from a Pygments class."""
try:
return self._style_cache[token_type]
except KeyError:
try:
pygments_style = self._pygments_style_class.style_for_token(token_type)
except KeyError:
style = Style.null()
else:
color = pygments_style["color"]
bgcolor = pygments_style["bgcolor"]
style = Style(
color="#" + color if color else "#000000",
bgcolor="#" + bgcolor if bgcolor else self._background_color,
bold=pygments_style["bold"],
italic=pygments_style["italic"],
underline=pygments_style["underline"],
)
self._style_cache[token_type] = style
return style
def get_background_style(self) -> Style:
return self._background_style
class ANSISyntaxTheme(SyntaxTheme):
"""Syntax theme to use standard colors."""
def __init__(self, style_map: Dict[TokenType, Style]) -> None:
self.style_map = style_map
self._missing_style = Style.null()
self._background_style = Style.null()
self._style_cache: Dict[TokenType, Style] = {}
def get_style_for_token(self, token_type: TokenType) -> Style:
"""Look up style in the style map."""
try:
return self._style_cache[token_type]
except KeyError:
# Styles form a hierarchy
# We need to go from most to least specific
# e.g. ("foo", "bar", "baz") to ("foo", "bar") to ("foo",)
get_style = self.style_map.get
token = tuple(token_type)
style = self._missing_style
while token:
_style = get_style(token)
if _style is not None:
style = _style
break
token = token[:-1]
self._style_cache[token_type] = style
return style
def get_background_style(self) -> Style:
return self._background_style
SyntaxPosition = Tuple[int, int]
class _SyntaxHighlightRange(NamedTuple):
"""
A range to highlight in a Syntax object.
`start` and `end` are 2-integers tuples, where the first integer is the line number
(starting from 1) and the second integer is the column index (starting from 0).
"""
style: StyleType
start: SyntaxPosition
end: SyntaxPosition
class Syntax(JupyterMixin):
"""Construct a Syntax object to render syntax highlighted code.
Args:
code (str): Code to highlight.
lexer (Lexer | str): Lexer to use (see https://pygments.org/docs/lexers/)
theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "monokai".
dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False.
line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.
start_line (int, optional): Starting number for line numbers. Defaults to 1.
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.
word_wrap (bool, optional): Enable word wrapping.
background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
indent_guides (bool, optional): Show indent guides. Defaults to False.
padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).
"""
_pygments_style_class: Type[PygmentsStyle]
_theme: SyntaxTheme
@classmethod
def get_theme(cls, name: Union[str, SyntaxTheme]) -> SyntaxTheme:
"""Get a syntax theme instance."""
if isinstance(name, SyntaxTheme):
return name
theme: SyntaxTheme
if name in RICH_SYNTAX_THEMES:
theme = ANSISyntaxTheme(RICH_SYNTAX_THEMES[name])
else:
theme = PygmentsSyntaxTheme(name)
return theme
def __init__(
self,
code: str,
lexer: Union[Lexer, str],
*,
theme: Union[str, SyntaxTheme] = DEFAULT_THEME,
dedent: bool = False,
line_numbers: bool = False,
start_line: int = 1,
line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,
highlight_lines: Optional[Set[int]] = None,
code_width: Optional[int] = None,
tab_size: int = 4,
word_wrap: bool = False,
background_color: Optional[str] = None,
indent_guides: bool = False,
padding: PaddingDimensions = 0,
) -> None:
self.code = code
self._lexer = lexer
self.dedent = dedent
self.line_numbers = line_numbers
self.start_line = start_line
self.line_range = line_range
self.highlight_lines = highlight_lines or set()
self.code_width = code_width
self.tab_size = tab_size
self.word_wrap = word_wrap
self.background_color = background_color
self.background_style = (
Style(bgcolor=background_color) if background_color else Style()
)
self.indent_guides = indent_guides
self.padding = padding
self._theme = self.get_theme(theme)
self._stylized_ranges: List[_SyntaxHighlightRange] = []
@classmethod
def from_path(
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,
line_range: Optional[Tuple[int, int]] = None,
start_line: int = 1,
highlight_lines: Optional[Set[int]] = None,
code_width: Optional[int] = None,
tab_size: int = 4,
word_wrap: bool = False,
background_color: Optional[str] = None,
indent_guides: bool = False,
padding: PaddingDimensions = 0,
) -> "Syntax":
"""Construct a Syntax object from a file.
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.
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.
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.
word_wrap (bool, optional): Enable word wrapping of code.
background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
indent_guides (bool, optional): Show indent guides. Defaults to False.
padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).
Returns:
[Syntax]: A Syntax object that may be printed to the console
"""
code = Path(path).read_text(encoding=encoding)
if not lexer:
lexer = cls.guess_lexer(path, code=code)
return cls(
code,
lexer,
theme=theme,
dedent=dedent,
line_numbers=line_numbers,
line_range=line_range,
start_line=start_line,
highlight_lines=highlight_lines,
code_width=code_width,
tab_size=tab_size,
word_wrap=word_wrap,
background_color=background_color,
indent_guides=indent_guides,
padding=padding,
)
@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
return default_style
def _get_token_color(self, token_type: TokenType) -> Optional[Color]:
"""Get a color (if any) for the given token.
Args:
token_type (TokenType): A token type tuple from Pygments.
Returns:
Optional[Color]: Color from theme, or None for no color.
"""
style = self._theme.get_style_for_token(token_type)
return style.color
@property
def lexer(self) -> Optional[Lexer]:
"""The lexer for this syntax, or None if no lexer was found.
Tries to find the lexer by name if a string was passed to the constructor.
"""
if isinstance(self._lexer, Lexer):
return self._lexer
try:
return get_lexer_by_name(
self._lexer,
stripnl=False,
ensurenl=True,
tabsize=self.tab_size,
)
except ClassNotFound:
return None
def highlight(
self,
code: str,
line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,
) -> Text:
"""Highlight code and return a Text instance.
Args:
code (str): Code to highlight.
line_range(Tuple[int, int], optional): Optional line range to highlight.
Returns:
Text: A text instance containing highlighted syntax.
"""
base_style = self._get_base_style()
justify: JustifyMethod = (
"default" if base_style.transparent_background else "left"
)
text = Text(
justify=justify,
style=base_style,
tab_size=self.tab_size,
no_wrap=not self.word_wrap,
)
_get_theme_style = self._theme.get_style_for_token
lexer = self.lexer
if lexer is None:
text.append(code)
else:
if line_range:
# More complicated path to only stylize a portion of the code
# This speeds up further operations as there are less spans to process
line_start, line_end = line_range
def line_tokenize() -> Iterable[Tuple[Any, str]]:
"""Split tokens to one per line."""
assert lexer # required to make MyPy happy - we know lexer is not None at this point
for token_type, token in lexer.get_tokens(code):
while token:
line_token, new_line, token = token.partition("\n")
yield token_type, line_token + new_line
def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]:
"""Convert tokens to spans."""
tokens = iter(line_tokenize())
line_no = 0
_line_start = line_start - 1 if line_start else 0
# Skip over tokens until line start
while line_no < _line_start:
try:
_token_type, token = next(tokens)
except StopIteration:
break
yield (token, None)
if token.endswith("\n"):
line_no += 1
# Generate spans until line end
for token_type, token in tokens:
yield (token, _get_theme_style(token_type))
if token.endswith("\n"):
line_no += 1
if line_end and line_no >= line_end:
break
text.append_tokens(tokens_to_spans())
else:
text.append_tokens(
(token, _get_theme_style(token_type))
for token_type, token in lexer.get_tokens(code)
)
if self.background_color is not None:
text.stylize(f"on {self.background_color}")
if self._stylized_ranges:
self._apply_stylized_ranges(text)
return text
def stylize_range(
self, style: StyleType, start: SyntaxPosition, end: SyntaxPosition
) -> None:
"""
Adds a custom style on a part of the code, that will be applied to the syntax display when it's rendered.
Line numbers are 1-based, while column indexes are 0-based.
Args:
style (StyleType): The style to apply.
start (Tuple[int, int]): The start of the range, in the form `[line number, column index]`.
end (Tuple[int, int]): The end of the range, in the form `[line number, column index]`.
"""
self._stylized_ranges.append(_SyntaxHighlightRange(style, start, end))
def _get_line_numbers_color(self, blend: float = 0.3) -> Color:
background_style = self._theme.get_background_style() + self.background_style
background_color = background_style.bgcolor
if background_color is None or background_color.is_system_defined:
return Color.default()
foreground_color = self._get_token_color(Token.Text)
if foreground_color is None or foreground_color.is_system_defined:
return foreground_color or Color.default()
new_color = blend_rgb(
background_color.get_truecolor(),
foreground_color.get_truecolor(),
cross_fade=blend,
)
return Color.from_triplet(new_color)
@property
def _numbers_column_width(self) -> int:
"""Get the number of characters used to render the numbers column."""
column_width = 0
if self.line_numbers:
column_width = (
len(str(self.start_line + self.code.count("\n")))
+ NUMBERS_COLUMN_DEFAULT_PADDING
)
return column_width
def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]:
"""Get background, number, and highlight styles for line numbers."""
background_style = self._get_base_style()
if background_style.transparent_background:
return Style.null(), Style(dim=True), Style.null()
if console.color_system in ("256", "truecolor"):
number_style = Style.chain(
background_style,
self._theme.get_style_for_token(Token.Text),
Style(color=self._get_line_numbers_color()),
self.background_style,
)
highlight_number_style = Style.chain(
background_style,
self._theme.get_style_for_token(Token.Text),
Style(bold=True, color=self._get_line_numbers_color(0.9)),
self.background_style,
)
else:
number_style = background_style + Style(dim=True)
highlight_number_style = background_style + Style(dim=False)
return background_style, number_style, highlight_number_style
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
_, right, _, left = Padding.unpack(self.padding)
padding = left + right
if self.code_width is not None:
width = self.code_width + self._numbers_column_width + padding + 1
return Measurement(self._numbers_column_width, width)
lines = self.code.splitlines()
width = (
self._numbers_column_width
+ padding
+ (max(cell_len(line) for line in lines) if lines else 0)
)
if self.line_numbers:
width += 1
return Measurement(self._numbers_column_width, width)
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
segments = Segments(self._get_syntax(console, options))
if self.padding:
yield Padding(
segments, style=self._theme.get_background_style(), pad=self.padding
)
else:
yield segments
def _get_syntax(
self,
console: Console,
options: ConsoleOptions,
) -> Iterable[Segment]:
"""
Get the Segments for the Syntax object, excluding any vertical/horizontal padding
"""
transparent_background = self._get_base_style().transparent_background
code_width = (
(
(options.max_width - self._numbers_column_width - 1)
if self.line_numbers
else options.max_width
)
if self.code_width is None
else self.code_width
)
ends_on_nl, processed_code = self._process_code(self.code)
text = self.highlight(processed_code, self.line_range)
if not self.line_numbers and not self.word_wrap and not self.line_range:
if not ends_on_nl:
text.remove_suffix("\n")
# Simple case of just rendering text
style = (
self._get_base_style()
+ self._theme.get_style_for_token(Comment)
+ Style(dim=True)
+ self.background_style
)
if self.indent_guides and not options.ascii_only:
text = text.with_indent_guides(self.tab_size, style=style)
text.overflow = "crop"
if style.transparent_background:
yield from console.render(
text, options=options.update(width=code_width)
)
else:
syntax_lines = console.render_lines(
text,
options.update(width=code_width, height=None, justify="left"),
style=self.background_style,
pad=True,
new_lines=True,
)
for syntax_line in syntax_lines:
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:
if line_offset > len(lines):
return
lines = lines[line_offset:end_line]
if self.indent_guides and not options.ascii_only:
style = (
self._get_base_style()
+ self._theme.get_style_for_token(Comment)
+ Style(dim=True)
+ self.background_style
)
lines = (
Text("\n")
.join(lines)
.with_indent_guides(self.tab_size, style=style + Style(italic=False))
.split("\n", allow_blank=True)
)
numbers_column_width = self._numbers_column_width
render_options = options.update(width=code_width)
highlight_line = self.highlight_lines.__contains__
_Segment = Segment
new_line = _Segment("\n")
line_pointer = "> " if options.legacy_windows else ""
(
background_style,
number_style,
highlight_number_style,
) = self._get_number_styles(console)
for line_no, line in enumerate(lines, self.start_line + line_offset):
if self.word_wrap:
wrapped_lines = console.render_lines(
line,
render_options.update(height=None, justify="left"),
style=background_style,
pad=not transparent_background,
)
else:
segments = list(line.render(console, end=""))
if options.no_wrap:
wrapped_lines = [segments]
else:
wrapped_lines = [
_Segment.adjust_line_length(
segments,
render_options.max_width,
style=background_style,
pad=not transparent_background,
)
]
if self.line_numbers:
wrapped_line_left_pad = _Segment(
" " * numbers_column_width + " ", background_style
)
for first, wrapped_line in loop_first(wrapped_lines):
if first:
line_column = str(line_no).rjust(numbers_column_width - 2) + " "
if highlight_line(line_no):
yield _Segment(line_pointer, Style(color="red"))
yield _Segment(line_column, highlight_number_style)
else:
yield _Segment(" ", highlight_number_style)
yield _Segment(line_column, number_style)
else:
yield wrapped_line_left_pad
yield from wrapped_line
yield new_line
else:
for wrapped_line in wrapped_lines:
yield from wrapped_line
yield new_line
def _apply_stylized_ranges(self, text: Text) -> None:
"""
Apply stylized ranges to a text instance,
using the given code to determine the right portion to apply the style to.
Args:
text (Text): Text instance to apply the style to.
"""
code = text.plain
newlines_offsets = [
# Let's add outer boundaries at each side of the list:
0,
# N.B. using "\n" here is much faster than using metacharacters such as "^" or "\Z":
*[
match.start() + 1
for match in re.finditer("\n", code, flags=re.MULTILINE)
],
len(code) + 1,
]
for stylized_range in self._stylized_ranges:
start = _get_code_index_for_syntax_position(
newlines_offsets, stylized_range.start
)
end = _get_code_index_for_syntax_position(
newlines_offsets, stylized_range.end
)
if start is not None and end is not None:
text.stylize(stylized_range.style, start, end)
def _process_code(self, code: str) -> Tuple[bool, str]:
"""
Applies various processing to a raw code string
(normalises it so it always ends with a line return, dedents it if necessary, etc.)
Args:
code (str): The raw code string to process
Returns:
Tuple[bool, str]: the boolean indicates whether the raw code ends with a line return,
while the string is the processed code.
"""
ends_on_nl = code.endswith("\n")
processed_code = code if ends_on_nl else code + "\n"
processed_code = (
textwrap.dedent(processed_code) if self.dedent else processed_code
)
processed_code = processed_code.expandtabs(self.tab_size)
return ends_on_nl, processed_code
def _get_code_index_for_syntax_position(
newlines_offsets: Sequence[int], position: SyntaxPosition
) -> Optional[int]:
"""
Returns the index of the code string for the given positions.
Args:
newlines_offsets (Sequence[int]): The offset of each newline character found in the code snippet.
position (SyntaxPosition): The position to search for.
Returns:
Optional[int]: The index of the code string for this position, or `None`
if the given position's line number is out of range (if it's the column that is out of range
we silently clamp its value so that it reaches the end of the line)
"""
lines_count = len(newlines_offsets)
line_number, column_index = position
if line_number > lines_count or len(newlines_offsets) < (line_number + 1):
return None # `line_number` is out of range
line_index = line_number - 1
line_length = newlines_offsets[line_index + 1] - newlines_offsets[line_index] - 1
# If `column_index` is out of range: let's silently clamp it:
column_index = min(line_length, column_index)
return newlines_offsets[line_index] + column_index
if __name__ == "__main__": # pragma: no cover
import argparse
import sys
parser = argparse.ArgumentParser(
description="Render syntax to the console with Rich"
)
parser.add_argument(
"path",
metavar="PATH",
help="path to file, or - for stdin",
)
parser.add_argument(
"-c",
"--force-color",
dest="force_color",
action="store_true",
default=None,
help="force color for non-terminals",
)
parser.add_argument(
"-i",
"--indent-guides",
dest="indent_guides",
action="store_true",
default=False,
help="display indent guides",
)
parser.add_argument(
"-l",
"--line-numbers",
dest="line_numbers",
action="store_true",
help="render line numbers",
)
parser.add_argument(
"-w",
"--width",
type=int,
dest="width",
default=None,
help="width of output (default will auto-detect)",
)
parser.add_argument(
"-r",
"--wrap",
dest="word_wrap",
action="store_true",
default=False,
help="word wrap long lines",
)
parser.add_argument(
"-s",
"--soft-wrap",
action="store_true",
dest="soft_wrap",
default=False,
help="enable soft wrapping mode",
)
parser.add_argument(
"-t", "--theme", dest="theme", default="monokai", help="pygments theme"
)
parser.add_argument(
"-b",
"--background-color",
dest="background_color",
default=None,
help="Override background color",
)
parser.add_argument(
"-x",
"--lexer",
default=None,
dest="lexer_name",
help="Lexer name",
)
parser.add_argument(
"-p", "--padding", type=int, default=0, dest="padding", help="Padding"
)
parser.add_argument(
"--highlight-line",
type=int,
default=None,
dest="highlight_line",
help="The line number (not index!) to highlight",
)
args = parser.parse_args()
from pip._vendor.rich.console import Console
console = Console(force_terminal=args.force_color, width=args.width)
if args.path == "-":
code = sys.stdin.read()
syntax = Syntax(
code=code,
lexer=args.lexer_name,
line_numbers=args.line_numbers,
word_wrap=args.word_wrap,
theme=args.theme,
background_color=args.background_color,
indent_guides=args.indent_guides,
padding=args.padding,
highlight_lines={args.highlight_line},
)
else:
syntax = Syntax.from_path(
args.path,
lexer=args.lexer_name,
line_numbers=args.line_numbers,
word_wrap=args.word_wrap,
theme=args.theme,
background_color=args.background_color,
indent_guides=args.indent_guides,
padding=args.padding,
highlight_lines={args.highlight_line},
)
console.print(syntax, soft_wrap=args.soft_wrap)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,153 @@
from typing import List, Optional, Tuple
from .color_triplet import ColorTriplet
from .palette import Palette
_ColorTuple = Tuple[int, int, int]
class TerminalTheme:
"""A color theme used when exporting console content.
Args:
background (Tuple[int, int, int]): The background color.
foreground (Tuple[int, int, int]): The foreground (text) color.
normal (List[Tuple[int, int, int]]): A list of 8 normal intensity colors.
bright (List[Tuple[int, int, int]], optional): A list of 8 bright colors, or None
to repeat normal intensity. Defaults to None.
"""
def __init__(
self,
background: _ColorTuple,
foreground: _ColorTuple,
normal: List[_ColorTuple],
bright: Optional[List[_ColorTuple]] = None,
) -> None:
self.background_color = ColorTriplet(*background)
self.foreground_color = ColorTriplet(*foreground)
self.ansi_colors = Palette(normal + (bright or normal))
DEFAULT_TERMINAL_THEME = TerminalTheme(
(255, 255, 255),
(0, 0, 0),
[
(0, 0, 0),
(128, 0, 0),
(0, 128, 0),
(128, 128, 0),
(0, 0, 128),
(128, 0, 128),
(0, 128, 128),
(192, 192, 192),
],
[
(128, 128, 128),
(255, 0, 0),
(0, 255, 0),
(255, 255, 0),
(0, 0, 255),
(255, 0, 255),
(0, 255, 255),
(255, 255, 255),
],
)
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),
],
)
SVG_EXPORT_THEME = TerminalTheme(
(41, 41, 41),
(197, 200, 198),
[
(75, 78, 85),
(204, 85, 90),
(152, 168, 75),
(208, 179, 68),
(96, 138, 177),
(152, 114, 159),
(104, 160, 179),
(197, 200, 198),
(154, 155, 153),
],
[
(255, 38, 39),
(0, 130, 61),
(208, 132, 66),
(25, 132, 233),
(255, 44, 122),
(57, 130, 128),
(253, 253, 197),
],
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,115 @@
import configparser
from typing import Dict, List, IO, Mapping, Optional
from .default_styles import DEFAULT_STYLES
from .style import Style, StyleType
class Theme:
"""A container for style information, used by :class:`~rich.console.Console`.
Args:
styles (Dict[str, Style], optional): A mapping of style names on to styles. Defaults to None for a theme with no styles.
inherit (bool, optional): Inherit default styles. Defaults to True.
"""
styles: Dict[str, Style]
def __init__(
self, styles: Optional[Mapping[str, StyleType]] = None, inherit: bool = True
):
self.styles = DEFAULT_STYLES.copy() if inherit else {}
if styles is not None:
self.styles.update(
{
name: style if isinstance(style, Style) else Style.parse(style)
for name, style in styles.items()
}
)
@property
def config(self) -> str:
"""Get contents of a config file for this theme."""
config = "[styles]\n" + "\n".join(
f"{name} = {style}" for name, style in sorted(self.styles.items())
)
return config
@classmethod
def from_file(
cls, config_file: IO[str], source: Optional[str] = None, inherit: bool = True
) -> "Theme":
"""Load a theme from a text mode file.
Args:
config_file (IO[str]): An open conf file.
source (str, optional): The filename of the open file. Defaults to None.
inherit (bool, optional): Inherit default styles. Defaults to True.
Returns:
Theme: A New theme instance.
"""
config = configparser.ConfigParser()
config.read_file(config_file, source=source)
styles = {name: Style.parse(value) for name, value in config.items("styles")}
theme = Theme(styles, inherit=inherit)
return theme
@classmethod
def read(
cls, path: str, inherit: bool = True, encoding: Optional[str] = None
) -> "Theme":
"""Read a theme from a path.
Args:
path (str): Path to a config file readable by Python configparser module.
inherit (bool, optional): Inherit default styles. Defaults to True.
encoding (str, optional): Encoding of the config file. Defaults to None.
Returns:
Theme: A new theme instance.
"""
with open(path, "rt", encoding=encoding) as config_file:
return cls.from_file(config_file, source=path, inherit=inherit)
class ThemeStackError(Exception):
"""Base exception for errors related to the theme stack."""
class ThemeStack:
"""A stack of themes.
Args:
theme (Theme): A theme instance
"""
def __init__(self, theme: Theme) -> None:
self._entries: List[Dict[str, Style]] = [theme.styles]
self.get = self._entries[-1].get
def push_theme(self, theme: Theme, inherit: bool = True) -> None:
"""Push a theme on the top of the stack.
Args:
theme (Theme): A Theme instance.
inherit (boolean, optional): Inherit styles from current top of stack.
"""
styles: Dict[str, Style]
styles = (
{**self._entries[-1], **theme.styles} if inherit else theme.styles.copy()
)
self._entries.append(styles)
self.get = self._entries[-1].get
def pop_theme(self) -> None:
"""Pop (and discard) the top-most theme."""
if len(self._entries) == 1:
raise ThemeStackError("Unable to pop base theme")
self._entries.pop()
self.get = self._entries[-1].get
if __name__ == "__main__": # pragma: no cover
theme = Theme()
print(theme.config)

View file

@ -0,0 +1,5 @@
from .default_styles import DEFAULT_STYLES
from .theme import Theme
DEFAULT = Theme(DEFAULT_STYLES)

View file

@ -0,0 +1,756 @@
from __future__ import absolute_import
import linecache
import os
import platform
import sys
from dataclasses import dataclass, field
from traceback import walk_tb
from types import ModuleType, TracebackType
from typing import (
Any,
Callable,
Dict,
Iterable,
List,
Optional,
Sequence,
Tuple,
Type,
Union,
)
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_last
from .columns import Columns
from .console import Console, ConsoleOptions, ConsoleRenderable, RenderResult, group
from .constrain import Constrain
from .highlighter import RegexHighlighter, ReprHighlighter
from .panel import Panel
from .scope import render_scope
from .style import Style
from .syntax import Syntax
from .text import Text
from .theme import Theme
WINDOWS = platform.system() == "Windows"
LOCALS_MAX_LENGTH = 10
LOCALS_MAX_STRING = 80
def install(
*,
console: Optional[Console] = None,
width: Optional[int] = 100,
extra_lines: int = 3,
theme: Optional[str] = None,
word_wrap: bool = False,
show_locals: bool = False,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
locals_hide_dunder: bool = True,
locals_hide_sunder: Optional[bool] = None,
indent_guides: bool = True,
suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100,
) -> Callable[[Type[BaseException], BaseException, Optional[TracebackType]], Any]:
"""Install a rich traceback handler.
Once installed, any tracebacks will be printed with syntax highlighting and rich formatting.
Args:
console (Optional[Console], optional): Console to write exception to. Default uses internal Console instance.
width (Optional[int], optional): Width (in characters) of traceback. Defaults to 100.
extra_lines (int, optional): Extra lines of code. Defaults to 3.
theme (Optional[str], optional): Pygments theme to use in traceback. Defaults to ``None`` which will pick
a theme appropriate for the platform.
word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
show_locals (bool, optional): Enable display of local variables. Defaults to False.
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.
locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.
locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.
indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
Returns:
Callable: The previous exception handler that was replaced.
"""
traceback_console = Console(stderr=True) if console is None else console
locals_hide_sunder = (
True
if (traceback_console.is_jupyter and locals_hide_sunder is None)
else locals_hide_sunder
)
def excepthook(
type_: Type[BaseException],
value: BaseException,
traceback: Optional[TracebackType],
) -> None:
traceback_console.print(
Traceback.from_exception(
type_,
value,
traceback,
width=width,
extra_lines=extra_lines,
theme=theme,
word_wrap=word_wrap,
show_locals=show_locals,
locals_max_length=locals_max_length,
locals_max_string=locals_max_string,
locals_hide_dunder=locals_hide_dunder,
locals_hide_sunder=bool(locals_hide_sunder),
indent_guides=indent_guides,
suppress=suppress,
max_frames=max_frames,
)
)
def ipy_excepthook_closure(ip: Any) -> None: # pragma: no cover
tb_data = {} # store information about showtraceback call
default_showtraceback = ip.showtraceback # keep reference of default traceback
def ipy_show_traceback(*args: Any, **kwargs: Any) -> None:
"""wrap the default ip.showtraceback to store info for ip._showtraceback"""
nonlocal tb_data
tb_data = kwargs
default_showtraceback(*args, **kwargs)
def ipy_display_traceback(
*args: Any, is_syntax: bool = False, **kwargs: Any
) -> None:
"""Internally called traceback from ip._showtraceback"""
nonlocal tb_data
exc_tuple = ip._get_exc_info()
# do not display trace on syntax error
tb: Optional[TracebackType] = None if is_syntax else exc_tuple[2]
# determine correct tb_offset
compiled = tb_data.get("running_compiled_code", False)
tb_offset = tb_data.get("tb_offset", 1 if compiled else 0)
# remove ipython internal frames from trace with tb_offset
for _ in range(tb_offset):
if tb is None:
break
tb = tb.tb_next
excepthook(exc_tuple[0], exc_tuple[1], tb)
tb_data = {} # clear data upon usage
# replace _showtraceback instead of showtraceback to allow ipython features such as debugging to work
# this is also what the ipython docs recommends to modify when subclassing InteractiveShell
ip._showtraceback = ipy_display_traceback
# add wrapper to capture tb_data
ip.showtraceback = ipy_show_traceback
ip.showsyntaxerror = lambda *args, **kwargs: ipy_display_traceback(
*args, is_syntax=True, **kwargs
)
try: # pragma: no cover
# if within ipython, use customized traceback
ip = get_ipython() # type: ignore[name-defined]
ipy_excepthook_closure(ip)
return sys.excepthook
except Exception:
# otherwise use default system hook
old_excepthook = sys.excepthook
sys.excepthook = excepthook
return old_excepthook
@dataclass
class Frame:
filename: str
lineno: int
name: str
line: str = ""
locals: Optional[Dict[str, pretty.Node]] = None
@dataclass
class _SyntaxError:
offset: int
filename: str
line: str
lineno: int
msg: str
@dataclass
class Stack:
exc_type: str
exc_value: str
syntax_error: Optional[_SyntaxError] = None
is_cause: bool = False
frames: List[Frame] = field(default_factory=list)
@dataclass
class Trace:
stacks: List[Stack]
class PathHighlighter(RegexHighlighter):
highlights = [r"(?P<dim>.*/)(?P<bold>.+)"]
class Traceback:
"""A Console renderable that renders a traceback.
Args:
trace (Trace, optional): A `Trace` object produced from `extract`. Defaults to None, which uses
the last exception.
width (Optional[int], optional): Number of characters used to traceback. Defaults to 100.
extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
theme (str, optional): Override pygments theme used in traceback.
word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
show_locals (bool, optional): Enable display of local variables. Defaults to False.
indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
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.
locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.
locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.
suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
"""
LEXERS = {
"": "text",
".py": "python",
".pxd": "cython",
".pyx": "cython",
".pxi": "pyrex",
}
def __init__(
self,
trace: Optional[Trace] = None,
*,
width: Optional[int] = 100,
extra_lines: int = 3,
theme: Optional[str] = None,
word_wrap: bool = False,
show_locals: bool = False,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
locals_hide_dunder: bool = True,
locals_hide_sunder: bool = False,
indent_guides: bool = True,
suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100,
):
if trace is None:
exc_type, exc_value, traceback = sys.exc_info()
if exc_type is None or exc_value is None or traceback is None:
raise ValueError(
"Value for 'trace' required if not called in except: block"
)
trace = self.extract(
exc_type, exc_value, traceback, show_locals=show_locals
)
self.trace = trace
self.width = width
self.extra_lines = extra_lines
self.theme = Syntax.get_theme(theme or "ansi_dark")
self.word_wrap = word_wrap
self.show_locals = show_locals
self.indent_guides = indent_guides
self.locals_max_length = locals_max_length
self.locals_max_string = locals_max_string
self.locals_hide_dunder = locals_hide_dunder
self.locals_hide_sunder = locals_hide_sunder
self.suppress: Sequence[str] = []
for suppress_entity in suppress:
if not isinstance(suppress_entity, str):
assert (
suppress_entity.__file__ is not None
), f"{suppress_entity!r} must be a module with '__file__' attribute"
path = os.path.dirname(suppress_entity.__file__)
else:
path = suppress_entity
path = os.path.normpath(os.path.abspath(path))
self.suppress.append(path)
self.max_frames = max(4, max_frames) if max_frames > 0 else 0
@classmethod
def from_exception(
cls,
exc_type: Type[Any],
exc_value: BaseException,
traceback: Optional[TracebackType],
*,
width: Optional[int] = 100,
extra_lines: int = 3,
theme: Optional[str] = None,
word_wrap: bool = False,
show_locals: bool = False,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
locals_hide_dunder: bool = True,
locals_hide_sunder: bool = False,
indent_guides: bool = True,
suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100,
) -> "Traceback":
"""Create a traceback from exception info
Args:
exc_type (Type[BaseException]): Exception type.
exc_value (BaseException): Exception value.
traceback (TracebackType): Python Traceback object.
width (Optional[int], optional): Number of characters used to traceback. Defaults to 100.
extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
theme (str, optional): Override pygments theme used in traceback.
word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
show_locals (bool, optional): Enable display of local variables. Defaults to False.
indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
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.
locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.
locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.
suppress (Iterable[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
Returns:
Traceback: A Traceback instance that may be printed.
"""
rich_traceback = cls.extract(
exc_type,
exc_value,
traceback,
show_locals=show_locals,
locals_max_length=locals_max_length,
locals_max_string=locals_max_string,
locals_hide_dunder=locals_hide_dunder,
locals_hide_sunder=locals_hide_sunder,
)
return cls(
rich_traceback,
width=width,
extra_lines=extra_lines,
theme=theme,
word_wrap=word_wrap,
show_locals=show_locals,
indent_guides=indent_guides,
locals_max_length=locals_max_length,
locals_max_string=locals_max_string,
locals_hide_dunder=locals_hide_dunder,
locals_hide_sunder=locals_hide_sunder,
suppress=suppress,
max_frames=max_frames,
)
@classmethod
def extract(
cls,
exc_type: Type[BaseException],
exc_value: BaseException,
traceback: Optional[TracebackType],
*,
show_locals: bool = False,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
locals_hide_dunder: bool = True,
locals_hide_sunder: bool = False,
) -> Trace:
"""Extract traceback information.
Args:
exc_type (Type[BaseException]): Exception type.
exc_value (BaseException): Exception value.
traceback (TracebackType): Python Traceback object.
show_locals (bool, optional): Enable display of local variables. Defaults to False.
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.
locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.
locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.
Returns:
Trace: A Trace instance which you can use to construct a `Traceback`.
"""
stacks: List[Stack] = []
is_cause = False
from pip._vendor.rich import _IMPORT_CWD
def safe_str(_object: Any) -> str:
"""Don't allow exceptions from __str__ to propagate."""
try:
return str(_object)
except Exception:
return "<exception str() failed>"
while True:
stack = Stack(
exc_type=safe_str(exc_type.__name__),
exc_value=safe_str(exc_value),
is_cause=is_cause,
)
if isinstance(exc_value, SyntaxError):
stack.syntax_error = _SyntaxError(
offset=exc_value.offset or 0,
filename=exc_value.filename or "?",
lineno=exc_value.lineno or 0,
line=exc_value.text or "",
msg=exc_value.msg,
)
stacks.append(stack)
append = stack.frames.append
def get_locals(
iter_locals: Iterable[Tuple[str, object]]
) -> Iterable[Tuple[str, object]]:
"""Extract locals from an iterator of key pairs."""
if not (locals_hide_dunder or locals_hide_sunder):
yield from iter_locals
return
for key, value in iter_locals:
if locals_hide_dunder and key.startswith("__"):
continue
if locals_hide_sunder and key.startswith("_"):
continue
yield key, value
for frame_summary, line_no in walk_tb(traceback):
filename = frame_summary.f_code.co_filename
if filename and not filename.startswith("<"):
if not os.path.isabs(filename):
filename = os.path.join(_IMPORT_CWD, filename)
if frame_summary.f_locals.get("_rich_traceback_omit", False):
continue
frame = Frame(
filename=filename or "?",
lineno=line_no,
name=frame_summary.f_code.co_name,
locals={
key: pretty.traverse(
value,
max_length=locals_max_length,
max_string=locals_max_string,
)
for key, value in get_locals(frame_summary.f_locals.items())
}
if show_locals
else None,
)
append(frame)
if frame_summary.f_locals.get("_rich_traceback_guard", False):
del stack.frames[:]
cause = getattr(exc_value, "__cause__", None)
if cause:
exc_type = cause.__class__
exc_value = cause
# __traceback__ can be None, e.g. for exceptions raised by the
# 'multiprocessing' module
traceback = cause.__traceback__
is_cause = True
continue
cause = exc_value.__context__
if cause and not getattr(exc_value, "__suppress_context__", False):
exc_type = cause.__class__
exc_value = cause
traceback = cause.__traceback__
is_cause = False
continue
# No cover, code is reached but coverage doesn't recognize it.
break # pragma: no cover
trace = Trace(stacks=stacks)
return trace
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
theme = self.theme
background_style = theme.get_background_style()
token_style = theme.get_style_for_token
traceback_theme = Theme(
{
"pretty": token_style(TextToken),
"pygments.text": token_style(Token),
"pygments.string": token_style(String),
"pygments.function": token_style(Name.Function),
"pygments.number": token_style(Number),
"repr.indent": token_style(Comment) + Style(dim=True),
"repr.str": token_style(String),
"repr.brace": token_style(TextToken) + Style(bold=True),
"repr.number": token_style(Number),
"repr.bool_true": token_style(Keyword.Constant),
"repr.bool_false": token_style(Keyword.Constant),
"repr.none": token_style(Keyword.Constant),
"scope.border": token_style(String.Delimiter),
"scope.equals": token_style(Operator),
"scope.key": token_style(Name),
"scope.key.special": token_style(Name.Constant) + Style(dim=True),
},
inherit=False,
)
highlighter = ReprHighlighter()
for last, stack in loop_last(reversed(self.trace.stacks)):
if stack.frames:
stack_renderable: ConsoleRenderable = Panel(
self._render_stack(stack),
title="[traceback.title]Traceback [dim](most recent call last)",
style=background_style,
border_style="traceback.border",
expand=True,
padding=(0, 1),
)
stack_renderable = Constrain(stack_renderable, self.width)
with console.use_theme(traceback_theme):
yield stack_renderable
if stack.syntax_error is not None:
with console.use_theme(traceback_theme):
yield Constrain(
Panel(
self._render_syntax_error(stack.syntax_error),
style=background_style,
border_style="traceback.border.syntax_error",
expand=True,
padding=(0, 1),
width=self.width,
),
self.width,
)
yield Text.assemble(
(f"{stack.exc_type}: ", "traceback.exc_type"),
highlighter(stack.syntax_error.msg),
)
elif stack.exc_value:
yield Text.assemble(
(f"{stack.exc_type}: ", "traceback.exc_type"),
highlighter(stack.exc_value),
)
else:
yield Text.assemble((f"{stack.exc_type}", "traceback.exc_type"))
if not last:
if stack.is_cause:
yield Text.from_markup(
"\n[i]The above exception was the direct cause of the following exception:\n",
)
else:
yield Text.from_markup(
"\n[i]During handling of the above exception, another exception occurred:\n",
)
@group()
def _render_syntax_error(self, syntax_error: _SyntaxError) -> RenderResult:
highlighter = ReprHighlighter()
path_highlighter = PathHighlighter()
if syntax_error.filename != "<stdin>":
if os.path.exists(syntax_error.filename):
text = Text.assemble(
(f" {syntax_error.filename}", "pygments.string"),
(":", "pygments.text"),
(str(syntax_error.lineno), "pygments.number"),
style="pygments.text",
)
yield path_highlighter(text)
syntax_error_text = highlighter(syntax_error.line.rstrip())
syntax_error_text.no_wrap = True
offset = min(syntax_error.offset - 1, len(syntax_error_text))
syntax_error_text.stylize("bold underline", offset, offset)
syntax_error_text += Text.from_markup(
"\n" + " " * offset + "[traceback.offset]▲[/]",
style="pygments.text",
)
yield syntax_error_text
@classmethod
def _guess_lexer(cls, filename: str, code: str) -> str:
ext = os.path.splitext(filename)[-1]
if not ext:
# No extension, look at first line to see if it is a hashbang
# Note, this is an educated guess and not a guarantee
# If it fails, the only downside is that the code is highlighted strangely
new_line_index = code.index("\n")
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"
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:
path_highlighter = PathHighlighter()
theme = self.theme
def read_code(filename: str) -> str:
"""Read files, and cache results on filename.
Args:
filename (str): Filename to read
Returns:
str: Contents of file
"""
return "".join(linecache.getlines(filename))
def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
if frame.locals:
yield render_scope(
frame.locals,
title="locals",
indent_guides=self.indent_guides,
max_length=self.locals_max_length,
max_string=self.locals_max_string,
)
exclude_frames: Optional[range] = None
if self.max_frames != 0:
exclude_frames = range(
self.max_frames // 2,
len(stack.frames) - self.max_frames // 2,
)
excluded = False
for frame_index, frame in enumerate(stack.frames):
if exclude_frames and frame_index in exclude_frames:
excluded = True
continue
if excluded:
assert exclude_frames is not None
yield Text(
f"\n... {len(exclude_frames)} frames hidden ...",
justify="center",
style="traceback.error",
)
excluded = False
first = frame_index == 0
frame_filename = frame.filename
suppressed = any(frame_filename.startswith(path) for path in self.suppress)
if os.path.exists(frame.filename):
text = Text.assemble(
path_highlighter(Text(frame.filename, style="pygments.string")),
(":", "pygments.text"),
(str(frame.lineno), "pygments.number"),
" in ",
(frame.name, "pygments.function"),
style="pygments.text",
)
else:
text = Text.assemble(
"in ",
(frame.name, "pygments.function"),
(":", "pygments.text"),
(str(frame.lineno), "pygments.number"),
style="pygments.text",
)
if not frame.filename.startswith("<") and not first:
yield ""
yield text
if frame.filename.startswith("<"):
yield from render_locals(frame)
continue
if not suppressed:
try:
code = read_code(frame.filename)
if not code:
# code may be an empty string if the file doesn't exist, OR
# if the traceback filename is generated dynamically
continue
lexer_name = self._guess_lexer(frame.filename, code)
syntax = Syntax(
code,
lexer_name,
theme=theme,
line_numbers=True,
line_range=(
frame.lineno - self.extra_lines,
frame.lineno + self.extra_lines,
),
highlight_lines={frame.lineno},
word_wrap=self.word_wrap,
code_width=88,
indent_guides=self.indent_guides,
dedent=False,
)
yield ""
except Exception as error:
yield Text.assemble(
(f"\n{error}", "traceback.error"),
)
else:
yield (
Columns(
[
syntax,
*render_locals(frame),
],
padding=1,
)
if frame.locals
else syntax
)
if __name__ == "__main__": # pragma: no cover
from .console import Console
console = Console()
import sys
def bar(a: Any) -> None: # 这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑
one = 1
print(one / a)
def foo(a: Any) -> None:
_rich_traceback_guard = True
zed = {
"characters": {
"Paul Atreides",
"Vladimir Harkonnen",
"Thufir Hawat",
"Duncan Idaho",
},
"atomic_types": (None, False, True),
}
bar(a)
def error() -> None:
try:
try:
foo(0)
except:
slfkjsldkfj # type: ignore[name-defined]
except:
console.print_exception(show_locals=True)
error()

View file

@ -0,0 +1,251 @@
from typing import Iterator, List, Optional, Tuple
from ._loop import loop_first, loop_last
from .console import Console, ConsoleOptions, RenderableType, RenderResult
from .jupyter import JupyterMixin
from .measure import Measurement
from .segment import Segment
from .style import Style, StyleStack, StyleType
from .styled import Styled
class Tree(JupyterMixin):
"""A renderable for a tree structure.
Args:
label (RenderableType): The renderable or str for the tree label.
style (StyleType, optional): Style of this tree. Defaults to "tree".
guide_style (StyleType, optional): Style of the guide lines. Defaults to "tree.line".
expanded (bool, optional): Also display children. Defaults to True.
highlight (bool, optional): Highlight renderable (if str). Defaults to False.
"""
def __init__(
self,
label: RenderableType,
*,
style: StyleType = "tree",
guide_style: StyleType = "tree.line",
expanded: bool = True,
highlight: bool = False,
hide_root: bool = False,
) -> None:
self.label = label
self.style = style
self.guide_style = guide_style
self.children: List[Tree] = []
self.expanded = expanded
self.highlight = highlight
self.hide_root = hide_root
def add(
self,
label: RenderableType,
*,
style: Optional[StyleType] = None,
guide_style: Optional[StyleType] = None,
expanded: bool = True,
highlight: Optional[bool] = False,
) -> "Tree":
"""Add a child tree.
Args:
label (RenderableType): The renderable or str for the tree label.
style (StyleType, optional): Style of this tree. Defaults to "tree".
guide_style (StyleType, optional): Style of the guide lines. Defaults to "tree.line".
expanded (bool, optional): Also display children. Defaults to True.
highlight (Optional[bool], optional): Highlight renderable (if str). Defaults to False.
Returns:
Tree: A new child Tree, which may be further modified.
"""
node = Tree(
label,
style=self.style if style is None else style,
guide_style=self.guide_style if guide_style is None else guide_style,
expanded=expanded,
highlight=self.highlight if highlight is None else highlight,
)
self.children.append(node)
return node
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
stack: List[Iterator[Tuple[bool, Tree]]] = []
pop = stack.pop
push = stack.append
new_line = Segment.line()
get_style = console.get_style
null_style = Style.null()
guide_style = get_style(self.guide_style, default="") or null_style
SPACE, CONTINUE, FORK, END = range(4)
ASCII_GUIDES = (" ", "| ", "+-- ", "`-- ")
TREE_GUIDES = [
(" ", "", "├── ", "└── "),
(" ", "", "┣━━ ", "┗━━ "),
(" ", "", "╠══ ", "╚══ "),
]
_Segment = Segment
def make_guide(index: int, style: Style) -> Segment:
"""Make a Segment for a level of the guide lines."""
if options.ascii_only:
line = ASCII_GUIDES[index]
else:
guide = 1 if style.bold else (2 if style.underline2 else 0)
line = TREE_GUIDES[0 if options.legacy_windows else guide][index]
return _Segment(line, style)
levels: List[Segment] = [make_guide(CONTINUE, guide_style)]
push(iter(loop_last([self])))
guide_style_stack = StyleStack(get_style(self.guide_style))
style_stack = StyleStack(get_style(self.style))
remove_guide_styles = Style(bold=False, underline2=False)
depth = 0
while stack:
stack_node = pop()
try:
last, node = next(stack_node)
except StopIteration:
levels.pop()
if levels:
guide_style = levels[-1].style or null_style
levels[-1] = make_guide(FORK, guide_style)
guide_style_stack.pop()
style_stack.pop()
continue
push(stack_node)
if last:
levels[-1] = make_guide(END, levels[-1].style or null_style)
guide_style = guide_style_stack.current + get_style(node.guide_style)
style = style_stack.current + get_style(node.style)
prefix = levels[(2 if self.hide_root else 1) :]
renderable_lines = console.render_lines(
Styled(node.label, style),
options.update(
width=options.max_width
- sum(level.cell_length for level in prefix),
highlight=self.highlight,
height=None,
),
pad=options.justify is not None,
)
if not (depth == 0 and self.hide_root):
for first, line in loop_first(renderable_lines):
if prefix:
yield from _Segment.apply_style(
prefix,
style.background_style,
post_style=remove_guide_styles,
)
yield from line
yield new_line
if first and prefix:
prefix[-1] = make_guide(
SPACE if last else CONTINUE, prefix[-1].style or null_style
)
if node.expanded and node.children:
levels[-1] = make_guide(
SPACE if last else CONTINUE, levels[-1].style or null_style
)
levels.append(
make_guide(END if len(node.children) == 1 else FORK, guide_style)
)
style_stack.push(get_style(node.style))
guide_style_stack.push(get_style(node.guide_style))
push(iter(loop_last(node.children)))
depth += 1
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
stack: List[Iterator[Tree]] = [iter([self])]
pop = stack.pop
push = stack.append
minimum = 0
maximum = 0
measure = Measurement.get
level = 0
while stack:
iter_tree = pop()
try:
tree = next(iter_tree)
except StopIteration:
level -= 1
continue
push(iter_tree)
min_measure, max_measure = measure(console, options, tree.label)
indent = level * 4
minimum = max(min_measure + indent, minimum)
maximum = max(max_measure + indent, maximum)
if tree.expanded and tree.children:
push(iter(tree.children))
level += 1
return Measurement(minimum, maximum)
if __name__ == "__main__": # pragma: no cover
from pip._vendor.rich.console import Group
from pip._vendor.rich.markdown import Markdown
from pip._vendor.rich.panel import Panel
from pip._vendor.rich.syntax import Syntax
from pip._vendor.rich.table import Table
table = Table(row_styles=["", "dim"])
table.add_column("Released", style="cyan", no_wrap=True)
table.add_column("Title", style="magenta")
table.add_column("Box Office", justify="right", style="green")
table.add_row("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$952,110,690")
table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
table.add_row("Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889")
table.add_row("Dec 16, 2016", "Rogue One: A Star Wars Story", "$1,332,439,889")
code = """\
class Segment(NamedTuple):
text: str = ""
style: Optional[Style] = None
is_control: bool = False
"""
syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
markdown = Markdown(
"""\
### example.md
> Hello, World!
>
> Markdown _all_ the things
"""
)
root = Tree("🌲 [b green]Rich Tree", highlight=True, hide_root=True)
node = root.add(":file_folder: Renderables", guide_style="red")
simple_node = node.add(":file_folder: [bold yellow]Atomic", guide_style="uu green")
simple_node.add(Group("📄 Syntax", syntax))
simple_node.add(Group("📄 Markdown", Panel(markdown, border_style="green")))
containers_node = node.add(
":file_folder: [bold magenta]Containers", guide_style="bold magenta"
)
containers_node.expanded = True
panel = Panel.fit("Just a panel", border_style="red")
containers_node.add(Group("📄 Panels", panel))
containers_node.add(Group("📄 [b magenta]Table", table))
console = Console()
console.print(root)