diff options
-rw-r--r-- | Doc/library/functions.rst | 48 | ||||
-rw-r--r-- | Doc/library/sys.rst | 47 | ||||
-rw-r--r-- | Doc/using/cmdline.rst | 12 | ||||
-rw-r--r-- | Doc/whatsnew/3.7.rst | 19 | ||||
-rw-r--r-- | Lib/test/test_builtin.py | 110 | ||||
-rw-r--r-- | Lib/test/test_inspect.py | 3 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Core and Builtins/2017-09-05-14-19-02.bpo-31353.oGZUeJ.rst | 5 | ||||
-rw-r--r-- | Python/bltinmodule.c | 23 | ||||
-rw-r--r-- | Python/sysmodule.c | 80 |
9 files changed, 324 insertions, 23 deletions
diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 015231d..08093e6 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 85f3136..aa7bd47 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 7b60a9b..a8cdff6 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 19b766f..3e8617e 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 87dcda7..0a61c05 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 7cc1e78..819fcc5 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 0000000..55f81cd --- /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 2269fe2..6215a63 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 6d2cc96..e38a200 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", |