I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1,8 @@
from .markdown import MarkdownRenderer # noqa
from .printer import Printer # noqa
from .tables import row, table # noqa
from .traceback_printer import TracebackPrinter # noqa
from .util import MESSAGES # noqa
from .util import color, diff_strings, format_repr, get_raw_input, wrap # noqa
msg = Printer()

View File

@ -0,0 +1,8 @@
import sys
# Use typing_extensions for Python versions < 3.8
if sys.version_info < (3, 8):
from typing_extensions import Protocol, Literal
else:
from typing import Protocol, Literal # noqa: F401

View File

@ -0,0 +1,126 @@
from typing import Iterable, List, Optional, Sequence
from .compat import Literal
class MarkdownRenderer:
"""Simple helper for generating raw Markdown."""
def __init__(self, no_emoji: bool = False):
"""Initialize the renderer.
no_emoji (bool): Don't show emoji in titles etc.
"""
self.data: List = []
self.no_emoji = no_emoji
@property
def text(self) -> str:
"""RETURNS (str): The Markdown document."""
return "\n\n".join(self.data)
def add(self, content: str):
"""Add a string to the Markdown document.
content (str): Add content to the document.
"""
self.data.append(content)
def table(
self,
data: Iterable[Iterable[str]],
header: Sequence[str],
aligns: Optional[Sequence[Literal["r", "c", "l"]]] = None,
) -> str:
"""Create a Markdown table.
data (Iterable[Iterable[str]]): The body, one iterable per row,
containig an interable of column contents.
header (Sequence[str]): The column names.
aligns (Optional[Sequence[Literal["r", "c", "l"]]]): Optional alignment-mode for each column. Values should
either be 'l' (left), 'r' (right), or 'c' (center). Optional.
RETURNS (str): The rendered table.
"""
if aligns is None:
aligns = ["l"] * len(header)
if len(aligns) != len(header):
err = "Invalid aligns: {} (header length: {})".format(aligns, len(header))
raise ValueError(err)
get_divider = lambda a: ":---:" if a == "c" else "---:" if a == "r" else "---"
head = "| {} |".format(" | ".join(header))
divider = "| {} |".format(
" | ".join(get_divider(aligns[i]) for i in range(len(header)))
)
body = "\n".join("| {} |".format(" | ".join(row)) for row in data)
return "{}\n{}\n{}".format(head, divider, body)
def title(self, level: int, text: str, emoji: Optional[str] = None) -> str:
"""Create a Markdown heading.
level (int): The heading level, e.g. 3 for ###
text (str): The heading text.
emoji (Optional[str]): Optional emoji to show before heading text, if enabled.
RETURNS (str): The rendered title.
"""
prefix = "{} ".format(emoji) if emoji and not self.no_emoji else ""
return "{} {}{}".format("#" * level, prefix, text)
def list(self, items: Iterable[str], numbered: bool = False) -> str:
"""Create a non-nested list.
items (Iterable[str]): The list items.
numbered (bool): Whether to use a numbered list.
RETURNS (str): The rendered list.
"""
content = []
for i, item in enumerate(items):
if numbered:
content.append("{}. {}".format(i + 1, item))
else:
content.append("- {}".format(item))
return "\n".join(content)
def link(self, text: str, url: str) -> str:
"""Create a Markdown link.
text (str): The link text.
url (str): The link URL.
RETURNS (str): The rendered link.
"""
return "[{}]({})".format(text, url)
def code_block(self, text: str, lang: str = "") -> str:
"""Create a Markdown code block.
text (str): The code text.
lang (str): Optional code language.
RETURNS (str): The rendered code block.
"""
return "```{}\n{}\n```".format(lang, text)
def code(self, text: str) -> str:
"""Create Markdown inline code.
text (str): The inline code text.
RETURNS (str): The rendered code text.
"""
return self._wrap(text, "`")
def bold(self, text: str) -> str:
"""Create bold text.
text (str): The text to format in boldface.
RETURNS (str): The formatted text.
"""
return self._wrap(text, "**")
def italic(self, text: str):
"""Create italic text.
text (str): The text to italicize.
RETURNS (str): The formatted text.
"""
return self._wrap(text, "_")
def _wrap(self, text, marker):
return "{}{}{}".format(marker, text, marker)

View File

