diff --git a/Lib/code.py b/Lib/code.py
index 2bd5fa3e79..2777c31118 100644
--- a/Lib/code.py
+++ b/Lib/code.py
@@ -5,6 +5,7 @@
# Inspired by similar code by Jeff Epler and Fredrik Lundh.
+import builtins
import sys
import traceback
from codeop import CommandCompiler, compile_command
@@ -12,6 +13,7 @@
__all__ = ["InteractiveInterpreter", "InteractiveConsole", "interact",
"compile_command"]
+
class InteractiveInterpreter:
"""Base class for InteractiveConsole.
@@ -24,10 +26,10 @@ class InteractiveInterpreter:
def __init__(self, locals=None):
"""Constructor.
- The optional 'locals' argument specifies the dictionary in
- which code will be executed; it defaults to a newly created
- dictionary with key "__name__" set to "__console__" and key
- "__doc__" set to None.
+ The optional 'locals' argument specifies a mapping to use as the
+ namespace in which code will be executed; it defaults to a newly
+ created dictionary with key "__name__" set to "__console__" and
+ key "__doc__" set to None.
"""
if locals is None:
@@ -63,7 +65,7 @@ def runsource(self, source, filename="", symbol="single"):
code = self.compile(source, filename, symbol)
except (OverflowError, SyntaxError, ValueError):
# Case 1
- self.showsyntaxerror(filename)
+ self.showsyntaxerror(filename, source=source)
return False
if code is None:
@@ -93,7 +95,7 @@ def runcode(self, code):
except:
self.showtraceback()
- def showsyntaxerror(self, filename=None):
+ def showsyntaxerror(self, filename=None, **kwargs):
"""Display the syntax error that just occurred.
This doesn't display a stack trace because there isn't one.
@@ -105,29 +107,14 @@ def showsyntaxerror(self, filename=None):
The output is written by self.write(), below.
"""
- type, value, tb = sys.exc_info()
- sys.last_exc = value
- sys.last_type = type
- sys.last_value = value
- sys.last_traceback = tb
- if filename and type is SyntaxError:
- # Work hard to stuff the correct filename in the exception
- try:
- msg, (dummy_filename, lineno, offset, line) = value.args
- except ValueError:
- # Not the format we expect; leave it alone
- pass
- else:
- # Stuff in the right filename
- value = SyntaxError(msg, (filename, lineno, offset, line))
- sys.last_exc = sys.last_value = value
- if sys.excepthook is sys.__excepthook__:
- lines = traceback.format_exception_only(type, value)
- self.write(''.join(lines))
- else:
- # If someone has set sys.excepthook, we let that take precedence
- # over self.write
- sys.excepthook(type, value, tb)
+ try:
+ typ, value, tb = sys.exc_info()
+ if filename and issubclass(typ, SyntaxError):
+ value.filename = filename
+ source = kwargs.pop('source', "")
+ self._showtraceback(typ, value, None, source)
+ finally:
+ typ = value = tb = None
def showtraceback(self):
"""Display the exception that just occurred.
@@ -137,19 +124,46 @@ def showtraceback(self):
The output is written by self.write(), below.
"""
- sys.last_type, sys.last_value, last_tb = ei = sys.exc_info()
- sys.last_traceback = last_tb
- sys.last_exc = ei[1]
try:
- lines = traceback.format_exception(ei[0], ei[1], last_tb.tb_next)
- if sys.excepthook is sys.__excepthook__:
- self.write(''.join(lines))
- else:
- # If someone has set sys.excepthook, we let that take precedence
- # over self.write
- sys.excepthook(ei[0], ei[1], last_tb)
+ typ, value, tb = sys.exc_info()
+ self._showtraceback(typ, value, tb.tb_next, '')
finally:
- last_tb = ei = None
+ typ = value = tb = None
+
+ def _showtraceback(self, typ, value, tb, source):
+ sys.last_type = typ
+ sys.last_traceback = tb
+ value = value.with_traceback(tb)
+ # Set the line of text that the exception refers to
+ lines = source.splitlines()
+ if (source and typ is SyntaxError
+ and not value.text and value.lineno is not None
+ and len(lines) >= value.lineno):
+ value.text = lines[value.lineno - 1]
+ sys.last_exc = sys.last_value = value = value.with_traceback(tb)
+ if sys.excepthook is sys.__excepthook__:
+ self._excepthook(typ, value, tb)
+ else:
+ # If someone has set sys.excepthook, we let that take precedence
+ # over self.write
+ try:
+ sys.excepthook(typ, value, tb)
+ except SystemExit:
+ raise
+ except BaseException as e:
+ e.__context__ = None
+ e = e.with_traceback(e.__traceback__.tb_next)
+ print('Error in sys.excepthook:', file=sys.stderr)
+ sys.__excepthook__(type(e), e, e.__traceback__)
+ print(file=sys.stderr)
+ print('Original exception was:', file=sys.stderr)
+ sys.__excepthook__(typ, value, tb)
+
+ def _excepthook(self, typ, value, tb):
+ # This method is being overwritten in
+ # _pyrepl.console.InteractiveColoredConsole
+ lines = traceback.format_exception(typ, value, tb)
+ self.write(''.join(lines))
def write(self, data):
"""Write a string.
@@ -169,7 +183,7 @@ class InteractiveConsole(InteractiveInterpreter):
"""
- def __init__(self, locals=None, filename=""):
+ def __init__(self, locals=None, filename="", *, local_exit=False):
"""Constructor.
The optional locals argument will be passed to the
@@ -181,6 +195,7 @@ def __init__(self, locals=None, filename=""):
"""
InteractiveInterpreter.__init__(self, locals)
self.filename = filename
+ self.local_exit = local_exit
self.resetbuffer()
def resetbuffer(self):
@@ -219,29 +234,66 @@ def interact(self, banner=None, exitmsg=None):
elif banner:
self.write("%s\n" % str(banner))
more = 0
- while 1:
- try:
- if more:
- prompt = sys.ps2
- else:
- prompt = sys.ps1
+
+ # When the user uses exit() or quit() in their interactive shell
+ # they probably just want to exit the created shell, not the whole
+ # process. exit and quit in builtins closes sys.stdin which makes
+ # it super difficult to restore
+ #
+ # When self.local_exit is True, we overwrite the builtins so
+ # exit() and quit() only raises SystemExit and we can catch that
+ # to only exit the interactive shell
+
+ _exit = None
+ _quit = None
+
+ if self.local_exit:
+ if hasattr(builtins, "exit"):
+ _exit = builtins.exit
+ builtins.exit = Quitter("exit")
+
+ if hasattr(builtins, "quit"):
+ _quit = builtins.quit
+ builtins.quit = Quitter("quit")
+
+ try:
+ while True:
try:
- line = self.raw_input(prompt)
- except EOFError:
- self.write("\n")
- break
- else:
- more = self.push(line)
- except KeyboardInterrupt:
- self.write("\nKeyboardInterrupt\n")
- self.resetbuffer()
- more = 0
- if exitmsg is None:
- self.write('now exiting %s...\n' % self.__class__.__name__)
- elif exitmsg != '':
- self.write('%s\n' % exitmsg)
-
- def push(self, line):
+ if more:
+ prompt = sys.ps2
+ else:
+ prompt = sys.ps1
+ try:
+ line = self.raw_input(prompt)
+ except EOFError:
+ self.write("\n")
+ break
+ else:
+ more = self.push(line)
+ except KeyboardInterrupt:
+ self.write("\nKeyboardInterrupt\n")
+ self.resetbuffer()
+ more = 0
+ except SystemExit as e:
+ if self.local_exit:
+ self.write("\n")
+ break
+ else:
+ raise e
+ finally:
+ # restore exit and quit in builtins if they were modified
+ if _exit is not None:
+ builtins.exit = _exit
+
+ if _quit is not None:
+ builtins.quit = _quit
+
+ if exitmsg is None:
+ self.write('now exiting %s...\n' % self.__class__.__name__)
+ elif exitmsg != '':
+ self.write('%s\n' % exitmsg)
+
+ def push(self, line, filename=None, _symbol="single"):
"""Push a line to the interpreter.
The line should not have a trailing newline; it may have
@@ -257,7 +309,9 @@ def push(self, line):
"""
self.buffer.append(line)
source = "\n".join(self.buffer)
- more = self.runsource(source, self.filename)
+ if filename is None:
+ filename = self.filename
+ more = self.runsource(source, filename, symbol=_symbol)
if not more:
self.resetbuffer()
return more
@@ -276,8 +330,22 @@ def raw_input(self, prompt=""):
return input(prompt)
+class Quitter:
+ def __init__(self, name):
+ self.name = name
+ if sys.platform == "win32":
+ self.eof = 'Ctrl-Z plus Return'
+ else:
+ self.eof = 'Ctrl-D (i.e. EOF)'
+
+ def __repr__(self):
+ return f'Use {self.name} or {self.eof} to exit'
+
+ def __call__(self, code=None):
+ raise SystemExit(code)
+
-def interact(banner=None, readfunc=None, local=None, exitmsg=None):
+def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False):
"""Closely emulate the interactive Python interpreter.
This is a backwards compatible interface to the InteractiveConsole
@@ -290,9 +358,10 @@ def interact(banner=None, readfunc=None, local=None, exitmsg=None):
readfunc -- if not None, replaces InteractiveConsole.raw_input()
local -- passed to InteractiveInterpreter.__init__()
exitmsg -- passed to InteractiveConsole.interact()
+ local_exit -- passed to InteractiveConsole.__init__()
"""
- console = InteractiveConsole(local)
+ console = InteractiveConsole(local, local_exit=local_exit)
if readfunc is not None:
console.raw_input = readfunc
else:
@@ -308,7 +377,7 @@ def interact(banner=None, readfunc=None, local=None, exitmsg=None):
parser = argparse.ArgumentParser()
parser.add_argument('-q', action='store_true',
- help="don't print version and copyright messages")
+ help="don't print version and copyright messages")
args = parser.parse_args()
if args.q or sys.flags.quiet:
banner = ''
diff --git a/Lib/test/test_code_module.py b/Lib/test/test_code_module.py
index 5ac17ef16e..346cf6746a 100644
--- a/Lib/test/test_code_module.py
+++ b/Lib/test/test_code_module.py
@@ -1,20 +1,17 @@
"Test InteractiveConsole and InteractiveInterpreter from code module"
import sys
+import traceback
import unittest
from textwrap import dedent
from contextlib import ExitStack
from unittest import mock
+from test.support import force_not_colorized_test_class
from test.support import import_helper
-
code = import_helper.import_module('code')
-class TestInteractiveConsole(unittest.TestCase):
-
- def setUp(self):
- self.console = code.InteractiveConsole()
- self.mock_sys()
+class MockSys:
def mock_sys(self):
"Mock system environment for InteractiveConsole"
@@ -32,6 +29,15 @@ def mock_sys(self):
del self.sysmod.ps1
del self.sysmod.ps2
+
+@force_not_colorized_test_class
+class TestInteractiveConsole(unittest.TestCase, MockSys):
+ maxDiff = None
+
+ def setUp(self):
+ self.console = code.InteractiveConsole()
+ self.mock_sys()
+
def test_ps1(self):
self.infunc.side_effect = EOFError('Finished')
self.console.interact()
@@ -44,9 +50,9 @@ def test_ps2(self):
self.infunc.side_effect = EOFError('Finished')
self.console.interact()
self.assertEqual(self.sysmod.ps2, '... ')
- self.sysmod.ps1 = 'custom2> '
+ self.sysmod.ps2 = 'custom2> '
self.console.interact()
- self.assertEqual(self.sysmod.ps1, 'custom2> ')
+ self.assertEqual(self.sysmod.ps2, 'custom2> ')
def test_console_stderr(self):
self.infunc.side_effect = ["'antioch'", "", EOFError('Finished')]
@@ -57,22 +63,162 @@ def test_console_stderr(self):
else:
raise AssertionError("no console stdout")
+ # TODO: RUSTPYTHON
+ @unittest.expectedFailure
def test_syntax_error(self):
- self.infunc.side_effect = ["undefined", EOFError('Finished')]
+ self.infunc.side_effect = ["def f():",
+ " x = ?",
+ "",
+ EOFError('Finished')]
self.console.interact()
- for call in self.stderr.method_calls:
- if 'NameError' in ''.join(call[1]):
- break
- else:
- raise AssertionError("No syntax error from console")
+ output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
+ output = output[output.index('(InteractiveConsole)'):]
+ output = output[:output.index('\nnow exiting')]
+ self.assertEqual(output.splitlines()[1:], [
+ ' File "", line 2',
+ ' x = ?',
+ ' ^',
+ 'SyntaxError: invalid syntax'])
+ self.assertIs(self.sysmod.last_type, SyntaxError)
+ self.assertIs(type(self.sysmod.last_value), SyntaxError)
+ self.assertIsNone(self.sysmod.last_traceback)
+ self.assertIsNone(self.sysmod.last_value.__traceback__)
+ self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
+
+ # TODO: RUSTPYTHON
+ @unittest.expectedFailure
+ def test_indentation_error(self):
+ self.infunc.side_effect = [" 1", EOFError('Finished')]
+ self.console.interact()
+ output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
+ output = output[output.index('(InteractiveConsole)'):]
+ output = output[:output.index('\nnow exiting')]
+ self.assertEqual(output.splitlines()[1:], [
+ ' File "", line 1',
+ ' 1',
+ 'IndentationError: unexpected indent'])
+ self.assertIs(self.sysmod.last_type, IndentationError)
+ self.assertIs(type(self.sysmod.last_value), IndentationError)
+ self.assertIsNone(self.sysmod.last_traceback)
+ self.assertIsNone(self.sysmod.last_value.__traceback__)
+ self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
+
+ # TODO: RUSTPYTHON
+ @unittest.expectedFailure
+ def test_unicode_error(self):
+ self.infunc.side_effect = ["'\ud800'", EOFError('Finished')]
+ self.console.interact()
+ output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
+ output = output[output.index('(InteractiveConsole)'):]
+ output = output[output.index('\n') + 1:]
+ self.assertTrue(output.startswith('UnicodeEncodeError: '), output)
+ self.assertIs(self.sysmod.last_type, UnicodeEncodeError)
+ self.assertIs(type(self.sysmod.last_value), UnicodeEncodeError)
+ self.assertIsNone(self.sysmod.last_traceback)
+ self.assertIsNone(self.sysmod.last_value.__traceback__)
+ self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
def test_sysexcepthook(self):
- self.infunc.side_effect = ["raise ValueError('')",
+ self.infunc.side_effect = ["def f():",
+ " raise ValueError('BOOM!')",
+ "",
+ "f()",
+ EOFError('Finished')]
+ hook = mock.Mock()
+ self.sysmod.excepthook = hook
+ self.console.interact()
+ hook.assert_called()
+ hook.assert_called_with(self.sysmod.last_type,
+ self.sysmod.last_value,
+ self.sysmod.last_traceback)
+ self.assertIs(self.sysmod.last_type, ValueError)
+ self.assertIs(type(self.sysmod.last_value), ValueError)
+ self.assertIs(self.sysmod.last_traceback, self.sysmod.last_value.__traceback__)
+ self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
+ self.assertEqual(traceback.format_exception(self.sysmod.last_exc), [
+ 'Traceback (most recent call last):\n',
+ ' File "", line 1, in \n',
+ ' File "", line 2, in f\n',
+ 'ValueError: BOOM!\n'])
+
+ # TODO: RUSTPYTHON
+ @unittest.expectedFailure
+ def test_sysexcepthook_syntax_error(self):
+ self.infunc.side_effect = ["def f():",
+ " x = ?",
+ "",
EOFError('Finished')]
hook = mock.Mock()
self.sysmod.excepthook = hook
self.console.interact()
- self.assertTrue(hook.called)
+ hook.assert_called()
+ hook.assert_called_with(self.sysmod.last_type,
+ self.sysmod.last_value,
+ self.sysmod.last_traceback)
+ self.assertIs(self.sysmod.last_type, SyntaxError)
+ self.assertIs(type(self.sysmod.last_value), SyntaxError)
+ self.assertIsNone(self.sysmod.last_traceback)
+ self.assertIsNone(self.sysmod.last_value.__traceback__)
+ self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
+ self.assertEqual(traceback.format_exception(self.sysmod.last_exc), [
+ ' File "", line 2\n',
+ ' x = ?\n',
+ ' ^\n',
+ 'SyntaxError: invalid syntax\n'])
+
+ # TODO: RUSTPYTHON
+ @unittest.expectedFailure
+ def test_sysexcepthook_indentation_error(self):
+ self.infunc.side_effect = [" 1", EOFError('Finished')]
+ hook = mock.Mock()
+ self.sysmod.excepthook = hook
+ self.console.interact()
+ hook.assert_called()
+ hook.assert_called_with(self.sysmod.last_type,
+ self.sysmod.last_value,
+ self.sysmod.last_traceback)
+ self.assertIs(self.sysmod.last_type, IndentationError)
+ self.assertIs(type(self.sysmod.last_value), IndentationError)
+ self.assertIsNone(self.sysmod.last_traceback)
+ self.assertIsNone(self.sysmod.last_value.__traceback__)
+ self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
+ self.assertEqual(traceback.format_exception(self.sysmod.last_exc), [
+ ' File "", line 1\n',
+ ' 1\n',
+ 'IndentationError: unexpected indent\n'])
+
+ def test_sysexcepthook_crashing_doesnt_close_repl(self):
+ self.infunc.side_effect = ["1/0", "a = 123", "print(a)", EOFError('Finished')]
+ self.sysmod.excepthook = 1
+ self.console.interact()
+ self.assertEqual(['write', ('123', ), {}], self.stdout.method_calls[0])
+ error = "".join(call.args[0] for call in self.stderr.method_calls if call[0] == 'write')
+ self.assertIn("Error in sys.excepthook:", error)
+ self.assertEqual(error.count("'int' object is not callable"), 1)
+ self.assertIn("Original exception was:", error)
+ self.assertIn("division by zero", error)
+
+ def test_sysexcepthook_raising_BaseException(self):
+ self.infunc.side_effect = ["1/0", "a = 123", "print(a)", EOFError('Finished')]
+ s = "not so fast"
+ def raise_base(*args, **kwargs):
+ raise BaseException(s)
+ self.sysmod.excepthook = raise_base
+ self.console.interact()
+ self.assertEqual(['write', ('123', ), {}], self.stdout.method_calls[0])
+ error = "".join(call.args[0] for call in self.stderr.method_calls if call[0] == 'write')
+ self.assertIn("Error in sys.excepthook:", error)
+ self.assertEqual(error.count("not so fast"), 1)
+ self.assertIn("Original exception was:", error)
+ self.assertIn("division by zero", error)
+
+ def test_sysexcepthook_raising_SystemExit_gets_through(self):
+ self.infunc.side_effect = ["1/0"]
+ def raise_base(*args, **kwargs):
+ raise SystemExit
+ self.sysmod.excepthook = raise_base
+ with self.assertRaises(SystemExit):
+ self.console.interact()
def test_banner(self):
# with banner
@@ -115,6 +261,7 @@ def test_exit_msg(self):
expected = message + '\n'
self.assertEqual(err_msg, ['write', (expected,), {}])
+
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_cause_tb(self):
@@ -132,9 +279,12 @@ def test_cause_tb(self):
ValueError
""")
self.assertIn(expected, output)
+ self.assertIs(self.sysmod.last_type, ValueError)
+ self.assertIs(type(self.sysmod.last_value), ValueError)
+ self.assertIs(self.sysmod.last_traceback, self.sysmod.last_value.__traceback__)
+ self.assertIsNotNone(self.sysmod.last_traceback)
+ self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
- # TODO: RUSTPYTHON
- @unittest.expectedFailure
def test_context_tb(self):
self.infunc.side_effect = ["try: ham\nexcept: eggs\n",
EOFError('Finished')]
@@ -152,6 +302,28 @@ def test_context_tb(self):
NameError: name 'eggs' is not defined
""")
self.assertIn(expected, output)
+ self.assertIs(self.sysmod.last_type, NameError)
+ self.assertIs(type(self.sysmod.last_value), NameError)
+ self.assertIs(self.sysmod.last_traceback, self.sysmod.last_value.__traceback__)
+ self.assertIsNotNone(self.sysmod.last_traceback)
+ self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
+
+
+class TestInteractiveConsoleLocalExit(unittest.TestCase, MockSys):
+
+ def setUp(self):
+ self.console = code.InteractiveConsole(local_exit=True)
+ self.mock_sys()
+
+ @unittest.skipIf(sys.flags.no_site, "exit() isn't defined unless there's a site module")
+ def test_exit(self):
+ # default exit message
+ self.infunc.side_effect = ["exit()"]
+ self.console.interact(banner='')
+ self.assertEqual(len(self.stderr.method_calls), 2)
+ err_msg = self.stderr.method_calls[1]
+ expected = 'now exiting InteractiveConsole...\n'
+ self.assertEqual(err_msg, ['write', (expected,), {}])
if __name__ == "__main__":