summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Doc/library/functions.rst48
-rw-r--r--Doc/library/sys.rst47
-rw-r--r--Doc/using/cmdline.rst12
-rw-r--r--Doc/whatsnew/3.7.rst19
-rw-r--r--Lib/test/test_builtin.py110
-rw-r--r--Lib/test/test_inspect.py3
-rw-r--r--Misc/NEWS.d/next/Core and Builtins/2017-09-05-14-19-02.bpo-31353.oGZUeJ.rst5
-rw-r--r--Python/bltinmodule.c23
-rw-r--r--Python/sysmodule.c80
9 files changed, 324 insertions, 23 deletions
diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst
index 015231d9cf..08093e61fe 100644
--- a/Doc/library/functions.rst
+++ b/Doc/library/functions.rst
@@ -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:
diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst
index 85f31368c3..aa7bd477b0 100644
--- a/Doc/library/sys.rst
+++ b/Doc/library/sys.rst
@@ -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()
diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst
index 7b60a9b71f..a8cdff641e 100644
--- a/Doc/using/cmdline.rst
+++ b/Doc/using/cmdline.rst
@@ -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
diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst
index 19b766fd2f..3e8617ef2a 100644
--- a/Doc/whatsnew/3.7.rst
+++ b/Doc/whatsnew/3.7.rst
@@ -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
======================
diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py
index 87dcda7b43..0a61c05444 100644
--- a/Lib/test/test_builtin.py
+++ b/Lib/test/test_builtin.py
@@ -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
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
index 7cc1e78f78..819fcc5853 100644
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -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
diff --git a/Misc/NEWS.d/next/Core and Builtins/2017-09-05-14-19-02.bpo-31353.oGZUeJ.rst b/Misc/NEWS.d/next/Core and Builtins/2017-09-05-14-19-02.bpo-31353.oGZUeJ.rst
new file mode 100644
index 0000000000..55f81cde8a
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2017-09-05-14-19-02.bpo-31353.oGZUeJ.rst
@@ -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__``.
diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c
index 2269fe2165..6215a638c9 100644
--- a/Python/bltinmodule.c
+++ b/Python/bltinmodule.c
@@ -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
diff --git a/Python/sysmodule.c b/Python/sysmodule.c
index 6d2cc96b5e..e38a200c00 100644
--- a/Python/sysmodule.c
+++ b/Python/sysmodule.c
@@ -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",