From 24559e483400073c6ae1446c0ac2a7990c755f02 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 3 May 2015 13:19:46 +0300 Subject: [PATCH] Issue #22619: Added negative limit support in the traceback module. Based on patch by Dmitry Kazakov. --- Doc/library/traceback.rst | 52 +++++++++------ Lib/test/test_traceback.py | 127 +++++++++++++++++++++++++++++++++++-- Lib/traceback.py | 14 ++-- Misc/ACKS | 1 + Misc/NEWS | 3 + 5 files changed, 170 insertions(+), 27 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index c6b5d0c9568..8d216d07e39 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -22,15 +22,20 @@ The module defines the following functions: .. function:: print_tb(traceback, limit=None, file=None) - Print up to *limit* stack trace entries from *traceback*. If *limit* is omitted - or ``None``, all entries are printed. If *file* is omitted or ``None``, the - output goes to ``sys.stderr``; otherwise it should be an open file or file-like - object to receive the output. + Print up to *limit* stack trace entries from *traceback* (starting from + the caller's frame) if *limit* is positive. Otherwise, print the last + ``abs(limit)`` entries. If *limit* is omitted or ``None``, all entries + are printed. If *file* is omitted or ``None``, the output goes to + ``sys.stderr``; otherwise it should be an open file or file-like object + to receive the output. + + .. versionchanged:: 3.5 + Added negative *limit* support. .. function:: print_exception(type, value, traceback, limit=None, file=None, chain=True) - Print exception information and up to *limit* stack trace entries from + Print exception information and stack trace entries from *traceback* to *file*. This differs from :func:`print_tb` in the following ways: @@ -41,6 +46,7 @@ The module defines the following functions: prints the line where the syntax error occurred with a caret indicating the approximate position of the error. + The optional *limit* argument has the same meaning as for :func:`print_tb`. 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 @@ -49,33 +55,41 @@ The module defines the following functions: .. function:: print_exc(limit=None, file=None, chain=True) - This is a shorthand for ``print_exception(*sys.exc_info())``. + This is a shorthand for ``print_exception(*sys.exc_info(), limit, file, + chain)``. .. function:: print_last(limit=None, file=None, chain=True) This is a shorthand for ``print_exception(sys.last_type, sys.last_value, - sys.last_traceback, limit, file)``. 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)``. 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) - This function prints a stack trace from its invocation point. The optional *f* - argument can be used to specify an alternate stack frame to start. The optional - *limit* and *file* arguments have the same meaning as for - :func:`print_exception`. + Print up to *limit* stack trace entries (starting from the invocation + point) if *limit* is positive. Otherwise, print the last ``abs(limit)`` + entries. If *limit* is omitted or ``None``, all entries are printed. + The optional *f* argument can be used to specify an alternate stack frame + to start. The optional *file* argument has the same meaning as for + :func:`print_tb`. + + .. versionchanged:: 3.5 + Added negative *limit* support. .. function:: extract_tb(traceback, limit=None) - Return a list of up to *limit* "pre-processed" stack trace entries extracted - from the traceback object *traceback*. It is useful for alternate formatting of - stack traces. If *limit* is omitted or ``None``, all entries are extracted. A - "pre-processed" stack trace entry is a 4-tuple (*filename*, *line number*, - *function name*, *text*) representing the information that is usually printed - for a stack trace. The *text* is a string with leading and trailing whitespace - stripped; if the source is not available it is ``None``. + Return a list of "pre-processed" stack trace entries extracted from the + traceback object *traceback*. It is useful for alternate formatting of + stack traces. The optional *limit* argument has the same meaning as for + :func:`print_tb`. A "pre-processed" stack trace entry is a 4-tuple + (*filename*, *line number*, *function name*, *text*) representing the + information that is usually printed for a stack trace. The *text* is a + string with leading and trailing whitespace stripped; if the source is + not available it is ``None``. .. function:: extract_stack(f=None, limit=None) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 2fa85f54a76..9364695f38f 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -6,6 +6,7 @@ import linecache import sys import unittest import re +from test import support from test.support import TESTFN, Error, captured_output, unlink, cpython_only from test.script_helper import assert_python_ok import textwrap @@ -453,6 +454,126 @@ class CExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): return s.getvalue() +class LimitTests(unittest.TestCase): + + ''' Tests for limit argument. + It's enough to test extact_tb, extract_stack and format_exception ''' + + def last_raises1(self): + raise Exception('Last raised') + + def last_raises2(self): + self.last_raises1() + + def last_raises3(self): + self.last_raises2() + + def last_raises4(self): + self.last_raises3() + + def last_raises5(self): + self.last_raises4() + + def last_returns_frame1(self): + return sys._getframe() + + def last_returns_frame2(self): + return self.last_returns_frame1() + + def last_returns_frame3(self): + return self.last_returns_frame2() + + def last_returns_frame4(self): + return self.last_returns_frame3() + + def last_returns_frame5(self): + return self.last_returns_frame4() + + def test_extract_stack(self): + frame = self.last_returns_frame5() + def extract(**kwargs): + return traceback.extract_stack(frame, **kwargs) + def assertEqualExcept(actual, expected, ignore): + self.assertEqual(actual[:ignore], expected[:ignore]) + self.assertEqual(actual[ignore+1:], expected[ignore+1:]) + self.assertEqual(len(actual), len(expected)) + + with support.swap_attr(sys, 'tracebacklimit', 1000): + nolim = extract() + self.assertGreater(len(nolim), 5) + self.assertEqual(extract(limit=2), nolim[-2:]) + assertEqualExcept(extract(limit=100), nolim[-100:], -5-1) + self.assertEqual(extract(limit=-2), nolim[:2]) + assertEqualExcept(extract(limit=-100), nolim[:100], len(nolim)-5-1) + self.assertEqual(extract(limit=0), []) + del sys.tracebacklimit + assertEqualExcept(extract(), nolim, -5-1) + sys.tracebacklimit = 2 + self.assertEqual(extract(), nolim[-2:]) + self.assertEqual(extract(limit=3), nolim[-3:]) + self.assertEqual(extract(limit=-3), nolim[:3]) + sys.tracebacklimit = 0 + self.assertEqual(extract(), []) + sys.tracebacklimit = -1 + self.assertEqual(extract(), []) + + def test_extract_tb(self): + try: + self.last_raises5() + except Exception: + exc_type, exc_value, tb = sys.exc_info() + def extract(**kwargs): + return traceback.extract_tb(tb, **kwargs) + + with support.swap_attr(sys, 'tracebacklimit', 1000): + nolim = extract() + self.assertEqual(len(nolim), 5+1) + self.assertEqual(extract(limit=2), nolim[:2]) + self.assertEqual(extract(limit=10), nolim) + self.assertEqual(extract(limit=-2), nolim[-2:]) + self.assertEqual(extract(limit=-10), nolim) + self.assertEqual(extract(limit=0), []) + del sys.tracebacklimit + self.assertEqual(extract(), nolim) + sys.tracebacklimit = 2 + self.assertEqual(extract(), nolim[:2]) + self.assertEqual(extract(limit=3), nolim[:3]) + self.assertEqual(extract(limit=-3), nolim[-3:]) + sys.tracebacklimit = 0 + self.assertEqual(extract(), []) + sys.tracebacklimit = -1 + self.assertEqual(extract(), []) + + def test_format_exception(self): + try: + self.last_raises5() + except Exception: + exc_type, exc_value, tb = sys.exc_info() + # [1:-1] to exclude "Traceback (...)" header and + # exception type and value + def extract(**kwargs): + return traceback.format_exception(exc_type, exc_value, tb, **kwargs)[1:-1] + + with support.swap_attr(sys, 'tracebacklimit', 1000): + nolim = extract() + self.assertEqual(len(nolim), 5+1) + self.assertEqual(extract(limit=2), nolim[:2]) + self.assertEqual(extract(limit=10), nolim) + self.assertEqual(extract(limit=-2), nolim[-2:]) + self.assertEqual(extract(limit=-10), nolim) + self.assertEqual(extract(limit=0), []) + del sys.tracebacklimit + self.assertEqual(extract(), nolim) + sys.tracebacklimit = 2 + self.assertEqual(extract(), nolim[:2]) + self.assertEqual(extract(limit=3), nolim[:3]) + self.assertEqual(extract(limit=-3), nolim[-3:]) + sys.tracebacklimit = 0 + self.assertEqual(extract(), []) + sys.tracebacklimit = -1 + self.assertEqual(extract(), []) + + class MiscTracebackCases(unittest.TestCase): # # Check non-printing functions in traceback module @@ -592,16 +713,14 @@ class TestStack(unittest.TestCase): traceback.walk_stack(None), capture_locals=True, limit=1) s = some_inner(3, 4) self.assertEqual( - [' File "' + __file__ + '", line 592, ' - 'in some_inner\n' + [' File "%s", line %d, in some_inner\n' ' traceback.walk_stack(None), capture_locals=True, limit=1)\n' ' a = 1\n' ' b = 2\n' ' k = 3\n' - ' v = 4\n' + ' v = 4\n' % (__file__, some_inner.__code__.co_firstlineno + 4) ], s.format()) - class TestTracebackException(unittest.TestCase): def test_smoke(self): diff --git a/Lib/traceback.py b/Lib/traceback.py index 8a554cfc068..61c9fb57ace 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1,8 +1,9 @@ """Extract, format and print information about Python stack traces.""" +import collections +import itertools import linecache import sys -import operator __all__ = ['extract_stack', 'extract_tb', 'format_exception', 'format_exception_only', 'format_list', 'format_stack', @@ -315,12 +316,17 @@ class StackSummary(list): """ 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 pos, (f, lineno) in enumerate(frame_gen): - if limit is not None and pos >= limit: - break + for f, lineno in frame_gen: co = f.f_code filename = co.co_filename name = co.co_name diff --git a/Misc/ACKS b/Misc/ACKS index ca0fcab1802..e34d7113ee7 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -712,6 +712,7 @@ Anton Kasyanov Lou Kates Makoto Kato Hiroaki Kawai +Dmitry Kazakov Brian Kearns Sebastien Keim Ryan Kelly diff --git a/Misc/NEWS b/Misc/NEWS index 6a3c30b48ee..f2a8594ea7b 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -25,6 +25,9 @@ Core and Builtins Library ------- +- Issue #22619: Added negative limit support in the traceback module. + Based on patch by Dmitry Kazakov. + - Issue #24094: Fix possible crash in json.encode with poorly behaved dict subclasses.