Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

gh-106670: Allow Pdb to move between chained exceptions#106676

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
iritkatriel merged 12 commits intopython:mainfromCarreau:gh-106670
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
NextNext commit
Allow Pdb to move between chained exception.
This lets Pdb receive an exception, instead of a traceback, and whenthis is the case and the exception are chained, the new `exceptions` commandallows to both list (no arguments) and move between the chained exceptions.That is to say if you have something like    def out():        try:            middle()                                # B        except Exception as e:            raise ValueError("foo(): bar failed")   # A    def middle():        try:            return inner(0)                         # D        except Exception as e:            raise ValueError("Middle fail") from e  # C    def inner(x):        1 / x                                       # EOnly A was reachable after calling `out()` and doing post mortem debug.With this all A-E points are reachable with a combination of up/down,and ``exception <number>``.This also change the default behavior of ``pdb.pm()``, as well as`python -m pdb <script.py>` to receive `sys.last_exc` so that chainedexception navigation is enabled.We do follow the logic of the ``traceback`` module and handle the``_context__`` and ``__cause__`` in the same way. That is to say, we try``__cause__`` first, and if not present chain with ``__context__``. Inthe same vein, if we encounter an exception that has``__suppress_context__`` (like when ``raise ... from None``), we do stopwalking the chain.Some implementation notes: - We do handle cycle in exceptions - cleanup of references to tracebacks are not cleared in ``forget()``, as   ``setup()`` and ``forget()`` are both for setting a single   exception. - We do not handle sub-exceptions of exception groups.Closesgh-106670
  • Loading branch information
@Carreau
Carreau committedAug 16, 2023
commitd39fbceda41ab9d8ff03beabbe6e1f2ba505401d
52 changes: 50 additions & 2 deletionsDoc/library/pdb.rst
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,8 +175,8 @@ slightly different way:

.. function:: pm()

Enter post-mortem debugging of thetraceback found in
:data:`sys.last_traceback`.
Enter post-mortem debugging of theexception found in
:data:`sys.last_exc`.


The ``run*`` functions and :func:`set_trace` are aliases for instantiating the
Expand DownExpand Up@@ -639,6 +639,54 @@ can be overridden by the local file.

Print the return value for the last return of the current function.

.. pdbcommand:: exceptions [excnumber]

List or jump between chained exceptions.

When using ``pdb.pm()`` or ``Pdb.post_mortem(...)`` with a chained exception
instead of a traceback, it will now allow the user to move between the
chained exceptions using ``exceptions`` command to list exceptions, and
``exception <number>`` to switch to that exception.


Example::

def out():
try:
middle()
except Exception as e:
raise ValueError("reraise middle() error") from e

def middle():
try:
return inner(0)
except Exception as e:
raise ValueError("Middle fail")

def inner(x):
1 / x

out()

calling ``pdb.pm()`` will allow to move between exceptions::

> example.py(5)out()
-> raise ValueError("reraise middle() error") from e

(Pdb) exceptions
0 ZeroDivisionError('division by zero')
1 ValueError('Middle fail')
> 2 ValueError('reraise middle() error')

(Pdb) exceptions 0
> example.py(16)inner()
-> 1 / x

(Pdb) up
> example.py(10)middle()
-> return inner(0)


.. rubric:: Footnotes

.. [1] Whether a frame is considered to originate in a certain module
Expand Down
95 changes: 86 additions & 9 deletionsLib/pdb.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -206,6 +206,10 @@ def namespace(self):
line_prefix = '\n-> ' # Probably a better default

class Pdb(bdb.Bdb, cmd.Cmd):
# the max number of chained exceptions + exception groups we accept to navigate.
Copy link
Member

Choose a reason for hiding this comment

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

is this comment in the right place?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

moved and rephrased.

_max_chained_exception_depth = 999
_chained_exceptions = tuple()
_chained_exception_index = 0

Choose a reason for hiding this comment

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

Why do we put these in the class? Is there any case_chained_exceptions and_chained_exception_index would be used without proper initialization in the instances?previous_sigint_handler is used asPdb._previous_sigint_handler and it's probably set for allPdb instances, but the exceptions are per instance right? I think we should init it as instance members in__init__.

As for_max_chained_exception_depth, if that's an arbitrary constant without a documented way to change, we can consider that as a constant and useMAX_CHAINED_EXCEPTION_DEPTH?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

I rarely see multiple Pdb instances used so I don't have any particular preferences.

Though I don't quite like global variables, and I have the impression that attaching constant to the only class that use them make things a tiny bit more self contained and discoverable.

I still did both of the changes you suggested though.

Choose a reason for hiding this comment

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