@ -0,0 +1,345 @@
import datetime
import itertools
import os
import sys
import time
import traceback
from collections import Counter
from contextlib import contextmanager
from multiprocessing import Process
from typing import Any, Collection, Dict, NoReturn, Optional, Union, cast, overload
from .compat import Literal
from .tables import row, table
from .util import COLORS, ICONS, MESSAGES, can_render
from .util import color as _color
from .util import locale_escape, supports_ansi, wrap
class Printer(object):
def __init__(
self,
pretty: bool = True,
no_print: bool = False,
colors: Optional[Dict] = None,
icons: Optional[Dict] = None,
line_max: int = 80,
animation: str = "⠙⠹⠸⠼⠴⠦⠧⠇⠏",
animation_ascii: str = "|/-\\",
hide_animation: bool = False,
ignore_warnings: bool = False,
env_prefix: str = "WASABI",
timestamp: bool = False,
):
"""Initialize the command-line printer.
pretty (bool): Pretty-print output (colors, icons).
no_print (bool): Don't actually print, just return.
colors (Optional[Dict]): Optional color values to add or overwrite, name mapped to value.
icons (Optional[Dict]): Optional icons to add or overwrite. Name mapped to unicode icon.
line_max (int): Maximum line length (for divider).
animation (str): Steps of loading animation for loading() method.
animation_ascii (str): Alternative animation for ASCII terminals.
hide_animation (bool): Don't display animation, e.g. for logs.
ignore_warnings (bool): Do not output messages of type MESSAGE.WARN.
env_prefix (str): Prefix for environment variables, e.g.
WASABI_LOG_FRIENDLY.
timestamp (bool): Print a timestamp (default False).
RETURNS (Printer): The initialized printer.
"""
env_log_friendly = os.getenv("{}_LOG_FRIENDLY".format(env_prefix), False)
env_no_pretty = os.getenv("{}_NO_PRETTY".format(env_prefix), False)
self._counts: Counter = Counter()
self.pretty = pretty and not env_no_pretty
self.no_print = no_print
self.show_color = supports_ansi() and not env_log_friendly
self.hide_animation = hide_animation or env_log_friendly
self.ignore_warnings = ignore_warnings
self.line_max = line_max
self.colors = dict(COLORS)
self.icons = dict(ICONS)
self.timestamp = timestamp
if colors:
self.colors.update(colors)
if icons:
self.icons.update(icons)
self.anim = animation if can_render(animation) else animation_ascii
@property
def counts(self) -> Counter:
"""Get the counts of how often the special printers were fired,
e.g. MESSAGES.GOOD. Can be used to print an overview like "X warnings".
"""
return self._counts
def good(
self,
title: Any = "",
text: Any = "",
show: bool = True,
spaced: bool = False,
exits: Optional[int] = None,
):
"""Print a success message.
title (Any): The main text to print.
text (Any): Optional additional text to print.
show (bool): Whether to print or not. Can be used to only output
messages under certain condition, e.g. if --verbose flag is set.
spaced (bool): Whether to add newlines around the output.
exits (Optional[int]): Optional toggle to perform a system exit.
"""
return self._get_msg(
title, text, style=MESSAGES.GOOD, show=show, spaced=spaced, exits=exits
)
@overload
def fail(
self,
title: Any = "",
text: Any = "",
show: bool = True,
spaced: bool = False,
exits: Optional[Literal[0, False]] = None,
):
...
@overload
def fail(
self,
title: Any = "",
text: Any = "",
show: bool = True,
spaced: bool = False,
exits: Literal[1, True] = True,
) -> NoReturn:
...
def fail(
self,
title: Any = "",
text: Any = "",
show: bool = True,
spaced: bool = False,
exits: Optional[Union[int, bool]] = None,
) -> Union[str, None, NoReturn]:
"""Print an error message.
title (Any): The main text to print.
text (Any): Optional additional text to print.
show (bool): Whether to print or not. Can be used to only output
messages under certain condition, e.g. if --verbose flag is set.
spaced (bool): Whether to add newlines around the output.
exits (Optional[int]): Optional toggle to perform a system exit.
"""
return self._get_msg(
title, text, style=MESSAGES.FAIL, show=show, spaced=spaced, exits=exits
)
def warn(
self,
title: Any = "",
text: Any = "",
show: bool = True,
spaced: bool = False,
exits: Optional[int] = None,
):
"""Print a warning message.
title (Any): The main text to print.
text (Any): Optional additional text to print.
show (bool): Whether to print or not. Can be used to only output
messages under certain condition, e.g. if --verbose flag is set.
spaced (bool): Whether to add newlines around the output.
exits (Optional[int]): Optional toggle to perform a system exit.
"""
return self._get_msg(
title, text, style=MESSAGES.WARN, show=show, spaced=spaced, exits=exits
)
def info(
self,
title: Any = "",
text: Any = "",
show: bool = True,
spaced: bool = False,
exits: Optional[int] = None,
):
"""Print an informational message.
title (Any): The main text to print.
text (Any): Optional additional text to print.
show (bool): Whether to print or not. Can be used to only output
messages under certain condition, e.g. if --verbose flag is set.
spaced (bool): Whether to add newlines around the output.
exits (Optional[int]): Optional toggle to perform a system exit.
"""
return self._get_msg(
title, text, style=MESSAGES.INFO, show=show, spaced=spaced, exits=exits
)
def text(
self,
title: Any = "",
text: Any = "",
color: Optional[Union[str, int]] = None,
bg_color: Optional[Union[str, int]] = None,
icon: Optional[str] = None,
spaced: bool = False,
show: bool = True,
no_print: bool = False,
exits: Optional[int] = None,
):
"""Print a message.
title (Any): The main text to print.
text (Any): Optional additional text to print.
color (Optional[Union[str, int]]): Optional foreground color.
bg_color (Optional[Union[str, int]]): Optional background color.
icon (Optional[str]): Optional name of icon to add.
spaced (bool): Whether to add newlines around the output.
show (bool): Whether to print or not. Can be used to only output
messages under certain condition, e.g. if --verbose flag is set.
no_print (bool): Don't actually print, just return.
exits (Optional[int]): Perform a system exit. Optional.
"""
if not show:
return
if self.pretty:
color = self.colors.get(cast(str, color), color)
bg_color = self.colors.get(cast(str, bg_color), bg_color)
icon = self.icons.get(cast(str, icon))
if icon:
title = locale_escape("{} {}".format(icon, title)).strip()
if self.show_color:
title = _color(title, fg=color, bg=bg_color)
title = wrap(title, indent=0)
if text:
title = "{}\n{}".format(title, wrap(text, indent=0))
if self.timestamp:
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
title = "{}\t{}".format(now, title)
if exits is not None or spaced:
title = "\n{}\n".format(title)
if not self.no_print and not no_print:
print(title)
if exits is not None:
sys.stdout.flush()
sys.stderr.flush()
if self.no_print or no_print and exits != 0:
try:
raise RuntimeError(title.strip())
except Exception as e:
# Remove wasabi from the traceback and re-raise
tb = "\n".join(traceback.format_stack()[:-3])
raise SystemExit("{}\n{}".format(tb, e))
sys.exit(exits)
if self.no_print or no_print:
return title
def divider(
self,
text: str = "",
char: str = "=",
show: bool = True,
icon: Optional[str] = None,
):
"""Print a divider with a headline:
============================ Headline here ===========================
text (str): Headline text. If empty, only the line is printed.
char (str): Line character to repeat, e.g. =.
show (bool): Whether to print or not.
icon (Optional[str]): Optional icon to display with title.
"""
if not show:
return
if len(char) != 1:
raise ValueError(
"Divider chars need to be one character long. "
"Received: {}".format(char)
)
if self.pretty:
icon = self.icons.get(cast(str, icon))
if icon:
text = locale_escape("{} {}".format(icon, text)).strip()
deco = char * (int(round((self.line_max - len(text))) / 2) - 2)
text = " {} ".format(text) if text else ""
text = _color(
"\n{deco}{text}{deco}".format(deco=deco, text=text), bold=True
)
if len(text) < self.line_max:
text = text + char * (self.line_max - len(text))
if self.no_print:
return text
print(text)
def table(self, data: Union[Collection, Dict], **kwargs):
"""Print data as a table.
data (Union[Collection, Dict]): The data to render. Either a list of lists
(one per row) or a dict for two-column tables.
kwargs: Table settings. See tables.table for details.
"""
title = kwargs.pop("title", None)
text = table(data, **kwargs)
if title:
self.divider(title)
if self.no_print:
return text
print(text)
def row(self, data: Collection, **kwargs):
"""Print a table row.
data (Collection): The individual columns to format.
kwargs: Row settings. See tables.row for details.
"""
text = row(data, **kwargs)
if self.no_print:
return text
print(text)
@contextmanager
def loading(self, text: str = "Loading..."):
if self.no_print:
yield
elif self.hide_animation:
print(text)
yield
else:
sys.stdout.flush()
t = Process(target=self._spinner, args=(text,))
t.start()
try:
yield
except Exception as e:
# Handle exception inside the with block
t.terminate()
sys.stdout.write("\n")
raise (e)
t.terminate()
sys.stdout.write("\r\x1b[2K") # erase line
sys.stdout.flush()
def _spinner(self, text: str = "Loading..."):
for char in itertools.cycle(self.anim):
sys.stdout.write("\r{} {}".format(char, text))
sys.stdout.flush()
time.sleep(0.1)
def _get_msg(
self,
title: Any,
text: Any,
style: Optional[str] = None,
show: bool = False,
spaced: bool = False,
exits: Optional[int] = None,
):
if self.ignore_warnings and style == MESSAGES.WARN:
show = False
self._counts[style] += 1
return self.text(
title, text, color=style, icon=style, show=show, spaced=spaced, exits=exits
)

View File

View File

