##// END OF EJS Templates
Truncate tracebacks on recursion error...
Thomas Kluyver -
Show More
@@ -8,7 +8,12 b' from textwrap import dedent'
8 import traceback
8 import traceback
9 import unittest
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 from IPython.testing import tools as tt
19 from IPython.testing import tools as tt
@@ -226,6 +231,70 b' except Exception:'
226 ip.run_cell(self.SUPPRESS_CHAINING_CODE)
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 # module testing (minimal)
300 # module testing (minimal)
@@ -422,6 +422,57 b' def _format_traceback_lines(lnum, index, lines, Colors, lvals=None, scheme=None)'
422 i = i + 1
422 i = i + 1
423 return res
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 # Module classes
478 # Module classes
@@ -775,15 +826,27 b' class VerboseTB(TBTools):'
775 check_cache = linecache.checkcache
826 check_cache = linecache.checkcache
776 self.check_cache = check_cache
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 Colors = self.Colors # just a shorthand + quicker name lookup
844 Colors = self.Colors # just a shorthand + quicker name lookup
780 ColorsNormal = Colors.Normal # used a lot
845 ColorsNormal = Colors.Normal # used a lot
781 col_scheme = self.color_scheme_table.active_scheme_name
846 col_scheme = self.color_scheme_table.active_scheme_name
782 indent = ' ' * INDENT_SIZE
847 indent = ' ' * INDENT_SIZE
783 em_normal = '%s\n%s%s' % (Colors.valEm, indent, ColorsNormal)
848 em_normal = '%s\n%s%s' % (Colors.valEm, indent, ColorsNormal)
784 undefined = '%sundefined%s' % (Colors.em, ColorsNormal)
849 undefined = '%sundefined%s' % (Colors.em, ColorsNormal)
785 frames = []
786 # build some color string templates outside these nested loops
787 tpl_link = '%s%%s%s' % (Colors.filenameEm, ColorsNormal)
850 tpl_link = '%s%%s%s' % (Colors.filenameEm, ColorsNormal)
788 tpl_call = 'in %s%%s%s%%s%s' % (Colors.vName, Colors.valEm,
851 tpl_call = 'in %s%%s%s%%s%s' % (Colors.vName, Colors.valEm,
789 ColorsNormal)
852 ColorsNormal)
@@ -799,8 +862,8 b' class VerboseTB(TBTools):'
799 ColorsNormal)
862 ColorsNormal)
800
863
801 abspath = os.path.abspath
864 abspath = os.path.abspath
802 for frame, file, lnum, func, lines, index in records:
865
803 #print '*** record:',file,lnum,func,lines,index # dbg
866
804 if not file:
867 if not file:
805 file = '?'
868 file = '?'
806 elif file.startswith(str("<")) and file.endswith(str(">")):
869 elif file.startswith(str("<")) and file.endswith(str(">")):
@@ -854,8 +917,8 b' class VerboseTB(TBTools):'
854
917
855 # Don't attempt to tokenize binary files.
918 # Don't attempt to tokenize binary files.
856 if file.endswith(('.so', '.pyd', '.dll')):
919 if file.endswith(('.so', '.pyd', '.dll')):
857 frames.append('%s %s\n' % (link, call))
920 return '%s %s\n' % (link, call)
858 continue
921
859 elif file.endswith(('.pyc', '.pyo')):
922 elif file.endswith(('.pyc', '.pyo')):
860 # Look up the corresponding source file.
923 # Look up the corresponding source file.
861 file = openpy.source_from_cache(file)
924 file = openpy.source_from_cache(file)
@@ -942,13 +1005,11 b' class VerboseTB(TBTools):'
942 level = '%s %s\n' % (link, call)
1005 level = '%s %s\n' % (link, call)
943
1006
944 if index is None:
1007 if index is None:
945 frames.append(level)
1008 return level
946 else:
1009 else:
947 frames.append('%s%s' % (level, ''.join(
1010 return '%s%s' % (level, ''.join(
948 _format_traceback_lines(lnum, index, lines, Colors, lvals,
1011 _format_traceback_lines(lnum, index, lines, Colors, lvals,
949 col_scheme))))
1012 col_scheme)))
950
951 return frames
952
1013
953 def prepare_chained_exception_message(self, cause):
1014 def prepare_chained_exception_message(self, cause):
954 direct_cause = "\nThe above exception was the direct cause of the following exception:\n"
1015 direct_cause = "\nThe above exception was the direct cause of the following exception:\n"
@@ -1016,7 +1077,13 b' class VerboseTB(TBTools):'
1016 return exception
1077 return exception
1017
1078
1018 def format_exception_as_a_whole(self, etype, evalue, etb, number_of_lines_of_context, tb_offset):
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 # some locals
1085 # some locals
1086 orig_etype = etype
1020 try:
1087 try:
1021 etype = etype.__name__
1088 etype = etype.__name__
1022 except AttributeError:
1089 except AttributeError:
@@ -1029,7 +1096,9 b' class VerboseTB(TBTools):'
1029 if records is None:
1096 if records is None:
1030 return ""
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 formatted_exception = self.format_exception(etype, evalue)
1103 formatted_exception = self.format_exception(etype, evalue)
1035 if records:
1104 if records:
General Comments 0
You need to be logged in to leave comments. Login now