Skip to content

Commit 36c1d1f

Browse files
authored
PEP 553 built-in breakpoint() function (bpo-31353) (#3355)
Implement PEP 553, built-in breakpoint() with support from sys.breakpointhook(), along with documentation and tests. Closes bpo-31353
1 parent 4d07189 commit 36c1d1f

File tree

9 files changed

+324
-23
lines changed

9 files changed

+324
-23
lines changed

Doc/library/functions.rst

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,24 @@ Built-in Functions
77
The Python interpreter has a number of functions and types built into it that
88
are always available. They are listed here in alphabetical order.
99

10-
=================== ================= ================== ================ ====================
11-
.. .. Built-in Functions .. ..
12-
=================== ================= ================== ================ ====================
13-
:func:`abs` |func-dict|_ :func:`help` :func:`min` :func:`setattr`
14-
:func:`all` :func:`dir` :func:`hex` :func:`next` :func:`slice`
15-
:func:`any` :func:`divmod` :func:`id` :func:`object` :func:`sorted`
16-
:func:`ascii` :func:`enumerate` :func:`input` :func:`oct` :func:`staticmethod`
17-
:func:`bin` :func:`eval` :func:`int` :func:`open` |func-str|_
18-
:func:`bool` :func:`exec` :func:`isinstance` :func:`ord` :func:`sum`
19-
|func-bytearray|_ :func:`filter` :func:`issubclass` :func:`pow` :func:`super`
20-
|func-bytes|_ :func:`float` :func:`iter` :func:`print` |func-tuple|_
21-
:func:`callable` :func:`format` :func:`len` :func:`property` :func:`type`
22-
:func:`chr` |func-frozenset|_ |func-list|_ |func-range|_ :func:`vars`
23-
:func:`classmethod` :func:`getattr` :func:`locals` :func:`repr` :func:`zip`
24-
:func:`compile` :func:`globals` :func:`map` :func:`reversed` :func:`__import__`
10+
=================== ================= ================== ================== ====================
11+
.. .. Built-in Functions .. ..
12+
=================== ================= ================== ================== ====================
13+
:func:`abs` :func:`delattr` :func:`hash` |func-memoryview|_ |func-set|_
14+
:func:`all` |func-dict|_ :func:`help` :func:`min` :func:`setattr`
15+
:func:`any` :func:`dir` :func:`hex` :func:`next` :func:`slice`
16+
:func:`ascii` :func:`divmod` :func:`id` :func:`object` :func:`sorted`
17+
:func:`bin` :func:`enumerate` :func:`input` :func:`oct` :func:`staticmethod`
18+
:func:`bool` :func:`eval` :func:`int` :func:`open` |func-str|_
19+
:func:`breakpoint` :func:`exec` :func:`isinstance` :func:`ord` :func:`sum`
20+
|func-bytearray|_ :func:`filter` :func:`issubclass` :func:`pow` :func:`super`
21+
|func-bytes|_ :func:`float` :func:`iter` :func:`print` |func-tuple|_
22+
:func:`callable` :func:`format` :func:`len` :func:`property` :func:`type`
23+
:func:`chr` |func-frozenset|_ |func-list|_ |func-range|_ :func:`vars`
24+
:func:`classmethod` :func:`getattr` :func:`locals` :func:`repr` :func:`zip`
25+
:func:`compile` :func:`globals` :func:`map` :func:`reversed` :func:`__import__`
2526
:func:`complex` :func:`hasattr` :func:`max` :func:`round`
26-
:func:`delattr` :func:`hash` |func-memoryview|_ |func-set|_
27-
=================== ================= ================== ================ ====================
27+
=================== ================= ================== ================== ====================
2828

2929
.. using :func:`dict` would create a link to another page, so local targets are
3030
used, with replacement texts to make the output in the table consistent
@@ -113,6 +113,20 @@ are always available. They are listed here in alphabetical order.
113113
.. index:: pair: Boolean; type
114114

115115

116+
.. function:: breakpoint(*args, **kws)
117+
118+
This function drops you into the debugger at the call site. Specifically,
119+
it calls :func:`sys.breakpointhook`, passing ``args`` and ``kws`` straight
120+
through. By default, ``sys.breakpointhook()`` calls
121+
:func:`pdb.set_trace()` expecting no arguments. In this case, it is
122+
purely a convenience function so you don't have to explicitly import
123+
:mod:`pdb` or type as much code to enter the debugger. However,
124+
:func:`sys.breakpointhook` can be set to some other function and
125+
:func:`breakpoint` will automatically call that, allowing you to drop into
126+
the debugger of choice.
127+
128+
.. versionadded:: 3.7
129+
116130
.. _func-bytearray:
117131
.. class:: bytearray([source[, encoding[, errors]]])
118132
:noindex:

Doc/library/sys.rst

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,40 @@ always available.
109109
This function should be used for internal and specialized purposes only.
110110

111111

112+
.. function:: breakpointhook()
113+
114+
This hook function is called by built-in :func:`breakpoint`. By default,
115+
it drops you into the :mod:`pdb` debugger, but it can be set to any other
116+
function so that you can choose which debugger gets used.
117+
118+
The signature of this function is dependent on what it calls. For example,
119+
the default binding (e.g. ``pdb.set_trace()``) expects no arguments, but
120+
you might bind it to a function that expects additional arguments
121+
(positional and/or keyword). The built-in ``breakpoint()`` function passes
122+
its ``*args`` and ``**kws`` straight through. Whatever
123+
``breakpointhooks()`` returns is returned from ``breakpoint()``.
124+
125+
The default implementation first consults the environment variable
126+
:envvar:`PYTHONBREAKPOINT`. If that is set to ``"0"`` then this function
127+
returns immediately; i.e. it is a no-op. If the environment variable is
128+
not set, or is set to the empty string, ``pdb.set_trace()`` is called.
129+
Otherwise this variable should name a function to run, using Python's
130+
dotted-import nomenclature, e.g. ``package.subpackage.module.function``.
131+
In this case, ``package.subpackage.module`` would be imported and the
132+
resulting module must have a callable named ``function()``. This is run,
133+
passing in ``*args`` and ``**kws``, and whatever ``function()`` returns,
134+
``sys.breakpointhook()`` returns to the built-in :func:`breakpoint`
135+
function.
136+
137+
Note that if anything goes wrong while importing the callable named by
138+
:envvar:`PYTHONBREAKPOINT`, a :exc:`RuntimeWarning` is reported and the
139+
breakpoint is ignored.
140+
141+
Also note that if ``sys.breakpointhook()`` is overridden programmatically,
142+
:envvar:`PYTHONBREAKPOINT` is *not* consulted.
143+
144+
.. versionadded:: 3.7
145+
112146
.. function:: _debugmallocstats()
113147

114148
Print low-level information to stderr about the state of CPython's memory
@@ -187,14 +221,19 @@ always available.
187221
customized by assigning another three-argument function to ``sys.excepthook``.
188222

189223

190-
.. data:: __displayhook__
224+
.. data:: __breakpointhook__
225+
__displayhook__
191226
__excepthook__
192227

193-
These objects contain the original values of ``displayhook`` and ``excepthook``
194-
at the start of the program. They are saved so that ``displayhook`` and
195-
``excepthook`` can be restored in case they happen to get replaced with broken
228+
These objects contain the original values of ``breakpointhook``,
229+
``displayhook``, and ``excepthook`` at the start of the program. They are
230+
saved so that ``breakpointhook``, ``displayhook`` and ``excepthook`` can be
231+
restored in case they happen to get replaced with broken or alternative
196232
objects.
197233

234+
.. versionadded:: 3.7
235+
__breakpointhook__
236+
198237

199238
.. function:: exc_info()
200239

Doc/using/cmdline.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,18 @@ conflict.
502502
:option:`-O` multiple times.
503503

504504

505+
.. envvar:: PYTHONBREAKPOINT
506+
507+
If this is set, it names a callable using dotted-path notation. The module
508+
containing the callable will be imported and then the callable will be run
509+
by the default implementation of :func:`sys.breakpointhook` which itself is
510+
called by built-in :func:`breakpoint`. If not set, or set to the empty
511+
string, it is equivalent to the value "pdb.set_trace". Setting this to the
512+
string "0" causes the default implementation of :func:`sys.breakpointhook`
513+
to do nothing but return immediately.
514+
515+
.. versionadded:: 3.7
516+
505517
.. envvar:: PYTHONDEBUG
506518

507519
If this is set to a non-empty string it is equivalent to specifying the

Doc/whatsnew/3.7.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,25 @@ locale remains active when the core interpreter is initialized.
107107
:pep:`538` -- Coercing the legacy C locale to a UTF-8 based locale
108108
PEP written and implemented by Nick Coghlan.
109109

110+
.. _whatsnew37-pep553:
111+
112+
PEP 553: Built-in breakpoint()
113+
------------------------------
114+
115+
:pep:`553` describes a new built-in called ``breakpoint()`` which makes it
116+
easy and consistent to enter the Python debugger. Built-in ``breakpoint()``
117+
calls ``sys.breakpointhook()``. By default, this latter imports ``pdb`` and
118+
then calls ``pdb.set_trace()``, but by binding ``sys.breakpointhook()`` to the
119+
function of your choosing, ``breakpoint()`` can enter any debugger. Or, the
120+
environment variable :envvar:`PYTHONBREAKPOINT` can be set to the callable of
121+
your debugger of choice. Set ``PYTHONBREAKPOINT=0`` to completely disable
122+
built-in ``breakpoint()``.
123+
124+
.. seealso::
125+
126+
:pep:`553` -- Built-in breakpoint()
127+
PEP written and implemented by Barry Warsaw
128+
110129

111130
Other Language Changes
112131
======================

Lib/test/test_builtin.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
import types
1818
import unittest
1919
import warnings
20+
from contextlib import ExitStack
2021
from operator import neg
21-
from test.support import TESTFN, unlink, check_warnings
22+
from test.support import (
23+
EnvironmentVarGuard, TESTFN, check_warnings, swap_attr, unlink)
2224
from test.support.script_helper import assert_python_ok
25+
from unittest.mock import MagicMock, patch
2326
try:
2427
import pty, signal
2528
except ImportError:
@@ -1514,6 +1517,111 @@ def test_construct_singletons(self):
15141517
self.assertRaises(TypeError, tp, 1, 2)
15151518
self.assertRaises(TypeError, tp, a=1, b=2)
15161519

1520+
1521+
class TestBreakpoint(unittest.TestCase):
1522+
def setUp(self):
1523+
# These tests require a clean slate environment. For example, if the
1524+
# test suite is run with $PYTHONBREAKPOINT set to something else, it
1525+
# will mess up these tests. Similarly for sys.breakpointhook.
1526+
# Cleaning the slate here means you can't use breakpoint() to debug
1527+
# these tests, but I think that's okay. Just use pdb.set_trace() if
1528+
# you must.
1529+
self.resources = ExitStack()
1530+
self.addCleanup(self.resources.close)
1531+
self.env = self.resources.enter_context(EnvironmentVarGuard())
1532+
del self.env['PYTHONBREAKPOINT']
1533+
self.resources.enter_context(
1534+
swap_attr(sys, 'breakpointhook', sys.__breakpointhook__))
1535+
1536+
def test_breakpoint(self):
1537+
with patch('pdb.set_trace') as mock:
1538+
breakpoint()
1539+
mock.assert_called_once()
1540+
1541+
def test_breakpoint_with_breakpointhook_set(self):
1542+
my_breakpointhook = MagicMock()
1543+
sys.breakpointhook = my_breakpointhook
1544+
breakpoint()
1545+
my_breakpointhook.assert_called_once_with()
1546+
1547+
def test_breakpoint_with_breakpointhook_reset(self):
1548+
my_breakpointhook = MagicMock()
1549+
sys.breakpointhook = my_breakpointhook
1550+
breakpoint()
1551+
my_breakpointhook.assert_called_once_with()
1552+
# Reset the hook and it will not be called again.
1553+
sys.breakpointhook = sys.__breakpointhook__
1554+
with patch('pdb.set_trace') as mock:
1555+
breakpoint()
1556+
mock.assert_called_once_with()
1557+
my_breakpointhook.assert_called_once_with()
1558+
1559+
def test_breakpoint_with_args_and_keywords(self):
1560+
my_breakpointhook = MagicMock()
1561+
sys.breakpointhook = my_breakpointhook
1562+
breakpoint(1, 2, 3, four=4, five=5)
1563+
my_breakpointhook.assert_called_once_with(1, 2, 3, four=4, five=5)
1564+
1565+
def test_breakpoint_with_passthru_error(self):
1566+
def my_breakpointhook():
1567+
pass
1568+
sys.breakpointhook = my_breakpointhook
1569+
self.assertRaises(TypeError, breakpoint, 1, 2, 3, four=4, five=5)
1570+
1571+
@unittest.skipIf(sys.flags.ignore_environment, '-E was given')
1572+
def test_envar_good_path_builtin(self):
1573+
self.env['PYTHONBREAKPOINT'] = 'int'
1574+
with patch('builtins.int') as mock:
1575+
breakpoint('7')
1576+
mock.assert_called_once_with('7')
1577+
1578+
@unittest.skipIf(sys.flags.ignore_environment, '-E was given')
1579+
def test_envar_good_path_other(self):
1580+
self.env['PYTHONBREAKPOINT'] = 'sys.exit'
1581+
with patch('sys.exit') as mock:
1582+
breakpoint()
1583+
mock.assert_called_once_with()
1584+
1585+
@unittest.skipIf(sys.flags.ignore_environment, '-E was given')
1586+
def test_envar_good_path_noop_0(self):
1587+
self.env['PYTHONBREAKPOINT'] = '0'
1588+
with patch('pdb.set_trace') as mock:
1589+
breakpoint()
1590+
mock.assert_not_called()
1591+
1592+
def test_envar_good_path_empty_string(self):
1593+
# PYTHONBREAKPOINT='' is the same as it not being set.
1594+
self.env['PYTHONBREAKPOINT'] = ''
1595+
with patch('pdb.set_trace') as mock:
1596+
breakpoint()
1597+
mock.assert_called_once_with()
1598+
1599+
@unittest.skipIf(sys.flags.ignore_environment, '-E was given')
1600+
def test_envar_unimportable(self):
1601+
for envar in (
1602+
'.', '..', '.foo', 'foo.', '.int', 'int.'
1603+
'nosuchbuiltin',
1604+
'nosuchmodule.nosuchcallable',
1605+
):
1606+
with self.subTest(envar=envar):
1607+
self.env['PYTHONBREAKPOINT'] = envar
1608+
mock = self.resources.enter_context(patch('pdb.set_trace'))
1609+
w = self.resources.enter_context(check_warnings(quiet=True))
1610+
breakpoint()
1611+
self.assertEqual(
1612+
str(w.message),
1613+
f'Ignoring unimportable $PYTHONBREAKPOINT: "{envar}"')
1614+
self.assertEqual(w.category, RuntimeWarning)
1615+
mock.assert_not_called()
1616+
1617+
def test_envar_ignored_when_hook_is_set(self):
1618+
self.env['PYTHONBREAKPOINT'] = 'sys.exit'
1619+
with patch('sys.exit') as mock:
1620+
sys.breakpointhook = int
1621+
breakpoint()
1622+
mock.assert_not_called()
1623+
1624+
15171625
@unittest.skipUnless(pty, "the pty and signal modules must be available")
15181626
class PtyTests(unittest.TestCase):
15191627
"""Tests that use a pseudo terminal to guarantee stdin and stdout are

Lib/test/test_inspect.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3523,7 +3523,8 @@ def test_builtins_have_signatures(self):
35233523
needs_semantic_update = {"round"}
35243524
no_signature |= needs_semantic_update
35253525
# These need *args support in Argument Clinic
3526-
needs_varargs = {"min", "max", "print", "__build_class__"}
3526+
needs_varargs = {"breakpoint", "min", "max", "print",
3527+
"__build_class__"}
35273528
no_signature |= needs_varargs
35283529
# These simply weren't covered in the initial AC conversion
35293530
# for builtin callables
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:pep:`553` - Add a new built-in called ``breakpoint()`` which calls
2+
``sys.breakpointhook()``. By default this imports ``pdb`` and calls
3+
``pdb.set_trace()``, but users may override ``sys.breakpointhook()`` to call
4+
whatever debugger they want. The original value of the hook is saved in
5+
``sys.__breakpointhook__``.

Python/bltinmodule.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,28 @@ builtin_callable(PyObject *module, PyObject *obj)
422422
return PyBool_FromLong((long)PyCallable_Check(obj));
423423
}
424424

425+
static PyObject *
426+
builtin_breakpoint(PyObject *self, PyObject **args, Py_ssize_t nargs, PyObject *keywords)
427+
{
428+
PyObject *hook = PySys_GetObject("breakpointhook");
429+
430+
if (hook == NULL) {
431+
PyErr_SetString(PyExc_RuntimeError, "lost sys.breakpointhook");
432+
return NULL;
433+
}
434+
Py_INCREF(hook);
435+
PyObject *retval = _PyObject_FastCallKeywords(hook, args, nargs, keywords);
436+
Py_DECREF(hook);
437+
return retval;
438+
}
439+
440+
PyDoc_STRVAR(breakpoint_doc,
441+
"breakpoint(*args, **kws)\n\
442+
\n\
443+
Call sys.breakpointhook(*args, **kws). sys.breakpointhook() must accept\n\
444+
whatever arguments are passed.\n\
445+
\n\
446+
By default, this drops you into the pdb debugger.");
425447

426448
typedef struct {
427449
PyObject_HEAD
@@ -2627,6 +2649,7 @@ static PyMethodDef builtin_methods[] = {
26272649
BUILTIN_ANY_METHODDEF
26282650
BUILTIN_ASCII_METHODDEF
26292651
BUILTIN_BIN_METHODDEF
2652+
{"breakpoint", (PyCFunction)builtin_breakpoint, METH_FASTCALL | METH_KEYWORDS, breakpoint_doc},
26302653
BUILTIN_CALLABLE_METHODDEF
26312654
BUILTIN_CHR_METHODDEF
26322655
BUILTIN_COMPILE_METHODDEF

0 commit comments

Comments
 (0)