Coverage for C:\leo.repo\leo-editor\leo\core\leoDebugger.py: 18%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

347 statements  

1# -*- coding: utf-8 -*- 

2#@+leo-ver=5-thin 

3#@+node:ekr.20130302121602.10208: * @file leoDebugger.py 

4#@@first 

5 

6# Disable all mypy errors. 

7# type:ignore 

8 

9#@+<< leoDebugger.py docstring >> 

10#@+node:ekr.20181006100710.1: ** << leoDebugger.py docstring >> 

11""" 

12Leo's integrated debugger supports the xdb and various db-* commands, 

13corresponding to the pdb commands: 

14 

15**Commands** 

16 

17xdb: Start a Leo's integrated debugger. 

18The presently-selected node should be within an @<file> tree. 

19 

20Now you are ready to execute all the db-* commands. You can execute these 

21commands from the minibuffer, or from the the Debug pane. The following 

22correspond to the pdb commands:: 

23 

24 db-b: Set a breakpoint at the line shown in Leo. It should be an executable line. 

25 db-c: Continue, that is, run until the next breakpoint. 

26 db-h: Print the help message (in the console, for now) 

27 db-l: List a few lines around present location. 

28 db-n: Execute the next line. 

29 db-q: End the debugger. 

30 db-r: Return from the present function/method. 

31 db-s: Step into the next line. 

32 db-w: Print the stack. 

33 

34There are two additional commands:: 

35 

36 db-again: Run the previous db-command. 

37 db-input: Prompt for any pdb command, then execute it. 

38 

39The db-input command allows you can enter any pdb command at all. For 

40example: "print c". But you don't have to run these commands from the 

41minibuffer, as discussed next. 

42 

43**Setting breakpoints in the gutter** 

44 

45When @bool use_gutter = True, Leo shows a border in the body pane. By 

46default, the line-numbering.py plugin is enabled, and if so, the gutter 

47shows correct line number in the external file. 

48 

49After executing the xdb command, you can set a breakpoint on any executable 

50line by clicking in the gutter to the right of the line number. You can 

51also set a breakpoint any time the debugger is stopped. 

52 

53Using the gutter is optional. You can also set breakpoints with the db-b 

54command or by typing "d line-number" or "d file-name:line-number" using the 

55db-input command, or by using the Debug pane (see below) 

56 

57**The Debug pane** 

58 

59The xdb_pane.py plugin creates the Debug pane in the Log pane. The pane 

60contains buttons for all the commands listed above. In addition, there is 

61an input area in which you can enter pdb commands. This is a bit easier to 

62use than the db-input command. 

63 

64**Summary** 

65 

66The xdb and db-* commands are always available. They correspond to pdb 

67commands. 

68 

69When xdb is active you may set breakpoints by clicking in the gutter next 

70to any executable line. The line_numbering plugin must be enabled and @bool 

71use_gutter must be True. 

72 

73The xdb_pane plugin creates the Debug pane in the Log window. 

74""" 

75#@-<< leoDebugger.py docstring >> 

76#@+<< leoDebugger.py imports >> 

77#@+node:ekr.20181006100604.1: ** << leoDebugger.py imports >> 

78import bdb 

79import queue 

80import os 

81import pdb 

82import re 

83import sys 

84import threading 

85from leo.core import leoGlobals as g 

86#@-<< leoDebugger.py imports >> 

87#@+others 

88#@+node:ekr.20180701050839.5: ** class Xdb (pdb.Pdb, threading.Thread) 

89class Xdb(pdb.Pdb, threading.Thread): 

