variable changes are diffed and printed on the next trace event. the last
line of a with block has no next event before __exit__, so its new/modified
variables were dropped on python < 3.10 (3.10+ emits an extra block-exit line
event that flushes them).
flush the pending changes in __exit__, but only when a later event hasn't
already captured the final state. track a per-frame 'pending' flag: a body
line leaves changes pending, while the block-exit line event and non-line
events (exception/return) clear it. so:
- 3.10+ normal exit: the block-exit line event clears pending -> no double
eval of repr/watch/custom_repr.
- exceptional exit: skipped entirely (the exception events already captured,
and this avoids replacing the user's exception from a callback).
- one-line 'with snoop(): x=1': no trace event at all, so we still flush.
- same with statement reused in a loop: the with line is resolved from
f_lasti (reliable) instead of f_lineno at __enter__ (which can be a stale
body line on 3.8/3.9), so every iteration's final value is reported.
cleanup runs in finally so a failing callback can't strand frame references.
tests: final line that modifies an existing var and creates a new one, the
one-line body, no double repr eval, exceptional exit (exception survives, no
extra snapshot, state cleaned), and a with block reused in a loop. ran the
full suite on 3.9, 3.11 and 3.14.
The tracer reads co_code[frame.f_lasti] to decide whether a 'return' event was
caused by an exception. f_lasti can be -1 (e.g. a frame that hasn't executed
an instruction), and co_code[-1] reads the last byte -- the wrong opcode --
which can misreport a normal return as "Call ended by exception". Extract the
check into call_ended_by_exception() and return False when f_lasti < 0.
Fix '_thread._local' object has no attribute 'depth' raised by snooper if there's exception upon calling snooped func by initializing thread_global dict with depth when entering the trace() func