Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions devtools/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import sys
from collections.abc import Generator
from types import FrameType
from typing import Any, TextIO
from typing import Any, TextIO, cast

from .ansi import sformat
from .prettier import PrettyFormat
Expand Down Expand Up @@ -133,9 +133,19 @@ def format(self, *args: Any, frame_depth_: int = 2, **kwargs: Any) -> DebugOutpu
return self._process(args, kwargs, frame_depth_)

def breakpoint(self) -> None:
"""
Start the IPython debugger if IPython is installed, otherwise `pdb`.
"""
import pdb # noqa: T100

pdb.Pdb(skip=['devtools.*']).set_trace()
pdb_class: type[pdb.Pdb]
try:
from IPython.terminal.debugger import TerminalPdb # pyright: ignore[reportMissingImports, reportUnknownVariableType]
except ImportError:
pdb_class = pdb.Pdb
else:
pdb_class = cast('type[pdb.Pdb]', TerminalPdb)
pdb_class(skip=['devtools.*']).set_trace()

def timer(self, name: str | None = None, *, verbose: bool = True, file: TextIO | None = None, dp: int = 3) -> Timer:
return Timer(name=name, verbose=verbose, file=file, dp=dp)
Expand Down
2 changes: 1 addition & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The debug namespace includes a number of other useful functions:

* `debug.format()` same as calling `debug()` but returns a `DebugOutput` rather than printing the output
* `debug.timer()` returns an instance of *devtool's* `Timer` class suitable for timing code execution
* `debug.breakpoint()` introduces a breakpoint using `pdb`
* `debug.breakpoint()` introduces a breakpoint using IPython's debugger if IPython is installed, otherwise `pdb`

```py
--8<-- "examples/other.py"
Expand Down
28 changes: 26 additions & 2 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re
import sys
import types
from collections.abc import Generator
from pathlib import Path
from subprocess import run
Expand Down Expand Up @@ -248,13 +249,36 @@ def test_inspect_error(mocker: MockerFixture):
assert str(v) == "<unknown>:0 (error parsing code, call stack too shallow)\n 'x' (str) len=1"


def test_breakpoint(mocker: MockerFixture):
# not much else we can do here
def test_breakpoint_pdb(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
# a `None` entry in sys.modules makes the import raise ImportError whether or not IPython is installed
monkeypatch.setitem(sys.modules, 'IPython.terminal.debugger', None)
mocked_set_trace = mocker.patch('pdb.Pdb.set_trace')
debug.breakpoint()
assert mocked_set_trace.called


def test_breakpoint_ipython(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
ipython_set_trace = mocker.MagicMock()
constructed_with: list[list[str]] = []

class FakeTerminalPdb:
def __init__(self, skip: list[str]) -> None:
constructed_with.append(skip)

set_trace = ipython_set_trace

fake_module = types.ModuleType('IPython.terminal.debugger')
fake_module.TerminalPdb = FakeTerminalPdb # pyright: ignore[reportAttributeAccessIssue]
monkeypatch.setitem(sys.modules, 'IPython.terminal.debugger', fake_module)
mocked_pdb_set_trace = mocker.patch('pdb.Pdb.set_trace')

debug.breakpoint()

assert ipython_set_trace.called
assert constructed_with == [['devtools.*']]
assert not mocked_pdb_set_trace.called


@pytest.mark.xfail(
sys.platform == 'win32' and sys.version_info >= (3, 9),
reason='see https://github.com/alexmojaki/executing/issues/27',
Expand Down
Loading