snoop Python Debugging Toolkit

repository·main·Indexed 23 days ago

https://github.com/alexmojaki/snoop

A featureful Python debugging toolkit providing enhanced tracing via the @snoop decorator, advanced pretty-printing with pp, and deep subexpression inspection via pp.deep. It includes capabilities for monitoring specific expressions with watch and watch_explode, integration with the birdseye debugger via @spy, and support for IPython/Jupyter cell magics. snoop can be configured globally through snoop.install() to manage output destinations, formatting, and variable inspection metadata.

Tokens
2K
Snippets
8
Records
14
Agent score
31%

What's inside snoop

  1. Use multiple independent `snoop` configurations

    main
    If snoop.install() is too global, you can instantiate a snoop.Config object to manage independent debugging streams (e.g., writing different traces to different files). The Config object provides its own snoop, pp, and spy methods which follow the same configuration rules as the global functions.
  2. Disable `snoop` effects globally

    main
    To keep snoop decorators in your code but prevent them from executing (e.g., in production environments), call snoop.install(enabled=False). This minimizes performance impact and suppresses all output. You can re-enable them dynamically at any time by calling snoop.install(enabled=True).
  3. Integrate `snoop` with IPython and Jupyter

    main

    You can use snoop in Jupyter notebooks or IPython shells using cell magics.

    1. Load the extension:
      • In a notebook cell: %load_ext snoop
      • In ipython_config.py: c.InteractiveShellApp.extensions = ['snoop']
    2. Use the magic at the top of a cell: %%snoop
  4. Global setup with `snoop.install()`

    main

    To make snoop, pp, and spy available in every file of your project without explicit imports, call snoop.install() early in your application's lifecycle.

    Customizing names: You can rename the injected functions using keyword arguments <original name>=<new name>. For example, snoop.install(snoop="ss") allows you to use @ss as a decorator.

    Disabling injection: If you want to use install() for configuration but do not want the functions injected into your global namespace, pass builtins=False.

    import snoop
    
    snoop.install()
    # Or with custom names:
    snoop.install(snoop="ss")
  5. Configure `snoop` output destination and format

    main

    The snoop.install() function accepts several arguments to control how debugging information is displayed:

    • out: The output destination.
      • A str or Path to append to a file. Use overwrite=True to clear the file first.
      • An object with a .write() method (e.g., sys.stdout).
      • A callable with a single string argument (e.g., logger.info).
    • color: Controls syntax highlighting. Pass True, False, or a Pygments style name (e.g., 'monokai').
    • prefix: A string prepended to every line of output (useful for grep).
    • columns: A space-separated string or list of column names to include at the start of each line. Available columns:
      • time: Current time.
      • thread: Thread name.
      • thread_ident: Thread identifier.
      • file: Filename.
      • full_file: Full file path.
      • function: Function name.
      • function_qualname: Qualified function name.
    • pformat: The pretty-formatting function to use.
  6. Use @snoop to trace function execution

    main

    The @snoop decorator provides a play-by-play log of a function, showing which lines ran and when local variables changed. You can use it as a decorator or as a context manager for specific blocks of code.

    To use the decorator, import snoop and apply @snoop to your function. If you prefer not to use the magical import, you can use snoop.snoop or from snoop import snoop.

    To trace only a specific part of a function, wrap that section in a with snoop: block.

    import snoop
    
    @snoop
    def number_to_bits(number):
        if number:
            bits = []
            while number:
                number, remainder = divmod(number, 2)
                bits.insert(0, remainder)
            return bits
        else:
            return [0]
    
    number_to_bits(6)
  7. Combine snoop and birdseye with @spy

    main

    The @spy decorator combines @snoop with the birdseye debugger. This allows you to get snoop-style logs and, if needed, open the birdseye UI for even deeper inspection without rerunning your code.

    To use @spy, you must install birdseye separately via pip install birdseye. @spy accepts the same arguments as @snoop (e.g., @spy(depth=2, watch='x.y')).

    Note: @spy significantly reduces performance and should be avoided for functions with many loop iterations.

    from snoop import spy
    
    @spy
    def foo():
        # ... code ...
        pass
  8. Trace subexpressions with pp.deep

    main

    If you want to see the evaluation of every intermediate subexpression within a complex expression, use pp.deep(lambda: <expression>). This logs every step in the correct order without side effects and returns the final value. It is also useful for identifying exactly which subexpression caused an exception.

    from snoop import pp
    # Traces every step of the calculation
    pp.deep(lambda: x + 1 + max(y + 2, y + 3))
  9. Use pp for awesome print debugging

    main

    The pp function is a powerful version of print that outputs x = <pretty printed value of x>. It shows the source code of its arguments and uses pprint.pformat (or prettyprinter/pprintpp if installed) to format complex data structures.

    pp returns its arguments directly, allowing it to be inserted into existing expressions. If multiple arguments are provided, it returns them as a tuple.

    Example of nested pp calls:

    from snoop import pp
    x = 1
    y = 2
    pp(pp(x + 1) + max(*pp(y + 2, y + 3)))
  10. Configure snoop with depth, watch, and watch_explode

    main

    You can pass arguments to @snoop to customize the level of detail in your logs:

    • depth: An integer specifying how many levels deep to step into inner function calls. The default is 1 (no inner calls).
    • watch: A tuple of strings representing arbitrary expressions to monitor. For example: @snoop(watch=('foo.bar', 'self.x["whatever"]')).
    • watch_explode: A list of variables or expressions to expand, showing all their attributes or items (e.g., list/dict contents). For example: @snoop(watch_explode=['foo', 'self']).

    Example of using depth to trace into a memoizing decorator:

    import snoop
    
    def cache(func):
        d = {}
        def wrapper(*args):
            try:
                return d[args]
            except KeyError:
                result = d[args] = func(*args)
                return result
        return wrapper
    
    @snoop(depth=2)
    @cache
    def add(x, y):
        return x + y
    
    add(1, 2)
    @snoop(depth=2)
    @cache
    def add(x, y):
        return x + y