##// END OF EJS Templates
Truncate tracebacks on recursion error...
Thomas Kluyver -
Show More
@@ -8,7 +8,12 b' from textwrap import dedent'
8 8 import traceback
9 9 import unittest
10 10
11 from ..ultratb import ColorTB, VerboseTB
11 try:
12 from unittest import mock
13 except ImportError:
14 import mock # Python 2
15
16 from ..ultratb import ColorTB, VerboseTB, find_recursion
12 17
13 18
14 19 from IPython.testing import tools as tt
@@ -226,6 +231,70 b' except Exception:'
226 231 ip.run_cell(self.SUPPRESS_CHAINING_CODE)
227 232
228 233
234 class RecursionTest(unittest.TestCase):
235 DEFINITIONS = """
236 def non_recurs():
237 1/0
238
239 def r1():
240 r1()
241
242 def r3a():
243 r3b()
244
245 def r3b():
246 r3c()
247
248 def r3c():
249 r3a()
250
251 def r3o1():
252 r3a()
253
254 def r3o2():
255 r3o1()
256 """
257 def setUp(self):
258 ip.run_cell(self.DEFINITIONS)
259
260 def test_no_recursion(self):
261 with tt.AssertNotPrints("frames repeated"):
262 ip.run_cell("non_recurs()")
263
264 def test_recursion_one_frame(self):
265 with tt.AssertPrints("1 frames repeated"):
266 ip.run_cell("r1()")
267
268 def test_recursion_three_frames(self):
269 with tt.AssertPrints("3 frames repeated"):
270 ip.run_cell("r3o2()")
271
272 def test_find_recursion(self):
273 captured = []
274 def capture_exc(*args, **kwargs):
275 captured.append(sys.exc_info())
276 with mock.patch.object(ip, 'showtraceback', capture_exc):
277 ip.run_cell("r3o2()")
278
279 self.assertEqual(len(captured), 1)
280 etype, evalue, tb = captured[0]
281 self.assertIn("recursion", str(evalue))
282
283 records = ip.InteractiveTB.get_records(tb, 3, ip.InteractiveTB.tb_offset)
284 for r in records[:10]:
285 print(r[1:4])
286
287 # The outermost frames should be:
288 # 0: the 'cell' that was running when the exception came up
289 # 1: r3o2()
290 # 2: r3o1()
291 # 3: r3a()
292 # Then repeating r3b, r3c, r3a
293 last_unique, repeat_length = find_recursion(etype, evalue, records)
294 self.assertEqual(last_unique, 2)
295 self.assertEqual(repeat_length, 3)
296
297
229 298 #----------------------------------------------------------------------------
230 299
231 300 # module testing (minimal)
@@ -422,6 +422,57 b' def _format_traceback_lines(lnum, index, lines, Colors, lvals=None, scheme=None)'
422 422 i = i + 1
423 423 return res
424 424
425 def is_recursion_error(etype, value, records):
426 try:
427 # RecursionError is new in Python 3.5
428 recursion_error_type = RecursionError
429 except NameError:
430 recursion_error_type = RuntimeError
431
432 # The default recursion limit is 1000, but some of that will be taken up
433 # by stack frames in IPython itself. >500 frames probably indicates
434 # a recursion error.
435 return (etype is recursion_error_type) \
436 and "recursion" in str(value).lower() \
437 and len(records) > 500
438
439 def find_recursion(etype, value, records):
440 """Identify the repeating stack frames from a RecursionError traceback
441
442 'records' is a list as returned by VerboseTB.get_records()
443
444 Returns (last_unique, repeat_length)
445 """
446 # This involves a bit of guesswork - we want to show enough of the traceback
447 # to indicate where the recursion is occurring. We guess that the innermost
448 # quarter of the traceback (250 frames by default) is repeats, and find the
449 # first frame (from in to out) that looks different.
450 if not is_recursion_error(etype, value, records):
451 return len(records), 0
452
453 # Select filename, lineno, func_name to track frames with
454 records = [r[1:4] for r in records]
455 inner_frames = records[-(len(records)//4):]
456 frames_repeated = set(inner_frames)
457
458 last_seen_at = {}
459 longest_repeat = 0
460 i = len(records)
461 for frame in reversed(records):
462 i -= 1
463 if frame not in frames_repeated:
464 last_unique = i
465 break
466
467 if frame in last_seen_at:
468 distance = last_seen_at[frame] - i
469 longest_repeat = max(longest_repeat, distance)
470
471 last_seen_at[frame] = i
472 else:
473 last_unique = 0 # The whole traceback was recursion
474
475 return last_unique, longest_repeat
425 476
426 477 #---------------------------------------------------------------------------
427 478 # Module classes
@@ -775,15 +826,27 b' class VerboseTB(TBTools):'
775 826 check_cache = linecache.checkcache
776 827 self.check_cache = check_cache
777 828
778 def format_records(self, records):
829 def format_records(self, records, last_unique, recursion_repeat):
830 """Format the stack frames of the traceback"""
831 frames = []
832 for r in records[:last_unique+recursion_repeat+1]:
833 #print '*** record:',file,lnum,func,lines,index # dbg
834 frames.append(self.format_record(*r))
835
836 if recursion_repeat:
837 frames.append('Last %d frames repeated, from:\n' % recursion_repeat)
838 frames.append(self.format_record(*records[last_unique+recursion_repeat+1]))
839
840 return frames
841
842 def format_record(self, frame, file, lnum, func, lines, index):
843 """Format a single stack frame"""
779 844 Colors = self.Colors # just a shorthand + quicker name lookup
780 845 ColorsNormal = Colors.Normal # used a lot
781 846 col_scheme = self.color_scheme_table.active_scheme_name
782 847 indent = ' ' * INDENT_SIZE
783 848 em_normal = '%s\n%s%s' % (Colors.valEm, indent, ColorsNormal)
784 849 undefined = '%sundefined%s' % (Colors.em, ColorsNormal)
785 frames = []
786 # build some color string templates outside these nested loops
787 850 tpl_link = '%s%%s%s' % (Colors.filenameEm, ColorsNormal)
788 851 tpl_call = 'in %s%%s%s%%s%s' % (Colors.vName, Colors.valEm,
789 852 ColorsNormal)
@@ -799,8 +862,8 b' class VerboseTB(TBTools):'
799 862 ColorsNormal)
800 863
801 864 abspath = os.path.abspath
802 for frame, file, lnum, func, lines, index in records:
803 #print '*** record:',file,lnum,func,lines,index # dbg
865
866
804 867 if not file:
805 868 file = '?'
806 869 elif file.startswith(str("<")) and file.endswith(str(">")):
@@ -854,8 +917,8 b' class VerboseTB(TBTools):'
854 917
855 918 # Don't attempt to tokenize binary files.
856 919 if file.endswith(('.so', '.pyd', '.dll')):
857 frames.append('%s %s\n' % (link, call))
858 continue
920 return '%s %s\n' % (link, call)
921
859 922 elif file.endswith(('.pyc', '.pyo')):
860 923 # Look up the corresponding source file.
861 924 file = openpy.source_from_cache(file)
@@ -942,13 +1005,11 b' class VerboseTB(TBTools):'
942 1005 level = '%s %s\n' % (link, call)
943 1006
944 1007 if index is None:
945 frames.append(level)
1008 return level
946 1009 else:
947 frames.append('%s%s' % (level, ''.join(
1010 return '%s%s' % (level, ''.join(
948 1011 _format_traceback_lines(lnum, index, lines, Colors, lvals,
949 col_scheme))))
950
951 return frames
1012 col_scheme)))
952 1013
953 1014 def prepare_chained_exception_message(self, cause):
954 1015 direct_cause = "\nThe above exception was the direct cause of the following exception:\n"
@@ -1016,7 +1077,13 b' class VerboseTB(TBTools):'
1016 1077 return exception
1017 1078
1018 1079 def format_exception_as_a_whole(self, etype, evalue, etb, number_of_lines_of_context, tb_offset):
1080 """Formats the header, traceback and exception message for a single exception.
1081
1082 This may be called multiple times by Python 3 exception chaining
1083 (PEP 3134).
1084 """
1019 1085 # some locals
1086 orig_etype = etype
1020 1087 try:
1021 1088 etype = etype.__name__
1022 1089 except AttributeError:
@@ -1029,7 +1096,9 b' class VerboseTB(TBTools):'
1029 1096 if records is None:
1030 1097 return ""
1031 1098
1032 frames = self.format_records(records)
1099 last_unique, recursion_repeat = find_recursion(orig_etype, evalue, records)
1100
1101 frames = self.format_records(records, last_unique, recursion_repeat)
1033 1102
1034 1103 formatted_exception = self.format_exception(etype, evalue)
1035 1104 if records:
General Comments 0
You need to be logged in to leave comments. Login now