Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/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
5import types
6from typing import Generator
7from typing import Tuple
8from typing import Union
10from _pytest import outcomes
11from _pytest._code import ExceptionInfo
12from _pytest.compat import TYPE_CHECKING
13from _pytest.config import Config
14from _pytest.config import ConftestImportFailure
15from _pytest.config import hookimpl
16from _pytest.config import PytestPluginManager
17from _pytest.config.argparsing import Parser
18from _pytest.config.exceptions import UsageError
19from _pytest.nodes import Node
20from _pytest.reports import BaseReport
22if TYPE_CHECKING:
23 from _pytest.capture import CaptureManager
24 from _pytest.runner import CallInfo
27def _validate_usepdb_cls(value: str) -> Tuple[str, str]:
28 """Validate syntax of --pdbcls option."""
29 try:
30 modname, classname = value.split(":")
31 except ValueError as e:
32 raise argparse.ArgumentTypeError(
33 "{!r} is not in the format 'modname:classname'".format(value)
34 ) from e
35 return (modname, classname)
38def pytest_addoption(parser: Parser) -> None:
39 group = parser.getgroup("general")
40 group._addoption(
41 "--pdb",
42 dest="usepdb",
43 action="store_true",
44 help="start the interactive Python debugger on errors or KeyboardInterrupt.",
45 )
46 group._addoption(
47 "--pdbcls",
48 dest="usepdb_cls",
49 metavar="modulename:classname",
50 type=_validate_usepdb_cls,
51 help="start a custom interactive Python debugger on errors. "
52 "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb",
53 )
54 group._addoption(
55 "--trace",
56 dest="trace",
57 action="store_true",
58 help="Immediately break when running each test.",
59 )
62def pytest_configure(config: Config) -> None:
63 import pdb
65 if config.getvalue("trace"):
66 config.pluginmanager.register(PdbTrace(), "pdbtrace")
67 if config.getvalue("usepdb"):
68 config.pluginmanager.register(PdbInvoke(), "pdbinvoke")
70 pytestPDB._saved.append(
71 (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config)
72 )
73 pdb.set_trace = pytestPDB.set_trace
74 pytestPDB._pluginmanager = config.pluginmanager
75 pytestPDB._config = config
77 # NOTE: not using pytest_unconfigure, since it might get called although
78 # pytest_configure was not (if another plugin raises UsageError).
79 def fin() -> None:
80 (
81 pdb.set_trace,
82 pytestPDB._pluginmanager,
83 pytestPDB._config,
84 ) = pytestPDB._saved.pop()
86 config._cleanup.append(fin)
89class pytestPDB:
90 """ Pseudo PDB that defers to the real pdb. """
92 _pluginmanager = None # type: PytestPluginManager
93 _config = None # type: Config
94 _saved = [] # type: list
95 _recursive_debug = 0
96 _wrapped_pdb_cls = None
98 @classmethod
99 def _is_capturing(cls, capman: "CaptureManager") -> Union[str, bool]:
100 if capman:
101 return capman.is_capturing()
102 return False
104 @classmethod
105 def _import_pdb_cls(cls, capman: "CaptureManager"):
106 if not cls._config:
107 import pdb
109 # Happens when using pytest.set_trace outside of a test.
110 return pdb.Pdb
112 usepdb_cls = cls._config.getvalue("usepdb_cls")
114 if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls:
115 return cls._wrapped_pdb_cls[1]
117 if usepdb_cls:
118 modname, classname = usepdb_cls
120 try:
121 __import__(modname)
122 mod = sys.modules[modname]
124 # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
125 parts = classname.split(".")
126 pdb_cls = getattr(mod, parts[0])
127 for part in parts[1:]:
128 pdb_cls = getattr(pdb_cls, part)
129 except Exception as exc:
130 value = ":".join((modname, classname))
131 raise UsageError(
132 "--pdbcls: could not import {!r}: {}".format(value, exc)
133 ) from exc
134 else:
135 import pdb
137 pdb_cls = pdb.Pdb
139 wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
140 cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls)
141 return wrapped_cls
143 @classmethod
144 def _get_pdb_wrapper_class(cls, pdb_cls, capman: "CaptureManager"):
145 import _pytest.config
147 # Type ignored because mypy doesn't support "dynamic"
148 # inheritance like this.
149 class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc]
150 _pytest_capman = capman
151 _continued = False
153 def do_debug(self, arg):
154 cls._recursive_debug += 1
155 ret = super().do_debug(arg)
156 cls._recursive_debug -= 1
157 return ret
159 def do_continue(self, arg):
160 ret = super().do_continue(arg)
161 if cls._recursive_debug == 0:
162 tw = _pytest.config.create_terminal_writer(cls._config)
163 tw.line()
165 capman = self._pytest_capman
166 capturing = pytestPDB._is_capturing(capman)
167 if capturing:
168 if capturing == "global":
169 tw.sep(">", "PDB continue (IO-capturing resumed)")
170 else:
171 tw.sep(
172 ">",
173 "PDB continue (IO-capturing resumed for %s)"
174 % capturing,
175 )
176 capman.resume()
177 else:
178 tw.sep(">", "PDB continue")
179 cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self)
180 self._continued = True
181 return ret
183 do_c = do_cont = do_continue
185 def do_quit(self, arg):
186 """Raise Exit outcome when quit command is used in pdb.
188 This is a bit of a hack - it would be better if BdbQuit
189 could be handled, but this would require to wrap the
190 whole pytest run, and adjust the report etc.
191 """
192 ret = super().do_quit(arg)
194 if cls._recursive_debug == 0:
195 outcomes.exit("Quitting debugger")
197 return ret
199 do_q = do_quit
200 do_exit = do_quit
202 def setup(self, f, tb):
203 """Suspend on setup().
205 Needed after do_continue resumed, and entering another
206 breakpoint again.
207 """
208 ret = super().setup(f, tb)
209 if not ret and self._continued:
210 # pdb.setup() returns True if the command wants to exit
211 # from the interaction: do not suspend capturing then.
212 if self._pytest_capman:
213 self._pytest_capman.suspend_global_capture(in_=True)
214 return ret
216 def get_stack(self, f, t):
217 stack, i = super().get_stack(f, t)
218 if f is None:
219 # Find last non-hidden frame.
220 i = max(0, len(stack) - 1)
221 while i and stack[i][0].f_locals.get("__tracebackhide__", False):
222 i -= 1
223 return stack, i
225 return PytestPdbWrapper
227 @classmethod
228 def _init_pdb(cls, method, *args, **kwargs):
229 """ Initialize PDB debugging, dropping any IO capturing. """
230 import _pytest.config
232 if cls._pluginmanager is not None:
233 capman = cls._pluginmanager.getplugin("capturemanager")
234 else:
235 capman = None
236 if capman:
237 capman.suspend(in_=True)
239 if cls._config:
240 tw = _pytest.config.create_terminal_writer(cls._config)
241 tw.line()
243 if cls._recursive_debug == 0:
244 # Handle header similar to pdb.set_trace in py37+.
245 header = kwargs.pop("header", None)
246 if header is not None:
247 tw.sep(">", header)
248 else:
249 capturing = cls._is_capturing(capman)
250 if capturing == "global":
251 tw.sep(">", "PDB {} (IO-capturing turned off)".format(method))
252 elif capturing:
253 tw.sep(
254 ">",
255 "PDB %s (IO-capturing turned off for %s)"
256 % (method, capturing),
257 )
258 else:
259 tw.sep(">", "PDB {}".format(method))
261 _pdb = cls._import_pdb_cls(capman)(**kwargs)
263 if cls._pluginmanager:
264 cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
265 return _pdb
267 @classmethod
268 def set_trace(cls, *args, **kwargs) -> None:
269 """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
270 frame = sys._getframe().f_back
271 _pdb = cls._init_pdb("set_trace", *args, **kwargs)
272 _pdb.set_trace(frame)
275class PdbInvoke:
276 def pytest_exception_interact(
277 self, node: Node, call: "CallInfo", report: BaseReport
278 ) -> None:
279 capman = node.config.pluginmanager.getplugin("capturemanager")
280 if capman:
281 capman.suspend_global_capture(in_=True)
282 out, err = capman.read_global_capture()
283 sys.stdout.write(out)
284 sys.stdout.write(err)
285 assert call.excinfo is not None
286 _enter_pdb(node, call.excinfo, report)
288 def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None:
289 tb = _postmortem_traceback(excinfo)
290 post_mortem(tb)
293class PdbTrace:
294 @hookimpl(hookwrapper=True)
295 def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]:
296 wrap_pytest_function_for_tracing(pyfuncitem)
297 yield
300def wrap_pytest_function_for_tracing(pyfuncitem):
301 """Changes the python function object of the given Function item by a wrapper which actually
302 enters pdb before calling the python function itself, effectively leaving the user
303 in the pdb prompt in the first statement of the function.
304 """
305 _pdb = pytestPDB._init_pdb("runcall")
306 testfunction = pyfuncitem.obj
308 # we can't just return `partial(pdb.runcall, testfunction)` because (on
309 # python < 3.7.4) runcall's first param is `func`, which means we'd get
310 # an exception if one of the kwargs to testfunction was called `func`
311 @functools.wraps(testfunction)
312 def wrapper(*args, **kwargs):
313 func = functools.partial(testfunction, *args, **kwargs)
314 _pdb.runcall(func)
316 pyfuncitem.obj = wrapper
319def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
320 """Wrap the given pytestfunct item for tracing support if --trace was given in
321 the command line"""
322 if pyfuncitem.config.getvalue("trace"):
323 wrap_pytest_function_for_tracing(pyfuncitem)
326def _enter_pdb(
327 node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport
328) -> BaseReport:
329 # XXX we re-use the TerminalReporter's terminalwriter
330 # because this seems to avoid some encoding related troubles
331 # for not completely clear reasons.
332 tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
333 tw.line()
335 showcapture = node.config.option.showcapture
337 for sectionname, content in (
338 ("stdout", rep.capstdout),
339 ("stderr", rep.capstderr),
340 ("log", rep.caplog),
341 ):
342 if showcapture in (sectionname, "all") and content:
343 tw.sep(">", "captured " + sectionname)
344 if content[-1:] == "\n":
345 content = content[:-1]
346 tw.line(content)
348 tw.sep(">", "traceback")
349 rep.toterminal(tw)
350 tw.sep(">", "entering PDB")
351 tb = _postmortem_traceback(excinfo)
352 rep._pdbshown = True # type: ignore[attr-defined]
353 post_mortem(tb)
354 return rep
357def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.TracebackType:
358 from doctest import UnexpectedException
360 if isinstance(excinfo.value, UnexpectedException):
361 # A doctest.UnexpectedException is not useful for post_mortem.
362 # Use the underlying exception instead:
363 return excinfo.value.exc_info[2]
364 elif isinstance(excinfo.value, ConftestImportFailure):
365 # A config.ConftestImportFailure is not useful for post_mortem.
366 # Use the underlying exception instead:
367 return excinfo.value.excinfo[2]
368 else:
369 assert excinfo._excinfo is not None
370 return excinfo._excinfo[2]
373def post_mortem(t: types.TracebackType) -> None:
374 p = pytestPDB._init_pdb("post_mortem")
375 p.reset()
376 p.interaction(None, t)
377 if p.quitting:
378 outcomes.exit("Quitting debugger")