Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" interactive debugging with PDB, the Python Debugger. """ 

2import argparse 

3import functools 

4import sys 

5 

6from _pytest import outcomes 

7from _pytest.config import hookimpl 

8from _pytest.config.exceptions import UsageError 

9 

10 

11def _validate_usepdb_cls(value): 

12 """Validate syntax of --pdbcls option.""" 

13 try: 

14 modname, classname = value.split(":") 

15 except ValueError: 

16 raise argparse.ArgumentTypeError( 

17 "{!r} is not in the format 'modname:classname'".format(value) 

18 ) 

19 return (modname, classname) 

20 

21 

22def pytest_addoption(parser): 

23 group = parser.getgroup("general") 

24 group._addoption( 

25 "--pdb", 

26 dest="usepdb", 

27 action="store_true", 

28 help="start the interactive Python debugger on errors or KeyboardInterrupt.", 

29 ) 

30 group._addoption( 

31 "--pdbcls", 

32 dest="usepdb_cls", 

33 metavar="modulename:classname", 

34 type=_validate_usepdb_cls, 

35 help="start a custom interactive Python debugger on errors. " 

36 "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb", 

37 ) 

38 group._addoption( 

39 "--trace", 

40 dest="trace", 

41 action="store_true", 

42 help="Immediately break when running each test.", 

43 ) 

44 

45 

46def pytest_configure(config): 

47 import pdb 

48 

49 if config.getvalue("trace"): 

50 config.pluginmanager.register(PdbTrace(), "pdbtrace") 

51 if config.getvalue("usepdb"): 

52 config.pluginmanager.register(PdbInvoke(), "pdbinvoke") 

53 

54 pytestPDB._saved.append( 

55 (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config) 

56 ) 

57 pdb.set_trace = pytestPDB.set_trace 

58 pytestPDB._pluginmanager = config.pluginmanager 

59 pytestPDB._config = config 

60 

61 # NOTE: not using pytest_unconfigure, since it might get called although 

62 # pytest_configure was not (if another plugin raises UsageError). 

63 def fin(): 

64 ( 

65 pdb.set_trace, 

66 pytestPDB._pluginmanager, 

67 pytestPDB._config, 

68 ) = pytestPDB._saved.pop() 

69 

70 config._cleanup.append(fin) 

71 

72 

73class pytestPDB: 

74 """ Pseudo PDB that defers to the real pdb. """ 

75 

76 _pluginmanager = None 

77 _config = None 

78 _saved = [] # type: list 

79 _recursive_debug = 0 

80 _wrapped_pdb_cls = None 

81 

82 @classmethod 

83 def _is_capturing(cls, capman): 

84 if capman: 

85 return capman.is_capturing() 

86 return False 

87 

88 @classmethod 

89 def _import_pdb_cls(cls, capman): 

90 if not cls._config: 

91 import pdb 

92 

93 # Happens when using pytest.set_trace outside of a test. 

94 return pdb.Pdb 

95 

96 usepdb_cls = cls._config.getvalue("usepdb_cls") 

97 

98 if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls: 

99 return cls._wrapped_pdb_cls[1] 

100 

101 if usepdb_cls: 

102 modname, classname = usepdb_cls 

103 

104 try: 

105 __import__(modname) 

106 mod = sys.modules[modname] 

107 

108 # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp). 

109 parts = classname.split(".") 

110 pdb_cls = getattr(mod, parts[0]) 

111 for part in parts[1:]: 

112 pdb_cls = getattr(pdb_cls, part) 

113 except Exception as exc: 

114 value = ":".join((modname, classname)) 

115 raise UsageError( 

116 "--pdbcls: could not import {!r}: {}".format(value, exc) 

117 ) 

118 else: 

119 import pdb 

120 

121 pdb_cls = pdb.Pdb 

122 

123 wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman) 

124 cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls) 

125 return wrapped_cls 

126 

127 @classmethod 

128 def _get_pdb_wrapper_class(cls, pdb_cls, capman): 

129 import _pytest.config 

130 

131 class PytestPdbWrapper(pdb_cls): 

132 _pytest_capman = capman 

133 _continued = False 

134 

135 def do_debug(self, arg): 

136 cls._recursive_debug += 1 

137 ret = super().do_debug(arg) 

138 cls._recursive_debug -= 1 

139 return ret 

140 

141 def do_continue(self, arg): 

142 ret = super().do_continue(arg) 

143 if cls._recursive_debug == 0: 

144 tw = _pytest.config.create_terminal_writer(cls._config) 

145 tw.line() 

146 

147 capman = self._pytest_capman 

148 capturing = pytestPDB._is_capturing(capman) 

149 if capturing: 

150 if capturing == "global": 

151 tw.sep(">", "PDB continue (IO-capturing resumed)") 

152 else: 

153 tw.sep( 

154 ">", 

155 "PDB continue (IO-capturing resumed for %s)" 

156 % capturing, 

157 ) 

158 capman.resume() 

159 else: 

160 tw.sep(">", "PDB continue") 

161 cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self) 

162 self._continued = True 

163 return ret 

164 

165 do_c = do_cont = do_continue 

166 

167 def do_quit(self, arg): 

168 """Raise Exit outcome when quit command is used in pdb. 

169 

170 This is a bit of a hack - it would be better if BdbQuit 

171 could be handled, but this would require to wrap the 

172 whole pytest run, and adjust the report etc. 

173 """ 

174 ret = super().do_quit(arg) 

175 

176 if cls._recursive_debug == 0: 

177 outcomes.exit("Quitting debugger") 

178 

179 return ret 

180 

181 do_q = do_quit 

182 do_exit = do_quit 

183 

