meerschaum.utils.formatting

Utilities for formatting output text

  1#! /usr/bin/env python
  2# -*- coding: utf-8 -*-
  3# vim:fenc=utf-8
  4
  5"""
  6Utilities for formatting output text
  7"""
  8
  9from __future__ import annotations
 10import platform
 11import os
 12import sys
 13import meerschaum as mrsm
 14from meerschaum.utils.typing import Optional, Union, Any, Dict, Iterable
 15from meerschaum.utils.formatting._shell import make_header
 16from meerschaum.utils.formatting._pprint import pprint
 17from meerschaum.utils.formatting._dataframe import pprint_df, format_dataframe
 18from meerschaum.utils.formatting._pipes import (
 19    pprint_pipes,
 20    highlight_pipes,
 21    format_pipe_success_tuple,
 22    print_pipes_results,
 23    extract_stats_from_message,
 24    pipe_repr,
 25)
 26from meerschaum.utils.threading import Lock, RLock
 27
 28_attrs = {
 29    'ANSI': None,
 30    'UNICODE': None,
 31    'CHARSET': None,
 32    'RESET': '\033[0m',
 33}
 34__all__ = sorted([
 35    'ANSI', 'CHARSET', 'UNICODE', 'RESET',
 36    'ansi_allowed',
 37    'stderr_ansi',
 38    'colored',
 39    'translate_rich_to_termcolor',
 40    'get_console',
 41    'print_exception',
 42    'format_success_tuple',
 43    'print_tuple',
 44    'print_options',
 45    'fill_ansi',
 46    'pprint',
 47    'pprint_df',
 48    'format_dataframe',
 49    'highlight_pipes',
 50    'pprint_pipes',
 51    'make_header',
 52    'pipe_repr',
 53    'print_pipes_results',
 54    'extract_stats_from_message',
 55    'format_bytes',
 56])
 57__pdoc__ = {}
 58_locks = {
 59    '_colorama_init': RLock(),
 60}
 61
 62
 63def colored_fallback(*args, **kw):
 64    return ' '.join(args)
 65
 66
 67def _env_flag(name: str) -> str:
 68    """The trimmed, lower-case value of an environment variable (`''` when unset)."""
 69    return (os.environ.get(name, None) or '').strip().lower()
 70
 71
 72def ansi_allowed(stream: Optional[Any] = None) -> bool:
 73    """
 74    Return whether output may carry ANSI styles, before the `formatting:ansi` setting.
 75
 76    `NO_COLOR` (any non-empty value) turns styles off. Otherwise styles are on when the stream is
 77    a terminal, inside a job (its logs keep their colors, `MRSM_DAEMON_ID`), in a Jupyter notebook
 78    or when `FORCE_COLOR` is set (any value but `0`, `false` or `no`). Writing to a file or a
 79    pipe (`mrsm show pipes > pipes.txt`) therefore prints plain text.
 80
 81    Parameters
 82    ----------
 83    stream: Optional[Any], default None
 84        The stream the output is written to (`sys.stderr` for warnings). Defaults to `sys.stdout`.
 85    """
 86    if os.environ.get('NO_COLOR', None):
 87        return False
 88    force = _env_flag('FORCE_COLOR')
 89    if force and force not in ('0', 'false', 'no'):
 90        return True
 91    from meerschaum._internal.static import STATIC_CONFIG
 92    if os.environ.get(STATIC_CONFIG['environment']['daemon_id'], None):
 93        return True
 94    if _in_notebook():
 95        return True
 96    try:
 97        return bool((sys.stdout if stream is None else stream).isatty())
 98    except Exception:
 99        return False
