Initial commit: Tamigo CLI with Gitea Actions and global installation support

This commit is contained in:
Daniel Dybing
2026-03-11 12:07:08 +01:00
commit 146b79660d
2675 changed files with 462625 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 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, callables at the top, leading and trailing underscores ignored. 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 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,245 @@
import colorsys
import io
from time import process_time
from rich import box
from rich.color import Color
from rich.console import Console, ConsoleOptions, Group, RenderableType, RenderResult
from rich.markdown import Markdown
from rich.measure import Measurement
from rich.pretty import Pretty
from rich.segment import Segment
from rich.style import Style
from rich.syntax import Syntax
from rich.table import Table
from 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
from rich.panel import Panel
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)
console = Console()
console.print(f"[dim]rendered in [not dim]{pre_cache_taken}ms[/] (cold cache)")
console.print(f"[dim]rendered in [not dim]{taken}ms[/] (warm cache)")
console.print()
console.print(
Panel(
"[b magenta]Hope you enjoy using Rich![/]\n\n"
"Consider sponsoring to ensure this project is maintained.\n\n"
"[cyan]https://github.com/sponsors/willmcgugan[/cyan]",
border_style="green",
title="Help ensure Rich is maintained",
padding=(1, 2),
)
)

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,272 @@
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, callables at the top, leading and trailing underscores ignored. 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 not isinstance(qualname, str):
qualname = getattr(obj, "__name__", name)
if not isinstance(qualname, str):
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 docstring'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 rich.console import Console
c = Console()
c.print("[on blue]Hello", justify="right")
c.log("[on blue]hello", justify="right")

View File

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

View File

@@ -0,0 +1,69 @@
from types import TracebackType
from typing import IO, Iterable, Iterator, List, Optional, Type
class NullFile(IO[str]):
def close(self) -> None:
pass
def isatty(self) -> bool:
return False
def read(self, __n: int = 1) -> str:
return ""
def readable(self) -> bool:
return False
def readline(self, __limit: int = 1) -> str:
return ""
def readlines(self, __hint: int = 1) -> List[str]:
return []
def seek(self, __offset: int, __whence: int = 1) -> int:
return 0
def seekable(self) -> bool:
return False
def tell(self) -> int:
return 0
def truncate(self, __size: Optional[int] = 1) -> int:
return 0
def writable(self) -> bool:
return False
def writelines(self, __lines: Iterable[str]) -> None:
pass
def __next__(self) -> str:
return ""
def __iter__(self) -> Iterator[str]:
return iter([""])
def __enter__(self) -> IO[str]:
return self
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,153 @@
from fractions import Fraction
from math import ceil
from typing import cast, List, Optional, Sequence, Protocol
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,93 @@
from __future__ import annotations
import bisect
import os
import sys
if sys.version_info[:2] >= (3, 9):
from functools import cache
else:
from functools import lru_cache as cache # pragma: no cover
from importlib import import_module
from typing import TYPE_CHECKING, cast
from rich._unicode_data._versions import VERSIONS
if TYPE_CHECKING:
from rich.cells import CellTable
VERSION_ORDER = sorted(
[
tuple(
map(int, version.split(".")),
)
for version in VERSIONS
]
)
VERSION_SET = frozenset(VERSIONS)
def _parse_version(version: str) -> tuple[int, int, int]:
"""Parse a version string into a tuple of 3 integers.
Args:
version: A version string.
Raises:
ValueError: If the version string is invalid.
Returns:
A tuple of 3 integers.
"""
version_integers: tuple[int, ...]
try:
version_integers = tuple(
map(int, version.split(".")),
)
except ValueError:
raise ValueError(
f"unicode version string {version!r} is badly formatted"
) from None
while len(version_integers) < 3:
version_integers = version_integers + (0,)
triple = cast("tuple[int, int, int]", version_integers[:3])
return triple
@cache
def load(unicode_version: str = "auto") -> CellTable:
"""Load a cell table for the given unicode version.
Args:
unicode_version: Unicode version, or `None` to auto-detect.
"""
if unicode_version == "auto":
unicode_version = os.environ.get("UNICODE_VERSION", "latest")
try:
_parse_version(unicode_version)
except ValueError:
# The environment variable is invalid
# Fallback to using the latest version seems reasonable
unicode_version = "latest"
if unicode_version == "latest":
version = VERSIONS[-1]
else:
try:
version_numbers = _parse_version(unicode_version)
except ValueError:
version_numbers = _parse_version(VERSIONS[-1])
major, minor, patch = version_numbers
version = f"{major}.{minor}.{patch}"
if version not in VERSION_SET:
insert_position = bisect.bisect_left(VERSION_ORDER, version_numbers)
version = VERSIONS[max(0, insert_position - 1)]
version_path_component = version.replace(".", "-")
module_name = f".unicode{version_path_component}"
module = import_module(module_name, "rich._unicode_data")
if TYPE_CHECKING:
assert isinstance(module.cell_table, CellTable)
return module.cell_table

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