Module meerschaum.utils.formatting
Utilities for formatting output text
Expand source code
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
"""
Utilities for formatting output text
"""
from __future__ import annotations
import platform
import os
import sys
from meerschaum.utils.typing import Optional, Union, Any
from meerschaum.utils.formatting._shell import make_header
from meerschaum.utils.formatting._pprint import pprint
from meerschaum.utils.formatting._pipes import pprint_pipes, highlight_pipes
from meerschaum.utils.threading import Lock, RLock
_attrs = {
'ANSI': None,
'UNICODE': None,
'CHARSET': None,
}
__all__ = sorted([
'ANSI', 'CHARSET', 'UNICODE',
'colored',
'translate_rich_to_termcolor',
'get_console',
'print_tuple',
'fill_ansi',
'pprint',
'highlight_pipes',
'pprint_pipes',
'make_header',
])
__pdoc__ = {}
_locks = {
'_colorama_init': RLock(),
}
def colored_fallback(*args, **kw):
return ' '.join(args)
def translate_rich_to_termcolor(*colors) -> tuple:
"""Translate between rich and more_termcolor terminology.
This is probably prone to breaking.
Parameters
----------
*colors :
Returns
-------
"""
_colors = []
for c in colors:
_c_list = []
### handle 'bright'
c = c.replace('bright_', 'bright ')
### handle 'on'
if ' on ' in c:
_on = c.split(' on ')
_colors.append(_on[0])
for _c in _on[1:]:
_c_list.append('on ' + _c)
else:
_c_list += [c]
_colors += _c_list
return tuple(_colors)
def rich_text_to_str(text: 'rich.text.Text') -> str:
"""Convert a `rich.text.Text` object to a string with ANSI in-tact."""
_console = get_console()
if _console is None:
return str(text)
with console.capture() as cap:
console.print(text)
string = cap.get()
return string[:-1]
def _init():
"""
Initial color settings (mostly for Windows).
"""
if platform.system() != "Windows":
return
if 'PYTHONIOENCODING' not in os.environ:
os.environ['PYTHONIOENCODING'] = 'utf-8'
if 'PYTHONLEGACYWINDOWSSTDIO' not in os.environ:
os.environ['PYTHONLEGACYWINDOWSSTDIO'] = 'utf-8'
sys.stdin.reconfigure(encoding='utf-8')
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
from ctypes import windll
k = windll.kernel32
k.SetConsoleMode(k.GetStdHandle(-11), 7)
os.system("color")
from meerschaum.utils.packages import attempt_import
### init colorama for Windows color output
colorama, more_termcolor = attempt_import(
'colorama',
'more_termcolor',
lazy = False,
warn = False,
color = False,
)
try:
colorama.init(autoreset=False)
success = True
except Exception as e:
import traceback
traceback.print_exc()
_attrs['ANSI'], _attrs['UNICODE'], _attrs['CHARSET'] = False, False, 'ascii'
success = False
if more_termcolor is None:
_attrs['ANSI'], _attrs['UNICODE'], _attrs['CHARSET'] = False, False, 'ascii'
success = False
return success
_colorama_init = False
def colored(text: str, *colors, as_rich_text: bool=False, **kw) -> Union[str, 'rich.text.Text']:
"""Apply colors and rich styles to a string.
If a `style` keyword is provided, a `rich.text.Text` object will be parsed into a string.
Otherwise attempt to use the legacy `more_termcolor.colored` method.
Parameters
----------
text: str
The string to apply formatting to.
*colors:
A list of colors to pass to `more_termcolor.colored()`.
Has no effect if `style` is provided.
style: str, default None
If provided, pass to `rich` for processing.
as_rich_text: bool, default False
If `True`, return a `rich.Text` object.
`style` must be provided.
**kw:
Keyword arguments to pass to `rich.text.Text` or `more_termcolor`.
Returns
-------
An ANSI-formatted string or a `rich.text.Text` object if `as_rich_text` is `True`.
"""
from meerschaum.utils.packages import import_rich, attempt_import
global _colorama_init
_init()
with _locks['_colorama_init']:
if not _colorama_init:
_colorama_init = _init()
if 'style' in kw:
rich = import_rich()
rich_text = attempt_import('rich.text')
text_obj = rich_text.Text(text, **kw)
if as_rich_text:
return text_obj
return rich_text_to_str(text_obj)
more_termcolor = attempt_import('more_termcolor', lazy=False)
try:
colored_text = more_termcolor.colored(text, *colors, **kw)
except Exception as e:
colored_text = None
if colored_text is not None:
return colored_text
try:
_colors = translate_rich_to_termcolor(*colors)
colored_text = more_termcolor.colored(text, *_colors, **kw)
except Exception as e:
colored_text = None
if colored_text is None:
### NOTE: warn here?
return text
return colored_text
console = None
def get_console():
"""Get the rich console."""
global console
from meerschaum.utils.packages import import_rich, attempt_import
rich = import_rich()
rich_console = attempt_import('rich.console')
try:
console = rich_console.Console(force_terminal=True, color_system='truecolor')
except Exception as e:
console = None
return console
def print_tuple(
tup: tuple,
skip_common: bool = True,
common_only: bool = False,
upper_padding: int = 0,
lower_padding: int = 0,
_progress: Optional['rich.progress.Progress'] = None,
) -> None:
"""Print `meerschaum.utils.typing.SuccessTuple`."""
from meerschaum.config.static import STATIC_CONFIG
_init()
try:
status = 'success' if tup[0] else 'failure'
except TypeError:
status = 'failure'
tup = None, None
omit_messages = STATIC_CONFIG['system']['success']['ignore']
do_print = True
if common_only:
skip_common = False
do_print = tup[1] in omit_messages
if skip_common:
do_print = tup[1] not in omit_messages
if do_print:
ANSI, CHARSET = __getattr__('ANSI'), __getattr__('CHARSET')
from meerschaum.config import get_config
status_config = get_config('formatting', status, patch=True)
msg = ' ' + status_config[CHARSET]['icon'] + ' ' + str(tup[1])
lines = msg.split('\n')
lines = [lines[0]] + [
((' ' + line if not line.startswith(' ') else line))
for line in lines[1:]
]
if ANSI:
lines[0] = fill_ansi(highlight_pipes(lines[0]), **status_config['ansi']['rich'])
msg = '\n'.join(lines)
msg = ('\n' * upper_padding) + msg + ('\n' * lower_padding)
print(msg)
def fill_ansi(string: str, style: str = '') -> str:
"""
Fill in non-formatted segments of ANSI text.
Parameters
----------
string: str
A string which contains ANSI escape codes.
style: str
Style arguments to pass to `rich.text.Text`.
Returns
-------
A string with ANSI styling applied to the segments which don't yet have a style applied.
"""
from meerschaum.utils.packages import import_rich, attempt_import
from meerschaum.utils.misc import iterate_chunks
rich = import_rich()
Text = attempt_import('rich.text').Text
try:
msg = Text.from_ansi(string)
except AttributeError as e:
import traceback
traceback.print_stack()
msg = ''
plain_indices = []
for left_span, right_span in iterate_chunks(msg.spans, 2, fillvalue=len(msg)):
left = left_span.end
right = right_span.start if not isinstance(right_span, int) else right_span
if left != right:
plain_indices.append((left, right))
if msg.spans:
if msg.spans[0].start != 0:
plain_indices = [(0, msg.spans[0].start)] + plain_indices
if plain_indices and msg.spans[-1].end != len(msg) and plain_indices[-1][1] != len(msg):
plain_indices.append((msg.spans[-1].end, len(msg)))
if plain_indices:
for left, right in plain_indices:
msg.stylize(style, left, right)
else:
msg = Text(str(msg), style)
return rich_text_to_str(msg)
def __getattr__(name: str) -> str:
"""
Lazily load module-level variables.
"""
if name.startswith('__') and name.endswith('__'):
raise AttributeError("Cannot import dunders from this module.")
if name in _attrs:
if _attrs[name] is not None:
return _attrs[name]
from meerschaum.config import get_config
if name.lower() in get_config('formatting'):
_attrs[name] = get_config('formatting', name.lower())
elif name == 'CHARSET':
_attrs[name] = 'unicode' if __getattr__('UNICODE') else 'ascii'
return _attrs[name]
if name == '__wrapped__':
import sys
return sys.modules[__name__]
if name == '__all__':
return __all__
try:
return globals()[name]
except KeyError:
raise AttributeError(f"Could not find '{name}'")
Functions
def colored(text: str, *colors, as_rich_text: bool = False, **kw) ‑> Union[str, 'rich.text.Text']
-
Apply colors and rich styles to a string. If a
style
keyword is provided, arich.text.Text
object will be parsed into a string. Otherwise attempt to use the legacymore_termcolor.colored
method.Parameters
text
:str
- The string to apply formatting to.
*colors: A list of colors to pass to
more_termcolor.colored()
. Has no effect ifstyle
is provided.style
:str
, defaultNone
- If provided, pass to
rich
for processing. as_rich_text
:bool
, defaultFalse
- If
True
, return arich.Text
object.style
must be provided.
**kw: Keyword arguments to pass to
rich.text.Text
ormore_termcolor
.Returns
An ANSI-formatted string or a
rich.text.Text
object ifas_rich_text
isTrue
.Expand source code
def colored(text: str, *colors, as_rich_text: bool=False, **kw) -> Union[str, 'rich.text.Text']: """Apply colors and rich styles to a string. If a `style` keyword is provided, a `rich.text.Text` object will be parsed into a string. Otherwise attempt to use the legacy `more_termcolor.colored` method. Parameters ---------- text: str The string to apply formatting to. *colors: A list of colors to pass to `more_termcolor.colored()`. Has no effect if `style` is provided. style: str, default None If provided, pass to `rich` for processing. as_rich_text: bool, default False If `True`, return a `rich.Text` object. `style` must be provided. **kw: Keyword arguments to pass to `rich.text.Text` or `more_termcolor`. Returns ------- An ANSI-formatted string or a `rich.text.Text` object if `as_rich_text` is `True`. """ from meerschaum.utils.packages import import_rich, attempt_import global _colorama_init _init() with _locks['_colorama_init']: if not _colorama_init: _colorama_init = _init() if 'style' in kw: rich = import_rich() rich_text = attempt_import('rich.text') text_obj = rich_text.Text(text, **kw) if as_rich_text: return text_obj return rich_text_to_str(text_obj) more_termcolor = attempt_import('more_termcolor', lazy=False) try: colored_text = more_termcolor.colored(text, *colors, **kw) except Exception as e: colored_text = None if colored_text is not None: return colored_text try: _colors = translate_rich_to_termcolor(*colors) colored_text = more_termcolor.colored(text, *_colors, **kw) except Exception as e: colored_text = None if colored_text is None: ### NOTE: warn here? return text return colored_text
def fill_ansi(string: str, style: str = '') ‑> str
-
Fill in non-formatted segments of ANSI text.
Parameters
string
:str
- A string which contains ANSI escape codes.
style
:str
- Style arguments to pass to
rich.text.Text
.
Returns
A string with ANSI styling applied to the segments which don't yet have a style applied.
Expand source code
def fill_ansi(string: str, style: str = '') -> str: """ Fill in non-formatted segments of ANSI text. Parameters ---------- string: str A string which contains ANSI escape codes. style: str Style arguments to pass to `rich.text.Text`. Returns ------- A string with ANSI styling applied to the segments which don't yet have a style applied. """ from meerschaum.utils.packages import import_rich, attempt_import from meerschaum.utils.misc import iterate_chunks rich = import_rich() Text = attempt_import('rich.text').Text try: msg = Text.from_ansi(string) except AttributeError as e: import traceback traceback.print_stack() msg = '' plain_indices = [] for left_span, right_span in iterate_chunks(msg.spans, 2, fillvalue=len(msg)): left = left_span.end right = right_span.start if not isinstance(right_span, int) else right_span if left != right: plain_indices.append((left, right)) if msg.spans: if msg.spans[0].start != 0: plain_indices = [(0, msg.spans[0].start)] + plain_indices if plain_indices and msg.spans[-1].end != len(msg) and plain_indices[-1][1] != len(msg): plain_indices.append((msg.spans[-1].end, len(msg))) if plain_indices: for left, right in plain_indices: msg.stylize(style, left, right) else: msg = Text(str(msg), style) return rich_text_to_str(msg)
def get_console()
-
Get the rich console.
Expand source code
def get_console(): """Get the rich console.""" global console from meerschaum.utils.packages import import_rich, attempt_import rich = import_rich() rich_console = attempt_import('rich.console') try: console = rich_console.Console(force_terminal=True, color_system='truecolor') except Exception as e: console = None return console
def highlight_pipes(message: str) ‑> str
-
Add syntax highlighting to an info message containing stringified
Pipe
objects.Expand source code
def highlight_pipes(message: str) -> str: """ Add syntax highlighting to an info message containing stringified `meerschaum.Pipe` objects. """ if 'Pipe(' not in message or ')' not in message: return message from meerschaum import Pipe segments = message.split('Pipe(') msg = '' _d = {} for i, segment in enumerate(segments): if ',' in segment and ')' in segment: paren_index = segment.find(')') + 1 code = "_d['pipe'] = Pipe(" + segment[:paren_index] try: exec(code) _to_add = pipe_repr(_d['pipe']) + segment[paren_index:] except Exception as e: _to_add = 'Pipe(' + segment msg += _to_add continue msg += segment return msg
def make_header(message: str, ruler: str = '─') ‑> str
-
Format a message string with a ruler. Length of the ruler is the length of the longest word.
Example: 'My
header' -> 'My header ──────'
Expand source code
def make_header( message : str, ruler : str = '─', ) -> str: """Format a message string with a ruler. Length of the ruler is the length of the longest word. Example: 'My\nheader' -> 'My\nheader\n──────' """ from meerschaum.utils.formatting import ANSI, UNICODE, colored if not UNICODE: ruler = '-' words = message.split('\n') max_length = 0 for w in words: length = len(w) if length > max_length: max_length = length s = message + "\n" for i in range(max_length): s += ruler return s
def pprint(*args, detect_password: bool = True, nopretty: bool = False, **kw)
-
Pretty print an object according to the configured ANSI and UNICODE settings. If detect_password is True (default), search and replace passwords with '*' characters. Does not mutate objects.
Parameters
*args :
detect_password
:bool :
- (Default value = True)
nopretty
:bool :
- (Default value = False)
**kw :
Returns
Expand source code
def pprint( *args, detect_password : bool = True, nopretty : bool = False, **kw ): """Pretty print an object according to the configured ANSI and UNICODE settings. If detect_password is True (default), search and replace passwords with '*' characters. Does not mutate objects. Parameters ---------- *args : detect_password : bool : (Default value = True) nopretty : bool : (Default value = False) **kw : Returns ------- """ from meerschaum.utils.packages import attempt_import, import_rich from meerschaum.utils.formatting import ANSI, UNICODE, get_console from meerschaum.utils.warnings import error from meerschaum.utils.misc import replace_password, dict_from_od, filter_keywords from collections import OrderedDict import copy, json modify = True rich_pprint = None if ANSI and not nopretty: rich = import_rich() if rich is not None: rich_pretty = attempt_import('rich.pretty') if rich_pretty is not None: def _rich_pprint(*args, **kw): _console = get_console() _kw = filter_keywords(_console.print, **kw) _console.print(*args, **_kw) rich_pprint = _rich_pprint elif not nopretty: pprintpp = attempt_import('pprintpp', warn=False) try: _pprint = pprintpp.pprint except Exception as e: import pprint as _pprint_module _pprint = _pprint_module.pprint func = ( _pprint if rich_pprint is None else rich_pprint ) if not nopretty else print try: args_copy = copy.deepcopy(args) except Exception as e: args_copy = args modify = False _args = [] for a in args: c = a ### convert OrderedDict into dict if isinstance(a, OrderedDict) or issubclass(type(a), OrderedDict): c = dict_from_od(copy.deepcopy(c)) _args.append(c) args = _args _args = list(args) if detect_password and modify: _args = [] for a in args: c = a if isinstance(c, dict): c = replace_password(copy.deepcopy(c)) if nopretty: try: c = json.dumps(c) is_json = True except Exception as e: is_json = False if not is_json: try: c = str(c) except Exception as e: pass _args.append(c) ### filter out unsupported keywords func_kw = filter_keywords(func, **kw) if not nopretty else {} error_msg = None try: func(*_args, **func_kw) except Exception as e: error_msg = e if error_msg is not None: error(error_msg)
def pprint_pipes(pipes: PipesDict) ‑> None
-
Print a stylized tree of a Pipes dictionary. Supports ANSI and UNICODE global settings.
Expand source code
def pprint_pipes(pipes: PipesDict) -> None: """Print a stylized tree of a Pipes dictionary. Supports ANSI and UNICODE global settings.""" from meerschaum.utils.warnings import error from meerschaum.utils.packages import attempt_import, import_rich from meerschaum.utils.misc import sorted_dict, replace_pipes_in_dict from meerschaum.utils.formatting import UNICODE, ANSI, CHARSET, pprint, colored, get_console from meerschaum.config import get_config import copy rich = import_rich('rich', warn=False) Text = None if rich is not None: rich_text = attempt_import('rich.text', lazy=False) Text = rich_text.Text icons = get_config('formatting', 'pipes', CHARSET, 'icons') styles = get_config('formatting', 'pipes', 'ansi', 'styles') if not ANSI: styles = {k: '' for k in styles} print() def ascii_print_pipes(): """Print the dictionary with no unicode allowed. Also works in case rich fails to import (though rich should auto-install when `attempt_import()` is called).""" asciitree = attempt_import('asciitree') ascii_dict, replace_dict = {}, {'connector': {}, 'metric': {}, 'location': {}} for conn_keys, metrics in pipes.items(): _colored_conn_key = colored(icons['connector'] + conn_keys, style=styles['connector']) if Text is not None: replace_dict['connector'][_colored_conn_key] = ( Text(conn_keys, style=styles['connector']) ) ascii_dict[_colored_conn_key] = {} for metric, locations in metrics.items(): _colored_metric_key = colored(icons['metric'] + metric, style=styles['metric']) if Text is not None: replace_dict['metric'][_colored_metric_key] = ( Text(metric, style=styles['metric']) ) ascii_dict[_colored_conn_key][_colored_metric_key] = {} for location, pipe in locations.items(): _location_style = styles[('none' if location is None else 'location')] pipe_addendum = '\n ' + pipe.__repr__() + '\n' _colored_location = colored( icons['location'] + str(location), style=_location_style ) _colored_location_key = _colored_location + pipe_addendum if Text is not None: replace_dict['location'][_colored_location] = ( Text(str(location), style=_location_style) ) ascii_dict[_colored_conn_key][_colored_metric_key][_colored_location_key] = {} tree = asciitree.LeftAligned() output = '' cols = [] ### This is pretty terrible, unreadable code. ### Please know that I'm normally better than this. key_str = ( (Text(" ") if Text is not None else " ") + ( Text("Key", style='underline') if Text is not None else colored("Key", style='underline') ) + (Text('\n\n ') if Text is not None else '\n\n ') + ( Text("Connector", style=styles['connector']) if Text is not None else colored("Connector", style=styles['connector']) ) + (Text('\n +-- ') if Text is not None else '\n +-- ') + ( Text("Metric", style=styles['metric']) if Text is not None else colored("Metric", style=styles['metric']) ) + (Text('\n +-- ') if Text is not None else '\n +-- ') + ( Text("Location", style=styles['location']) if Text is not None else colored("Location", style=styles['location']) ) + (Text('\n\n') if Text is not None else '\n\n') ) output += str(key_str) cols.append(key_str) def replace_tree_text(tree_str : str) -> Text: """Replace the colored words with stylized Text instead. Is not executed if ANSI and UNICODE are disabled.""" tree_text = Text(tree_str) if Text is not None else None for k, v in replace_dict.items(): for _colored, _text in v.items(): parts = [] lines = tree_text.split(_colored) for part in lines: parts += [part, _text] if lines[-1] != Text(''): parts = parts[:-1] _tree_text = Text('') for part in parts: _tree_text += part tree_text = _tree_text return tree_text tree_output = "" for k, v in ascii_dict.items(): branch = {k : v} tree_output += tree(branch) + '\n\n' if not UNICODE and not ANSI: _col = (Text(tree(branch)) if Text is not None else tree(branch)) else: _col = replace_tree_text(tree(branch)) cols.append(_col) if len(output) > 0: tree_output = tree_output[:-2] output += tree_output if rich is None: return print(output) rich_columns = attempt_import('rich.columns') Columns = rich_columns.Columns columns = Columns(cols) get_console().print(columns) if not UNICODE: return ascii_print_pipes() rich_panel, rich_tree, rich_text, rich_columns, rich_table = attempt_import( 'rich.panel', 'rich.tree', 'rich.text', 'rich.columns', 'rich.table', ) from rich import box Panel = rich_panel.Panel Tree = rich_tree.Tree Text = rich_text.Text Columns = rich_columns.Columns Table = rich_table.Table key_panel = Panel( ( Text("\n") + Text(icons['connector'] + "Connector", style=styles['connector']) + Text("\n\n") + Text(icons['metric'] + "Metric", style=styles['metric']) + Text("\n\n") + Text(icons['location'] + "Location", style=styles['location']) + Text("\n") ), title = Text(icons['key'] + "Keys", style=styles['guide']), border_style = styles['guide'], expand = True ) cols = [] conn_trees = {} metric_trees = {} pipes = sorted_dict(pipes) for conn_keys, metrics in pipes.items(): conn_trees[conn_keys] = Tree( Text( icons['connector'] + conn_keys, style = styles['connector'], ), guide_style = styles['connector'] ) metric_trees[conn_keys] = {} for metric, locations in metrics.items(): metric_trees[conn_keys][metric] = Tree( Text( icons['metric'] + metric, style = styles['metric'] ), guide_style = styles['metric'] ) conn_trees[conn_keys].add(metric_trees[conn_keys][metric]) for location, pipe in locations.items(): _location = ( Text(str(location), style=styles['none']) if location is None else Text(location, style=styles['location']) ) _location = ( Text(icons['location']) + _location + Text('\n') + pipe_repr(pipe, as_rich_text=True) + Text('\n') ) metric_trees[conn_keys][metric].add(_location) cols += [key_panel] for k, t in conn_trees.items(): cols.append(t) columns = Columns(cols) get_console().print(columns)
def print_tuple(tup: tuple, skip_common: bool = True, common_only: bool = False, upper_padding: int = 0, lower_padding: int = 0) ‑> None
-
Print
meerschaum.utils.typing.SuccessTuple
.Expand source code
def print_tuple( tup: tuple, skip_common: bool = True, common_only: bool = False, upper_padding: int = 0, lower_padding: int = 0, _progress: Optional['rich.progress.Progress'] = None, ) -> None: """Print `meerschaum.utils.typing.SuccessTuple`.""" from meerschaum.config.static import STATIC_CONFIG _init() try: status = 'success' if tup[0] else 'failure' except TypeError: status = 'failure' tup = None, None omit_messages = STATIC_CONFIG['system']['success']['ignore'] do_print = True if common_only: skip_common = False do_print = tup[1] in omit_messages if skip_common: do_print = tup[1] not in omit_messages if do_print: ANSI, CHARSET = __getattr__('ANSI'), __getattr__('CHARSET') from meerschaum.config import get_config status_config = get_config('formatting', status, patch=True) msg = ' ' + status_config[CHARSET]['icon'] + ' ' + str(tup[1]) lines = msg.split('\n') lines = [lines[0]] + [ ((' ' + line if not line.startswith(' ') else line)) for line in lines[1:] ] if ANSI: lines[0] = fill_ansi(highlight_pipes(lines[0]), **status_config['ansi']['rich']) msg = '\n'.join(lines) msg = ('\n' * upper_padding) + msg + ('\n' * lower_padding) print(msg)
def translate_rich_to_termcolor(*colors) ‑> tuple
-
Translate between rich and more_termcolor terminology. This is probably prone to breaking.
Parameters
*colors :
Returns
Expand source code
def translate_rich_to_termcolor(*colors) -> tuple: """Translate between rich and more_termcolor terminology. This is probably prone to breaking. Parameters ---------- *colors : Returns ------- """ _colors = [] for c in colors: _c_list = [] ### handle 'bright' c = c.replace('bright_', 'bright ') ### handle 'on' if ' on ' in c: _on = c.split(' on ') _colors.append(_on[0]) for _c in _on[1:]: _c_list.append('on ' + _c) else: _c_list += [c] _colors += _c_list return tuple(_colors)