Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PEP 553 built-in debug() function (bpo-31353) #3355

Merged
merged 45 commits into from Oct 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9dc577a
Watch out for NULL return.
warsaw Aug 30, 2017
bc4634d
Add a blurb entry for the issue.
warsaw Sep 4, 2017
f7e3c40
Typo in blurb entry.
warsaw Sep 4, 2017
6696c23
Merge branch 'master' into compile-null-ptr
warsaw Sep 4, 2017
4600bcf
Set an appropriate exception.
warsaw Sep 4, 2017
0f46af7
Add a debug() builtin.
warsaw Sep 4, 2017
a141f63
Document the new stuff.
warsaw Sep 4, 2017
79d7ac8
Add tests for the debug() builtin.
warsaw Sep 4, 2017
1666925
Undo a branch leak.
warsaw Sep 4, 2017
8437033
Add versionadded tags and cleanup.
warsaw Sep 5, 2017
6534542
Merge branch 'master' into debughook
warsaw Sep 5, 2017
2d75d7a
Merge branch 'master' into debughook
warsaw Sep 5, 2017
ed649fa
Add a blurb for PEP 553.
warsaw Sep 5, 2017
0bea882
Merge branch 'master' into debughook
warsaw Sep 6, 2017
eee151d
Merge branch 'master' into debughook
warsaw Sep 6, 2017
05b7cae
debug() -> breakpoint()
warsaw Sep 6, 2017
6b9a59c
Add a test for breakpointhook reset.
warsaw Sep 6, 2017
5e0b93a
Merge branch 'master' into debughook
warsaw Sep 7, 2017
c57d0dc
Signature changed to breakpoint(*args, **kws).
warsaw Sep 7, 2017
eb17d0f
Merge branch 'master' into debughook
warsaw Sep 7, 2017
244bff5
Update documentation.
warsaw Sep 7, 2017
c4bed66
Merge branch 'master' into debughook
warsaw Sep 7, 2017
6eb3470
Make patchheck happy.
warsaw Sep 7, 2017
af81484
Merge branch 'master' into debughook
warsaw Sep 8, 2017
f3200c7
Support $PYTHONBREAKPOINT
warsaw Sep 8, 2017
1c60e11
Merge branch 'master' into debughook
warsaw Sep 10, 2017
ed41006
$PYTHONBREAKPOINT processing moved to sys.breakpointhook()
warsaw Sep 12, 2017
b84a018
Accept *args, **kws
warsaw Sep 12, 2017
f6db9e3
Merge branch 'master' into debughook
warsaw Sep 12, 2017
cf9e7ac
Add a test and fix some behavior.
warsaw Sep 12, 2017
e46c59d
Merge branch 'master' into debughook
warsaw Sep 13, 2017
9bc36b8
Merge branch 'debughook' of github.com:warsaw/cpython into debughook
warsaw Sep 13, 2017
b5c2393
$PYTHONBREAKPOINT docs.
warsaw Sep 13, 2017
2846d3b
Merge branch 'master' into debughook
warsaw Oct 2, 2017
50c3e40
Add a What's New.
warsaw Oct 3, 2017
6b23d5b
Merge branch 'master' into debughook
warsaw Oct 3, 2017
0d2fae5
Several fixes addressing review comments.
warsaw Oct 4, 2017
1cf3e06
Respond to reviewer comments
warsaw Oct 5, 2017
6217e90
Use METH_FAST and _PyObject_FastCallKeywords()
warsaw Oct 5, 2017
28bd8e4
Merge branch 'master' into debughook
warsaw Oct 5, 2017
8808367
Use a MagicMock.
warsaw Oct 5, 2017
bd68b76
Merge branch 'master' into debughook
warsaw Oct 5, 2017
fea5d3d
Merge branch 'master' into debughook
warsaw Oct 5, 2017
599108d
Some tests must be skipped when -E is given
warsaw Oct 5, 2017
a74f23e
Add a missing versionadded
warsaw Oct 5, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 31 additions & 17 deletions Doc/library/functions.rst
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
47 changes: 43 additions & 4 deletions Doc/library/sys.rst
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PYTHONBREAKPOINT should be written:

:envvar:`PYTHONBREAKPOINT`

(as you did in the What's New in Python 3.7) and you should document the variable in Doc/using/cmdline.rst.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed!


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
Expand Down Expand Up @@ -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()

Expand Down
12 changes: 12 additions & 0 deletions Doc/using/cmdline.rst
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

".. versionadded:: 3.7" is missing here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


.. versionadded:: 3.7

.. envvar:: PYTHONDEBUG

If this is set to a non-empty string it is equivalent to specifying the
Expand Down
19 changes: 19 additions & 0 deletions Doc/whatsnew/3.7.rst
Expand Up @@ -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
======================
Expand Down
110 changes: 109 additions & 1 deletion Lib/test/test_builtin.py
Expand Up @@ -17,9 +17,12 @@
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:
Expand Down Expand Up @@ -1514,6 +1517,111 @@ def test_construct_singletons(self):
self.assertRaises(TypeError, tp, 1, 2)
self.assertRaises(TypeError, tp, a=1, b=2)


class TestBreakpoint(unittest.TestCase):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you should add a setUp() method using self.addCleanup() to save/restore sys.breakpointhook value, instead of doing that in each test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is a great suggestion, coupled with the observation below that the test suite could be run with PYTHONBREAKPOINT or sys.breakpointhook set, we really need the setUp() to clean the slate for the subsequent tests.

I'm going to use an ExitStack() to do this in the setUp() with a addCleanup() to close the context manager and restore the original state.

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
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_inspect.py
Expand Up @@ -3523,7 +3523,8 @@ def test_builtins_have_signatures(self):
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
Expand Down
@@ -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__``.
23 changes: 23 additions & 0 deletions Python/bltinmodule.c
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down