Skip to content

Commit 2241c98

Browse files
Merge pull request #3331 from tonybaloney/breakpoint_support
Support for the new builtin breakpoint function in Python 3.7
2 parents 7153370 + 0762666 commit 2241c98

File tree

5 files changed

+179
-0
lines changed

5 files changed

+179
-0
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Andreas Zeidler
1717
Andrzej Ostrowski
1818
Andy Freeland
1919
Anthon van der Neut
20+
Anthony Shaw
2021
Anthony Sottile
2122
Antony Lee
2223
Armin Rigo

_pytest/debugging.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@
22
from __future__ import absolute_import, division, print_function
33
import pdb
44
import sys
5+
import os
56
from doctest import UnexpectedException
67

8+
try:
9+
from builtins import breakpoint # noqa
10+
SUPPORTS_BREAKPOINT_BUILTIN = True
11+
except ImportError:
12+
SUPPORTS_BREAKPOINT_BUILTIN = False
13+
714

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

37+
# Use custom Pdb class set_trace instead of default Pdb on breakpoint() call
38+
if SUPPORTS_BREAKPOINT_BUILTIN:
39+
_environ_pythonbreakpoint = os.environ.get('PYTHONBREAKPOINT', '')
40+
if _environ_pythonbreakpoint == '':
41+
sys.breakpointhook = pytestPDB.set_trace
42+
3043
old = (pdb.set_trace, pytestPDB._pluginmanager)
3144

3245
def fin():
3346
pdb.set_trace, pytestPDB._pluginmanager = old
3447
pytestPDB._config = None
3548
pytestPDB._pdb_cls = pdb.Pdb
49+
if SUPPORTS_BREAKPOINT_BUILTIN:
50+
sys.breakpointhook = sys.__breakpointhook__
3651

3752
pdb.set_trace = pytestPDB.set_trace
3853
pytestPDB._pluginmanager = config.pluginmanager

changelog/3180.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

doc/en/usage.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,20 @@ in your code and pytest automatically disables its output capture for that test:
189189
for test output occurring after you exit the interactive PDB_ tracing session
190190
and continue with the regular test run.
191191

192+
193+
.. _`breakpoint-builtin`:
194+
195+
Using the builtin breakpoint function
196+
-------------------------------------
197+
198+
Python 3.7 introduces a builtin ``breakpoint()`` function.
199+
Pytest supports the use of ``breakpoint()`` with the following behaviours:
200+
201+
- 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``.
202+
- When tests are complete, the system will default back to the system ``Pdb`` trace UI.
203+
- If ``--pdb`` is called on execution of pytest, the custom internal Pdb trace UI is used on ``bothbreakpoint()`` and failed tests/unhandled exceptions.
204+
- 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.
205+
192206
.. _durations:
193207

194208
Profiling test execution duration

testing/test_pdb.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
from __future__ import absolute_import, division, print_function
22
import sys
33
import platform
4+
import os
45

56
import _pytest._code
7+
from _pytest.debugging import SUPPORTS_BREAKPOINT_BUILTIN
68
import pytest
79

810

11+
_ENVIRON_PYTHONBREAKPOINT = os.environ.get('PYTHONBREAKPOINT', '')
12+
13+
914
def runpdb_and_get_report(testdir, source):
1015
p = testdir.makepyfile(source)
1116
result = testdir.runpytest_inprocess("--pdb", p)
@@ -33,6 +38,30 @@ def interaction(self, *args):
3338
return called
3439

3540

41+
@pytest.fixture
42+
def custom_debugger_hook():
43+
called = []
44+
45+
# install dummy debugger class and track which methods were called on it
46+
class _CustomDebugger(object):
47+
def __init__(self, *args, **kwargs):
48+
called.append("init")
49+
50+
def reset(self):
51+
called.append("reset")
52+
53+
def interaction(self, *args):
54+
called.append("interaction")
55+
56+
def set_trace(self, frame):
57+
print("**CustomDebugger**")
58+
called.append("set_trace")
59+
60+
_pytest._CustomDebugger = _CustomDebugger
61+
yield called
62+
del _pytest._CustomDebugger
63+
64+
3665
class TestPDB(object):
3766

3867
@pytest.fixture
@@ -470,3 +499,122 @@ def test_foo():
470499

