virtuelle Umgebung teil20b

This commit is contained in:
Olli Graf
2023-09-01 16:01:37 +02:00
parent c9aee44812
commit c04c88e2af
7025 changed files with 1325157 additions and 0 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,78 @@
CONSOLE_HTML_FORMAT = """\
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<style>
{stylesheet}
body {{
color: {foreground};
background-color: {background};
}}
</style>
</head>
<html>
<body>
<code>
<pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace">{code}</pre>
</code>
</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,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,83 @@
from types import TracebackType
from typing import IO, Iterable, Iterator, List, Optional, Type
class NullFile(IO[str]):
# TODO: "mode", "name" and "closed" are only required for Python 3.6.
@property
def mode(self) -> str:
return ""
@property
def name(self) -> str:
return "NullFile"
def closed(self) -> bool:
return False
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="Algin",
)
console.print(
Align.center(panel, vertical="middle", style="on red", height=console.height)
)

View File

@@ -0,0 +1,237 @@
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.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:
character (str): A single 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,618 @@
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
red, green, blue = self.triplet.normalized
_h, l, s = rgb_to_hls(red, green, blue)
# If saturation is under 10% assume it is grayscale
if s < 0.1:
gray = round(l * 25.0)
if gray == 0:
color_number = 16
elif gray == 25:
color_number = 231
else:
color_number = 231 + gray
return Color(self.name, ColorType.EIGHT_BIT, number=color_number)
color_number = (
16 + 36 * round(red * 5.0) + 6 * round(green * 5.0) + round(blue * 5.0)
)
return Color(self.name, ColorType.EIGHT_BIT, number=color_number)
# 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,188 @@
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.emph": Style(italic=True),
"markdown.strong": Style(bold=True),
"markdown.code": Style(bgcolor="black", color="bright_white"),
"markdown.code_block": Style(dim=True, 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"),
"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,54 @@
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)
del buffer[:]
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[:]

Some files were not shown because too many files have changed in this diff Show More