Skip to content

Commit 7e633d7

Browse files
gh-130698: Add safe methods to get prompts for new REPL
1 parent 8b1edae commit 7e633d7

File tree

3 files changed

+128
-5
lines changed

3 files changed

+128
-5
lines changed

Lib/_pyrepl/reader.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
from . import commands, console, input
3333
from .utils import ANSI_ESCAPE_SEQUENCE, wlen, str_width
34+
from .utils import DEFAULT_PS1, DEFAULT_PS2, DEFAULT_PS3, DEFAULT_PS4
3435
from .trace import trace
3536

3637

@@ -531,22 +532,37 @@ def get_arg(self, default: int = 1) -> int:
531532
return default
532533
return self.arg
533534

535+
@staticmethod
536+
def __get_prompt_str(prompt, default_prompt) -> str:
537+
"""
538+
Convert prompt object to string.
539+
540+
If prompt raise BaseException, MemoryError and SystemError then stop
541+
the REPL. For other exceptions return default_prompt.
542+
"""
543+
try:
544+
return str(prompt)
545+
except (MemoryError, SystemError):
546+
raise
547+
except Exception:
548+
return default_prompt
549+
534550
def get_prompt(self, lineno: int, cursor_on_line: bool) -> str:
535551
"""Return what should be in the left-hand margin for line
536552
'lineno'."""
537553
if self.arg is not None and cursor_on_line:
538-
prompt = f"(arg: {self.arg}) "
554+
prompt = self.__get_prompt_str(f"(arg: {self.arg}) ", DEFAULT_PS1)
539555
elif self.paste_mode and not self.in_bracketed_paste:
540556
prompt = "(paste) "
541557
elif "\n" in self.buffer:
542558
if lineno == 0:
543-
prompt = self.ps2
559+
prompt = self.__get_prompt_str(self.ps2, DEFAULT_PS2)
544560
elif self.ps4 and lineno == self.buffer.count("\n"):
545-
prompt = self.ps4
561+
prompt = self.__get_prompt_str(self.ps4, DEFAULT_PS4)
546562
else:
547-
prompt = self.ps3
563+
prompt = self.__get_prompt_str(self.ps3, DEFAULT_PS3)
548564
else:
549-
prompt = self.ps1
565+
prompt = self.__get_prompt_str(self.ps1, DEFAULT_PS1)
550566

551567
if self.can_colorize:
552568
prompt = f"{ANSIColors.BOLD_MAGENTA}{prompt}{ANSIColors.RESET}"

Lib/_pyrepl/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]")
66

77

8+
DEFAULT_PS1 = ">>> "
9+
DEFAULT_PS2 = "... "
10+
DEFAULT_PS3 = "... "
11+
DEFAULT_PS4 = "... "
12+
13+
814
@functools.cache
915
def str_width(c: str) -> int:
1016
if ord(c) < 128:

Lib/test/test_pyrepl/test_reader.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .support import handle_all_events, handle_events_narrow_console, code_to_events, prepare_reader, prepare_console
88
from _pyrepl.console import Event
99
from _pyrepl.reader import Reader
10+
from _pyrepl.utils import DEFAULT_PS1, DEFAULT_PS2, DEFAULT_PS3, DEFAULT_PS4
1011

1112

1213
class TestReader(TestCase):
@@ -279,6 +280,106 @@ def test_prompt_length(self):
279280
self.assertEqual(prompt, "\033[0;32m樂>\033[0m> ")
280281
self.assertEqual(l, 5)
281282

283+
def test_prompt_ps1_raise_exception(self):
284+
# Handles simple ASCII prompt
285+
class Prompt:
286+
def __str__(self): 1/0
287+
288+
def prepare_reader_keep_prompts(*args, **kwargs):
289+
reader = prepare_reader(*args, **kwargs)
290+
del reader.get_prompt
291+
reader.ps1 = Prompt()
292+
reader.ps2 = "+++ "
293+
reader.ps3 = "... "
294+
reader.ps4 = ""
295+
reader.can_colorize = False
296+
reader.paste_mode = False
297+
return reader
298+
299+
events = code_to_events("a=1")
300+
reader, _ = handle_events_narrow_console(
301+
events,
302+
prepare_reader=prepare_reader_keep_prompts,
303+
)
304+
305+
prompt = reader.get_prompt(0, False)
306+
self.assertEqual(prompt, DEFAULT_PS1)
307+
308+
def test_prompt_ps2_raise_exception(self):
309+
# Handles simple ASCII prompt
310+
class Prompt:
311+
def __str__(self): 1/0
312+
313+
def prepare_reader_keep_prompts(*args, **kwargs):
314+
reader = prepare_reader(*args, **kwargs)
315+
del reader.get_prompt
316+
reader.ps1 = "+++ "
317+
reader.ps2 = Prompt()
318+
reader.ps3 = "--- "
319+
reader.ps4 = "~~~ "
320+
reader.can_colorize = False
321+
reader.paste_mode = False
322+
return reader
323+
324+
events = code_to_events("if some_condition:\nsome_function()")
325+
reader, _ = handle_events_narrow_console(
326+
events,
327+
prepare_reader=prepare_reader_keep_prompts,
328+
)
329+
330+
prompt = reader.get_prompt(0, False)
331+
self.assertEqual(prompt, DEFAULT_PS2)
332+
333+
def test_prompt_ps3_raise_exception(self):
334+
# Handles simple ASCII prompt
335+
class Prompt:
336+
def __str__(self): 1/0
337+
338+
def prepare_reader_keep_prompts(*args, **kwargs):
339+
reader = prepare_reader(*args, **kwargs)
340+
del reader.get_prompt
341+
reader.ps1 = "+++ "
342+
reader.ps2 = "--- "
343+
reader.ps3 = Prompt()
344+
reader.ps4 = ""
345+
reader.can_colorize = False
346+
reader.paste_mode = False
347+
return reader
348+
349+
events = code_to_events("if some_condition:\nsome_function()")
350+
reader, _ = handle_events_narrow_console(
351+
events,
352+
prepare_reader=prepare_reader_keep_prompts,
353+
)
354+
355+
prompt = reader.get_prompt(1, False)
356+
self.assertEqual(prompt, DEFAULT_PS3)
357+
358+
def test_prompt_ps4_raise_exception(self):
359+
# Handles simple ASCII prompt
360+
class Prompt:
361+
def __str__(self): 1/0
362+
363+
def prepare_reader_keep_prompts(*args, **kwargs):
364+
reader = prepare_reader(*args, **kwargs)
365+
del reader.get_prompt
366+
reader.ps1 = "+++ "
367+
reader.ps2 = "--- "
368+
reader.ps3 = "~~~ "
369+
reader.ps4 = Prompt()
370+
reader.can_colorize = False
371+
reader.paste_mode = False
372+
return reader
373+
374+
events = code_to_events("if some_condition:\nsome_function()")
375+
reader, _ = handle_events_narrow_console(
376+
events,
377+
prepare_reader=prepare_reader_keep_prompts,
378+
)
379+
380+
prompt = reader.get_prompt(1, False)
381+
self.assertEqual(prompt, DEFAULT_PS4)
382+
282383
def test_completions_updated_on_key_press(self):
283384
namespace = {"itertools": itertools}
284385
code = "itertools."

0 commit comments

Comments
 (0)