Skip to content
This repository has been archived by the owner on Oct 1, 2024. It is now read-only.

Commit

Permalink
Improve debugging support (#119)
Browse files Browse the repository at this point in the history
* Improve debugging support

* Formatting, and including .python-version in ignore

* Add breakpointhook

* Testing around breakpointhooks

* Add docs on debugging support

* Prepare 0.52.0b0
  • Loading branch information
darrenburns authored Mar 22, 2021
1 parent 6de0557 commit 50ad3e0
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 22 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ share/
.idea/
.vscode/
codealike.json
.python-version
Binary file added docs/source/_static/debugging_support.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
author = 'Darren Burns'

# The full version, including alpha/beta/rc tags
release = '0.51.2b0'
release = '0.52.0b0'


# -- General configuration ---------------------------------------------------
Expand Down
11 changes: 10 additions & 1 deletion docs/source/guide/running_tests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,13 @@ output.
.. image:: ../_static/show_diff_symbols.png
:align: center
:height: 150
:alt: Ward output with diff symbols enabled
:alt: Ward output with diff symbols enabled

Debugging your code with ``pdb``/``breakpoint()``
-------------------------------------------------

Ward will automatically disable output capturing when you use `pdb.set_trace()` or `breakpoint()`, and re-enable it when you exit the debugger.

.. image:: ../_static/debugging_support.png
:align: center
:alt: Ward debugging example
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ path = ["ward/tests"]

[tool.poetry]
name = "ward"
version = "0.51.2b0"
version = "0.52.0b0"
description = "A modern Python testing framework"
exclude = ["ward/tests"]
authors = ["Darren Burns <[email protected]>"]
Expand Down
8 changes: 8 additions & 0 deletions ward/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
CONFIG_FILE = "pyproject.toml"


def _breakpoint_supported() -> bool:
try:
breakpoint
except NameError:
return False
return True


def read_config_toml(project_root: Path, config_file: str) -> Config:
path = project_root / config_file
if not path.is_file():
Expand Down
59 changes: 59 additions & 0 deletions ward/debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import importlib
import inspect
import io
import os
import warnings

import click
import sys

from ward.config import _breakpoint_supported
from ward.terminal import console

original_stdout = sys.stdout


def init_breakpointhooks(pdb_module, sys_module) -> None:
# breakpoint() is Python 3.7+
if _breakpoint_supported():
sys_module.breakpointhook = _breakpointhook
pdb_module.set_trace = _breakpointhook


def _breakpointhook(*args, **kwargs):
hookname = os.getenv("PYTHONBREAKPOINT")
if hookname is None or len(hookname) == 0:
hookname = "pdb.set_trace"
kwargs.setdefault("frame", inspect.currentframe().f_back)
elif hookname == "0":
return None

modname, dot, funcname = hookname.rpartition(".")
if dot == "":
modname = "builtins"

try:
module = importlib.import_module(modname)
if hookname == "pdb.set_trace":
set_trace = module.Pdb(stdout=original_stdout, skip=["ward*"]).set_trace
hook = set_trace
else:
hook = getattr(module, funcname)
except:
warnings.warn(
f"Ignoring unimportable $PYTHONBREAKPOINT: {hookname}", RuntimeWarning
)
return None

context = click.get_current_context()
capture_enabled = context.params.get("capture_output")
capture_active = isinstance(sys.stdout, io.StringIO)

if capture_enabled and capture_active:
sys.stdout = original_stdout
console.print(f"Entering {modname} - output capturing disabled.", style="info")
return hook(*args, **kwargs)
return hook(*args, **kwargs)


__breakpointhook__ = _breakpointhook
7 changes: 5 additions & 2 deletions ward/run.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import pdb

import sys
from pathlib import Path
from timeit import default_timer
from typing import Optional, Tuple, Union
from typing import Optional, Tuple

import click
import click_completion
Expand All @@ -19,6 +21,7 @@
filter_fixtures,
)
from ward.config import set_defaults_from_config
from ward.debug import init_breakpointhooks
from ward.rewrite import rewrite_assertions_in_tests
from ward.suite import Suite
from ward.fixtures import _DEFINED_FIXTURES
Expand Down Expand Up @@ -139,14 +142,14 @@ def test(
dry_run: bool,
):
"""Run tests."""
init_breakpointhooks(pdb, sys)
start_run = default_timer()
paths = [Path(p) for p in path]
mod_infos = get_info_for_modules(paths, exclude)
modules = list(load_modules(mod_infos))
unfiltered_tests = get_tests_in_modules(modules, capture_output)
filtered_tests = list(filter_tests(unfiltered_tests, query=search, tag_expr=tags,))

