updates
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
# mypy: allow-untyped-defs
|
||||
# ruff: noqa: T100
|
||||
"""Interactive debugging with PDB, the Python Debugger."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Generator
|
||||
import functools
|
||||
import sys
|
||||
import types
|
||||
from typing import Any
|
||||
import unittest
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from _pytest import outcomes
|
||||
from _pytest._code import ExceptionInfo
|
||||
from _pytest.capture import CaptureManager
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ConftestImportFailure
|
||||
from _pytest.config import hookimpl
|
||||
@@ -24,10 +24,13 @@ from _pytest.config.argparsing import Parser
|
||||
from _pytest.config.exceptions import UsageError
|
||||
from _pytest.nodes import Node
|
||||
from _pytest.reports import BaseReport
|
||||
from _pytest.runner import CallInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.capture import CaptureManager
|
||||
from _pytest.runner import CallInfo
|
||||
|
||||
|
||||
def _validate_usepdb_cls(value: str) -> tuple[str, str]:
|
||||
def _validate_usepdb_cls(value: str) -> Tuple[str, str]:
|
||||
"""Validate syntax of --pdbcls option."""
|
||||
try:
|
||||
modname, classname = value.split(":")
|
||||
@@ -40,13 +43,13 @@ def _validate_usepdb_cls(value: str) -> tuple[str, str]:
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
group = parser.getgroup("general")
|
||||
group.addoption(
|
||||
group._addoption(
|
||||
"--pdb",
|
||||
dest="usepdb",
|
||||
action="store_true",
|
||||
help="Start the interactive Python debugger on errors or KeyboardInterrupt",
|
||||
)
|
||||
group.addoption(
|
||||
group._addoption(
|
||||
"--pdbcls",
|
||||
dest="usepdb_cls",
|
||||
metavar="modulename:classname",
|
||||
@@ -54,7 +57,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
help="Specify a custom interactive Python debugger for use with --pdb."
|
||||
"For example: --pdbcls=IPython.terminal.debugger:TerminalPdb",
|
||||
)
|
||||
group.addoption(
|
||||
group._addoption(
|
||||
"--trace",
|
||||
dest="trace",
|
||||
action="store_true",
|
||||
@@ -92,22 +95,22 @@ def pytest_configure(config: Config) -> None:
|
||||
class pytestPDB:
|
||||
"""Pseudo PDB that defers to the real pdb."""
|
||||
|
||||
_pluginmanager: PytestPluginManager | None = None
|
||||
_config: Config | None = None
|
||||
_saved: list[
|
||||
tuple[Callable[..., None], PytestPluginManager | None, Config | None]
|
||||
_pluginmanager: Optional[PytestPluginManager] = None
|
||||
_config: Optional[Config] = None
|
||||
_saved: List[
|
||||
Tuple[Callable[..., None], Optional[PytestPluginManager], Optional[Config]]
|
||||
] = []
|
||||
_recursive_debug = 0
|
||||
_wrapped_pdb_cls: tuple[type[Any], type[Any]] | None = None
|
||||
_wrapped_pdb_cls: Optional[Tuple[Type[Any], Type[Any]]] = None
|
||||
|
||||
@classmethod
|
||||
def _is_capturing(cls, capman: CaptureManager | None) -> str | bool:
|
||||
def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]:
|
||||
if capman:
|
||||
return capman.is_capturing()
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _import_pdb_cls(cls, capman: CaptureManager | None):
|
||||
def _import_pdb_cls(cls, capman: Optional["CaptureManager"]):
|
||||
if not cls._config:
|
||||
import pdb
|
||||
|
||||
@@ -146,10 +149,12 @@ class pytestPDB:
|
||||
return wrapped_cls
|
||||
|
||||
@classmethod
|
||||
def _get_pdb_wrapper_class(cls, pdb_cls, capman: CaptureManager | None):
|
||||
def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]):
|
||||
import _pytest.config
|
||||
|
||||
class PytestPdbWrapper(pdb_cls):
|
||||
# Type ignored because mypy doesn't support "dynamic"
|
||||
# inheritance like this.
|
||||
class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc]
|
||||
_pytest_capman = capman
|
||||
_continued = False
|
||||
|
||||
@@ -159,9 +164,6 @@ class pytestPDB:
|
||||
cls._recursive_debug -= 1
|
||||
return ret
|
||||
|
||||
if hasattr(pdb_cls, "do_debug"):
|
||||
do_debug.__doc__ = pdb_cls.do_debug.__doc__
|
||||
|
||||
def do_continue(self, arg):
|
||||
ret = super().do_continue(arg)
|
||||
if cls._recursive_debug == 0:
|
||||
@@ -177,7 +179,8 @@ class pytestPDB:
|
||||
else:
|
||||
tw.sep(
|
||||
">",
|
||||
f"PDB continue (IO-capturing resumed for {capturing})",
|
||||
"PDB continue (IO-capturing resumed for %s)"
|
||||
% capturing,
|
||||
)
|
||||
assert capman is not None
|
||||
capman.resume()
|
||||
@@ -188,17 +191,15 @@ class pytestPDB:
|
||||
self._continued = True
|
||||
return ret
|
||||
|
||||
if hasattr(pdb_cls, "do_continue"):
|
||||
do_continue.__doc__ = pdb_cls.do_continue.__doc__
|
||||
|
||||
do_c = do_cont = do_continue
|
||||
|
||||
def do_quit(self, arg):
|
||||
# Raise Exit outcome when quit command is used in pdb.
|
||||
#
|
||||
# This is a bit of a hack - it would be better if BdbQuit
|
||||
# could be handled, but this would require to wrap the
|
||||
# whole pytest run, and adjust the report etc.
|
||||
"""Raise Exit outcome when quit command is used in pdb.
|
||||
|
||||
This is a bit of a hack - it would be better if BdbQuit
|
||||
could be handled, but this would require to wrap the
|
||||
whole pytest run, and adjust the report etc.
|
||||
"""
|
||||
ret = super().do_quit(arg)
|
||||
|
||||
if cls._recursive_debug == 0:
|
||||
@@ -206,9 +207,6 @@ class pytestPDB:
|
||||
|
||||
return ret
|
||||
|
||||
if hasattr(pdb_cls, "do_quit"):
|
||||
do_quit.__doc__ = pdb_cls.do_quit.__doc__
|
||||
|
||||
do_q = do_quit
|
||||
do_exit = do_quit
|
||||
|
||||
@@ -243,7 +241,7 @@ class pytestPDB:
|
||||
import _pytest.config
|
||||
|
||||
if cls._pluginmanager is None:
|
||||
capman: CaptureManager | None = None
|
||||
capman: Optional[CaptureManager] = None
|
||||
else:
|
||||
capman = cls._pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
@@ -265,7 +263,8 @@ class pytestPDB:
|
||||
elif capturing:
|
||||
tw.sep(
|
||||
">",
|
||||
f"PDB {method} (IO-capturing turned off for {capturing})",
|
||||
"PDB %s (IO-capturing turned off for %s)"
|
||||
% (method, capturing),
|
||||
)
|
||||
else:
|
||||
tw.sep(">", f"PDB {method}")
|
||||
@@ -286,7 +285,7 @@ class pytestPDB:
|
||||
|
||||
class PdbInvoke:
|
||||
def pytest_exception_interact(
|
||||
self, node: Node, call: CallInfo[Any], report: BaseReport
|
||||
self, node: Node, call: "CallInfo[Any]", report: BaseReport
|
||||
) -> None:
|
||||
capman = node.config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
@@ -300,18 +299,18 @@ class PdbInvoke:
|
||||
_enter_pdb(node, call.excinfo, report)
|
||||
|
||||
def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
||||
exc_or_tb = _postmortem_exc_or_tb(excinfo)
|
||||
post_mortem(exc_or_tb)
|
||||
tb = _postmortem_traceback(excinfo)
|
||||
post_mortem(tb)
|
||||
|
||||
|
||||
class PdbTrace:
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]:
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]:
|
||||
wrap_pytest_function_for_tracing(pyfuncitem)
|
||||
return (yield)
|
||||
yield
|
||||
|
||||
|
||||
def wrap_pytest_function_for_tracing(pyfuncitem) -> None:
|
||||
def wrap_pytest_function_for_tracing(pyfuncitem):
|
||||
"""Change the Python function object of the given Function item by a
|
||||
wrapper which actually enters pdb before calling the python function
|
||||
itself, effectively leaving the user in the pdb prompt in the first
|
||||
@@ -323,14 +322,14 @@ def wrap_pytest_function_for_tracing(pyfuncitem) -> None:
|
||||
# python < 3.7.4) runcall's first param is `func`, which means we'd get
|
||||
# an exception if one of the kwargs to testfunction was called `func`.
|
||||
@functools.wraps(testfunction)
|
||||
def wrapper(*args, **kwargs) -> None:
|
||||
def wrapper(*args, **kwargs):
|
||||
func = functools.partial(testfunction, *args, **kwargs)
|
||||
_pdb.runcall(func)
|
||||
|
||||
pyfuncitem.obj = wrapper
|
||||
|
||||
|
||||
def maybe_wrap_pytest_function_for_tracing(pyfuncitem) -> None:
|
||||
def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
|
||||
"""Wrap the given pytestfunct item for tracing support if --trace was given in
|
||||
the command line."""
|
||||
if pyfuncitem.config.getvalue("trace"):
|
||||
@@ -340,7 +339,7 @@ def maybe_wrap_pytest_function_for_tracing(pyfuncitem) -> None:
|
||||
def _enter_pdb(
|
||||
node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport
|
||||
) -> BaseReport:
|
||||
# XXX we reuse the TerminalReporter's terminalwriter
|
||||
# XXX we re-use the TerminalReporter's terminalwriter
|
||||
# because this seems to avoid some encoding related troubles
|
||||
# for not completely clear reasons.
|
||||
tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
|
||||
@@ -362,46 +361,31 @@ def _enter_pdb(
|
||||
tw.sep(">", "traceback")
|
||||
rep.toterminal(tw)
|
||||
tw.sep(">", "entering PDB")
|
||||
tb_or_exc = _postmortem_exc_or_tb(excinfo)
|
||||
tb = _postmortem_traceback(excinfo)
|
||||
rep._pdbshown = True # type: ignore[attr-defined]
|
||||
post_mortem(tb_or_exc)
|
||||
post_mortem(tb)
|
||||
return rep
|
||||
|
||||
|
||||
def _postmortem_exc_or_tb(
|
||||
excinfo: ExceptionInfo[BaseException],
|
||||
) -> types.TracebackType | BaseException:
|
||||
def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.TracebackType:
|
||||
from doctest import UnexpectedException
|
||||
|
||||
get_exc = sys.version_info >= (3, 13)
|
||||
if isinstance(excinfo.value, UnexpectedException):
|
||||
# A doctest.UnexpectedException is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
underlying_exc = excinfo.value
|
||||
if get_exc:
|
||||
return underlying_exc.exc_info[1]
|
||||
|
||||
return underlying_exc.exc_info[2]
|
||||
return excinfo.value.exc_info[2]
|
||||
elif isinstance(excinfo.value, ConftestImportFailure):
|
||||
# A config.ConftestImportFailure is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
cause = excinfo.value.cause
|
||||
if get_exc:
|
||||
return cause
|
||||
|
||||
assert cause.__traceback__ is not None
|
||||
return cause.__traceback__
|
||||
return excinfo.value.excinfo[2]
|
||||
else:
|
||||
assert excinfo._excinfo is not None
|
||||
if get_exc:
|
||||
return excinfo._excinfo[1]
|
||||
|
||||
return excinfo._excinfo[2]
|
||||
|
||||
|
||||
def post_mortem(tb_or_exc: types.TracebackType | BaseException) -> None:
|
||||
def post_mortem(t: types.TracebackType) -> None:
|
||||
p = pytestPDB._init_pdb("post_mortem")
|
||||
p.reset()
|
||||
p.interaction(None, tb_or_exc)
|
||||
p.interaction(None, t)
|
||||
if p.quitting:
|
||||
outcomes.exit("Quitting debugger")
|
||||
|
||||
Reference in New Issue
Block a user