From ccd60b0c6eb0a6f57d3e1f8250a9cd8156d6a217 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet <jgbt@protonmail.com> Date: Thu, 5 Dec 2024 20:55:12 -0500 Subject: [PATCH] Add `breakpoint()` hook restoration example + test --- .../debugging/restore_builtin_breakpoint.py | 35 ++++++++++-- tests/devx/test_debugger.py | 53 ++++++++++++++++++- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/examples/debugging/restore_builtin_breakpoint.py b/examples/debugging/restore_builtin_breakpoint.py index 6e141dfc..89605075 100644 --- a/examples/debugging/restore_builtin_breakpoint.py +++ b/examples/debugging/restore_builtin_breakpoint.py @@ -6,19 +6,46 @@ import tractor async def main() -> None: - async with tractor.open_nursery(debug_mode=True) as an: - assert os.environ['PYTHONBREAKPOINT'] == 'tractor._debug._set_trace' + # intially unset, no entry. + orig_pybp_var: int = os.environ.get('PYTHONBREAKPOINT') + assert orig_pybp_var in {None, "0"} + + async with tractor.open_nursery( + debug_mode=True, + ) as an: + assert an + assert ( + (pybp_var := os.environ['PYTHONBREAKPOINT']) + == + 'tractor.devx._debug._sync_pause_from_builtin' + ) # TODO: an assert that verifies the hook has indeed been, hooked # XD - assert sys.breakpointhook is not tractor._debug._set_trace + assert ( + (pybp_hook := sys.breakpointhook) + is not tractor.devx._debug._set_trace + ) + print( + f'$PYTHONOBREAKPOINT: {pybp_var!r}\n' + f'`sys.breakpointhook`: {pybp_hook!r}\n' + ) breakpoint() + pass # first bp, tractor hook set. - # TODO: an assert that verifies the hook is unhooked.. + # XXX AFTER EXIT (of actor-runtime) verify the hook is unset.. + # + # YES, this is weird but it's how stdlib docs say to do it.. + # https://docs.python.org/3/library/sys.html#sys.breakpointhook + assert os.environ.get('PYTHONBREAKPOINT') is orig_pybp_var assert sys.breakpointhook + + # now ensure a regular builtin pause still works breakpoint() + pass # last bp, stdlib hook restored + if __name__ == '__main__': trio.run(main) diff --git a/tests/devx/test_debugger.py b/tests/devx/test_debugger.py index 2a24bf98..5327fb0b 100644 --- a/tests/devx/test_debugger.py +++ b/tests/devx/test_debugger.py @@ -1229,6 +1229,53 @@ def test_shield_pause( child.expect(EOF) +def test_breakpoint_hook_restored( + spawn, +): + ''' + Ensures our actor runtime sets a custom `breakpoint()` hook + on open then restores the stdlib's default on close. + + The hook state validation is done via `assert`s inside the + invoked script with only `breakpoint()` (not `tractor.pause()`) + calls used. + + ''' + child = spawn('restore_builtin_breakpoint') + + child.expect(PROMPT) + assert_before( + child, + [ + _pause_msg, + "<Task '__main__.main'", + "('root'", + "first bp, tractor hook set", + ] + ) + child.sendline('c') + child.expect(PROMPT) + assert_before( + child, + [ + "last bp, stdlib hook restored", + ] + ) + + # since the stdlib hook was already restored there should be NO + # `tractor` `log.pdb()` content from console! + assert not in_prompt_msg( + child, + [ + _pause_msg, + "<Task '__main__.main'", + "('root'", + ], + ) + child.sendline('c') + child.expect(EOF) + + # TODO: better error for "non-ideal" usage from the root actor. # -[ ] if called from an async scope emit a message that suggests # using `await tractor.pause()` instead since it's less overhead @@ -1246,6 +1293,7 @@ def test_sync_pause_from_bg_task_in_root_actor_(): ''' ... + # TODO: needs ANSI code stripping tho, see `assert_before()` # above! def test_correct_frames_below_hidden(): ''' @@ -1262,8 +1310,9 @@ def test_cant_pause_from_paused_task(): ''' Pausing from with an already paused task should raise an error. - Normally this should only happen in practise while debugging the call stack of `tractor.pause()` itself, likely - by a `.pause()` line somewhere inside our runtime. + Normally this should only happen in practise while debugging the + call stack of `tractor.pause()` itself, likely by a `.pause()` + line somewhere inside our runtime. ''' ...