100
101
102def stderr_ansi() -> bool:
103    """
104    Whether text written to stderr (warnings) may carry ANSI styles: `formatting:ansi` and
105    `ansi_allowed(sys.stderr)`, so `2> errors.log` gets plain text while stdout is a terminal.
106    """
107    __getattr__('ANSI')  ### Loads `formatting:ansi` into `_attrs`.
108    return bool(_attrs['ANSI']) and ansi_allowed(sys.stderr)
109
110
111def _in_notebook() -> bool:
112    """
113    Whether this is a Jupyter (IPython kernel) process: its outputs render ANSI styles although
114    its stdout is not a terminal. Only an already-imported `ipykernel` is looked at.
115    """
116    if 'ipykernel' not in sys.modules:
117        return False
118    try:
119        from IPython import get_ipython
120        shell = get_ipython()
121    except Exception:
122        return False
123    return shell is not None and getattr(shell, 'kernel', None) is not None
124
125
126def format_bytes(num_bytes: Optional[Union[int, float]], precision: int = 1) -> str:
127    """
128    Return a human-readable representation of a number of bytes.
129
130    Parameters
131    ----------
132    num_bytes: Optional[Union[int, float]]
133        The number of bytes to format. If `None`, return `'?'`.
134
135    precision: int, default 1
136        The number of decimal places to display for non-byte units.
137
138    Returns
139    -------
140    A human-readable string such as `'1.2 MB'` or `'340.0 kB'`.
141
142    Examples
143    --------
144    >>> format_bytes(0)
145    '0 B'
146    >>> format_bytes(1536)
147    '1.5 kB'
148    >>> format_bytes(None)
149    '?'
150    """
151    if num_bytes is None:
152        return '?'
153    try:
154        value = float(num_bytes)
155    except (TypeError, ValueError):
156        return '?'
157
158    sign = '-' if value < 0 else ''
159    value = abs(value)
160    units = ('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB')
161    unit_index = 0
162    while value >= 1000.0 and unit_index < len(units) - 1:
163        value /= 1000.0
164        unit_index += 1
165
166    if unit_index == 0:
167        return f"{sign}{int(value)} {units[unit_index]}"
168    return f"{sign}{value:.{precision}f} {units[unit_index]}"
169
170def translate_rich_to_termcolor(*colors) -> tuple:
171    """Translate between rich and more_termcolor terminology."""
172    _colors = []
173    for c in colors:
174        _c_list = []
175        ### handle 'bright'
176        c = c.replace('bright_', 'bright ')
177
178        ### handle 'on'
179        if ' on ' in c:
180            _on = c.split(' on ')
181            _colors.append(_on[0])
182            for _c in _on[1:]:
183                _c_list.append('on ' + _c)
184        else:
185            _c_list += [c]
186
187        _colors += _c_list
188
189    return tuple(_colors)
190
191
192def rich_text_to_str(text: 'rich.text.Text', ansi: Optional[bool] = None) -> str:
193    """
194    Convert a `rich.text.Text` object to a string with ANSI in-tact.
195
196    Parameters
197    ----------
198    text: rich.text.Text
199        The text to render.
200
201    ansi: Optional[bool], default None
202        `True` renders the styles whatever `ANSI` says (text for another stream, such as a
203        warning for a terminal stderr while stdout is a file). Defaults to `get_console()`'s.
204    """
205    _console = _styled_console() if ansi else get_console()
206    if _console is None:
207        return str(text)
208    with _console.capture() as cap:
209        _console.print(text)
210    string = cap.get()
211    return string[:-1]
212
213
214_styled = None
215
216
217def _styled_console():
218    """A rich console that always renders styles (see `rich_text_to_str(ansi=True)`)."""
219    global _styled
220    if _styled is not None:
221        return _styled
222    from meerschaum.utils.packages import import_rich, attempt_import
223    import_rich(lazy=False)
224    rich_console = attempt_import('rich.console', lazy=False)
225    if rich_console is None:
226        return None
227    _styled = rich_console.Console(force_terminal=True, color_system='truecolor')
228    return _styled
229
230
231def _init():
232    """
233    Initial color settings (mostly for Windows).
234    """
235    if platform.system() != "Windows":
236        return
237    if 'PYTHONIOENCODING' not in os.environ:
238        os.environ['PYTHONIOENCODING'] = 'utf-8'
239    if 'PYTHONLEGACYWINDOWSSTDIO' not in os.environ:
240        os.environ['PYTHONLEGACYWINDOWSSTDIO'] = 'utf-8'
241    sys.stdin.reconfigure(encoding='utf-8')
242    sys.stdout.reconfigure(encoding='utf-8')
243    sys.stderr.reconfigure(encoding='utf-8')
244
245    from ctypes import windll
246    k = windll.kernel32
247    k.SetConsoleMode(k.GetStdHandle(-11), 7)
248    os.system("color")
249
250    from meerschaum.utils.packages import attempt_import
251    ### init colorama for Windows color output
252    colorama, more_termcolor = attempt_import(
253        'colorama',
254        'more_termcolor',
255        lazy = False,
256        warn = False,
257        color = False,
258    )
259    try:
260        colorama.init(autoreset=False)
261        success = True
262    except Exception:
263        import traceback
264        traceback.print_exc()
265        _attrs['ANSI'], _attrs['UNICODE'], _attrs['CHARSET'] = False, False, 'ascii'
266        success = False
267
268    if more_termcolor is None:
269        _attrs['ANSI'], _attrs['UNICODE'], _attrs['CHARSET'] = False, False, 'ascii'
270        success = False
271
272    return success
273
274_colorama_init = False
275def colored(text: str, *colors, as_rich_text: bool=False, **kw) -> Union[str, 'rich.text.Text']:
276    """Apply colors and rich styles to a string.
277    If a `style` keyword is provided, a `rich.text.Text` object will be parsed into a string.
278    Otherwise attempt to use the legacy `more_termcolor.colored` method.
279
280    Parameters
281    ----------
282    text: str
283        The string to apply formatting to.
284        
285    *colors:
286        A list of colors to pass to `more_termcolor.colored()`.
287        Has no effect if `style` is provided.
288
289    style: str, default None
290        If provided, pass to `rich` for processing.
291
292    as_rich_text: bool, default False
293        If `True`, return a `rich.Text` object.
294        `style` must be provided.
295        
296    **kw:
297        Keyword arguments to pass to `rich.text.Text` or `more_termcolor`.
298        
299
300    Returns
301    -------
302    An ANSI-formatted string or a `rich.text.Text` object if `as_rich_text` is `True`.
303
304    """
305    from meerschaum.utils.packages import import_rich, attempt_import
306    global _colorama_init
307    _init()
308    with _locks['_colorama_init']:
309        if not _colorama_init:
310            _colorama_init = _init()
311
312    if 'style' in kw:
313        import_rich(lazy=False)
314        rich_text = attempt_import('rich.text', lazy=False)
315        if rich_text is None:
316            ### `rich` is not a dependency of the base wheel: without it, text stays unstyled.
317            return text
318        text_obj = rich_text.Text(text, **kw)
319        if as_rich_text:
320            return text_obj
321        return rich_text_to_str(text_obj)
322
323    ### Off a terminal, under `NO_COLOR` or with `formatting:ansi` off, the text stays plain.
324    if not __getattr__('ANSI'):
325        return text
326
327    more_termcolor = attempt_import('more_termcolor', lazy=False)
328    try:
329        colored_text = more_termcolor.colored(text, *colors, **kw)
330    except Exception as e:
331        colored_text = None
332
333    if colored_text is not None:
334        return colored_text
335
336    try:
337        _colors = translate_rich_to_termcolor(*colors)
338        colored_text = more_termcolor.colored(text, *_colors, **kw)
339    except Exception as e:
340        colored_text = None
341
342    if colored_text is None:
343        ### NOTE: warn here?
344        return text
345
346    return colored_text
347
348console = None
349_console_ansi = None
350def get_console():
351    """
352    Get the rich console: styled (truecolor) when `ANSI` is on, plain text otherwise
353    (see `ansi_allowed()`).
354    """
355    global console, _console_ansi
356    ansi = bool(__getattr__('ANSI'))
357    if console is not None and _console_ansi == ansi:
358        return console
359    from meerschaum.utils.packages import import_rich, attempt_import
360    import_rich(lazy=False)
361    rich_console = attempt_import('rich.console', lazy=False)
362    if rich_console is None:
363        ### `rich` is not a dependency of the base wheel.
364        return None
365    try:
366        console = (
367            rich_console.Console(force_terminal=True, color_system='truecolor')
368            if ansi
369            else rich_console.Console(force_terminal=False, color_system=None, no_color=True)
370        )
371        _console_ansi = ansi
372    except Exception:
373        import traceback
374        traceback.print_exc()
375        console = None
376    return console
377
378
379def print_exception(**kw: Any) -> None:
380    """
381    Print the exception being handled: with `rich` when it is installed, else as plain text.
382    """
383    console = get_console()
384    if console is None:
385        import traceback
386        traceback.print_exc()
387        return
388    console.print_exception(**kw)
389
390
391def print_tuple(
392    tup: mrsm.SuccessTuple,
393    skip_common: bool = True,
394    common_only: bool = False,
395    upper_padding: int = 1,
396    lower_padding: int = 1,
397    left_padding: int = 1,
398    calm: bool = False,
399    _progress: Optional['rich.progress.Progress'] = None,
400) -> None:
401    """
402    Format `meerschaum.utils.typing.SuccessTuple`.
403
404    Parameters
405    ----------
406    skip_common: bool, default True
407        If `True`, do not print common success tuples (i.e. `(True, "Success")`).
408
409    common_only: bool, default False
410        If `True`, only print if the success tuple is common.
411
412    upper_padding: int, default 0
413        How many newlines to prepend to the message.
414
415    lower_padding: int, default 0
416        How many newlines to append to the message.
417
418    left_padding: int, default 1
419        How mant spaces to preprend to the message.
420
421    calm: bool, default False
422        If `True`, use the default emoji and color scheme.
423
424    """
425    from meerschaum._internal.static import STATIC_CONFIG
426    do_print = True
427
428    omit_messages = STATIC_CONFIG['system']['success']['ignore']
429
430    if common_only:
431        skip_common = False
432        do_print = tup[1].strip() in omit_messages
433
434    if skip_common:
435        do_print = tup[1].strip() not in omit_messages
436
437    if not do_print:
438        return
439
440    print(format_success_tuple(
441        tup,
442        upper_padding=upper_padding,
443        lower_padding=lower_padding,
444        calm=calm,
445        _progress=_progress,
446    ))
447
448
449def format_success_tuple(
450    tup: mrsm.SuccessTuple,
451    upper_padding: int = 0,
452    lower_padding: int = 0,
453    left_padding: int = 1,
454    calm: bool = False,
455    _progress: Optional['rich.progress.Progress'] = None,
456) -> str:
457    """
458    Format `meerschaum.utils.typing.SuccessTuple`.
459
460    Parameters
461    ----------
462    upper_padding: int, default 0
463        How many newlines to prepend to the message.
464
465    lower_padding: int, default 0
466        How many newlines to append to the message.
467
468    left_padding: int, default 1
469        How mant spaces to preprend to the message.
470
471    calm: bool, default False
472        If `True`, use the default emoji and color scheme.
473    """
474    _init()
475    try:
476        status = 'success' if tup[0] else 'failure'
477    except TypeError:
478        status = 'failure'
479        tup = None, None
480
481    if calm:
482        status += '_calm'
483
484    ANSI, CHARSET = __getattr__('ANSI'), __getattr__('CHARSET')
485    from meerschaum.config import get_config
486    status_config = get_config('formatting', status, patch=True)
487
488    msg = (' ' * left_padding) + status_config[CHARSET]['icon'] + ' ' + str(highlight_pipes(tup[1]))
489    lines = msg.split('\n')
490    lines = [lines[0]] + [
491        (('    ' + line if not line.startswith(' ') else line))
492        for line in lines[1:]
493    ]
494    if ANSI:
495        lines[0] = fill_ansi(lines[0], **status_config['ansi']['rich'])
496
497    msg = '\n'.join(lines)
498    msg = ('\n' * upper_padding) + msg + ('\n' * lower_padding)
499    return msg
500
501
502def print_options(
503    options: Optional[Iterable[Any]] = None,
504    nopretty: bool = False,
505    no_rich: bool = False,
506    name: str = 'options',
507    header: Optional[str] = None,
508    num_cols: Optional[int] = None,
509    adjust_cols: bool = True,
510    sort_options: bool = False,
511    number_options: bool = False,
512    **kw
513) -> None:
514    """
515    Print items in an iterable as a fancy table.
516
517    Parameters
518    ----------
519    options: Optional[Dict[str, Any]], default None
520        The iterable to be printed.
521
522    nopretty: bool, default False
523        If `True`, don't use fancy formatting.
524
525    no_rich: bool, default False
526        If `True`, don't use `rich` to format the output.
527
528    name: str, default 'options'
529        The text in the default header after `'Available'`.
530
531    header: Optional[str], default None
532        If provided, override `name` and use this as the header text.
533
534    num_cols: Optional[int], default None
535        How many columns in the table. Depends on the terminal size. If `None`, use 8.
536
537    adjust_cols: bool, default True
538        If `True`, adjust the number of columns depending on the terminal size.
539
540    sort_options: bool, default False
541        If `True`, print the options in sorted order.
542
543    number_options: bool, default False
544        If `True`, print the option's number in the list (1 index).
545
546    """
547    from meerschaum.utils.packages import import_rich
548    from meerschaum.utils.formatting import highlight_pipes
549    from meerschaum.utils.misc import get_cols_lines, string_width
550
551    if options is None:
552        options = {}
553    _options = []
554    for o in options:
555        _options.append(str(o))
556    if sort_options:
557        _options = sorted(_options)
558    _header = f"\nAvailable {name}" if header is None else header
559
560    if num_cols is None:
561        num_cols = 8
562
563    def _print_options_no_rich():
564        if not nopretty:
565            print()
566            print(make_header(_header))
567        ### print actions
568        for i, option in enumerate(_options):
569            marker = '-' if not number_options else (str(i + 1) + '.')
570            if not nopretty:
571                print(f"  {marker} ", end="")
572            print(option)
573        if not nopretty:
574            print()
575
576    ### `rich` is not a dependency of the base wheel: without it, options print as plain text.
577    rich = import_rich(lazy=False)
578    if rich is None or nopretty or no_rich or get_console() is None:
579        _print_options_no_rich()
580        return None
581
582    ### Prevent too many options from being truncated on small terminals.
583    if adjust_cols and _options:
584        _cols, _lines = get_cols_lines()
585        while num_cols > 1:
586            cell_len = int(((_cols - 4) - (3 * (num_cols - 1))) / num_cols)
587            num_too_big = sum([(1 if string_width(o) > cell_len else 0) for o in _options])
588            if num_too_big > int(len(_options) / 3):
589                num_cols -= 1
590                continue
591            break
592
593    from meerschaum.utils.packages import attempt_import
594    rich_table = attempt_import('rich.table')
595    Text = attempt_import('rich.text').Text
596    box = attempt_import('rich.box')
597    Table = rich_table.Table
598
599    if _header is not None:
600        table = Table(
601            title=_header,
602            box=box.SIMPLE,
603            show_header=False,
604            show_footer=False,
605            title_style='',
606            expand = True,
607        )
608    else:
609        table = Table.grid(padding=(0, 2))
610    for i in range(num_cols):
611        table.add_column()
612
613    if len(_options) < 12:
614        ### If fewer than 12 items, use a single column
615        for i, option in enumerate(_options):
616            item = highlight_pipes(option)
617            if number_options:
618                item = str(i + 1) + '. ' + item
619            table.add_row(Text.from_ansi(item))
620    else:
621        ### Otherwise, use multiple columns as before
622        num_rows = (len(_options) + num_cols - 1) // num_cols
623        item_ix = 0
624        for i in range(num_rows):
625            row = []
626            for j in range(num_cols):
627                index = i + j * num_rows
628                if index < len(_options):
629                    item = highlight_pipes(_options[index])
630                    if number_options:
631                        item = str(i + 1) + '. ' + item
632                    row.append(Text.from_ansi(item))
633                    item_ix += 1
634                else:
635                    row.append('')
636            table.add_row(*row)
637
638    get_console().print(table)
639    return None
640
641
642def fill_ansi(string: str, style: str = '', ansi: Optional[bool] = None) -> str:
643    """
644    Fill in non-formatted segments of ANSI text.
645
646    Parameters
647    ----------
648    string: str
649        A string which contains ANSI escape codes.
650
651    style: str
652        Style arguments to pass to `rich.text.Text`.
653
654    ansi: Optional[bool], default None
655        `True` keeps the styles whatever `ANSI` says (see `rich_text_to_str()`).
656
657    Returns
658    -------
659    A string with ANSI styling applied to the segments which don't yet have a style applied.
660    """
661    from meerschaum.utils.packages import import_rich, attempt_import
662    from meerschaum.utils.misc import iterate_chunks
663    _ = import_rich(lazy=False)
664    rich_ansi, rich_text = attempt_import('rich.ansi', 'rich.text', lazy=False)
665    if rich_ansi is None or rich_text is None:
666        ### `rich` is not a dependency of the base wheel: without it, text stays unstyled.
667        return string
668    Text = rich_text.Text
669    try:
670        msg = Text.from_ansi(string)
671    except AttributeError:
672        import traceback
673        traceback.print_exc()
674        msg = ''
675
676    plain_indices = []
677    for left_span, right_span in iterate_chunks(msg.spans, 2, fillvalue=len(msg)):
678        left = left_span.end
679        right = right_span.start if not isinstance(right_span, int) else right_span
680        if left != right:
681            plain_indices.append((left, right))
682    if msg.spans:
683        if msg.spans[0].start != 0:
684            plain_indices = [(0, msg.spans[0].start)] + plain_indices
685        if plain_indices and msg.spans[-1].end != len(msg) and plain_indices[-1][1] != len(msg):
686            plain_indices.append((msg.spans[-1].end, len(msg)))
687
688    if plain_indices:
689        for left, right in plain_indices:
690            msg.stylize(style, left, right)
691    else:
692        msg = Text(str(msg), style)
693
694    return rich_text_to_str(msg, ansi=ansi)
695
696
697def __getattr__(name: str) -> str:
698    """
699    Lazily load module-level variables.
700    """
701    if name.startswith('__') and name.endswith('__'):
702        raise AttributeError("Cannot import dunders from this module.")
703
704    if name in _attrs:
705        if _attrs[name] is None:
706            from meerschaum.config import get_config
707            if name.lower() in get_config('formatting'):
708                _attrs[name] = get_config('formatting', name.lower())
709            elif name == 'CHARSET':
710                _attrs[name] = 'unicode' if __getattr__('UNICODE') else 'ascii'
711        if name == 'ANSI':
712            ### `formatting:ansi`, and only where styles belong (`ansi_allowed()`).
713            return bool(_attrs[name]) and ansi_allowed()
714        return _attrs[name]
715    
716    if name == '__wrapped__':
717        import sys
718        return sys.modules[__name__]
719    if name == '__all__':
720        return __all__
721
722    try:
723        return globals()[name]
724    except KeyError:
725        raise AttributeError(f"Could not find '{name}'")
ANSI
CHARSET
RESET
UNICODE
def ansi_allowed(stream: Any | None = None) -> bool:
 73def ansi_allowed(stream: Optional[Any] = None) -> bool:
 74    """
 75    Return whether output may carry ANSI styles, before the `formatting:ansi` setting.
 76
 77    `NO_COLOR` (any non-empty value) turns styles off. Otherwise styles are on when the stream is
 78    a terminal, inside a job (its logs keep their colors, `MRSM_DAEMON_ID`), in a Jupyter notebook
 79    or when `FORCE_COLOR` is set (any value but `0`, `false` or `no`). Writing to a file or a
 80    pipe (`mrsm show pipes > pipes.txt`) therefore prints plain text.
 81
 82    Parameters
 83    ----------
 84    stream: Optional[Any], default None
 85        The stream the output is written to (`sys.stderr` for warnings). Defaults to `sys.stdout`.
 86    """
 87    if os.environ.get('NO_COLOR', None):
 88        return False
 89    force = _env_flag('FORCE_COLOR')
 90    if force and force not in ('0', 'false', 'no'):
 91        return True
 92    from meerschaum._internal.static import STATIC_CONFIG
 93    if os.environ.get(STATIC_CONFIG['environment']['daemon_id'], None):
 94        return True
 95    if _in_notebook():
 96        return True
 97    try:
 98        return bool((sys.stdout if stream is None else stream).isatty())
 99    except Exception:
100        return False

Return whether output may carry ANSI styles, before the formatting:ansi setting.

NO_COLOR (any non-empty value) turns styles off. Otherwise styles are on when the stream is a terminal, inside a job (its logs keep their colors, MRSM_DAEMON_ID), in a Jupyter notebook or when FORCE_COLOR is set (any value but 0, false or no). Writing to a file or a pipe (mrsm show pipes > pipes.txt) therefore prints plain text.

Parameters
  • stream (Optional[Any], default None): The stream the output is written to (sys.stderr for warnings). Defaults to sys.stdout.
def colored( text: str, *colors, as_rich_text: bool = False, **kw) -> str | rich.text.Text:
276def colored(text: str, *colors, as_rich_text: bool=False, **kw) -> Union[str, 'rich.text.Text']:
277    """Apply colors and rich styles to a string.
278    If a `style` keyword is provided, a `rich.text.Text` object will be parsed into a string.
279    Otherwise attempt to use the legacy `more_termcolor.colored` method.
280
281    Parameters
282    ----------
283    text: str
284        The string to apply formatting to.
285        
286    *colors:
287        A list of colors to pass to `more_termcolor.colored()`.
288        Has no effect if `style` is provided.
289
290    style: str, default None
291        If provided, pass to `rich` for processing.
292
293    as_rich_text: bool, default False
294        If `True`, return a `rich.Text` object.
295        `style` must be provided.
296        
297    **kw:
298        Keyword arguments to pass to `rich.text.Text` or `more_termcolor`.
299        
300
301    Returns
302    -------
303    An ANSI-formatted string or a `rich.text.Text` object if `as_rich_text` is `True`.
304
305    """
306    from meerschaum.utils.packages import import_rich, attempt_import
307    global _colorama_init
308    _init()
309    with _locks['_colorama_init']:
310        if not _colorama_init:
311            _colorama_init = _init()
312
313    if 'style' in kw:
314        import_rich(lazy=False)
315        rich_text = attempt_import('rich.text', lazy=False)
316        if rich_text is None:
317            ### `rich` is not a dependency of the base wheel: without it, text stays unstyled.
318            return text
319        text_obj = rich_text.Text(text, **kw)
320        if as_rich_text:
321            return text_obj
322        return rich_text_to_str(text_obj)
323
324    ### Off a terminal, under `NO_COLOR` or with `formatting:ansi` off, the text stays plain.
325    if not __getattr__('ANSI'):
326        return text
327
328    more_termcolor = attempt_import('more_termcolor', lazy=False)
329    try:
330        colored_text = more_termcolor.colored(text, *colors, **kw)
331    except Exception as e:
332        colored_text = None
333
334    if colored_text is not None:
335        return colored_text
336
337    try:
338        _colors = translate_rich_to_termcolor(*colors)
339        colored_text = more_termcolor.colored(text, *_colors, **kw)
340    except Exception as e:
341        colored_text = None
342
343    if colored_text is None:
344        ### NOTE: warn here?
345        return text
346
347    return colored_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.
def extract_stats_from_message(message: str, stat_keys: List[str] | None = None) -> Dict[str, int]:
575def extract_stats_from_message(
576    message: str,
577    stat_keys: Optional[List[str]] = None,
578) -> Dict[str, int]:
579    """
580    Given a sync message, return the insert, update, upsert stats from within.
581
582    Parameters
583    ----------
584    message: str
585        The message to parse for statistics.
586
587    stat_keys: Optional[List[str]], default None
588        If provided, search for these words (case insensitive) in the message.
589        Defaults to `['inserted', 'updated', 'upserted']`.
590
591    Returns
592    -------
593    A dictionary mapping the stat keys to the total number of rows affected.
594    """
595    stat_keys = stat_keys or ['inserted', 'updated', 'upserted', 'checked']
596    lines_stats = [extract_stats_from_line(line, stat_keys) for line in message.split('\n')]
597    message_stats = {
598        stat_key: sum(stats.get(stat_key, 0) for stats in lines_stats)
599        for stat_key in stat_keys
600    }
601    return message_stats