@ -0,0 +1,158 @@
import os
from itertools import zip_longest
from typing import Collection, Dict, Iterable, List, Optional, Sequence, Union
from typing import cast
from .compat import Literal
from .util import COLORS
from .util import color as _color
from .util import supports_ansi
ALIGN_MAP = {"l": "<", "r": ">", "c": "^"}
def table(
# fmt: off
data: Union[Collection, Dict],
header: Optional[Iterable] = None,
footer: Optional[Iterable] = None,
divider: bool = False,
widths: Union[Iterable[int], Literal["auto"]] = "auto",
max_col: int = 30,
spacing: int = 3,
aligns: Optional[Union[Iterable[Literal["r", "c", "l"]], Literal["r", "c", "l"]]] = None,
multiline: bool = False,
env_prefix: str = "WASABI",
color_values: Optional[Dict] = None,
fg_colors: Optional[Iterable] = None,
bg_colors: Optional[Iterable] = None,
# fmt: on
) -> str:
"""Format tabular data.
data (Union[Collection, Dict]): The data to render. Either a list of lists (one per
row) or a dict for two-column tables.
header (Optional[Iterable]): Optional header columns.
footer (Optional[Iterable]): Optional footer columns.
divider (bool): Show a divider line between header/footer and body.
widths (Union[Iterable[int], Literal['auto']]): Column widths in order. If "auto", widths
will be calculated automatically based on the largest value.
max_col (int): Maximum column width.
spacing (int): Spacing between columns, in spaces.
aligns (Optional[Union[Iterable[str], str]]): Optional column alignments
in order. 'l' (left, default), 'r' (right) or 'c' (center). If a string,
value is used for all columns.
multiline (bool): If a cell value is a list of a tuple, render it on
multiple lines, with one value per line.
env_prefix (str): Prefix for environment variables, e.g.
WASABI_LOG_FRIENDLY.
color_values (Optional[Dict]): Optional color values to add or overwrite, name mapped to value.
fg_colors (Optional[Iterable]): Optional foreground colors, one per column. None can be specified
for individual columns to retain the default foreground color.
bg_colors (Optional[Iterable]): Optional background colors, one per column. None can be specified
for individual columns to retain the default background color.
RETURNS (str): The formatted table.
"""
if fg_colors is not None or bg_colors is not None:
colors = dict(COLORS)
if color_values is not None:
colors.update(color_values)
if fg_colors is not None:
fg_colors = [colors.get(fg_color, fg_color) for fg_color in fg_colors]
if bg_colors is not None:
bg_colors = [colors.get(bg_color, bg_color) for bg_color in bg_colors]
if isinstance(data, dict):
data = list(data.items())
if multiline:
zipped_data = []
for i, item in enumerate(data):
vals = [v if isinstance(v, (list, tuple)) else [v] for v in item]
zipped_data.extend(list(zip_longest(*vals, fillvalue="")))
if i < len(data) - 1:
zipped_data.append(tuple(["" for i in item]))
data = zipped_data
if widths == "auto":
widths = _get_max_widths(data, header, footer, max_col)
settings = {
"widths": widths,
"spacing": spacing,
"aligns": aligns,
"env_prefix": env_prefix,
"fg_colors": fg_colors,
"bg_colors": bg_colors,
}
divider_row = row(["-" * width for width in widths], **settings) # type: ignore
rows = []
if header:
rows.append(row(header, **settings)) # type: ignore
if divider:
rows.append(divider_row)
for i, item in enumerate(data):
rows.append(row(item, **settings)) # type: ignore
if footer:
if divider:
rows.append(divider_row)
rows.append(row(footer, **settings)) # type: ignore
return "\n{}\n".format("\n".join(rows))
def row(
data: Collection,
widths: Union[Sequence[int], int, Literal["auto"]] = "auto",
spacing: int = 3,
aligns: Optional[Union[Sequence[Literal["r", "c", "l"]], str]] = None,
env_prefix: str = "WASABI",
fg_colors: Optional[Sequence] = None,
bg_colors: Optional[Sequence] = None,
) -> str:
"""Format data as a table row.
data (Collection): The individual columns to format.
widths (Union[Sequence[int], int, Literal['auto']]): Column widths, either one integer for all
columns or an iterable of values. If "auto", widths will be calculated
automatically based on the largest value.
spacing (int): Spacing between columns, in spaces.
aligns (Optional[Union[Sequence[Literal['r', 'c', 'l']], str]]): Optional column
alignments in order. 'l' (left, default), 'r' (right) or 'c' (center).
If a string, value is used for all columns.
env_prefix (str): Prefix for environment variables, e.g.
WASABI_LOG_FRIENDLY.
fg_colors (Optional[Sequence]): Optional foreground colors for the columns, in order. None can be
specified for individual columns to retain the default foreground color.
bg_colors (Optional[Sequence]): Optional background colors for the columns, in order. None can be
specified for individual columns to retain the default background color.
RETURNS (str): The formatted row.
"""
env_log_friendly = os.getenv("{}_LOG_FRIENDLY".format(env_prefix), False)
show_colors = (
supports_ansi()
and not env_log_friendly
and (fg_colors is not None or bg_colors is not None)
)
cols: List[str] = []
_aligns = (
[aligns for _ in data] if isinstance(aligns, str) else cast(List[str], aligns)
)
if not hasattr(widths, "__iter__") and widths != "auto": # single number
widths = cast(List[int], [widths for _ in range(len(data))])
for i, col in enumerate(data):
align = ALIGN_MAP.get(_aligns[i] if _aligns and i < len(_aligns) else "l")
col_width = len(col) if widths == "auto" else cast(List[int], widths)[i]
tpl = "{:%s%d}" % (align, col_width)
col = tpl.format(str(col))
if show_colors:
fg = fg_colors[i] if fg_colors is not None else None
bg = bg_colors[i] if bg_colors is not None else None
col = _color(col, fg=fg, bg=bg)
cols.append(col)
return (" " * spacing).join(cols)
def _get_max_widths(data, header, footer, max_col):
all_data = list(data)
if header:
all_data.append(header)
if footer:
all_data.append(footer)
widths = [[len(str(col)) for col in item] for item in all_data]
return [min(max(w), max_col) for w in list(zip(*widths))]

View File

@ -0,0 +1,42 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "eb5586f8",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"import wasabi\n",
"\n",
"wasabi.msg.warn(\"This is a test. This is only a test.\")\n",
"if sys.version_info >= (3, 7):\n",
" assert wasabi.util.supports_ansi()\n",
"\n",
"print(sys.stdout)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,36 @@
from pathlib import Path
import subprocess
import os
import sys
import wasabi
TEST_DATA = Path(__file__).absolute().parent / "test-data"
WASABI_DIR = Path(wasabi.__file__).absolute().parent.parent
def test_jupyter():
# This runs some code in a jupyter notebook environment, but without actually
# starting up the notebook UI. Historically we once had a bug that caused crashes
# when importing wasabi in a jupyter notebook, because they replace
# sys.stdout/stderr with custom objects that aren't "real" files/ttys. So this makes
# sure that we can import and use wasabi inside a notebook without crashing.
env = dict(os.environ)
if "PYTHONPATH" in env:
env["PYTHONPATH"] = f"{WASABI_DIR}{os.pathsep}{env['PYTHONPATH']}"
else:
env["PYTHONPATH"] = str(WASABI_DIR)
subprocess.run(
[
sys.executable,
"-m",
"nbconvert",
str(TEST_DATA / "wasabi-test-notebook.ipynb"),
"--execute",
"--stdout",
"--to",
"notebook",
],
env=env,
check=True,
)

View File

@ -0,0 +1,26 @@
import pytest
from wasabi.markdown import MarkdownRenderer
def test_markdown():
md = MarkdownRenderer()
md.add(md.title(1, "Title"))
md.add("Paragraph with {}".format(md.bold("bold")))
md.add(md.list(["foo", "bar"]))
md.add(md.table([("a", "b"), ("c", "d")], ["foo", "bar"]))
md.add(md.code_block('import spacy\n\nnlp = spacy.blank("en")', "python"))
md.add(md.list(["first", "second"], numbered=True))
expected = """# Title\n\nParagraph with **bold**\n\n- foo\n- bar\n\n| foo | bar |\n| --- | --- |\n| a | b |\n| c | d |\n\n```python\nimport spacy\n\nnlp = spacy.blank("en")\n```\n\n1. first\n2. second"""
assert md.text == expected
def test_markdown_table_aligns():
md = MarkdownRenderer()
md.add(md.table([("a", "b", "c")], ["foo", "bar", "baz"], aligns=("c", "r", "l")))
expected = """| foo | bar | baz |\n| :---: | ---: | --- |\n| a | b | c |"""
assert md.text == expected
with pytest.raises(ValueError):
md.table([("a", "b", "c")], ["foo", "bar", "baz"], aligns=("c", "r"))
with pytest.raises(ValueError):
md.table([("a", "b", "c")], ["foo", "bar", "baz"], aligns=("c", "r", "l", "l"))

View File

