Skip to main content

Introspection and stack-walking utilities for the simplibs ecosystem.

Project description

simplibs-introspection

Find what you're looking for in the call stack — safely.

Diagnostic tools that inspect the runtime state without crashing your program. Perfect for logging, error handling, and defensive architecture.

Python Licence PyPI

from simplibs.introspection import extract_caller_info

# You're deep in a library. Find who called you — without crashing.
info = extract_caller_info()
if info:
    print(f"{info['function']} in {info['file']}:{info['line']}")

Why This Library?

You're building a library. Your function is buried three levels deep in helper calls — it needs to know who the actual user is, not which internal function called it directly. You reach for Python's inspect module... and suddenly you're wrestling with stack frames, dynamic frames, exception handling, and hardcoding frame offsets.

simplibs-introspection solves this. It gives you a simple, defensive interface for reading the call stack. Filter out your library's internals. Get the first frame that matters. No crashes, no exceptions — just data or None.


Installation

pip install simplibs-introspection

Quick Start

Level 1: "Who called me?"

The simplest case: you want the immediate caller in user code (skipping your library's internals).

from simplibs.introspection import extract_caller_info

info = extract_caller_info()
# Returns the first frame outside simplibs-introspection

Level 2: "Walk up the call chain"

You need the second or third user frame — useful when tracing ownership through wrapper layers.

from simplibs.introspection import extract_caller_info

# Get the caller's caller
info = extract_caller_info(expected_frames=2)

# Get the third level up
info = extract_caller_info(expected_frames=3)

Level 3: "Hide extra patterns"

When building a wrapper library, you may want to hide your internals too. Pass patterns to skip.

from simplibs.introspection import extract_caller_info

# Skip both simplibs-introspection AND your own internal modules
info = extract_caller_info(
    expected_frames=1,
    excluded_patterns=("my_internal_lib", "decorators")
)

Core Functions

extract_caller_info() — Find the first relevant frame

Walks the call stack and returns information about the first frame that isn't in your exclusion list.

from simplibs.introspection import extract_caller_info

info = extract_caller_info()

if info:
    print(f"File: {info['file']}")              # "main.py"
    print(f"Full path: {info['full_path']}")    # "/home/user/project/main.py"
    print(f"Function: {info['function']}")      # "process_data"
    print(f"Line: {info['line']}")              # 42 (int)
else:
    print("No relevant frame found (rare)")

Return dictionary:

Key Type Meaning
file str Filename without path (basename)
full_path str Absolute file path, platform-native format
line int Line number where the call happened
function str Function name (or <module> for module level)

Parameters:

Parameter Type Default Purpose
expected_frames int 1 Which valid frame to return (1 = first, 2 = second, etc.)
excluded_patterns tuple () Strings that, if found in a path, cause that frame to be skipped

How It Works

The Two-Layer Filter

By default, the function skips two types of frames:

  1. Always excluded: Python's dynamic frames (<string>, <frozen importlib>, etc.)
  2. Auto-excluded: simplibs-introspection itself

But it keeps every other frame.

Then it counts: "How many kept frames have I seen?" When that count reaches expected_frames, it returns that frame.

This means expected_frames=1 always returns the first line of user code, regardless of how many internal wrappers sit above it:

Stack (bottom to top):
0  extract_caller_info        ← skipped (the function itself)
1  library.helper()           ← skipped (your internal code)
2  library.validate()         ← skipped (your internal code)
3  user_code.py:main()        ← frame #1 (valid) → returned
4  user_code.py:wrapper()     ← frame #2 (valid) → would be returned for expected_frames=2

extract_caller_info(expected_frames=1)  # → Returns frame 3
extract_caller_info(expected_frames=2)  # → Returns frame 4

Cross-Platform Paths

The function automatically normalizes paths. Whether you're on Windows or Unix, patterns work consistently:

# Works on both Windows and Unix:
excluded_patterns = ("my_lib/utils", "my_lib/helpers")

info = extract_caller_info(expected_frames=1, excluded_patterns=excluded_patterns)
# On Windows: C:\project\my_lib\utils\tool.py is matched
# On Unix:    /home/user/project/my_lib/utils/tool.py is matched

The Fallout Mechanism: Always Returns Something

When the stack is shallower than expected_frames, the function doesn't return None. Instead, it returns the last frame it examined — usually the program entry point.

This is intentional:

# Stack is only 3 frames deep, but you ask for frame #10
info = extract_caller_info(expected_frames=10)

# You get frame #3 (the deepest)
# Why? Because even if you don't get what you asked for,
# returning *something* is better than silence.

This is especially useful in error handling: even if your code path is shallower than expected, you still get some diagnostic context.


Filtering Your Own Code

When you're building a library, use excluded_patterns to hide your internals:

from simplibs.introspection import extract_caller_info

class MyLibrary:
    def __init__(self):
        self._internal_patterns = ("mylib.internals", "mylib.decorators")
    
    def public_function(self, user_data):
        # Tell the user where they're calling from
        info = extract_caller_info(excluded_patterns=self._internal_patterns)
        
        if info:
            print(f"Called from user code: {info['function']} ({info['file']}:{info['line']})")

Advanced: Custom Frame Selection

Walking up the call chain

Track the flow through multiple user-level function calls:

from simplibs.introspection import extract_caller_info

def first_level():
    """You are here"""
    info = extract_caller_info(expected_frames=1)
    # → Returns the function that called first_level()

def second_level():
    info = extract_caller_info(expected_frames=2)
    # → Returns the function that called the function that called second_level()

Real use case: a logging decorator that needs to know the original user function, not the decorator itself.

def log_calls(func):
    def wrapper(*args, **kwargs):
        # Find the actual user code, not wrapper()
        info = extract_caller_info(expected_frames=2, excluded_patterns=("decorator_lib",))
        
        if info:
            print(f"[LOG] User called {info['function']} at {info['file']}:{info['line']}")
        
        return func(*args, **kwargs)
    return wrapper

@log_calls
def process():
    pass

Excluding multiple patterns

Hide multiple parts of your codebase:

info = extract_caller_info(
    expected_frames=1,
    excluded_patterns=("mylib.core", "mylib.internals", "mylib.helpers")
)

Every frame containing any of these strings is skipped. The first frame not containing them is returned.


Advanced: Error Reporting with Diagnostics

Use stack inspection to provide context in error messages:

from simplibs.introspection import extract_caller_info
from simplibs.exception import SimpleException

def validate_config(config):
    if not isinstance(config, dict):
        # Report where the bad call came from
        info = extract_caller_info()
        location = f"{info['file']}:{info['line']}" if info else "unknown"
        
        raise SimpleException(
            problem=f"config must be dict, got {type(config).__name__}",
            value=config,
            # Include the caller's location in the error context
        )

Error Handling

extract_caller_info() is defensive. It will never raise an exception.

Returns:

  • A dictionary with keys file, full_path, line, function — on success
  • None — only in exceptional circumstances (stack inspection failed internally)

It always degrades gracefully:

result = extract_caller_info()

# Always safe to check:
if result:
    print(f"{result['function']} at {result['file']}:{result['line']}")
else:
    print("Could not inspect stack (very rare)")

Invalid inputs are handled silently:

# Bad input? Just returns None or a frame, never raises
extract_caller_info(expected_frames="not an int")        # → None or frame
extract_caller_info(excluded_patterns="not a tuple")     # → None or frame
extract_caller_info(expected_frames=-999)                # → None or frame

Performance

  • Single-pass stack walk: Iterates the stack once, early exit on success
  • No source code loading: Uses context=0 to skip file I/O
  • Minimal overhead: Only filters and counts, no heavy operations
  • Safe defaults: Filtering is fast string matching

For most use cases, extract_caller_info() takes less than 1ms.


Under the Hood

Why expected_frames instead of fixed offsets?

Old approach: skip_frames=2 means "skip 2 frames and return the 3rd".

Problem: If your library has internal frames, the math breaks:

Real stack:     0  1  2  3  4
Your code:      X  X  X  ✓  ✓
skip_frames=2:  Skip to → 3 ✗ (wrong, that's library internals)

New approach: expected_frames=1 means "return the 1st frame that isn't excluded".

Real stack:     0  1  2  3  4
Your code:      X  X  X  ✓  ✓
Filter:         ×  ×  ×  ✓  ✓
expected_frames=1:  → 3 ✓ (correct, first user frame)

Cross-platform normalization

Paths are normalized via Path().as_posix(), which converts Windows backslashes to forward slashes. This makes pattern matching work consistently:

# Pattern: "mylib/utils"
# Windows path: C:\project\mylib\utils\helper.py → normalized to C:/project/mylib/utils/helper.py
# Unix path:    /home/user/mylib/utils/helper.py → unchanged
# Match result: ✓ Both match

About simplibs

simplibs-introspection is part of the simplibs ecosystem — a collection of small, self-contained Python libraries.

Philosophy

Dyslexia-friendly — minimize mental load. Atomize code into self-contained units, name things after what they do, explain why not just what.

Programmer's zen — nothing missing, nothing superfluous. Better to go slowly and correctly than quickly with mistakes. Gradual refinement towards crystallization.

Defensive style — anticipate failure modes. Degrade gracefully, never raise unexpected errors.

Minimalism — find the shortest path to the goal, but leave nothing out. Each file has one responsibility.

Code as craft — code is a small work of art. Optimize for the reader.

Designed for expansion

Currently simplibs-introspection offers stack frame inspection. As the ecosystem grows, additional introspection tools (call graph analysis, runtime state snapshots, etc.) may be added while keeping the same defensive philosophy.


Reference: Common Patterns

Pattern: Library error reporting with context

from simplibs.introspection import extract_caller_info

def my_library_validate(value):
    if not isinstance(value, str):
        info = extract_caller_info(excluded_patterns=("my_library",))
        location = f"{info['file']}:{info['line']}" if info else "unknown location"
        raise ValueError(f"Expected str at {location}, got {type(value)}")

# User code
my_library_validate(42)
# Error message includes: my_script.py:10

Pattern: Tracing through decorator layers

from simplibs.introspection import extract_caller_info

def traced_decorator(func):
    def wrapper(*args, **kwargs):
        # Find the original caller, skip the decorator wrapper
        info = extract_caller_info(expected_frames=2)
        
        print(f"Trace: {func.__name__} called from {info['function'] if info else '?'}")
        return func(*args, **kwargs)
    return wrapper

@traced_decorator
def my_function():
    pass

# my_function()  →  prints: "Trace: my_function called from <module>"

Pattern: Conditional logging based on depth

from simplibs.introspection import extract_caller_info

def deep_function():
    # Log where we're being called from
    for level in range(1, 4):
        info = extract_caller_info(expected_frames=level)
        if info:
            print(f"Level {level}: {info['function']} in {info['file']}")
        else:
            break

# Shows the call chain: who called who

Pattern: Multi-library environment

from simplibs.introspection import extract_caller_info

# Hide multiple libraries' internals
info = extract_caller_info(
    expected_frames=1,
    excluded_patterns=("mylib", "simplibs", "requests", "flask")
)

if info:
    print(f"First user code: {info['function']} ({info['file']})")

License

MIT.


Tests are part of the repository and serve as living documentation of expected behavior.

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

simplibs_introspection-0.2.0.tar.gz (15.4 kB view details)

Uploaded Source

Built Distribution

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

simplibs_introspection-0.2.0-py3-none-any.whl (12.0 kB view details)

Uploaded Python 3

File details

Details for the file simplibs_introspection-0.2.0.tar.gz.

File metadata

  • Download URL: simplibs_introspection-0.2.0.tar.gz
  • Upload date:
  • Size: 15.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for simplibs_introspection-0.2.0.tar.gz
Algorithm Hash digest
SHA256 786e4a4a5a77c25f7bc7dee8ce4556976117400396c91a2c5c5849d39ec59ce8
MD5 a2fbc66b6f7ef136186b25f1b8fe0d6b
BLAKE2b-256 074f11aea227a0c57a37cf69498f3b0ae546b8fac5b66077bd1106acad94b649

See more details on using hashes here.

File details

Details for the file simplibs_introspection-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for simplibs_introspection-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 228804e12b754efedbdd7a4995cac13663c838735501cbac8a92cd76b88d6568
MD5 5567efffc909460f77d4c4dc6c624d0e
BLAKE2b-256 4ee8f8e037786f2b3f1c1717e3555df586cea8075811f0fa68b55b330b6fbea1

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