Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ jobs:
strategy:
matrix:
include:
- python-version: "3.9"
toxenv: py39
- python-version: "3.10"
toxenv: py310
- python-version: "3.11"
Expand Down
10 changes: 9 additions & 1 deletion spewer/spewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,19 @@ def spew(
show_values=show_values,
functions_only=functions_only,
)
sys.settrace(TraceHook(config))
hook = TraceHook(config)

# Use setprofile for functions_only mode to capture built-ins
if functions_only:
sys.setprofile(hook)
else:
sys.settrace(hook)


def unspew() -> None:
"""Remove the trace hook installed by spew."""
sys.settrace(None)
sys.setprofile(None) # Clear both


class SpewContext:
Expand Down Expand Up @@ -53,3 +60,4 @@ def __enter__(self):

def __exit__(self, exc_type, exc_val, exc_tb):
unspew()
return False # Don't suppress exceptions
18 changes: 14 additions & 4 deletions spewer/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,25 @@ def __init__(self, config: SpewConfig):

def __call__(self, frame: Any, event: str, arg: Any) -> TraceHook:
"""Trace hook callback that processes execution events."""
if self.config.functions_only and event == "call":
self._handle_function_call(frame)

if self.config.functions_only and event in ("call", "c_call"):
self._handle_function_call(frame, event, arg)
elif not self.config.functions_only and event == "line":
self._handle_line_execution(frame)

return self

def _handle_function_call(self, frame: Any) -> None:
"""Handle function call events."""
def _handle_function_call(self, frame: Any, event: str, arg: Any) -> None:
"""Handle function call events including built-in functions."""
# Handle C/built-in function calls
if event == "c_call":
if arg is not None:
func_name = getattr(arg, "__name__", "<unknown>")
module = getattr(arg, "__module__", "<unknown>")
print(f"{module}: {func_name}()")
return

# Handle regular Python function calls
lineno = frame.f_lineno
func_name = frame.f_code.co_name

Expand Down
310 changes: 310 additions & 0 deletions tests/test_builtin_tracing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
import contextlib
import sys
from io import StringIO # Add this
from pathlib import Path

from spewer import spew, unspew


def test_builtin_functions_traced(capsys):
"""Test that built-in functions with valid arg are traced - covers lines 41-45"""
spew(functions_only=True)

# Call built-in functions without assignment (no unused variable warnings)
len([1, 2, 3])
max([1, 2, 3])
min([1, 2, 3])

unspew()

captured = capsys.readouterr()
output_str = captured.out
assert "len" in output_str or "builtins" in output_str


def test_builtin_with_exception(capsys):
"""Test built-in functions that raise exceptions"""
spew(functions_only=True)

with contextlib.suppress(ValueError):
max([]) # Raises ValueError

unspew()
captured = capsys.readouterr()
assert "max" in captured.out or "builtins" in captured.out


def test_multiple_builtins_sequence(capsys):
"""Test multiple built-in functions called in sequence"""
spew(functions_only=True)

len([1, 2, 3])
max([1, 2, 3])
min([1, 2, 3])
abs(-5)

unspew()
captured = capsys.readouterr()
result = captured.out
# At least one built-in should be traced
assert any(func in result for func in ["len", "max", "min", "abs", "builtins"])


def test_nested_builtin_and_python(capsys):
"""Test nested calls mixing built-in and Python functions"""

def custom_func(x):
return len(x)

spew(functions_only=True)
custom_func([1, 2, 3])
unspew()

captured = capsys.readouterr()
output_str = captured.out
assert "custom_func" in output_str or "len" in output_str


def test_inspect_getsourcelines_fallback(capsys):
"""Test OSError exception handling when source unavailable - covers line 84"""
spew(show_values=True)

# Built-in functions don't have source lines, triggering OSError
Path.cwd() # Use Path.cwd() instead of os.getcwd()

unspew()
# Should handle gracefully without crashing
capsys.readouterr() # Clear output, don't assign


def test_show_values_with_line_execution(capsys):
"""Test variable value display during line execution - covers line 95"""
spew(show_values=True)

# Execute code that will call _show_variable_values
x = 10
y = 20
# Use the variables immediately to avoid lint warnings
assert x + y == 30

unspew()

# Should show variable values
captured = capsys.readouterr()
output_str = captured.out
# Variable values should appear in output
assert len(output_str) > 0


def test_global_variable_inspection(capsys):
"""Test displaying global variables in details - covers line 118"""
# Create module-level code that uses globals
spew(show_values=True)

# This will trigger global variable inspection
global_test = 42
local_test = global_test + 10
assert local_test == 52

unspew()

captured = capsys.readouterr()
# Should have some output
assert len(captured.out) > 0


def test_custom_function_with_builtins(capsys):
"""Test custom function that uses built-in functions"""