@ -0,0 +1,236 @@
import os
import re
import time
import pytest
from wasabi.printer import Printer
from wasabi.util import MESSAGES, NO_UTF8, supports_ansi
SUPPORTS_ANSI = supports_ansi()
def test_printer():
p = Printer(no_print=True)
text = "This is a test."
good = p.good(text)
fail = p.fail(text)
warn = p.warn(text)
info = p.info(text)
assert p.text(text) == text
if SUPPORTS_ANSI and not NO_UTF8:
assert good == "\x1b[38;5;2m\u2714 {}\x1b[0m".format(text)
assert fail == "\x1b[38;5;1m\u2718 {}\x1b[0m".format(text)
assert warn == "\x1b[38;5;3m\u26a0 {}\x1b[0m".format(text)
assert info == "\x1b[38;5;4m\u2139 {}\x1b[0m".format(text)
if SUPPORTS_ANSI and NO_UTF8:
assert good == "\x1b[38;5;2m[+] {}\x1b[0m".format(text)
assert fail == "\x1b[38;5;1m[x] {}\x1b[0m".format(text)
assert warn == "\x1b[38;5;3m[!] {}\x1b[0m".format(text)
assert info == "\x1b[38;5;4m[i] {}\x1b[0m".format(text)
if not SUPPORTS_ANSI and not NO_UTF8:
assert good == "\u2714 {}".format(text)
assert fail == "\u2718 {}".format(text)
assert warn == "\u26a0 {}".format(text)
assert info == "\u2139 {}".format(text)
if not SUPPORTS_ANSI and NO_UTF8:
assert good == "[+] {}".format(text)
assert fail == "[x] {}".format(text)
assert warn == "[!] {}".format(text)
assert info == "[i] {}".format(text)
def test_printer_print():
p = Printer()
text = "This is a test."
p.good(text)
p.fail(text)
p.info(text)
p.text(text)
def test_printer_print_timestamp():
p = Printer(no_print=True, timestamp=True)
result = p.info("Hello world")
matches = re.match("^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}", result)
assert matches
def test_printer_no_pretty():
p = Printer(no_print=True, pretty=False)
text = "This is a test."
assert p.good(text) == text
assert p.fail(text) == text
assert p.warn(text) == text
assert p.info(text) == text
assert p.text(text) == text
def test_printer_custom():
colors = {"yellow": 220, "purple": 99}
icons = {"warn": "\u26a0\ufe0f", "question": "?"}
p = Printer(no_print=True, colors=colors, icons=icons)
text = "This is a test."
purple_question = p.text(text, color="purple", icon="question")
warning = p.warn(text)
if SUPPORTS_ANSI and not NO_UTF8:
assert purple_question == "\x1b[38;5;99m? {}\x1b[0m".format(text)
assert warning == "\x1b[38;5;3m\u26a0\ufe0f {}\x1b[0m".format(text)
if SUPPORTS_ANSI and NO_UTF8:
assert purple_question == "\x1b[38;5;99m? {}\x1b[0m".format(text)
assert warning == "\x1b[38;5;3m?? {}\x1b[0m".format(text)
if not SUPPORTS_ANSI and not NO_UTF8:
assert purple_question == "? {}".format(text)
assert warning == "\u26a0\ufe0f {}".format(text)
if not SUPPORTS_ANSI and NO_UTF8:
assert purple_question == "? {}".format(text)
assert warning == "?? {}".format(text)
def test_color_as_int():
p = Printer(no_print=True)
text = "This is a text."
result = p.text(text, color=220)
if SUPPORTS_ANSI:
assert result == "\x1b[38;5;220mThis is a text.\x1b[0m"
else:
assert result == "This is a text."
def test_bg_color():
p = Printer(no_print=True)
text = "This is a text."
result = p.text(text, bg_color="red")
print(result)
if SUPPORTS_ANSI:
assert result == "\x1b[48;5;1mThis is a text.\x1b[0m"
else:
assert result == "This is a text."
def test_bg_color_as_int():
p = Printer(no_print=True)
text = "This is a text."
result = p.text(text, bg_color=220)
print(result)
if SUPPORTS_ANSI:
assert result == "\x1b[48;5;220mThis is a text.\x1b[0m"
else:
assert result == "This is a text."
def test_color_and_bc_color():
p = Printer(no_print=True)
text = "This is a text."
result = p.text(text, color="green", bg_color="yellow")
print(result)
if SUPPORTS_ANSI:
assert result == "\x1b[38;5;2;48;5;3mThis is a text.\x1b[0m"
else:
assert result == "This is a text."
def test_printer_counts():
p = Printer()
text = "This is a test."
for i in range(2):
p.good(text)
for i in range(1):
p.fail(text)
for i in range(4):
p.warn(text)
assert p.counts[MESSAGES.GOOD] == 2
assert p.counts[MESSAGES.FAIL] == 1
assert p.counts[MESSAGES.WARN] == 4
def test_printer_spaced():
p = Printer(no_print=True, pretty=False)
text = "This is a test."
assert p.good(text) == text
assert p.good(text, spaced=True) == "\n{}\n".format(text)
def test_printer_divider():
p = Printer(line_max=20, no_print=True)
p.divider() == "\x1b[1m\n================\x1b[0m"
p.divider("test") == "\x1b[1m\n====== test ======\x1b[0m"
p.divider("test", char="*") == "\x1b[1m\n****** test ******\x1b[0m"
assert (
p.divider("This is a very long text, it is very long")
== "\x1b[1m\n This is a very long text, it is very long \x1b[0m"
)
with pytest.raises(ValueError):
p.divider("test", char="~.")
@pytest.mark.parametrize("hide_animation", [False, True])
def test_printer_loading(hide_animation):
p = Printer(hide_animation=hide_animation)
print("\n")
with p.loading("Loading..."):
time.sleep(1)
p.good("Success!")
with p.loading("Something else..."):
time.sleep(2)
p.good("Yo!")
with p.loading("Loading..."):
time.sleep(1)
p.good("Success!")
def test_printer_loading_raises_exception():
def loading_with_exception():
p = Printer()
print("\n")
with p.loading():
raise Exception("This is an error.")
with pytest.raises(Exception):
loading_with_exception()
def test_printer_loading_no_print():
p = Printer(no_print=True)
with p.loading("Loading..."):
time.sleep(1)
p.good("Success!")
def test_printer_log_friendly():
text = "This is a test."
ENV_LOG_FRIENDLY = "WASABI_LOG_FRIENDLY"
os.environ[ENV_LOG_FRIENDLY] = "True"
p = Printer(no_print=True)
assert p.good(text) in ("\u2714 This is a test.", "[+] This is a test.")
del os.environ[ENV_LOG_FRIENDLY]
def test_printer_log_friendly_prefix():
text = "This is a test."
ENV_LOG_FRIENDLY = "CUSTOM_LOG_FRIENDLY"
os.environ[ENV_LOG_FRIENDLY] = "True"
p = Printer(no_print=True, env_prefix="CUSTOM")
assert p.good(text) in ("\u2714 This is a test.", "[+] This is a test.")
print(p.good(text))
del os.environ[ENV_LOG_FRIENDLY]
@pytest.mark.skip(reason="Now seems to raise TypeError: readonly attribute?")
def test_printer_none_encoding(monkeypatch):
"""Test that printer works even if sys.stdout.encoding is set to None. This
previously caused a very confusing error."""
monkeypatch.setattr("sys.stdout.encoding", None)
p = Printer() # noqa: F841
def test_printer_no_print_raise_on_exit():
"""Test that the printer raises if a non-zero exit code is provided, even
if no_print is set to True."""
err = "This is an error."
p = Printer(no_print=True, pretty=False)
with pytest.raises(SystemExit) as e:
p.fail(err, exits=True)
assert str(e.value).strip()[-len(err) :] == err

View File

@ -0,0 +1,421 @@
import os
import pytest
from wasabi.tables import row, table
from wasabi.util import supports_ansi
SUPPORTS_ANSI = supports_ansi()
@pytest.fixture()
def data():
return [("Hello", "World", "12344342"), ("This is a test", "World", "1234")]
@pytest.fixture()
def header():
return ["COL A", "COL B", "COL 3"]
@pytest.fixture()
def footer():
return ["", "", "2030203.00"]
@pytest.fixture()
def fg_colors():
return ["", "yellow", "87"]
@pytest.fixture()
def bg_colors():
return ["green", "23", ""]
def test_table_default(data):
result = table(data)
assert (
result
== "\nHello World 12344342\nThis is a test World 1234 \n"
)
def test_table_header(data, header):
result = table(data, header=header)
assert (
result
== "\nCOL A COL B COL 3 \nHello World 12344342\nThis is a test World 1234 \n"
)
def test_table_header_footer_divider(data, header, footer):
result = table(data, header=header, footer=footer, divider=True)
assert (
result
== "\nCOL A COL B COL 3 \n-------------- ----- ----------\nHello World 12344342 \nThis is a test World 1234 \n-------------- ----- ----------\n 2030203.00\n"
)
def test_table_aligns(data):
result = table(data, aligns=("r", "c", "l"))
assert (
result
== "\n Hello World 12344342\nThis is a test World 1234 \n"
)
def test_table_aligns_single(data):
result = table(data, aligns="r")
assert (
result
== "\n Hello World 12344342\nThis is a test World 1234\n"
)
def test_table_widths():
data = [("a", "bb", "ccc"), ("d", "ee", "fff")]
widths = (5, 2, 10)
result = table(data, widths=widths)
assert result == "\na bb ccc \nd ee fff \n"
def test_row_single_widths():
data = ("a", "bb", "ccc")
result = row(data, widths=10)
assert result == "a bb ccc "
def test_table_multiline(header):
data = [
("hello", ["foo", "bar", "baz"], "world"),
("hello", "world", ["world 1", "world 2"]),
]
result = table(data, header=header, divider=True, multiline=True)
assert (
result
== "\nCOL A COL B COL 3 \n----- ----- -------\nhello foo world \n bar \n baz \n \nhello world world 1\n world 2\n"
)
def test_row_fg_colors(fg_colors):
result = row(("Hello", "World", "12344342"), fg_colors=fg_colors)
if SUPPORTS_ANSI:
assert (
result == "Hello \x1b[38;5;3mWorld\x1b[0m \x1b[38;5;87m12344342\x1b[0m"
)
else:
assert result == "Hello World 12344342"
def test_row_bg_colors(bg_colors):
result = row(("Hello", "World", "12344342"), bg_colors=bg_colors)
if SUPPORTS_ANSI:
assert (
result == "\x1b[48;5;2mHello\x1b[0m \x1b[48;5;23mWorld\x1b[0m 12344342"
)
else:
assert result == "Hello World 12344342"
def test_row_fg_colors_and_bg_colors(fg_colors, bg_colors):
result = row(
("Hello", "World", "12344342"), fg_colors=fg_colors, bg_colors=bg_colors
)
if SUPPORTS_ANSI:
assert (
result
== "\x1b[48;5;2mHello\x1b[0m \x1b[38;5;3;48;5;23mWorld\x1b[0m \x1b[38;5;87m12344342\x1b[0m"
)
else:
assert result == "Hello World 12344342"
def test_row_fg_colors_and_bg_colors_log_friendly(fg_colors, bg_colors):
ENV_LOG_FRIENDLY = "WASABI_LOG_FRIENDLY"
os.environ[ENV_LOG_FRIENDLY] = "True"
result = row(
("Hello", "World", "12344342"), fg_colors=fg_colors, bg_colors=bg_colors
)
assert result == "Hello World 12344342"
del os.environ[ENV_LOG_FRIENDLY]
def test_row_fg_colors_and_bg_colors_log_friendly_prefix(fg_colors, bg_colors):
ENV_LOG_FRIENDLY = "CUSTOM_LOG_FRIENDLY"
os.environ[ENV_LOG_FRIENDLY] = "True"
result = row(
("Hello", "World", "12344342"),
fg_colors=fg_colors,
bg_colors=bg_colors,
env_prefix="CUSTOM",
)
assert result == "Hello World 12344342"
del os.environ[ENV_LOG_FRIENDLY]
def test_row_fg_colors_and_bg_colors_supports_ansi_false(fg_colors, bg_colors):
os.environ["ANSI_COLORS_DISABLED"] = "True"
result = row(
("Hello", "World", "12344342"), fg_colors=fg_colors, bg_colors=bg_colors
)
assert result == "Hello World 12344342"
del os.environ["ANSI_COLORS_DISABLED"]
def test_colors_whole_table_with_automatic_widths(
data, header, footer, fg_colors, bg_colors
):
result = table(
data,
header=header,
footer=footer,
divider=True,
fg_colors=fg_colors,
bg_colors=bg_colors,
)
if SUPPORTS_ANSI:
assert (
result
== "\n\x1b[48;5;2mCOL A \x1b[0m \x1b[38;5;3;48;5;23mCOL B\x1b[0m \x1b[38;5;87mCOL 3 \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m \x1b[38;5;3;48;5;23m-----\x1b[0m \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2mHello \x1b[0m \x1b[38;5;3;48;5;23mWorld\x1b[0m \x1b[38;5;87m12344342 \x1b[0m\n\x1b[48;5;2mThis is a test\x1b[0m \x1b[38;5;3;48;5;23mWorld\x1b[0m \x1b[38;5;87m1234 \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m \x1b[38;5;3;48;5;23m-----\x1b[0m \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m \x1b[0m \x1b[38;5;3;48;5;23m \x1b[0m \x1b[38;5;87m2030203.00\x1b[0m\n"
)
else:
assert (
result
== "\nCOL A COL B COL 3 \n-------------- ----- ----------\nHello World 12344342 \nThis is a test World 1234 \n-------------- ----- ----------\n 2030203.00\n"
)
def test_colors_whole_table_only_fg_colors(data, header, footer, fg_colors):
result = table(
data,
header=header,
footer=footer,
divider=True,
fg_colors=fg_colors,
)
if SUPPORTS_ANSI:
assert (
result
== "\nCOL A \x1b[38;5;3mCOL B\x1b[0m \x1b[38;5;87mCOL 3 \x1b[0m\n-------------- \x1b[38;5;3m-----\x1b[0m \x1b[38;5;87m----------\x1b[0m\nHello \x1b[38;5;3mWorld\x1b[0m \x1b[38;5;87m12344342 \x1b[0m\nThis is a test \x1b[38;5;3mWorld\x1b[0m \x1b[38;5;87m1234 \x1b[0m\n-------------- \x1b[38;5;3m-----\x1b[0m \x1b[38;5;87m----------\x1b[0m\n \x1b[38;5;3m \x1b[0m \x1b[38;5;87m2030203.00\x1b[0m\n"
)
else:
assert (
result
== "\nCOL A COL B COL 3 \n-------------- ----- ----------\nHello World 12344342 \nThis is a test World 1234 \n-------------- ----- ----------\n 2030203.00\n"
)
def test_colors_whole_table_only_bg_colors(data, header, footer, bg_colors):
result = table(
data,
header=header,
footer=footer,
divider=True,
bg_colors=bg_colors,
)
if SUPPORTS_ANSI:
assert (
result
== "\n\x1b[48;5;2mCOL A \x1b[0m \x1b[48;5;23mCOL B\x1b[0m COL 3 \n\x1b[48;5;2m--------------\x1b[0m \x1b[48;5;23m-----\x1b[0m ----------\n\x1b[48;5;2mHello \x1b[0m \x1b[48;5;23mWorld\x1b[0m 12344342 \n\x1b[48;5;2mThis is a test\x1b[0m \x1b[48;5;23mWorld\x1b[0m 1234 \n\x1b[48;5;2m--------------\x1b[0m \x1b[48;5;23m-----\x1b[0m ----------\n\x1b[48;5;2m \x1b[0m \x1b[48;5;23m \x1b[0m 2030203.00\n"
)
else:
assert (
result
== "\nCOL A COL B COL 3 \n-------------- ----- ----------\nHello World 12344342 \nThis is a test World 1234 \n-------------- ----- ----------\n 2030203.00\n"
)
def test_colors_whole_table_with_supplied_spacing(
data, header, footer, fg_colors, bg_colors
):
result = table(
data,
header=header,
footer=footer,
divider=True,
fg_colors=fg_colors,
bg_colors=bg_colors,
spacing=5,
)
if SUPPORTS_ANSI:
assert (
result
== "\n\x1b[48;5;2mCOL A \x1b[0m \x1b[38;5;3;48;5;23mCOL B\x1b[0m \x1b[38;5;87mCOL 3 \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m \x1b[38;5;3;48;5;23m-----\x1b[0m \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2mHello \x1b[0m \x1b[38;5;3;48;5;23mWorld\x1b[0m \x1b[38;5;87m12344342 \x1b[0m\n\x1b[48;5;2mThis is a test\x1b[0m \x1b[38;5;3;48;5;23mWorld\x1b[0m \x1b[38;5;87m1234 \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m \x1b[38;5;3;48;5;23m-----\x1b[0m \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m \x1b[0m \x1b[38;5;3;48;5;23m \x1b[0m \x1b[38;5;87m2030203.00\x1b[0m\n"
)
else:
assert (
result
== "\nCOL A COL B COL 3 \n-------------- ----- ----------\nHello World 12344342 \nThis is a test World 1234 \n-------------- ----- ----------\n 2030203.00\n"
)
def test_colors_whole_table_with_supplied_widths(
data, header, footer, fg_colors, bg_colors
):
result = table(
data,
header=header,
footer=footer,
divider=True,
fg_colors=fg_colors,
bg_colors=bg_colors,
widths=(5, 2, 10),
)
if SUPPORTS_ANSI:
assert (
result
== "\n\x1b[48;5;2mCOL A\x1b[0m \x1b[38;5;3;48;5;23mCOL B\x1b[0m \x1b[38;5;87mCOL 3 \x1b[0m\n\x1b[48;5;2m-----\x1b[0m \x1b[38;5;3;48;5;23m--\x1b[0m \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2mHello\x1b[0m \x1b[38;5;3;48;5;23mWorld\x1b[0m \x1b[38;5;87m12344342 \x1b[0m\n\x1b[48;5;2mThis is a test\x1b[0m \x1b[38;5;3;48;5;23mWorld\x1b[0m \x1b[38;5;87m1234 \x1b[0m\n\x1b[48;5;2m-----\x1b[0m \x1b[38;5;3;48;5;23m--\x1b[0m \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m \x1b[0m \x1b[38;5;3;48;5;23m \x1b[0m \x1b[38;5;87m2030203.00\x1b[0m\n"
)
else:
assert (
result
== "\nCOL A COL B COL 3 \n----- -- ----------\nHello World 12344342 \nThis is a test World 1234 \n----- -- ----------\n 2030203.00\n"
)
def test_colors_whole_table_with_single_alignment(
data, header, footer, fg_colors, bg_colors
):
result = table(
data,
header=header,
footer=footer,
divider=True,
fg_colors=fg_colors,
bg_colors=bg_colors,
aligns="r",
)
if SUPPORTS_ANSI:
assert (
result
== "\n\x1b[48;5;2m COL A\x1b[0m \x1b[38;5;3;48;5;23mCOL B\x1b[0m \x1b[38;5;87m COL 3\x1b[0m\n\x1b[48;5;2m--------------\x1b[0m \x1b[38;5;3;48;5;23m-----\x1b[0m \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m Hello\x1b[0m \x1b[38;5;3;48;5;23mWorld\x1b[0m \x1b[38;5;87m 12344342\x1b[0m\n\x1b[48;5;2mThis is a test\x1b[0m \x1b[38;5;3;48;5;23mWorld\x1b[0m \x1b[38;5;87m 1234\x1b[0m\n\x1b[48;5;2m--------------\x1b[0m \x1b[38;5;3;48;5;23m-----\x1b[0m \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m \x1b[0m \x1b[38;5;3;48;5;23m \x1b[0m \x1b[38;5;87m2030203.00\x1b[0m\n"
)
else:
assert (
result
== "\n COL A COL B COL 3\n-------------- ----- ----------\n Hello World 12344342\nThis is a test World 1234\n-------------- ----- ----------\n 2030203.00\n"
)
def test_colors_whole_table_with_multiple_alignment(
data, header, footer, fg_colors, bg_colors
):
result = table(
data,
header=header,
footer=footer,
divider=True,
fg_colors=fg_colors,
bg_colors=bg_colors,
aligns=("c", "r", "l"),
)
if SUPPORTS_ANSI:
assert (
result
== "\n\x1b[48;5;2m COL A \x1b[0m \x1b[38;5;3;48;5;23mCOL B\x1b[0m \x1b[38;5;87mCOL 3 \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m \x1b[38;5;3;48;5;23m-----\x1b[0m \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m Hello \x1b[0m \x1b[38;5;3;48;5;23mWorld\x1b[0m \x1b[38;5;87m12344342 \x1b[0m\n\x1b[48;5;2mThis is a test\x1b[0m \x1b[38;5;3;48;5;23mWorld\x1b[0m \x1b[38;5;87m1234 \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m \x1b[38;5;3;48;5;23m-----\x1b[0m \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m \x1b[0m \x1b[38;5;3;48;5;23m \x1b[0m \x1b[38;5;87m2030203.00\x1b[0m\n"
)
else:
assert (
result
== "\n COL A COL B COL 3 \n-------------- ----- ----------\n Hello World 12344342 \nThis is a test World 1234 \n-------------- ----- ----------\n 2030203.00\n"
)
def test_colors_whole_table_with_multiline(data, header, footer, fg_colors, bg_colors):
result = table(
data=((["Charles", "Quinton", "Murphy"], "my", "brother"), ("1", "2", "3")),
fg_colors=fg_colors,
bg_colors=bg_colors,
multiline=True,
)
if SUPPORTS_ANSI:
assert (
result
== "\n\x1b[48;5;2mCharles\x1b[0m \x1b[38;5;3;48;5;23mmy\x1b[0m \x1b[38;5;87mbrother\x1b[0m\n\x1b[48;5;2mQuinton\x1b[0m \x1b[38;5;3;48;5;23m \x1b[0m \x1b[38;5;87m \x1b[0m\n\x1b[48;5;2mMurphy \x1b[0m \x1b[38;5;3;48;5;23m \x1b[0m \x1b[38;5;87m \x1b[0m\n\x1b[48;5;2m \x1b[0m \x1b[38;5;3;48;5;23m \x1b[0m \x1b[38;5;87m \x1b[0m\n\x1b[48;5;2m1 \x1b[0m \x1b[38;5;3;48;5;23m2 \x1b[0m \x1b[38;5;87m3 \x1b[0m\n"
)
else:
assert (
result
== "\nCharles my brother\nQuinton \nMurphy \n \n1 2 3 \n"
)
def test_colors_whole_table_log_friendly(data, header, footer, fg_colors, bg_colors):
ENV_LOG_FRIENDLY = "WASABI_LOG_FRIENDLY"
os.environ[ENV_LOG_FRIENDLY] = "True"
result = table(
data,
header=header,
footer=footer,
divider=True,
fg_colors=fg_colors,
bg_colors=bg_colors,
)
assert (
result
== "\nCOL A COL B COL 3 \n-------------- ----- ----------\nHello World 12344342 \nThis is a test World 1234 \n-------------- ----- ----------\n 2030203.00\n"
)
del os.environ[ENV_LOG_FRIENDLY]
def test_colors_whole_table_log_friendly_prefix(
data, header, footer, fg_colors, bg_colors
):
ENV_LOG_FRIENDLY = "CUSTOM_LOG_FRIENDLY"
os.environ[ENV_LOG_FRIENDLY] = "True"
result = table(
data,
header=header,
footer=footer,
divider=True,
fg_colors=fg_colors,
bg_colors=bg_colors,
env_prefix="CUSTOM",
)
assert (
result
== "\nCOL A COL B COL 3 \n-------------- ----- ----------\nHello World 12344342 \nThis is a test World 1234 \n-------------- ----- ----------\n 2030203.00\n"
)
del os.environ[ENV_LOG_FRIENDLY]
def test_colors_whole_table_supports_ansi_false(
data, header, footer, fg_colors, bg_colors
):
os.environ["ANSI_COLORS_DISABLED"] = "True"
result = table(
data,
header=header,
footer=footer,
divider=True,
fg_colors=fg_colors,
bg_colors=bg_colors,
)
assert (
result
== "\nCOL A COL B COL 3 \n-------------- ----- ----------\nHello World 12344342 \nThis is a test World 1234 \n-------------- ----- ----------\n 2030203.00\n"
)
del os.environ["ANSI_COLORS_DISABLED"]
def test_colors_whole_table_color_values(data, header, footer, fg_colors, bg_colors):
result = table(
data,
header=header,
footer=footer,
divider=True,
fg_colors=fg_colors,
bg_colors=bg_colors,
color_values={"yellow": 11},
)
if SUPPORTS_ANSI:
assert (
result
== "\n\x1b[48;5;2mCOL A \x1b[0m \x1b[38;5;11;48;5;23mCOL B\x1b[0m \x1b[38;5;87mCOL 3 \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m \x1b[38;5;11;48;5;23m-----\x1b[0m \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2mHello \x1b[0m \x1b[38;5;11;48;5;23mWorld\x1b[0m \x1b[38;5;87m12344342 \x1b[0m\n\x1b[48;5;2mThis is a test\x1b[0m \x1b[38;5;11;48;5;23mWorld\x1b[0m \x1b[38;5;87m1234 \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m \x1b[38;5;11;48;5;23m-----\x1b[0m \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m \x1b[0m \x1b[38;5;11;48;5;23m \x1b[0m \x1b[38;5;87m2030203.00\x1b[0m\n"
)
else:
assert (
result
== "\nCOL A COL B COL 3 \n-------------- ----- ----------\nHello World 12344342 \nThis is a test World 1234 \n-------------- ----- ----------\n 2030203.00\n"
)

