Skip to content

Commit 53ba658

Browse files
Merge pull request #8 from AnshKumarTripathi/feature/issue-3-return-exception-events
feat: Add return and exception event tracing
2 parents 96e08bc + 8cf3bfc commit 53ba658

5 files changed

Lines changed: 796 additions & 9 deletions

File tree

README.md

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ A Python debugging library for detailed code execution tracing. This library pro
1111

1212
- **Line-by-line execution tracing**: See exactly which lines are being executed
1313
- **Function/method call tracing**: Trace only function and method calls without line details
14+
- **Return event tracing**: Track function return values and completion
15+
- **Exception event tracing**: Monitor exception raising and handling
1416
- **Variable value inspection**: View the values of variables at each execution step
1517
- **Module filtering**: Trace only specific modules or all modules
1618
- **Context manager support**: Use with `with` statements for automatic cleanup
@@ -119,20 +121,87 @@ This will output:
119121
```
120122
__main__:15: my_function()
121123
args: x=10, y=20
124+
__main__:17: my_function() -> 30
125+
```
126+
127+
### Return Event Tracing
128+
129+
```python
130+
from spewer import SpewContext
131+
132+
# Trace function calls and returns
133+
with SpewContext(functions_only=True, show_values=True, trace_returns=True):
134+
def calculate(x, y):
135+
return x * y + 10
136+
137+
result = calculate(5, 3)
138+
```
139+
140+
This will output:
141+
```
142+
__main__:15: calculate()
143+
args: x=5, y=3
144+
__main__:16: calculate() -> 25
145+
```
146+
147+
### Exception Event Tracing
148+
149+
```python
150+
from spewer import SpewContext
151+
152+
# Trace function calls and exceptions
153+
with SpewContext(functions_only=True, show_values=True, trace_exceptions=True):
154+
def risky_function(x, y):
155+
if y == 0:
156+
raise ValueError("Cannot divide by zero")
157+
return x / y
158+
159+
try:
160+
result = risky_function(10, 0)
161+
except ValueError as e:
162+
print(f"Caught: {e}")
163+
```
164+
165+
This will output:
166+
```
167+
__main__:15: risky_function()
168+
args: x=10, y=0
169+
__main__:16: risky_function() -> ValueError('Cannot divide by zero')
170+
```
171+
172+
### Disable Specific Event Types
173+
174+
```python
175+
from spewer import SpewContext
176+
177+
# Only trace function calls, not returns or exceptions
178+
with SpewContext(functions_only=True, trace_returns=False, trace_exceptions=False):
179+
def my_function():
180+
return 42
181+
182+
result = my_function()
183+
```
184+
185+
This will output:
186+
```
187+
__main__:15: my_function()
188+
# No return event traced
122189
```
123190

124191
## API Reference
125192

126193
### Functions
127194

128-
#### `spew(trace_names=None, show_values=False, functions_only=False)`
195+
#### `spew(trace_names=None, show_values=False, functions_only=False, trace_returns=False, trace_exceptions=False)`
129196

130197
Install a trace hook which writes detailed logs about code execution.
131198

132199
**Parameters:**
133200
- `trace_names` (Optional[List[str]]): List of module names to trace. If None, traces all modules.
134-
- `show_values` (bool): Whether to show variable values during tracing.
135-
- `functions_only` (bool): Whether to trace only function/method calls instead of line-by-line execution.
201+
- `show_values` (bool): Whether to show variable values during tracing. Default: False.
202+
- `functions_only` (bool): Whether to trace only function/method calls instead of line-by-line execution. Default: False.
203+
- `trace_returns` (bool): Whether to trace function return events. Default: False.
204+
- `trace_exceptions` (bool): Whether to trace exception events. Default: False.
136205

137206
#### `unspew()`
138207

@@ -142,23 +211,27 @@ Remove the trace hook installed by `spew()`.
142211

143212

144213

145-
#### `SpewContext(trace_names=None, show_values=False, functions_only=False)`
214+
#### `SpewContext(trace_names=None, show_values=False, functions_only=False, trace_returns=False, trace_exceptions=False)`
146215

147216
Context manager for automatic spew/unspew operations.
148217

149218
**Parameters:**
150219
- `trace_names` (Optional[List[str]]): List of module names to trace. If None, traces all modules.
151-
- `show_values` (bool): Whether to show variable values during tracing.
152-
- `functions_only` (bool): Whether to trace only function/method calls instead of line-by-line execution.
220+
- `show_values` (bool): Whether to show variable values during tracing. Default: False.
221+
- `functions_only` (bool): Whether to trace only function/method calls instead of line-by-line execution. Default: False.
222+
- `trace_returns` (bool): Whether to trace function return events. Default: False.
223+
- `trace_exceptions` (bool): Whether to trace exception events. Default: False.
153224

