Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7e633d7
gh-130698: Add safe methods to get prompts for new REPL
sergey-miryanov Mar 11, 2025
707bd38
Fix missing types
sergey-miryanov Mar 11, 2025
ab84ee6
Fix handling of arg prompt and add tests for it
sergey-miryanov Mar 12, 2025
5d91db7
Make mypy happy again
sergey-miryanov Mar 12, 2025
e0b474b
Add news entry
sergey-miryanov Mar 12, 2025
02e7e84
Update Misc/NEWS.d/next/Library/2025-03-13-00-39-54.gh-issue-130698.o…
sergey-miryanov Apr 19, 2025
c68a695
Merge branch 'main' into gh-130698-pyrepl-ps1-exception
sergey-miryanov Apr 19, 2025
5293313
Merge branch 'gh-130698-pyrepl-ps1-exception' of github.com:sergey-mi…
sergey-miryanov Apr 19, 2025
4fc6853
Simplify tests for reader with buggy prompt
sergey-miryanov Apr 19, 2025
ba8633b
Fix DEFAULT_PS2 string
sergey-miryanov Apr 19, 2025
cac161d
Make __get_prompt_str less stricter
sergey-miryanov Apr 19, 2025
60c189f
Update docstring for __get_prompt_str
sergey-miryanov Apr 20, 2025
fb66f37
Merge branch 'main' into gh-130698-pyrepl-ps1-exception
sergey-miryanov Jun 13, 2025
abd1737
Merge branch 'gh-130698-pyrepl-ps1-exception' of github.com:sergey-mi…
sergey-miryanov Jun 13, 2025
9baac77
Merge branch 'main' into gh-130698-pyrepl-ps1-exception
efimov-mikhail Nov 1, 2025
e99d76a
Merge branch 'main' into gh-130698-pyrepl-ps1-exception
sergey-miryanov Nov 2, 2025
575007b
Use DEFAULT_PS* in other places and add MULTILINE_PS*
sergey-miryanov Nov 2, 2025
a1ac233
Fix import
sergey-miryanov Nov 3, 2025
aac7470
Add comment for MULTILINE_PS*
sergey-miryanov Nov 3, 2025
8b23989
Update Lib/_pyrepl/reader.py
sergey-miryanov Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions Lib/_pyrepl/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@


from . import commands, console, input
from .utils import DEFAULT_PS1, DEFAULT_PS2, DEFAULT_PS3, DEFAULT_PS4
from .utils import wlen, unbracket, disp_str
from .trace import trace

Expand Down Expand Up @@ -474,22 +475,40 @@ def get_arg(self, default: int = 1) -> int:
return default
return self.arg

@staticmethod
def __get_prompt_str(prompt: object, default_prompt: str) -> str:
"""
Convert prompt object to string.

If str(prompt) raises BaseException, MemoryError or SystemError then stop
the REPL. For other exceptions return default_prompt.
"""
try:
return str(prompt)
except (MemoryError, SystemError):
raise
except Exception:
return default_prompt

def get_prompt(self, lineno: int, cursor_on_line: bool) -> str:
"""Return what should be in the left-hand margin for line
'lineno'."""
if self.arg is not None and cursor_on_line:
prompt = f"(arg: {self.arg}) "
prompt = DEFAULT_PS1
arg = self.__get_prompt_str(self.arg, "")
if arg:
prompt = f"(arg: {self.arg}) "
elif self.paste_mode and not self.in_bracketed_paste:
prompt = "(paste) "
elif "\n" in self.buffer:
if lineno == 0:
prompt = self.ps2
prompt = self.__get_prompt_str(self.ps2, DEFAULT_PS2)
elif self.ps4 and lineno == self.buffer.count("\n"):
prompt = self.ps4
prompt = self.__get_prompt_str(self.ps4, DEFAULT_PS4)
else:
prompt = self.ps3
prompt = self.__get_prompt_str(self.ps3, DEFAULT_PS3)
else:
prompt = self.ps1
prompt = self.__get_prompt_str(self.ps1, DEFAULT_PS1)