Given a sync message, return the insert, update, upsert stats from within.

Parameters
  • message (str): The message to parse for statistics.
  • stat_keys (Optional[List[str]], default None): If provided, search for these words (case insensitive) in the message. Defaults to ['inserted', 'updated', 'upserted'].
Returns
  • A dictionary mapping the stat keys to the total number of rows affected.
def fill_ansi(string: str, style: str = '', ansi: bool | None = None) -> str:
643def fill_ansi(string: str, style: str = '', ansi: Optional[bool] = None) -> str:
644    """
645    Fill in non-formatted segments of ANSI text.
646
647    Parameters
648    ----------
649    string: str
650        A string which contains ANSI escape codes.
651
652    style: str
653        Style arguments to pass to `rich.text.Text`.
654
655    ansi: Optional[bool], default None
656        `True` keeps the styles whatever `ANSI` says (see `rich_text_to_str()`).
657
658    Returns
659    -------
660    A string with ANSI styling applied to the segments which don't yet have a style applied.
661    """
662    from meerschaum.utils.packages import import_rich, attempt_import
663    from meerschaum.utils.misc import iterate_chunks
664    _ = import_rich(lazy=False)
665    rich_ansi, rich_text = attempt_import('rich.ansi', 'rich.text', lazy=False)
666    if rich_ansi is None or rich_text is None:
667        ### `rich` is not a dependency of the base wheel: without it, text stays unstyled.
668        return string
669    Text = rich_text.Text
670    try:
671        msg = Text.from_ansi(string)
672    except AttributeError:
673        import traceback
674        traceback.print_exc()
675        msg = ''
676
677    plain_indices = []
678    for left_span, right_span in iterate_chunks(msg.spans, 2, fillvalue=len(msg)):
679        left = left_span.end
680        right = right_span.start if not isinstance(right_span, int) else right_span
681        if left != right:
682            plain_indices.append((left, right))
683    if msg.spans:
684        if msg.spans[0].start != 0:
685            plain_indices = [(0, msg.spans[0].start)] + plain_indices
686        if plain_indices and msg.spans[-1].end != len(msg) and plain_indices[-1][1] != len(msg):
687            plain_indices.append((msg.spans[-1].end, len(msg)))
688
689    if plain_indices:
690        for left, right in plain_indices:
691            msg.stylize(style, left, right)
692    else:
693        msg = Text(str(msg), style)
694
695    return rich_text_to_str(msg, ansi=ansi)

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.
  • ansi (Optional[bool], default None): True keeps the styles whatever ANSI says (see rich_text_to_str()).
