1# -*- coding: utf-8 -*- 2""" interactive debugging with PDB, the Python Debugger. """ 3from __future__ import absolute_import 4from __future__ import division 5from __future__ import print_function 6 7import argparse 8import pdb 9import sys 10from doctest import UnexpectedException 11 12from _pytest import outcomes 13from _pytest.config import hookimpl 14from _pytest.config.exceptions import UsageError 15 16 17def _validate_usepdb_cls(value): 18 """Validate syntax of --pdbcls option.""" 19 try: 20 modname, classname = value.split(":") 21 except ValueError: 22 raise argparse.ArgumentTypeError( 23 "{!r} is not in the format 'modname:classname'".format(value) 24 ) 25 return (modname, classname) 26 27 28def pytest_addoption(parser): 29 group = parser.getgroup("general") 30 group._addoption( 31 "--pdb", 32 dest="usepdb", 33 action="store_true", 34 help="start the interactive Python debugger on errors or KeyboardInterrupt.", 35 ) 36 group._addoption( 37 "--pdbcls", 38 dest="usepdb_cls", 39 metavar="modulename:classname", 40 type=_validate_usepdb_cls, 41 help="start a custom interactive Python debugger on errors. " 42 "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb", 43 ) 44 group._addoption( 45 "--trace", 46 dest="trace", 47 action="store_true", 48 help="Immediately break when running each test.", 49 ) 50 51 52def pytest_configure(config): 53 if config.getvalue("trace"): 54 config.pluginmanager.register(PdbTrace(), "pdbtrace") 55 if config.getvalue("usepdb"): 56 config.pluginmanager.register(PdbInvoke(), "pdbinvoke") 57 58 pytestPDB._saved.append( 59 (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config) 60 ) 61 pdb.set_trace = pytestPDB.set_trace 62 pytestPDB._pluginmanager = config.pluginmanager 63 pytestPDB._config = config 64 65 # NOTE: not using pytest_unconfigure, since it might get called although 66 # pytest_configure was not (if another plugin raises UsageError). 67 def fin(): 68 ( 69 pdb.set_trace, 70 pytestPDB._pluginmanager, 71 pytestPDB._config, 72 ) = pytestPDB._saved.pop() 73 74 config._cleanup.append(fin) 75 76 77class pytestPDB(object): 78 """ Pseudo PDB that defers to the real pdb. """ 79 80 _pluginmanager = None 81 _config = None 82 _saved = [] 83 _recursive_debug = 0 84 _wrapped_pdb_cls = None 85 86 @classmethod 87 def _is_capturing(cls, capman): 88 if capman: 89 return capman.is_capturing() 90 return False 91 92 @classmethod 93 def _import_pdb_cls(cls, capman): 94 if not cls._config: 95 # Happens when using pytest.set_trace outside of a test. 96 return pdb.Pdb 97 98 usepdb_cls = cls._config.getvalue("usepdb_cls") 99 100 if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls: 101 return cls._wrapped_pdb_cls[1] 102 103 if usepdb_cls: 104 modname, classname = usepdb_cls 105 106 try: 107 __import__(modname) 108 mod = sys.modules[modname] 109 110 # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp). 111 parts = classname.split(".") 112 pdb_cls = getattr(mod, parts[0]) 113 for part in parts[1:]: 114 pdb_cls = getattr(pdb_cls, part) 115 except Exception as exc: 116 value = ":".join((modname, classname)) 117 raise UsageError( 118 "--pdbcls: could not import {!r}: {}".format(value, exc) 119 ) 120 else: 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, object): 132 _pytest_capman = capman 133 _continued = False 134 135 def do_debug(self, arg): 136 cls._recursive_debug += 1 137 ret = super(PytestPdbWrapper, self).do_debug(arg) 138 cls._recursive_debug -= 1 139 return ret 140 141 def do_continue(self, arg): 142 ret = super(PytestPdbWrapper, self).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(PytestPdbWrapper, self).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(PytestPdbWrapper, self).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(PytestPdbWrapper, self).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 %s (IO-capturing turned off)" % (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 %s" % (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(object): 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(object): 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 pyfuncitem.obj = _pdb.runcall 283 if "func" in pyfuncitem._fixtureinfo.argnames: # pragma: no branch 284 raise ValueError("--trace can't be used with a fixture named func!") 285 pyfuncitem.funcargs["func"] = testfunction 286 new_list = list(pyfuncitem._fixtureinfo.argnames) 287 new_list.append("func") 288 pyfuncitem._fixtureinfo.argnames = tuple(new_list) 289 290 291def _enter_pdb(node, excinfo, rep): 292 # XXX we re-use the TerminalReporter's terminalwriter 293 # because this seems to avoid some encoding related troubles 294 # for not completely clear reasons. 295 tw = node.config.pluginmanager.getplugin("terminalreporter")._tw 296 tw.line() 297 298 showcapture = node.config.option.showcapture 299 300 for sectionname, content in ( 301 ("stdout", rep.capstdout), 302 ("stderr", rep.capstderr), 303 ("log", rep.caplog), 304 ): 305 if showcapture in (sectionname, "all") and content: 306 tw.sep(">", "captured " + sectionname) 307 if content[-1:] == "\n": 308 content = content[:-1] 309 tw.line(content) 310 311 tw.sep(">", "traceback") 312 rep.toterminal(tw) 313 tw.sep(">", "entering PDB") 314 tb = _postmortem_traceback(excinfo) 315 rep._pdbshown = True 316 post_mortem(tb) 317 return rep 318 319 320def _postmortem_traceback(excinfo): 321 if isinstance(excinfo.value, UnexpectedException): 322 # A doctest.UnexpectedException is not useful for post_mortem. 323 # Use the underlying exception instead: 324 return excinfo.value.exc_info[2] 325 else: 326 return excinfo._excinfo[2] 327 328 329def post_mortem(t): 330 p = pytestPDB._init_pdb("post_mortem") 331 p.reset() 332 p.interaction(None, t) 333 if p.quitting: 334 outcomes.exit("Quitting debugger") 335