muutils.dbg
this code is based on an implementation of the Rust builtin dbg! for Python, originally from
https://github.com/tylerwince/pydbg/blob/master/pydbg.py
although it has been significantly modified
licensed under MIT:
Copyright (c) 2019 Tyler Wince
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1""" 2 3this code is based on an implementation of the Rust builtin `dbg!` for Python, originally from 4https://github.com/tylerwince/pydbg/blob/master/pydbg.py 5although it has been significantly modified 6 7licensed under MIT: 8 9Copyright (c) 2019 Tyler Wince 10 11Permission is hereby granted, free of charge, to any person obtaining a copy 12of this software and associated documentation files (the "Software"), to deal 13in the Software without restriction, including without limitation the rights 14to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15copies of the Software, and to permit persons to whom the Software is 16furnished to do so, subject to the following conditions: 17 18The above copyright notice and this permission notice shall be included in 19all copies or substantial portions of the Software. 20 21THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27THE SOFTWARE. 28 29""" 30 31from __future__ import annotations 32 33import inspect 34import sys 35import typing 36from pathlib import Path 37import re 38 39# type defs 40_ExpType = typing.TypeVar("_ExpType") 41_ExpType_dict = typing.TypeVar( 42 "_ExpType_dict", bound=typing.Dict[typing.Any, typing.Any] 43) 44_ExpType_list = typing.TypeVar("_ExpType_list", bound=typing.List[typing.Any]) 45 46 47# TypedDict definitions for configuration dictionaries 48class DBGDictDefaultsType(typing.TypedDict): 49 key_types: bool 50 val_types: bool 51 max_len: int 52 indent: str 53 max_depth: int 54 55 56class DBGListDefaultsType(typing.TypedDict): 57 max_len: int 58 summary_show_types: bool 59 60 61class DBGTensorArraySummaryDefaultsType(typing.TypedDict): 62 fmt: typing.Literal["unicode", "latex", "ascii"] 63 precision: int 64 stats: bool 65 shape: bool 66 dtype: bool 67 device: bool 68 requires_grad: bool 69 sparkline: bool 70 sparkline_bins: int 71 sparkline_logy: typing.Union[None, bool] 72 colored: bool 73 eq_char: str 74 75 76# Sentinel type for no expression passed 77class _NoExpPassedSentinel: 78 """Unique sentinel type used to indicate that no expression was passed.""" 79 80 pass 81 82 83_NoExpPassed = _NoExpPassedSentinel() 84 85# global variables 86_CWD: Path = Path.cwd().absolute() 87_COUNTER: int = 0 88 89# configuration 90PATH_MODE: typing.Literal["relative", "absolute"] = "relative" 91DEFAULT_VAL_JOINER: str = " = " 92 93 94# path processing 95def _process_path(path: Path) -> str: 96 path_abs: Path = path.absolute() 97 fname: Path 98 if PATH_MODE == "absolute": 99 fname = path_abs 100 elif PATH_MODE == "relative": 101 try: 102 # if it's inside the cwd, print the relative path 103 fname = path.relative_to(_CWD) 104 except ValueError: 105 # if its not in the subpath, use the absolute path 106 fname = path_abs 107 else: 108 raise ValueError("PATH_MODE must be either 'relative' or 'absolute") 109 110 return fname.as_posix() 111 112 113# actual dbg function 114@typing.overload 115def dbg() -> _NoExpPassedSentinel: ... 116@typing.overload 117def dbg( 118 exp: _NoExpPassedSentinel, 119 formatter: typing.Optional[typing.Callable[[typing.Any], str]] = None, 120 val_joiner: str = DEFAULT_VAL_JOINER, 121) -> _NoExpPassedSentinel: ... 122@typing.overload 123def dbg( 124 exp: _ExpType, 125 formatter: typing.Optional[typing.Callable[[typing.Any], str]] = None, 126 val_joiner: str = DEFAULT_VAL_JOINER, 127) -> _ExpType: ... 128def dbg( 129 exp: typing.Union[_ExpType, _NoExpPassedSentinel] = _NoExpPassed, 130 formatter: typing.Optional[typing.Callable[[typing.Any], str]] = None, 131 val_joiner: str = DEFAULT_VAL_JOINER, 132) -> typing.Union[_ExpType, _NoExpPassedSentinel]: 133 """Call dbg with any variable or expression. 134 135 Calling dbg will print to stderr the current filename and lineno, 136 as well as the passed expression and what the expression evaluates to: 137 138 from muutils.dbg import dbg 139 140 a = 2 141 b = 5 142 143 dbg(a+b) 144 145 def square(x: int) -> int: 146 return x * x 147 148 dbg(square(a)) 149 150 """ 151 global _COUNTER 152 153 # get the context 154 line_exp: str = "unknown" 155 current_file: str = "unknown" 156 dbg_frame: typing.Optional[inspect.FrameInfo] = None 157 for frame in inspect.stack(): 158 if frame.code_context is None: 159 continue 160 line: str = frame.code_context[0] 161 if "dbg" in line: 162 current_file = _process_path(Path(frame.filename)) 163 dbg_frame = frame 164 start: int = line.find("(") + 1 165 end: int = line.rfind(")") 166 if end == -1: 167 end = len(line) 168 line_exp = line[start:end] 169 break 170 171 fname: str = "unknown" 172 if current_file.startswith("/tmp/ipykernel_"): 173 stack: list[inspect.FrameInfo] = inspect.stack() 174 filtered_functions: list[str] = [] 175 # this loop will find, in this order: 176 # - the dbg function call 177 # - the functions we care about displaying 178 # - `<module>` 179 # - a bunch of jupyter internals we don't care about 180 for frame_info in stack: 181 if _process_path(Path(frame_info.filename)) != current_file: 182 continue 183 if frame_info.function == "<module>": 184 break 185 if frame_info.function.startswith("dbg"): 186 continue 187 filtered_functions.append(frame_info.function) 188 if dbg_frame is not None: 189 filtered_functions.append(f"<ipykernel>:{dbg_frame.lineno}") 190 else: 191 filtered_functions.append(current_file) 192 filtered_functions.reverse() 193 fname = " -> ".join(filtered_functions) 194 elif dbg_frame is not None: 195 fname = f"{current_file}:{dbg_frame.lineno}" 196 197 # assemble the message 198 msg: str 199 if exp is _NoExpPassed: 200 # if no expression is passed, just show location and counter value 201 msg = f"[ {fname} ] <dbg {_COUNTER}>" 202 _COUNTER += 1 203 else: 204 # if expression passed, format its value and show location, expr, and value 205 exp_val: str = formatter(exp) if formatter else repr(exp) 206 msg = f"[ {fname} ] {line_exp}{val_joiner}{exp_val}" 207 208 # print the message 209 print( 210 msg, 211 file=sys.stderr, 212 ) 213 214 # return the expression itself 215 return exp 216 217 218# formatted `dbg_*` functions with their helpers 219 220DBG_TENSOR_ARRAY_SUMMARY_DEFAULTS: DBGTensorArraySummaryDefaultsType = { 221 "fmt": "unicode", 222 "precision": 2, 223 "stats": True, 224 "shape": True, 225 "dtype": True, 226 "device": True, 227 "requires_grad": True, 228 "sparkline": True, 229 "sparkline_bins": 7, 230 "sparkline_logy": None, # None means auto-detect 231 "colored": True, 232 "eq_char": "=", 233} 234 235 236DBG_TENSOR_VAL_JOINER: str = ": " 237 238 239def tensor_info(tensor: typing.Any) -> str: 240 from muutils.tensor_info import array_summary 241 242 # TODO: explicitly pass args to avoid type: ignore (mypy can't match overloads with **TypedDict spread) 243 return array_summary(tensor, as_list=False, **DBG_TENSOR_ARRAY_SUMMARY_DEFAULTS) # type: ignore[call-overload] 244 245 246DBG_DICT_DEFAULTS: DBGDictDefaultsType = { 247 "key_types": True, 248 "val_types": True, 249 "max_len": 32, 250 "indent": " ", 251 "max_depth": 3, 252} 253 254DBG_LIST_DEFAULTS: DBGListDefaultsType = { 255 "max_len": 16, 256 "summary_show_types": True, 257} 258 259 260def list_info( 261 lst: typing.List[typing.Any], 262) -> str: 263 len_l: int = len(lst) 264 output: str 265 if len_l > DBG_LIST_DEFAULTS["max_len"]: 266 output = f"<list of len()={len_l}" 267 if DBG_LIST_DEFAULTS["summary_show_types"]: 268 val_types: typing.Set[str] = set(type(x).__name__ for x in lst) 269 output += f", types={{{', '.join(sorted(val_types))}}}" 270 output += ">" 271 else: 272 output = "[" + ", ".join(repr(x) for x in lst) + "]" 273 274 return output 275 276 277TENSOR_STR_TYPES: typing.Set[str] = { 278 "<class 'torch.Tensor'>", 279 "<class 'numpy.ndarray'>", 280} 281 282 283def dict_info( 284 d: typing.Dict[typing.Any, typing.Any], 285 depth: int = 0, 286) -> str: 287 len_d: int = len(d) 288 indent: str = DBG_DICT_DEFAULTS["indent"] 289 290 # summary line 291 output: str = f"{indent * depth}<dict of len()={len_d}" 292 293 if DBG_DICT_DEFAULTS["key_types"] and len_d > 0: 294 key_types: typing.Set[str] = set(type(k).__name__ for k in d.keys()) 295 key_types_str: str = "{" + ", ".join(sorted(key_types)) + "}" 296 output += f", key_types={key_types_str}" 297 298 if DBG_DICT_DEFAULTS["val_types"] and len_d > 0: 299 val_types: typing.Set[str] = set(type(v).__name__ for v in d.values()) 300 val_types_str: str = "{" + ", ".join(sorted(val_types)) + "}" 301 output += f", val_types={val_types_str}" 302 303 output += ">" 304 305 # keys/values if not to deep and not too many 306 if depth < DBG_DICT_DEFAULTS["max_depth"]: 307 if len_d > 0 and len_d < DBG_DICT_DEFAULTS["max_len"]: 308 for k, v in d.items(): 309 key_str: str = repr(k) if not isinstance(k, str) else k 310 311 val_str: str 312 val_type_str: str = str(type(v)) 313 if isinstance(v, dict): 314 val_str = dict_info(v, depth + 1) 315 elif val_type_str in TENSOR_STR_TYPES: 316 val_str = tensor_info(v) 317 elif isinstance(v, list): 318 val_str = list_info(v) 319 else: 320 val_str = repr(v) 321 322 output += ( 323 f"\n{indent * (depth + 1)}{key_str}{DBG_TENSOR_VAL_JOINER}{val_str}" 324 ) 325 326 return output 327 328 329def info_auto( 330 obj: typing.Any, 331) -> str: 332 """Automatically format an object for debugging.""" 333 if isinstance(obj, dict): 334 return dict_info(obj) 335 elif isinstance(obj, list): 336 return list_info(obj) 337 elif str(type(obj)) in TENSOR_STR_TYPES: 338 return tensor_info(obj) 339 else: 340 return repr(obj) 341 342 343def dbg_tensor( 344 tensor: _ExpType, # numpy array or torch tensor 345) -> _ExpType: 346 """dbg function for tensors, using tensor_info formatter.""" 347 return dbg( 348 tensor, 349 formatter=tensor_info, 350 val_joiner=DBG_TENSOR_VAL_JOINER, 351 ) 352 353 354def dbg_dict( 355 d: _ExpType_dict, 356) -> _ExpType_dict: 357 """dbg function for dictionaries, using dict_info formatter.""" 358 return dbg( 359 d, 360 formatter=dict_info, 361 val_joiner=DBG_TENSOR_VAL_JOINER, 362 ) 363 364 365def dbg_auto( 366 obj: _ExpType, 367) -> _ExpType: 368 """dbg function for automatic formatting based on type.""" 369 return dbg( 370 obj, 371 formatter=info_auto, 372 val_joiner=DBG_TENSOR_VAL_JOINER, 373 ) 374 375 376def _normalize_for_loose(text: str) -> str: 377 """Normalize text for loose matching by replacing non-alphanumeric chars with spaces.""" 378 normalized: str = re.sub(r"[^a-zA-Z0-9]+", " ", text) 379 return " ".join(normalized.split()) 380 381 382def _compile_pattern( 383 pattern: str | re.Pattern[str], 384 *, 385 cased: bool = False, 386 loose: bool = False, 387) -> re.Pattern[str]: 388 """Compile pattern with appropriate flags for case sensitivity and loose matching.""" 389 if isinstance(pattern, re.Pattern): 390 return pattern 391 392 # Start with no flags for case-insensitive default 393 flags: int = 0 394 if not cased: 395 flags |= re.IGNORECASE 396 397 if loose: 398 pattern = _normalize_for_loose(pattern) 399 400 return re.compile(pattern, flags) 401 402 403def grep_repr( 404 obj: typing.Any, 405 pattern: str | re.Pattern[str], 406 *, 407 char_context: int | None = 20, 408 line_context: int | None = None, 409 before_context: int = 0, 410 after_context: int = 0, 411 context: int | None = None, 412 max_count: int | None = None, 413 cased: bool = False, 414 loose: bool = False, 415 line_numbers: bool = False, 416 highlight: bool = True, 417 color: str = "31", 418 separator: str = "--", 419 quiet: bool = False, 420) -> typing.List[str] | None: 421 """grep-like search on ``repr(obj)`` with improved grep-style options. 422 423 By default, string patterns are case-insensitive. Pre-compiled regex 424 patterns use their own flags. 425 426 Parameters: 427 - obj: Object to search (its repr() string is scanned) 428 - pattern: Regular expression pattern (string or pre-compiled) 429 - char_context: Characters of context before/after each match (default: 20) 430 - line_context: Lines of context before/after; overrides char_context 431 - before_context: Lines of context before match (like grep -B) 432 - after_context: Lines of context after match (like grep -A) 433 - context: Lines of context before AND after (like grep -C) 434 - max_count: Stop after this many matches 435 - cased: Force case-sensitive search for string patterns 436 - loose: Normalize spaces/punctuation for flexible matching 437 - line_numbers: Show line numbers in output 438 - highlight: Wrap matches with ANSI color codes 439 - color: ANSI color code (default: "31" for red) 440 - separator: Separator between multiple matches 441 - quiet: Return results instead of printing 442 443 Returns: 444 - None if quiet=False (prints to stdout) 445 - List[str] if quiet=True (returns formatted output lines) 446 """ 447 # Handle context parameter shortcuts 448 if context is not None: 449 before_context = after_context = context 450 451 # Prepare text and pattern 452 text: str = repr(obj) 453 if loose: 454 text = _normalize_for_loose(text) 455 456 regex: re.Pattern[str] = _compile_pattern(pattern, cased=cased, loose=loose) 457 458 def _color_match(segment: str) -> str: 459 if not highlight: 460 return segment 461 return regex.sub(lambda m: f"\033[1;{color}m{m.group(0)}\033[0m", segment) 462 463 output_lines: list[str] = [] 464 match_count: int = 0 465 466 # Determine if we're using line-based context 467 using_line_context = ( 468 line_context is not None or before_context > 0 or after_context > 0 469 ) 470 471 if using_line_context: 472 lines: list[str] = text.splitlines() 473 line_starts: list[int] = [] 474 pos: int = 0 475 for line in lines: 476 line_starts.append(pos) 477 pos += len(line) + 1 # +1 for newline 478 479 processed_lines: set[int] = set() 480 481 for match in regex.finditer(text): 482 if max_count is not None and match_count >= max_count: 483 break 484 485 # Find which line contains this match 486 match_line = max( 487 i for i, start in enumerate(line_starts) if start <= match.start() 488 ) 489 490 # Calculate context range 491 ctx_before: int 492 ctx_after: int 493 if line_context is not None: 494 ctx_before = ctx_after = line_context 495 else: 496 ctx_before, ctx_after = before_context, after_context 497 498 start_line: int = max(0, match_line - ctx_before) 499 end_line: int = min(len(lines), match_line + ctx_after + 1) 500 501 # Avoid duplicate output for overlapping contexts 502 line_range: set[int] = set(range(start_line, end_line)) 503 if line_range & processed_lines: 504 continue 505 processed_lines.update(line_range) 506 507 # Format the context block 508 context_lines: list[str] = [] 509 for i in range(start_line, end_line): 510 line_text = lines[i] 511 if line_numbers: 512 line_prefix = f"{i + 1}:" 513 line_text = f"{line_prefix}{line_text}" 514 context_lines.append(_color_match(line_text)) 515 516 if output_lines and separator: 517 output_lines.append(separator) 518 output_lines.extend(context_lines) 519 match_count += 1 520 521 else: 522 # Character-based context 523 ctx: int = 0 if char_context is None else char_context 524 525 for match in regex.finditer(text): 526 if max_count is not None and match_count >= max_count: 527 break 528 529 start: int = max(0, match.start() - ctx) 530 end: int = min(len(text), match.end() + ctx) 531 snippet: str = text[start:end] 532 533 if output_lines and separator: 534 output_lines.append(separator) 535 output_lines.append(_color_match(snippet)) 536 match_count += 1 537 538 if quiet: 539 return output_lines 540 else: 541 for line in output_lines: 542 print(line) 543 return None
49class DBGDictDefaultsType(typing.TypedDict): 50 key_types: bool 51 val_types: bool 52 max_len: int 53 indent: str 54 max_depth: int
Inherited Members
- builtins.dict
- get
- setdefault
- pop
- popitem
- keys
- items
- values
- update
- fromkeys
- clear
- copy
Inherited Members
- builtins.dict
- get
- setdefault
- pop
- popitem
- keys
- items
- values
- update
- fromkeys
- clear
- copy
62class DBGTensorArraySummaryDefaultsType(typing.TypedDict): 63 fmt: typing.Literal["unicode", "latex", "ascii"] 64 precision: int 65 stats: bool 66 shape: bool 67 dtype: bool 68 device: bool 69 requires_grad: bool 70 sparkline: bool 71 sparkline_bins: int 72 sparkline_logy: typing.Union[None, bool] 73 colored: bool 74 eq_char: str
Inherited Members
- builtins.dict
- get
- setdefault
- pop
- popitem
- keys
- items
- values
- update
- fromkeys
- clear
- copy
129def dbg( 130 exp: typing.Union[_ExpType, _NoExpPassedSentinel] = _NoExpPassed, 131 formatter: typing.Optional[typing.Callable[[typing.Any], str]] = None, 132 val_joiner: str = DEFAULT_VAL_JOINER, 133) -> typing.Union[_ExpType, _NoExpPassedSentinel]: 134 """Call dbg with any variable or expression. 135 136 Calling dbg will print to stderr the current filename and lineno, 137 as well as the passed expression and what the expression evaluates to: 138 139 from muutils.dbg import dbg 140 141 a = 2 142 b = 5 143 144 dbg(a+b) 145 146 def square(x: int) -> int: 147 return x * x 148 149 dbg(square(a)) 150 151 """ 152 global _COUNTER 153 154 # get the context 155 line_exp: str = "unknown" 156 current_file: str = "unknown" 157 dbg_frame: typing.Optional[inspect.FrameInfo] = None 158 for frame in inspect.stack(): 159 if frame.code_context is None: 160 continue 161 line: str = frame.code_context[0] 162 if "dbg" in line: 163 current_file = _process_path(Path(frame.filename)) 164 dbg_frame = frame 165 start: int = line.find("(") + 1 166 end: int = line.rfind(")") 167 if end == -1: 168 end = len(line) 169 line_exp = line[start:end] 170 break 171 172 fname: str = "unknown" 173 if current_file.startswith("/tmp/ipykernel_"): 174 stack: list[inspect.FrameInfo] = inspect.stack() 175 filtered_functions: list[str] = [] 176 # this loop will find, in this order: 177 # - the dbg function call 178 # - the functions we care about displaying 179 # - `<module>` 180 # - a bunch of jupyter internals we don't care about 181 for frame_info in stack: 182 if _process_path(Path(frame_info.filename)) != current_file: 183 continue 184 if frame_info.function == "<module>": 185 break 186 if frame_info.function.startswith("dbg"): 187 continue 188 filtered_functions.append(frame_info.function) 189 if dbg_frame is not None: 190 filtered_functions.append(f"<ipykernel>:{dbg_frame.lineno}") 191 else: 192 filtered_functions.append(current_file) 193 filtered_functions.reverse() 194 fname = " -> ".join(filtered_functions) 195 elif dbg_frame is not None: 196 fname = f"{current_file}:{dbg_frame.lineno}" 197 198 # assemble the message 199 msg: str 200 if exp is _NoExpPassed: 201 # if no expression is passed, just show location and counter value 202 msg = f"[ {fname} ] <dbg {_COUNTER}>" 203 _COUNTER += 1 204 else: 205 # if expression passed, format its value and show location, expr, and value 206 exp_val: str = formatter(exp) if formatter else repr(exp) 207 msg = f"[ {fname} ] {line_exp}{val_joiner}{exp_val}" 208 209 # print the message 210 print( 211 msg, 212 file=sys.stderr, 213 ) 214 215 # return the expression itself 216 return exp
Call dbg with any variable or expression.
Calling dbg will print to stderr the current filename and lineno, as well as the passed expression and what the expression evaluates to:
from muutils.dbg import dbg
a = 2
b = 5
dbg(a+b)
def square(x: int) -> int:
return x * x
dbg(square(a))
240def tensor_info(tensor: typing.Any) -> str: 241 from muutils.tensor_info import array_summary 242 243 # TODO: explicitly pass args to avoid type: ignore (mypy can't match overloads with **TypedDict spread) 244 return array_summary(tensor, as_list=False, **DBG_TENSOR_ARRAY_SUMMARY_DEFAULTS) # type: ignore[call-overload]
261def list_info( 262 lst: typing.List[typing.Any], 263) -> str: 264 len_l: int = len(lst) 265 output: str 266 if len_l > DBG_LIST_DEFAULTS["max_len"]: 267 output = f"<list of len()={len_l}" 268 if DBG_LIST_DEFAULTS["summary_show_types"]: 269 val_types: typing.Set[str] = set(type(x).__name__ for x in lst) 270 output += f", types={{{', '.join(sorted(val_types))}}}" 271 output += ">" 272 else: 273 output = "[" + ", ".join(repr(x) for x in lst) + "]" 274 275 return output
284def dict_info( 285 d: typing.Dict[typing.Any, typing.Any], 286 depth: int = 0, 287) -> str: 288 len_d: int = len(d) 289 indent: str = DBG_DICT_DEFAULTS["indent"] 290 291 # summary line 292 output: str = f"{indent * depth}<dict of len()={len_d}" 293 294 if DBG_DICT_DEFAULTS["key_types"] and len_d > 0: 295 key_types: typing.Set[str] = set(type(k).__name__ for k in d.keys()) 296 key_types_str: str = "{" + ", ".join(sorted(key_types)) + "}" 297 output += f", key_types={key_types_str}" 298 299 if DBG_DICT_DEFAULTS["val_types"] and len_d > 0: 300 val_types: typing.Set[str] = set(type(v).__name__ for v in d.values()) 301 val_types_str: str = "{" + ", ".join(sorted(val_types)) + "}" 302 output += f", val_types={val_types_str}" 303 304 output += ">" 305 306 # keys/values if not to deep and not too many 307 if depth < DBG_DICT_DEFAULTS["max_depth"]: 308 if len_d > 0 and len_d < DBG_DICT_DEFAULTS["max_len"]: 309 for k, v in d.items(): 310 key_str: str = repr(k) if not isinstance(k, str) else k 311 312 val_str: str 313 val_type_str: str = str(type(v)) 314 if isinstance(v, dict): 315 val_str = dict_info(v, depth + 1) 316 elif val_type_str in TENSOR_STR_TYPES: 317 val_str = tensor_info(v) 318 elif isinstance(v, list): 319 val_str = list_info(v) 320 else: 321 val_str = repr(v) 322 323 output += ( 324 f"\n{indent * (depth + 1)}{key_str}{DBG_TENSOR_VAL_JOINER}{val_str}" 325 ) 326 327 return output
330def info_auto( 331 obj: typing.Any, 332) -> str: 333 """Automatically format an object for debugging.""" 334 if isinstance(obj, dict): 335 return dict_info(obj) 336 elif isinstance(obj, list): 337 return list_info(obj) 338 elif str(type(obj)) in TENSOR_STR_TYPES: 339 return tensor_info(obj) 340 else: 341 return repr(obj)
Automatically format an object for debugging.
344def dbg_tensor( 345 tensor: _ExpType, # numpy array or torch tensor 346) -> _ExpType: 347 """dbg function for tensors, using tensor_info formatter.""" 348 return dbg( 349 tensor, 350 formatter=tensor_info, 351 val_joiner=DBG_TENSOR_VAL_JOINER, 352 )
dbg function for tensors, using tensor_info formatter.
355def dbg_dict( 356 d: _ExpType_dict, 357) -> _ExpType_dict: 358 """dbg function for dictionaries, using dict_info formatter.""" 359 return dbg( 360 d, 361 formatter=dict_info, 362 val_joiner=DBG_TENSOR_VAL_JOINER, 363 )
dbg function for dictionaries, using dict_info formatter.
366def dbg_auto( 367 obj: _ExpType, 368) -> _ExpType: 369 """dbg function for automatic formatting based on type.""" 370 return dbg( 371 obj, 372 formatter=info_auto, 373 val_joiner=DBG_TENSOR_VAL_JOINER, 374 )
dbg function for automatic formatting based on type.
404def grep_repr( 405 obj: typing.Any, 406 pattern: str | re.Pattern[str], 407 *, 408 char_context: int | None = 20, 409 line_context: int | None = None, 410 before_context: int = 0, 411 after_context: int = 0, 412 context: int | None = None, 413 max_count: int | None = None, 414 cased: bool = False, 415 loose: bool = False, 416 line_numbers: bool = False, 417 highlight: bool = True, 418 color: str = "31", 419 separator: str = "--", 420 quiet: bool = False, 421) -> typing.List[str] | None: 422 """grep-like search on ``repr(obj)`` with improved grep-style options. 423 424 By default, string patterns are case-insensitive. Pre-compiled regex 425 patterns use their own flags. 426 427 Parameters: 428 - obj: Object to search (its repr() string is scanned) 429 - pattern: Regular expression pattern (string or pre-compiled) 430 - char_context: Characters of context before/after each match (default: 20) 431 - line_context: Lines of context before/after; overrides char_context 432 - before_context: Lines of context before match (like grep -B) 433 - after_context: Lines of context after match (like grep -A) 434 - context: Lines of context before AND after (like grep -C) 435 - max_count: Stop after this many matches 436 - cased: Force case-sensitive search for string patterns 437 - loose: Normalize spaces/punctuation for flexible matching 438 - line_numbers: Show line numbers in output 439 - highlight: Wrap matches with ANSI color codes 440 - color: ANSI color code (default: "31" for red) 441 - separator: Separator between multiple matches 442 - quiet: Return results instead of printing 443 444 Returns: 445 - None if quiet=False (prints to stdout) 446 - List[str] if quiet=True (returns formatted output lines) 447 """ 448 # Handle context parameter shortcuts 449 if context is not None: 450 before_context = after_context = context 451 452 # Prepare text and pattern 453 text: str = repr(obj) 454 if loose: 455 text = _normalize_for_loose(text) 456 457 regex: re.Pattern[str] = _compile_pattern(pattern, cased=cased, loose=loose) 458 459 def _color_match(segment: str) -> str: 460 if not highlight: 461 return segment 462 return regex.sub(lambda m: f"\033[1;{color}m{m.group(0)}\033[0m", segment) 463 464 output_lines: list[str] = [] 465 match_count: int = 0 466 467 # Determine if we're using line-based context 468 using_line_context = ( 469 line_context is not None or before_context > 0 or after_context > 0 470 ) 471 472 if using_line_context: 473 lines: list[str] = text.splitlines() 474 line_starts: list[int] = [] 475 pos: int = 0 476 for line in lines: 477 line_starts.append(pos) 478 pos += len(line) + 1 # +1 for newline 479 480 processed_lines: set[int] = set() 481 482 for match in regex.finditer(text): 483 if max_count is not None and match_count >= max_count: 484 break 485 486 # Find which line contains this match 487 match_line = max( 488 i for i, start in enumerate(line_starts) if start <= match.start() 489 ) 490 491 # Calculate context range 492 ctx_before: int 493 ctx_after: int 494 if line_context is not None: 495 ctx_before = ctx_after = line_context 496 else: 497 ctx_before, ctx_after = before_context, after_context 498 499 start_line: int = max(0, match_line - ctx_before) 500 end_line: int = min(len(lines), match_line + ctx_after + 1) 501 502 # Avoid duplicate output for overlapping contexts 503 line_range: set[int] = set(range(start_line, end_line)) 504 if line_range & processed_lines: 505 continue 506 processed_lines.update(line_range) 507 508 # Format the context block 509 context_lines: list[str] = [] 510 for i in range(start_line, end_line): 511 line_text = lines[i] 512 if line_numbers: 513 line_prefix = f"{i + 1}:" 514 line_text = f"{line_prefix}{line_text}" 515 context_lines.append(_color_match(line_text)) 516 517 if output_lines and separator: 518 output_lines.append(separator) 519 output_lines.extend(context_lines) 520 match_count += 1 521 522 else: 523 # Character-based context 524 ctx: int = 0 if char_context is None else char_context 525 526 for match in regex.finditer(text): 527 if max_count is not None and match_count >= max_count: 528 break 529 530 start: int = max(0, match.start() - ctx) 531 end: int = min(len(text), match.end() + ctx) 532 snippet: str = text[start:end] 533 534 if output_lines and separator: 535 output_lines.append(separator) 536 output_lines.append(_color_match(snippet)) 537 match_count += 1 538 539 if quiet: 540 return output_lines 541 else: 542 for line in output_lines: 543 print(line) 544 return None
grep-like search on repr(obj) with improved grep-style options.
By default, string patterns are case-insensitive. Pre-compiled regex patterns use their own flags.
Parameters:
- obj: Object to search (its repr() string is scanned)
- pattern: Regular expression pattern (string or pre-compiled)
- char_context: Characters of context before/after each match (default: 20)
- line_context: Lines of context before/after; overrides char_context
- before_context: Lines of context before match (like grep -B)
- after_context: Lines of context after match (like grep -A)
- context: Lines of context before AND after (like grep -C)
- max_count: Stop after this many matches
- cased: Force case-sensitive search for string patterns
- loose: Normalize spaces/punctuation for flexible matching
- line_numbers: Show line numbers in output
- highlight: Wrap matches with ANSI color codes
- color: ANSI color code (default: "31" for red)
- separator: Separator between multiple matches
- quiet: Return results instead of printing
Returns:
- None if quiet=False (prints to stdout)
- List[str] if quiet=True (returns formatted output lines)