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:
parent
4d07189788
commit
36c1d1f1e5
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
======================
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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__``.
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue