##// 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,156 +862,154 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:
803 #print '*** record:',file,lnum,func,lines,index # dbg
804 if not file:
805 file = '?'
806 elif file.startswith(str("<")) and file.endswith(str(">")):
807 # Not a real filename, no problem...
808 pass
809 elif not os.path.isabs(file):
810 # Try to make the filename absolute by trying all
811 # sys.path entries (which is also what linecache does)
812 for dirname in sys.path:
813 try:
814 fullname = os.path.join(dirname, file)
815 if os.path.isfile(fullname):
816 file = os.path.abspath(fullname)
817 break
818 except Exception:
819 # Just in case that sys.path contains very
820 # strange entries...
821 pass
822
865
823 file = py3compat.cast_unicode(file, util_path.fs_encoding)
824 link = tpl_link % file
825 args, varargs, varkw, locals = fixed_getargvalues(frame)
826
866
827 if func == '?':
867 if not file:
828 call = ''
868 file = '?'
829 else:
869 elif file.startswith(str("<")) and file.endswith(str(">")):
830 # Decide whether to include variable details or not
870 # Not a real filename, no problem...
831 var_repr = self.include_vars and eqrepr or nullrepr
871 pass
872 elif not os.path.isabs(file):
873 # Try to make the filename absolute by trying all
874 # sys.path entries (which is also what linecache does)
875 for dirname in sys.path:
832 try:
876 try:
833 call = tpl_call % (func, inspect.formatargvalues(args,
877 fullname = os.path.join(dirname, file)
834 varargs, varkw,
878 if os.path.isfile(fullname):
835 locals, formatvalue=var_repr))
879 file = os.path.abspath(fullname)
836 except KeyError:
837 # This happens in situations like errors inside generator
838 # expressions, where local variables are listed in the
839 # line, but can't be extracted from the frame. I'm not
840 # 100% sure this isn't actually a bug in inspect itself,
841 # but since there's no info for us to compute with, the
842 # best we can do is report the failure and move on. Here
843 # we must *not* call any traceback construction again,
844 # because that would mess up use of %debug later on. So we
845 # simply report the failure and move on. The only
846 # limitation will be that this frame won't have locals
847 # listed in the call signature. Quite subtle problem...
848 # I can't think of a good way to validate this in a unit
849 # test, but running a script consisting of:
850 # dict( (k,v.strip()) for (k,v) in range(10) )
851 # will illustrate the error, if this exception catch is
852 # disabled.
853 call = tpl_call_fail % func
854
855 # Don't attempt to tokenize binary files.
856 if file.endswith(('.so', '.pyd', '.dll')):
857 frames.append('%s %s\n' % (link, call))
858 continue
859 elif file.endswith(('.pyc', '.pyo')):
860 # Look up the corresponding source file.
861 file = openpy.source_from_cache(file)
862
863 def linereader(file=file, lnum=[lnum], getline=ulinecache.getline):
864 line = getline(file, lnum[0])
865 lnum[0] += 1
866 return line
867
868 # Build the list of names on this line of code where the exception
869 # occurred.
870 try:
871 names = []
872 name_cont = False
873
874 for token_type, token, start, end, line in generate_tokens(linereader):
875 # build composite names
876 if token_type == tokenize.NAME and token not in keyword.kwlist:
877 if name_cont:
878 # Continuation of a dotted name
879 try:
880 names[-1].append(token)
881 except IndexError:
882 names.append([token])
883 name_cont = False
884 else:
885 # Regular new names. We append everything, the caller
886 # will be responsible for pruning the list later. It's
887 # very tricky to try to prune as we go, b/c composite
888 # names can fool us. The pruning at the end is easy
889 # to do (or the caller can print a list with repeated
890 # names if so desired.
891 names.append([token])
892 elif token == '.':
893 name_cont = True
894 elif token_type == tokenize.NEWLINE:
895 break
880 break
881 except Exception:
882 # Just in case that sys.path contains very
883 # strange entries...
884 pass
896
885
897 except (IndexError, UnicodeDecodeError, SyntaxError):
886 file = py3compat.cast_unicode(file, util_path.fs_encoding)
898 # signals exit of tokenizer
887 link = tpl_link % file
899 # SyntaxError can occur if the file is not actually Python
888 args, varargs, varkw, locals = fixed_getargvalues(frame)
900 # - see gh-6300
889
901 pass
890 if func == '?':
902 except tokenize.TokenError as msg:
891 call = ''
903 _m = ("An unexpected error occurred while tokenizing input\n"
892 else:
904 "The following traceback may be corrupted or invalid\n"
893 # Decide whether to include variable details or not
905 "The error message is: %s\n" % msg)
894 var_repr = self.include_vars and eqrepr or nullrepr
906 error(_m)
895 try:
907
896 call = tpl_call % (func, inspect.formatargvalues(args,
908 # Join composite names (e.g. "dict.fromkeys")
897 varargs, varkw,
909 names = ['.'.join(n) for n in names]
898 locals, formatvalue=var_repr))
910 # prune names list of duplicates, but keep the right order
899 except KeyError:
911 unique_names = uniq_stable(names)
900 # This happens in situations like errors inside generator
912
901 # expressions, where local variables are listed in the
913 # Start loop over vars
902 # line, but can't be extracted from the frame. I'm not
914 lvals = []
903 # 100% sure this isn't actually a bug in inspect itself,
915 if self.include_vars:
904 # but since there's no info for us to compute with, the
916 for name_full in unique_names:
905 # best we can do is report the failure and move on. Here
917 name_base = name_full.split('.', 1)[0]
906 # we must *not* call any traceback construction again,
918 if name_base in frame.f_code.co_varnames:
907 # because that would mess up use of %debug later on. So we
919 if name_base in locals:
908 # simply report the failure and move on. The only
920 try:
909 # limitation will be that this frame won't have locals
921 value = repr(eval(name_full, locals))
910 # listed in the call signature. Quite subtle problem...
922 except:
911 # I can't think of a good way to validate this in a unit
923 value = undefined
912 # test, but running a script consisting of:
924 else:
913 # dict( (k,v.strip()) for (k,v) in range(10) )
914 # will illustrate the error, if this exception catch is
915 # disabled.
916 call = tpl_call_fail % func
917
918 # Don't attempt to tokenize binary files.
919 if file.endswith(('.so', '.pyd', '.dll')):
920 return '%s %s\n' % (link, call)
921
922 elif file.endswith(('.pyc', '.pyo')):
923 # Look up the corresponding source file.
924 file = openpy.source_from_cache(file)
925
926 def linereader(file=file, lnum=[lnum], getline=ulinecache.getline):
927 line = getline(file, lnum[0])
928 lnum[0] += 1
929 return line
930
931 # Build the list of names on this line of code where the exception
932 # occurred.
933 try:
934 names = []
935 name_cont = False
936
937 for token_type, token, start, end, line in generate_tokens(linereader):
938 # build composite names
939 if token_type == tokenize.NAME and token not in keyword.kwlist:
940 if name_cont:
941 # Continuation of a dotted name
942 try:
943 names[-1].append(token)
944 except IndexError:
945 names.append([token])
946 name_cont = False
947 else:
948 # Regular new names. We append everything, the caller
949 # will be responsible for pruning the list later. It's
950 # very tricky to try to prune as we go, b/c composite
951 # names can fool us. The pruning at the end is easy
952 # to do (or the caller can print a list with repeated
953 # names if so desired.
954 names.append([token])
955 elif token == '.':
956 name_cont = True
957 elif token_type == tokenize.NEWLINE:
958 break
959
960 except (IndexError, UnicodeDecodeError, SyntaxError):
961 # signals exit of tokenizer
962 # SyntaxError can occur if the file is not actually Python
963 # - see gh-6300
964 pass
965 except tokenize.TokenError as msg:
966 _m = ("An unexpected error occurred while tokenizing input\n"
967 "The following traceback may be corrupted or invalid\n"
968 "The error message is: %s\n" % msg)
969 error(_m)
970
971 # Join composite names (e.g. "dict.fromkeys")
972 names = ['.'.join(n) for n in names]
973 # prune names list of duplicates, but keep the right order
974 unique_names = uniq_stable(names)
975
976 # Start loop over vars
977 lvals = []
978 if self.include_vars:
979 for name_full in unique_names:
980 name_base = name_full.split('.', 1)[0]
981 if name_base in frame.f_code.co_varnames:
982 if name_base in locals:
983 try:
984 value = repr(eval(name_full, locals))
985 except:
925 value = undefined
986 value = undefined
926 name = tpl_local_var % name_full
927 else:
987 else:
928 if name_base in frame.f_globals:
988 value = undefined
929 try:
989 name = tpl_local_var % name_full
930 value = repr(eval(name_full, frame.f_globals))
990 else:
931 except:
991 if name_base in frame.f_globals:
932 value = undefined
992 try:
933 else:
993 value = repr(eval(name_full, frame.f_globals))
994 except:
934 value = undefined
995 value = undefined
935 name = tpl_global_var % name_full
996 else:
936 lvals.append(tpl_name_val % (name, value))
997 value = undefined
937 if lvals:
998 name = tpl_global_var % name_full
938 lvals = '%s%s' % (indent, em_normal.join(lvals))
999 lvals.append(tpl_name_val % (name, value))
939 else:
1000 if lvals:
940 lvals = ''
1001 lvals = '%s%s' % (indent, em_normal.join(lvals))
941
1002 else:
942 level = '%s %s\n' % (link, call)
1003 lvals = ''
943
1004
944 if index is None:
1005 level = '%s %s\n' % (link, call)
945 frames.append(level)
946 else:
947 frames.append('%s%s' % (level, ''.join(
948 _format_traceback_lines(lnum, index, lines, Colors, lvals,
949 col_scheme))))
950
1006
951 return frames
1007 if index is None:
1008 return level
1009 else:
1010 return '%s%s' % (level, ''.join(
1011 _format_traceback_lines(lnum, index, lines, Colors, lvals,
1012 col_scheme)))
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