Skip to content

Commit

Permalink
Merge pull request #3331 from tonybaloney/breakpoint_support
Browse files Browse the repository at this point in the history
Support for the new builtin breakpoint function in Python 3.7
  • Loading branch information
RonnyPfannschmidt authored Apr 10, 2018
2 parents 7153370 + 0762666 commit 2241c98
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 0 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Andreas Zeidler
Andrzej Ostrowski
Andy Freeland
Anthon van der Neut
Anthony Shaw
Anthony Sottile
Antony Lee
Armin Rigo
Expand Down
15 changes: 15 additions & 0 deletions _pytest/debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@
from __future__ import absolute_import, division, print_function
import pdb
import sys
import os
from doctest import UnexpectedException

try:
from builtins import breakpoint # noqa
SUPPORTS_BREAKPOINT_BUILTIN = True
except ImportError:
SUPPORTS_BREAKPOINT_BUILTIN = False


def pytest_addoption(parser):
group = parser.getgroup("general")
Expand All @@ -27,12 +34,20 @@ def pytest_configure(config):
if config.getvalue("usepdb"):
config.pluginmanager.register(PdbInvoke(), 'pdbinvoke')

# Use custom Pdb class set_trace instead of default Pdb on breakpoint() call
if SUPPORTS_BREAKPOINT_BUILTIN:
_environ_pythonbreakpoint = os.environ.get('PYTHONBREAKPOINT', '')
if _environ_pythonbreakpoint == '':
sys.breakpointhook = pytestPDB.set_trace

old = (pdb.set_trace, pytestPDB._pluginmanager)

def fin():
pdb.set_trace, pytestPDB._pluginmanager = old
pytestPDB._config = None
pytestPDB._pdb_cls = pdb.Pdb
if SUPPORTS_BREAKPOINT_BUILTIN:
sys.breakpointhook = sys.__breakpointhook__

pdb.set_trace = pytestPDB.set_trace
pytestPDB._pluginmanager = config.pluginmanager
Expand Down
1 change: 1 addition & 0 deletions changelog/3180.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support for Python 3.7's builtin ``breakpoint()`` method, see `Using the builtin breakpoint function <https://docs.pytest.org/en/latest/usage.html#breakpoint-builtin>`_ for details.
14 changes: 14 additions & 0 deletions doc/en/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,20 @@ in your code and pytest automatically disables its output capture for that test:
for test output occurring after you exit the interactive PDB_ tracing session
and continue with the regular test run.


.. _`breakpoint-builtin`:

Using the builtin breakpoint function
-------------------------------------

Python 3.7 introduces a builtin ``breakpoint()`` function.
Pytest supports the use of ``breakpoint()`` with the following behaviours:

- When ``breakpoint()`` is called and ``PYTHONBREAKPOINT`` is set to the default value, pytest will use the custom internal PDB trace UI instead of the system default ``Pdb``.
- When tests are complete, the system will default back to the system ``Pdb`` trace UI.
- If ``--pdb`` is called on execution of pytest, the custom internal Pdb trace UI is used on ``bothbreakpoint()`` and failed tests/unhandled exceptions.
- If ``--pdbcls`` is used, the custom class debugger will be executed when a test fails (as expected within existing behaviour), but also when ``breakpoint()`` is called from within a test, the custom class debugger will be instantiated.

.. _durations:

Profiling test execution duration
Expand Down
148 changes: 148 additions & 0 deletions testing/test_pdb.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from __future__ import absolute_import, division, print_function
import sys
import platform
import os

import _pytest._code
from _pytest.debugging import SUPPORTS_BREAKPOINT_BUILTIN
import pytest


_ENVIRON_PYTHONBREAKPOINT = os.environ.get('PYTHONBREAKPOINT', '')


def runpdb_and_get_report(testdir, source):
p = testdir.makepyfile(source)
result = testdir.runpytest_inprocess("--pdb", p)
Expand Down Expand Up @@ -33,6 +38,30 @@ def interaction(self, *args):
return called


@pytest.fixture
def custom_debugger_hook():
called = []

# install dummy debugger class and track which methods were called on it
class _CustomDebugger(object):
def __init__(self, *args, **kwargs):
called.append("init")

def reset(self):
called.append("reset")

def interaction(self, *args):
called.append("interaction")

