Debugger Integration
This module provides the integration of Python's interactive debugger (PDB) within the pytest testing framework. Its core purpose is to enable developers to enter an interactive debugging session automatically when tests fail, raise exceptions, or when explicitly requested via command line options. This integration facilitates immediate investigation of test failures by dropping into PDB, improving developer productivity and debugging efficiency.
Core Concepts and Purpose
Automatic Debugging on Failures: When a test encounters an error or an unexpected exception, pytest can automatically invoke the debugger at the point of failure.
Manual Debugging via CLI Options: Users can enable debugging for all tests or on demand by passing options like
--pdband--traceto pytest.Custom Debugger Support: The integration supports specifying alternative debugger classes besides the standard
pdb.Pdb, allowing, for example, IPython's enhanced debugger.Seamless Capture Management: Since pytest captures stdout/stderr during tests, the debugger integration carefully manages IO capturing to avoid interference with the interactive debugging session.
Plugin-Based Architecture: The debugger integration is implemented as a pytest plugin, registering hooks and command line options to extend pytest's behavior non-intrusively.
How the Debugger Integration Works
Command Line Options and Plugin Registration
The integration registers three key CLI options via the `pytest_addoption` hook:
--pdb: Enables starting the debugger automatically on errors orKeyboardInterrupt.--pdbcls: Allows specifying a custom debugger class by module and class name.--trace: Causes pytest to break into the debugger immediately when running each test.
Based on these options, the integration registers two plugin classes during `pytest_configure`:
PdbInvoke: Handles invoking the debugger on test errors and internal pytest errors.PdbTrace: Wraps test functions to start debugging immediately on test start when--traceis used.
The `pytest_configure` function also overrides the global [pdb.set_trace](/projects/286/67332) function with a pytest-aware wrapper to integrate with pytest's capturing and reporting systems.
Wrapping Test Functions for Immediate Debugging
When the `--trace` option is enabled, `PdbTrace` wraps each test function. The wrapper runs the test function inside the debugger's `runcall` method, causing pytest to enter the debugger prompt at the very first statement of the test function.
This wrapping is done by replacing the `obj` attribute (the test function) on the pytest test item with a debugger-invoking wrapper function.
Handling Debugging on Test Failures
When a test fails, the `pytest_exception_interact` hook in `PdbInvoke` is triggered. This hook:
Suspends any pytest output capturing to allow interaction.
Prints captured output (stdout, stderr, logs) for context.
Calls
_enter_pdbto start a post-mortem debugging session at the failure point.
The `_enter_pdb` function uses the test report to show captured output and the traceback before invoking `post_mortem` to start the debugger at the exception traceback or exception object.
Custom Debugger Class Support
Users can specify a debugger class using the `--pdbcls` option with the syntax `module:classname`. The integration dynamically imports and wraps this debugger class to integrate with pytest's capture management and reporting.
The wrapping adds features such as:
Suspending and resuming pytest IO capturing on debugger commands like continue.
Handling
quitcommands by raising a pytest-specific exit outcome to stop test execution.Overriding debugger methods to display appropriate messages about capturing state.
This wrapper class is cached to avoid repeated imports and wrapping during a test run.
Overriding pdb.set_trace
The module replaces the global [pdb.set_trace](/projects/286/67332) function with `pytestPDB.set_trace`. This replacement ensures that when [set_trace()](/projects/286/67332) is called programmatically within tests or fixtures, pytest suspends IO capturing and initializes the debugger properly with pytest's enhancements.
Interaction with Other System Components
Capture Manager (
capture.py): The debugger integration interacts closely with pytest's capture manager to pause and resume IO capturing, ensuring that the debugger's input/output works correctly.Configuration and Plugin Manager (
config/__init__.py): Registers hooks and handles CLI options; the debugger integration hooks into pytest's plugin system here.Test Reporting (
reports.py,terminal.py): Before entering the debugger on failure, it displays captured output and detailed tracebacks using pytest's reporting mechanisms.Test Execution (
runner.py): The debugger can intervene during test execution, especially when wrapping test functions for immediate tracing.Outcomes and Exit Handling (
outcomes.py): The integration uses pytest's outcome mechanism to exit cleanly when the debuggerquitcommand is issued.
Key Functionalities and Workflows
Debugging on Test Failure Workflow
Test execution raises an exception.
Pytest's hook
pytest_exception_interactis invoked.The capture manager suspends output capturing.
Captured output and tracebacks are displayed.
The debugger is initialized at the exact failure point (post-mortem).
User interacts with the debugger to inspect state.
Debugger continue resumes test execution or
quitstops the session.
Immediate Debugging on Test Start (--trace)
Pytest wraps the test function with a debugger call before execution.
The debugger prompt appears immediately on test start.
User can step through the test interactively from the first statement.
Custom Debugger Class Loading
Parses the
--pdbclsoption.Dynamically imports the specified module and retrieves the class.
Wraps the debugger class with pytest-specific functionality.
Uses this custom debugger class for all debugging sessions.
Important Design Patterns and Approaches
Decorator/Wrapper Pattern: Wrapping test functions to add debugging behavior without modifying test code.
Plugin Hook System: Uses pytest's hook system (
pytest_exception_interact,pytest_pyfunc_call) to inject debugging capabilities.Dynamic Import and Lazy Loading: Imports custom debugger classes on demand based on CLI arguments.
Stateful Singleton Class:
pytestPDBholds shared state and wrapping logic for the debugger integration.Context Management: Carefully suspends and resumes output capturing to avoid interference with user interactions.
Exception Handling and Outcome Management: Uses pytest's outcomes to handle quitting the debugger cleanly.
Code Snippets Illustrating Key Concepts
Registering CLI Options and Plugins
def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("general")
group.addoption("--pdb", dest="usepdb", action="store_true", help="Start debugger on errors")
group.addoption("--pdbcls", dest="usepdb_cls", metavar="modulename:classname", type=_validate_usepdb_cls, help="Custom debugger class")
group.addoption("--trace", dest="trace", action="store_true", help="Break immediately when running each test")
def pytest_configure(config: Config) -> None:
if config.getvalue("trace"):
config.pluginmanager.register(PdbTrace(), "pdbtrace")
if config.getvalue("usepdb"):
config.pluginmanager.register(PdbInvoke(), "pdbinvoke")
Wrapping a Test Function to Enter Debugger Immediately (--trace)
def wrap_pytest_function_for_tracing(pyfuncitem) -> None:
_pdb = pytestPDB._init_pdb("runcall")
testfunction = pyfuncitem.obj
@functools.wraps(testfunction)
def wrapper(*args, **kwargs) -> None:
func = functools.partial(testfunction, *args, **kwargs)
_pdb.runcall(func)
pyfuncitem.obj = wrapper
Entering Post-Mortem Debugging on Test Failure
def pytest_exception_interact(self, node, call, report) -> None:
capman = node.config.pluginmanager.getplugin("capturemanager")
if capman:
capman.suspend_global_capture(in_=True)
_enter_pdb(node, call.excinfo, report)
def _enter_pdb(node, excinfo, rep):
tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
tw.sep(">", "traceback")
rep.toterminal(tw)
tw.sep(">", "entering PDB")
tb_or_exc = _postmortem_exc_or_tb(excinfo)
post_mortem(tb_or_exc)
Mermaid Sequence Diagram: Debugging on Test Failure Flow
sequenceDiagram
participant TestRunner
participant CaptureManager
participant PdbInvoke
participant TerminalReporter
participant Debugger
TestRunner->>PdbInvoke: pytest_exception_interact(node, call, report)
PdbInvoke->>CaptureManager: suspend_global_capture(in_=True)
CaptureManager-->>PdbInvoke: capturing suspended
PdbInvoke->>TerminalReporter: display captured output & traceback
TerminalReporter-->>PdbInvoke: output displayed
PdbInvoke->>Debugger: post_mortem(traceback)
Debugger-->>PdbInvoke: interactive debugging started
Debugger->>TestRunner: on quit exit testing
This documentation explains how pytest integrates the Python debugger (PDB) to provide an interactive debugging experience on test failures or via command line options. The integration manages output capturing intricately and supports wrapping tests for immediate debugging, making it a powerful tool for developers diagnosing test problems.