Coverage for /usr/local/lib/python3.7/site-packages/_pytest/debugging.py : 5%

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
6from _pytest import outcomes
7from _pytest.config import hookimpl
8from _pytest.config.exceptions import UsageError
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)
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 )
46def pytest_configure(config):
47 import pdb
49 if config.getvalue("trace"):
50 config.pluginmanager.register(PdbTrace(), "pdbtrace")
51 if config.getvalue("usepdb"):
52 config.pluginmanager.register(PdbInvoke(), "pdbinvoke")
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
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()
70 config._cleanup.append(fin)
73class pytestPDB:
74 """ Pseudo PDB that defers to the real pdb. """
76 _pluginmanager = None
77 _config = None
78 _saved = [] # type: list
79 _recursive_debug = 0
80 _wrapped_pdb_cls = None
82 @classmethod
83 def _is_capturing(cls, capman):
84 if capman:
85 return capman.is_capturing()
86 return False
88 @classmethod
89 def _import_pdb_cls(cls, capman):
90 if not cls._config:
91 import pdb
93 # Happens when using pytest.set_trace outside of a test.
94 return pdb.Pdb
96 usepdb_cls = cls._config.getvalue("usepdb_cls")
98 if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls:
99 return cls._wrapped_pdb_cls[1]
101 if usepdb_cls:
102 modname, classname = usepdb_cls
104 try:
105 __import__(modname)
106 mod = sys.modules[modname]
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
121 pdb_cls = pdb.Pdb
123 wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
124 cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls)
125 return wrapped_cls
127 @classmethod
128 def _get_pdb_wrapper_class(cls, pdb_cls, capman):
129 import _pytest.config
131 class PytestPdbWrapper(pdb_cls):
132 _pytest_capman = capman
133 _continued = False
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
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()
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
165 do_c = do_cont = do_continue
167 def do_quit(self, arg):
168 """Raise Exit outcome when quit command is used in pdb.
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)
176 if cls._recursive_debug == 0:
177 outcomes.exit("Quitting debugger")
179 return ret
181 do_q = do_quit
182 do_exit = do_quit
184 def setup(self, f, tb):
185 """Suspend on setup().
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
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
207 return PytestPdbWrapper
209 @classmethod
210 def _init_pdb(cls, method, *args, **kwargs):
211 """ Initialize PDB debugging, dropping any IO capturing. """
212 import _pytest.config
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)
221 if cls._config:
222 tw = _pytest.config.create_terminal_writer(cls._config)
223 tw.line()
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))
243 _pdb = cls._import_pdb_cls(capman)(**kwargs)
245 if cls._pluginmanager:
246 cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
247 return _pdb
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)
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)
267 def pytest_internalerror(self, excrepr, excinfo):
268 tb = _postmortem_traceback(excinfo)
269 post_mortem(tb)
272class PdbTrace:
273 @hookimpl(hookwrapper=True)
274 def pytest_pyfunc_call(self, pyfuncitem):
275 _test_pytest_function(pyfuncitem)
276 yield
279def _test_pytest_function(pyfuncitem):
280 _pdb = pytestPDB._init_pdb("runcall")
281 testfunction = pyfuncitem.obj
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)
291 pyfuncitem.obj = wrapper
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()
301 showcapture = node.config.option.showcapture
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)
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
323def _postmortem_traceback(excinfo):
324 from doctest import UnexpectedException
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]
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")