|
|
- # coding: utf8
- from __future__ import unicode_literals, print_function
-
- import datetime
- from collections import Counter
- from contextlib import contextmanager
- from multiprocessing import Process
- import itertools
- import sys
- import time
- import os
-
- from .tables import table, row
- from .util import wrap, supports_ansi, can_render, locale_escape
- from .util import MESSAGES, COLORS, ICONS
- from .util import color as _color
-
-
- class Printer(object):
- def __init__(
- self,
- pretty=True,
- no_print=False,
- colors=None,
- icons=None,
- line_max=80,
- animation="⠙⠹⠸⠼⠴⠦⠧⠇⠏",
- animation_ascii="|/-\\",
- hide_animation=False,
- ignore_warnings=False,
- env_prefix="WASABI",
- timestamp=False,
- ):
- """Initialize the command-line printer.
-
- pretty (bool): Pretty-print output (colors, icons).
- no_print (bool): Don't actually print, just return.
- colors (dict): Add or overwrite color values, name mapped to value.
- icons (dict): Add or overwrite icons. Name mapped to unicode icon.
- line_max (int): Maximum line length (for divider).
- animation (unicode): Steps of loading animation for loading() method.
- animation_ascii (unicode): 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 (unicode): 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()
- 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):
- """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="", text="", show=True, spaced=False, exits=None):
- """Print a success message."""
- return self._get_msg(
- title, text, style=MESSAGES.GOOD, show=show, spaced=spaced, exits=exits
- )
-
- def fail(self, title="", text="", show=True, spaced=False, exits=None):
- """Print an error message."""
- return self._get_msg(
- title, text, style=MESSAGES.FAIL, show=show, spaced=spaced, exits=exits
- )
-
- def warn(self, title="", text="", show=True, spaced=False, exits=None):
- """Print a warning message."""
- return self._get_msg(
- title, text, style=MESSAGES.WARN, show=show, spaced=spaced, exits=exits
- )
-
- def info(self, title="", text="", show=True, spaced=False, exits=None):
- """Print an informational message."""
- return self._get_msg(
- title, text, style=MESSAGES.INFO, show=show, spaced=spaced, exits=exits
- )
-
- def text(
- self,
- title="",
- text="",
- color=None,
- icon=None,
- spaced=False,
- show=True,
- no_print=False,
- exits=None,
- ):
- """Print a message.
-
- title (unicode): The main text to print.
- text (unicode): Optional additional text to print.
- color (unicode / int): Foreground color.
- icon (unicode): Name of icon to add.
- spaced (unicode): 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 (int): Perform a system exit.
- """
- if not show:
- return
- if self.pretty:
- color = self.colors.get(color)
- icon = self.icons.get(icon)
- if icon:
- title = locale_escape("{} {}".format(icon, title)).strip()
- if self.show_color:
- title = _color(title, fg=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()
- sys.exit(exits)
- if self.no_print or no_print:
- return title
-
- def divider(self, text="", char="=", show=True, icon=None):
- """Print a divider with a headline:
- ============================ Headline here ===========================
-
- text (unicode): Headline text. If empty, only the line is printed.
- char (unicode): Line character to repeat, e.g. =.
- show (bool): Whether to print or not.
- icon (unicode): Optional icon to display with title.
- """
- if len(char) != 1:
- raise ValueError(
- "Divider chars need to be one character long. "
- "Received: {}".format(char)
- )
- if self.pretty:
- icon = self.icons.get(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, **kwargs):
- """Print data as a table.
-
- data (iterable / 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, **kwargs):
- """Print a table row.
-
- data (iterable): 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="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="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, text, style=None, show=None, spaced=False, exits=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
- )
|