Skip to content

Commit db30ea3

Browse files
committed
Added tests.
1 parent 11cdbda commit db30ea3

File tree

2 files changed

+65
-14
lines changed

2 files changed

+65
-14
lines changed

cmd2/cmd2.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ def __init__(
444444
if auto_suggest:
445445
self.auto_suggest = AutoSuggestFromHistory()
446446

447-
self.session = self._build_session()
447+
self.session = self._init_session()
448448

449449
# Commands to exclude from the history command
450450
self.exclude_from_history = [constants.EOF, 'history']
@@ -609,8 +609,8 @@ def __init__(
609609
# the current command being executed
610610
self.current_command: Statement | None = None
611611

612-
def _build_session(self) -> PromptSession[str]:
613-
"""Construct the primary PromptSession for the cmd2 application.
612+
def _init_session(self) -> PromptSession[str]:
613+
"""Initialize and return the core PromptSession for the application.
614614
615615
Builds an interactive session if stdin is a TTY. Otherwise, uses
616616
dummy drivers to support non-interactive streams like pipes or files.
@@ -3197,21 +3197,33 @@ def _read_raw_input(
31973197
prompt: Callable[[], ANSI | str] | ANSI | str,
31983198
session: PromptSession[str],
31993199
completer: Completer,
3200-
**prompt_kwargs: Any, # optional keyword args for session.prompt()
3200+
**prompt_kwargs: Any,
32013201
) -> str:
3202-
"""Read input from either an interactive terminal session or a redirected stream."""
3203-
# _build_session() sets session.input to a DummyInput when not in a TTY.
3202+
"""Execute the low-level input read from either a terminal or a redirected stream.
3203+
3204+
If the session is interactive (TTY), it uses `prompt_toolkit` to render a
3205+
rich UI with completion and `patch_stdout` protection. If non-interactive
3206+
(Pipe/File), it performs a direct line read from `stdin`.
3207+
3208+
:param prompt: the prompt text or a callable that returns the prompt.
3209+
:param session: the PromptSession instance to use for reading.
3210+
:param completer: the completer to use for this specific input.
3211+
:param prompt_kwargs: additional arguments passed directly to session.prompt().
3212+
:return: the stripped input string.
3213+
:raises EOFError: if the input stream is closed or the user signals EOF (e.g., Ctrl+D)
3214+
"""
3215+
# Check if the session is configured for interactive terminal use.
32043216
if not isinstance(session.input, DummyInput):
32053217
with patch_stdout():
3206-
return session.prompt(prompt, completer=completer, **prompt_kwargs) # type: ignore[arg-type]
3218+
return session.prompt(prompt, completer=completer, **prompt_kwargs)
32073219

32083220
# We're not at a terminal, so we're likely reading from a file or a pipe.
32093221
# We wait for a line of data before we print anything.
32103222
line = self.stdin.readline()
32113223

32123224
# If the stream is empty, we've reached the end of the input.
32133225
if not line:
3214-
return constants.EOF
3226+
raise EOFError
32153227

32163228
# If echo is on, we want the output to look like a session transcript.
32173229
# Print the prompt and the command before the results.
@@ -3266,6 +3278,10 @@ def read_input(
32663278
) -> str:
32673279
"""Read a line of input with optional completion and history.
32683280
3281+
:param prompt: prompt to display to user
3282+
:param history: optional Sequence of strings to use for up-arrow history. The passed in history
3283+
will not be edited. It is the caller's responsibility to add the returned input
3284+
to history if desired. Defaults to None.
32693285
:param preserve_quotes: if True, then quoted tokens will keep their quotes when processed by
32703286
ArgparseCompleter. This is helpful in cases when you're completing
32713287
flag-like tokens (e.g. -o, --option) and you don't want them to be
@@ -3278,7 +3294,8 @@ def read_input(
32783294
:param completer: completion function that provides choices for single argument
32793295
:param parser: an argument parser which supports the completion of multiple arguments
32803296
:return: the line read from stdin with all trailing new lines removed
3281-
:raises Exception: any exceptions raised by prompt()
3297+
:raises EOFError: if the input stream is closed or the user signals EOF (e.g., Ctrl+D)
3298+
:raises Exception: any other exceptions raised by prompt()
32823299
"""
32833300
completer_to_use = self._resolve_completer(
32843301
preserve_quotes=preserve_quotes,
@@ -3303,7 +3320,7 @@ def _read_command_line(self, prompt: str) -> str:
33033320
33043321
:param prompt: prompt to display to user
33053322
:return: command line text or 'eof' if an EOFError was caught
3306-
:raises Exception: whatever exceptions are raised by input() except for EOFError
3323+
:raises Exception: any exceptions raised by prompt(), except EOFError
33073324
"""
33083325
try:
33093326
# Use dynamic prompt if the prompt matches self.prompt

tests/test_cmd2.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1936,8 +1936,8 @@ def test_read_raw_input_pipe_echo(capsys) -> None:
19361936

19371937
def test_read_raw_input_eof() -> None:
19381938
app = cmd2.Cmd(stdin=io.StringIO(""))
1939-
result = app._read_raw_input("prompt> ", app.session, DummyCompleter())
1940-
assert result == constants.EOF
1939+
with pytest.raises(EOFError):
1940+
app._read_raw_input("prompt> ", app.session, DummyCompleter())
19411941

19421942

19431943
def test_resolve_completer_none(base_app: cmd2.Cmd) -> None:
@@ -3502,7 +3502,7 @@ def test_custom_completekey():
35023502
assert app.completekey == '?'
35033503

35043504

3505-
def test_build_session_exception(monkeypatch):
3505+
def test_init_session_exception(monkeypatch):
35063506

35073507
# Mock PromptSession to raise ValueError on first call, then succeed
35083508
valid_session_mock = mock.MagicMock(spec=PromptSession)
@@ -3590,7 +3590,7 @@ def test_multiline_complete_statement_keyboard_interrupt(multiline_app, monkeypa
35903590
poutput_mock.assert_called_with('^C')
35913591

35923592

3593-
def test_build_session_no_console_error(monkeypatch):
3593+
def test_init_session_no_console_error(monkeypatch):
35943594
from cmd2.cmd2 import NoConsoleScreenBufferError
35953595

35963596
# Mock PromptSession to raise NoConsoleScreenBufferError on first call, then succeed
@@ -3610,6 +3610,40 @@ def test_build_session_no_console_error(monkeypatch):
36103610
assert isinstance(kwargs['output'], DummyOutput)
36113611

36123612

3613+
def test_init_session_with_custom_tty() -> None:
3614+
# Create a mock stdin with says it's a TTY
3615+
custom_stdin = mock.MagicMock(spec=io.TextIOWrapper)
3616+
custom_stdin.isatty.return_value = True
3617+
assert custom_stdin is not sys.stdin
3618+
3619+
# Create a mock stdout which is not sys.stdout
3620+
custom_stdout = mock.MagicMock(spec=io.TextIOWrapper)
3621+
assert custom_stdout is not sys.stdout
3622+
3623+
# Check if the streams were wrapped
3624+
with (
3625+
mock.patch('cmd2.cmd2.create_input') as mock_create_input,
3626+
mock.patch('cmd2.cmd2.create_output') as mock_create_output,
3627+
):
3628+
app = cmd2.Cmd()
3629+
app.stdin = custom_stdin
3630+
app.stdout = custom_stdout
3631+
app._init_session()
3632+
3633+
mock_create_input.assert_called_once_with(stdin=custom_stdin)
3634+
mock_create_output.assert_called_once_with(stdout=custom_stdout)
3635+
3636+
3637+
def test_init_session_non_interactive() -> None:
3638+
# Set up a mock for a non-TTY stream (like a pipe)
3639+
mock_stdin = mock.MagicMock(spec=io.TextIOWrapper)
3640+
mock_stdin.isatty.return_value = False
3641+
3642+
app = cmd2.Cmd(stdin=mock_stdin)
3643+
assert isinstance(app.session.input, DummyInput)
3644+
assert isinstance(app.session.output, DummyOutput)
3645+
3646+
36133647
def test_no_console_screen_buffer_error_dummy():
36143648
from cmd2.cmd2 import NoConsoleScreenBufferError
36153649

0 commit comments

Comments
 (0)