#!/usr/bin/env python ''' Programmatic debugging helper for `pdbp` REPL human-like interaction but built to allow `claude` to interact with crashes and `tractor.pause()` breakpoints along side a human dev. Originally written by `clauded` during a backfiller inspection session with @goodboy trying to resolve duplicate/gappy ohlcv ts issues discovered while testing the new `nativedb` tsdb. Allows `claude` to run `pdb` commands and capture output in an "offline" manner but generating similar output as if it was iteracting with the debug REPL. The use of `pexpect` is heavily based on tractor's REPL UX test suite(s), namely various `tests/devx/test_debugger.py` patterns. ''' import sys import os import time import pexpect from pexpect.exceptions import ( TIMEOUT, EOF, ) PROMPT: str = r'\(Pdb\+\)' def expect( child: pexpect.spawn, patt: str, **kwargs, ) -> None: ''' Expect wrapper that prints last console data before failing. ''' try: child.expect( patt, **kwargs, ) except TIMEOUT: before: str = ( str(child.before.decode()) if isinstance(child.before, bytes) else str(child.before) ) print( f'TIMEOUT waiting for pattern: {patt}\n' f'Last seen output:\n{before}' ) raise def run_pdb_commands( commands: list[str], initial_cmd: str = 'piker store ldshm xmrusdt.usdtm.perp.binance', timeout: int = 30, print_output: bool = True, ) -> dict[str, str]: ''' Spawn piker process, wait for pdb prompt, execute commands. Returns dict mapping command -> output. ''' results: dict[str, str] = {} # Disable colored output for easier parsing os.environ['PYTHON_COLORS'] = '0' # Spawn the process if print_output: print(f'Spawning: {initial_cmd}') child: pexpect.spawn = pexpect.spawn( initial_cmd, timeout=timeout, encoding='utf-8', echo=False, ) # Wait for pdb prompt try: expect(child, PROMPT, timeout=timeout) if print_output: print('Reached pdb prompt!') # Execute each command for cmd in commands: if print_output: print(f'\n>>> {cmd}') child.sendline(cmd) time.sleep(0.1) # Wait for next prompt expect(child, PROMPT, timeout=timeout) # Capture output (everything before the prompt) output: str = ( str(child.before.decode()) if isinstance(child.before, bytes) else str(child.before) ) results[cmd] = output if print_output: print(output) # Quit debugger gracefully child.sendline('quit') try: child.expect(EOF, timeout=5) except (TIMEOUT, EOF): pass except TIMEOUT as e: print(f'Timeout: {e}') if child.before: before: str = ( str(child.before.decode()) if isinstance(child.before, bytes) else str(child.before) ) print(f'Buffer:\n{before}') results['_error'] = str(e) finally: if child.isalive(): child.close(force=True) return results class InteractivePdbSession: ''' Interactive pdb session manager for incremental debugging. ''' def __init__( self, cmd: str = 'piker store ldshm xmrusdt.usdtm.perp.binance', timeout: int = 30, ): self.cmd: str = cmd self.timeout: int = timeout self.child: pexpect.spawn|None = None self.history: list[tuple[str, str]] = [] def start(self) -> None: ''' Start the piker process and wait for first prompt. ''' os.environ['PYTHON_COLORS'] = '0' print(f'Starting: {self.cmd}') self.child = pexpect.spawn( self.cmd, timeout=self.timeout, encoding='utf-8', echo=False, ) # Wait for initial prompt expect(self.child, PROMPT, timeout=self.timeout) print('Ready at pdb prompt!') def run( self, cmd: str, print_output: bool = True, ) -> str: ''' Execute a single pdb command and return output. ''' if not self.child or not self.child.isalive(): raise RuntimeError('Session not started or dead') if print_output: print(f'\n>>> {cmd}') self.child.sendline(cmd) time.sleep(0.1) # Wait for next prompt expect(self.child, PROMPT, timeout=self.timeout) output: str = ( str(self.child.before.decode()) if isinstance(self.child.before, bytes) else str(self.child.before) ) self.history.append((cmd, output)) if print_output: print(output) return output def quit(self) -> None: ''' Exit the debugger and cleanup. ''' if self.child and self.child.isalive(): self.child.sendline('quit') try: self.child.expect(EOF, timeout=5) except (TIMEOUT, EOF): pass self.child.close(force=True) def __enter__(self): self.start() return self def __exit__(self, *args): self.quit() if __name__ == '__main__': # Example inspection commands inspect_cmds: list[str] = [ 'locals().keys()', 'type(deduped)', 'deduped.shape', ( 'step_gaps.shape ' 'if "step_gaps" in locals() ' 'else "N/A"' ), ( 'venue_gaps.shape ' 'if "venue_gaps" in locals() ' 'else "N/A"' ), ] # Allow commands from CLI args if len(sys.argv) > 1: inspect_cmds = sys.argv[1:] # Interactive session example with InteractivePdbSession() as session: for cmd in inspect_cmds: session.run(cmd) print('\n=== Session Complete ===')