View File

@ -0,0 +1,66 @@
import traceback
import pytest
from wasabi.traceback_printer import TracebackPrinter
@pytest.fixture
def tb():
return traceback.extract_stack()
def test_traceback_printer(tb):
tbp = TracebackPrinter(tb_base="wasabi")
msg = tbp("Hello world", "This is a test", tb=tb)
print(msg)
def test_traceback_printer_highlight(tb):
tbp = TracebackPrinter(tb_base="wasabi")
msg = tbp("Hello world", "This is a test", tb=tb, highlight="kwargs")
print(msg)
def test_traceback_printer_custom_colors(tb):
tbp = TracebackPrinter(
tb_base="wasabi", color_error="blue", color_highlight="green", color_tb="yellow"
)
msg = tbp("Hello world", "This is a test", tb=tb, highlight="kwargs")
print(msg)
def test_traceback_printer_only_title(tb):
tbp = TracebackPrinter(tb_base="wasabi")
msg = tbp("Hello world", tb=tb)
print(msg)
def test_traceback_dot_relative_path_tb_base(tb):
tbp = TracebackPrinter(tb_base=".")
msg = tbp("Hello world", tb=tb)
print(msg)
def test_traceback_tb_base_none(tb):
tbp = TracebackPrinter()
msg = tbp("Hello world", tb=tb)
print(msg)
def test_traceback_printer_no_tb():
tbp = TracebackPrinter(tb_base="wasabi")
msg = tbp("Hello world", "This is a test")
print(msg)
def test_traceback_printer_custom_tb_range():
tbp = TracebackPrinter(tb_range_start=-10, tb_range_end=-3)
msg = tbp("Hello world", "This is a test")
print(msg)
def test_traceback_printer_custom_tb_range_start():
tbp = TracebackPrinter(tb_range_start=-1)
msg = tbp("Hello world", "This is a test")
print(msg)

