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}'")
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.stderrfor warnings). Defaults tosys.stdout.
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 ifstyleis provided. - style (str, default None):
If provided, pass to
richfor processing. - as_rich_text (bool, default False):
If
True, return arich.Textobject.stylemust be provided. - **kw:: Keyword arguments to pass to
rich.text.Textormore_termcolor.
Returns
- An ANSI-formatted string or a
rich.text.Textobject ifas_rich_textisTrue.
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.
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):
Truekeeps the styles whateverANSIsays (seerich_text_to_str()).
Returns
- A string with ANSI styling applied to the segments which don't yet have a style applied.
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)
'?'
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_rowsrows and append a note.Nonerenders 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.
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.
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()).
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.
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 ──────'
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.
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.
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.
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.
380def print_exception(**kw: Any) -> None: 381 """ 382 Print the exception being handled: with `rich` when it is installed, else as plain text. 383 """ 384 console = get_console() 385 if console is None: 386 import traceback 387 traceback.print_exc() 388 return 389 console.print_exception(**kw)
Print the exception being handled: with rich when it is installed, else as plain text.
503def print_options( 504 options: Optional[Iterable[Any]] = None, 505 nopretty: bool = False, 506 no_rich: bool = False, 507 name: str = 'options', 508 header: Optional[str] = None, 509 num_cols: Optional[int] = None, 510 adjust_cols: bool = True, 511 sort_options: bool = False, 512 number_options: bool = False, 513 **kw 514) -> None: 515 """ 516 Print items in an iterable as a fancy table. 517 518 Parameters 519 ---------- 520 options: Optional[Dict[str, Any]], default None 521 The iterable to be printed. 522 523 nopretty: bool, default False 524 If `True`, don't use fancy formatting. 525 526 no_rich: bool, default False 527 If `True`, don't use `rich` to format the output. 528 529 name: str, default 'options' 530 The text in the default header after `'Available'`. 531 532 header: Optional[str], default None 533 If provided, override `name` and use this as the header text. 534 535 num_cols: Optional[int], default None 536 How many columns in the table. Depends on the terminal size. If `None`, use 8. 537 538 adjust_cols: bool, default True 539 If `True`, adjust the number of columns depending on the terminal size. 540 541 sort_options: bool, default False 542 If `True`, print the options in sorted order. 543 544 number_options: bool, default False 545 If `True`, print the option's number in the list (1 index). 546 547 """ 548 from meerschaum.utils.packages import import_rich 549 from meerschaum.utils.formatting import highlight_pipes 550 from meerschaum.utils.misc import get_cols_lines, string_width 551 552 if options is None: 553 options = {} 554 _options = [] 555 for o in options: 556 _options.append(str(o)) 557 if sort_options: 558 _options = sorted(_options) 559 _header = f"\nAvailable {name}" if header is None else header 560 561 if num_cols is None: 562 num_cols = 8 563 564 def _print_options_no_rich(): 565 if not nopretty: 566 print() 567 print(make_header(_header)) 568 ### print actions 569 for i, option in enumerate(_options): 570 marker = '-' if not number_options else (str(i + 1) + '.') 571 if not nopretty: 572 print(f" {marker} ", end="") 573 print(option) 574 if not nopretty: 575 print() 576 577 ### `rich` is not a dependency of the base wheel: without it, options print as plain text. 578 rich = import_rich(lazy=False) 579 if rich is None or nopretty or no_rich or get_console() is None: 580 _print_options_no_rich() 581 return None 582 583 ### Prevent too many options from being truncated on small terminals. 584 if adjust_cols and _options: 585 _cols, _lines = get_cols_lines() 586 while num_cols > 1: 587 cell_len = int(((_cols - 4) - (3 * (num_cols - 1))) / num_cols) 588 num_too_big = sum([(1 if string_width(o) > cell_len else 0) for o in _options]) 589 if num_too_big > int(len(_options) / 3): 590 num_cols -= 1 591 continue 592 break 593 594 from meerschaum.utils.packages import attempt_import 595 rich_table = attempt_import('rich.table') 596 Text = attempt_import('rich.text').Text 597 box = attempt_import('rich.box') 598 Table = rich_table.Table 599 600 if _header is not None: 601 table = Table( 602 title=_header, 603 box=box.SIMPLE, 604 show_header=False, 605 show_footer=False, 606 title_style='', 607 expand = True, 608 ) 609 else: 610 table = Table.grid(padding=(0, 2)) 611 for i in range(num_cols): 612 table.add_column() 613 614 if len(_options) < 12: 615 ### If fewer than 12 items, use a single column 616 for i, option in enumerate(_options): 617 item = highlight_pipes(option) 618 if number_options: 619 item = str(i + 1) + '. ' + item 620 table.add_row(Text.from_ansi(item)) 621 else: 622 ### Otherwise, use multiple columns as before 623 num_rows = (len(_options) + num_cols - 1) // num_cols 624 item_ix = 0 625 for i in range(num_rows): 626 row = [] 627 for j in range(num_cols): 628 index = i + j * num_rows 629 if index < len(_options): 630 item = highlight_pipes(_options[index]) 631 if number_options: 632 item = str(i + 1) + '. ' + item 633 row.append(Text.from_ansi(item)) 634 item_ix += 1 635 else: 636 row.append('') 637 table.add_row(*row) 638 639 get_console().print(table) 640 return None
Print items in an iterable as a fancy table.
Parameters
- options (Optional[Dict[str, Any]], default None): The iterable to be printed.
- nopretty (bool, default False):
If
True, don't use fancy formatting. - no_rich (bool, default False):
If
True, don't userichto format the output. - name (str, default 'options'):
The text in the default header after
'Available'. - header (Optional[str], default None):
If provided, override
nameand use this as the header text. - num_cols (Optional[int], default None):
How many columns in the table. Depends on the terminal size. If
None, use 8. - adjust_cols (bool, default True):
If
True, adjust the number of columns depending on the terminal size. - sort_options (bool, default False):
If
True, print the options in sorted order. - number_options (bool, default False):
If
True, print the option's number in the list (1 index).
521def print_pipes_results( 522 pipes_results: Dict[mrsm.Pipe, SuccessTuple], 523 success_header: Optional[str] = 'Successes', 524 failure_header: Optional[str] = 'Failures', 525 nopretty: bool = False, 526 **kwargs: Any 527 ) -> None: 528 """ 529 Print the pipes and their result SuccessTuples. 530 531 Parameters 532 ---------- 533 pipes_results: Dict[mrsm.Pipe, SuccessTuple] 534 A dictionary mapping pipes to their resulting SuccessTuples. 535 536 success_header: Optional[str], default 'Successes' 537 The header to print above the successful pipes. 538 539 failure_header: Optional[str], default 'Fails' 540 The header to print above the failed pipes. 541 542 kwargs: Any 543 All other keyword arguments are passed to `meerschaum.utils.misc.print_options`. 544 """ 545 from meerschaum.utils.misc import print_options 546 successes = [pipe for pipe, (success, msg) in pipes_results.items() if success] 547 fails = [pipe for pipe, (success, msg) in pipes_results.items() if success] 548 success_options = [ 549 format_pipe_success_tuple(pipe, success_tuple, nopretty=nopretty) 550 for pipe, success_tuple in pipes_results.items() 551 if success_tuple[0] 552 ] 553 failure_options = [ 554 format_pipe_success_tuple(pipe, success_tuple, nopretty=nopretty) 555 for pipe, success_tuple in pipes_results.items() 556 if not success_tuple[0] 557 ] 558 559 if success_options: 560 print_options( 561 success_options, 562 header = success_header, 563 nopretty = nopretty, 564 **kwargs 565 ) 566 if failure_options: 567 print_options( 568 failure_options, 569 header = failure_header, 570 nopretty = nopretty, 571 **kwargs 572 )
Print the pipes and their result SuccessTuples.
Parameters
- pipes_results (Dict[mrsm.Pipe, SuccessTuple]): A dictionary mapping pipes to their resulting SuccessTuples.
- success_header (Optional[str], default 'Successes'): The header to print above the successful pipes.
- failure_header (Optional[str], default 'Fails'): The header to print above the failed pipes.
- kwargs (Any):
All other keyword arguments are passed to
meerschaum.utils.misc.print_options.
392def print_tuple( 393 tup: mrsm.SuccessTuple, 394 skip_common: bool = True, 395 common_only: bool = False, 396 upper_padding: int = 1, 397 lower_padding: int = 1, 398 left_padding: int = 1, 399 calm: bool = False, 400 _progress: Optional['rich.progress.Progress'] = None, 401) -> None: 402 """ 403 Format `meerschaum.utils.typing.SuccessTuple`. 404 405 Parameters 406 ---------- 407 skip_common: bool, default True 408 If `True`, do not print common success tuples (i.e. `(True, "Success")`). 409 410 common_only: bool, default False 411 If `True`, only print if the success tuple is common. 412 413 upper_padding: int, default 0 414 How many newlines to prepend to the message. 415 416 lower_padding: int, default 0 417 How many newlines to append to the message. 418 419 left_padding: int, default 1 420 How mant spaces to preprend to the message. 421 422 calm: bool, default False 423 If `True`, use the default emoji and color scheme. 424 425 """ 426 from meerschaum._internal.static import STATIC_CONFIG 427 do_print = True 428 429 omit_messages = STATIC_CONFIG['system']['success']['ignore'] 430 431 if common_only: 432 skip_common = False 433 do_print = tup[1].strip() in omit_messages 434 435 if skip_common: 436 do_print = tup[1].strip() not in omit_messages 437 438 if not do_print: 439 return 440 441 print(format_success_tuple( 442 tup, 443 upper_padding=upper_padding, 444 lower_padding=lower_padding, 445 calm=calm, 446 _progress=_progress, 447 ))
Format meerschaum.utils.typing.SuccessTuple.
Parameters
- skip_common (bool, default True):
If
True, do not print common success tuples (i.e.(True, "Success")). - common_only (bool, default False):
If
True, only print if the success tuple is common. - 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.
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.
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.