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.
Release files for simplibs-introspection 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| simplibs_introspection-0.2.0.tar.gz | 15.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| simplibs_introspection-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 27.5 kB
Release files / simplibs_introspection-0.2.0.tar.gz
| Download URL | simplibs_introspection-0.2.0.tar.gz |
|---|---|
| Size | 15.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
786e4a4a5a77c25f7bc7dee8ce4556976117400396c91a2c5c5849d39ec59ce8
|
|
BLAKE2b-256 checksum How to use checksums |
074f11aea227a0c57a37cf69498f3b0ae546b8fac5b66077bd1106acad94b649
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|
Release files / simplibs_introspection-0.2.0-py3-none-any.whl
| Download URL | simplibs_introspection-0.2.0-py3-none-any.whl |
|---|---|
| Size | 12.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
228804e12b754efedbdd7a4995cac13663c838735501cbac8a92cd76b88d6568
|
|
BLAKE2b-256 checksum How to use checksums |
4ee8f8e037786f2b3f1c1717e3555df586cea8075811f0fa68b55b330b6fbea1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|