Skip to content

Commit d0ca4a8

Browse files
committed
Use new REPL for the code module
1 parent e11315d commit d0ca4a8

3 files changed

Lines changed: 101 additions & 26 deletions

File tree

Lib/code.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77

88
import builtins
9+
import os
910
import sys
1011
import traceback
1112
from codeop import CommandCompiler, compile_command
@@ -201,7 +202,7 @@ def resetbuffer(self):
201202
"""Reset the input buffer."""
202203
self.buffer = []
203204

204-
def interact(self, banner=None, exitmsg=None):
205+
def interact(self, banner=None, exitmsg=None, *, use_pyrepl=None):
205206
"""Closely emulate the interactive Python console.
206207
207208
The optional banner argument specifies the banner to print
@@ -216,7 +217,29 @@ def interact(self, banner=None, exitmsg=None):
216217
printing an exit message. If exitmsg is not given or None,
217218
a default message is printed.
218219
220+
The use_pyrepl argument controls whether to use the pyrepl-based REPL
221+
when available. When True, pyrepl is used. When False, the basic
222+
readline-based REPL is used. When None (the default), pyrepl is used
223+
automatically if available and the PYTHON_BASIC_REPL environment
224+
variable is not set.
225+
219226
"""
227+
if use_pyrepl is None:
228+
use_pyrepl = not os.getenv('PYTHON_BASIC_REPL')
229+
230+
if use_pyrepl:
231+
try:
232+
from _pyrepl.main import CAN_USE_PYREPL
233+
if CAN_USE_PYREPL:
234+
from _pyrepl.simple_interact import (
235+
run_multiline_interactive_console,
236+
)
237+
run_multiline_interactive_console(self)
238+
return
239+
except ImportError:
240+
pass
241+
# Fall through to basic REPL if pyrepl is unavailable
242+
220243
try:
221244
sys.ps1
222245
delete_ps1_after = False
@@ -355,7 +378,7 @@ def __call__(self, code=None):
355378
raise SystemExit(code)
356379

357380

358-
def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False):
381+
def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=False, *, use_pyrepl=None):
359382
"""Closely emulate the interactive Python interpreter.
360383
361384
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
369392
local -- passed to InteractiveInterpreter.__init__()
370393
exitmsg -- passed to InteractiveConsole.interact()
371394
local_exit -- passed to InteractiveConsole.__init__()
395+
use_pyrepl -- passed to InteractiveConsole.interact()
372396
373397
"""
374398
console = InteractiveConsole(local, local_exit=local_exit)
@@ -379,7 +403,7 @@ def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=Fa
379403
import readline # noqa: F401
380404
except ImportError:
381405
pass
382-
console.interact(banner, exitmsg)
406+
console.interact(banner, exitmsg, use_pyrepl=use_pyrepl)
383407

384408

385409
if __name__ == "__main__":

Lib/sqlite3/__main__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ def main(*args):
148148
# No SQL provided; start the REPL.
149149
with completer(con):
150150
console = SqliteInteractiveConsole(con, use_color=True)
151-
console.interact(banner, exitmsg="")
151+
# Keep using basic REPL until completion and syntax
152+
# highlighting are adapted for PyREPL.
153+
console.interact(banner, exitmsg="", use_pyrepl=False)
152154
finally:
153155
con.close()
154156

Lib/test/test_code_module.py

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"Test InteractiveConsole and InteractiveInterpreter from code module"
2+
import os
23
import sys
34
import traceback
45
import unittest
@@ -44,7 +45,7 @@ def test_ps1(self):
4445
"code.sys.ps1",
4546
EOFError('Finished')
4647
]
47-
self.console.interact()
48+
self.console.interact(use_pyrepl=False)
4849
output = ''.join(''.join(call[1]) for call in self.stdout.method_calls)
4950
self.assertIn('>>> ', output)
5051
self.assertNotHasAttr(self.sysmod, 'ps1')
@@ -55,7 +56,7 @@ def test_ps1(self):
5556
EOFError('Finished')
5657
]
5758
self.sysmod.ps1 = 'custom1> '
58-
self.console.interact()
59+
self.console.interact(use_pyrepl=False)
5960
output = ''.join(''.join(call[1]) for call in self.stdout.method_calls)
6061
self.assertIn('custom1> ', output)
6162
self.assertEqual(self.sysmod.ps1, 'custom1> ')
@@ -66,7 +67,7 @@ def test_ps2(self):
6667
"code.sys.ps2",
6768
EOFError('Finished')
6869
]
69-
self.console.interact()
70+
self.console.interact(use_pyrepl=False)
7071
output = ''.join(''.join(call[1]) for call in self.stdout.method_calls)
7172
self.assertIn('... ', output)
7273
self.assertNotHasAttr(self.sysmod, 'ps2')
@@ -77,14 +78,14 @@ def test_ps2(self):
7778
EOFError('Finished')
7879
]
7980
self.sysmod.ps2 = 'custom2> '
80-
self.console.interact()
81+
self.console.interact(use_pyrepl=False)
8182
output = ''.join(''.join(call[1]) for call in self.stdout.method_calls)
8283
self.assertIn('custom2> ', output)
8384
self.assertEqual(self.sysmod.ps2, 'custom2> ')
8485

