docs for muutils v0.9.0
View Source on GitHub

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

class DBGDictDefaultsType(typing.TypedDict):
49class DBGDictDefaultsType(typing.TypedDict):
50    key_types: bool
51    val_types: bool
52    max_len: int
53    indent: str
54    max_depth: int
key_types: bool
val_types: bool
max_len: int
indent: str
max_depth: int
Inherited Members
builtins.dict
get
setdefault
pop
popitem
keys
items
values
update
fromkeys
clear
copy
class DBGListDefaultsType(typing.TypedDict):
57class DBGListDefaultsType(typing.TypedDict):
58    max_len: int
59    summary_show_types: bool
max_len: int
summary_show_types: bool
Inherited Members
builtins.dict
get
setdefault
pop
popitem
keys
items
values
update
fromkeys
clear
copy
class DBGTensorArraySummaryDefaultsType(typing.TypedDict):
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
fmt: Literal['unicode', 'latex', 'ascii']
precision: int
stats: bool
shape: bool
dtype: bool
device: bool
requires_grad: bool
sparkline: bool
sparkline_bins: int
sparkline_logy: Optional[bool]
colored: bool
eq_char: str
Inherited Members
builtins.dict
get
setdefault
pop
popitem
keys
items
values
update
fromkeys
clear
copy
PATH_MODE: Literal['relative', 'absolute'] = 'relative'
DEFAULT_VAL_JOINER: str = ' = '
def dbg( exp: Union[~_ExpType, muutils.dbg._NoExpPassedSentinel] = <muutils.dbg._NoExpPassedSentinel object>, formatter: Optional[Callable[[Any], str]] = None, val_joiner: str = ' = ') -> Union[~_ExpType, muutils.dbg._NoExpPassedSentinel]:
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))
DBG_TENSOR_ARRAY_SUMMARY_DEFAULTS: DBGTensorArraySummaryDefaultsType = {'fmt': 'unicode', 'precision': 2, 'stats': True, 'shape': True, 'dtype': True, 'device': True, 'requires_grad': True, 'sparkline': True, 'sparkline_bins': 7, 'sparkline_logy': None, 'colored': True, 'eq_char': '='}
DBG_TENSOR_VAL_JOINER: str = ': '
def tensor_info(tensor: Any) -> str:
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]
DBG_DICT_DEFAULTS: DBGDictDefaultsType = {'key_types': True, 'val_types': True, 'max_len': 32, 'indent': ' ', 'max_depth': 3}
DBG_LIST_DEFAULTS: DBGListDefaultsType = {'max_len': 16, 'summary_show_types': True}
def list_info(lst: List[Any]) -> str:
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
TENSOR_STR_TYPES: Set[str] = {"<class 'numpy.ndarray'>", "<class 'torch.Tensor'>"}
def dict_info(d: Dict[Any, Any], depth: int = 0) -> str:
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
def info_auto(obj: Any) -> str:
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.

def dbg_tensor(tensor: ~_ExpType) -> ~_ExpType:
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.

def dbg_dict(d: ~_ExpType_dict) -> ~_ExpType_dict:
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.

def dbg_auto(obj: ~_ExpType) -> ~_ExpType:
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.

def grep_repr( obj: Any, pattern: str | re.Pattern[str], *, char_context: int | None = 20, line_context: int | None = None, before_context: int = 0, after_context: int = 0, context: int | None = None, max_count: int | None = None, cased: bool = False, loose: bool = False, line_numbers: bool = False, highlight: bool = True, color: str = '31', separator: str = '--', quiet: bool = False) -> Optional[List[str]]:
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)