def custom_function():
data = [1, 2, 3]
len(data)
max(data)
return True

spew(functions_only=True)
custom_function()
unspew()

captured = capsys.readouterr()
result = captured.out
# Should trace either the custom function or built-ins
assert len(result) > 0


def test_c_call_event_with_arg(capsys):
"""Test c_call event handling with valid arg - covers lines 39-43"""
# Store original profile function
old_profile = sys.getprofile()

spew(functions_only=True)

# These should trigger c_call events
len([1, 2, 3])
print("test") # print is a built-in
str(123)

unspew()

# Restore original profile
if old_profile:
sys.setprofile(old_profile)

captured = capsys.readouterr()
# Should have traced built-in functions
output = captured.out
assert len(output) > 0


def test_source_line_unavailable(capsys):
"""Test when source lines are unavailable - covers line 81"""
spew(show_values=True)

# Call functions from compiled modules
_ = sys.version_info # Assign to _ to avoid "useless expression" warning

# Call a C function that has no source
sorted([3, 1, 2])

unspew()
capsys.readouterr() # Clear output, don't assign


def test_show_variable_values_call(capsys):
"""Test _show_variable_values is called - covers line 92"""
spew(show_values=True, functions_only=False)

# Execute multiple lines to trigger variable inspection
test_var = 100
another_var = 200
result = test_var + another_var
assert result == 300

unspew()
captured = capsys.readouterr()
output = captured.out
# Should show variable values
assert len(output) > 0


def test_frame_globals_access(capsys):
"""Test accessing frame globals - covers line 115"""
spew(show_values=True, functions_only=False)

# Create code that accesses globals
test_global = 999
local_value = test_global * 2
assert local_value == 1998

unspew()
capsys.readouterr() # Clear output, don't assign


def test_builtin_print_traced(capsys):
"""Ensure print() itself is traced as a built-in"""

# Redirect stdout temporarily
old_stdout = sys.stdout
sys.stdout = StringIO()

spew(functions_only=True)
print("Hello")
unspew()

sys.stdout = old_stdout
capsys.readouterr() # Clear output, don't assign


def test_multiple_c_call_events(capsys):
"""Test multiple c_call events in sequence"""
spew(functions_only=True)

# Chain multiple built-in calls
data = [5, 2, 8, 1]
len(data)
max(data)
min(data)
sorted(data)
sum(data)

unspew()
captured = capsys.readouterr()
output = captured.out
# Should have some trace output
assert isinstance(output, str)


def test_verify_setprofile_active():
"""Verify sys.setprofile is actually set when using functions_only"""
spew(functions_only=True)

# Check if profile hook is installed
profile_func = sys.getprofile()
assert profile_func is not None, "sys.setprofile should be set"

# Call a built-in
len([1, 2, 3])

unspew()

# Profile should be cleared
assert sys.getprofile() is None


def test_debug_c_call_events():
"""Debug test to see if c_call events are actually triggered"""
# Track what events we receive
events_received = []

def debug_profiler(_frame, event, arg): # Prefix unused arg with _
events_received.append((event, arg))
return debug_profiler

# Set up profiler manually
sys.setprofile(debug_profiler)

# Call a built-in
len([1, 2, 3])
max([5, 2, 8])

# Clean up
sys.setprofile(None)

# Check what events we got
print(f"\nEvents received: {len(events_received)}")
for event, arg in events_received:
print(f"Event: {event}, Arg type: {type(arg).__name__}, Arg: {arg}")

# We should have received c_call events
c_call_events = [e for e in events_received if e[0] == "c_call"]
print(f"\nc_call events: {len(c_call_events)}")

assert len(c_call_events) > 0, "Should have received c_call events"


def test_spew_captures_builtin_functions(capsys):
"""Direct test that spew() with functions_only captures built-ins"""
# Use spew
spew(functions_only=True)

# Verify setprofile is active
assert sys.getprofile() is not None, "setprofile should be set"

# Call built-ins
len([1, 2, 3])
max([5, 2, 8])
print("test")

unspew()

captured = capsys.readouterr()
output = captured.out

print(f"\nCaptured output:\n{output}")
print(f"Output length: {len(output)}")

# Should have captured built-in function calls
assert len(output) > 0, "Should have captured some output"
assert "len" in output or "max" in output or "builtins" in output, (
f"Should have traced built-in functions. Got: {output}"
)
4 changes: 2 additions & 2 deletions tests/test_spewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import inspect

import pytest
import pytest # type: ignore[import-untyped]

from spewer import SpewConfig, SpewContext, TraceHook, spew, unspew

Expand Down Expand Up @@ -262,7 +262,7 @@ def __init__(self):
frame = MockFrame()

# This should handle the unknown file case gracefully
hook._handle_function_call(frame)
hook._handle_function_call(frame, "call", None)


class TestSpewContext:
Expand Down