From 8828598f5f9127ca178ce5942f04cb16c2535277 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 26 Feb 2026 14:49:48 -0500 Subject: [PATCH 1/3] Fixed bug where complete() did not edit temporary session created by read_input(). --- cmd2/cmd2.py | 102 +++++++++++++++------------ cmd2/pt_utils.py | 6 +- examples/async_commands.py | 6 +- tests/test_cmd2.py | 81 +++++++++++---------- tests/test_custom_key_binding.py | 22 ------ tests/test_dynamic_complete_style.py | 18 +++-- tests/test_history.py | 2 +- tests/test_pt_utils.py | 11 --- 8 files changed, 122 insertions(+), 126 deletions(-) delete mode 100644 tests/test_custom_key_binding.py diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 0a604cb05..333f04ab7 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -71,6 +71,15 @@ import rich.box from prompt_toolkit import print_formatted_text from prompt_toolkit.application import get_app +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory +from prompt_toolkit.completion import Completer, DummyCompleter +from prompt_toolkit.formatted_text import ANSI, FormattedText +from prompt_toolkit.history import InMemoryHistory +from prompt_toolkit.input import DummyInput, create_input +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.output import DummyOutput, create_output +from prompt_toolkit.patch_stdout import patch_stdout +from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, set_title from rich.console import ( Group, RenderableType, @@ -158,16 +167,6 @@ with contextlib.suppress(ImportError): from IPython import start_ipython -from prompt_toolkit.auto_suggest import AutoSuggestFromHistory -from prompt_toolkit.completion import Completer, DummyCompleter -from prompt_toolkit.formatted_text import ANSI, FormattedText -from prompt_toolkit.history import InMemoryHistory -from prompt_toolkit.input import DummyInput, create_input -from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.output import DummyOutput, create_output -from prompt_toolkit.patch_stdout import patch_stdout -from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, set_title - try: if sys.platform == "win32": from prompt_toolkit.output.win32 import NoConsoleScreenBufferError # type: ignore[attr-defined] @@ -413,9 +412,6 @@ def __init__( else: self.stdout = sys.stdout - # Key used for completion - self.completekey = completekey - # Attributes which should NOT be dynamically settable via the set command at runtime self.default_to_shell = False # Attempt to run unrecognized commands as shell commands self.allow_redirection = allow_redirection # Security setting to prevent redirection of stdout @@ -468,17 +464,14 @@ def __init__( self._persistent_history_length = persistent_history_length self._initialize_history(persistent_history_file) - # Initialize prompt-toolkit PromptSession - self.history_adapter = Cmd2History(self) - self.completer = Cmd2Completer(self) - self.lexer = Cmd2Lexer(self) + # Initialize the main PromptSession self.bottom_toolbar = bottom_toolbar + self.main_session = self._initialize_main_session(auto_suggest, completekey) - self.auto_suggest = None - if auto_suggest: - self.auto_suggest = AutoSuggestFromHistory() - - self.session = self._init_session() + # The session currently holding focus (either the main REPL or a command's + # custom prompt). Completion and UI logic should reference this variable + # to ensure they modify the correct session state. + self.active_session = self.main_session # Commands to exclude from the history command self.exclude_from_history = ['_eof', 'history'] @@ -651,18 +644,18 @@ def __init__( # the current command being executed self.current_command: Statement | None = None - def _init_session(self) -> PromptSession[str]: + def _initialize_main_session(self, auto_suggest: bool, completekey: str) -> PromptSession[str]: """Initialize and return the core PromptSession for the application. Builds an interactive session if stdin is a TTY. Otherwise, uses dummy drivers to support non-interactive streams like pipes or files. """ key_bindings = None - if self.completekey != self.DEFAULT_COMPLETEKEY: + if completekey != self.DEFAULT_COMPLETEKEY: # Configure prompt_toolkit `KeyBindings` with the custom key for completion key_bindings = KeyBindings() - @key_bindings.add(self.completekey) + @key_bindings.add(completekey) def _(event: Any) -> None: # pragma: no cover """Trigger completion.""" b = event.current_buffer @@ -673,15 +666,15 @@ def _(event: Any) -> None: # pragma: no cover # Base configuration kwargs: dict[str, Any] = { - "auto_suggest": self.auto_suggest, + "auto_suggest": AutoSuggestFromHistory() if auto_suggest else None, "bottom_toolbar": self.get_bottom_toolbar if self.bottom_toolbar else None, "complete_style": CompleteStyle.MULTI_COLUMN, "complete_in_thread": True, "complete_while_typing": False, - "completer": self.completer, - "history": self.history_adapter, + "completer": Cmd2Completer(self), + "history": Cmd2History(self), "key_bindings": key_bindings, - "lexer": self.lexer, + "lexer": Cmd2Lexer(self), "rprompt": self.get_rprompt, } @@ -2448,9 +2441,9 @@ def complete( # Swap between COLUMN and MULTI_COLUMN style based on the number of matches. if len(completions) > self.max_column_completion_results: - self.session.complete_style = CompleteStyle.MULTI_COLUMN + self.active_session.complete_style = CompleteStyle.MULTI_COLUMN else: - self.session.complete_style = CompleteStyle.COLUMN + self.active_session.complete_style = CompleteStyle.COLUMN return completions # noqa: TRY300 @@ -3227,11 +3220,23 @@ def completedefault(self, *_ignored: Sequence[str]) -> Completions: def _suggest_similar_command(self, command: str) -> str | None: return suggest_similar(command, self.get_visible_commands()) + @staticmethod + def _is_tty_session(session: PromptSession[str]) -> bool: + """Determine if the session supports full terminal interactions. + + Returns True if the session is attached to a real TTY or a virtual + terminal (like PipeInput in tests). Returns False if the session is + running in a headless environment (DummyInput). + """ + # Validate against the session's assigned input driver rather than sys.stdin. + # This respects the fallback logic in _initialize_session() and allows unit + # tests to inject PipeInput for programmatic interaction. + return not isinstance(session.input, DummyInput) + def _read_raw_input( self, prompt: Callable[[], ANSI | str] | ANSI | str, session: PromptSession[str], - completer: Completer, **prompt_kwargs: Any, ) -> str: """Execute the low-level input read from either a terminal or a redirected stream. @@ -3242,17 +3247,23 @@ def _read_raw_input( :param prompt: the prompt text or a callable that returns the prompt. :param session: the PromptSession instance to use for reading. - :param completer: the completer to use for this specific input. :param prompt_kwargs: additional arguments passed directly to session.prompt(). :return: the stripped input string. :raises EOFError: if the input stream is closed or the user signals EOF (e.g., Ctrl+D) """ # Check if the session is configured for interactive terminal use. - if not isinstance(session.input, DummyInput): + if self._is_tty_session(session): + if not callable(prompt): + prompt = pt_filter_style(prompt) + with patch_stdout(): - if not callable(prompt): - prompt = pt_filter_style(prompt) - return session.prompt(prompt, completer=completer, **prompt_kwargs) + try: + # Set this session as the active one for UI/completion logic. + self.active_session = session + return session.prompt(prompt, **prompt_kwargs) + finally: + # Revert back to the main session. + self.active_session = self.main_session # We're not at a terminal, so we're likely reading from a file or a pipe. prompt_obj = prompt() if callable(prompt) else prompt @@ -3350,14 +3361,18 @@ def read_input( ) temp_session: PromptSession[str] = PromptSession( - complete_style=self.session.complete_style, - complete_while_typing=self.session.complete_while_typing, + auto_suggest=self.main_session.auto_suggest, + complete_style=self.main_session.complete_style, + complete_in_thread=self.main_session.complete_in_thread, + complete_while_typing=self.main_session.complete_while_typing, + completer=completer_to_use, history=InMemoryHistory(history) if history is not None else InMemoryHistory(), - input=self.session.input, - output=self.session.output, + key_bindings=self.main_session.key_bindings, + input=self.main_session.input, + output=self.main_session.output, ) - return self._read_raw_input(prompt, temp_session, completer_to_use) + return self._read_raw_input(prompt, temp_session) def _process_alerts(self) -> None: """Background worker that processes queued alerts and dynamic prompt updates.""" @@ -3452,8 +3467,7 @@ def _pre_prompt() -> None: try: return self._read_raw_input( prompt=prompt_to_use, - session=self.session, - completer=self.completer, + session=self.main_session, pre_run=_pre_prompt, ) finally: diff --git a/cmd2/pt_utils.py b/cmd2/pt_utils.py index c2a4ee6f3..cd825ef28 100644 --- a/cmd2/pt_utils.py +++ b/cmd2/pt_utils.py @@ -70,8 +70,7 @@ def get_completions(self, document: Document, _complete_event: object) -> Iterab # Define delimiters for completion to match cmd2/readline behavior delimiters = BASE_DELIMITERS - if hasattr(self.cmd_app, 'statement_parser'): - delimiters += "".join(self.cmd_app.statement_parser.terminators) + delimiters += "".join(self.cmd_app.statement_parser.terminators) # Find last delimiter before cursor to determine the word being completed begidx = 0 @@ -275,8 +274,7 @@ def get_line(lineno: int) -> list[tuple[str, str]]: # Get redirection tokens and terminators to avoid highlighting them as values exclude_tokens = set(constants.REDIRECTION_TOKENS) - if hasattr(self.cmd_app, 'statement_parser'): - exclude_tokens.update(self.cmd_app.statement_parser.terminators) + exclude_tokens.update(self.cmd_app.statement_parser.terminators) for m in arg_pattern.finditer(rest): space, flag, quoted, word = m.groups() diff --git a/examples/async_commands.py b/examples/async_commands.py index 3656b7073..aa1b2bab6 100755 --- a/examples/async_commands.py +++ b/examples/async_commands.py @@ -79,11 +79,11 @@ def __init__(self) -> None: super().__init__() self.intro = 'Welcome to the Async Commands example. Type "help" to see available commands.' - if self.session.key_bindings is None: - self.session.key_bindings = KeyBindings() + if self.main_session.key_bindings is None: + self.main_session.key_bindings = KeyBindings() # Add a custom key binding for +T that calls a method so it has access to self - @self.session.key_bindings.add('c-t') + @self.main_session.key_bindings.add('c-t') def _(_event: Any) -> None: self.handle_control_t(_event) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 56e787b5a..ea2a24924 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1249,11 +1249,11 @@ def test_async_alert(base_app, msg, prompt, is_stale, at_continuation_prompt) -> base_app._at_continuation_prompt = at_continuation_prompt with create_pipe_input() as pipe_input: - base_app.session = PromptSession( + base_app.main_session = PromptSession( input=pipe_input, output=DummyOutput(), - history=base_app.session.history, - completer=base_app.session.completer, + history=base_app.main_session.history, + completer=base_app.main_session.completer, ) pipe_input.send_text("quit\n") @@ -2007,15 +2007,15 @@ def test_echo(capsys) -> None: ) def test_read_raw_input_tty(base_app: cmd2.Cmd) -> None: with create_pipe_input() as pipe_input: - base_app.session = PromptSession( + base_app.main_session = PromptSession( input=pipe_input, output=DummyOutput(), - history=base_app.session.history, - completer=base_app.session.completer, + history=base_app.main_session.history, + completer=base_app.main_session.completer, ) pipe_input.send_text("foo\n") - result = base_app._read_raw_input("prompt> ", base_app.session, DummyCompleter()) + result = base_app._read_raw_input("prompt> ", base_app.main_session) assert result == "foo" @@ -2023,7 +2023,7 @@ def test_read_raw_input_interactive_pipe(capsys) -> None: prompt = "prompt> " app = cmd2.Cmd(stdin=io.StringIO("input from pipe\n")) app.interactive_pipe = True - result = app._read_raw_input(prompt, app.session, DummyCompleter()) + result = app._read_raw_input(prompt, app.main_session) assert result == "input from pipe" # In interactive mode, _read_raw_input() prints the prompt. @@ -2036,7 +2036,7 @@ def test_read_raw_input_non_interactive_pipe_echo_off(capsys) -> None: app = cmd2.Cmd(stdin=io.StringIO("input from pipe\n")) app.interactive_pipe = False app.echo = False - result = app._read_raw_input(prompt, app.session, DummyCompleter()) + result = app._read_raw_input(prompt, app.main_session) assert result == "input from pipe" # When not echoing in non-interactive mode, _read_raw_input() prints nothing. @@ -2049,7 +2049,7 @@ def test_read_raw_input_non_interactive_pipe_echo_on(capsys) -> None: app = cmd2.Cmd(stdin=io.StringIO("input from pipe\n")) app.interactive_pipe = False app.echo = True - result = app._read_raw_input(prompt, app.session, DummyCompleter()) + result = app._read_raw_input(prompt, app.main_session) assert result == "input from pipe" # When echoing in non-interactive mode, _read_raw_input() prints the prompt and input text. @@ -2060,7 +2060,7 @@ def test_read_raw_input_non_interactive_pipe_echo_on(capsys) -> None: def test_read_raw_input_eof() -> None: app = cmd2.Cmd(stdin=io.StringIO("")) with pytest.raises(EOFError): - app._read_raw_input("prompt> ", app.session, DummyCompleter()) + app._read_raw_input("prompt> ", app.main_session) def test_resolve_completer_none(base_app: cmd2.Cmd) -> None: @@ -3650,13 +3650,27 @@ class SynonymApp(cmd2.cmd2.Cmd): assert synonym_parser is help_parser -def test_custom_completekey(): - # Test setting a custom completekey - app = cmd2.Cmd(completekey='?') - assert app.completekey == '?' +def test_custom_completekey_ctrl_k(): + from prompt_toolkit.keys import Keys + # Test setting a custom completekey to + K + # In prompt_toolkit, this is 'c-k' + app = cmd2.Cmd(completekey='c-k') -def test_init_session_exception(monkeypatch): + assert app.main_session.key_bindings is not None + + # Check that we have a binding for c-k (Keys.ControlK) + found = False + for binding in app.main_session.key_bindings.bindings: + # binding.keys is a tuple of keys + if binding.keys == (Keys.ControlK,): + found = True + break + + assert found, "Could not find binding for 'c-k' (Keys.ControlK) in session key bindings" + + +def test_init_main_session_exception(monkeypatch): # Mock PromptSession to raise ValueError on first call, then succeed valid_session_mock = mock.MagicMock(spec=PromptSession) @@ -3744,7 +3758,7 @@ def test_multiline_complete_statement_keyboard_interrupt(multiline_app, monkeypa poutput_mock.assert_called_with('^C') -def test_init_session_no_console_error(monkeypatch): +def test_init_main_session_no_console_error(monkeypatch): from cmd2.cmd2 import NoConsoleScreenBufferError # Mock PromptSession to raise NoConsoleScreenBufferError on first call, then succeed @@ -3764,7 +3778,7 @@ def test_init_session_no_console_error(monkeypatch): assert isinstance(kwargs['output'], DummyOutput) -def test_init_session_with_custom_tty() -> None: +def test_init_main_session_with_custom_tty() -> None: # Create a mock stdin with says it's a TTY custom_stdin = mock.MagicMock(spec=io.TextIOWrapper) custom_stdin.isatty.return_value = True @@ -3782,20 +3796,20 @@ def test_init_session_with_custom_tty() -> None: app = cmd2.Cmd() app.stdin = custom_stdin app.stdout = custom_stdout - app._init_session() + app._initialize_main_session(auto_suggest=True, completekey=app.DEFAULT_COMPLETEKEY) mock_create_input.assert_called_once_with(stdin=custom_stdin) mock_create_output.assert_called_once_with(stdout=custom_stdout) -def test_init_session_non_interactive() -> None: +def test_init_main_session_non_interactive() -> None: # Set up a mock for a non-TTY stream (like a pipe) mock_stdin = mock.MagicMock(spec=io.TextIOWrapper) mock_stdin.isatty.return_value = False app = cmd2.Cmd(stdin=mock_stdin) - assert isinstance(app.session.input, DummyInput) - assert isinstance(app.session.output, DummyOutput) + assert isinstance(app.main_session.input, DummyInput) + assert isinstance(app.main_session.output, DummyOutput) def test_no_console_screen_buffer_error_dummy(): @@ -3816,7 +3830,7 @@ def test_read_command_line_dynamic_prompt(base_app: cmd2.Cmd) -> None: # will go down the TTY route. mock_session = mock.MagicMock() mock_session.input = mock.MagicMock() - base_app.session = mock_session + base_app.main_session = mock_session base_app._read_command_line(base_app.prompt) # Check that mock_prompt was called with a callable for the prompt @@ -3846,14 +3860,14 @@ def test_read_input_history_isolation(base_app: cmd2.Cmd) -> None: args, _ = mock_raw.call_args passed_session = args[1] - # Verify the session's history is an InMemoryHistory containing our list + # Verify the session's history contains our list loaded_history = list(passed_session.history.load_history_strings()) assert "secret_command" in loaded_history assert "another_command" in loaded_history # Verify the main app session was not touched # This is the crucial check for isolation - main_history = base_app.session.history.get_strings() + main_history = base_app.main_session.history.get_strings() assert "secret_command" not in main_history @@ -3868,11 +3882,11 @@ def test_pre_prompt_running_loop(base_app): # Set up pipe input to feed data to prompt_toolkit with create_pipe_input() as pipe_input: # Create a new session with our pipe input because the input property is read-only - base_app.session = PromptSession( + base_app.main_session = PromptSession( input=pipe_input, output=DummyOutput(), - history=base_app.session.history, - completer=base_app.session.completer, + history=base_app.main_session.history, + completer=base_app.main_session.completer, ) loop_check = {'running': False} @@ -3917,21 +3931,16 @@ def test_get_bottom_toolbar_narrow_terminal(base_app, monkeypatch): def test_auto_suggest_true(): """Test that auto_suggest=True initializes AutoSuggestFromHistory.""" app = cmd2.Cmd(auto_suggest=True) - assert app.auto_suggest is not None - assert isinstance(app.auto_suggest, AutoSuggestFromHistory) - assert app.session.auto_suggest is app.auto_suggest + assert isinstance(app.main_session.auto_suggest, AutoSuggestFromHistory) def test_auto_suggest_false(): """Test that auto_suggest=False does not initialize AutoSuggestFromHistory.""" app = cmd2.Cmd(auto_suggest=False) - assert app.auto_suggest is None - assert app.session.auto_suggest is None + assert app.main_session.auto_suggest is None def test_auto_suggest_default(): """Test that auto_suggest defaults to True.""" app = cmd2.Cmd() - assert app.auto_suggest is not None - assert isinstance(app.auto_suggest, AutoSuggestFromHistory) - assert app.session.auto_suggest is app.auto_suggest + assert isinstance(app.main_session.auto_suggest, AutoSuggestFromHistory) diff --git a/tests/test_custom_key_binding.py b/tests/test_custom_key_binding.py deleted file mode 100644 index 88cac7799..000000000 --- a/tests/test_custom_key_binding.py +++ /dev/null @@ -1,22 +0,0 @@ -from prompt_toolkit.keys import Keys - -import cmd2 - - -def test_custom_completekey_ctrl_k(): - # Test setting a custom completekey to + K - # In prompt_toolkit, this is 'c-k' - app = cmd2.Cmd(completekey='c-k') - - assert app.completekey == 'c-k' - assert app.session.key_bindings is not None - - # Check that we have a binding for c-k (Keys.ControlK) - found = False - for binding in app.session.key_bindings.bindings: - # binding.keys is a tuple of keys - if binding.keys == (Keys.ControlK,): - found = True - break - - assert found, "Could not find binding for 'c-k' (Keys.ControlK) in session key bindings" diff --git a/tests/test_dynamic_complete_style.py b/tests/test_dynamic_complete_style.py index 260e885ee..e044a9845 100644 --- a/tests/test_dynamic_complete_style.py +++ b/tests/test_dynamic_complete_style.py @@ -1,5 +1,5 @@ import pytest -from prompt_toolkit.shortcuts import CompleteStyle +from prompt_toolkit.shortcuts import CompleteStyle, PromptSession import cmd2 from cmd2 import Completions @@ -32,30 +32,38 @@ def app(): def test_dynamic_complete_style(app): + # Cmd.complete() interacts with app.active_session. + # Set it here since it's normally set when the prompt is created. + app.active_session: PromptSession[str] = PromptSession() + # Default max_column_completion_results is 7 assert app.max_column_completion_results == 7 # Complete 'foo' which has 10 items (> 7) # text='item', state=0, line='foo item', begidx=4, endidx=8 app.complete('item', 'foo item', 4, 8) - assert app.session.complete_style == CompleteStyle.MULTI_COLUMN + assert app.active_session.complete_style == CompleteStyle.MULTI_COLUMN # Complete 'bar' which has 5 items (<= 7) app.complete('item', 'bar item', 4, 8) - assert app.session.complete_style == CompleteStyle.COLUMN + assert app.active_session.complete_style == CompleteStyle.COLUMN def test_dynamic_complete_style_custom_limit(app): + # Cmd.complete() interacts with app.active_session. + # Set it here since it's normally set when the prompt is created. + app.active_session: PromptSession[str] = PromptSession() + # Change limit to 3 app.max_column_completion_results = 3 # Complete 'bar' which has 5 items (> 3) app.complete('item', 'bar item', 4, 8) - assert app.session.complete_style == CompleteStyle.MULTI_COLUMN + assert app.active_session.complete_style == CompleteStyle.MULTI_COLUMN # Change limit to 15 app.max_column_completion_results = 15 # Complete 'foo' which has 10 items (<= 15) app.complete('item', 'foo item', 4, 8) - assert app.session.complete_style == CompleteStyle.COLUMN + assert app.active_session.complete_style == CompleteStyle.COLUMN diff --git a/tests/test_history.py b/tests/test_history.py index 9791a1204..56758afc4 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -968,7 +968,7 @@ def test_history_populates_pt(hist_file) -> None: # prompt-toolkit only adds a single entry for multiple sequential identical commands # so we check to make sure that cmd2 populated the prompt-toolkit history # using the same rules - pt_history = app.session.history.get_strings() + pt_history = app.main_session.history.get_strings() assert len(pt_history) == 3 assert pt_history[0] == 'help' assert pt_history[1] == 'shortcuts' diff --git a/tests/test_pt_utils.py b/tests/test_pt_utils.py index b9a483756..859855e68 100644 --- a/tests/test_pt_utils.py +++ b/tests/test_pt_utils.py @@ -492,17 +492,6 @@ def test_init_with_custom_settings(self, mock_cmd_app: MockCmd) -> None: mock_cmd_app.complete.assert_called_once() assert mock_cmd_app.complete.call_args[1]['custom_settings'] == custom_settings - def test_get_completions_no_statement_parser(self, mock_cmd_app: MockCmd) -> None: - """Test initialization and completion without statement_parser.""" - del mock_cmd_app.statement_parser - completer = pt_utils.Cmd2Completer(cast(Any, mock_cmd_app)) - - document = Document("foo bar", cursor_position=7) - list(completer.get_completions(document, None)) - - # Should still work with default delimiters - mock_cmd_app.complete.assert_called_once() - def test_get_completions_custom_delimiters(self, mock_cmd_app: MockCmd) -> None: """Test that custom delimiters (terminators) are respected.""" mock_cmd_app.statement_parser.terminators = ['#'] From 06dc8b708c67a13d3ce63e53b9d5da9f60cbe34b Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 26 Feb 2026 15:04:48 -0500 Subject: [PATCH 2/3] Fixing Windows tests. --- tests/test_dynamic_complete_style.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_dynamic_complete_style.py b/tests/test_dynamic_complete_style.py index e044a9845..aa03b15e5 100644 --- a/tests/test_dynamic_complete_style.py +++ b/tests/test_dynamic_complete_style.py @@ -34,7 +34,10 @@ def app(): def test_dynamic_complete_style(app): # Cmd.complete() interacts with app.active_session. # Set it here since it's normally set when the prompt is created. - app.active_session: PromptSession[str] = PromptSession() + app.active_session: PromptSession[str] = PromptSession( + input=app.main_session.input, + output=app.main_session.output, + ) # Default max_column_completion_results is 7 assert app.max_column_completion_results == 7 @@ -52,7 +55,10 @@ def test_dynamic_complete_style(app): def test_dynamic_complete_style_custom_limit(app): # Cmd.complete() interacts with app.active_session. # Set it here since it's normally set when the prompt is created. - app.active_session: PromptSession[str] = PromptSession() + app.active_session: PromptSession[str] = PromptSession( + input=app.main_session.input, + output=app.main_session.output, + ) # Change limit to 3 app.max_column_completion_results = 3 From 710db8f9fa4f4e284196173f2e7cfbe12396521e Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 26 Feb 2026 15:14:41 -0500 Subject: [PATCH 3/3] Renamed function. --- cmd2/cmd2.py | 10 +++++----- tests/test_cmd2.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 333f04ab7..ca199c95a 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -464,9 +464,9 @@ def __init__( self._persistent_history_length = persistent_history_length self._initialize_history(persistent_history_file) - # Initialize the main PromptSession + # Create the main PromptSession self.bottom_toolbar = bottom_toolbar - self.main_session = self._initialize_main_session(auto_suggest, completekey) + self.main_session = self._create_main_session(auto_suggest, completekey) # The session currently holding focus (either the main REPL or a command's # custom prompt). Completion and UI logic should reference this variable @@ -644,8 +644,8 @@ def __init__( # the current command being executed self.current_command: Statement | None = None - def _initialize_main_session(self, auto_suggest: bool, completekey: str) -> PromptSession[str]: - """Initialize and return the core PromptSession for the application. + def _create_main_session(self, auto_suggest: bool, completekey: str) -> PromptSession[str]: + """Create and return the main PromptSession for the application. Builds an interactive session if stdin is a TTY. Otherwise, uses dummy drivers to support non-interactive streams like pipes or files. @@ -3229,7 +3229,7 @@ def _is_tty_session(session: PromptSession[str]) -> bool: running in a headless environment (DummyInput). """ # Validate against the session's assigned input driver rather than sys.stdin. - # This respects the fallback logic in _initialize_session() and allows unit + # This respects the fallback logic in _create_main_session() and allows unit # tests to inject PipeInput for programmatic interaction. return not isinstance(session.input, DummyInput) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index ea2a24924..edbec24be 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -3670,7 +3670,7 @@ def test_custom_completekey_ctrl_k(): assert found, "Could not find binding for 'c-k' (Keys.ControlK) in session key bindings" -def test_init_main_session_exception(monkeypatch): +def test_create_main_session_exception(monkeypatch): # Mock PromptSession to raise ValueError on first call, then succeed valid_session_mock = mock.MagicMock(spec=PromptSession) @@ -3758,7 +3758,7 @@ def test_multiline_complete_statement_keyboard_interrupt(multiline_app, monkeypa poutput_mock.assert_called_with('^C') -def test_init_main_session_no_console_error(monkeypatch): +def test_create_main_session_no_console_error(monkeypatch): from cmd2.cmd2 import NoConsoleScreenBufferError # Mock PromptSession to raise NoConsoleScreenBufferError on first call, then succeed @@ -3778,7 +3778,7 @@ def test_init_main_session_no_console_error(monkeypatch): assert isinstance(kwargs['output'], DummyOutput) -def test_init_main_session_with_custom_tty() -> None: +def test_create_main_session_with_custom_tty() -> None: # Create a mock stdin with says it's a TTY custom_stdin = mock.MagicMock(spec=io.TextIOWrapper) custom_stdin.isatty.return_value = True @@ -3796,13 +3796,13 @@ def test_init_main_session_with_custom_tty() -> None: app = cmd2.Cmd() app.stdin = custom_stdin app.stdout = custom_stdout - app._initialize_main_session(auto_suggest=True, completekey=app.DEFAULT_COMPLETEKEY) + app._create_main_session(auto_suggest=True, completekey=app.DEFAULT_COMPLETEKEY) mock_create_input.assert_called_once_with(stdin=custom_stdin) mock_create_output.assert_called_once_with(stdout=custom_stdout) -def test_init_main_session_non_interactive() -> None: +def test_create_main_session_non_interactive() -> None: # Set up a mock for a non-TTY stream (like a pipe) mock_stdin = mock.MagicMock(spec=io.TextIOWrapper) mock_stdin.isatty.return_value = False