90 """ 

91 An debugger, a subclass of Pdb, that runs in a separate thread without 

92 hanging Leo. Only one debugger, g.app.xdb, can be active at any time. 

93 

94 A singleton listener method, g.app,xdb_timer, runs in the main Leo 

95 thread. The listener runs until Leo exists. 

96 

97 Two Queues communicate between the threads: 

98 

99 - xdb.qc contains commands from the main thread to this thread. 

100 All xdb/pdb input comes from this queue. 

101 

102 - xdb.qr contains requests from the xdb thread to the main thread. 

103 All xdb/pdb output goes to this queue. 

104 

105 Settings 

106 -------- 

107 

108 - @bool use_xdb_pane_output_area: when True, all debugger output is sent 

109 to an output area in the Debug pane. 

110 

111 @bool use_gutter: when True, line numbers appear to the left of 

112 the body pane. Clicking to the left of the gutter toggles breakpoints 

113 when xdb is active. 

114 """ 

115 #@+others 

116 #@+node:ekr.20180701050839.4: *3* class QueueStdin (obj) 

117 class QueueStdin: 

118 """ 

119 A replacement for Python's stdin class containing only readline(). 

120 """ 

121 

122 def __init__(self, qc): 

123 self.qc = qc 

124 

125 def readline(self): 

126 """Return the next line from the qc channel.""" 

127 s = self.qc.get() # blocks 

128 if 1: 

129 # Just echo. 

130 print(s.rstrip()) 

131 else: 

132 # Use the output area. 

133 xdb = getattr(g.app, 'xdb') 

134 if xdb: 

135 xdb.write(s) 

136 else: 

137 print(s) 

138 return s 

139 #@+node:ekr.20181003020344.1: *3* class QueueStdout (obj) 

140 class QueueStdout: 

141 """ 

142 A replacement for Python's stdout class containing only write(). 

143 """ 

144 

145 def __init__(self, qr): 

146 self.qr = qr 

147 

148 def flush(self): 

149 pass 

150 

151 def write(self, s): 

152 """Write s to the qr channel""" 

153 self.qr.put(['put-stdout', s]) 

154 #@+node:ekr.20181006160108.1: *3* xdb.__init__ 

155 def __init__(self, path=None): 

156 

157 self.qc = queue.Queue() # The command queue. 

158 self.qr = queue.Queue() # The request queue. 

159 stdin_q = self.QueueStdin(qc=self.qc) 

160 stdout_q = self.QueueStdout(qr=self.qr) 

161 # Start the singleton listener, in the main Leo thread. 

162 timer = getattr(g.app, 'xdb_timer', None) 

163 if timer: 

164 self.timer = timer 

165 else: 

166 self.timer = g.IdleTime(listener, delay=0) 

167 self.timer.start() 

168 # Init the base classes. 

169 threading.Thread.__init__(self) 

170 super().__init__( 

171 stdin=stdin_q, 

172 stdout=stdout_q, 

173 readrc=False, 

174 # Don't read a .rc file. 

175 ) 

176 sys.stdout = stdout_q 

177 self.daemon = True 

178 self.path = path 

179 self.prompt = '(xdb) ' 

180 self.saved_frame = None 

181 self.saved_traceback = None 

182 #@+node:ekr.20181002053718.1: *3* Overrides 

183 #@+node:ekr.20190108040329.1: *4* xdb.checkline (overrides Pdb) 

184 def checkline(self, path, n): 

185 # pylint: disable=arguments-differ 

186 # filename, lineno 

187 try: 

188 return pdb.Pdb.checkline(self, path, n) 

189 except AttributeError: 

190 return False 

191 except Exception: 

192 g.es_exception() 

193 return False 

194 #@+node:ekr.20181002061627.1: *4* xdb.cmdloop (overrides Cmd) 

195 def cmdloop(self, intro=None): 

196 """Override Cmd.cmdloop.""" 

197 assert not intro, repr(intro) 

198 stop = None 

199 while not stop: 

200 if self.cmdqueue: 

201 # Pdb.precmd sets cmdqueue. 

202 line = self.cmdqueue.pop(0) 

203 else: 

204 self.stdout.write(self.prompt) 

205 self.stdout.flush() 

206 # Get the input from Leo's main thread. 

207 line = self.stdin.readline() # QueueStdin.readline: 

208 line = line.rstrip('\r\n') if line else 'EOF' 

209 line = self.precmd(line) # Pdb.precmd. 

