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