def set_trace(self, frame):
print("**CustomDebugger**")
called.append("set_trace")

_pytest._CustomDebugger = _CustomDebugger
yield called
del _pytest._CustomDebugger


class TestPDB(object):

@pytest.fixture
Expand Down Expand Up @@ -470,3 +499,122 @@ def test_foo():

child.expect('custom set_trace>')
self.flush(child)


class TestDebuggingBreakpoints(object):

def test_supports_breakpoint_module_global(self):
"""
Test that supports breakpoint global marks on Python 3.7+ and not on
CPython 3.5, 2.7
"""
if sys.version_info.major == 3 and sys.version_info.minor >= 7:
assert SUPPORTS_BREAKPOINT_BUILTIN is True
if sys.version_info.major == 3 and sys.version_info.minor == 5:
assert SUPPORTS_BREAKPOINT_BUILTIN is False
if sys.version_info.major == 2 and sys.version_info.minor == 7:
assert SUPPORTS_BREAKPOINT_BUILTIN is False

@pytest.mark.skipif(not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin")
@pytest.mark.parametrize('arg', ['--pdb', ''])
def test_sys_breakpointhook_configure_and_unconfigure(self, testdir, arg):
"""
Test that sys.breakpointhook is set to the custom Pdb class once configured, test that
hook is reset to system value once pytest has been unconfigured
"""
testdir.makeconftest("""
import sys
from pytest import hookimpl
from _pytest.debugging import pytestPDB
def pytest_configure(config):
config._cleanup.append(check_restored)
def check_restored():
assert sys.breakpointhook == sys.__breakpointhook__
def test_check():
assert sys.breakpointhook == pytestPDB.set_trace
""")
testdir.makepyfile("""
def test_nothing(): pass
""")
args = (arg,) if arg else ()
result = testdir.runpytest_subprocess(*args)
result.stdout.fnmatch_lines([
'*1 passed in *',
])

@pytest.mark.skipif(not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin")
def test_pdb_custom_cls(self, testdir, custom_debugger_hook):
p1 = testdir.makepyfile("""
def test_nothing():
breakpoint()
""")
result = testdir.runpytest_inprocess(
"--pdb", "--pdbcls=_pytest:_CustomDebugger", p1)
result.stdout.fnmatch_lines([
"*CustomDebugger*",
"*1 passed*",
])
assert custom_debugger_hook == ["init", "set_trace"]

@pytest.mark.parametrize('arg', ['--pdb', ''])
@pytest.mark.skipif(not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin")
def test_environ_custom_class(self, testdir, custom_debugger_hook, arg):
testdir.makeconftest("""
import os
import sys
os.environ['PYTHONBREAKPOINT'] = '_pytest._CustomDebugger.set_trace'
def pytest_configure(config):
config._cleanup.append(check_restored)
def check_restored():
assert sys.breakpointhook == sys.__breakpointhook__
def test_check():
import _pytest
assert sys.breakpointhook is _pytest._CustomDebugger.set_trace
""")
testdir.makepyfile("""
def test_nothing(): pass
""")
args = (arg,) if arg else ()
result = testdir.runpytest_subprocess(*args)
result.stdout.fnmatch_lines([
'*1 passed in *',
])

@pytest.mark.skipif(not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin")
@pytest.mark.skipif(not _ENVIRON_PYTHONBREAKPOINT == '', reason="Requires breakpoint() default value")
def test_sys_breakpoint_interception(self, testdir):
p1 = testdir.makepyfile("""
def test_1():
breakpoint()
""")
child = testdir.spawn_pytest(str(p1))
child.expect("test_1")
child.expect("(Pdb)")
child.sendeof()
rest = child.read().decode("utf8")
assert "1 failed" in rest
assert "reading from stdin while output" not in rest
TestPDB.flush(child)

@pytest.mark.skipif(not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin")
def test_pdb_not_altered(self, testdir):
p1 = testdir.makepyfile("""
import pdb
def test_1():
pdb.set_trace()
""")
child = testdir.spawn_pytest(str(p1))
child.expect("test_1")
child.expect("(Pdb)")
child.sendeof()
rest = child.read().decode("utf8")
assert "1 failed" in rest
assert "reading from stdin while output" not in rest
TestPDB.flush(child)

0 comments on commit 2241c98

Please sign in to comment.