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
This commit is contained in:
Barry Warsaw 2017-10-05 12:11:18 -04:00 committed by GitHub
parent 4d07189788
commit 36c1d1f1e5
9 changed files with 324 additions and 23 deletions

View File

@ -7,24 +7,24 @@ Built-in Functions
The Python interpreter has a number of functions and types built into it that
are always available. They are listed here in alphabetical order.
=================== ================= ================== ================ ====================
.. .. Built-in Functions .. ..
=================== ================= ================== ================ ====================
:func:`abs` |func-dict|_ :func:`help` :func:`min` :func:`setattr`
:func:`all` :func:`dir` :func:`hex` :func:`next` :func:`slice`
:func:`any` :func:`divmod` :func:`id` :func:`object` :func:`sorted`
:func:`ascii` :func:`enumerate` :func:`input` :func:`oct` :func:`staticmethod`
:func:`bin` :func:`eval` :func:`int` :func:`open` |func-str|_
:func:`bool` :func:`exec` :func:`isinstance` :func:`ord` :func:`sum`
|func-bytearray|_ :func:`filter` :func:`issubclass` :func:`pow` :func:`super`
|func-bytes|_ :func:`float` :func:`iter` :func:`print` |func-tuple|_
:func:`callable` :func:`format` :func:`len` :func:`property` :func:`type`
:func:`chr` |func-frozenset|_ |func-list|_ |func-range|_ :func:`vars`
:func:`classmethod` :func:`getattr` :func:`locals` :func:`repr` :func:`zip`
:func:`compile` :func:`globals` :func:`map` :func:`reversed` :func:`__import__`
=================== ================= ================== ================== ====================
.. .. Built-in Functions .. ..
=================== ================= ================== ================== ====================
:func:`abs` :func:`delattr` :func:`hash` |func-memoryview|_ |func-set|_
:func:`all` |func-dict|_ :func:`help` :func:`min` :func:`setattr`
:func:`any` :func:`dir` :func:`hex` :func:`next` :func:`slice`
:func:`ascii` :func:`divmod` :func:`id` :func:`object` :func:`sorted`
:func:`bin` :func:`enumerate` :func:`input` :func:`oct` :func:`staticmethod`
:func:`bool` :func:`eval` :func:`int` :func:`open` |func-str|_
:func:`breakpoint` :func:`exec` :func:`isinstance` :func:`ord` :func:`sum`
|func-bytearray|_ :func:`filter` :func:`issubclass` :func:`pow` :func:`super`
|func-bytes|_ :func:`float` :func:`iter` :func:`print` |func-tuple|_
:func:`callable` :func:`format` :func:`len` :func:`property` :func:`type`
:func:`chr` |func-frozenset|_ |func-list|_ |func-range|_ :func:`vars`
:func:`classmethod` :func:`getattr` :func:`locals` :func:`repr` :func:`zip`
:func:`compile` :func:`globals` :func:`map` :func:`reversed` :func:`__import__`
:func:`complex` :func:`hasattr` :func:`max` :func:`round`
:func:`delattr` :func:`hash` |func-memoryview|_ |func-set|_
=================== ================= ================== ================ ====================
=================== ================= ================== ================== ====================
.. using :func:`dict` would create a link to another page, so local targets are
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.
.. index:: pair: Boolean; type
.. function:: breakpoint(*args, **kws)
This function drops you into the debugger at the call site. Specifically,
it calls :func:`sys.breakpointhook`, passing ``args`` and ``kws`` straight
through. By default, ``sys.breakpointhook()`` calls
:func:`pdb.set_trace()` expecting no arguments. In this case, it is
purely a convenience function so you don't have to explicitly import
:mod:`pdb` or type as much code to enter the debugger. However,
:func:`sys.breakpointhook` can be set to some other function and
:func:`breakpoint` will automatically call that, allowing you to drop into
the debugger of choice.
.. versionadded:: 3.7
.. _func-bytearray:
.. class:: bytearray([source[, encoding[, errors]]])
:noindex:

View File

@ -109,6 +109,40 @@ always available.
This function should be used for internal and specialized purposes only.
.. function:: breakpointhook()
This hook function is called by built-in :func:`breakpoint`. By default,
it drops you into the :mod:`pdb` debugger, but it can be set to any other
function so that you can choose which debugger gets used.
The signature of this function is dependent on what it calls. For example,
the default binding (e.g. ``pdb.set_trace()``) expects no arguments, but
you might bind it to a function that expects additional arguments
(positional and/or keyword). The built-in ``breakpoint()`` function passes
its ``*args`` and ``**kws`` straight through. Whatever
``breakpointhooks()`` returns is returned from ``breakpoint()``.
The default implementation first consults the environment variable
:envvar:`PYTHONBREAKPOINT`. If that is set to ``"0"`` then this function
returns immediately; i.e. it is a no-op. If the environment variable is
not set, or is set to the empty string, ``pdb.set_trace()`` is called.
Otherwise this variable should name a function to run, using Python's
dotted-import nomenclature, e.g. ``package.subpackage.module.function``.
In this case, ``package.subpackage.module`` would be imported and the
resulting module must have a callable named ``function()``. This is run,
passing in ``*args`` and ``**kws``, and whatever ``function()`` returns,
``sys.breakpointhook()`` returns to the built-in :func:`breakpoint`
function.
Note that if anything goes wrong while importing the callable named by
:envvar:`PYTHONBREAKPOINT`, a :exc:`RuntimeWarning` is reported and the
breakpoint is ignored.
Also note that if ``sys.breakpointhook()`` is overridden programmatically,
:envvar:`PYTHONBREAKPOINT` is *not* consulted.
.. versionadded:: 3.7
.. function:: _debugmallocstats()
Print low-level information to stderr about the state of CPython's memory
@ -187,14 +221,19 @@ always available.
customized by assigning another three-argument function to ``sys.excepthook``.
.. data:: __displayhook__
.. data:: __breakpointhook__
__displayhook__
__excepthook__
These objects contain the original values of ``displayhook`` and ``excepthook``
at the start of the program. They are saved so that ``displayhook`` and
``excepthook`` can be restored in case they happen to get replaced with broken
These objects contain the original values of ``breakpointhook``,
``displayhook``, and ``excepthook`` at the start of the program. They are
saved so that ``breakpointhook``, ``displayhook`` and ``excepthook`` can be
restored in case they happen to get replaced with broken or alternative
objects.
.. versionadded:: 3.7
__breakpointhook__
.. function:: exc_info()

View File

@ -502,6 +502,18 @@ conflict.
:option:`-O` multiple times.
.. envvar:: PYTHONBREAKPOINT
If this is set, it names a callable using dotted-path notation. The module
containing the callable will be imported and then the callable will be run
by the default implementation of :func:`sys.breakpointhook` which itself is
called by built-in :func:`breakpoint`. If not set, or set to the empty
string, it is equivalent to the value "pdb.set_trace". Setting this to the
string "0" causes the default implementation of :func:`sys.breakpointhook`
to do nothing but return immediately.
.. versionadded:: 3.7
.. envvar:: PYTHONDEBUG
If this is set to a non-empty string it is equivalent to specifying the

View File

@ -107,6 +107,25 @@ locale remains active when the core interpreter is initialized.
:pep:`538` -- Coercing the legacy C locale to a UTF-8 based locale
PEP written and implemented by Nick Coghlan.
.. _whatsnew37-pep553:
PEP 553: Built-in breakpoint()
------------------------------
:pep:`553` describes a new built-in called ``breakpoint()`` which makes it
easy and consistent to enter the Python debugger. Built-in ``breakpoint()``
calls ``sys.breakpointhook()``. By default, this latter imports ``pdb`` and
then calls ``pdb.set_trace()``, but by binding ``sys.breakpointhook()`` to the
function of your choosing, ``breakpoint()`` can enter any debugger. Or, the
environment variable :envvar:`PYTHONBREAKPOINT` can be set to the callable of
your debugger of choice. Set ``PYTHONBREAKPOINT=0`` to completely disable
built-in ``breakpoint()``.
.. seealso::
:pep:`553` -- Built-in breakpoint()
PEP written and implemented by Barry Warsaw
Other Language Changes
======================

View File

