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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
1000 | if lvals: | |
940 | lvals = '' |
|
1001 | lvals = '%s%s' % (indent, em_normal.join(lvals)) | |
941 |
|
1002 | else: | ||
942 |
l |
|
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