Issue #22619: Added negative limit support in the traceback module.

Based on patch by Dmitry Kazakov.
This commit is contained in:
Serhiy Storchaka 2015-05-03 13:19:46 +03:00
parent 9a578d9ee6
commit 24559e4834
5 changed files with 170 additions and 27 deletions

View File

@ -22,15 +22,20 @@ The module defines the following functions:
.. function:: print_tb(traceback, limit=None, file=None) .. function:: print_tb(traceback, limit=None, file=None)
Print up to *limit* stack trace entries from *traceback*. If *limit* is omitted Print up to *limit* stack trace entries from *traceback* (starting from
or ``None``, all entries are printed. If *file* is omitted or ``None``, the the caller's frame) if *limit* is positive. Otherwise, print the last
output goes to ``sys.stderr``; otherwise it should be an open file or file-like ``abs(limit)`` entries. If *limit* is omitted or ``None``, all entries
object to receive the output. 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) .. 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 *traceback* to *file*. This differs from :func:`print_tb` in the following
ways: ways:
@ -41,6 +46,7 @@ The module defines the following functions:
prints the line where the syntax error occurred with a caret indicating the prints the line where the syntax error occurred with a caret indicating the
approximate position of the error. 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 If *chain* is true (the default), then chained exceptions (the
:attr:`__cause__` or :attr:`__context__` attributes of the exception) will be :attr:`__cause__` or :attr:`__context__` attributes of the exception) will be
printed as well, like the interpreter itself does when printing an unhandled 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) .. 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) .. function:: print_last(limit=None, file=None, chain=True)
This is a shorthand for ``print_exception(sys.last_type, sys.last_value, 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 sys.last_traceback, limit, file, chain)``. In general it will work only
an exception has reached an interactive prompt (see :data:`sys.last_type`). after an exception has reached an interactive prompt (see
:data:`sys.last_type`).
.. function:: print_stack(f=None, limit=None, file=None) .. function:: print_stack(f=None, limit=None, file=None)
This function prints a stack trace from its invocation point. The optional *f* Print up to *limit* stack trace entries (starting from the invocation
argument can be used to specify an alternate stack frame to start. The optional point) if *limit* is positive. Otherwise, print the last ``abs(limit)``
*limit* and *file* arguments have the same meaning as for entries. If *limit* is omitted or ``None``, all entries are printed.
:func:`print_exception`. 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) .. function:: extract_tb(traceback, limit=None)
Return a list of up to *limit* "pre-processed" stack trace entries extracted Return a list of "pre-processed" stack trace entries extracted from the
from the traceback object *traceback*. It is useful for alternate formatting of traceback object *traceback*. It is useful for alternate formatting of
stack traces. If *limit* is omitted or ``None``, all entries are extracted. A stack traces. The optional *limit* argument has the same meaning as for
"pre-processed" stack trace entry is a 4-tuple (*filename*, *line number*, :func:`print_tb`. A "pre-processed" stack trace entry is a 4-tuple
*function name*, *text*) representing the information that is usually printed (*filename*, *line number*, *function name*, *text*) representing the
for a stack trace. The *text* is a string with leading and trailing whitespace information that is usually printed for a stack trace. The *text* is a
stripped; if the source is not available it is ``None``. string with leading and trailing whitespace stripped; if the source is
not available it is ``None``.
.. function:: extract_stack(f=None, limit=None) .. function:: extract_stack(f=None, limit=None)

View File

@ -6,6 +6,7 @@ import linecache
import sys import sys
import unittest import unittest
import re import re
from test import support
from test.support import TESTFN, Error, captured_output, unlink, cpython_only from test.support import TESTFN, Error, captured_output, unlink, cpython_only
from test.script_helper import assert_python_ok from test.script_helper import assert_python_ok
import textwrap import textwrap
@ -453,6 +454,126 @@ class CExcReportingTests(BaseExceptionReportingTests, unittest.TestCase):
return s.getvalue() 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): class MiscTracebackCases(unittest.TestCase):
# #
# Check non-printing functions in traceback module # Check non-printing functions in traceback module
@ -592,16 +713,14 @@ class TestStack(unittest.TestCase):
traceback.walk_stack(None), capture_locals=True, limit=1) traceback.walk_stack(None), capture_locals=True, limit=1)
s = some_inner(3, 4) s = some_inner(3, 4)
self.assertEqual( self.assertEqual(
[' File "' + __file__ + '", line 592, ' [' File "%s", line %d, in some_inner\n'
'in some_inner\n'
' traceback.walk_stack(None), capture_locals=True, limit=1)\n' ' traceback.walk_stack(None), capture_locals=True, limit=1)\n'
' a = 1\n' ' a = 1\n'
' b = 2\n' ' b = 2\n'
' k = 3\n' ' k = 3\n'
' v = 4\n' ' v = 4\n' % (__file__, some_inner.__code__.co_firstlineno + 4)
], s.format()) ], s.format())
class TestTracebackException(unittest.TestCase): class TestTracebackException(unittest.TestCase):
def test_smoke(self): def test_smoke(self):

View File

@ -1,8 +1,9 @@
"""Extract, format and print information about Python stack traces.""" """Extract, format and print information about Python stack traces."""
import collections
import itertools
import linecache import linecache
import sys import sys
import operator
__all__ = ['extract_stack', 'extract_tb', 'format_exception', __all__ = ['extract_stack', 'extract_tb', 'format_exception',
'format_exception_only', 'format_list', 'format_stack', 'format_exception_only', 'format_list', 'format_stack',
@ -315,12 +316,17 @@ class StackSummary(list):
""" """
if limit is None: if limit is None:
limit = getattr(sys, 'tracebacklimit', 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() result = klass()
fnames = set() fnames = set()
for pos, (f, lineno) in enumerate(frame_gen): for f, lineno in frame_gen:
if limit is not None and pos >= limit:
break
co = f.f_code co = f.f_code
filename = co.co_filename filename = co.co_filename
name = co.co_name name = co.co_name

View File

@ -712,6 +712,7 @@ Anton Kasyanov
Lou Kates Lou Kates
Makoto Kato Makoto Kato
Hiroaki Kawai Hiroaki Kawai
Dmitry Kazakov
Brian Kearns Brian Kearns
Sebastien Keim Sebastien Keim
Ryan Kelly Ryan Kelly

View File

@ -25,6 +25,9 @@ Core and Builtins
Library 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 - Issue #24094: Fix possible crash in json.encode with poorly behaved dict
subclasses. subclasses.