@ -17,9 +17,12 @@ import traceback
import types
import unittest
import warnings
from contextlib import ExitStack
from operator import neg
from test.support import TESTFN, unlink, check_warnings
from test.support import (
EnvironmentVarGuard, TESTFN, check_warnings, swap_attr, unlink)
from test.support.script_helper import assert_python_ok
from unittest.mock import MagicMock, patch
try:
import pty, signal
except ImportError:
@ -1514,6 +1517,111 @@ class BuiltinTest(unittest.TestCase):
self.assertRaises(TypeError, tp, 1, 2)
self.assertRaises(TypeError, tp, a=1, b=2)
class TestBreakpoint(unittest.TestCase):
def setUp(self):
# These tests require a clean slate environment. For example, if the
# test suite is run with $PYTHONBREAKPOINT set to something else, it
# will mess up these tests. Similarly for sys.breakpointhook.
# Cleaning the slate here means you can't use breakpoint() to debug
# these tests, but I think that's okay. Just use pdb.set_trace() if
# you must.
self.resources = ExitStack()
self.addCleanup(self.resources.close)
self.env = self.resources.enter_context(EnvironmentVarGuard())
del self.env['PYTHONBREAKPOINT']
self.resources.enter_context(
swap_attr(sys, 'breakpointhook', sys.__breakpointhook__))
def test_breakpoint(self):
with patch('pdb.set_trace') as mock:
breakpoint()
mock.assert_called_once()
def test_breakpoint_with_breakpointhook_set(self):
my_breakpointhook = MagicMock()
sys.breakpointhook = my_breakpointhook
breakpoint()
my_breakpointhook.assert_called_once_with()
def test_breakpoint_with_breakpointhook_reset(self):
my_breakpointhook = MagicMock()
sys.breakpointhook = my_breakpointhook
breakpoint()
my_breakpointhook.assert_called_once_with()
# Reset the hook and it will not be called again.
sys.breakpointhook = sys.__breakpointhook__
with patch('pdb.set_trace') as mock:
breakpoint()
mock.assert_called_once_with()
my_breakpointhook.assert_called_once_with()
def test_breakpoint_with_args_and_keywords(self):
my_breakpointhook = MagicMock()
sys.breakpointhook = my_breakpointhook
breakpoint(1, 2, 3, four=4, five=5)
my_breakpointhook.assert_called_once_with(1, 2, 3, four=4, five=5)
def test_breakpoint_with_passthru_error(self):
def my_breakpointhook():
pass
sys.breakpointhook = my_breakpointhook
self.assertRaises(TypeError, breakpoint, 1, 2, 3, four=4, five=5)
@unittest.skipIf(sys.flags.ignore_environment, '-E was given')
def test_envar_good_path_builtin(self):
self.env['PYTHONBREAKPOINT'] = 'int'
with patch('builtins.int') as mock:
breakpoint('7')
mock.assert_called_once_with('7')
@unittest.skipIf(sys.flags.ignore_environment, '-E was given')
def test_envar_good_path_other(self):
self.env['PYTHONBREAKPOINT'] = 'sys.exit'
with patch('sys.exit') as mock:
breakpoint()
mock.assert_called_once_with()
@unittest.skipIf(sys.flags.ignore_environment, '-E was given')
def test_envar_good_path_noop_0(self):
self.env['PYTHONBREAKPOINT'] = '0'
with patch('pdb.set_trace') as mock:
breakpoint()
mock.assert_not_called()
def test_envar_good_path_empty_string(self):
# PYTHONBREAKPOINT='' is the same as it not being set.
self.env['PYTHONBREAKPOINT'] = ''
with patch('pdb.set_trace') as mock:
breakpoint()
mock.assert_called_once_with()
@unittest.skipIf(sys.flags.ignore_environment, '-E was given')
def test_envar_unimportable(self):
for envar in (
'.', '..', '.foo', 'foo.', '.int', 'int.'
'nosuchbuiltin',
'nosuchmodule.nosuchcallable',
):
with self.subTest(envar=envar):
self.env['PYTHONBREAKPOINT'] = envar
mock = self.resources.enter_context(patch('pdb.set_trace'))
w = self.resources.enter_context(check_warnings(quiet=True))
breakpoint()
self.assertEqual(
str(w.message),
f'Ignoring unimportable $PYTHONBREAKPOINT: "{envar}"')
self.assertEqual(w.category, RuntimeWarning)
mock.assert_not_called()
def test_envar_ignored_when_hook_is_set(self):
self.env['PYTHONBREAKPOINT'] = 'sys.exit'
with patch('sys.exit') as mock:
sys.breakpointhook = int
breakpoint()
mock.assert_not_called()
@unittest.skipUnless(pty, "the pty and signal modules must be available")
class PtyTests(unittest.TestCase):
"""Tests that use a pseudo terminal to guarantee stdin and stdout are

View File

@ -3523,7 +3523,8 @@ class TestSignatureDefinitions(unittest.TestCase):
needs_semantic_update = {"round"}
no_signature |= needs_semantic_update
# These need *args support in Argument Clinic
needs_varargs = {"min", "max", "print", "__build_class__"}
needs_varargs = {"breakpoint", "min", "max", "print",
"__build_class__"}
no_signature |= needs_varargs
# These simply weren't covered in the initial AC conversion
# for builtin callables

View File

@ -0,0 +1,5 @@
:pep:`553` - Add a new built-in called ``breakpoint()`` which calls
``sys.breakpointhook()``. By default this imports ``pdb`` and calls
``pdb.set_trace()``, but users may override ``sys.breakpointhook()`` to call
whatever debugger they want. The original value of the hook is saved in
``sys.__breakpointhook__``.

View File

@ -422,6 +422,28 @@ builtin_callable(PyObject *module, PyObject *obj)
return PyBool_FromLong((long)PyCallable_Check(obj));
}
static PyObject *
builtin_breakpoint(PyObject *self, PyObject **args, Py_ssize_t nargs, PyObject *keywords)
{
PyObject *hook = PySys_GetObject("breakpointhook");
if (hook == NULL) {
PyErr_SetString(PyExc_RuntimeError, "lost sys.breakpointhook");
return NULL;
}
Py_INCREF(hook);
PyObject *retval = _PyObject_FastCallKeywords(hook, args, nargs, keywords);
Py_DECREF(hook);
return retval;
}
PyDoc_STRVAR(breakpoint_doc,
"breakpoint(*args, **kws)\n\
\n\
Call sys.breakpointhook(*args, **kws). sys.breakpointhook() must accept\n\
whatever arguments are passed.\n\
\n\
By default, this drops you into the pdb debugger.");
typedef struct {
PyObject_HEAD
@ -2627,6 +2649,7 @@ static PyMethodDef builtin_methods[] = {
BUILTIN_ANY_METHODDEF
BUILTIN_ASCII_METHODDEF
BUILTIN_BIN_METHODDEF
{"breakpoint", (PyCFunction)builtin_breakpoint, METH_FASTCALL | METH_KEYWORDS, breakpoint_doc},
BUILTIN_CALLABLE_METHODDEF
BUILTIN_CHR_METHODDEF
BUILTIN_COMPILE_METHODDEF

View File

@ -96,6 +96,81 @@ PySys_SetObject(const char *name, PyObject *v)
return PyDict_SetItemString(sd, name, v);
}
static PyObject *
sys_breakpointhook(PyObject *self, PyObject **args, Py_ssize_t nargs, PyObject *keywords)
{
assert(!PyErr_Occurred());
char *envar = Py_GETENV("PYTHONBREAKPOINT");
if (envar == NULL || strlen(envar) == 0) {
envar = "pdb.set_trace";
}
else if (!strcmp(envar, "0")) {
/* The breakpoint is explicitly no-op'd. */
Py_RETURN_NONE;
}
char *last_dot = strrchr(envar, '.');
char *attrname = NULL;
PyObject *modulepath = NULL;
if (last_dot == NULL) {
/* The breakpoint is a built-in, e.g. PYTHONBREAKPOINT=int */
modulepath = PyUnicode_FromString("builtins");
attrname = envar;
}
else {
/* Split on the last dot; */
modulepath = PyUnicode_FromStringAndSize(envar, last_dot - envar);
attrname = last_dot + 1;
}
if (modulepath == NULL) {
return NULL;
}
PyObject *fromlist = Py_BuildValue("(s)", attrname);
if (fromlist == NULL) {
Py_DECREF(modulepath);
return NULL;
}
PyObject *module = PyImport_ImportModuleLevelObject(
modulepath, NULL, NULL, fromlist, 0);
Py_DECREF(modulepath);
Py_DECREF(fromlist);
if (module == NULL) {
goto error;
}
PyObject *hook = PyObject_GetAttrString(module, attrname);
Py_DECREF(module);
if (hook == NULL) {
goto error;
}
PyObject *retval = _PyObject_FastCallKeywords(hook, args, nargs, keywords);
Py_DECREF(hook);
return retval;
error:
/* If any of the imports went wrong, then warn and ignore. */
PyErr_Clear();
int status = PyErr_WarnFormat(
PyExc_RuntimeWarning, 0,
"Ignoring unimportable $PYTHONBREAKPOINT: \"%s\"", envar);
if (status < 0) {
/* Printing the warning raised an exception. */
return NULL;
}
/* The warning was (probably) issued. */
Py_RETURN_NONE;
}
PyDoc_STRVAR(breakpointhook_doc,
"breakpointhook(*args, **kws)\n"
"\n"
"This hook function is called by built-in breakpoint().\n"
);
/* Write repr(o) to sys.stdout using sys.stdout.encoding and 'backslashreplace'
error handler. If sys.stdout has a buffer attribute, use
sys.stdout.buffer.write(encoded), otherwise redecode the string and use
@ -1365,6 +1440,8 @@ sys_getandroidapilevel(PyObject *self)
static PyMethodDef sys_methods[] = {
/* Might as well keep this in alphabetic order */
{"breakpointhook", (PyCFunction)sys_breakpointhook,
METH_FASTCALL | METH_KEYWORDS, breakpointhook_doc},
{"callstats", (PyCFunction)sys_callstats, METH_NOARGS,
callstats_doc},
{"_clear_type_cache", sys_clear_type_cache, METH_NOARGS,
@ -1977,6 +2054,9 @@ _PySys_BeginInit(void)
PyDict_GetItemString(sysdict, "displayhook"));
SET_SYS_FROM_STRING_BORROW("__excepthook__",
PyDict_GetItemString(sysdict, "excepthook"));
SET_SYS_FROM_STRING_BORROW(
"__breakpointhook__",
PyDict_GetItemString(sysdict, "breakpointhook"));
SET_SYS_FROM_STRING("version",
PyUnicode_FromString(Py_GetVersion()));
SET_SYS_FROM_STRING("hexversion",