# Rewrite assertions in each test
tests = rewrite_assertions_in_tests(filtered_tests)

time_to_collect = default_timer() - start_run
Expand Down
15 changes: 0 additions & 15 deletions ward/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,21 +87,6 @@ def make_indent(depth=1):
console = Console(theme=theme, highlighter=NullHighlighter())


def print_no_break(e: Any):
console.print(e, end="")


def multiline_description(s: str, indent: int, width: int) -> str:
wrapped = wrap(s, width)
if len(wrapped) == 1:
return wrapped[0]
rv = wrapped[0]
for line in wrapped[1:]:
indent_str = " " * indent
rv += f"\n{indent_str}{line}"
return rv


def format_test_id(test_result: TestResult) -> str:
"""
Format module name, line number, and test case number
Expand Down
7 changes: 7 additions & 0 deletions ward/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import inspect
import traceback
import uuid
from bdb import BdbQuit
from collections import defaultdict
from contextlib import ExitStack, closing, redirect_stderr, redirect_stdout
from dataclasses import dataclass, field
Expand Down Expand Up @@ -156,6 +157,12 @@ def run(self, cache: FixtureCache, dry_run=False) -> "TestResult":
except FixtureError as e:
outcome = TestOutcome.FAIL
error: Optional[Exception] = e
except BdbQuit:
# We don't want to treat the user quitting the debugger
# as an exception, so we'll ignore BdbQuit. This will
# also prevent a large pdb-internal stack trace flooding
# the terminal.
pass
except (Exception, SystemExit) as e:
outcome = (
TestOutcome.XFAIL
Expand Down
32 changes: 32 additions & 0 deletions ward/tests/test_debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from types import SimpleNamespace
from unittest.mock import Mock

from ward import test, debug
from ward.debug import init_breakpointhooks, _breakpointhook


@test("init_breakpointhooks always patches pdb.set_trace")
def _():
mock_pdb = Mock()
init_breakpointhooks(pdb_module=mock_pdb, sys_module=Mock())
assert mock_pdb.set_trace == _breakpointhook


@test("init_breakpointhooks sets sys.breakpointhook when it's supported")
def _():
old_func = debug._breakpoint_supported
debug._breakpoint_supported = lambda: True
mock_sys = SimpleNamespace()
init_breakpointhooks(pdb_module=Mock(), sys_module=mock_sys)
debug._breakpoint_supported = old_func
assert mock_sys.breakpointhook == _breakpointhook


@test("init_breakpointhooks doesnt set breakpointhook when it's unsupported")
def _():
old_func = debug._breakpoint_supported
debug._breakpoint_supported = lambda: False
mock_sys = SimpleNamespace()
init_breakpointhooks(pdb_module=Mock(), sys_module=mock_sys)
debug._breakpoint_supported = old_func
assert not hasattr(mock_sys, "breakpointhook")
3 changes: 1 addition & 2 deletions ward/tests/test_util.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import os
import sys
from pathlib import Path

from ward import test, using, fixture
from ward.testing import each, xfail, skip
from ward.testing import each
from ward.tests.utilities import make_project
from ward.util import (
truncate,
Expand Down

0 comments on commit 50ad3e0

Please sign in to comment.