8586
def test_console_stderr(self):
8687
self.infunc.side_effect = ["'antioch'", "", EOFError('Finished')]
87-
self.console.interact()
88+
self.console.interact(use_pyrepl=False)
8889
for call in list(self.stdout.method_calls):
8990
if 'antioch' in ''.join(call[1]):
9091
break
@@ -96,7 +97,7 @@ def test_syntax_error(self):
9697
" x = ?",
9798
"",
9899
EOFError('Finished')]
99-
self.console.interact()
100+
self.console.interact(use_pyrepl=False)
100101
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
101102
output = output[output.index('(InteractiveConsole)'):]
102103
output = output[:output.index('\nnow exiting')]
@@ -113,7 +114,7 @@ def test_syntax_error(self):
113114

114115
def test_indentation_error(self):
115116
self.infunc.side_effect = [" 1", EOFError('Finished')]
116-
self.console.interact()
117+
self.console.interact(use_pyrepl=False)
117118
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
118119
output = output[output.index('(InteractiveConsole)'):]
119120
output = output[:output.index('\nnow exiting')]
@@ -129,7 +130,7 @@ def test_indentation_error(self):
129130

130131
def test_unicode_error(self):
131132
self.infunc.side_effect = ["'\ud800'", EOFError('Finished')]
132-
self.console.interact()
133+
self.console.interact(use_pyrepl=False)
133134
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
134135
output = output[output.index('(InteractiveConsole)'):]
135136
output = output[output.index('\n') + 1:]
@@ -148,7 +149,7 @@ def test_sysexcepthook(self):
148149
EOFError('Finished')]
149150
hook = mock.Mock()
150151
self.sysmod.excepthook = hook
151-
self.console.interact()
152+
self.console.interact(use_pyrepl=False)
152153
hook.assert_called()
153154
hook.assert_called_with(self.sysmod.last_type,
154155
self.sysmod.last_value,
@@ -170,7 +171,7 @@ def test_sysexcepthook_syntax_error(self):
170171
EOFError('Finished')]
171172
hook = mock.Mock()
172173
self.sysmod.excepthook = hook
173-
self.console.interact()
174+
self.console.interact(use_pyrepl=False)
174175
hook.assert_called()
175176
hook.assert_called_with(self.sysmod.last_type,
176177
self.sysmod.last_value,
@@ -190,7 +191,7 @@ def test_sysexcepthook_indentation_error(self):
190191
self.infunc.side_effect = [" 1", EOFError('Finished')]
191192
hook = mock.Mock()
192193
self.sysmod.excepthook = hook
193-
self.console.interact()
194+
self.console.interact(use_pyrepl=False)
194195
hook.assert_called()
195196
hook.assert_called_with(self.sysmod.last_type,
196197
self.sysmod.last_value,
@@ -208,7 +209,7 @@ def test_sysexcepthook_indentation_error(self):
208209
def test_sysexcepthook_crashing_doesnt_close_repl(self):
209210
self.infunc.side_effect = ["1/0", "a = 123", "print(a)", EOFError('Finished')]
210211
self.sysmod.excepthook = 1
211-
self.console.interact()
212+
self.console.interact(use_pyrepl=False)
212213
self.assertEqual(['write', ('123', ), {}], self.stdout.method_calls[0])
213214
error = "".join(call.args[0] for call in self.stderr.method_calls if call[0] == 'write')
214215
self.assertIn("Error in sys.excepthook:", error)
@@ -222,7 +223,7 @@ def test_sysexcepthook_raising_BaseException(self):
222223
def raise_base(*args, **kwargs):
223224
raise BaseException(s)
224225
self.sysmod.excepthook = raise_base
225-
self.console.interact()
226+
self.console.interact(use_pyrepl=False)
226227
self.assertEqual(['write', ('123', ), {}], self.stdout.method_calls[0])
227228
error = "".join(call.args[0] for call in self.stderr.method_calls if call[0] == 'write')
228229
self.assertIn("Error in sys.excepthook:", error)
@@ -236,26 +237,26 @@ def raise_base(*args, **kwargs):
236237
raise SystemExit
237238
self.sysmod.excepthook = raise_base
238239
with self.assertRaises(SystemExit):
239-
self.console.interact()
240+
self.console.interact(use_pyrepl=False)
240241

241242
def test_banner(self):
242243
# with banner
243244
self.infunc.side_effect = EOFError('Finished')
244-
self.console.interact(banner='Foo')
245+
self.console.interact(banner='Foo', use_pyrepl=False)
245246
self.assertEqual(len(self.stderr.method_calls), 3)
246247
banner_call = self.stderr.method_calls[0]
247248
self.assertEqual(banner_call, ['write', ('Foo\n',), {}])
248249

249250
# no banner
250251
self.stderr.reset_mock()
251252
self.infunc.side_effect = EOFError('Finished')
252-
self.console.interact(banner='')
253+
self.console.interact(banner='', use_pyrepl=False)
253254
self.assertEqual(len(self.stderr.method_calls), 2)
254255

255256
def test_exit_msg(self):
256257
# default exit message
257258
self.infunc.side_effect = EOFError('Finished')
258-
self.console.interact(banner='')
259+
self.console.interact(banner='', use_pyrepl=False)
259260
self.assertEqual(len(self.stderr.method_calls), 2)
260261
err_msg = self.stderr.method_calls[1]
261262
expected = 'now exiting InteractiveConsole...\n'
@@ -264,7 +265,7 @@ def test_exit_msg(self):
264265
# no exit message
265266
self.stderr.reset_mock()
266267
self.infunc.side_effect = EOFError('Finished')
267-
self.console.interact(banner='', exitmsg='')
268+
self.console.interact(banner='', exitmsg='', use_pyrepl=False)
268269
self.assertEqual(len(self.stderr.method_calls), 1)
269270

270271
# custom exit message
@@ -273,7 +274,7 @@ def test_exit_msg(self):
273274
'bye! \N{GREEK SMALL LETTER ZETA}\N{CYRILLIC SMALL LETTER ZHE}'
274275
)
275276
self.infunc.side_effect = EOFError('Finished')
276-
self.console.interact(banner='', exitmsg=message)
277+
self.console.interact(banner='', exitmsg=message, use_pyrepl=False)
277278
self.assertEqual(len(self.stderr.method_calls), 2)
278279
err_msg = self.stderr.method_calls[1]
279280
expected = message + '\n'
@@ -283,7 +284,7 @@ def test_exit_msg(self):
283284
def test_cause_tb(self):
284285
self.infunc.side_effect = ["raise ValueError('') from AttributeError",
285286
EOFError('Finished')]
286-
self.console.interact()
287+
self.console.interact(use_pyrepl=False)
287288
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
288289
expected = dedent("""
289290
AttributeError
@@ -304,7 +305,7 @@ def test_cause_tb(self):
304305
def test_context_tb(self):
305306
self.infunc.side_effect = ["try: ham\nexcept: eggs\n",
306307
EOFError('Finished')]
307-
self.console.interact()
308+
self.console.interact(use_pyrepl=False)
308309
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
309310
expected = dedent("""
310311
Traceback (most recent call last):
@@ -335,12 +336,60 @@ def setUp(self):
335336
def test_exit(self):
336337
# default exit message
337338
self.infunc.side_effect = ["exit()"]
338-
self.console.interact(banner='')
339+
self.console.interact(banner='', use_pyrepl=False)
339340
self.assertEqual(len(self.stderr.method_calls), 2)
340341
err_msg = self.stderr.method_calls[1]
341342
expected = 'now exiting InteractiveConsole...\n'
342343
self.assertEqual(err_msg, ['write', (expected,), {}])
343344

344345

346+
class TestInteractiveConsoleUsePyrepl(unittest.TestCase, MockSys):
347+
"""Tests for the use_pyrepl parameter of InteractiveConsole.interact()."""
348+
349+
def setUp(self):
350+
self.console = code.InteractiveConsole()
351+
self.mock_sys()
352+
353+
def test_use_pyrepl_false_uses_basic_repl(self):
354+
"""When use_pyrepl=False, the basic REPL should be used."""
355+
self.infunc.side_effect = ["'test'", EOFError('Finished')]
356+
self.console.interact(banner='', use_pyrepl=False)
357+
# Should have used code.input (the basic REPL)
358+
self.infunc.assert_called()
359+
360+
@mock.patch.object(os, 'getenv', return_value='1')
361+
def test_python_basic_repl_env_uses_basic_repl(self, mock_getenv):
362+
"""When PYTHON_BASIC_REPL is set, the basic REPL should be used."""
363+
self.infunc.side_effect = ["'test'", EOFError('Finished')]
364+
self.console.interact(banner='')
365+
# Should have used code.input (the basic REPL)
366+
self.infunc.assert_called()
367+
368+
def test_use_pyrepl_false_with_input(self):
369+
"""Test that use_pyrepl=False correctly processes input."""
370+
self.infunc.side_effect = [
371+
"x = 1",
372+
"x",
373+
EOFError('Finished')
374+
]
375+
self.console.interact(banner='', use_pyrepl=False)
376+
output = ''.join(''.join(call[1]) for call in self.stdout.method_calls)
377+
self.assertIn('1', output)
378+
379+
380+
class TestInteractFunctionUsePyrepl(unittest.TestCase, MockSys):
381+
"""Tests for the use_pyrepl parameter of the top-level interact() function."""
382+
383+
def setUp(self):
384+
self.mock_sys()
385+
386+
def test_interact_use_pyrepl_false(self):
387+
"""When use_pyrepl=False, the basic REPL should be used."""
388+
self.infunc.side_effect = ["'test'", EOFError('Finished')]
389+
with mock.patch('code.input', create=True, side_effect=self.infunc):
390+
code.interact(banner='', use_pyrepl=False)
391+
self.infunc.assert_called()
392+
393+
345394
if __name__ == "__main__":
346395
unittest.main()

0 commit comments

Comments
 (0)