if self.can_colorize:
prompt = f"{ANSIColors.BOLD_MAGENTA}{prompt}{ANSIColors.RESET}"
Expand Down
6 changes: 6 additions & 0 deletions Lib/_pyrepl/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""})


DEFAULT_PS1 = ">>> "
DEFAULT_PS2 = ">>> "
DEFAULT_PS3 = "... "
DEFAULT_PS4 = "... "


@functools.cache
def str_width(c: str) -> int:
if ord(c) < 128:
Expand Down
101 changes: 101 additions & 0 deletions Lib/test/test_pyrepl/test_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@
from .support import prepare_reader, prepare_console
from _pyrepl.console import Event
from _pyrepl.reader import Reader
from _pyrepl.utils import DEFAULT_PS1, DEFAULT_PS2, DEFAULT_PS3, DEFAULT_PS4


def prepare_reader_with_prompt(
console, ps1=DEFAULT_PS1, ps2=DEFAULT_PS2, ps3=DEFAULT_PS3, ps4=DEFAULT_PS4):
reader = prepare_reader(
console,
can_colorize=False,
paste_mode=False,
ps1=ps1,
ps2=ps2,
ps3=ps3,
ps4=ps4
)

# we should use original get_prompt from reader to get exceptions
del reader.get_prompt
return reader


class TestReader(ScreenEqualMixin, TestCase):
Expand Down Expand Up @@ -298,6 +316,89 @@ def test_prompt_length(self):
self.assertEqual(prompt, "\033[0;32m樂>\033[0m> ")
self.assertEqual(l, 5)

def test_prompt_ps1_raise_exception(self):
# Handles exceptions from ps1 prompt
class Prompt:
def __str__(self): 1/0

_prepare_reader = functools.partial(
prepare_reader_with_prompt,
ps1=Prompt(),
)

reader, _ = handle_all_events(
events=code_to_events("a=1"),
prepare_reader=_prepare_reader
)

prompt = reader.get_prompt(0, False)
self.assertEqual(prompt, DEFAULT_PS1)

def test_prompt_ps2_ps3_ps4_raise_exception(self):
# Handles exceptions from ps2, ps3 and ps4 prompts
class Prompt:
def __str__(self): 1/0

_prepare_reader = functools.partial(
prepare_reader_with_prompt,
ps1=Prompt(),
ps2=Prompt(),
ps3=Prompt(),
ps4=Prompt(),
)

reader, _ = handle_all_events(
events=code_to_events("if cond:\nfunc()\nfunc()"),
prepare_reader=_prepare_reader
)

prompt = reader.get_prompt(0, False)
self.assertEqual(prompt, DEFAULT_PS2)

prompt = reader.get_prompt(1, False)
self.assertEqual(prompt, DEFAULT_PS3)

prompt = reader.get_prompt(2, False)
self.assertEqual(prompt, DEFAULT_PS4)

def test_prompt_arg_raise_exception(self):
# Handles exceptions from arg prompt
class Prompt:
def __str__(self): 1/0
def __rmul__(self, b): return b

reader, _ = handle_all_events(
events=code_to_events("if some_condition:\nsome_function()"),
prepare_reader=prepare_reader_with_prompt,
)

reader.arg = Prompt()
prompt = reader.get_prompt(0, True)
self.assertEqual(prompt, DEFAULT_PS1)

def test_prompt_raise_exception(self):
# Tests unrecoverable exceptions from prompts
cases = [
(MemoryError, "No memory for prompt"),
(SystemError, "System error for prompt"),
]
for cls, msg in cases:
with self.subTest(msg):

class Prompt:
def __str__(self): raise cls(msg)

_prepare_reader = functools.partial(
prepare_reader_with_prompt,
ps1=Prompt(),
)

with self.assertRaisesRegex(cls, msg):
handle_events_narrow_console(
events=code_to_events("a=1"),
prepare_reader=_prepare_reader,
)

def test_completions_updated_on_key_press(self):
namespace = {"itertools": itertools}
code = "itertools."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid exiting the new REPL when prompt object raises an exception.
Loading