# encoding: utf-8 """Tests for IPython.core.ultratb """ import io import logging import sys import os.path from textwrap import dedent import traceback import unittest from unittest import mock import IPython.core.ultratb as ultratb from IPython.core.ultratb import ColorTB, VerboseTB, find_recursion from IPython.testing import tools as tt from IPython.testing.decorators import onlyif_unicode_paths from IPython.utils.syspathcontext import prepended_to_syspath from IPython.utils.tempdir import TemporaryDirectory file_1 = """1 2 3 def f(): 1/0 """ file_2 = """def f(): 1/0 """ def recursionlimit(frames): """ decorator to set the recursion limit temporarily """ def inner(test_function): def wrapper(*args, **kwargs): _orig_rec_limit = ultratb._FRAME_RECURSION_LIMIT ultratb._FRAME_RECURSION_LIMIT = 50 rl = sys.getrecursionlimit() sys.setrecursionlimit(frames) try: return test_function(*args, **kwargs) finally: sys.setrecursionlimit(rl) ultratb._FRAME_RECURSION_LIMIT = _orig_rec_limit return wrapper return inner class ChangedPyFileTest(unittest.TestCase): def test_changing_py_file(self): """Traceback produced if the line where the error occurred is missing? https://github.com/ipython/ipython/issues/1456 """ with TemporaryDirectory() as td: fname = os.path.join(td, "foo.py") with open(fname, "w") as f: f.write(file_1) with prepended_to_syspath(td): ip.run_cell("import foo") with tt.AssertPrints("ZeroDivisionError"): ip.run_cell("foo.f()") # Make the file shorter, so the line of the error is missing. with open(fname, "w") as f: f.write(file_2) # For some reason, this was failing on the *second* call after # changing the file, so we call f() twice. with tt.AssertNotPrints("Internal Python error", channel='stderr'): with tt.AssertPrints("ZeroDivisionError"): ip.run_cell("foo.f()") with tt.AssertPrints("ZeroDivisionError"): ip.run_cell("foo.f()") iso_8859_5_file = u'''# coding: iso-8859-5 def fail(): """дбИЖ""" 1/0 # дбИЖ ''' class NonAsciiTest(unittest.TestCase): @onlyif_unicode_paths def test_nonascii_path(self): # Non-ascii directory name as well. with TemporaryDirectory(suffix=u'é') as td: fname = os.path.join(td, u"fooé.py") with open(fname, "w") as f: f.write(file_1) with prepended_to_syspath(td): ip.run_cell("import foo") with tt.AssertPrints("ZeroDivisionError"): ip.run_cell("foo.f()") def test_iso8859_5(self): with TemporaryDirectory() as td: fname = os.path.join(td, 'dfghjkl.py') with io.open(fname, 'w', encoding='iso-8859-5') as f: f.write(iso_8859_5_file) with prepended_to_syspath(td): ip.run_cell("from dfghjkl import fail") with tt.AssertPrints("ZeroDivisionError"): with tt.AssertPrints(u'дбИЖ', suppress=False): ip.run_cell('fail()') def test_nonascii_msg(self): cell = u"raise Exception('é')" expected = u"Exception('é')" ip.run_cell("%xmode plain") with tt.AssertPrints(expected): ip.run_cell(cell) ip.run_cell("%xmode verbose") with tt.AssertPrints(expected): ip.run_cell(cell) ip.run_cell("%xmode context") with tt.AssertPrints(expected): ip.run_cell(cell) ip.run_cell("%xmode minimal") with tt.AssertPrints(u"Exception: é"): ip.run_cell(cell) # Put this back into Context mode for later tests. ip.run_cell("%xmode context") class NestedGenExprTestCase(unittest.TestCase): """ Regression test for the following issues: https://github.com/ipython/ipython/issues/8293 https://github.com/ipython/ipython/issues/8205 """ def test_nested_genexpr(self): code = dedent( """\ class SpecificException(Exception): pass def foo(x): raise SpecificException("Success!") sum(sum(foo(x) for _ in [0]) for x in [0]) """ ) with tt.AssertPrints('SpecificException: Success!', suppress=False): ip.run_cell(code) indentationerror_file = """if True: zoon() """ class IndentationErrorTest(unittest.TestCase): def test_indentationerror_shows_line(self): # See issue gh-2398 with tt.AssertPrints("IndentationError"): with tt.AssertPrints("zoon()", suppress=False): ip.run_cell(indentationerror_file) with TemporaryDirectory() as td: fname = os.path.join(td, "foo.py") with open(fname, "w") as f: f.write(indentationerror_file) with tt.AssertPrints("IndentationError"): with tt.AssertPrints("zoon()", suppress=False): ip.magic('run %s' % fname) se_file_1 = """1 2 7/ """ se_file_2 = """7/ """ class SyntaxErrorTest(unittest.TestCase): def test_syntaxerror_without_lineno(self): with tt.AssertNotPrints("TypeError"): with tt.AssertPrints("line unknown"): ip.run_cell("raise SyntaxError()") def test_syntaxerror_no_stacktrace_at_compile_time(self): syntax_error_at_compile_time = """ def foo(): .. """ with tt.AssertPrints("SyntaxError"): ip.run_cell(syntax_error_at_compile_time) with tt.AssertNotPrints("foo()"): ip.run_cell(syntax_error_at_compile_time) def test_syntaxerror_stacktrace_when_running_compiled_code(self): syntax_error_at_runtime = """ def foo(): eval("..") def bar(): foo() bar() """ with tt.AssertPrints("SyntaxError"): ip.run_cell(syntax_error_at_runtime) # Assert syntax error during runtime generate stacktrace with tt.AssertPrints(["foo()", "bar()"]): ip.run_cell(syntax_error_at_runtime) del ip.user_ns['bar'] del ip.user_ns['foo'] def test_changing_py_file(self): with TemporaryDirectory() as td: fname = os.path.join(td, "foo.py") with open(fname, 'w') as f: f.write(se_file_1) with tt.AssertPrints(["7/", "SyntaxError"]): ip.magic("run " + fname) # Modify the file with open(fname, 'w') as f: f.write(se_file_2) # The SyntaxError should point to the correct line with tt.AssertPrints(["7/", "SyntaxError"]): ip.magic("run " + fname) def test_non_syntaxerror(self): # SyntaxTB may be called with an error other than a SyntaxError # See e.g. gh-4361 try: raise ValueError('QWERTY') except ValueError: with tt.AssertPrints('QWERTY'): ip.showsyntaxerror() class Python3ChainedExceptionsTest(unittest.TestCase): DIRECT_CAUSE_ERROR_CODE = """ try: x = 1 + 2 print(not_defined_here) except Exception as e: x += 55 x - 1 y = {} raise KeyError('uh') from e """ EXCEPTION_DURING_HANDLING_CODE = """ try: x = 1 + 2 print(not_defined_here) except Exception as e: x += 55 x - 1 y = {} raise KeyError('uh') """ SUPPRESS_CHAINING_CODE = """ try: 1/0 except Exception: raise ValueError("Yikes") from None """ def test_direct_cause_error(self): with tt.AssertPrints(["KeyError", "NameError", "direct cause"]): ip.run_cell(self.DIRECT_CAUSE_ERROR_CODE) def test_exception_during_handling_error(self): with tt.AssertPrints(["KeyError", "NameError", "During handling"]): ip.run_cell(self.EXCEPTION_DURING_HANDLING_CODE) def test_suppress_exception_chaining(self): with tt.AssertNotPrints("ZeroDivisionError"), \ tt.AssertPrints("ValueError", suppress=False): ip.run_cell(self.SUPPRESS_CHAINING_CODE) class RecursionTest(unittest.TestCase): DEFINITIONS = """ def non_recurs(): 1/0 def r1(): r1() def r3a(): r3b() def r3b(): r3c() def r3c(): r3a() def r3o1(): r3a() def r3o2(): r3o1() """ def setUp(self): ip.run_cell(self.DEFINITIONS) def test_no_recursion(self): with tt.AssertNotPrints("frames repeated"): ip.run_cell("non_recurs()") @recursionlimit(150) def test_recursion_one_frame(self): with tt.AssertPrints("1 frames repeated"): ip.run_cell("r1()") @recursionlimit(150) def test_recursion_three_frames(self): with tt.AssertPrints("3 frames repeated"): ip.run_cell("r3o2()") @recursionlimit(150) def test_find_recursion(self): captured = [] def capture_exc(*args, **kwargs): captured.append(sys.exc_info()) with mock.patch.object(ip, 'showtraceback', capture_exc): ip.run_cell("r3o2()") self.assertEqual(len(captured), 1) etype, evalue, tb = captured[0] self.assertIn("recursion", str(evalue)) records = ip.InteractiveTB.get_records(tb, 3, ip.InteractiveTB.tb_offset) for r in records[:10]: print(r[1:4]) # The outermost frames should be: # 0: the 'cell' that was running when the exception came up # 1: r3o2() # 2: r3o1() # 3: r3a() # Then repeating r3b, r3c, r3a last_unique, repeat_length = find_recursion(etype, evalue, records) self.assertEqual(last_unique, 2) self.assertEqual(repeat_length, 3) #---------------------------------------------------------------------------- # module testing (minimal) def test_handlers(): def spam(c, d_e): (d, e) = d_e x = c + d y = c * d foo(x, y) def foo(a, b, bar=1): eggs(a, b + bar) def eggs(f, g, z=globals()): h = f + g i = f - g return h / i buff = io.StringIO() buff.write('') buff.write('*** Before ***') try: buff.write(spam(1, (2, 3))) except: traceback.print_exc(file=buff) handler = ColorTB(ostream=buff) buff.write('*** ColorTB ***') try: buff.write(spam(1, (2, 3))) except: handler(*sys.exc_info()) buff.write('') handler = VerboseTB(ostream=buff) buff.write('*** VerboseTB ***') try: buff.write(spam(1, (2, 3))) except: handler(*sys.exc_info()) buff.write('') from IPython.testing.decorators import skipif class TokenizeFailureTest(unittest.TestCase): """Tests related to https://github.com/ipython/ipython/issues/6864.""" # that appear to test that we are handling an exception that can be thrown # by the tokenizer due to a bug that seem to have been fixed in 3.8, though # I'm unsure if other sequences can make it raise this error. Let's just # skip in 3.8 for now @skipif(sys.version_info > (3,8)) def testLogging(self): message = "An unexpected error occurred while tokenizing input" cell = 'raise ValueError("""a\nb""")' stream = io.StringIO() handler = logging.StreamHandler(stream) logger = logging.getLogger() loglevel = logger.level logger.addHandler(handler) self.addCleanup(lambda: logger.removeHandler(handler)) self.addCleanup(lambda: logger.setLevel(loglevel)) logger.setLevel(logging.INFO) with tt.AssertNotPrints(message): ip.run_cell(cell) self.assertNotIn(message, stream.getvalue()) logger.setLevel(logging.DEBUG) with tt.AssertNotPrints(message): ip.run_cell(cell) self.assertIn(message, stream.getvalue())