I obviously misrepresented myself. I meant useMAX_CHAINED_EXCEPTION_DEPTH as a class member instead of_max_chained_exception_depth. Just change the variable name to indicate that this is a constant instead of a private variable. I still want to put it inclass Pdb because that's the only scope where it would be used.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Ah, sure, I moved it back as a class variable.


_previous_sigint_handler = None

Expand DownExpand Up@@ -414,8 +418,37 @@ def preloop(self):
self.message('display %s: %r [old: %r]' %
(expr, newvalue, oldvalue))

def interaction(self, frame,traceback):
def interaction(self, frame,tb_or_exc):
# Restore the previous signal handler at the Pdb prompt.

_exceptions = []
if isinstance(tb_or_exc, BaseException):
traceback, exception = tb_or_exc.__traceback__, tb_or_exc
current = exception

while current is not None:
if current in _exceptions:
break
_exceptions.append(current)
if current.__cause__ is not None:
current = current.__cause__
elif (
current.__context__ is not None and not current.__suppress_context__
):
current = current.__context__

if len(_exceptions) >= self._max_chained_exception_depth:
self.message(
f"More than {self._max_chained_exception_depth}"
" chained exceptions found, not all exceptions"
"will be browsable with `exceptions`."
)
break
else:
traceback = tb_or_exc
self._chained_exceptions = tuple(reversed(_exceptions))
self._chained_exception_index = len(_exceptions) - 1

if Pdb._previous_sigint_handler:
try:
signal.signal(signal.SIGINT, Pdb._previous_sigint_handler)
Expand All@@ -432,6 +465,12 @@ def interaction(self, frame, traceback):
self._cmdloop()
self.forget()

# we can't put those in forget as otherwise they would
# be cleared on exception change
self._chained_exceptions = tuple()
self._chained_exception_index = 0

Choose a reason for hiding this comment

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

We should keep the single line between methods.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Done.


def displayhook(self, obj):
"""Custom displayhook for the exec in default(), which prevents
assignment of the _ variable in the builtins.
Expand DownExpand Up@@ -1073,6 +1112,44 @@ def _select_frame(self, number):
self.print_stack_entry(self.stack[self.curindex])
self.lineno = None

def do_exceptions(self, arg):
"""exceptions [number]

List or change current exception in an exception chain.

Without arguments, list all the current exception in the exception
chain. Exceptions will be numbered, with the current exception indicated
with an arrow.

If given an integer as argument, switch to the exception at that index.
"""
if not self._chained_exceptions:
self.message(
"Did not find chained exceptions. To move between"
" exceptions, pdb/post_mortem must be given an exception"
" object instead of a traceback."
)
return
if not arg:
for ix, exc in enumerate(self._chained_exceptions):
prompt = ">" if ix == self._chained_exception_index else " "
rep = repr(exc)
if len(rep) > 80:
rep = rep[:77] + "..."
self.message(f"{prompt} {ix:>3} {rep}")
else:
try:
number = int(arg)
except ValueError:
self.error("Argument must be an integer")
return
if 0 <= number < len(self._chained_exceptions):
self._chained_exception_index = number
self.setup(None, self._chained_exceptions[number].__traceback__)
self.print_stack_entry(self.stack[self.curindex])
else:
self.error("No exception with that number")

def do_up(self, arg):
"""u(p) [count]

Expand DownExpand Up@@ -1890,11 +1967,16 @@ def set_trace(*, header=None):
# Post-Mortem interface

def post_mortem(t=None):
"""Enter post-mortem debugging of the given *traceback* object.
"""Enter post-mortem debugging of the given *traceback*, or *exception*
object.

If no traceback is given, it uses the one of the exception that is
currently being handled (an exception must be being handled if the
default is to be used).

If `t` is an Exception and is a chained exception (i.e it has a __context__,
or a __cause__), pdb will be able to list and move to other exceptions in
the chain using the `exceptions` command
"""
# handling the default
if t is None:
Expand All@@ -1912,11 +1994,7 @@ def post_mortem(t=None):

def pm():
"""Enter post-mortem debugging of the traceback found in sys.last_traceback."""
if hasattr(sys, 'last_exc'):
tb = sys.last_exc.__traceback__
else:
tb = sys.last_traceback
post_mortem(tb)
post_mortem(sys.last_exc)


# Main program for testing
Expand DownExpand Up@@ -1996,8 +2074,7 @@ def main():
traceback.print_exc()
print("Uncaught exception. Entering post mortem debugging")
print("Running 'cont' or 'step' will restart the program")
t = e.__traceback__
pdb.interaction(None, t)
pdb.interaction(None, e)
print("Post mortem debugger finished. The " + target +
" will be restarted")

Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp