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.
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:
- Always excluded: Python's dynamic frames (
<string>,<frozen importlib>, etc.) - Auto-excluded:
simplibs-introspectionitself
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=0to 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
786e4a4a5a77c25f7bc7dee8ce4556976117400396c91a2c5c5849d39ec59ce8
|
|
| MD5 |
a2fbc66b6f7ef136186b25f1b8fe0d6b
|
|
| BLAKE2b-256 |
074f11aea227a0c57a37cf69498f3b0ae546b8fac5b66077bd1106acad94b649
|
File details
Details for the file simplibs_introspection-0.2.0-py3-none-any.whl.
File metadata
- Download URL: simplibs_introspection-0.2.0-py3-none-any.whl
- Upload date:
- Size: 12.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
228804e12b754efedbdd7a4995cac13663c838735501cbac8a92cd76b88d6568
|
|
| MD5 |
5567efffc909460f77d4c4dc6c624d0e
|
|
| BLAKE2b-256 |
4ee8f8e037786f2b3f1c1717e3555df586cea8075811f0fa68b55b330b6fbea1
|