Returns
  • A string with ANSI styling applied to the segments which don't yet have a style applied.
def format_bytes(num_bytes: int | float | None, precision: int = 1) -> str:
127def format_bytes(num_bytes: Optional[Union[int, float]], precision: int = 1) -> str:
128    """
129    Return a human-readable representation of a number of bytes.
130
131    Parameters
132    ----------
133    num_bytes: Optional[Union[int, float]]
134        The number of bytes to format. If `None`, return `'?'`.
135
136    precision: int, default 1
137        The number of decimal places to display for non-byte units.
138
139    Returns
140    -------
141    A human-readable string such as `'1.2 MB'` or `'340.0 kB'`.
142
143    Examples
144    --------
145    >>> format_bytes(0)
146    '0 B'
147    >>> format_bytes(1536)
148    '1.5 kB'
149    >>> format_bytes(None)
150    '?'
151    """
152    if num_bytes is None:
153        return '?'
154    try:
155        value = float(num_bytes)
156    except (TypeError, ValueError):
157        return '?'
158
159    sign = '-' if value < 0 else ''
160    value = abs(value)
161    units = ('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB')
162    unit_index = 0
163    while value >= 1000.0 and unit_index < len(units) - 1:
164        value /= 1000.0
165        unit_index += 1
166
167    if unit_index == 0:
168        return f"{sign}{int(value)} {units[unit_index]}"
169    return f"{sign}{value:.{precision}f} {units[unit_index]}"

Return a human-readable representation of a number of bytes.

Parameters
  • num_bytes (Optional[Union[int, float]]): The number of bytes to format. If None, return '?'.
  • precision (int, default 1): The number of decimal places to display for non-byte units.
Returns
  • A human-readable string such as '1.2 MB' or '340.0 kB'.
Examples
>>> format_bytes(0)
'0 B'
>>> format_bytes(1536)
'1.5 kB'
>>> format_bytes(None)
'?'
def format_dataframe(df: Any, max_rows: int | None = None) -> str:
20def format_dataframe(df: Any, max_rows: int | None = None) -> str:
21    """
22    Return a full, untruncated string representation of a DataFrame.
23
24    Parameters
25    ----------
26    df: pandas.DataFrame
27        The DataFrame to format.
28
29    max_rows: int | None, default None
30        If set, only render the first `max_rows` rows and append a note.
31        `None` renders every row.
32
33    Returns
34    -------
35    A Markdown table (falling back to a plain fixed-width table if `tabulate`
36    is unavailable), followed by a `[rows x columns]` shape footer.
37    """
38    from meerschaum.utils.packages import attempt_import
39    pd = attempt_import('pandas', lazy=False)
40
41    n_rows, n_cols = df.shape
42    render_df = df if (max_rows is None or n_rows <= max_rows) else df.head(max_rows)
43    truncated_rows = n_rows - len(render_df)
44
45    ### Never let pandas insert `...` for columns, rows, or cell contents.
46    options = [
47        'display.max_columns', None,
48        'display.max_rows', None,
49        'display.width', None,
50        'display.max_colwidth', None,
51    ]
52
53    body = None
54    with pd.option_context(*options):
55        try:
56            tabulate = attempt_import('tabulate', warn=False)
57            if tabulate is not None:
58                body = render_df.to_markdown(index=False)
59        except Exception:
60            body = None
61
62        if body is None:
63            ### Fall back to the built-in fixed-width table (no extra dependency).
64            body = render_df.to_string(index=False, max_rows=None, max_cols=None)
65
66    footer = (
67        f"\n[{n_rows} row{'s' if n_rows != 1 else ''} "
68        f"x {n_cols} column{'s' if n_cols != 1 else ''}]"
69    )
70    if truncated_rows > 0:
71        footer = (
72            f"\n[showing first {len(render_df)} of {n_rows} rows "
73            f"x {n_cols} column{'s' if n_cols != 1 else ''}]"
74        )
75    return body + footer

Return a full, untruncated string representation of a DataFrame.

Parameters
  • df (pandas.DataFrame): The DataFrame to format.
  • max_rows (int | None, default None): If set, only render the first max_rows rows and append a note. None renders every row.
Returns
  • A Markdown table (falling back to a plain fixed-width table if tabulate
  • is unavailable), followed by a [rows x columns] shape footer.
