mirror of https://github.com/python/cpython.git
GH-102895 Add an option local_exit in code.interact to block exit() from terminating the whole process (GH-102896)
This commit is contained in:
parent
cb1bf89c40
commit
e6eb8cafca
|
@ -23,20 +23,25 @@ build applications which provide an interactive interpreter prompt.
|
||||||
``'__doc__'`` set to ``None``.
|
``'__doc__'`` set to ``None``.
|
||||||
|
|
||||||
|
|
||||||
.. class:: InteractiveConsole(locals=None, filename="<console>")
|
.. class:: InteractiveConsole(locals=None, filename="<console>", local_exit=False)
|
||||||
|
|
||||||
Closely emulate the behavior of the interactive Python interpreter. This class
|
Closely emulate the behavior of the interactive Python interpreter. This class
|
||||||
builds on :class:`InteractiveInterpreter` and adds prompting using the familiar
|
builds on :class:`InteractiveInterpreter` and adds prompting using the familiar
|
||||||
``sys.ps1`` and ``sys.ps2``, and input buffering.
|
``sys.ps1`` and ``sys.ps2``, and input buffering. If *local_exit* is True,
|
||||||
|
``exit()`` and ``quit()`` in the console will not raise :exc:`SystemExit`, but
|
||||||
|
instead return to the calling code.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.13
|
||||||
|
Added *local_exit* parameter.
|
||||||
|
|
||||||
.. function:: interact(banner=None, readfunc=None, local=None, exitmsg=None)
|
.. function:: interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False)
|
||||||
|
|
||||||
Convenience function to run a read-eval-print loop. This creates a new
|
Convenience function to run a read-eval-print loop. This creates a new
|
||||||
instance of :class:`InteractiveConsole` and sets *readfunc* to be used as
|
instance of :class:`InteractiveConsole` and sets *readfunc* to be used as
|
||||||
the :meth:`InteractiveConsole.raw_input` method, if provided. If *local* is
|
the :meth:`InteractiveConsole.raw_input` method, if provided. If *local* is
|
||||||
provided, it is passed to the :class:`InteractiveConsole` constructor for
|
provided, it is passed to the :class:`InteractiveConsole` constructor for
|
||||||
use as the default namespace for the interpreter loop. The :meth:`interact`
|
use as the default namespace for the interpreter loop. If *local_exit* is provided,
|
||||||
|
it is passed to the :class:`InteractiveConsole` constructor. The :meth:`interact`
|
||||||
method of the instance is then run with *banner* and *exitmsg* passed as the
|
method of the instance is then run with *banner* and *exitmsg* passed as the
|
||||||
banner and exit message to use, if provided. The console object is discarded
|
banner and exit message to use, if provided. The console object is discarded
|
||||||
after use.
|
after use.
|
||||||
|
@ -44,6 +49,8 @@ build applications which provide an interactive interpreter prompt.
|
||||||
.. versionchanged:: 3.6
|
.. versionchanged:: 3.6
|
||||||
Added *exitmsg* parameter.
|
Added *exitmsg* parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.13
|
||||||
|
Added *local_exit* parameter.
|
||||||
|
|
||||||
.. function:: compile_command(source, filename="<input>", symbol="single")
|
.. function:: compile_command(source, filename="<input>", symbol="single")
|
||||||
|
|
||||||
|
|
62
Lib/code.py
62
Lib/code.py
|
@ -5,6 +5,7 @@
|
||||||
# Inspired by similar code by Jeff Epler and Fredrik Lundh.
|
# Inspired by similar code by Jeff Epler and Fredrik Lundh.
|
||||||
|
|
||||||
|
|
||||||
|
import builtins
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from codeop import CommandCompiler, compile_command
|
from codeop import CommandCompiler, compile_command
|
||||||
|
@ -169,7 +170,7 @@ class InteractiveConsole(InteractiveInterpreter):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, locals=None, filename="<console>"):
|
def __init__(self, locals=None, filename="<console>", local_exit=False):
|
||||||
"""Constructor.
|
"""Constructor.
|
||||||
|
|
||||||
The optional locals argument will be passed to the
|
The optional locals argument will be passed to the
|
||||||
|
@ -181,6 +182,7 @@ def __init__(self, locals=None, filename="<console>"):
|
||||||
"""
|
"""
|
||||||
InteractiveInterpreter.__init__(self, locals)
|
InteractiveInterpreter.__init__(self, locals)
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
|
self.local_exit = local_exit
|
||||||
self.resetbuffer()
|
self.resetbuffer()
|
||||||
|
|
||||||
def resetbuffer(self):
|
def resetbuffer(self):
|
||||||
|
@ -219,7 +221,30 @@ def interact(self, banner=None, exitmsg=None):
|
||||||
elif banner:
|
elif banner:
|
||||||
self.write("%s\n" % str(banner))
|
self.write("%s\n" % str(banner))
|
||||||
more = 0
|
more = 0
|
||||||
while 1:
|
|
||||||
|
# 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:
|
try:
|
||||||
if more:
|
if more:
|
||||||
prompt = sys.ps2
|
prompt = sys.ps2
|
||||||
|
@ -236,6 +261,20 @@ def interact(self, banner=None, exitmsg=None):
|
||||||
self.write("\nKeyboardInterrupt\n")
|
self.write("\nKeyboardInterrupt\n")
|
||||||
self.resetbuffer()
|
self.resetbuffer()
|
||||||
more = 0
|
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:
|
if exitmsg is None:
|
||||||
self.write('now exiting %s...\n' % self.__class__.__name__)
|
self.write('now exiting %s...\n' % self.__class__.__name__)
|
||||||
elif exitmsg != '':
|
elif exitmsg != '':
|
||||||
|
@ -276,8 +315,22 @@ def raw_input(self, prompt=""):
|
||||||
return input(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 interact(banner=None, readfunc=None, local=None, exitmsg=None):
|
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, local_exit=False):
|
||||||
"""Closely emulate the interactive Python interpreter.
|
"""Closely emulate the interactive Python interpreter.
|
||||||
|
|
||||||
This is a backwards compatible interface to the InteractiveConsole
|
This is a backwards compatible interface to the InteractiveConsole
|
||||||
|
@ -290,9 +343,10 @@ def interact(banner=None, readfunc=None, local=None, exitmsg=None):
|
||||||
readfunc -- if not None, replaces InteractiveConsole.raw_input()
|
readfunc -- if not None, replaces InteractiveConsole.raw_input()
|
||||||
local -- passed to InteractiveInterpreter.__init__()
|
local -- passed to InteractiveInterpreter.__init__()
|
||||||
exitmsg -- passed to InteractiveConsole.interact()
|
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:
|
if readfunc is not None:
|
||||||
console.raw_input = readfunc
|
console.raw_input = readfunc
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1741,7 +1741,7 @@ def do_interact(self, arg):
|
||||||
contains all the (global and local) names found in the current scope.
|
contains all the (global and local) names found in the current scope.
|
||||||
"""
|
"""
|
||||||
ns = {**self.curframe.f_globals, **self.curframe_locals}
|
ns = {**self.curframe.f_globals, **self.curframe_locals}
|
||||||
code.interact("*interactive*", local=ns)
|
code.interact("*interactive*", local=ns, local_exit=True)
|
||||||
|
|
||||||
def do_alias(self, arg):
|
def do_alias(self, arg):
|
||||||
"""alias [name [command]]
|
"""alias [name [command]]
|
||||||
|
|
|
@ -10,11 +10,7 @@
|
||||||
code = import_helper.import_module('code')
|
code = import_helper.import_module('code')
|
||||||
|
|
||||||
|
|
||||||
class TestInteractiveConsole(unittest.TestCase):
|
class MockSys:
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.console = code.InteractiveConsole()
|
|
||||||
self.mock_sys()
|
|
||||||
|
|
||||||
def mock_sys(self):
|
def mock_sys(self):
|
||||||
"Mock system environment for InteractiveConsole"
|
"Mock system environment for InteractiveConsole"
|
||||||
|
@ -32,6 +28,13 @@ def mock_sys(self):
|
||||||
del self.sysmod.ps1
|
del self.sysmod.ps1
|
||||||
del self.sysmod.ps2
|
del self.sysmod.ps2
|
||||||
|
|
||||||
|
|
||||||
|
class TestInteractiveConsole(unittest.TestCase, MockSys):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.console = code.InteractiveConsole()
|
||||||
|
self.mock_sys()
|
||||||
|
|
||||||
def test_ps1(self):
|
def test_ps1(self):
|
||||||
self.infunc.side_effect = EOFError('Finished')
|
self.infunc.side_effect = EOFError('Finished')
|
||||||
self.console.interact()
|
self.console.interact()
|
||||||
|
@ -151,5 +154,21 @@ def test_context_tb(self):
|
||||||
self.assertIn(expected, output)
|
self.assertIn(expected, output)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInteractiveConsoleLocalExit(unittest.TestCase, MockSys):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.console = code.InteractiveConsole(local_exit=True)
|
||||||
|
self.mock_sys()
|
||||||
|
|
||||||
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Added a parameter ``local_exit`` for :func:`code.interact` to prevent ``exit()`` and ``quit`` from closing ``sys.stdin`` and raise ``SystemExit``.
|
Loading…
Reference in New Issue