diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index 55d331c996a187..005b34c5aa9fb8 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -36,7 +36,7 @@ The module defines the following functions: Added negative *limit* support. -.. function:: print_exception(etype, value, tb, limit=None, file=None, chain=True) +.. function:: print_exception(etype, value, tb, limit=None, file=None, chain=True, filter_frames=None) Print exception information and stack trace entries from traceback object *tb* to *file*. This differs from :func:`print_tb` in the following @@ -53,24 +53,29 @@ The module defines the following functions: If *chain* is true (the default), then chained exceptions (the :attr:`__cause__` or :attr:`__context__` attributes of the exception) will be printed as well, like the interpreter itself does when printing an unhandled - exception. + exception. If *filter_frames* is not ``None``, it should be a callable + that accepts a single frame object. Only traceback entries for which + *filter_frames* returns ``True`` will be displayed. .. versionchanged:: 3.5 The *etype* argument is ignored and inferred from the type of *value*. + .. versionadded:: 3.7 + *filter_frames* argument. -.. function:: print_exc(limit=None, file=None, chain=True) + +.. function:: print_exc(limit=None, file=None, chain=True, filter_frames=None) This is a shorthand for ``print_exception(*sys.exc_info(), limit, file, - chain)``. + chain, filter_frames)``. -.. function:: print_last(limit=None, file=None, chain=True) +.. function:: print_last(limit=None, file=None, chain=True, filter_frames=None) This is a shorthand for ``print_exception(sys.last_type, sys.last_value, - sys.last_traceback, limit, file, chain)``. In general it will work only - after an exception has reached an interactive prompt (see - :data:`sys.last_type`). + sys.last_traceback, limit, file, chain, filter_frames)``. + In general it will work only after an exception has reached an interactive + prompt (see :data:`sys.last_type`). .. function:: print_stack(f=None, limit=None, file=None) @@ -126,7 +131,7 @@ The module defines the following functions: which exception occurred is the always last string in the list. -.. function:: format_exception(etype, value, tb, limit=None, chain=True) +.. function:: format_exception(etype, value, tb, limit=None, chain=True, filter_frames=None) Format a stack trace and the exception information. The arguments have the same meaning as the corresponding arguments to :func:`print_exception`. The @@ -138,10 +143,10 @@ The module defines the following functions: The *etype* argument is ignored and inferred from the type of *value*. -.. function:: format_exc(limit=None, chain=True) +.. function:: format_exc(limit=None, chain=True, filter_frames=None) - This is like ``print_exc(limit)`` but returns a string instead of printing to - a file. + This is like ``print_exc(limit, chain, filter_frames)`` but returns a string + instead of printing to a file. .. function:: format_tb(tb, limit=None) @@ -239,12 +244,14 @@ capture data for later printing in a lightweight fashion. Note that when locals are captured, they are also shown in the traceback. - .. method:: format(*, chain=True) + .. method:: format(*, chain=True, filter_frames=None) Format the exception. If *chain* is not ``True``, ``__cause__`` and ``__context__`` will not - be formatted. + be formatted. If *filter_frames* is not ``None``, it should be a callable + that accepts a single frame object. Only traceback entries for which + *filter_frames* returns ``True`` will be returned. The return value is a generator of strings, each ending in a newline and some containing internal newlines. :func:`~traceback.print_exception` @@ -253,6 +260,9 @@ capture data for later printing in a lightweight fashion. The message indicating which exception occurred is always the last string in the output. + .. versionadded:: 3.7 + *filter_frames* argument. + .. method:: format_exception_only() Format the exception part of the traceback. diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index bffc03e663ff78..7415fc7ed0d0cd 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1092,6 +1092,38 @@ def test_traceback_header(self): exc = traceback.TracebackException(Exception, Exception("haven"), None) self.assertEqual(list(exc.format()), ["Exception: haven\n"]) + def test_filtering(self): + def a(): + b() + + def b(): + c() + + def c(): + try: + d() + except: + raise TypeError + + def d(): + 1/0 + + try: + a() + except Exception: + exc_info = sys.exc_info() + exc = traceback.TracebackException(*exc_info) + full = list(exc.format()) + + # Must only remove the current frame + current = sys._getframe() + filtered = exc.format(chain=False, filter_frames=lambda f: f is not current) + self.assertEqual(list(filtered), [full[-6]] + full[-4:]) + + # Must remove c frames from all chained exceptions + filtered = exc.format(filter_frames=lambda f: f.f_code.co_name != 'c') + self.assertEqual(list(filtered), full[:1] + full[2:9] + full[10:]) + class MiscTest(unittest.TestCase): diff --git a/Lib/traceback.py b/Lib/traceback.py index ee8c73914032e1..7b0467b81ada13 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -80,7 +80,7 @@ def extract_tb(tb, limit=None): "another exception occurred:\n\n") -def print_exception(etype, value, tb, limit=None, file=None, chain=True): +def print_exception(etype, value, tb, limit=None, file=None, chain=True, filter_frames=None): """Print exception up to 'limit' stack trace entries from 'tb' to 'file'. This differs from print_tb() in the following ways: (1) if @@ -96,12 +96,12 @@ def print_exception(etype, value, tb, limit=None, file=None, chain=True): # ignore it here (rather than in the new TracebackException API). if file is None: file = sys.stderr - for line in TracebackException( - type(value), value, tb, limit=limit).format(chain=chain): + exc = TracebackException(type(value), value, tb, limit=limit) + for line in exc.format(chain=chain, filter_frames=filter_frames): print(line, file=file, end="") -def format_exception(etype, value, tb, limit=None, chain=True): +def format_exception(etype, value, tb, limit=None, chain=True, filter_frames=None): """Format a stack trace and the exception information. The arguments have the same meaning as the corresponding arguments @@ -113,8 +113,8 @@ def format_exception(etype, value, tb, limit=None, chain=True): # format_exception has ignored etype for some time, and code such as cgitb # passes in bogus values as a result. For compatibility with such code we # ignore it here (rather than in the new TracebackException API). - return list(TracebackException( - type(value), value, tb, limit=limit).format(chain=chain)) + exc = TracebackException(type(value), value, tb, limit=limit) + return list(exc.format(chain=chain, filter_frames=filter_frames)) def format_exception_only(etype, value): @@ -154,21 +154,24 @@ def _some_str(value): # -- -def print_exc(limit=None, file=None, chain=True): - """Shorthand for 'print_exception(*sys.exc_info(), limit, file)'.""" - print_exception(*sys.exc_info(), limit=limit, file=file, chain=chain) +def print_exc(limit=None, file=None, chain=True, filter_frames=None): + """Shorthand for 'print_exception(*sys.exc_info(), limit, file, + chain, filter_frames)'.""" + print_exception(*sys.exc_info(), limit=limit, file=file, + chain=chain, filter_frames=filter_frames) -def format_exc(limit=None, chain=True): +def format_exc(limit=None, chain=True, filter_frames=None): """Like print_exc() but return a string.""" - return "".join(format_exception(*sys.exc_info(), limit=limit, chain=chain)) + return "".join(format_exception(*sys.exc_info(), limit=limit, + chain=chain, filter_frames=filter_frames)) -def print_last(limit=None, file=None, chain=True): +def print_last(limit=None, file=None, chain=True, filter_frames=None): """This is a shorthand for 'print_exception(sys.last_type, - sys.last_value, sys.last_traceback, limit, file)'.""" + sys.last_value, sys.last_traceback, limit, file, chain, filter_frames)'.""" if not hasattr(sys, "last_type"): raise ValueError("no last exception") print_exception(sys.last_type, sys.last_value, sys.last_traceback, - limit, file, chain) + limit, file, chain, filter_frames) # # Printing and Extracting Stacks. @@ -306,6 +309,20 @@ def walk_tb(tb): tb = tb.tb_next +def _walk_with_limit(frame_gen, limit=None): + if limit is None: + limit = getattr(sys, 'tracebacklimit', None) + if limit is not None and limit < 0: + limit = 0 + if limit is not None: + if limit >= 0: + return itertools.islice(frame_gen, limit) + else: + return collections.deque(frame_gen, maxlen=-limit) + + return frame_gen + + class StackSummary(list): """A stack of frames.""" @@ -323,19 +340,10 @@ def extract(klass, frame_gen, *, limit=None, lookup_lines=True, :param capture_locals: If True, the local variables from each frame will be captured as object representations into the FrameSummary. """ - if limit is None: - limit = getattr(sys, 'tracebacklimit', None) - if limit is not None and limit < 0: - limit = 0 - if limit is not None: - if limit >= 0: - frame_gen = itertools.islice(frame_gen, limit) - else: - frame_gen = collections.deque(frame_gen, maxlen=-limit) - result = klass() fnames = set() - for f, lineno in frame_gen: + + for f, lineno in _walk_with_limit(frame_gen, limit): co = f.f_code filename = co.co_filename name = co.co_name @@ -491,8 +499,10 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, self.__suppress_context__ = \ exc_value.__suppress_context__ if exc_value else False # TODO: locals. + # Keep actual frame objects for filtering + self._raw_stack = list(_walk_with_limit(walk_tb(exc_traceback), limit=limit)) self.stack = StackSummary.extract( - walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines, + self._raw_stack, limit=limit, lookup_lines=lookup_lines, capture_locals=capture_locals) self.exc_type = exc_type # Capture now to permit freeing resources: only complication is in the @@ -573,11 +583,15 @@ def format_exception_only(self): msg = self.msg or "" yield "{}: {}\n".format(stype, msg) - def format(self, *, chain=True): + def format(self, *, chain=True, filter_frames=None): """Format the exception. If chain is not *True*, *__cause__* and *__context__* will not be formatted. + If filter_frames is not *None*, it should be a callable that accepts a + single frame object. Only traceback entries for which filter_frames returns + *True* will be returned. + The return value is a generator of strings, each ending in a newline and some containing internal newlines. `print_exception` is a wrapper around this method which just prints the lines to a file. @@ -587,13 +601,24 @@ def format(self, *, chain=True): """ if chain: if self.__cause__ is not None: - yield from self.__cause__.format(chain=chain) + yield from self.__cause__.format(chain=chain, filter_frames=filter_frames) yield _cause_message elif (self.__context__ is not None and not self.__suppress_context__): - yield from self.__context__.format(chain=chain) + yield from self.__context__.format(chain=chain, filter_frames=filter_frames) yield _context_message if self.exc_traceback is not None: yield 'Traceback (most recent call last):\n' - yield from self.stack.format() + + if filter_frames is not None: + keep = [] + for (frame, _), summary in zip(self._raw_stack, self.stack): + if filter_frames(frame): + keep.append(summary) + + clean = StackSummary.from_list(keep) + yield from clean.format() + else: + yield from self.stack.format() + yield from self.format_exception_only() diff --git a/Misc/NEWS.d/next/Library/2018-01-16-23-32-52.bpo-31299.V_zOKL.rst b/Misc/NEWS.d/next/Library/2018-01-16-23-32-52.bpo-31299.V_zOKL.rst new file mode 100644 index 00000000000000..d2045a66d750ef --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-01-16-23-32-52.bpo-31299.V_zOKL.rst @@ -0,0 +1,3 @@ +``traceback.TracebackException.format`` and :mod:`traceback` functions that +use it now accept the callable ``filter_frames`` argument for hiding +unwanted traceback entries.