def format_success_tuple( tup: Tuple[bool, str], upper_padding: int = 0, lower_padding: int = 0, left_padding: int = 1, calm: bool = False, _progress: rich.progress.Progress | None = None) -> str:
450def format_success_tuple(
451    tup: mrsm.SuccessTuple,
452    upper_padding: int = 0,
453    lower_padding: int = 0,
454    left_padding: int = 1,
455    calm: bool = False,
456    _progress: Optional['rich.progress.Progress'] = None,
457) -> str:
458    """
459    Format `meerschaum.utils.typing.SuccessTuple`.
460
461    Parameters
462    ----------
463    upper_padding: int, default 0
464        How many newlines to prepend to the message.
465
466    lower_padding: int, default 0
467        How many newlines to append to the message.
468
469    left_padding: int, default 1
470        How mant spaces to preprend to the message.
471
472    calm: bool, default False
473        If `True`, use the default emoji and color scheme.
474    """
475    _init()
476    try:
477        status = 'success' if tup[0] else 'failure'
478    except TypeError:
479        status = 'failure'
480        tup = None, None
481
482    if calm:
483        status += '_calm'
484
485    ANSI, CHARSET = __getattr__('ANSI'), __getattr__('CHARSET')
486    from meerschaum.config import get_config
487    status_config = get_config('formatting', status, patch=True)
488
489    msg = (' ' * left_padding) + status_config[CHARSET]['icon'] + ' ' + str(highlight_pipes(tup[1]))
490    lines = msg.split('\n')
491    lines = [lines[0]] + [
492        (('    ' + line if not line.startswith(' ') else line))
493        for line in lines[1:]
494    ]
495    if ANSI:
496        lines[0] = fill_ansi(lines[0], **status_config['ansi']['rich'])
497
498    msg = '\n'.join(lines)
499    msg = ('\n' * upper_padding) + msg + ('\n' * lower_padding)
500    return msg

Format meerschaum.utils.typing.SuccessTuple.

Parameters
  • upper_padding (int, default 0): How many newlines to prepend to the message.
  • lower_padding (int, default 0): How many newlines to append to the message.
  • left_padding (int, default 1): How mant spaces to preprend to the message.
  • calm (bool, default False): If True, use the default emoji and color scheme.
def get_console():
351def get_console():
352    """
353    Get the rich console: styled (truecolor) when `ANSI` is on, plain text otherwise
354    (see `ansi_allowed()`).
355    """
356    global console, _console_ansi
357    ansi = bool(__getattr__('ANSI'))
358    if console is not None and _console_ansi == ansi:
359        return console
360    from meerschaum.utils.packages import import_rich, attempt_import
361    import_rich(lazy=False)
362    rich_console = attempt_import('rich.console', lazy=False)
363    if rich_console is None:
364        ### `rich` is not a dependency of the base wheel.
365        return None
366    try:
367        console = (
368            rich_console.Console(force_terminal=True, color_system='truecolor')
369            if ansi
370            else rich_console.Console(force_terminal=False, color_system=None, no_color=True)
371        )
372        _console_ansi = ansi
373    except Exception:
374        import traceback
375        traceback.print_exc()
376        console = None
377    return console

Get the rich console: styled (truecolor) when ANSI is on, plain text otherwise (see ansi_allowed()).

def highlight_pipes(message: str) -> str:
433def highlight_pipes(message: str) -> str:
434    """
435    Add syntax highlighting to an info message containing stringified `meerschaum.Pipe` objects.
436    """
437    if 'Pipe(' not in message:
438        return message
439
440    from meerschaum.utils.misc import parse_arguments_str
441    segments = message.split('Pipe(')
442    msg = ''
443    for i, segment in enumerate(segments):
444        if i == 0:
445            msg += segment
446            continue
447
448        paren_index = segment.find(')')
449        if paren_index == -1:
450            msg += 'Pipe(' + segment
451            continue
452        
453        pipe_args_str = segment[:paren_index]
454        try:
455            args, kwargs = parse_arguments_str(pipe_args_str)
456            pipe_dict = {
457                'connector_keys': args[0],
458                'metric_key': args[1],
459            }
460            if len(args) > 2:
461                pipe_dict['location_key'] = args[2]
462            if 'instance' in kwargs:
463                pipe_dict['instance_keys'] = kwargs['instance']
464            
465            _to_add = pipe_repr(pipe_dict) + segment[paren_index + 1:]
466        except Exception:
467            _to_add = 'Pipe(' + segment
468        msg += _to_add
469    return msg

Add syntax highlighting to an info message containing stringified meerschaum.Pipe objects.

def make_header( message: str, ruler: str = '─', left_pad: int = 2, top: bool = False, top_pad: int = 1) -> str:
17def make_header(
18    message: str,
19    ruler: str = '─',
20    left_pad: int = 2,
21    top: bool = False,
22    top_pad: int = 1,
23) -> str:
24    """
25    Format a message string with a ruler or box.
26    Length of the ruler is the length of the longest word.
27    
28    Example:
29        'My\nheader' -> '  My\n  header\n  ──────'
30    """
31
32    from meerschaum.utils.formatting import ANSI, UNICODE, colored
33    if not UNICODE:
34        ruler = '-'
35    words = message.split('\n')
36    max_length = 0
37    for w in words:
38        length = len(w)
39        if length > max_length:
40            max_length = length
41
42    left_buffer = left_pad * ' '
43
44    return (
45        (('\n' * top_pad) if top else "")
46        + left_buffer
47        + (((ruler * max_length) + '\n') if top else "")
48        + message.replace('\n', '\n' + left_buffer)
49        + "\n"
50        + left_buffer
51        + (ruler * max_length)
52    )

Format a message string with a ruler or box. Length of the ruler is the length of the longest word.

Example:
    'My

header' -> ' My header ──────'

def pipe_repr( pipe: meerschaum.Pipe | Dict[str, Any], as_rich_text: bool = False, ansi: bool | None = None) -> str | rich.text.Text:
314def pipe_repr(
315    pipe: Union[mrsm.Pipe, Dict[str, Any]],
316    as_rich_text: bool = False,
317    ansi: Optional[bool] = None,
318) -> Union[str, 'rich.text.Text']:
319    """
320    Return a formatted string for representing a `meerschaum.Pipe`.
321    """
322    from meerschaum.utils.formatting import ANSI, colored, rich_text_to_str
323    from meerschaum.utils.packages import import_rich, attempt_import
324    import meerschaum as mrsm
325
326    _ = import_rich(lazy=False)
327    rich_text = attempt_import('rich.text', lazy=False)
328
329    if isinstance(pipe, mrsm.Pipe):
330        connector_keys = pipe.connector_keys
331        metric_key = pipe.metric_key
332        location_key = pipe.location_key
333        instance_keys = pipe.instance_keys
334    else:
335        connector_keys = pipe.get('connector_keys')
336        metric_key = pipe.get('metric_key')
337        location_key = pipe.get('location_key')
338        instance_keys = pipe.get('instance_keys', get_config('meerschaum', 'instance'))
339
340    if rich_text is None:
341        ### `rich` is not a dependency of the base wheel: the plain representation.
342        plain = f"Pipe('{connector_keys}', '{metric_key}'"
343        if location_key is not None:
344            plain += f", '{location_key}'"
345        if instance_keys != get_config('meerschaum', 'instance'):
346            plain += f", instance='{instance_keys}'"
347        return plain + ')'
348    Text = rich_text.Text
349
350    styles = get_config('formatting', 'pipes', '__repr__', 'ansi', 'styles')
351    if not ANSI or (ansi is False):
352        styles = {k: '' for k in styles}
353    _pipe_style_prefix, _pipe_style_suffix = (
354        (("[" + styles['Pipe'] + "]"), ("[/" + styles['Pipe'] + "]")) if styles['Pipe']
355        else ('', '')
356    )
357    text_obj = (
358        Text.from_markup(_pipe_style_prefix + "Pipe(" + _pipe_style_suffix)
359        + colored(("'" + connector_keys + "'"), style=styles['connector'], as_rich_text=True)
360        + Text.from_markup(_pipe_style_prefix + ", " + _pipe_style_suffix)
361        + colored(("'" + metric_key + "'"), style=styles['metric'], as_rich_text=True)
362        + (
363            (
364                colored(', ', style=styles['punctuation'], as_rich_text=True)
365                + colored(
366                    ("'" + location_key + "'"),
367                    style=styles['location'], as_rich_text=True
368                )
369            ) if location_key is not None
370            else colored('', style='', as_rich_text=True)
371        ) + (
372            ( ### Add the `instance=` argument.
373                colored(', instance=', style=styles['punctuation'], as_rich_text=True)
374                + colored(
375                    ("'" + instance_keys + "'"),
376                    style=styles['instance'], as_rich_text=True
377                )
378            ) if instance_keys != get_config('meerschaum', 'instance')
379            else colored('', style='', as_rich_text=True)
380        )
381        + Text.from_markup(_pipe_style_prefix + ")" + _pipe_style_suffix)
382    )
383    if as_rich_text:
384        return text_obj
385    return rich_text_to_str(text_obj).replace('\n', '')

Return a formatted string for representing a meerschaum.Pipe.

def pprint( *args, detect_password: bool = True, nopretty: bool = False, **kw) -> None:
 10def pprint(
 11    *args,
 12    detect_password: bool = True,
 13    nopretty: bool = False,
 14    **kw
 15) -> None:
 16    """Pretty print an object according to the configured ANSI and UNICODE settings.
 17    If detect_password is True (default), search and replace passwords with '*' characters.
 18    Does not mutate objects.
 19    """
 20    import copy
 21    import json
 22    from meerschaum.utils.packages import attempt_import, import_rich
 23    from meerschaum.utils.formatting import ANSI, get_console, print_tuple
 24    from meerschaum.utils.warnings import error
 25    from meerschaum.utils.misc import replace_password, dict_from_od, filter_keywords
 26    from collections import OrderedDict
 27
 28    if (
 29        len(args) == 1
 30        and
 31        isinstance(args[0], tuple)
 32        and
 33        len(args[0]) == 2
 34        and
 35        isinstance(args[0][0], bool)
 36        and
 37        isinstance(args[0][1], str)
 38    ):
 39        return print_tuple(args[0], **filter_keywords(print_tuple, **kw))
 40
 41    modify = True
 42    rich_pprint = None
 43    if ANSI and not nopretty:
 44        ### `rich` is not a dependency of the base wheel: without it, output is not styled.
 45        rich = import_rich(lazy=False)
 46        rich_pretty = attempt_import('rich.pretty', lazy=False) if rich is not None else None
 47        if rich_pretty is not None and get_console() is not None:
 48            def _rich_pprint(*args, **kw):
 49                _console = get_console()
 50                _kw = filter_keywords(_console.print, **kw)
 51                _console.print(*args, **_kw)
 52            rich_pprint = _rich_pprint
 53    if rich_pprint is None and not nopretty:
 54        pprintpp = attempt_import('pprintpp', warn=False)
 55        try:
 56            _pprint = pprintpp.pprint
 57        except Exception :
 58            import pprint as _pprint_module
 59            _pprint = _pprint_module.pprint
 60
 61    func = (
 62        _pprint if rich_pprint is None else rich_pprint
 63    ) if not nopretty else print
 64
 65    try:
 66        args_copy = copy.deepcopy(args)
 67    except Exception:
 68        args_copy = args
 69        modify = False
 70
 71    _args = []
 72    for a in args:
 73        c = a
 74        ### convert OrderedDict into dict
 75        if isinstance(a, OrderedDict) or issubclass(type(a), OrderedDict):
 76            c = dict_from_od(copy.deepcopy(c))
 77        _args.append(c)
 78    args = _args
 79
 80    _args = list(args)
 81    if detect_password and modify:
 82        _args = []
 83        for a in args:
 84            c = a
 85            if isinstance(c, dict):
 86                c = replace_password(copy.deepcopy(c))
 87            if nopretty:
 88                try:
 89                    c = json.dumps(c)
 90                    is_json = True
 91                except Exception:
 92                    is_json = False
 93                if not is_json:
 94                    try:
 95                        c = str(c)
 96                    except Exception:
 97                        pass
 98            _args.append(c)
 99
100    ### filter out unsupported keywords
101    func_kw = filter_keywords(func, **kw) if not nopretty else {}
102    error_msg = None
103    try:
104        func(*_args, **func_kw)
105    except Exception as e:
106        error_msg = e
107    if error_msg is not None:
108        error(error_msg)

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.

def pprint_df(df: Any, max_rows: int | None = None) -> None:
78def pprint_df(df: Any, max_rows: int | None = None) -> None:
79    """
80    Print a DataFrame in full (no column truncation), Markdown-formatted.
81
82    See `format_dataframe` for details.
83    """
84    print(format_dataframe(df, max_rows=max_rows))

Print a DataFrame in full (no column truncation), Markdown-formatted.

See format_dataframe for details.

def pprint_pipes( pipes: Dict[str, Dict[str, Dict[str | None, meerschaum.Pipe]]]) -> None:
 17def pprint_pipes(pipes: PipesDict) -> None:
 18    """Print a stylized tree of a Pipes dictionary.
 19    Supports ANSI and UNICODE global settings."""
 20    from meerschaum.utils.warnings import error
 21    from meerschaum.utils.packages import attempt_import, import_rich
 22    from meerschaum.utils.misc import sorted_dict, replace_pipes_in_dict
 23    from meerschaum.utils.formatting import UNICODE, ANSI, CHARSET, pprint, colored, get_console
 24    import copy
 25    rich = import_rich(lazy=False, warn=False)
 26    Text = None
 27    if rich is not None:
 28        rich_text = attempt_import('rich.text', lazy=False)
 29        Text = rich_text.Text if rich_text is not None else None
 30        if Text is None:
 31            rich = None
 32
 33    def plain_print_pipes():
 34        """Print an indented list: `rich` (and `asciitree`) are not dependencies of the base wheel."""
 35        lines = []
 36        for conn_keys, metrics in pipes.items():
 37            lines.append(conn_keys)
 38            for metric, locations in metrics.items():
 39                lines.append('  ' + metric)
 40                for location, pipe in locations.items():
 41                    lines.append('    ' + str(location) + ': ' + str(pipe))
 42        print('\n'.join(lines))
 43
 44    icons = get_config('formatting', 'pipes', CHARSET, 'icons')
 45    styles = get_config('formatting', 'pipes', 'ansi', 'styles')
 46    if not ANSI:
 47        styles = {k: '' for k in styles}
 48    print()
 49
 50    if rich is None or get_console() is None:
 51        return plain_print_pipes()
 52
 53    def ascii_print_pipes():
 54        """Print the dictionary with no unicode allowed. Also works in case rich fails to import
 55        (though rich should auto-install when `attempt_import()` is called)."""
 56        asciitree = attempt_import('asciitree', lazy=False)
 57        if asciitree is None:
 58            return plain_print_pipes()
 59        ascii_dict, replace_dict = {}, {'connector': {}, 'metric': {}, 'location': {}}
 60        for conn_keys, metrics in pipes.items():
 61            _colored_conn_key = colored(icons['connector'] + conn_keys, style=styles['connector'])
 62            if Text is not None:
 63                replace_dict['connector'][_colored_conn_key] = (
 64                    Text(conn_keys, style=styles['connector'])
 65                )
 66            ascii_dict[_colored_conn_key] = {}
 67            for metric, locations in metrics.items():
 68                _colored_metric_key = colored(icons['metric'] + metric, style=styles['metric'])
 69                if Text is not None:
 70                    replace_dict['metric'][_colored_metric_key] = (
 71                        Text(metric, style=styles['metric'])
 72                    )
 73                ascii_dict[_colored_conn_key][_colored_metric_key] = {}
 74                for location, pipe in locations.items():
 75                    _location_style = styles[('none' if location is None else 'location')]
 76                    pipe_addendum = '\n         ' + pipe.__repr__() + '\n'
 77                    _colored_location = colored(
 78                        icons['location'] + str(location), style=_location_style
 79                    )
 80                    _colored_location_key = _colored_location + pipe_addendum
 81                    if Text is not None:
 82                        replace_dict['location'][_colored_location] = (
 83                            Text(str(location), style=_location_style)
 84                        )
 85                    ascii_dict[_colored_conn_key][_colored_metric_key][_colored_location_key] = {}
 86
 87        tree = asciitree.LeftAligned()
 88        output = ''
 89        cols = []
 90
 91        ### This is pretty terrible, unreadable code.
 92        ### Please know that I'm normally better than this.
 93        key_str = (
 94            (Text("     ") if Text is not None else "     ") +
 95            (
 96                Text("Key", style='underline') if Text is not None else
 97                colored("Key", style='underline')
 98            ) + (Text('\n\n  ') if Text is not None else '\n\n  ') +
 99            (
100                Text("Connector", style=styles['connector']) if Text is not None else
101                colored("Connector", style=styles['connector'])
102            ) + (Text('\n   +-- ') if Text is not None else '\n   +-- ') +
103            (
104                Text("Metric", style=styles['metric']) if Text is not None else
105                colored("Metric", style=styles['metric'])
106            ) + (Text('\n       +-- ') if Text is not None else '\n       +-- ') +
107            (
108                Text("Location", style=styles['location']) if Text is not None else
109                colored("Location", style=styles['location'])
110            ) + (Text('\n\n') if Text is not None else '\n\n')
111        )
112
113        output += str(key_str)
114        cols.append(key_str)
115
116        def replace_tree_text(tree_str : str) -> Text:
117            """Replace the colored words with stylized Text instead.
118            Is not executed if ANSI and UNICODE are disabled."""
119            tree_text = Text(tree_str) if Text is not None else None
120            for k, v in replace_dict.items():
121                for _colored, _text in v.items():
122                    parts = []
123                    lines = tree_text.split(_colored)
124                    for part in lines:
125                        parts += [part, _text]
126                    if lines[-1] != Text(''):
127                        parts = parts[:-1]
128                    _tree_text = Text('')
129                    for part in parts:
130                        _tree_text += part
131                    tree_text = _tree_text
132            return tree_text
133
134        tree_output = ""
135        for k, v in ascii_dict.items():
136            branch = {k : v}
137            tree_output += tree(branch) + '\n\n'
138            if not UNICODE and not ANSI:
139                _col = (Text(tree(branch)) if Text is not None else tree(branch))
140            else:
141                _col = replace_tree_text(tree(branch))
142            cols.append(_col)
143        if len(output) > 0:
144            tree_output = tree_output[:-2]
145        output += tree_output
146
147        if rich is None:
148            return print(output)
149
150        rich_columns = attempt_import('rich.columns')
151        Columns = rich_columns.Columns
152        columns = Columns(cols)
153        get_console().print(columns)
154
155    if not UNICODE:
156        return ascii_print_pipes()
157
158    rich_panel, rich_tree, rich_text, rich_columns, rich_table = attempt_import(
159        'rich.panel',
160        'rich.tree',
161        'rich.text',
162        'rich.columns',
163        'rich.table',
164    )
165    from rich import box
166    Panel = rich_panel.Panel
167    Tree = rich_tree.Tree
168    Text = rich_text.Text
169    Columns = rich_columns.Columns
170    Table = rich_table.Table
171
172    key_panel = Panel(
173        (
174            Text("\n") +
175            Text(icons['connector'] + "Connector", style=styles['connector']) + Text("\n\n") +
176            Text(icons['metric'] + "Metric", style=styles['metric']) + Text("\n\n") +
177            Text(icons['location'] + "Location", style=styles['location']) + Text("\n")
178        ),
179        title = Text(icons['key'] + "Keys", style=styles['guide']),
180        border_style = styles['guide'],
181        expand = True
182    )
183
184    cols = []
185    conn_trees = {}
186    metric_trees = {}
187    pipes = sorted_dict(pipes)
188    for conn_keys, metrics in pipes.items():
189        conn_trees[conn_keys] = Tree(
190            Text(
191                icons['connector'] + conn_keys,
192                style = styles['connector'],
193            ),
194            guide_style = styles['connector']
195        )
196        metric_trees[conn_keys] = {}
197        for metric, locations in metrics.items():
198            metric_trees[conn_keys][metric] = Tree(
199                Text(
200                    icons['metric'] + metric,
201                    style = styles['metric']
202                ),
203                guide_style = styles['metric']
204            )
205            conn_trees[conn_keys].add(metric_trees[conn_keys][metric])
206            for location, pipe in locations.items():
207                _location = (
208                    Text(str(location), style=styles['none']) if location is None
209                    else Text(location, style=styles['location'])
210                )
211                _location = (
212                    Text(icons['location'])
213                    + _location + Text('\n')
214                    + pipe_repr(pipe, as_rich_text=True) + Text('\n')
215                )
216                metric_trees[conn_keys][metric].add(_location)
217
218    cols += [key_panel]
219    for k, t in conn_trees.items():
220        cols.append(t)
221
222    columns = Columns(cols)
223    get_console().print(columns)

Print a stylized tree of a Pipes dictionary. Supports ANSI and UNICODE global settings.

def stderr_ansi() -> bool:
103def stderr_ansi() -> bool:
104    """
105    Whether text written to stderr (warnings) may carry ANSI styles: `formatting:ansi` and
106    `ansi_allowed(sys.stderr)`, so `2> errors.log` gets plain text while stdout is a terminal.
107    """
108    __getattr__('ANSI')  ### Loads `formatting:ansi` into `_attrs`.
109    return bool(_attrs['ANSI']) and ansi_allowed(sys.stderr)

Whether text written to stderr (warnings) may carry ANSI styles: formatting:ansi and ansi_allowed(sys.stderr), so 2> errors.log gets plain text while stdout is a terminal.

def translate_rich_to_termcolor(*colors) -> tuple:
171def translate_rich_to_termcolor(*colors) -> tuple:
172    """Translate between rich and more_termcolor terminology."""
173    _colors = []
174    for c in colors:
175        _c_list = []
176        ### handle 'bright'
177        c = c.replace('bright_', 'bright ')
178
179        ### handle 'on'
180        if ' on ' in c:
181            _on = c.split(' on ')
182            _colors.append(_on[0])
183            for _c in _on[1:]:
184                _c_list.append('on ' + _c)
185        else:
186            _c_list += [c]
187
188        _colors += _c_list
189
190    return tuple(_colors)

Translate between rich and more_termcolor terminology.