Source code for pawnlib.output.color_print

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import os
import json
import getpass
import traceback
import inspect
from contextlib import contextmanager, AbstractContextManager

from pawnlib.typing import (
    converter, date_utils, list_to_oneline_string, const, is_include_list, remove_tags,
    remove_ascii_color_codes, timestamp_to_string, is_hex, is_json
)
from pawnlib.models.response import json_default_serializer
from pawnlib.config import pawnlib_config as pawn, global_verbose
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import Terminal256Formatter
from dataclasses import is_dataclass, asdict
from rich.syntax import Syntax
from rich.table import Table
from rich.pretty import Pretty
from rich.progress import Progress, SpinnerColumn, TimeElapsedColumn
from rich.panel import Panel
from rich.console import Group, Console
from rich.tree import Tree
from rich.text import Text
from rich import print as rprint
from typing import Union, Callable
from datetime import datetime, timedelta, date
from uuid import UUID
import textwrap
from requests.structures import CaseInsensitiveDict

if sys.version_info >= (3, 8):
    from typing import Literal
else:
    from typing_extensions import Literal  # pragma: no cover

AlignMethod = Literal["left", "center", "right"]
VerticalAlignMethod = Literal["top", "middle", "bottom"]

TRANSFORM_DICT = {
    "time_stamp":         lambda x: f"{timestamp_to_string(int(x))}",
    "timestamp":          lambda x: f"{timestamp_to_string(int(x))}",
    "expireBlockHeight":  lambda x: f"{int(x,16):,}",
}
IGNORE_KEYS = ["Hash"]

_ATTRIBUTES = dict(
    list(zip([
        'bold',
        'dark',
        '',
        'underline',
        'blink',
        '',
        'reverse',
        'concealed'
    ],
        list(range(1, 9))
    ))
)
del _ATTRIBUTES['']

_HIGHLIGHTS = dict(
    list(zip([
        'on_grey',
        'on_red',
        'on_green',
        'on_yellow',
        'on_blue',
        'on_magenta',
        'on_cyan',
        'on_white'
    ],
        list(range(40, 48))
    ))
)

_COLORS = dict(
    list(zip([
        'grey',
        'red',
        'green',
        'yellow',
        'blue',
        'magenta',
        'cyan',
        'white',
    ],
        list(range(30, 38))
    ))
)

_RESET = '\033[0m'