210 stop = self.onecmd(line) # Pdb.onecmd. 

211 # Show the line in Leo. 

212 if stop: 

213 self.select_line(self.saved_frame, self.saved_traceback) 

214 #@+node:ekr.20180701050839.6: *4* xdb.do_clear (overides Pdb) 

215 def do_clear(self, arg=None): 

216 """cl(ear) filename:lineno\ncl(ear) [bpnumber [bpnumber...]] 

217 With a space separated list of breakpoint numbers, clear 

218 those breakpoints. Without argument, clear all breaks (but 

219 first ask confirmation). With a filename:lineno argument, 

220 clear all breaks at that line in that file. 

221 """ 

222 # Same as pdb.do_clear except uses self.stdin.readline (as it should). 

223 if not arg: 

224 bplist = [bp for bp in bdb.Breakpoint.bpbynumber if bp] 

225 if bplist: 

226 print('Clear all breakpoints?') 

227 reply = self.stdin.readline().strip().lower() 

228 if reply in ('y', 'yes'): 

229 self.clear_all_breaks() 

230 for bp in bplist: 

231 self.message(f"Deleted {bp}") 

232 return 

233 if ':' in arg: 

234 # Make sure it works for "clear C:\foo\bar.py:12" 

235 i = arg.rfind(':') 

236 filename = arg[:i] 

237 arg = arg[i + 1 :] 

238 try: 

239 lineno = int(arg) 

240 except ValueError: 

241 err = f"Invalid line number ({arg})" 

242 else: 

243 bplist = self.get_breaks(filename, lineno) 

244 err = self.clear_break(filename, lineno) 

245 if err: 

246 self.error(err) 

247 else: 

248 for bp in bplist: 

249 self.message(f"Deleted {bp}") 

250 return 

251 numberlist = arg.split() 

252 for i in numberlist: 

253 try: 

254 bp = self.get_bpbynumber(i) 

255 except ValueError as err: 

256 self.error(err) 

257 else: 

258 self.clear_bpbynumber(i) 

259 self.message(f"Deleted {bp}") 

260 

261 do_cl = do_clear # 'c' is already an abbreviation for 'continue' 

262 

263 # complete_clear = self._complete_location 

264 # complete_cl = self._complete_location 

265 #@+node:ekr.20180701050839.7: *4* xdb.do_quit (overrides Pdb) 

266 def do_quit(self, arg=None): 

267 """q(uit)\nexit 

268 Quit from the debugger. The program being executed is aborted. 

269 """ 

270 self.write('End xdb\n') 

271 self._user_requested_quit = True 

272 self.set_quit() 

273 self.qr.put(['stop-xdb']) 

274 # Kill xdb *after* all other messages have been sent. 

275 return 1 

276 

277 do_q = do_quit 

278 do_exit = do_quit 

279 #@+node:ekr.20180701050839.8: *4* xdb.interaction (overrides Pdb) 

280 def interaction(self, frame, traceback): 

281 """Override.""" 

282 self.saved_frame = frame 

283 self.saved_traceback = traceback 

284 self.select_line(frame, traceback) 

285 pdb.Pdb.interaction(self, frame, traceback) 

286 # Call the base class method. 

287 #@+node:ekr.20180701050839.10: *4* xdb.set_continue (overrides Bdb) 

288 def set_continue(self): 

289 """ override Bdb.set_continue""" 

290 # Don't stop except at breakpoints or when finished 

291 self._set_stopinfo(self.botframe, None, -1) 

292 if not self.breaks: 

293 # no breakpoints; run without debugger overhead. 

294 # Do *not call kill(): only db-kill and db-q do that. 

295 # self.write('clearing sys.settrace\n') 

296 sys.settrace(None) 

297 frame = sys._getframe().f_back 

298 while frame and frame is not self.botframe: 

299 del frame.f_trace 

300 frame = frame.f_back 

301 #@+node:ekr.20181006052604.1: *3* xdb.has_breakpoint & has_breakpoints 

302 def has_breakpoint(self, filename, lineno): 

