From cac4a0c43776a90c6deedadab78ddbeb5d231911 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 9 Jun 2026 21:39:53 -0400 Subject: [PATCH] Add a new complete_in_thread parameter to cmd2.Cmd.__init__ that defaults to True If this parameter is True, then completion is performed in a background thread separate from the main REPL thread. If False, completion is performed in the main thread and can block the main thread if slow. --- CHANGELOG.md | 8 ++++++++ cmd2/cmd2.py | 5 ++++- docs/features/initialization.md | 1 + tests/test_cmd2.py | 17 +++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f784e402d..b67cf797e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 4.1.0 (TBD) + +- Enhancements + - New `cmd2.Cmd` parameters + - **complete_in_thread**: (boolean) if `True`, then completion will run in a separate + thread. If `False` then completion runs in the main thread and causes it to block if slow. + Defaults to `True`. + ## 4.0.0 (June 5, 2026) ### Summary diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 82df92f89..88c78e873 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -368,6 +368,7 @@ def __init__( auto_load_commands: bool = False, auto_suggest: bool = True, bottom_toolbar: bool = False, + complete_in_thread: bool = True, command_sets: Iterable[CommandSet[Any]] | None = None, include_ipy: bool = False, include_py: bool = False, @@ -404,6 +405,7 @@ def __init__( based on history. User can press right-arrow key to accept the provided suggestion. :param bottom_toolbar: if ``True``, then a bottom toolbar will be displayed. + :param complete_in_thread: if ``True``, then completion will run in a separate thread. :param command_sets: Provide CommandSet instances to load during cmd2 initialization. This allows CommandSets with custom constructor parameters to be loaded. This also allows the a set of CommandSets to be provided @@ -528,6 +530,7 @@ def __init__( # Create the main PromptSession self.bottom_toolbar = bottom_toolbar + self.complete_in_thread = complete_in_thread self.main_session = self._create_main_session(auto_suggest, completekey) # The session currently holding focus (either the main REPL or a command's @@ -752,7 +755,7 @@ def _(event: Any) -> None: # pragma: no cover "bottom_toolbar": self.get_bottom_toolbar if self.bottom_toolbar else None, "color_depth": ColorDepth.TRUE_COLOR, "complete_style": CompleteStyle.MULTI_COLUMN, - "complete_in_thread": True, + "complete_in_thread": self.complete_in_thread, "complete_while_typing": False, "completer": Cmd2Completer(self), "history": Cmd2History(item.raw for item in self.history), diff --git a/docs/features/initialization.md b/docs/features/initialization.md index f15cd613a..491e71025 100644 --- a/docs/features/initialization.md +++ b/docs/features/initialization.md @@ -35,6 +35,7 @@ Here are instance attributes of `cmd2.Cmd` which developers might wish to overri - **bottom_toolbar**: if `True`, then a bottom toolbar will be displayed (Default: `False`) - **broken_pipe_warning**: if non-empty, this string will be displayed if a broken pipe error occurs +- **complete_in_thread**: if `True`, then completion will run in a separate thread (Default: `True`) - **continuation_prompt**: used for multiline commands on 2nd+ line of input - **debug**: if `True`, show full stack trace on error (Default: `False`) - **default_error**: the error that prints when a non-existent command is run diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 79cfd9f27..df6200898 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -64,6 +64,23 @@ def test_version(base_app) -> None: assert cmd2.__version__ +def test_complete_in_thread() -> None: + # Test default + app_default = cmd2.Cmd() + assert app_default.complete_in_thread is True + assert app_default.main_session.complete_in_thread is True + + # Test True + app_true = cmd2.Cmd(complete_in_thread=True) + assert app_true.complete_in_thread is True + assert app_true.main_session.complete_in_thread is True + + # Test False + app_false = cmd2.Cmd(complete_in_thread=False) + assert app_false.complete_in_thread is False + assert app_false.main_session.complete_in_thread is False + + def test_not_in_main_thread(base_app, capsys) -> None: import threading