Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
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 @@ -31,6 +31,7 @@

from . import commands, console, input
from .utils import ANSI_ESCAPE_SEQUENCE, wlen, str_width
from .utils import DEFAULT_PS1, DEFAULT_PS2, DEFAULT_PS3, DEFAULT_PS4
from .trace import trace


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

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

If prompt raise BaseException, MemoryError and 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 @@ -5,6 +5,12 @@
ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]")


DEFAULT_PS1 = ">>> "
DEFAULT_PS2 = "... "
DEFAULT_PS3 = "... "
DEFAULT_PS4 = "... "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these match?

ps1: str = "->> "
ps2: str = "/>> "
ps3: str = "|.. "
ps4: str = R"\__ "

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I believe this is should be like:

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

I had misread this

* ps1, ps2, ps3, ps4:
prompts. ps1 is the prompt for a one-line input; for a
multiline input it looks like:
ps2> first line of input goes here
ps3> second and further
ps3> lines get ps3
...
ps4> and the last one gets ps4
As with the usual top-level, you can set these to instances if



@functools.cache
def str_width(c: str) -> int:
if ord(c) < 128:
Expand Down
108 changes: 108 additions & 0 deletions Lib/test/test_pyrepl/test_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .support import handle_all_events, handle_events_narrow_console, code_to_events, 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


class TestReader(TestCase):
Expand Down Expand Up @@ -279,6 +280,113 @@ 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

def prepare_reader_keep_prompts(*args, **kwargs):
reader = prepare_reader(*args, **kwargs)
del reader.get_prompt
reader.ps1 = Prompt()
reader.ps2 = "... "
reader.ps3 = "... "
reader.ps4 = ""
reader.can_colorize = False
reader.paste_mode = False
return reader

events = code_to_events("a=1")
reader, _ = handle_events_narrow_console(
events,
prepare_reader=prepare_reader_keep_prompts,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use a slightly simpler setup code:

Suggested change
def prepare_reader_keep_prompts(*args, **kwargs):
reader = prepare_reader(*args, **kwargs)
del reader.get_prompt
reader.ps1 = Prompt()
reader.ps2 = "... "
reader.ps3 = "... "
reader.ps4 = ""
reader.can_colorize = False
reader.paste_mode = False
return reader
events = code_to_events("a=1")
reader, _ = handle_events_narrow_console(
events,
prepare_reader=prepare_reader_keep_prompts,
)
def prepare_reader(events):
console = FakeConsole(events)
config = ReadlineConfig(readline_completer=None)
reader = ReadlineAlikeReader(console=console, config=config)
reader.can_colorize = False
return reader
events = code_to_events("if some_condition:\nsome_function()")
reader = prepare_reader(events)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this doesn't simplify code. I have added more simpler one on my sight. Note, we should use handle_all_events (or handle_events_narrow_console) to process whole input string and get it in the reader.buffer to check if '\n' contains or not in buffer.


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

def prepare_reader_keep_prompts(*args, **kwargs):
reader = prepare_reader(*args, **kwargs)
del reader.get_prompt
reader.ps1 = Prompt()
reader.ps2 = Prompt()
reader.ps3 = Prompt()
reader.ps4 = Prompt()
reader.can_colorize = False
reader.paste_mode = False
return reader

events = code_to_events("if some_condition:\nsome_function()\nsome_function()")
reader, _ = handle_events_narrow_console(
events,
prepare_reader=prepare_reader_keep_prompts,
)

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

def prepare_reader_keep_prompts(*args, **kwargs):
reader = prepare_reader(*args, **kwargs)
del reader.get_prompt
reader.can_colorize = False
reader.paste_mode = False
return reader

events = code_to_events("if some_condition:\nsome_function()")
reader, _ = handle_events_narrow_console(
events,
prepare_reader=prepare_reader_keep_prompts,
)

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)

def prepare_reader_keep_prompts(*args, **kwargs):
reader = prepare_reader(*args, **kwargs)
del reader.get_prompt
reader.ps1 = Prompt()
reader.can_colorize = False
reader.paste_mode = False
return reader

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

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 exception.
Loading