Commit 36c1d1f1 authored by Barry Warsaw's avatar Barry Warsaw Committed by GitHub

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
parent 4d071897
...@@ -7,24 +7,24 @@ Built-in Functions ...@@ -7,24 +7,24 @@ Built-in Functions
The Python interpreter has a number of functions and types built into it that The Python interpreter has a number of functions and types built into it that
are always available. They are listed here in alphabetical order. are always available. They are listed here in alphabetical order.
=================== ================= ================== ================ ==================== =================== ================= ================== ================== ====================
.. .. Built-in Functions .. .. .. .. Built-in Functions .. ..
=================== ================= ================== ================ ==================== =================== ================= ================== ================== ====================
:func:`abs` |func-dict|_ :func:`help` :func:`min` :func:`setattr` :func:`abs` :func:`delattr` :func:`hash` |func-memoryview|_ |func-set|_
:func:`all` :func:`dir` :func:`hex` :func:`next` :func:`slice` :func:`all` |func-dict|_ :func:`help` :func:`min` :func:`setattr`
:func:`any` :func:`divmod` :func:`id` :func:`object` :func:`sorted` :func:`any` :func:`dir` :func:`hex` :func:`next` :func:`slice`
:func:`ascii` :func:`enumerate` :func:`input` :func:`oct` :func:`staticmethod` :func:`ascii` :func:`divmod` :func:`id` :func:`object` :func:`sorted`
:func:`bin` :func:`eval` :func:`int` :func:`open` |func-str|_ :func:`bin` :func:`enumerate` :func:`input` :func:`oct` :func:`staticmethod`
:func:`bool` :func:`exec` :func:`isinstance` :func:`ord` :func:`sum` :func:`bool` :func:`eval` :func:`int` :func:`open` |func-str|_
|func-bytearray|_ :func:`filter` :func:`issubclass` :func:`pow` :func:`super` :func:`breakpoint` :func:`exec` :func:`isinstance` :func:`ord` :func:`sum`
|func-bytes|_ :func:`float` :func:`iter` :func:`print` |func-tuple|_ |func-bytearray|_ :func:`filter` :func:`issubclass` :func:`pow` :func:`super`
:func:`callable` :func:`format` :func:`len` :func:`property` :func:`type` |func-bytes|_ :func:`float` :func:`iter` :func:`print` |func-tuple|_
:func:`chr` |func-frozenset|_ |func-list|_ |func-range|_ :func:`vars` :func:`callable` :func:`format` :func:`len` :func:`property` :func:`type`
:func:`classmethod` :func:`getattr` :func:`locals` :func:`repr` :func:`zip` :func:`chr` |func-frozenset|_ |func-list|_ |func-range|_ :func:`vars`
:func:`compile` :func:`globals` :func:`map` :func:`reversed` :func:`__import__` :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:`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 .. 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 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. ...@@ -113,6 +113,20 @@ are always available. They are listed here in alphabetical order.
.. index:: pair: Boolean; type .. 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: .. _func-bytearray:
.. class:: bytearray([source[, encoding[, errors]]]) .. class:: bytearray([source[, encoding[, errors]]])
:noindex: :noindex:
......
...@@ -109,6 +109,40 @@ always available. ...@@ -109,6 +109,40 @@ always available.
This function should be used for internal and specialized purposes only. 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() .. function:: _debugmallocstats()
Print low-level information to stderr about the state of CPython's memory Print low-level information to stderr about the state of CPython's memory
...@@ -187,14 +221,19 @@ always available. ...@@ -187,14 +221,19 @@ always available.
customized by assigning another three-argument function to ``sys.excepthook``. customized by assigning another three-argument function to ``sys.excepthook``.
.. data:: __displayhook__ .. data:: __breakpointhook__
__displayhook__
__excepthook__ __excepthook__
These objects contain the original values of ``displayhook`` and ``excepthook`` These objects contain the original values of ``breakpointhook``,
at the start of the program. They are saved so that ``displayhook`` and ``displayhook``, and ``excepthook`` at the start of the program. They are
``excepthook`` can be restored in case they happen to get replaced with broken saved so that ``breakpointhook``, ``displayhook`` and ``excepthook`` can be
restored in case they happen to get replaced with broken or alternative
objects. objects.
.. versionadded:: 3.7
__breakpointhook__
.. function:: exc_info() .. function:: exc_info()
......
...@@ -502,6 +502,18 @@ conflict. ...@@ -502,6 +502,18 @@ conflict.
:option:`-O` multiple times. :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 .. envvar:: PYTHONDEBUG
If this is set to a non-empty string it is equivalent to specifying the If this is set to a non-empty string it is equivalent to specifying the
......
...@@ -107,6 +107,25 @@ locale remains active when the core interpreter is initialized. ...@@ -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:`538` -- Coercing the legacy C locale to a UTF-8 based locale
PEP written and implemented by Nick Coghlan. 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 Other Language Changes
====================== ======================
......
...@@ -17,9 +17,12 @@ import traceback ...@@ -17,9 +17,12 @@ import traceback
import types import types
import unittest import unittest
import warnings import warnings
from contextlib import ExitStack
from operator import neg 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 test.support.script_helper import assert_python_ok
from unittest.mock import MagicMock, patch
try: try:
import pty, signal import pty, signal
except ImportError: except ImportError:
...@@ -1514,6 +1517,111 @@ class BuiltinTest(unittest.TestCase): ...@@ -1514,6 +1517,111 @@ class BuiltinTest(unittest.TestCase):
self.assertRaises(TypeError, tp, 1, 2) self.assertRaises(TypeError, tp, 1, 2)
self.assertRaises(TypeError, tp, a=1, b=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") @unittest.skipUnless(pty, "the pty and signal modules must be available")
class PtyTests(unittest.TestCase): class PtyTests(unittest.TestCase):
"""Tests that use a pseudo terminal to guarantee stdin and stdout are """Tests that use a pseudo terminal to guarantee stdin and stdout are
......
...@@ -3523,7 +3523,8 @@ class TestSignatureDefinitions(unittest.TestCase): ...@@ -3523,7 +3523,8 @@ class TestSignatureDefinitions(unittest.TestCase):
needs_semantic_update = {"round"} needs_semantic_update = {"round"}
no_signature |= needs_semantic_update no_signature |= needs_semantic_update
# These need *args support in Argument Clinic # 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 no_signature |= needs_varargs
# These simply weren't covered in the initial AC conversion # These simply weren't covered in the initial AC conversion
# for builtin callables # for builtin callables
......
: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__``.
...@@ -422,6 +422,28 @@ builtin_callable(PyObject *module, PyObject *obj) ...@@ -422,6 +422,28 @@ builtin_callable(PyObject *module, PyObject *obj)
return PyBool_FromLong((long)PyCallable_Check(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 { typedef struct {
PyObject_HEAD PyObject_HEAD
...@@ -2627,6 +2649,7 @@ static PyMethodDef builtin_methods[] = { ...@@ -2627,6 +2649,7 @@ static PyMethodDef builtin_methods[] = {
BUILTIN_ANY_METHODDEF BUILTIN_ANY_METHODDEF
BUILTIN_ASCII_METHODDEF BUILTIN_ASCII_METHODDEF
BUILTIN_BIN_METHODDEF BUILTIN_BIN_METHODDEF
{"breakpoint", (PyCFunction)builtin_breakpoint, METH_FASTCALL | METH_KEYWORDS, breakpoint_doc},
BUILTIN_CALLABLE_METHODDEF BUILTIN_CALLABLE_METHODDEF
BUILTIN_CHR_METHODDEF BUILTIN_CHR_METHODDEF
BUILTIN_COMPILE_METHODDEF BUILTIN_COMPILE_METHODDEF
......
...@@ -96,6 +96,81 @@ PySys_SetObject(const char *name, PyObject *v) ...@@ -96,6 +96,81 @@ PySys_SetObject(const char *name, PyObject *v)
return PyDict_SetItemString(sd, name, 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' /* Write repr(o) to sys.stdout using sys.stdout.encoding and 'backslashreplace'
error handler. If sys.stdout has a buffer attribute, use error handler. If sys.stdout has a buffer attribute, use
sys.stdout.buffer.write(encoded), otherwise redecode the string and use sys.stdout.buffer.write(encoded), otherwise redecode the string and use
...@@ -1365,6 +1440,8 @@ sys_getandroidapilevel(PyObject *self) ...@@ -1365,6 +1440,8 @@ sys_getandroidapilevel(PyObject *self)
static PyMethodDef sys_methods[] = { static PyMethodDef sys_methods[] = {
/* Might as well keep this in alphabetic order */ /* 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", (PyCFunction)sys_callstats, METH_NOARGS,
callstats_doc}, callstats_doc},
{"_clear_type_cache", sys_clear_type_cache, METH_NOARGS, {"_clear_type_cache", sys_clear_type_cache, METH_NOARGS,
...@@ -1977,6 +2054,9 @@ _PySys_BeginInit(void) ...@@ -1977,6 +2054,9 @@ _PySys_BeginInit(void)
PyDict_GetItemString(sysdict, "displayhook")); PyDict_GetItemString(sysdict, "displayhook"));
SET_SYS_FROM_STRING_BORROW("__excepthook__", SET_SYS_FROM_STRING_BORROW("__excepthook__",
PyDict_GetItemString(sysdict, "excepthook")); PyDict_GetItemString(sysdict, "excepthook"));
SET_SYS_FROM_STRING_BORROW(
"__breakpointhook__",
PyDict_GetItemString(sysdict, "breakpointhook"));
SET_SYS_FROM_STRING("version", SET_SYS_FROM_STRING("version",
PyUnicode_FromString(Py_GetVersion())); PyUnicode_FromString(Py_GetVersion()));
SET_SYS_FROM_STRING("hexversion", SET_SYS_FROM_STRING("hexversion",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment