From d0ca4a8f4e1206aa1ec37f6650ed985c37d2a85d Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 9 Apr 2026 00:36:49 +0800 Subject: [PATCH 1/8] Use new REPL for the code module --- Lib/code.py | 30 ++++++++++-- Lib/sqlite3/__main__.py | 4 +- Lib/test/test_code_module.py | 93 +++++++++++++++++++++++++++--------- 3 files changed, 101 insertions(+), 26 deletions(-) diff --git a/Lib/code.py b/Lib/code.py index f7e275d8801b7c..0a77cdc64f179e 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -6,6 +6,7 @@ import builtins +import os import sys import traceback from codeop import CommandCompiler, compile_command @@ -201,7 +202,7 @@ def resetbuffer(self): """Reset the input buffer.""" self.buffer = [] - def interact(self, banner=None, exitmsg=None): + def interact(self, banner=None, exitmsg=None, *, use_pyrepl=None): """Closely emulate the interactive Python console. The optional banner argument specifies the banner to print @@ -216,7 +217,29 @@ def interact(self, banner=None, exitmsg=None): printing an exit message. If exitmsg is not given or None, a default message is printed. + The use_pyrepl argument controls whether to use the pyrepl-based REPL + when available. When True, pyrepl is used. When False, the basic + readline-based REPL is used. When None (the default), pyrepl is used + automatically if available and the PYTHON_BASIC_REPL environment + variable is not set. + """ + if use_pyrepl is None: + use_pyrepl = not os.getenv('PYTHON_BASIC_REPL') + + if use_pyrepl: + try: + from _pyrepl.main import CAN_USE_PYREPL + if CAN_USE_PYREPL: + from _pyrepl.simple_interact import ( + run_multiline_interactive_console, + ) + run_multiline_interactive_console(self) + return + except ImportError: + pass + # Fall through to basic REPL if pyrepl is unavailable + try: sys.ps1 delete_ps1_after = False @@ -355,7 +378,7 @@ def __call__(self, code=None): raise SystemExit(code) -def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False): +def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False, *, use_pyrepl=None): """Closely emulate the interactive Python interpreter. This is a backwards compatible interface to the InteractiveConsole @@ -369,6 +392,7 @@ def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=Fa local -- passed to InteractiveInterpreter.__init__() exitmsg -- passed to InteractiveConsole.interact() local_exit -- passed to InteractiveConsole.__init__() + use_pyrepl -- passed to InteractiveConsole.interact() """ console = InteractiveConsole(local, local_exit=local_exit) @@ -379,7 +403,7 @@ def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=Fa import readline # noqa: F401 except ImportError: pass - console.interact(banner, exitmsg) + console.interact(banner, exitmsg, use_pyrepl=use_pyrepl) if __name__ == "__main__": diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 8805442b69e080..6fc7b354be865e 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -148,7 +148,9 @@ def main(*args): # No SQL provided; start the REPL. with completer(con): console = SqliteInteractiveConsole(con, use_color=True) - console.interact(banner, exitmsg="") + # Keep using basic REPL until completion and syntax + # highlighting are adapted for PyREPL. + console.interact(banner, exitmsg="", use_pyrepl=False) finally: con.close() diff --git a/Lib/test/test_code_module.py b/Lib/test/test_code_module.py index 3642b47c2c1f03..1d8fe59ff4d028 100644 --- a/Lib/test/test_code_module.py +++ b/Lib/test/test_code_module.py @@ -1,4 +1,5 @@ "Test InteractiveConsole and InteractiveInterpreter from code module" +import os import sys import traceback import unittest @@ -44,7 +45,7 @@ def test_ps1(self): "code.sys.ps1", EOFError('Finished') ] - self.console.interact() + self.console.interact(use_pyrepl=False) output = ''.join(''.join(call[1]) for call in self.stdout.method_calls) self.assertIn('>>> ', output) self.assertNotHasAttr(self.sysmod, 'ps1') @@ -55,7 +56,7 @@ def test_ps1(self): EOFError('Finished') ] self.sysmod.ps1 = 'custom1> ' - self.console.interact() + self.console.interact(use_pyrepl=False) output = ''.join(''.join(call[1]) for call in self.stdout.method_calls) self.assertIn('custom1> ', output) self.assertEqual(self.sysmod.ps1, 'custom1> ') @@ -66,7 +67,7 @@ def test_ps2(self): "code.sys.ps2", EOFError('Finished') ] - self.console.interact() + self.console.interact(use_pyrepl=False) output = ''.join(''.join(call[1]) for call in self.stdout.method_calls) self.assertIn('... ', output) self.assertNotHasAttr(self.sysmod, 'ps2') @@ -77,14 +78,14 @@ def test_ps2(self): EOFError('Finished') ] self.sysmod.ps2 = 'custom2> ' - self.console.interact() + self.console.interact(use_pyrepl=False) output = ''.join(''.join(call[1]) for call in self.stdout.method_calls) self.assertIn('custom2> ', output) self.assertEqual(self.sysmod.ps2, 'custom2> ') def test_console_stderr(self): self.infunc.side_effect = ["'antioch'", "", EOFError('Finished')] - self.console.interact() + self.console.interact(use_pyrepl=False) for call in list(self.stdout.method_calls): if 'antioch' in ''.join(call[1]): break @@ -96,7 +97,7 @@ def test_syntax_error(self): " x = ?", "", EOFError('Finished')] - self.console.interact() + self.console.interact(use_pyrepl=False) output = ''.join(''.join(call[1]) for call in self.stderr.method_calls) output = output[output.index('(InteractiveConsole)'):] output = output[:output.index('\nnow exiting')] @@ -113,7 +114,7 @@ def test_syntax_error(self): def test_indentation_error(self): self.infunc.side_effect = [" 1", EOFError('Finished')] - self.console.interact() + self.console.interact(use_pyrepl=False) output = ''.join(''.join(call[1]) for call in self.stderr.method_calls) output = output[output.index('(InteractiveConsole)'):] output = output[:output.index('\nnow exiting')] @@ -129,7 +130,7 @@ def test_indentation_error(self): def test_unicode_error(self): self.infunc.side_effect = ["'\ud800'", EOFError('Finished')] - self.console.interact() + self.console.interact(use_pyrepl=False) output = ''.join(''.join(call[1]) for call in self.stderr.method_calls) output = output[output.index('(InteractiveConsole)'):] output = output[output.index('\n') + 1:] @@ -148,7 +149,7 @@ def test_sysexcepthook(self): EOFError('Finished')] hook = mock.Mock() self.sysmod.excepthook = hook - self.console.interact() + self.console.interact(use_pyrepl=False) hook.assert_called() hook.assert_called_with(self.sysmod.last_type, self.sysmod.last_value, @@ -170,7 +171,7 @@ def test_sysexcepthook_syntax_error(self): EOFError('Finished')] hook = mock.Mock() self.sysmod.excepthook = hook - self.console.interact() + self.console.interact(use_pyrepl=False) hook.assert_called() hook.assert_called_with(self.sysmod.last_type, self.sysmod.last_value, @@ -190,7 +191,7 @@ def test_sysexcepthook_indentation_error(self): self.infunc.side_effect = [" 1", EOFError('Finished')] hook = mock.Mock() self.sysmod.excepthook = hook - self.console.interact() + self.console.interact(use_pyrepl=False) hook.assert_called() hook.assert_called_with(self.sysmod.last_type, self.sysmod.last_value, @@ -208,7 +209,7 @@ def test_sysexcepthook_indentation_error(self): 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.console.interact(use_pyrepl=False) 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) @@ -222,7 +223,7 @@ def test_sysexcepthook_raising_BaseException(self): def raise_base(*args, **kwargs): raise BaseException(s) self.sysmod.excepthook = raise_base - self.console.interact() + self.console.interact(use_pyrepl=False) 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) @@ -236,12 +237,12 @@ def raise_base(*args, **kwargs): raise SystemExit self.sysmod.excepthook = raise_base with self.assertRaises(SystemExit): - self.console.interact() + self.console.interact(use_pyrepl=False) def test_banner(self): # with banner self.infunc.side_effect = EOFError('Finished') - self.console.interact(banner='Foo') + self.console.interact(banner='Foo', use_pyrepl=False) self.assertEqual(len(self.stderr.method_calls), 3) banner_call = self.stderr.method_calls[0] self.assertEqual(banner_call, ['write', ('Foo\n',), {}]) @@ -249,13 +250,13 @@ def test_banner(self): # no banner self.stderr.reset_mock() self.infunc.side_effect = EOFError('Finished') - self.console.interact(banner='') + self.console.interact(banner='', use_pyrepl=False) self.assertEqual(len(self.stderr.method_calls), 2) def test_exit_msg(self): # default exit message self.infunc.side_effect = EOFError('Finished') - self.console.interact(banner='') + self.console.interact(banner='', use_pyrepl=False) self.assertEqual(len(self.stderr.method_calls), 2) err_msg = self.stderr.method_calls[1] expected = 'now exiting InteractiveConsole...\n' @@ -264,7 +265,7 @@ def test_exit_msg(self): # no exit message self.stderr.reset_mock() self.infunc.side_effect = EOFError('Finished') - self.console.interact(banner='', exitmsg='') + self.console.interact(banner='', exitmsg='', use_pyrepl=False) self.assertEqual(len(self.stderr.method_calls), 1) # custom exit message @@ -273,7 +274,7 @@ def test_exit_msg(self): 'bye! \N{GREEK SMALL LETTER ZETA}\N{CYRILLIC SMALL LETTER ZHE}' ) self.infunc.side_effect = EOFError('Finished') - self.console.interact(banner='', exitmsg=message) + self.console.interact(banner='', exitmsg=message, use_pyrepl=False) self.assertEqual(len(self.stderr.method_calls), 2) err_msg = self.stderr.method_calls[1] expected = message + '\n' @@ -283,7 +284,7 @@ def test_exit_msg(self): def test_cause_tb(self): self.infunc.side_effect = ["raise ValueError('') from AttributeError", EOFError('Finished')] - self.console.interact() + self.console.interact(use_pyrepl=False) output = ''.join(''.join(call[1]) for call in self.stderr.method_calls) expected = dedent(""" AttributeError @@ -304,7 +305,7 @@ def test_cause_tb(self): def test_context_tb(self): self.infunc.side_effect = ["try: ham\nexcept: eggs\n", EOFError('Finished')] - self.console.interact() + self.console.interact(use_pyrepl=False) output = ''.join(''.join(call[1]) for call in self.stderr.method_calls) expected = dedent(""" Traceback (most recent call last): @@ -335,12 +336,60 @@ def setUp(self): def test_exit(self): # default exit message self.infunc.side_effect = ["exit()"] - self.console.interact(banner='') + self.console.interact(banner='', use_pyrepl=False) 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,), {}]) +class TestInteractiveConsoleUsePyrepl(unittest.TestCase, MockSys): + """Tests for the use_pyrepl parameter of InteractiveConsole.interact().""" + + def setUp(self): + self.console = code.InteractiveConsole() + self.mock_sys() + + def test_use_pyrepl_false_uses_basic_repl(self): + """When use_pyrepl=False, the basic REPL should be used.""" + self.infunc.side_effect = ["'test'", EOFError('Finished')] + self.console.interact(banner='', use_pyrepl=False) + # Should have used code.input (the basic REPL) + self.infunc.assert_called() + + @mock.patch.object(os, 'getenv', return_value='1') + def test_python_basic_repl_env_uses_basic_repl(self, mock_getenv): + """When PYTHON_BASIC_REPL is set, the basic REPL should be used.""" + self.infunc.side_effect = ["'test'", EOFError('Finished')] + self.console.interact(banner='') + # Should have used code.input (the basic REPL) + self.infunc.assert_called() + + def test_use_pyrepl_false_with_input(self): + """Test that use_pyrepl=False correctly processes input.""" + self.infunc.side_effect = [ + "x = 1", + "x", + EOFError('Finished') + ] + self.console.interact(banner='', use_pyrepl=False) + output = ''.join(''.join(call[1]) for call in self.stdout.method_calls) + self.assertIn('1', output) + + +class TestInteractFunctionUsePyrepl(unittest.TestCase, MockSys): + """Tests for the use_pyrepl parameter of the top-level interact() function.""" + + def setUp(self): + self.mock_sys() + + def test_interact_use_pyrepl_false(self): + """When use_pyrepl=False, the basic REPL should be used.""" + self.infunc.side_effect = ["'test'", EOFError('Finished')] + with mock.patch('code.input', create=True, side_effect=self.infunc): + code.interact(banner='', use_pyrepl=False) + self.infunc.assert_called() + + if __name__ == "__main__": unittest.main() From 83086b2e6b6ceb75aec6f7132ba3f0c5a5a1fb83 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 9 Apr 2026 00:38:59 +0800 Subject: [PATCH 2/8] blurb --- .../next/Library/2026-04-09-00-38-52.gh-issue-119512.Z6V-kp.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-04-09-00-38-52.gh-issue-119512.Z6V-kp.rst diff --git a/Misc/NEWS.d/next/Library/2026-04-09-00-38-52.gh-issue-119512.Z6V-kp.rst b/Misc/NEWS.d/next/Library/2026-04-09-00-38-52.gh-issue-119512.Z6V-kp.rst new file mode 100644 index 00000000000000..cf6ed5919b7728 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-09-00-38-52.gh-issue-119512.Z6V-kp.rst @@ -0,0 +1 @@ +Use new REPL for the :modo:`code` module. From 355ae0adbc8a46fc06e1eb61d33fe0482f2f8499 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 9 Apr 2026 01:01:16 +0800 Subject: [PATCH 3/8] fix typo in news entry --- .../next/Library/2026-04-09-00-38-52.gh-issue-119512.Z6V-kp.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-04-09-00-38-52.gh-issue-119512.Z6V-kp.rst b/Misc/NEWS.d/next/Library/2026-04-09-00-38-52.gh-issue-119512.Z6V-kp.rst index cf6ed5919b7728..d666f710a4328c 100644 --- a/Misc/NEWS.d/next/Library/2026-04-09-00-38-52.gh-issue-119512.Z6V-kp.rst +++ b/Misc/NEWS.d/next/Library/2026-04-09-00-38-52.gh-issue-119512.Z6V-kp.rst @@ -1 +1 @@ -Use new REPL for the :modo:`code` module. +Use new REPL for the :mod:`code` module. From 970032b4a52f2e8a2f9f75381cfe21717600a470 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 9 Apr 2026 01:12:49 +0800 Subject: [PATCH 4/8] Disable PyREPL by default to avoid breaking third party applications --- Lib/code.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/code.py b/Lib/code.py index 0a77cdc64f179e..e842f212b50c1c 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -202,7 +202,7 @@ def resetbuffer(self): """Reset the input buffer.""" self.buffer = [] - def interact(self, banner=None, exitmsg=None, *, use_pyrepl=None): + def interact(self, banner=None, exitmsg=None, *, use_pyrepl=False): """Closely emulate the interactive Python console. The optional banner argument specifies the banner to print @@ -218,8 +218,8 @@ def interact(self, banner=None, exitmsg=None, *, use_pyrepl=None): a default message is printed. The use_pyrepl argument controls whether to use the pyrepl-based REPL - when available. When True, pyrepl is used. When False, the basic - readline-based REPL is used. When None (the default), pyrepl is used + when available. When True, pyrepl is used. When False (the default), + the basic readline-based REPL is used. When None, pyrepl is used automatically if available and the PYTHON_BASIC_REPL environment variable is not set. @@ -378,7 +378,7 @@ def __call__(self, code=None): raise SystemExit(code) -def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False, *, use_pyrepl=None): +def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False, *, use_pyrepl=False): """Closely emulate the interactive Python interpreter. This is a backwards compatible interface to the InteractiveConsole From a90ad0400499d798b1133cc36b5489fb501640a2 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 9 Apr 2026 01:19:17 +0800 Subject: [PATCH 5/8] Add documentation --- Doc/library/code.rst | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Doc/library/code.rst b/Doc/library/code.rst index 59c016d21501b0..1e7127ee957b63 100644 --- a/Doc/library/code.rst +++ b/Doc/library/code.rst @@ -40,7 +40,7 @@ build applications which provide an interactive interpreter prompt. .. versionchanged:: 3.13 Added *local_exit* parameter. -.. function:: interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False) +.. function:: interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False, *, use_pyrepl=False) Convenience function to run a read-eval-print loop. This creates a new instance of :class:`InteractiveConsole` and sets *readfunc* to be used as @@ -49,8 +49,9 @@ build applications which provide an interactive interpreter prompt. use as the default namespace for the interpreter loop. If *local_exit* is provided, it is passed to the :class:`InteractiveConsole` constructor. The :meth:`~InteractiveConsole.interact` 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 - after use. + banner and exit message to use, if provided. The *use_pyrepl* argument is + passed to :meth:`~InteractiveConsole.interact` to control whether to use the + pyrepl-based REPL. The console object is discarded after use. .. versionchanged:: 3.6 Added *exitmsg* parameter. @@ -58,6 +59,9 @@ build applications which provide an interactive interpreter prompt. .. versionchanged:: 3.13 Added *local_exit* parameter. + .. versionchanged:: next + Added *use_pyrepl* parameter. + .. function:: compile_command(source, filename="", symbol="single") This function is useful for programs that want to emulate Python's interpreter @@ -153,7 +157,7 @@ The :class:`InteractiveConsole` class is a subclass of interpreter objects as well as the following additions. -.. method:: InteractiveConsole.interact(banner=None, exitmsg=None) +.. method:: InteractiveConsole.interact(banner=None, exitmsg=None, *, use_pyrepl=False) Closely emulate the interactive Python console. The optional *banner* argument specify the banner to print before the first interaction; by default it prints a @@ -165,12 +169,21 @@ interpreter objects as well as the following additions. Pass the empty string to suppress the exit message. If *exitmsg* is not given or ``None``, a default message is printed. + The optional *use_pyrepl* argument controls whether to use the pyrepl-based REPL + when available. When ``True``, pyrepl is used. When ``False`` (the default), + the basic readline-based REPL is used. When ``None``, pyrepl is used + automatically if available and the :envvar:`PYTHON_BASIC_REPL` environment + variable is not set. + .. versionchanged:: 3.4 To suppress printing any banner, pass an empty string. .. versionchanged:: 3.6 Print an exit message when exiting. + .. versionchanged:: next + Added *use_pyrepl* parameter. + .. method:: InteractiveConsole.push(line) From 5e45ca6aa1f1507f4e309cf12969ed16140277d4 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 9 Apr 2026 01:20:40 +0800 Subject: [PATCH 6/8] Revert use_pyrepl=False in sqlite3 as use_pyrepl is False by default --- Lib/sqlite3/__main__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 6fc7b354be865e..8805442b69e080 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -148,9 +148,7 @@ def main(*args): # No SQL provided; start the REPL. with completer(con): console = SqliteInteractiveConsole(con, use_color=True) - # Keep using basic REPL until completion and syntax - # highlighting are adapted for PyREPL. - console.interact(banner, exitmsg="", use_pyrepl=False) + console.interact(banner, exitmsg="") finally: con.close() From d77fa60914bbd8c7cb9e04a4374b58f58ea592b5 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 9 Apr 2026 01:37:32 +0800 Subject: [PATCH 7/8] Add whatsnew --- Doc/whatsnew/3.15.rst | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 6543da4487e42f..0e36f829a0e1c9 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -722,6 +722,18 @@ calendar (Contributed by Pål Grønås Drange in :gh:`140212`.) +code +---- + +* The :meth:`InteractiveConsole.interact` method and the :func:`code.interact` + function now support a *use_pyrepl* parameter to control whether to use the + pyrepl-based REPL. By default, *use_pyrepl* is ``False``, using the basic + readline-based REPL. When set to ``True``, pyrepl is used if available. + When set to ``None``, pyrepl is used automatically if available and the + :envvar:`PYTHON_BASIC_REPL` environment variable is not set. + (Contributed by Long Tan in :gh:`148261`.) + + collections ----------- @@ -830,9 +842,9 @@ http.server is colored by default. This can be controlled with :ref:`environment variables `. - (Contributed by Hugo van Kemenade in :gh:`146292`.) - - + (Contributed by Hugo van Kemenade in :gh:`146292`.) + + inspect ------- From c9dbd8c524cfb61edf8df55c46f0033b613c7e02 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 9 Apr 2026 01:54:47 +0800 Subject: [PATCH 8/8] Doc: fix py:meth reference target not found --- Doc/whatsnew/3.15.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 0e36f829a0e1c9..757641f8106436 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -725,7 +725,7 @@ calendar code ---- -* The :meth:`InteractiveConsole.interact` method and the :func:`code.interact` +* The :meth:`code.InteractiveConsole.interact` method and the :func:`code.interact` function now support a *use_pyrepl* parameter to control whether to use the pyrepl-based REPL. By default, *use_pyrepl* is ``False``, using the basic readline-based REPL. When set to ``True``, pyrepl is used if available.