diff -r 20a5a56ce090 Lib/test/test_traceback.py --- a/Lib/test/test_traceback.py Wed Jan 07 13:14:47 2015 +1000 +++ b/Lib/test/test_traceback.py Wed Jan 07 05:48:16 2015 -0500 @@ -447,6 +447,95 @@ 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_raises(self, num): + ''' Explicitly raise Exception from the last call ''' + if not num: + raise Exception('Last raised') + else: + num -= 1 + self.last_raises(num) + + def last_returns_frame(self, num): + ''' Return a frame from the last call ''' + if not num: + return sys._getframe() + else: + num -= 1 + return self.last_returns_frame(num) + + def check_entries(self, limit, nolim, poslim, neglim, stack=False): + ''' Check loaded entries ''' + # The lengths must always be equal + self.assertEqual(len(poslim), len(neglim)) + # Must not be empty if limit > 0 + if limit > 0: + self.assertTrue(neglim) + self.assertTrue(poslim) + # These entries were loaded by extract_stack. + # The order is different, but this is the intended behaviour. + if stack: + self.assertListEqual(nolim[-limit:], poslim) + self.assertListEqual(nolim[:limit], neglim) + else: + self.assertListEqual(nolim[:limit], poslim) + self.assertListEqual(nolim[-limit:], neglim) + else: + self.assertFalse(neglim) + self.assertFalse(poslim) + + def test_tb_exception_limit(self): + for n in range(5): + # last_raises will be called n + 1 times + try: + self.last_raises(n) + except Exception: + e_type, e_value, tb = sys.exc_info() + + for lim in range(n + 2): + # Traceback subtest + with self.subTest(n=n, limit=lim, type='traceback'): + nolim = traceback.extract_tb(tb) + poslim = traceback.extract_tb(tb, limit=n) + neglim = traceback.extract_tb(tb, limit=-n) + self.check_entries(n, nolim, poslim, neglim) + + # Exception subtest + exc_header = ['Traceback (most recent call last):\n', + 'Exception: Last raised\n'] + + with self.subTest(calls=n, limit=lim, type='exception'): + nolim = traceback.format_exception(e_type, e_value, tb) + poslim = traceback.format_exception(e_type, e_value, + tb, limit=lim) + neglim = traceback.format_exception(e_type, e_value, + tb, limit=-lim) + # At least 2 elements, because format_exception always + # returns "Traceback ..." header and exception type and value + self.assertGreaterEqual(len(neglim), 2) + self.assertGreaterEqual(len(poslim), 2) + self.assertListEqual([neglim[0]] + [neglim[-1]], exc_header) + self.assertListEqual([poslim[0]] + [poslim[-1]], exc_header) + # [1:-1] to leave the entries + self.check_entries(lim, nolim[1:-1], poslim[1:-1], neglim[1:-1]) + + def test_stack_limit(self): + for n in range(5): + # last_returns_frame will be called n + 1 times + frame = self.last_returns_frame(n) + for lim in range(n + 2): + with self.subTest(n=n, limit=lim, type='stack'): + nolim = traceback.extract_stack(frame) + poslim = traceback.extract_stack(frame, limit=lim) + neglim = traceback.extract_stack(frame, limit=-lim) + self.check_entries(lim, nolim, poslim, neglim, stack=True) + + + class MiscTracebackCases(unittest.TestCase): # # Check non-printing functions in traceback module diff -r 20a5a56ce090 Lib/traceback.py --- a/Lib/traceback.py Wed Jan 07 13:14:47 2015 +1000 +++ b/Lib/traceback.py Wed Jan 07 05:48:16 2015 -0500 @@ -3,6 +3,7 @@ import linecache import sys import operator +import collections __all__ = ['extract_stack', 'extract_tb', 'format_exception', 'format_exception_only', 'format_list', 'format_stack', @@ -45,15 +46,10 @@ # Printing and Extracting Tracebacks. # -# extractor takes curr and needs to return a tuple of: -# - Frame object -# - Line number -# - Next item (same type as curr) -# In practice, curr is either a traceback or a frame. -def _extract_tb_or_stack_iter(curr, limit, extractor): - if limit is None: - limit = getattr(sys, 'tracebacklimit', None) - +def _extract_tb_or_stack_iter_raw(curr, limit, extractor): + ''' Yield (filename, lineno, name, f_globals) tuples. + The arguments have the same meaning as the corresponding arguments + to _extract_tb_or_stack_iter ''' n = 0 while curr is not None and (limit is None or n < limit): f, lineno, next_item = extractor(curr) @@ -61,18 +57,37 @@ filename = co.co_filename name = co.co_name - linecache.checkcache(filename) - line = linecache.getline(filename, lineno, f.f_globals) + yield (filename, lineno, name, f.f_globals) - if line: - line = line.strip() - else: - line = None - - yield (filename, lineno, name, line) curr = next_item n += 1 +def _extract_tb_or_stack_iter(curr, limit, extractor): + ''' Load source lines for tuples yielded by_extract_tb_or_stack_iter_raw + and yield (filename, lineno, name, line) tuples. + + extractor takes curr and needs to return a tuple of: + - Frame object + - Line number + - Next item (same type as curr) + In practice, curr is either a traceback or a frame. ''' + + # Check if limit is negative + negative_limit = limit is not None and limit < 0 + + if limit is None or negative_limit: + _limit = getattr(sys, 'tracebacklimit', None) + else: + _limit = limit + + raw = _extract_tb_or_stack_iter_raw(curr, _limit, extractor) + it = collections.deque(raw, maxlen=abs(limit)) if negative_limit else raw + + for filename, lineno, name, f_globals in it: + linecache.checkcache(filename) + line = linecache.getline(filename, lineno, f_globals) + yield (filename, lineno, name, line.strip() if line else None) + def _extract_tb_iter(tb, limit): return _extract_tb_or_stack_iter( tb, limit,