303 """Return True if there is a breakpoint at the given file and line.""" 

304 filename = self.canonic(filename) 

305 aList = self.breaks.get(filename) or [] 

306 return lineno in aList 

307 

308 def has_breakpoints(self): 

309 """Return True if there are any breakpoints.""" 

310 return self.breaks 

311 #@+node:ekr.20181002094126.1: *3* xdb.run 

312 def run(self): 

313 """The thread's run method: called via start.""" 

314 # pylint: disable=arguments-differ 

315 from leo.core.leoQt import QtCore 

316 QtCore.pyqtRemoveInputHook() # From g.pdb 

317 if self.path: 

318 self.run_path(self.path) 

319 else: 

320 self.set_trace() 

321 #@+node:ekr.20180701090439.1: *3* xdb.run_path 

322 def run_path(self, path): 

323 """Begin execution of the python file.""" 

324 source = g.readFileIntoUnicodeString(path) 

325 fn = g.shortFileName(path) 

326 try: 

327 code = compile(source, fn, 'exec') 

328 except Exception: 

329 g.es_exception() 

330 g.trace('can not compile', path) 

331 return 

332 self.reset() 

333 sys.settrace(self.trace_dispatch) 

334 try: 

335 self.quitting = False 

336 exec(code, {}, {}) 

337 except bdb.BdbQuit: 

338 if not self.quitting: 

339 self.do_quit() 

340 finally: 

341 self.quitting = True 

342 sys.settrace(None) 

343 #@+node:ekr.20180701151233.1: *3* xdb.select_line 

344 def select_line(self, frame, traceback): 

345 """Select the given line in Leo.""" 

346 stack, curindex = self.get_stack(frame, traceback) 

347 frame, lineno = stack[curindex] 

348 filename = frame.f_code.co_filename 

349 self.qr.put(['select-line', lineno, filename]) 

350 # Select the line in the main thread. 

351 # xdb.show_line finalizes the file name. 

352 #@+node:ekr.20181007044254.1: *3* xdb.write 

353 def write(self, s): 

354 """Write s to the output stream.""" 

355 self.qr.put(['put-stdout', s]) 

356 #@-others 

357#@+node:ekr.20181007063214.1: ** top-level functions 

358# These functions run in Leo's main thread. 

359#@+node:ekr.20181004120344.1: *3* function: get_gnx_from_file 

360def get_gnx_from_file(file_s, p, path): 

361 """Set p's gnx from the @file node in the derived file.""" 

362 pat = re.compile(r'^#@\+node:(.*): \*+ @file (.+)$') 

363 for line in g.splitLines(file_s): 

364 m = pat.match(line) 

365 if m: 

366 gnx, path2 = m.group(1), m.group(2) 

367 path2 = path2.replace('\\', '/') 

368 p.v.fileIndex = gnx 

369 if path == path2: 

370 return True 

371 g.trace(f"Not found: @+node for {path}") 

372 g.trace('Reverting to @auto') 

373 return False 

374#@+node:ekr.20180701050839.3: *3* function: listener 

375def listener(timer): 

376 """ 

377 Listen, at idle-time, in Leo's main thread, for data on the qr channel. 

378 

379 This is a singleton timer, created by the xdb command. 

380 """ 

381 if g.app.killed: 

382 return 

383 xdb = getattr(g.app, 'xdb', None) 

384 if not xdb: 

385 return 

386 c = g.app.log.c 

387 xpd_pane = getattr(c, 'xpd_pane', None) 

388 kill = False 

389 while not xdb.qr.empty(): 

390 aList = xdb.qr.get() # blocks 

391 kind = aList[0] 

392 if kind == 'clear-stdout': 

393 if xpd_pane: 

394 xpd_pane.clear() 

395 elif kind == 'put-stdout': 

396 message = aList[1] 

397 if xpd_pane: 

398 xpd_pane.write(message) 

399 else: 

400 sys.stdout.write(message) 

401 sys.stdout.flush() 

402 elif kind == 'stop-xdb': 

403 kill = True 

