Respond to Guido's comments.

This commit is contained in:
Barry Warsaw 2017-10-02 13:39:55 -04:00
parent db997d4a17
commit bede39a536
1 changed files with 74 additions and 70 deletions

View File

@ -14,7 +14,8 @@ Abstract
This PEP proposes adding a new built-in function called ``breakpoint()`` which
enters a Python debugger at the point of the call. Additionally, two new
names are added to the ``sys`` module to make the debugger pluggable.
names are added to the ``sys`` module to make the choice of which debugger is
entered configurable.
Rationale
@ -40,11 +41,20 @@ enter the debugger. However this idiom has several disadvantages.
environment.
* Python linters (e.g. flake8 [linters]_) complain about this line because it
contains two statements. Breaking the idiom up into two lines further
complicates the use of the debugger,
contains two statements. Breaking the idiom up into two lines complicates
its use because there are more opportunities for mistakes at clean up time.
I.e. you might forget to delete one of those lines when you no longer need
to debug the code.
These problems can be solved by modeling a solution based on prior art in
other languages, and utilizing a convention that already exists in Python.
Python developers also have many other debuggers to choose from, but
remembering how to invoke them can be problematic. For example, even when
IDEs have user interface for setting breakpoints, it may still be more
convenient to just edit the code. The APIs for entering the debugger
programmatically are inconsistent, so it can be difficult to remember exactly
what to type.
We can solve all these problems by providing a universal API for entering the
debugger, as proposed in this PEP.
Proposal
@ -66,18 +76,26 @@ module, called ``sys.breakpointhook()`` and
``sys.__breakpointhook__``. By default, ``sys.breakpointhook()``
implements the actual importing and entry into ``pdb.set_trace()``,
and it can be set to a different function to change the debugger that
``breakpoint()`` enters. ``sys.__breakpointhook__`` then stashes the
default value of ``sys.breakpointhook()`` to make it easy to reset.
This exactly models the existing ``sys.displayhook()`` /
``sys.__displayhook__`` and ``sys.excepthook()`` /
``sys.__excepthook__`` [hooks]_.
``breakpoint()`` enters.
``sys.__breakpointhook__`` is initialized to the same function as
``sys.breakpointhook()`` so that you can always easily reset
``sys.breakpointhook()`` to the default value (e.g. by doing
``sys.breakpointhook = sys.__breakpointhook__``). This is exactly the same as
how the the existing ``sys.displayhook()`` / ``sys.__displayhook__`` and
``sys.excepthook()`` / ``sys.__excepthook__`` work [hooks]_.
The signature of the built-in is ``breakpoint(*args, **kws)``. The positional
and keyword arguments are passed straight through to ``sys.breakpointhook()``
and the signatures must match or a ``TypeError`` will be raised. The return
from ``sys.breakpointhook()`` is passed back up to, and returned from
``breakpoint()``. Since ``sys.breakpointhook()`` by default calls
``pdb.set_trace()`` by default it accepts no arguments.
``breakpoint()``.
The rationale for this is based on the observation that the underlying
debuggers may accept additional optional arguments. For example, IPython
allows you to specify a string that gets printed when the break point is
entered [ipython-embed]_. As of Python 3.7, the pdb module also supports an
optional ``header`` argument [pdb-header]_.
Environment variable
@ -118,8 +136,10 @@ are handled. Some uses cases include:
reached. This allows processes to change its value during the execution of a
program and have ``breakpoint()`` respond to those changes. It is not
considered a performance critical section since entering a debugger by
definition stops execution. (Of note, the implementation fast-tracks the
``PYTHONBREAKPOINT=0`` case.)
definition stops execution. Thus, programs can do the following::
os.environ['PYTHONBREAKPOINT'] = 'foo.bar.baz'
breakpoint() # Imports foo.bar and calls foo.bar.baz()
Overriding ``sys.breakpointhook`` defeats the default consultation of
``PYTHONBREAKPOINT``. It is up to the overriding code to consult
@ -130,62 +150,6 @@ import fails, or the resulting module does not contain the callable), a
``RuntimeWarning`` is issued, and no breakpoint function is called.
Open issues
===========
Confirmation from other debugger vendors
----------------------------------------
We want to get confirmation from at least one alternative debugger
implementation (e.g. PyCharm) that the hooks provided in this PEP will
be useful to them.
Evaluation of $PYTHONBREAKPOINT
-------------------------------
There has been some mailing list discussion around this topic. The basic
behavior as described above does not appear to be controversial. Guido has
expressed a preference for ``$PYTHONBREAKPOINT`` consultation happening in
the default implementation of ``sys.breakpointhook`` [envar]_.
The one point of discussion relates to whether the value of
``$PYTHONBREAKPOINT`` should be loaded on interpreter start, and whether its
value should be cached the first time it's accessed.
It is the PEP author's opinion that the environment variable need only be
looked up at the time of use. It is also the author's opinion that the value
of the environment variable can be accessed each time ``sys.breakpointhook``
is run, to allow for maximum functional flexibility. Because this feature
enters the debugger, any performance improvements for caching will be
negligible and do not outweigh the flexibility. Further, because the special
case of ``PYTHONBREAKPOINT=0`` is fast-tracked, the no-op code path is quite
fast, and should be in the noise given the function calls of ``breakpoint()``
-> ``sys.breakpointhook()``.
Breakpoint bytecode
-------------------
Related, there has been an idea to add a bytecode that calls
``sys.breakpointhook()``. Whether built-in ``breakpoint()`` emits
this bytecode (or gets peephole optimized to the bytecode) is an open
issue. The bytecode is useful for debuggers that actively modify
bytecode streams to trampoline into their own debugger. Having a
"breakpoint" bytecode might allow them to avoid bytecode modification
in order to invoke this trampoline. *NOTE*: It probably makes sense to split
this idea into a separate PEP.
Call a fancier object by default
--------------------------------
Some folks want to be able to use other ``pdb`` interfaces such as
``pdb.pm()``. Although this is a less commonly used API, it could be
supported by binding ``sys.breakpointhook`` to an object that implements
``__call__()``. Calling this object would call ``pdb.set_trace()``, but the
object could expose other methods, such as ``pdb.pm()``, making invocation of
it as handy as ``breakpoint.pm()``.
Implementation
@ -193,6 +157,40 @@ Implementation
A pull request exists with the proposed implementation [impl]_.
While the actual implementation is in C, the Python pseudo-code for this
feature looks roughly like the following::
# In builtins.
def breakpoint(*args, **kws):
import sys
hook = getattr(sys, 'breakpointhook', None)
if hook is None:
raise RuntimeError('lost sys.breakpointhook')
return hook(*args, **kws)
# In sys.
def breakpointhook(*args, **kws):
import importlib, os, warnings
hookname = os.getenv('PYTHONBREAKPOINT')
if hookname is None or len(hookname) == 0:
hookname = 'pdb.set_trace'
elif hookname == '0':
return None
modname, dot, funcname = hookname.rpartition('.')
if dot == '':
modname = 'builtins'
try:
module = importlib.import_module(modname)
hook = getattr(module, funcname)
except:
warnings.warn(
'Ignoring unimportable $PYTHONBREAKPOINT: {}'.format(
hookname),
RuntimeWarning)
return hook(*args, **kws)
__breakpointhook__ = breakpointhook
Rejected alternatives
=====================
@ -248,6 +246,12 @@ Version History
References
==========
.. [ipython-embed]
http://ipython.readthedocs.io/en/stable/api/generated/IPython.terminal.embed.html
.. [pdb-header]
https://docs.python.org/3.7/library/pdb.html#pdb.set_trace
.. [linters]
http://flake8.readthedocs.io/en/latest/