184 def setup(self, f, tb): 

185 """Suspend on setup(). 

186 

187 Needed after do_continue resumed, and entering another 

188 breakpoint again. 

189 """ 

190 ret = super().setup(f, tb) 

191 if not ret and self._continued: 

192 # pdb.setup() returns True if the command wants to exit 

193 # from the interaction: do not suspend capturing then. 

194 if self._pytest_capman: 

195 self._pytest_capman.suspend_global_capture(in_=True) 

196 return ret 

197 

198 def get_stack(self, f, t): 

199 stack, i = super().get_stack(f, t) 

200 if f is None: 

201 # Find last non-hidden frame. 

202 i = max(0, len(stack) - 1) 

203 while i and stack[i][0].f_locals.get("__tracebackhide__", False): 

204 i -= 1 

205 return stack, i 

206 

207 return PytestPdbWrapper 

208 

209 @classmethod 

210 def _init_pdb(cls, method, *args, **kwargs): 

211 """ Initialize PDB debugging, dropping any IO capturing. """ 

212 import _pytest.config 

213 

214 if cls._pluginmanager is not None: 

215 capman = cls._pluginmanager.getplugin("capturemanager") 

216 else: 

217 capman = None 

218 if capman: 

219 capman.suspend(in_=True) 

220 

221 if cls._config: 

222 tw = _pytest.config.create_terminal_writer(cls._config) 

223 tw.line() 

224 

225 if cls._recursive_debug == 0: 

226 # Handle header similar to pdb.set_trace in py37+. 

227 header = kwargs.pop("header", None) 

228 if header is not None: 

229 tw.sep(">", header) 

230 else: 

231 capturing = cls._is_capturing(capman) 

232 if capturing == "global": 

233 tw.sep(">", "PDB {} (IO-capturing turned off)".format(method)) 

234 elif capturing: 

235 tw.sep( 

236 ">", 

237 "PDB %s (IO-capturing turned off for %s)" 

238 % (method, capturing), 

239 ) 

240 else: 

241 tw.sep(">", "PDB {}".format(method)) 

242 

243 _pdb = cls._import_pdb_cls(capman)(**kwargs) 

244 

245 if cls._pluginmanager: 

246 cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb) 

247 return _pdb 

248 

249 @classmethod 

250 def set_trace(cls, *args, **kwargs): 

251 """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing.""" 

252 frame = sys._getframe().f_back 

253 _pdb = cls._init_pdb("set_trace", *args, **kwargs) 

254 _pdb.set_trace(frame) 

255 

256 

257class PdbInvoke: 

258 def pytest_exception_interact(self, node, call, report): 

259 capman = node.config.pluginmanager.getplugin("capturemanager") 

260 if capman: 

261 capman.suspend_global_capture(in_=True) 

262 out, err = capman.read_global_capture() 

263 sys.stdout.write(out) 

264 sys.stdout.write(err) 

265 _enter_pdb(node, call.excinfo, report) 

266 

267 def pytest_internalerror(self, excrepr, excinfo): 

268 tb = _postmortem_traceback(excinfo) 

269 post_mortem(tb) 

270 

271 

272class PdbTrace: 

273 @hookimpl(hookwrapper=True) 

274 def pytest_pyfunc_call(self, pyfuncitem): 

275 _test_pytest_function(pyfuncitem) 

276 yield 

277 

278 

279def _test_pytest_function(pyfuncitem): 

280 _pdb = pytestPDB._init_pdb("runcall") 

281 testfunction = pyfuncitem.obj 

282 

283 # we can't just return `partial(pdb.runcall, testfunction)` because (on 

284 # python < 3.7.4) runcall's first param is `func`, which means we'd get 

285 # an exception if one of the kwargs to testfunction was called `func` 

286 @functools.wraps(testfunction) 

287 def wrapper(*args, **kwargs): 

288 func = functools.partial(testfunction, *args, **kwargs) 

289 _pdb.runcall(func) 

290 

291 pyfuncitem.obj = wrapper 

292 

293 

294def _enter_pdb(node, excinfo, rep): 

295 # XXX we re-use the TerminalReporter's terminalwriter 

296 # because this seems to avoid some encoding related troubles 

297 # for not completely clear reasons. 

298 tw = node.config.pluginmanager.getplugin("terminalreporter")._tw 

299 tw.line() 

300 

301 showcapture = node.config.option.showcapture 

302 

303 for sectionname, content in ( 

304 ("stdout", rep.capstdout), 

305 ("stderr", rep.capstderr), 

306 ("log", rep.caplog), 

307 ): 

308 if showcapture in (sectionname, "all") and content: 

309 tw.sep(">", "captured " + sectionname) 

310 if content[-1:] == "\n": 

311 content = content[:-1] 

312 tw.line(content) 

313 

314 tw.sep(">", "traceback") 

315 rep.toterminal(tw) 

316 tw.sep(">", "entering PDB") 

317 tb = _postmortem_traceback(excinfo) 

318 rep._pdbshown = True 

319 post_mortem(tb) 

320 return rep 

321 

322 

323def _postmortem_traceback(excinfo): 

324 from doctest import UnexpectedException 

325 

326 if isinstance(excinfo.value, UnexpectedException): 

327 # A doctest.UnexpectedException is not useful for post_mortem. 

328 # Use the underlying exception instead: 

329 return excinfo.value.exc_info[2] 

330 else: 

331 return excinfo._excinfo[2] 

332 

333 

334def post_mortem(t): 

335 p = pytestPDB._init_pdb("post_mortem") 

336 p.reset() 

337 p.interaction(None, t) 

338 if p.quitting: 

339 outcomes.exit("Quitting debugger")