View File

@ -0,0 +1,73 @@
import pytest
from wasabi.util import color, diff_strings, format_repr, locale_escape, wrap
def test_color():
assert color("test", fg="green") == "\x1b[38;5;2mtest\x1b[0m"
assert color("test", fg=4) == "\x1b[38;5;4mtest\x1b[0m"
assert color("test", bold=True) == "\x1b[1mtest\x1b[0m"
assert color("test", fg="red", underline=True) == "\x1b[4;38;5;1mtest\x1b[0m"
assert (
color("test", fg=7, bg="red", bold=True) == "\x1b[1;38;5;7;48;5;1mtest\x1b[0m"
)
def test_wrap():
text = "Hello world, this is a test."
assert wrap(text, indent=0) == text
assert wrap(text, indent=4) == " Hello world, this is a test."
assert wrap(text, wrap_max=10, indent=0) == "Hello\nworld,\nthis is a\ntest."
assert (
wrap(text, wrap_max=5, indent=2)
== " Hello\n world,\n this\n is\n a\n test."
)
def test_format_repr():
obj = {"hello": "world", "test": 123}
formatted = format_repr(obj)
assert formatted.replace("u'", "'") in [
"{'hello': 'world', 'test': 123}",
"{'test': 123, 'hello': 'world'}",
]
formatted = format_repr(obj, max_len=10)
assert formatted.replace("u'", "'") in [
"{'hel ... 123}",
"{'tes ... rld'}",
"{'te ... rld'}",
]
formatted = format_repr(obj, max_len=10, ellipsis="[...]")
assert formatted.replace("u'", "'") in [
"{'hel [...] 123}",
"{'tes [...] rld'}",
"{'te [...] rld'}",
]
@pytest.mark.parametrize(
"text,non_ascii",
[
("abc", ["abc"]),
("\u2714 abc", ["? abc"]),
("👻", ["??", "?"]), # On Python 3 windows, this becomes "?" instead of "??"
],
)
def test_locale_escape(text, non_ascii):
result = locale_escape(text)
assert result == text or result in non_ascii
print(result)
def test_diff_strings():
a = "hello\nworld\nwide\nweb"
b = "yo\nwide\nworld\nweb"
expected = "\x1b[38;5;16;48;5;2myo\x1b[0m\n\x1b[38;5;16;48;5;2mwide\x1b[0m\n\x1b[38;5;16;48;5;1mhello\x1b[0m\nworld\n\x1b[38;5;16;48;5;1mwide\x1b[0m\nweb"
assert diff_strings(a, b) == expected
def test_diff_strings_with_symbols():
a = "hello\nworld\nwide\nweb"
b = "yo\nwide\nworld\nweb"
expected = "\x1b[38;5;16;48;5;2m+ yo\x1b[0m\n\x1b[38;5;16;48;5;2m+ wide\x1b[0m\n\x1b[38;5;16;48;5;1m- hello\x1b[0m\nworld\n\x1b[38;5;16;48;5;1m- wide\x1b[0m\nweb"
assert diff_strings(a, b, add_symbols=True) == expected

View File

@ -0,0 +1,114 @@
import os
from typing import Optional, Tuple, Union
from .util import NO_UTF8, color, supports_ansi
LINE_EDGE = "└─" if not NO_UTF8 else "|_"
LINE_FORK = "├─" if not NO_UTF8 else "|__"
LINE_PATH = "──" if not NO_UTF8 else "__"
class TracebackPrinter(object):
def __init__(
self,
color_error: Union[str, int] = "red",
color_tb: Union[str, int] = "blue",
color_highlight: Union[str, int] = "yellow",
indent: int = 2,
tb_base: Optional[str] = None,
tb_exclude: Tuple = tuple(),
tb_range_start: int = -5,
tb_range_end: int = -2,
):
"""Initialize a traceback printer.
color_error (Union[str, int]): Color name or code for errors.
color_tb (Union[str, int]): Color name or code for traceback headline.
color_highlight (Union[str, int]): Color name or code for highlights.
indent (int): Indentation in spaces.
tb_base (Optional[str]): Optional name of directory to use to show relative paths. For
example, "thinc" will look for the last occurence of "/thinc/" in
a path and only show path to the right of it.
tb_exclude (tuple): List of filenames to exclude from traceback.
tb_range_start (int): The starting index from a traceback to include.
tb_range_end (int): The final index from a traceback to include. If None
the traceback will continue until the last record.
RETURNS (TracebackPrinter): The traceback printer.
"""
self.color_error = color_error
self.color_tb = color_tb
self.color_highlight = color_highlight
self.indent = " " * indent
if tb_base == ".":
tb_base = "{}{}".format(os.getcwd(), os.path.sep)
elif tb_base is not None:
tb_base = "/{}/".format(tb_base)
self.tb_base = tb_base
self.tb_exclude = tuple(tb_exclude)
self.tb_range_start = tb_range_start
self.tb_range_end = tb_range_end
self.supports_ansi = supports_ansi()
def __call__(self, title: str, *texts, **settings) -> str:
"""Output custom formatted tracebacks and errors.
title (str): The message title.
*texts (str): The texts to print (one per line).
RETURNS (str): The formatted traceback. Can be printed or raised
by custom exception.
"""
highlight = settings.get("highlight", False)
tb = settings.get("tb", None)
if self.supports_ansi: # use first line as title
title = color(title, fg=self.color_error, bold=True)
info = "\n" + "\n".join([self.indent + text for text in texts]) if texts else ""
tb = self._get_traceback(tb, highlight) if tb else ""
msg = "\n\n{}{}{}{}\n".format(self.indent, title, info, tb)
return msg
def _get_traceback(self, tb, highlight):
# Exclude certain file names from traceback
tb = [record for record in tb if not record[0].endswith(self.tb_exclude)]
tb_range = (
tb[self.tb_range_start : self.tb_range_end]
if self.tb_range_end is not None
else tb[self.tb_range_start :]
)
tb_list = [
self._format_traceback(path, line, fn, text, i, len(tb_range), highlight)
for i, (path, line, fn, text) in enumerate(tb_range)
]
tb_data = "\n".join(tb_list).strip()
title = "Traceback:"
if self.supports_ansi:
title = color(title, fg=self.color_tb, bold=True)
return "\n\n{indent}{title}\n{indent}{tb}".format(
title=title, tb=tb_data, indent=self.indent
)
def _format_traceback(self, path, line, fn, text, i, count, highlight):
template = "{base_indent}{indent} {fn} in {path}:{line}{text}"
indent = (LINE_EDGE if i == count - 1 else LINE_FORK) + LINE_PATH * i
if self.tb_base and self.tb_base in path:
path = path.rsplit(self.tb_base, 1)[1]
text = self._format_user_error(text, i, highlight) if i == count - 1 else ""
if self.supports_ansi:
fn = color(fn, bold=True)
path = color(path, underline=True)
return template.format(
base_indent=self.indent,
line=line,
indent=indent,
text=text,
fn=fn,
path=path,
)
def _format_user_error(self, text, i, highlight):
spacing = " " * i + " >>>"
if self.supports_ansi:
spacing = color(spacing, fg=self.color_error)
if highlight and self.supports_ansi:
formatted_highlight = color(highlight, fg=self.color_highlight)
text = text.replace(highlight, formatted_highlight)
return "\n{} {} {}".format(self.indent, spacing, text)