[docs]def colored(text, color=None, on_color=None, attrs=None): """Colorize text. Available text colors: red, green, yellow, blue, magenta, cyan, white. Available text highlights: on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white. Available _ATTRIBUTES: bold, dark, underline, blink, reverse, concealed. Example: colored('Hello, World!', 'red', 'on_grey', ['blue', 'blink']) colored('Hello, World!', 'green') """ if os.getenv('ANSI_COLORS_DISABLED') is None: fmt_str = '\033[%dm%s' if color is not None: text = fmt_str % (_COLORS[color], text) if on_color is not None: text = fmt_str % (_HIGHLIGHTS[on_color], text) if attrs is not None: for attr in attrs: if attr is not None: text = fmt_str % (_ATTRIBUTES[attr], text) text += _RESET return text
[docs]def cprint(text, color=None, on_color=None, attrs=None, **kwargs): """Print colorize text. It accepts arguments of print function. :param text: :param color: :param on_color: :param attrs: :param kwargs: :return: Example: .. code-block:: python cprint("message", "red") # >> message """ print((colored(text, color, on_color, attrs)), **kwargs)
[docs]class bcolors: HEADER = '\033[95m' OKBLUE = '\033[94m' OKGREEN = '\033[92m' GREEN = '\033[32;40m' CYAN = '\033[96m' WARNING = '\033[93m' FAIL = '\033[91m' RESET = '\033[0m' ENDC = '\033[0m' BOLD = '\033[1m' ITALIC = '\033[1;3m' UNDERLINE = '\033[4m' WHITE = '\033[97m' DARK_GREY = '\033[38;5;243m' LIGHT_GREY = '\033[37m'
[docs]class PrintRichTable: """ Print a table using a rich.table module. :param title: Title of table :param data: Data of table :param columns: Columns of table. Print only column parameter values. :param with_idx: Print the row count. :param call_value_func: The row value must be a string. If you want to perform other tasks, please add the function name. :param call_desc_func: Invoke a function that describes the value. :param display_output: Determines whether to print output (default: True) :param justify: Alignment value of the console. (default: left) Example: .. code-block:: python from pawnlib.output.color_print import PrintRichTable data = [ { "address": "1x038bd14d5ce28a4ac713c21e89f0e6ca5f107f08", "value": 399999999999999966445568, }, { "address": "2x038bd14d5ce28a4ac713c21e89f0e6ca5f107f08", "value": 399999999999999966445568, }, { "address": "3x038bd14d5ce28a4ac713c21e89f0e6ca5f107f08", "value": 399999999999999966445568, } ] PrintRichTable(title="RichTable", data=data) # RichTable # ┏━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓ # ┃ idx ┃ address ┃ value ┃ # ┡━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩ # │ 0 │ 1x038bd14d5ce28a4ac713c21e89f0e6ca5f107f08 │ 399999999999999966445568 │ # ├─────┼────────────────────────────────────────────┼──────────────────────────┤ # │ 1 │ 2x038bd14d5ce28a4ac713c21e89f0e6ca5f107f08 │ 399999999999999966445568 │ # ├─────┼────────────────────────────────────────────┼──────────────────────────┤ # │ 2 │ 3x038bd14d5ce28a4ac713c21e89f0e6ca5f107f08 │ 399999999999999966445568 │ # └─────┴────────────────────────────────────────────┴──────────────────────────┘ # # PrintRichTable(title="RichTable", data=data, columns=["address"]) # # RichTable # ┏━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ # ┃ idx ┃ address ┃ # ┡━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ # │ 0 │ 1x038bd14d5ce28a4ac713c21e89f0e6ca5f107f08 │ # ├─────┼────────────────────────────────────────────┤ # │ 1 │ 2x038bd14d5ce28a4ac713c21e89f0e6ca5f107f08 │ # ├─────┼────────────────────────────────────────────┤ # │ 2 │ 3x038bd14d5ce28a4ac713c21e89f0e6ca5f107f08 │ # └─────┴────────────────────────────────────────────┘ """ def __init__( self, title: str = "", data: Union[dict, list] = None, columns: list = None, remove_columns: list = None, with_idx: bool = True, call_value_func=str, call_desc_func=None, columns_options=None, display_output=True, no_wrap: bool = False, overflow="fold", justify="left", **kwargs ) -> None: if columns is None: columns = list() if data is None: data = dict() self.title = f"[bold dark_orange3] {title}" self.table_options = kwargs # self.table = Table(title=self.title, **kwargs) self.table = None self.data = data self.table_data = [] self.columns = columns self.remove_columns = remove_columns self.rows = [] self.row_count = 0 self.with_idx = with_idx self.call_value_func = call_value_func self.call_desc_func = call_desc_func self.display_output = display_output self.console_justify = justify self.overflow = overflow self.no_wrap = no_wrap _default_columns_option = dict( key=dict( justify="left", overflow=self.overflow, no_wrap=self.no_wrap ), value=dict( justify="right", overflow=self.overflow, no_wrap=self.no_wrap ), description=dict( justify="right", overflow=self.overflow, no_wrap=self.no_wrap ), ) self.columns_options = _default_columns_option if columns_options: self.columns_options.update(columns_options) self._check_columns_options() self._initialize_table() self._set_table_data() self._print_table() def _check_columns_options(self): allowed_columns = ["header_style", "footer_style", "style", "justify", "vertical", "overflow", "width", "min_width", "max_width", "ratio", "no_wrap"] for column_name, column_values in self.columns_options.items(): if isinstance(column_values, dict): for column_key, value in column_values.items(): if column_key not in allowed_columns: raise ValueError(f"name='{column_name}', key='{column_key}' is not allowed column option, allowed: {allowed_columns}") def _initialize_table(self): if isinstance(self.data, dict): self.table_data = self.data if not self.table_options.get('show_header'): self.table_options['show_header'] = False if self.table_options.get('show_lines', "NOT_DEFINED") == "NOT_DEFINED": self.table_options['show_lines'] = False elif isinstance(self.data, list): self.table_data = self.data else: self.table_data = [] self.table = Table(title=self.title, **self.table_options) # def _specify_columns def _is_showing_columns(self, item): if len(self.columns) == 0: return True if item in self.columns: return True return False def _draw_vertical_table(self): pawn.console.debug("Drawing vertical table") if self.with_idx: self.table.add_column("idx", **self.columns_options.get('idx', {})) self.table.add_column("key", **self.columns_options.get('key', {})) self.table.add_column("value", **self.columns_options.get('value', {})) if self.call_desc_func and callable(self.call_desc_func): self.table.add_column("description", **self.columns_options.get('description', {})) _count = 0 row_dict = {} for item, value in self.table_data.items(): if self._is_showing_columns(item): row_dict[item] = value if callable(self.call_value_func): value = self.call_value_func(value) columns = [f"{item}", f"{value}"] if self.with_idx: columns.insert(0, f"{_count}") if self.call_desc_func and callable(self.call_desc_func): columns.append(self.call_desc_func(*columns, **row_dict)) self.table.add_row(*columns) _count += 1 def _draw_horizontal_table(self): pawn.console.debug("Drawing horizontal table") for item in self.table_data: if isinstance(item, dict): line_row = [] row_dict = {} for column in self.columns: if self.with_idx and column == "idx": value = str(self.row_count) elif column == "desc": value = self.call_desc_func(*line_row, **row_dict) else: try: value = self.call_value_func(item.get(column), **{column: item.get(column)}) except Exception: value = self.call_value_func(item.get(column)) row_dict[column] = value line_row.append(value) self.rows.append(line_row) else: self.rows.append([f"{self.row_count}", f"{item}"]) self.row_count += 1 # for col in self.columns: # self.table.add_column(col, **self.columns_options.get(col, {"no_wrap": self.no_wrap})) for col in self.columns: column_options = self.columns_options.get(col, {}) no_wrap = column_options.get('no_wrap', self.no_wrap) overflow = column_options.get('overflow', self.overflow) self.table.add_column( col, **column_options, no_wrap=no_wrap, overflow=overflow ) def _extract_columns(self): # if self.table_data and len(self.columns) == 0 and isinstance(self.table_data[0], dict): if self.table_data and len(self.columns) == 0: try: self.columns = list(self.table_data[0].keys()) except Exception as e: self.columns = ["value"] if self.with_idx: self.columns.insert(0, "idx") if callable(self.call_desc_func): self.columns.append("desc") if self.columns and isinstance(self.remove_columns, list): for r_column in self.remove_columns: self.columns.remove(r_column) def _set_table_data(self): if isinstance(self.table_data, list): self._extract_columns() self._draw_horizontal_table() elif isinstance(self.table_data, dict): self._draw_vertical_table() def _print_table(self): for row in self.rows: self.table.add_row(*row) if self.display_output: if self.table.columns: pawn.console.print(self.table, justify=self.console_justify) else: pawn.console.print(f"{self.title} \n [i]No data ... [/i]") else: return self.table
[docs]class TablePrinter(object): "Print a list of dicts as a table"
[docs] def __init__(self, fmt=[], sep=' ', ul="-"): """ :param fmt: list of tuple(heading, key, width) heading: str, column label \n key: dictionary key to value to print \n width: int, column width in chars \n :param sep: string, separation between columns :param ul: string, character to underline column label, or None for no underlining Example: .. code-block:: python from pawnlib import output data = [ { "address": "1x038bd14d5ce28a4ac713c21e89f0e6ca5f107f08", "value": 399999999999999966445568, }, { "address": "2x038bd14d5ce28a4ac713c21e89f0e6ca5f107f08", "value": 399999999999999966445568, }, { "address": "3x038bd14d5ce28a4ac713c21e89f0e6ca5f107f08", "value": 399999999999999966445568, }, ] fmt = [ ('address', 'address', 10), ('value', 'value', 15) ] cprint("Print Table", "white") print(output.TablePrinter(fmt=fmt)(data)) print(output.TablePrinter()(data)) address value ---------- --------------- 1x038bd14d 399999999999999 2x038bd14d 399999999999999 3x038bd14d 399999999999999 """ super(TablePrinter, self).__init__() self._params = {"sep": sep, "ul": ul} self.fmt = str(sep).join('{lb}{0}:{1}{rb}'.format(key, width, lb='{', rb='}') for heading, key, width in fmt) self.head = {key: heading for heading, key, width in fmt} self.ul = {key: str(ul) * width for heading, key, width in fmt} if ul else None self.width = {key: width for heading, key, width in fmt} self.data_column = []
[docs] def row(self, data, head=False): if head: return self.fmt.format(**{k: get_bcolors(data.get(k), "WHITE", bold=True, width=w) for k, w in self.width.items()}) else: return self.fmt.format(**{k: str(data.get(k, ''))[:w] for k, w in self.width.items()})
[docs] def get_unique_columns(self): self.data_column = [] for item in self.data: self.data_column = self.data_column + list(item.keys()) self.data_column = list(set(self.data_column)) self.data_column.sort()
def __call__(self, data_list): if len(self.fmt) == 0: sep = self._params.get("sep") ul = self._params.get("ul") self.data = data_list self.get_unique_columns() fmt = self.data_column width = 12 self.fmt = str(sep).join('{lb}{0}:{1}{rb}'.format(key, width, lb='{', rb='}') for key in fmt) self.width = {key: width for key in fmt} self.ul = {key: str(ul) * width for key in fmt} if ul else None self.head = {key: key for key in fmt} _r = self.row res = [_r(data) for data in data_list] res.insert(0, _r(self.head, head=True)) if self.ul: res.insert(1, _r(self.ul)) return '\n'.join(res)
[docs]def get_bcolors(text, color, bold=False, underline=False, width=None): """ Returns the color from the bcolors object. :param text: :param color: :param bold: :param underline: :param width: :return: """ if width and len(text) <= width: text = text.center(width, ' ') return_text = f"{getattr(bcolors, color)}{text}{bcolors.ENDC}" if bold: return_text = f"{bcolors.BOLD}{return_text}" if underline: return_text = f"{bcolors.UNDERLINE}{return_text}" return str(return_text)
[docs]def colored_input(message, password=False, color="WHITE"): input_message = get_bcolors(text=message, color=color, bold=True, underline=True) + " " if password: return getpass.getpass(input_message) return input(input_message)
[docs]def get_colorful_object(v): if type(v) == bool: value = f"{bcolors.ITALIC}" if v is True: value += f"{bcolors.GREEN}" else: value += f"{bcolors.FAIL}" value += f"{str(v)}{bcolors.ENDC}" elif type(v) == int or type(v) == float: value = f"{bcolors.CYAN}{str(v)}{bcolors.ENDC}" elif type(v) == str: value = f"{bcolors.WARNING}'{str(v)}'{bcolors.ENDC}" elif v is None: value = f"{bcolors.FAIL}{str(v)}{bcolors.ENDC}" else: value = f"{v}" return value
def _format_number_for_dump(n): """ Converts floating-point to int if the fractional part is zero, otherwise returns the float as is. """ return n if (n % 1) else int(n) def _process_dump_value(value, key=None, debug=True, hex_to_int=False) -> str: """ 1) If 'key' is in TRANSFORM_DICT, apply the corresponding transformation. 2) If 'hex_to_int' is True and 'value' is a valid hex, apply TINT logic. 3) If 'debug' is True, append type and length info. Returns a string representation of the final result. """ v_str = get_colorful_object(value) if key in TRANSFORM_DICT: try: transformed_val = TRANSFORM_DICT[key](value) v_str = f"{v_str} {bcolors.LIGHT_GREY}(from {key} {transformed_val}{bcolors.ENDC})" except Exception: # In case of transform failure, keep the original pass converted_hex = "" if debug and hex_to_int and converter.is_hex(value): if not is_include_list(key, IGNORE_KEYS, ignore_case=False): try: if key == "timestamp": timestamp_int = int(value, 16) / 1_000_000 if timestamp_int > 10**10: timestamp_int //= 1000 # ms to sec dt_str = datetime.fromtimestamp(_format_number_for_dump(timestamp_int)).strftime('%Y-%m-%d %H:%M:%S') converted_hex = f"{dt_str} (from {key})" elif isinstance(value, str) and len(value) >= 60: pass else: if len(value) < 14: tint_val = 1 tint_str = "" else: tint_val = const.TINT tint_str = f"(from TINT) {key if key else ''}" converted_float = _format_number_for_dump(round(int(value, 16) / tint_val, 4)) converted_hex = f"{converted_float:,} {tint_str}" except Exception as e: pawn.console.print(f"[red]\\[WARN] {e}") if converted_hex: v_str += f" {bcolors.ITALIC}{bcolors.LIGHT_GREY}{converted_hex}{bcolors.ENDC}" if debug: v_str += f" {bcolors.HEADER}{str(type(value)):>20}{bcolors.ENDC}{bcolors.DARK_GREY} len={len(str(value))}{bcolors.ENDC}" return v_str
[docs]def dump(obj, nested_level=0, output=sys.stdout, hex_to_int=False, debug=True, _is_list=False, _last_key=None, is_compact=False): """ Prints a variable (obj) for debugging in a structured way. - dict: recursively prints nested key/value pairs - list: recursively prints elements - scalar: prints with optional transforms (transform_dict, hex->int, etc.) """ spacing = ' ' def_spacing = ' ' if isinstance(obj, dict): if nested_level == 0 or _is_list: print(f"{def_spacing + nested_level * spacing}{{", file=output) else: print("{", file=output) for k, v in obj.items(): if hasattr(v, '__iter__') and not isinstance(v, str): print( bcolors.OKGREEN + f"{def_spacing + (nested_level + 1) * spacing}{k}: " + bcolors.ENDC, end="", file=output) dump(v, nested_level + 1, output, hex_to_int, debug, _last_key=k, is_compact=is_compact) else: v_str = _process_dump_value(value=v, key=k, debug=debug, hex_to_int=hex_to_int) print( bcolors.OKGREEN+ f"{def_spacing + (nested_level + 1) * spacing}{k}:" + bcolors.WARNING + f" {v_str} "+ bcolors.ENDC,file=output) print(f"{def_spacing + nested_level * spacing}}}", file=output) elif isinstance(obj, list): end_char = ' ' if is_compact else '\n' print(f"{def_spacing + nested_level * spacing}[", file=output, end=end_char) for v in obj: if hasattr(v, '__iter__') and not isinstance(v, str): dump(v, nested_level + 1, output, hex_to_int, debug, _is_list=True,is_compact=is_compact) else: if is_compact: local_spacing = "" local_def_spacing = "" local_end = ', ' else: local_spacing = spacing local_def_spacing = def_spacing local_end = '\n' v_str = _process_dump_value(value=v, key=None, debug=debug, hex_to_int=hex_to_int) print(bcolors.WARNING + f"{local_def_spacing + (nested_level + 1) * local_spacing}{v_str}" + bcolors.ENDC, file=output,end=local_end) print(f"{def_spacing + nested_level * spacing}]", file=output) else: v_str = _process_dump_value(value=obj, key=_last_key, debug=debug, hex_to_int=hex_to_int) print(bcolors.WARNING + f"{def_spacing + nested_level * spacing}{v_str}"+ bcolors.ENDC, file=output)
[docs]def debug_print(text, color="green", on_color=None, attrs=None, view_time=True, **kwargs): """Print colorize text. It accepts arguments of print function. """ module_name = '' stack = inspect.stack() parent_frame = stack[1][0] module = inspect.getmodule(parent_frame) if module: module_pieces = module.__name__.split('.') module_name = list_to_oneline_string(module_pieces) function_name = stack[1][3] full_module_name = f"{module_name}.{function_name}({stack[1].lineno})" module_text = "" time_text = "" try: module_text = get_bcolors(f"[{full_module_name:<25}]", "WARNING") except: pass if view_time: time_text = "[" + get_bcolors(f"{date_utils.todaydate('log')}", "WHITE") + "]" main_text = (colored(str(text), color, on_color, attrs)) print(f"{time_text}{module_text} {main_text}", **kwargs)
[docs]def classdump(obj): """ For debugging, print the properties of the class. """ for attr in dir(obj): if not hasattr(obj, attr): continue try: value = getattr(obj, attr) print(bcolors.OKGREEN + f"obj.{attr} = " + bcolors.WARNING + f"{repr(value)}" + bcolors.ENDC) except Exception as e: print(bcolors.FAIL + f"obj.{attr}"+ bcolors.ENDC +f" = <ERROR: {e}>")
[docs]def kvPrint(key, value): """ print the {key: value} format. :param key: :param value: :return: """ key_width = 9 key_value = 3 print(bcolors.OKGREEN + "{:>{key_width}} : ".format(key, key_width=key_width) + bcolors.ENDC, end="") print(bcolors.WARNING + "{:>{key_value}} ".format(str(value), key_value=key_value) + bcolors.ENDC)
[docs]def pretty_json(obj, syntax=True, rich_syntax=False, style="one-dark", line_indent="", **kwargs): """ Return a prettified JSON string with optional syntax highlighting. :param obj: JSON object to prettify :param syntax: If True, apply syntax highlighting (default: True) :param rich_syntax: If True, use rich library for syntax highlighting (default: False) :param style: Style name for syntax highlighting (default: "one-dark") :param line_indent: Custom line indentation (default: "") :param kwargs: Additional keyword arguments for json.dumps Example: .. code-block:: python data = {"name": "John", "age": 30, "city": "New York"} print(pretty_json(data)) # { # "name": "John", # "age": 30, # "city": "New York" # } print(pretty_json(data, syntax=False)) # {"name": "John", "age": 30, "city": "New York"} """ if syntax and isinstance(kwargs, dict): kwargs.setdefault("indent", 4) line_indent = " " * 4 if not line_indent else line_indent kwargs.setdefault("default", json_default_serializer) def json_to_string(_obj): if isinstance(_obj, str): _obj = json.loads(_obj) if isinstance(_obj, (dict, list)): return json.dumps(_obj, **kwargs) else: return _obj json_string = json_to_string(obj).strip() if syntax: if rich_syntax: return Syntax(json_string, "json", line_numbers=False, background_color="rgb(40,40,40)", theme=style) else: return syntax_highlight(json_string, name="json", line_indent=line_indent, style=style).strip() else: return json_string
[docs]def debug_logging(message, dump_message=None): """ print debug_logging :param message: :param dump_message: :return: Example: .. code-block:: python from pawnlib import output output.debug_logging("message") [2022-07-25 16:35:15.105][DBG][/Users/jinwoo/work/python_prj/pawnlib/examples/asyncio/./run_async.py main(33)] : message """ stack = traceback.extract_stack() filename, code_line, func_name, text = stack[-2] def_msg = f"[{date_utils.todaydate('log')}][DBG][{filename} {func_name}({code_line})]" kvPrint(def_msg, message) if dump_message: dump(dump_message)
def _patched_make_iterencode(markers, _default, _encoder, _indent, _floatstr, _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, ## HACK: hand-optimized bytecode; turn globals into locals ValueError=ValueError, dict=dict, float=float, id=id, int=int, isinstance=isinstance, list=list, str=str, tuple=tuple, _intstr=int.__repr__, ): if _indent is not None and not isinstance(_indent, str): _indent = ' ' * _indent def _iterencode_list(lst, _current_indent_level): if not lst: yield '[]' return if markers is not None: markerid = id(lst) if markerid in markers: raise ValueError("Circular reference detected") markers[markerid] = lst buf = '[' if _indent is not None: _current_indent_level += 1 newline_indent = '\n' + _indent * _current_indent_level # separator = _item_separator + newline_indent separator = ", " buf += newline_indent else: newline_indent = None separator = _item_separator first = True for value in lst: if first: first = False else: buf = separator if isinstance(value, str): yield buf + _encoder(value) elif value is None: yield buf + 'null' elif value is True: yield buf + 'true' elif value is False: yield buf + 'false' elif isinstance(value, int): # Subclasses of int/float may override __repr__, but we still # want to encode them as integers/floats in JSON. One example # within the standard library is IntEnum. yield buf + _intstr(value) elif isinstance(value, float): # see comment above for int yield buf + _floatstr(value) else: yield buf if isinstance(value, (list, tuple)): chunks = _iterencode_list(value, _current_indent_level) elif isinstance(value, dict): chunks = _iterencode_dict(value, _current_indent_level) else: chunks = _iterencode(value, _current_indent_level) yield from chunks if newline_indent is not None: _current_indent_level -= 1 yield '\n' + _indent * _current_indent_level yield ']' if markers is not None: del markers[markerid] def _iterencode_dict(dct, _current_indent_level): if not dct: yield '{}' return if markers is not None: markerid = id(dct) if markerid in markers: raise ValueError("Circular reference detected") markers[markerid] = dct yield '{' if _indent is not None: _current_indent_level += 1 newline_indent = '\n' + _indent * _current_indent_level item_separator = _item_separator + newline_indent yield newline_indent else: newline_indent = None item_separator = _item_separator first = True if _sort_keys: items = sorted(dct.items()) else: items = dct.items() for key, value in items: if isinstance(key, str): pass # JavaScript is weakly typed for these, so it makes sense to # also allow them. Many encoders seem to do something like this. elif isinstance(key, float): # see comment for int/float in _make_iterencode key = _floatstr(key) elif key is True: key = 'true' elif key is False: key = 'false' elif key is None: key = 'null' elif isinstance(key, int): # see comment for int/float in _make_iterencode key = _intstr(key) elif _skipkeys: continue else: raise TypeError(f'keys must be str, int, float, bool or None, ' f'not {key.__class__.__name__}') if first: first = False else: yield item_separator yield _encoder(key) yield _key_separator if isinstance(value, str): yield _encoder(value) elif value is None: yield 'null' elif value is True: yield 'true' elif value is False: yield 'false' elif isinstance(value, int): # see comment for int/float in _make_iterencode yield _intstr(value) elif isinstance(value, float): # see comment for int/float in _make_iterencode yield _floatstr(value) else: if isinstance(value, (list, tuple)): chunks = _iterencode_list(value, _current_indent_level) elif isinstance(value, dict): chunks = _iterencode_dict(value, _current_indent_level) else: chunks = _iterencode(value, _current_indent_level) yield from chunks if newline_indent is not None: _current_indent_level -= 1 yield '\n' + _indent * _current_indent_level yield '}' if markers is not None: del markers[markerid] def _iterencode(o, _current_indent_level): if isinstance(o, str): yield _encoder(o) elif o is None: yield 'null' elif o is True: yield 'true' elif o is False: yield 'false' elif isinstance(o, int): # see comment for int/float in _make_iterencode yield _intstr(o) elif isinstance(o, float): # see comment for int/float in _make_iterencode yield _floatstr(o) elif isinstance(o, (list, tuple)): yield from _iterencode_list(o, _current_indent_level) elif isinstance(o, dict): yield from _iterencode_dict(o, _current_indent_level) else: if markers is not None: markerid = id(o) if markerid in markers: raise ValueError("Circular reference detected") markers[markerid] = o o = _default(o) yield from _iterencode(o, _current_indent_level) if markers is not None: del markers[markerid] return _iterencode
[docs]def json_compact_dumps(data, indent=4, monkey_patch=True): if monkey_patch: json.encoder._make_iterencode = _patched_make_iterencode return json.dumps(data, indent=indent)
[docs]class NoIndent(object): """ Value wrapper. """ def __init__(self, value): self.value = value
[docs]class NoListIndentEncoder(json.JSONEncoder): def __init__(self, *args, **kwargs): # Save copy of any keyword argument values needed for use here. super(NoListIndentEncoder, self).__init__(*args, **kwargs)
[docs] def iterencode(self, o, _one_shot=False): list_lvl = 0 for s in super(NoListIndentEncoder, self).iterencode(o, _one_shot=_one_shot): if s.startswith('['): list_lvl += 1 s = s.replace('\n', '').rstrip() s = s.replace(' ', '') elif 0 < list_lvl: s = s.replace('\n', '').rstrip() s = s.replace(' ', '') if s and s[-1] == ',': s = s[:-1] + self.item_separator elif s and s[-1] == ':': s = s[:-1] + self.key_separator if s.endswith(']'): list_lvl -= 1 s = s.replace(",", ", ") yield s
[docs]class ProgressTime(Progress): def __init__(self, **kwargs): if kwargs.get('transient', "__NOT_DEFINED__") == "__NOT_DEFINED__": kwargs['transient'] = True super().__init__( SpinnerColumn(), *Progress.get_default_columns(), TimeElapsedColumn(), console=pawn.console, **kwargs )
# def syntax_highlight(data, name="json", indent=4, style="material", oneline_list=True, line_indent='', rich=False, word_wrap=True, **kwargs): # """ # Syntax highlighting function # :param data: The data to be highlighted. # :param name: The name of the lexer to use for highlighting. # :param indent: The number of spaces to use for indentation. # :param style: The style to use for highlighting. # :param oneline_list: Whether to compact lists into one line. # :param line_indent: The string to use for line indentation. # :param rich: Whether to use rich text formatting. # :param word_wrap: Whether to enable word wrapping. # :return: The highlighted code as a string. # Example: # .. code-block:: python # from pawnlib import output # print(output.syntax_highlight("<html><head><meta name='viewport' content='width'>", "html", style=style)) # """ # # styles available as of pygments 2.8.1. # # ['default', 'emacs', 'friendly', 'colorful', 'autumn', 'murphy', 'manni', # # 'material', 'monokai', 'perldoc', 'pastie', 'borland', 'trac', 'native', # # 'fruity', 'bw', 'vim', 'vs', 'tango', 'rrt', 'xcode', 'igor', 'paraiso-light', # # 'paraiso-dark', 'lovelace', 'algol', 'algol_nu', 'arduino', 'rainbow_dash', # # 'abap', 'solarized-dark', 'solarized-light', 'sas', 'stata', 'stata-light', # # 'stata-dark', 'inkpot', 'zenburn'] # print(data) # if name == "json" and isinstance(data, (dict, list)): # data = data_clean(data) # code_data = json_compact_dumps(data, indent=indent, monkey_patch=oneline_list) # elif data: # code_data = data # else: # code_data = "" # if line_indent: # code_data = textwrap.indent(code_data, line_indent) # if rich: # return Syntax(code_data, name, theme=style, word_wrap=word_wrap, **kwargs) # else: # return highlight( # code=code_data, # lexer=get_lexer_by_name(name), # formatter=Terminal256Formatter(style=style))
[docs]def syntax_highlight(data, name="json", indent=4, style="material", oneline_list=True, line_indent='', rich=False, word_wrap=True, format_config=None, **kwargs): """ Syntax highlighting function with support for class instance representation instead of serialization. :param data: The data to be highlighted. :param name: The name of the lexer to use for highlighting. :param indent: The number of spaces to use for indentation. :param style: The style to use for highlighting. :param oneline_list: Whether to compact lists into one line. :param line_indent: The string to use for line indentation. :param rich: Whether to use rich text formatting. :param word_wrap: Whether to enable word wrapping. :param format_config: Configuration for formatting. :return: The highlighted code as a string. """ def convert_non_serializable(obj, format_config={}, seen=None) : """ Convert non-serializable objects to a debug-friendly string representation. :param obj: The object to convert. :param format_config: Configuration for formatting. :param seen: Set of object IDs to track recursion (default: None). :return: A string representation of the object. """ if seen is None: seen = set() obj_id = id(obj) if obj_id in seen: return f"<{obj.__class__.__name__} (recursive reference)>" seen.add(obj_id) format_config = format_config or {} try: if isinstance(obj, (datetime, date)): fmt = format_config.get("datetime") if callable(fmt): return fmt(obj) elif isinstance(fmt, str): return obj.strftime(fmt) return str(obj) elif isinstance(obj, timedelta): return str(obj) elif isinstance(obj, UUID): return str(obj) elif isinstance(obj, bytes): return obj.decode("utf-8", errors="replace") elif hasattr(obj, '__dict__'): if obj.__class__.__module__ != 'builtins': attrs = {k: convert_non_serializable(v, format_config=format_config) if isinstance(v, (dict, list, object)) else v for k, v in vars(obj).items()} return f"<{obj.__class__.__name__}> {attrs}" return repr(obj) else: return repr(obj) except Exception as e: print(f"Failed to convert {type(obj).__name__}: {e}") return repr(obj) finally: seen.discard(obj_id) if name == "json" and isinstance(data, (dict, list)): try: code_data = json.dumps(data, ensure_ascii=False, indent=indent, default=lambda o: convert_non_serializable(o, format_config=format_config)) except TypeError as e: print(f"Serialization error: {e}") code_data = json.dumps(data, ensure_ascii=False, indent=indent, default=lambda o: convert_non_serializable(o, format_config=format_config)) elif data: code_data = data else: code_data = "" if line_indent: code_data = textwrap.indent(code_data, line_indent) # Use Rich's syntax highlighting or fallback to terminal highlighting if rich: return Syntax(code_data, name, theme=style, word_wrap=word_wrap, **kwargs) # syntax = Syntax(code_data, name, theme=style, word_wrap=word_wrap, **kwargs) # Convert rich Syntax to plain text for consistent rendering in Panel # with pawn.console.capture() as capture: # pawn.console.print(syntax) # return capture.get() else: return highlight( code=code_data, lexer=get_lexer_by_name(name), formatter=Terminal256Formatter(style=style) )
[docs]def get_debug_here_info(): """ Get debug information from the previous frame. This function uses the inspect module to get information about the previous frame, including the filename, line number, function name, lines of context, and index. Returns: dict: A dictionary containing the debug information. """ filename, line_number, function_name, ln, index = inspect.getframeinfo(inspect.currentframe().f_back.f_back) return { "filename": filename, "line_number": line_number, "function_name": function_name, "ln": ln, "index": index }
[docs]def get_variable_name_list(var=None): """ Retrieve the name of var. :param var: variable to get name from :return: name of var Example: .. code-block:: python a = 5 get_variable_name_list(a) # >> ['a'] """ callers_local_vars = inspect.currentframe().f_back.f_locals.items() return [var_name for var_name, var_val in callers_local_vars if var_val is var]
[docs]def get_variable_name(var=None): """ Retrieve the name of the variable. :param var: variable to get the name from, defaults to None :type var: Any, optional :return: name of the variable :rtype: str Example: .. code-block:: python a = 10 get_variable_name(a) # >> 'a' """ stacks = inspect.stack() try: func = stacks[0].function code = stacks[1].code_context[0] s = code.index(func) s = code.index("(", s + len(func)) + 1 e = code.index(")", s) return code[s:e].strip() except: return ""
[docs]def dict_clean(data): """ Clean the dictionary data. This function iterates over the items in the dictionary. If the value is an instance of CaseInsensitiveDict, it converts it to a regular dictionary. If the value is None, it converts it to an empty string. :param data: The dictionary to clean. :type data: dict :return: The cleaned dictionary. :rtype: dict Example: .. code-block:: python data = {"key1": "value1", "key2": None, "key3": CaseInsensitiveDict({"subkey": "subvalue"})} dict_clean(data) # >> {"key1": "value1", "key2": "", "key3": {"subkey": "subvalue"}} """ result = {} for key, value in data.items(): if isinstance(value, CaseInsensitiveDict): value = dict(value) elif value is None: value = '' result[key] = value return result
[docs]def list_clean(data): """ Clean the list data. This function iterates over the items in the list. If the value is None, it converts it to an empty string. :param data: The list to clean. :type data: list :return: The cleaned list. :rtype: list Example: .. code-block:: python data = [None, 'hello', None, 'world'] list_clean(data) # >> ['', 'hello', '', 'world'] """ result = [] for value in data: if value is None: value = '' result.append(value) return result
[docs]def data_clean(data): """ Clean the data. This function checks the type of the data. If the data is a dictionary, it cleans it using the dict_clean function. If the data is a list, it cleans it using the list_clean function. :param data: The data to clean. :type data: Any :return: The cleaned data. :rtype: Any Example: .. code-block:: python data = {'name': ' John ', 'age': ' 25 '} clean_data = data_clean(data) # >> {'name': 'John', 'age': '25'} data = [' John ', ' 25 '] clean_data = data_clean(data) # >> ['John', '25'] """ if isinstance(data, dict): return dict(dict_clean(data)) elif isinstance(data, list): return list(list_clean(data)) return data
[docs]def count_nested_dict_len(d): """ Count the total number of keys in a nested dictionary. :param d: Dictionary to count keys in. :type d: dict :return: Total number of keys in the dictionary, including nested dictionaries. :rtype: int Example: .. code-block:: python nested_dict = { 'a': 1, 'b': {'c': 2, 'd': {'e': 3}}, 'f': {'g': 4} } count_nested_dict_len(nested_dict) # >> 6 count_nested_dict_len({'x': {'y': {'z': {}}}}) # >> 3 count_nested_dict_len({}) # >> 0 """ length = len(d) for key, value in d.items(): if isinstance(value, dict): length += count_nested_dict_len(value) return length
[docs]def get_var_name(var): """ Get the variable name from the call frame. This function uses the inspect and ast modules to get the variable name from the call frame. :param var: The variable whose name is to be retrieved. :return: The name of the variable as a string. Example: .. code-block:: python my_var = 10 get_var_name(my_var) # >> 'my_var' class MyClass: def __init__(self): self.attr = 5 instance = MyClass() get_var_name(instance.attr) # >> 'instance.attr' """ import ast try: frame = inspect.currentframe().f_back.f_back code_context = inspect.getframeinfo(frame).code_context if not code_context: return "" call_line = code_context[0].strip() tree = ast.parse(call_line) for node in ast.walk(tree): if isinstance(node, ast.Call): for arg in node.args: if isinstance(arg, ast.Name): if eval(arg.id, frame.f_globals, frame.f_locals) is var: return arg.id elif isinstance(arg, ast.Attribute): attr_names = [] while isinstance(arg, ast.Attribute): attr_names.append(arg.attr) arg = arg.value if isinstance(arg, ast.Name): attr_names.append(arg.id) full_name = '.'.join(reversed(attr_names)) if eval(full_name, frame.f_globals, frame.f_locals) is var: return full_name except Exception as e: pawn.console.log(f"Exception occurred in get_var_name: {e}") return ""
[docs]def get_data_length(data): """ Get the length of the data. If the data is a dictionary, it calculates the nested dictionary length. :param data: The input data which can be of any type. :return: A string representing the length of the data. Example: .. code-block:: python get_data_length([1, 2, 3]) # >> 'len=3' get_data_length({'a': 1, 'b': {'c': 2}}) # >> 'dict_len=2' get_data_length("hello") # >> 'len=5' """ try: if isinstance(data, dict): return f"dict_len={count_nested_dict_len(data)}" else: return f"len={len(data)}" except Exception: return ""
[docs]def add_node(tree, key, value, detail): """Recursively add nodes to the tree with syntax_highlight for dict and list.""" type_length_info = f"[dim]({type(value).__name__}) len={len(value)}[/dim]" if hasattr(value, '__len__') else "" if isinstance(value, dict) or isinstance(value, list): syntax_str = syntax_highlight(value, rich=True) tree.add(f"[cyan]{key}[/cyan] {type_length_info}\n{syntax_str}") elif hasattr(value, '__dict__') or is_dataclass(value): branch = tree.add(f"[cyan]{key}[/cyan] [green]{type_length_info}[/green]") if detail: attributes = asdict(value) if is_dataclass(value) else vars(value) for attr, attr_value in attributes.items(): add_node(branch, attr, attr_value, detail) else: tree.add(f"[cyan]{key}[/cyan] = {style_value(value)}")
[docs]def get_type_length_info_style(value): """ Get the type and length information as a styled string based on the value's type. This version provides a more intuitive output for type objects. """ if isinstance(value, type): type_name = value.__name__ length_info = "" type_length_info = f"[dim](class: {type_name})[/dim]" return f"[yellow]{type_length_info}[/yellow]" else: type_name = type(value).__name__ length_info = f"len={len(value)}" if hasattr(value, '__len__') else "" type_length_info = f"[dim]({type_name}) {length_info}[/dim]" if isinstance(value, dict): return f"[cyan]{type_length_info}[/cyan]" elif isinstance(value, list): return f"[magenta]{type_length_info}[/magenta]" elif hasattr(value, '__dict__') or is_dataclass(value): return f"[green]{type_length_info}[/green]" else: return f"[white]{type_length_info}[/white]"
[docs]def style_value(value): """ Apply different styles based on the value's type or value. """ type_length_info = get_type_length_info_style(value) if isinstance(value, bool): return f"[green]{value}[/green]" if value else f"[red]{value}[/red]" elif isinstance(value, (int, float)): return f"[cyan]{value} {type_length_info}[/cyan]" elif isinstance(value, str): return f"[yellow]{value} {type_length_info}[/yellow]" else: return f"[white]{value} {type_length_info}[/white]"
[docs]def create_kv_table(padding=0, key_ratio=2, value_ratio=7, overflow="fold"): table = Table( padding=padding, pad_edge=False, expand=True, # 테이블이 전체 화면에 맞춰 늘어나도록 설정 show_header=False, show_footer=False, show_edge=False, show_lines=False, box=None, ) table.add_column("Key", no_wrap=False, justify="left", style="bold yellow", min_width=padding, ratio=key_ratio, overflow=overflow) table.add_column("Separator", no_wrap=False, justify="left", width=3) table.add_column("Value", no_wrap=False, justify="left", ratio=value_ratio, max_width=None, overflow=overflow) # max_width 제한 해제 table.add_column("Debug Info", justify="right", style="grey84") return table
# def get_pretty_value(value): # if isinstance(value, (dict, list)): # pawn.console.log(value) # return Syntax(json.dumps(value, indent=4), "json", theme="material", line_numbers=False) # else: # if value and is_json(value): # __loaded_json = json.loads(value) # return Pretty(json.loads(value)) # return value
[docs]def get_pretty_value(value, is_force_syntax=False): if isinstance(value, (dict, list)): if is_force_syntax: return Syntax(json.dumps(value, indent=4), "json", theme="material", line_numbers=False) pretty_value = Pretty(value, expand_all=True) with pawn.console.capture() as capture: pawn.console.print(pretty_value) return capture.get() else: if value and is_json(value): __loaded_json = json.loads(value) pretty_value = Pretty(__loaded_json, expand_all=True) with pawn.console.capture() as capture: pawn.console.print(pretty_value) return capture.get() return value
[docs]def align_text(left_text: str = '', right_text: str = '', filler: str = '.', offset: int = 2): """ Aligns text to the left and right with a filler in between. :param left_text: The text to align to the left. :param right_text: The text to align to the right. :param filler: The character to use as filler between the left and right text. :param offset: The number of spaces to offset the right text from the right edge. Example: .. code-block:: python align_text('Hello', 'World', filler='-', offset=3) # >> 'Hello ---------------------------- World' align_text('Left', 'Right', filler='*', offset=5) # >> 'Left ************************** Right' """ cleaned_left_text = remove_ascii_color_codes(left_text) cleaned_right_text = remove_ascii_color_codes(right_text) cleaned_left_text = remove_tags(cleaned_left_text, case_sensitive='lower') cleaned_right_text = remove_tags(cleaned_right_text, case_sensitive='lower') padding_length = pawn.console.width - len(cleaned_left_text) - len(cleaned_right_text) - offset padding = filler * padding_length full_text = f"{left_text} {padding} {right_text}" return full_text
[docs]@contextmanager def disable_exception_traceback(): """ All traceback information is suppressed and only the exception type and value are printed """ default_value = getattr(sys, "tracebacklimit", 1000) # `1000` is a Python's default value sys.tracebacklimit = 0 yield sys.tracebacklimit = default_value # revert changes
# try: # sys.tracebacklimit = 0 # yield # finally: # sys.tracebacklimit = default_value # revert changes
[docs]class NoTraceBackException(Exception): def __init__(self, msg): try: line_no = sys.exc_info()[-1].tb_lineno filename = sys.exc_info()[-1].tb_filename except AttributeError: previous_frame = inspect.currentframe().f_back line_no = inspect.currentframe().f_back.f_lineno (filename, line_number, function_name, ln, index) = inspect.getframeinfo(previous_frame) # self.args = "<{0.__name__}> ({2} line {2}): \n {3}".format(type(self), filename, line_no, msg), # self.args = "{0}<{1.__name__}>{2} ({3} line {4}): \n {5}".format(bcolors.FAIL, type(self), bcolors.ENDC, filename, line_no, msg), # self.args = "<{0.__name__}>({1} line {2}): \n {3}".format(type(self), filename, line_no, msg), self.args = "<{0.__name__}> {1}".format(type(self), msg), # ex_type, ex_value, traceback = sys.exc_info() raise Exception(self)
[docs]def get_color_by_threshold(value, limit=100, unit="", thresholds=None, return_tuple=False): """ Determine the color based on the value and thresholds. :param value: The value to be evaluated. :param limit: The limit to compare the value against. Default is 100. :param unit: The unit to append to the value. Default is an empty string. :param thresholds: A dictionary defining the thresholds and their corresponding colors. The keys are the threshold values (as a fraction of the limit) and the values are the colors. Example: {0.9: "red", 0.8: "orange1", 0.7: "yellow"} Default is {0.9: "red", 0.8: "orange1", 0.7: "yellow", 0.0: "white"}. :param return_tuple: If True, returns a tuple (color, formatted_value). Default is False. :return: If return_tuple is False, returns a formatted string in rich text format. If return_tuple is True, returns a tuple (color, formatted_value). """ if thresholds is None: thresholds = {0.9: "red", 0.8: "orange1", 0.7: "yellow", 0.0: "white"} percent = value / limit if limit != 0 else 0 color = "green" for threshold, threshold_color in sorted(thresholds.items(), reverse=True): if percent >= threshold: color = threshold_color break formatted_value = f"{value}{unit}" if return_tuple: return color, formatted_value else: return f"[{color}]{formatted_value}[/{color}]"