Skip to main content

A Python package for capturing and comparing function input/output snapshots

Project description

Detective Snapshot 🕵️‍♂️🔍

A beautifully simple Python debugging tool with one purpose: capturing function inputs and outputs to files. When your code works perfectly four out of five times but mysteriously fails on that fifth run, you need to know what changed - not start another debugging session.

Just add @snapshot and set DEBUG=true. That's it. Every function call creates a clean JSON snapshot file, capturing all decorated inner function calls in a single file. No setup, no terminal noise - just clear, searchable records of exactly what went in and what came out.

Perfect for:

  • Catching that one input that led to an unexpected output
  • Tracking data transformations across your function chain
  • Having a permanent, searchable record of function behavior
  • Debugging nested function calls without print statement noise
  • Understanding exactly what changed between good and bad runs

Features

  • 📸 Capture function inputs and outputs
  • 🌳 Track nested function calls
  • 🎯 Select specific fields to snapshot
  • 📦 Support for Python objects, dataclasses, and protobufs
  • 💥 Capture exception details

Installation

pip install detective-snapshot

Quick Start

Enable debug mode by setting either environment variable:

export DEBUG=true
# or
export DETECTIVE=true

With debug mode on, each call to an outermost decorated function creates a new snapshot file under ./_snapshots/ with a timestamp and unique hash.

Here's a simple example using a library catalog system:

from detective import snapshot

@snapshot()
def get_book_details(book):
    author = get_author(book["author_id"])
    return f"{book['title']} by {author}"

@snapshot()
def get_author(author_id):
    # Simulate database lookup
    return "J.K. Rowling"

# Use the functions
book = {
    "title": "Harry Potter",
    "author_id": "jkr_001"
}
result = get_book_details(book)

This will create a debug file in ./_snapshots/ with content like:

{
    "FUNCTION": "get_book_details",
    "INPUTS": {
        "book": {
            "title": "Harry Potter",
            "author_id": "jkr_001"
        }
    },
    "OUTPUT": "Harry Potter by J.K. Rowling",
    "CALLS": [
        {
            "FUNCTION": "get_author",
            "INPUTS": {
                "author_id": "jkr_001"
            },
            "OUTPUT": "J.K. Rowling"
        }
    ]
}

Field Selection

Detective Snapshot supports both its own simple field selection syntax and full JSONPath expressions out of the box. You can capture specific fields using various selection patterns:

@snapshot(
    input_fields=["book.title", "book.author_id"],
    output_fields=["name"]
)
def process_book(book):
    # Only specified fields will be captured
    pass

Supported Field Selection Patterns

Pattern Example Description
Direct Field name Select a field directly from root
Nested Field user.address.city Navigate through nested objects
Array Index books[0].title Select specific array element
Array Wildcard books[*].title Select field from all array elements
Multiple Fields user.(name,age) Select multiple fields from an object
Wildcard Object users.*.name Select field from all child objects
Args Syntax args[0].name Select from function arguments
Mixed Access users[*].addresses.*.city Combine array and object access
JSONPath $.users[?(@.age > 18)].name Use full JSONPath expressions

For more examples of field selection patterns, check out our test files - particularly test_snapshot_fields_selection.py which contains comprehensive examples of different selection patterns and edge cases.

Advanced Usage

Capture Complex Objects

@dataclass
class Book:
    title: str
    author: str
    chapters: List[Chapter]

@snapshot(input_fields=["book.chapters[*].title"])
def get_chapter_titles(book: Book):
    return [chapter.title for chapter in book.chapters]

Handle Nested Function Calls

@snapshot()
def process_library(library):
    books = get_books(library.id)
    return categorize_books(books)

@snapshot()
def get_books(library_id):
    return ["Book1", "Book2"]

@snapshot()
def categorize_books(books):
    return {"fiction": books}

The debug file will include the complete call hierarchy with inputs and outputs for each function.

Exception Handling

If an exception occurs within a function decorated with @snapshot, Detective Snapshot will capture the exception details. The output will include an error field containing the exception type and message. Nested function calls that also raise exceptions will have their exceptions captured within the CALLS section of the parent function.

@snapshot()
def outer_function():
    try:
        inner_function()
    except ValueError:
        pass

@snapshot()
def inner_function():
    raise ValueError("This is an example error.")

outer_function()

This will produce a snapshot similar to:

{
    "FUNCTION": "outer_function",
    "INPUTS": {},
    "OUTPUT": null, // or {} depending on if there's a return before the exception
    "CALLS": [
        {
            "FUNCTION": "inner_function",
            "INPUTS": {},
            "OUTPUT": {
                "error": {
                    "type": "ValueError",
                    "message": "This is an example error."
                }
            }
        }
    ]
}

If the outermost function raises the exception, the OUTPUT will contain the error.

Class, Instance, and Static Methods

Detective Snapshot works seamlessly with class methods, instance methods, and static methods.

  • Class Methods: The cls parameter (the class itself) will be captured in the INPUTS. Only non-internal, non-callable, and non-decorator attributes of the class are included.
  • Instance Methods: The self parameter (the instance) will be captured. If self has a __dict__ attribute, it will be captured; otherwise, Detective Snapshot attempts to serialize it.
  • Static Methods: Static methods are treated like regular functions.

Here's an example demonstrating all three:

class MyClass:
    class_variable = "I'm a class variable!"

    def __init__(self, value):
        self.instance_variable = value

    @snapshot()
    def instance_method(self, x):
        return self.static_method(x + self.instance_variable)

    @classmethod
    @snapshot()
    def class_method(cls, y):
        return y * 2

    @staticmethod
    @snapshot()
    def static_method(z):
        return z * 3

instance = MyClass(10)
instance.instance_method(5)
MyClass.class_method(8)

The resulting snapshots will capture the relevant self, cls, and other parameters for each method type.

Contributing

Contributions are welcome! Please check out our Contributing Guide for details.

License

MIT License - see LICENSE for details.

License

MIT License - see LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

detective_snapshot-0.1.6.tar.gz (23.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

detective_snapshot-0.1.6-py3-none-any.whl (28.3 kB view details)

Uploaded Python 3

File details

Details for the file detective_snapshot-0.1.6.tar.gz.

File metadata

  • Download URL: detective_snapshot-0.1.6.tar.gz
  • Upload date:
  • Size: 23.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.8

File hashes

Hashes for detective_snapshot-0.1.6.tar.gz
Algorithm Hash digest
SHA256 0f9a484c2e23cf9ef5f1b93e5d43cf4cd064ce548f3a8f185ecf889c6bf34909
MD5 7696b55e10fbcc57b4e5d11775afeb52
BLAKE2b-256 cff64f696e90bc411ef59834750ef1394335ab5e5d2197538b4523f68c3e94dd

See more details on using hashes here.

File details

Details for the file detective_snapshot-0.1.6-py3-none-any.whl.

File metadata

File hashes

Hashes for detective_snapshot-0.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 5ff74b698ccb654fe246202a41375a1db24a9b7505ef59bd705a21ac8786343d
MD5 705d64d7bf6d1faa9e6f052abae6d34a
BLAKE2b-256 70152a8e39a88cfe40a50e77d72cf7765cfaeff50beb1d978b2a66da7677e664

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page