154-
#### `SpewConfig(trace_names=None, show_values=True, functions_only=False)`
225+
#### `SpewConfig(trace_names=None, show_values=True, functions_only=False, trace_returns=False, trace_exceptions=False)`
155226

156227
Configuration class for spewer debugging. Provides validation and centralized configuration management.
157228

158229
**Parameters:**
159230
- `trace_names` (Optional[List[str]]): List of module names to trace. If None, traces all modules.
160-
- `show_values` (bool): Whether to show variable values during tracing.
161-
- `functions_only` (bool): Whether to trace only function/method calls instead of line-by-line execution.
231+
- `show_values` (bool): Whether to show variable values during tracing. Default: True.
232+
- `functions_only` (bool): Whether to trace only function/method calls instead of line-by-line execution. Default: False.
233+
- `trace_returns` (bool): Whether to trace function return events. Default: False.
234+
- `trace_exceptions` (bool): Whether to trace exception events. Default: False.
162235

163236
#### `TraceHook(config)`
164237

@@ -169,6 +242,8 @@ Core trace hook implementation. This is the low-level class that handles the act
169242

170243
## Example Output
171244

245+
### Line-by-Line Tracing
246+
172247
When tracing with `show_values=True`, you'll see output like:
173248

174249
```
@@ -182,6 +257,20 @@ __main__:18: print(f"Result: {result}")
182257
result=30
183258
```
184259

260+
### Function-Only Tracing with Returns and Exceptions
261+
262+
When tracing with `functions_only=True, show_values=True, trace_returns=True, trace_exceptions=True`:
263+
264+
```
265+
__main__:15: calculate()
266+
args: x=10, y=5
267+
__main__:16: calculate() -> 15
268+
269+
__main__:20: risky_function()
270+
args: x=10, y=0
271+
__main__:22: risky_function() -> ValueError('division by zero')
272+
```
273+
185274
## Notes
186275

187276
- The library uses Python's `sys.settrace()` which can impact performance

spewer/config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ class SpewConfig:
1313
trace_names: Optional[list[str]] = None
1414
show_values: bool = True
1515
functions_only: bool = False
16+
trace_returns: bool = False
17+
trace_exceptions: bool = False
1618

1719
def __post_init__(self):
1820
"""Validate configuration after initialization."""
@@ -27,3 +29,11 @@ def __post_init__(self):
2729
if not isinstance(self.functions_only, bool):
2830
msg = "functions_only must be a boolean"
2931
raise TypeError(msg)
32+
33+
if not isinstance(self.trace_returns, bool):
34+
msg = "trace_returns must be a boolean"
35+
raise TypeError(msg)
36+
37+
if not isinstance(self.trace_exceptions, bool):
38+
msg = "trace_exceptions must be a boolean"
39+
raise TypeError(msg)

spewer/spewer.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ def spew(
1313
trace_names: Optional[list[str]] = None,
1414
show_values: bool = False,
1515
functions_only: bool = False,
16+
trace_returns: bool = False,
17+
trace_exceptions: bool = False,
1618
) -> None:
1719
"""Install a trace hook for detailed code execution logging."""
1820
config = SpewConfig(
1921
trace_names=trace_names,
2022
show_values=show_values,
2123
functions_only=functions_only,
24+
trace_returns=trace_returns,
25+
trace_exceptions=trace_exceptions,
2226
)
2327
hook = TraceHook(config)
2428

@@ -43,18 +47,24 @@ def __init__(
4347
trace_names: Optional[list[str]] = None,
4448
show_values: bool = False,
4549
functions_only: bool = False,
50+
trace_returns: bool = False,
51+
trace_exceptions: bool = False,
4652
):
4753
self.config = SpewConfig(
4854
trace_names=trace_names,
4955
show_values=show_values,
5056
functions_only=functions_only,
57+
trace_returns=trace_returns,
58+
trace_exceptions=trace_exceptions,
5159
)
5260

5361
def __enter__(self):
5462
spew(
5563
self.config.trace_names,
5664
self.config.show_values,
5765
self.config.functions_only,
66+
self.config.trace_returns,
67+
self.config.trace_exceptions,
5868
)
5969
return self
6070

spewer/trace.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ def __call__(self, frame: Any, event: str, arg: Any) -> TraceHook:
2626
self._handle_function_call(frame, event, arg)
2727
elif not self.config.functions_only and event == "line":
2828
self._handle_line_execution(frame)
29+
elif event == "return" and self.config.trace_returns:
30+
self._handle_line_return(frame, arg)
31+
elif event == "exception" and self.config.trace_exceptions:
32+
self._handle_line_exception(frame, arg)
2933