471500
child.expect('custom set_trace>')
472501
self.flush(child)
502+
503+
504+
class TestDebuggingBreakpoints(object):
505+
506+
def test_supports_breakpoint_module_global(self):
507+
"""
508+
Test that supports breakpoint global marks on Python 3.7+ and not on
509+
CPython 3.5, 2.7
510+
"""
511+
if sys.version_info.major == 3 and sys.version_info.minor >= 7:
512+
assert SUPPORTS_BREAKPOINT_BUILTIN is True
513+
if sys.version_info.major == 3 and sys.version_info.minor == 5:
514+
assert SUPPORTS_BREAKPOINT_BUILTIN is False
515+
if sys.version_info.major == 2 and sys.version_info.minor == 7:
516+
assert SUPPORTS_BREAKPOINT_BUILTIN is False
517+
518+
@pytest.mark.skipif(not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin")
519+
@pytest.mark.parametrize('arg', ['--pdb', ''])
520+
def test_sys_breakpointhook_configure_and_unconfigure(self, testdir, arg):
521+
"""
522+
Test that sys.breakpointhook is set to the custom Pdb class once configured, test that
523+
hook is reset to system value once pytest has been unconfigured
524+
"""
525+
testdir.makeconftest("""
526+
import sys
527+
from pytest import hookimpl
528+
from _pytest.debugging import pytestPDB
529+
530+
def pytest_configure(config):
531+
config._cleanup.append(check_restored)
532+
533+
def check_restored():
534+
assert sys.breakpointhook == sys.__breakpointhook__
535+
536+
def test_check():
537+
assert sys.breakpointhook == pytestPDB.set_trace
538+
""")
539+
testdir.makepyfile("""
540+
def test_nothing(): pass
541+
""")
542+
args = (arg,) if arg else ()
543+
result = testdir.runpytest_subprocess(*args)
544+
result.stdout.fnmatch_lines([
545+
'*1 passed in *',
546+
])
547+
548+
@pytest.mark.skipif(not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin")
549+
def test_pdb_custom_cls(self, testdir, custom_debugger_hook):
550+
p1 = testdir.makepyfile("""
551+
def test_nothing():
552+
breakpoint()
553+
""")
554+
result = testdir.runpytest_inprocess(
555+
"--pdb", "--pdbcls=_pytest:_CustomDebugger", p1)
556+
result.stdout.fnmatch_lines([
557+
"*CustomDebugger*",
558+
"*1 passed*",
559+
])
560+
assert custom_debugger_hook == ["init", "set_trace"]
561+
562+
@pytest.mark.parametrize('arg', ['--pdb', ''])
563+
@pytest.mark.skipif(not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin")
564+
def test_environ_custom_class(self, testdir, custom_debugger_hook, arg):
565+
testdir.makeconftest("""
566+
import os
567+
import sys
568+
569+
os.environ['PYTHONBREAKPOINT'] = '_pytest._CustomDebugger.set_trace'
570+
571+
def pytest_configure(config):
572+
config._cleanup.append(check_restored)
573+
574+
def check_restored():
575+
assert sys.breakpointhook == sys.__breakpointhook__
576+
577+
def test_check():
578+
import _pytest
579+
assert sys.breakpointhook is _pytest._CustomDebugger.set_trace
580+
""")
581+
testdir.makepyfile("""
582+
def test_nothing(): pass
583+
""")
584+
args = (arg,) if arg else ()
585+
result = testdir.runpytest_subprocess(*args)
586+
result.stdout.fnmatch_lines([
587+
'*1 passed in *',
588+
])
589+
590+
@pytest.mark.skipif(not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin")
591+
@pytest.mark.skipif(not _ENVIRON_PYTHONBREAKPOINT == '', reason="Requires breakpoint() default value")
592+
def test_sys_breakpoint_interception(self, testdir):
593+
p1 = testdir.makepyfile("""
594+
def test_1():
595+
breakpoint()
596+
""")
597+
child = testdir.spawn_pytest(str(p1))
598+
child.expect("test_1")
599+
child.expect("(Pdb)")
600+
child.sendeof()
601+
rest = child.read().decode("utf8")
602+
assert "1 failed" in rest
603+
assert "reading from stdin while output" not in rest
604+
TestPDB.flush(child)
605+
606+
@pytest.mark.skipif(not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin")
607+
def test_pdb_not_altered(self, testdir):
608+
p1 = testdir.makepyfile("""
609+
import pdb
610+
def test_1():
611+
pdb.set_trace()
612+
""")
613+
child = testdir.spawn_pytest(str(p1))
614+
child.expect("test_1")
615+
child.expect("(Pdb)")
616+
child.sendeof()
617+
rest = child.read().decode("utf8")
618+
assert "1 failed" in rest
619+
assert "reading from stdin while output" not in rest
620+
TestPDB.flush(child)

0 commit comments

Comments
 (0)