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 |
|
|
805 | 868 |
|
|
806 | 869 |
|
@@ -854,8 +917,8 b' class VerboseTB(TBTools):' | |||
|
854 | 917 | |
|
855 | 918 |
|
|
856 | 919 |
|
|
857 |
|
|
|
858 | continue | |
|
920 | return '%s %s\n' % (link, call) | |
|
921 | ||
|
859 | 922 |
|
|
860 | 923 |
|
|
861 | 924 |
|
@@ -942,13 +1005,11 b' class VerboseTB(TBTools):' | |||
|
942 | 1005 |
|
|
943 | 1006 | |
|
944 | 1007 |
|
|
945 |
|
|
|
1008 | return level | |
|
946 | 1009 |
|
|
947 |
|
|
|
1010 | return '%s%s' % (level, ''.join( | |
|
948 | 1011 |
|
|
949 |
|
|
|
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