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 |
|
|
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 |
|
|
920 | return '%s %s\n' % (link, call) | |
858 | continue |
|
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 |
|
|
1008 | return level | |
946 |
|
|
1009 | else: | |
947 |
|
|
1010 | return '%s%s' % (level, ''.join( | |
948 |
|
|
1011 | _format_traceback_lines(lnum, index, lines, Colors, lvals, | |
949 |
|
|
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