3034
return self
3135

@@ -118,3 +122,105 @@ def _show_variable_values(self, frame: Any, line: str) -> None:
118122

119123
if details:
120124
print(f"\t{' '.join(details)}")
125+
126+
def _handle_function_return(self, frame: Any, arg: Any) -> None:
127+
"""Handle function return events."""
128+
lineno = frame.f_lineno
129+
func_name = frame.f_code.co_name
130+
131+
# Get filename and handle compiled files
132+
if "__file__" in frame.f_globals:
133+
filename = frame.f_globals["__file__"]
134+
if filename.endswith((".pyc", ".pyo")):
135+
filename = filename[:-1]
136+
name = frame.f_globals["__name__"]
137+
else:
138+
name = "[unknown]"
139+
filename = "[unknown]"
140+
141+
# Check if we should trace this module
142+
if self.config.trace_names is None or name in self.config.trace_names:
143+
if self.config.show_values:
144+
print(f"{name}:{lineno}: {func_name}() -> {arg!r}")
145+
else:
146+
print(f"{name}:{lineno}: {func_name}() -> <return>")
147+
148+
def _handle_function_exception(self, frame: Any, arg: Any) -> None:
149+
"""Handle function exception events."""
150+
lineno = frame.f_lineno
151+
func_name = frame.f_code.co_name
152+
153+
# Get filename and handle compiled files
154+
if "__file__" in frame.f_globals:
155+
filename = frame.f_globals["__file__"]
156+
if filename.endswith((".pyc", ".pyo")):
157+
filename = filename[:-1]
158+
name = frame.f_globals["__name__"]
159+
else:
160+
name = "[unknown]"
161+
filename = "[unknown]"
162+
163+
# Check if we should trace this module
164+
if self.config.trace_names is None or name in self.config.trace_names:
165+
if self.config.show_values:
166+
exc_type, exc_value, _ = arg
167+
print(
168+
f"{name}:{lineno}: {func_name}() -> {exc_type.__name__}({exc_value!r})"
169+
)
170+
else:
171+
print(f"{name}:{lineno}: {func_name}() -> <exception>")
172+
173+
def _handle_line_return(self, frame: Any, arg: Any) -> None:
174+
"""Handle line return events."""
175+
lineno = frame.f_lineno
176+
177+
# Get filename and handle compiled files
178+
if "__file__" in frame.f_globals:
179+
filename = frame.f_globals["__file__"]
180+
if filename.endswith((".pyc", ".pyo")):
181+
filename = filename[:-1]
182+
name = frame.f_globals["__name__"]
183+
line = linecache.getline(filename, lineno)
184+
else:
185+
name = "[unknown]"
186+
try:
187+
src = inspect.getsourcelines(frame)
188+
line = src[lineno]
189+
except OSError:
190+
line = f"Unknown code named [{frame.f_code.co_name}]. VM instruction #{frame.f_lasti}"
191+
192+
# Check if we should trace this module
193+
if self.config.trace_names is None or name in self.config.trace_names:
194+
if self.config.show_values:
195+
print(f"{name}:{lineno}: {line.rstrip()} -> {arg!r}")
196+
else:
197+
print(f"{name}:{lineno}: {line.rstrip()} -> <return>")
198+
199+
def _handle_line_exception(self, frame: Any, arg: Any) -> None:
200+
"""Handle line exception events."""
201+
lineno = frame.f_lineno
202+
203+
# Get filename and handle compiled files
204+
if "__file__" in frame.f_globals:
205+
filename = frame.f_globals["__file__"]
206+
if filename.endswith((".pyc", ".pyo")):
207+
filename = filename[:-1]
208+
name = frame.f_globals["__name__"]
209+
line = linecache.getline(filename, lineno)
210+
else:
211+
name = "[unknown]"
212+
try:
213+
src = inspect.getsourcelines(frame)
214+
line = src[lineno]
215+
except OSError:
216+
line = f"Unknown code named [{frame.f_code.co_name}]. VM instruction #{frame.f_lasti}"
217+
218+
# Check if we should trace this module
219+
if self.config.trace_names is None or name in self.config.trace_names:
220+
if self.config.show_values:
221+
exc_type, exc_value, _ = arg
222+
print(
223+
f"{name}:{lineno}: {line.rstrip()} -> {exc_type.__name__}({exc_value!r})"
224+
)
225+
else:
226+
print(f"{name}:{lineno}: {line.rstrip()} -> <exception>")

0 commit comments

Comments
 (0)