404 elif kind == 'select-line': 

405 line, fn = aList[1], aList[2] 

406 show_line(line, fn) 

407 else: 

408 g.es('unknown qr message:', aList) 

409 if kill: 

410 g.app.xdb = None 

411 sys.stdout = sys.__stdout__ 

412 # Never stop the singleton timer. 

413 # self.timer.stop() 

414#@+node:ekr.20181004060517.1: *3* function: make_at_file_node 

415def make_at_file_node(line, path): 

416 """ 

417 Make and populate an @auto node for the given path. 

418 """ 

419 c = g.app.log.c 

420 if not c: 

421 return None 

422 path = g.os_path_finalize(path).replace('\\', '/') 

423 if not g.os_path_exists(path): 

424 g.trace('Not found:', repr(path)) 

425 return None 

426 # Create the new node. 

427 p = c.lastTopLevel().insertAfter() 

428 # Like c.looksLikeDerivedFile, but retaining the contents. 

429 with open(path, 'r') as f: 

430 file_s = f.read() 

431 is_derived = file_s.find('@+leo-ver=') > -1 

432 if is_derived: 

433 # Set p.v.gnx from the derived file. 

434 is_derived = get_gnx_from_file(file_s, p, path) 

435 kind = '@file' if is_derived else '@auto' 

436 p.h = f"{kind} {path}" 

437 c.selectPosition(p) 

438 c.refreshFromDisk() 

439 return p 

440#@+node:ekr.20180701061957.1: *3* function: show_line 

441def show_line(line, fn): 

442 """ 

443 Put the cursor on the requested line of the given file. 

444 fn should be a full path to a file. 

445 """ 

446 c = g.app.log.c 

447 target = g.os_path_finalize(fn).replace('\\', '/') 

448 if not g.os_path_exists(fn): 

449 g.trace('===== Does not exist', fn) 

450 return 

451 for p in c.all_positions(): 

452 if p.isAnyAtFileNode(): 

453 path = g.fullPath(c, p).replace('\\', '/') 

454 if target == path: 

455 # Select the line. 

456 junk_p, junk_offset, ok = c.gotoCommands.find_file_line(n=line, p=p) 

457 if not ok: 

458 g.trace('FAIL:', target) 

459 c.bodyWantsFocusNow() 

460 return 

461 p = make_at_file_node(line, target) 

462 junk_p, junk_offset, ok = c.gotoCommands.find_file_line(n=line, p=p) 

463 if not ok: 

464 g.trace('FAIL:', target) 

465#@+node:ekr.20181001054314.1: ** top-level xdb commands 

466#@+node:ekr.20181003015017.1: *3* db-again 

467@g.command('db-again') 

468def xdb_again(event): 

469 """Repeat the previous xdb command.""" 

470 xdb = getattr(g.app, 'xdb', None) 

471 if xdb: 

472 xdb.qc.put(xdb.lastcmd) 

473 else: 

474 print('xdb not active') 

475#@+node:ekr.20181003054157.1: *3* db-b 

476@g.command('db-b') 

477def xdb_breakpoint(event): 

478 """Set the breakpoint at the presently select line in Leo.""" 

479 c = event.get('c') 

480 if not c: 

481 return 

482 p = c.p 

483 xdb = getattr(g.app, 'xdb', None) 

484 if not xdb: 

485 print('xdb not active') 

486 return 

487 w = c.frame.body.wrapper 

488 if not w: 

489 return 

490 x = c.gotoCommands 

491 root, fileName = x.find_root(p) 

492 if not root: 

493 g.trace('no root', p.h) 

494 return 

495 path = g.fullPath(c, root) 

496 n0 = x.find_node_start(p=p) 

497 if n0 is None: 

498 g.trace('no n0') 

499 return 

500 c.bodyWantsFocusNow() 

501 i = w.getInsertPoint() 

502 s = w.getAllText() 

503 row, col = g.convertPythonIndexToRowCol(s, i) 

504 n = x.node_offset_to_file_line(row, p, root) 

505 if n is not None: 

506 xdb.qc.put(f"b {path}:{n + 1}") 

507#@+node:ekr.20180702074705.1: *3* db-c/h/l/n/q/r/s/w 

508@g.command('db-c') 

509def xdb_c(event): 

510 """execute the pdb 'continue' command.""" 

511 db_command(event, 'c') 

512 

513@g.command('db-h') 

514def xdb_h(event): 

515 """execute the pdb 'continue' command.""" 

516 db_command(event, 'h') 

517 

518@g.command('db-l') 

519def xdb_l(event): 

520 """execute the pdb 'list' command.""" 

521 db_command(event, 'l') 

522 

523@g.command('db-n') 

524def xdb_n(event): 

525 """execute the pdb 'next' command.""" 

526 db_command(event, 'n') 

527 

528@g.command('db-q') 

529def xdb_q(event): 

530 """execute the pdb 'quit' command.""" 

531 db_command(event, 'q') 

532 

533@g.command('db-r') 

534def xdb_r(event): 

535 """execute the pdb 'return' command.""" 

536 db_command(event, 'r') 

537 

538@g.command('db-s') 

539def xdb_s(event): 

540 """execute the pdb 'step' command.""" 

541 db_command(event, 's') 

542 

543@g.command('db-w') 

544def xdb_w(event): 

545 """execute the pdb 'where' command.""" 

546 db_command(event, 'w') 

547#@+node:ekr.20180701050839.2: *3* db-input 

548@g.command('db-input') 

549def xdb_input(event): 

550 """Prompt the user for a pdb command and execute it.""" 

551 c = event.get('c') 

552 if not c: 

553 g.trace('no c') 

554 return 

555 xdb = getattr(g.app, 'xdb', None) 

556 if not xdb: 

557 print('xdb not active') 

558 return 

559 

560 def callback(args, c, event): 

561 xdb = getattr(g.app, 'xdb', None) 

562 if xdb: 

563 command = args[0].strip() 

564 if not command: 

565 command = xdb.lastcmd 

566 xdb.qc.put(command) 

567 else: 

568 g.trace('xdb not active') 

569 

570 c.interactive(callback, event, prompts=['Debugger command: ']) 

571#@+node:ekr.20181003015636.1: *3* db-status 

572@g.command('db-status') 

573def xdb_status(event): 

574 """Print whether xdb is active.""" 

575 xdb = getattr(g.app, 'xdb', None) 

576 print('active' if xdb else 'inactive') 

577#@+node:ekr.20181006163454.1: *3* do_command 

578def db_command(event, command): 

579 

580 xdb = getattr(g.app, 'xdb', None) 

581 if xdb: 

582 xdb.qc.put(command) 

583 else: 

584 print('xdb not active') 

585#@+node:ekr.20180701050839.1: *3* xdb 

586@g.command('xdb') 

587def xdb_command(event): 

588 """Start the external debugger on a toy test program.""" 

589 c = event.get('c') 

590 if not c: 

591 return 

592 path = g.fullPath(c, c.p) 

593 if not path: 

594 g.trace('Not in an @<file> tree') 

595 return 

596 if not g.os_path_exists(path): 

597 g.trace('not found', path) 

598 return 

599 os.chdir(g.os_path_dirname(path)) 

600 xdb = getattr(g.app, 'xdb', None) 

601 if xdb: 

602 # Just issue a message. 

603 xdb.write('xdb active: use Quit button or db-q to terminate') 

604 # Killing the previous debugger works, 

605 # *provided* we don't try to restart xdb! 

606 # That would create a race condition on g.app.xdb. 

607 # xdb.do_quit() 

608 else: 

609 # Start the debugger in a separate thread. 

610 g.app.xdb = xdb = Xdb(path) 

611 xdb.start() 

612 xdb.qr.put(['clear-stdout']) 

613 # This is Threading.start(). 

614 # It runs the debugger in a separate thread. 

615 # It also selects the start of the file. 

616#@-others 

617#@@language python 

618#@@tabwidth -4 

619#@-leo