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,156 +862,154 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 | |
|
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 == '?': | |
|
828 |
|
|
|
829 | else: | |
|
830 | # Decide whether to include variable details or not | |
|
831 | var_repr = self.include_vars and eqrepr or nullrepr | |
|
867 | if not file: | |
|
868 | file = '?' | |
|
869 | elif file.startswith(str("<")) and file.endswith(str(">")): | |
|
870 | # Not a real filename, no problem... | |
|
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 | 876 | try: |
|
833 | call = tpl_call % (func, inspect.formatargvalues(args, | |
|
834 | varargs, varkw, | |
|
835 | locals, formatvalue=var_repr)) | |
|
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: | |
|
877 | fullname = os.path.join(dirname, file) | |
|
878 | if os.path.isfile(fullname): | |
|
879 | file = os.path.abspath(fullname) | |
|
895 | 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): | |
|
898 | # signals exit of tokenizer | |
|
899 | # SyntaxError can occur if the file is not actually Python | |
|
900 | # - see gh-6300 | |
|
901 | pass | |
|
902 | except tokenize.TokenError as msg: | |
|
903 | _m = ("An unexpected error occurred while tokenizing input\n" | |
|
904 | "The following traceback may be corrupted or invalid\n" | |
|
905 | "The error message is: %s\n" % msg) | |
|
906 |
|
|
|
907 | ||
|
908 | # Join composite names (e.g. "dict.fromkeys") | |
|
909 | names = ['.'.join(n) for n in names] | |
|
910 | # prune names list of duplicates, but keep the right order | |
|
911 | unique_names = uniq_stable(names) | |
|
912 | ||
|
913 | # Start loop over vars | |
|
914 | lvals = [] | |
|
915 | if self.include_vars: | |
|
916 | for name_full in unique_names: | |
|
917 | name_base = name_full.split('.', 1)[0] | |
|
918 | if name_base in frame.f_code.co_varnames: | |
|
919 | if name_base in locals: | |
|
920 | try: | |
|
921 | value = repr(eval(name_full, locals)) | |
|
922 | except: | |
|
923 | value = undefined | |
|
924 | else: | |
|
886 | file = py3compat.cast_unicode(file, util_path.fs_encoding) | |
|
887 | link = tpl_link % file | |
|
888 | args, varargs, varkw, locals = fixed_getargvalues(frame) | |
|
889 | ||
|
890 | if func == '?': | |
|
891 | call = '' | |
|
892 | else: | |
|
893 | # Decide whether to include variable details or not | |
|
894 | var_repr = self.include_vars and eqrepr or nullrepr | |
|
895 | try: | |
|
896 | call = tpl_call % (func, inspect.formatargvalues(args, | |
|
897 | varargs, varkw, | |
|
898 | locals, formatvalue=var_repr)) | |
|
899 | except KeyError: | |
|
900 | # This happens in situations like errors inside generator | |
|
901 | # expressions, where local variables are listed in the | |
|
902 | # line, but can't be extracted from the frame. I'm not | |
|
903 | # 100% sure this isn't actually a bug in inspect itself, | |
|
904 | # but since there's no info for us to compute with, the | |
|
905 | # best we can do is report the failure and move on. Here | |
|
906 | # we must *not* call any traceback construction again, | |
|
907 | # because that would mess up use of %debug later on. So we | |
|
908 | # simply report the failure and move on. The only | |
|
909 | # limitation will be that this frame won't have locals | |
|
910 | # listed in the call signature. Quite subtle problem... | |
|
911 | # I can't think of a good way to validate this in a unit | |
|
912 | # test, but running a script consisting of: | |
|
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 | 986 | value = undefined |
|
926 | name = tpl_local_var % name_full | |
|
927 | 987 | else: |
|
928 |
|
|
|
929 | try: | |
|
930 | value = repr(eval(name_full, frame.f_globals)) | |
|
931 | except: | |
|
932 |
|
|
|
933 | else: | |
|
988 | value = undefined | |
|
989 | name = tpl_local_var % name_full | |
|
990 | else: | |
|
991 | if name_base in frame.f_globals: | |
|
992 | try: | |
|
993 | value = repr(eval(name_full, frame.f_globals)) | |
|
994 | except: | |
|
934 | 995 | value = undefined |
|
935 | name = tpl_global_var % name_full | |
|
936 | lvals.append(tpl_name_val % (name, value)) | |
|
937 | if lvals: | |
|
938 | lvals = '%s%s' % (indent, em_normal.join(lvals)) | |
|
939 |
|
|
|
940 | lvals = '' | |
|
941 | ||
|
942 |
l |
|
|
996 | else: | |
|
997 | value = undefined | |
|
998 | name = tpl_global_var % name_full | |
|
999 | lvals.append(tpl_name_val % (name, value)) | |
|
1000 | if lvals: | |
|
1001 | lvals = '%s%s' % (indent, em_normal.join(lvals)) | |
|
1002 | else: | |
|
1003 | lvals = '' | |
|
943 | 1004 | |
|
944 | if index is None: | |
|
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)))) | |
|
1005 | level = '%s %s\n' % (link, call) | |
|
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 | 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