View File

@ -0,0 +1,221 @@
import difflib
import os
import sys
import textwrap
from typing import Any, Optional, Tuple, Union
STDOUT_ENCODING = sys.stdout.encoding if hasattr(sys.stdout, "encoding") else None
ENCODING = STDOUT_ENCODING or "ascii"
NO_UTF8 = ENCODING.lower() not in ("utf8", "utf-8")
# Environment variables
ENV_ANSI_DISABLED = "ANSI_COLORS_DISABLED" # no colors
class MESSAGES(object):
GOOD = "good"
FAIL = "fail"
WARN = "warn"
INFO = "info"
COLORS = {
MESSAGES.GOOD: 2,
MESSAGES.FAIL: 1,
MESSAGES.WARN: 3,
MESSAGES.INFO: 4,
"red": 1,
"green": 2,
"yellow": 3,
"blue": 4,
"pink": 5,
"cyan": 6,
"white": 7,
"grey": 8,
"black": 16,
}
ICONS = {
MESSAGES.GOOD: "\u2714" if not NO_UTF8 else "[+]",
MESSAGES.FAIL: "\u2718" if not NO_UTF8 else "[x]",
MESSAGES.WARN: "\u26a0" if not NO_UTF8 else "[!]",
MESSAGES.INFO: "\u2139" if not NO_UTF8 else "[i]",
}
INSERT_SYMBOL = "+"
DELETE_SYMBOL = "-"
def color(
text: str,
fg: Optional[Union[str, int]] = None,
bg: Optional[Union[str, int]] = None,
bold: bool = False,
underline: bool = False,
) -> str:
"""Color text by applying ANSI escape sequence.
text (str): The text to be formatted.
fg (Optional[Union[str, int]]): Optional foreground color. String name or 0 - 256 (see COLORS).
bg (Optional[Union[str, int]]): Optional background color. String name or 0 - 256 (see COLORS).
bold (bool): Format text in bold.
underline (bool): Underline text.
RETURNS (str): The formatted text.
"""
fg = COLORS.get(fg, fg) # type: ignore
bg = COLORS.get(bg, bg) # type: ignore
if not any([fg, bg, bold]):
return text
styles = []
if bold:
styles.append("1")
if underline:
styles.append("4")
if fg:
styles.append("38;5;{}".format(fg))
if bg:
styles.append("48;5;{}".format(bg))
return "\x1b[{}m{}\x1b[0m".format(";".join(styles), text)
def wrap(text: Any, wrap_max: int = 80, indent: int = 4) -> str:
"""Wrap text at given width using textwrap module.
text (Any): The text to wrap.
wrap_max (int): Maximum line width, including indentation. Defaults to 80.
indent (int): Number of spaces used for indentation. Defaults to 4.
RETURNS (str): The wrapped text with line breaks.
"""
indent_str = indent * " "
wrap_width = wrap_max - len(indent_str)
text = str(text)
return textwrap.fill(
text,
width=wrap_width,
initial_indent=indent_str,
subsequent_indent=indent_str,
break_long_words=False,
break_on_hyphens=False,
)
def format_repr(obj: Any, max_len: int = 50, ellipsis: str = "...") -> str:
"""Wrapper around `repr()` to print shortened and formatted string version.
obj: The object to represent.
max_len (int): Maximum string length. Longer strings will be cut in the
middle so only the beginning and end is displayed, separated by ellipsis.
ellipsis (str): Ellipsis character(s), e.g. "...".
RETURNS (str): The formatted representation.
"""
string = repr(obj)
if len(string) >= max_len:
half = int(max_len / 2)
return "{} {} {}".format(string[:half], ellipsis, string[-half:])
else:
return string
def diff_strings(
a: str,
b: str,
fg: Union[str, int] = "black",
bg: Union[Tuple[str, str], Tuple[int, int]] = ("green", "red"),
add_symbols: bool = False,
) -> str:
"""Compare two strings and return a colored diff with red/green background
for deletion and insertions.
a (str): The first string to diff.
b (str): The second string to diff.
fg (Union[str, int]): Foreground color. String name or 0 - 256 (see COLORS).
bg (Union[Tuple[str, str], Tuple[int, int]]): Background colors as
(insert, delete) tuple of string name or 0 - 256 (see COLORS).
add_symbols (bool): Whether to add symbols before the diff lines. Uses '+'
for inserts and '-' for deletions. Default is False.
RETURNS (str): The formatted diff.
"""
a_list = a.split("\n")
b_list = b.split("\n")
output = []
matcher = difflib.SequenceMatcher(None, a_list, b_list)
for opcode, a0, a1, b0, b1 in matcher.get_opcodes():
if opcode == "equal":
for item in a_list[a0:a1]:
output.append(item)
if opcode == "insert" or opcode == "replace":
for item in b_list[b0:b1]:
item = "{} {}".format(INSERT_SYMBOL, item) if add_symbols else item
output.append(color(item, fg=fg, bg=bg[0]))
if opcode == "delete" or opcode == "replace":
for item in a_list[a0:a1]:
item = "{} {}".format(DELETE_SYMBOL, item) if add_symbols else item
output.append(color(item, fg=fg, bg=bg[1]))
return "\n".join(output)
def get_raw_input(
description: str, default: Optional[Union[str, bool]] = False, indent: int = 4
) -> str:
"""Get user input from the command line via raw_input / input.
description (str): Text to display before prompt.
default (Optional[Union[str, bool]]): Optional default value to display with prompt.
indent (int): Indentation in spaces.
RETURNS (str): User input.
"""
additional = " (default: {})".format(default) if default else ""
prompt = wrap("{}{}: ".format(description, additional), indent=indent)
user_input = input(prompt)
return user_input
def locale_escape(string: Any, errors: str = "replace") -> str:
"""Mangle non-supported characters, for savages with ASCII terminals.
string (Any): The string to escape.
errors (str): The str.encode errors setting. Defaults to `"replace"`.
RETURNS (str): The escaped string.
"""
string = str(string)
string = string.encode(ENCODING, errors).decode("utf8")
return string
def can_render(string: str) -> bool:
"""Check if terminal can render unicode characters, e.g. special loading
icons. Can be used to display fallbacks for ASCII terminals.
string (str): The string to render.
RETURNS (bool): Whether the terminal can render the text.
"""
try:
string.encode(ENCODING)
return True
except UnicodeEncodeError:
return False
def supports_ansi() -> bool:
"""Returns True if the running system's terminal supports ANSI escape
sequences for color, formatting etc. and False otherwise.
RETURNS (bool): Whether the terminal supports ANSI colors.
"""
if os.getenv(ENV_ANSI_DISABLED):
return False
# We require colorama on Windows Python 3.7+, but we might be running on Unix, or we
# might be running on Windows Python 3.6. In both cases, colorama might be missing,
# *or* there might by accident happen to be an install of an old version that
# doesn't have just_fix_windows_console. So we need to confirm not just that we can
# import colorama, but that we can import just_fix_windows_console.
try:
from colorama import just_fix_windows_console
except ImportError:
if sys.platform == "win32" and "ANSICON" not in os.environ:
return False
else:
just_fix_windows_console()
return True