Skip to content

Commit 2d0f689

Browse files
committed
Added initialization argument to cmd2.Cmd to support fish-style auto-suggestions based on history
Also: - Organized keyword-only arguments to cmd2.Cmd2 in alphabetical order for maintainability.
1 parent 888151d commit 2d0f689

File tree

3 files changed

+89
-55
lines changed

3 files changed

+89
-55
lines changed

cmd2/cmd2.py

Lines changed: 64 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140
with contextlib.suppress(ImportError):
141141
from IPython import start_ipython
142142

143+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
143144
from prompt_toolkit.completion import Completer, DummyCompleter
144145
from prompt_toolkit.formatted_text import ANSI
145146
from prompt_toolkit.history import InMemoryHistory
@@ -293,83 +294,86 @@ def __init__(
293294
stdin: TextIO | None = None,
294295
stdout: TextIO | None = None,
295296
*,
296-
persistent_history_file: str = '',
297-
persistent_history_length: int = 1000,
298-
startup_script: str = '',
299-
silence_startup_script: bool = False,
300-
include_py: bool = False,
301-
include_ipy: bool = False,
302297
allow_cli_args: bool = True,
303-
transcript_files: list[str] | None = None,
298+
allow_clipboard: bool = True,
304299
allow_redirection: bool = True,
305-
multiline_commands: list[str] | None = None,
306-
terminators: list[str] | None = None,
307-
shortcuts: dict[str, str] | None = None,
308-
command_sets: Iterable[CommandSet] | None = None,
309300
auto_load_commands: bool = False,
310-
allow_clipboard: bool = True,
311-
suggest_similar_command: bool = False,
312-
intro: RenderableType = '',
301+
auto_suggest: bool = True,
313302
bottom_toolbar: bool = False,
303+
command_sets: Iterable[CommandSet] | None = None,
314304
complete_style: CompleteStyle = CompleteStyle.COLUMN,
305+
include_ipy: bool = False,
306+
include_py: bool = False,
307+
intro: RenderableType = '',
315308
max_column_completion_items: int = 7,
309+
multiline_commands: list[str] | None = None,
310+
persistent_history_file: str = '',
311+
persistent_history_length: int = 1000,
312+
shortcuts: dict[str, str] | None = None,
313+
silence_startup_script: bool = False,
314+
startup_script: str = '',
315+
suggest_similar_command: bool = False,
316+
terminators: list[str] | None = None,
317+
transcript_files: list[str] | None = None,
316318
) -> None:
317319
"""Easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package.
318320
319321
:param completekey: name of a completion key, default to Tab
320322
:param stdin: alternate input file object, if not specified, sys.stdin is used
321323
:param stdout: alternate output file object, if not specified, sys.stdout is used
322-
:param persistent_history_file: file path to load a persistent cmd2 command history from
323-
:param persistent_history_length: max number of history items to write
324-
to the persistent history file
325-
:param startup_script: file path to a script to execute at startup
326-
:param silence_startup_script: if ``True``, then the startup script's output will be
327-
suppressed. Anything written to stderr will still display.
328-
:param include_py: should the "py" command be included for an embedded Python shell
329-
:param include_ipy: should the "ipy" command be included for an embedded IPython shell
330324
:param allow_cli_args: if ``True``, then [cmd2.Cmd.__init__][] will process command
331325
line arguments as either commands to be run or, if ``-t`` or
332326
``--test`` are given, transcript files to run. This should be
333327
set to ``False`` if your application parses its own command line
334328
arguments.
335-
:param transcript_files: pass a list of transcript files to be run on initialization.
336-
This allows running transcript tests when ``allow_cli_args``
337-
is ``False``. If ``allow_cli_args`` is ``True`` this parameter
338-
is ignored.
329+
:param allow_clipboard: If False, cmd2 will disable clipboard interactions
339330
:param allow_redirection: If ``False``, prevent output redirection and piping to shell
340331
commands. This parameter prevents redirection and piping, but
341332
does not alter parsing behavior. A user can still type
342333
redirection and piping tokens, and they will be parsed as such
343334
but they won't do anything.
344-
:param multiline_commands: list of commands allowed to accept multi-line input
345-
:param terminators: list of characters that terminate a command. These are mainly
346-
intended for terminating multiline commands, but will also
347-
terminate single-line commands. If not supplied, the default
348-
is a semicolon. If your app only contains single-line commands
349-
and you want terminators to be treated as literals by the parser,
350-
then set this to an empty list.
351-
:param shortcuts: dictionary containing shortcuts for commands. If not supplied,
352-
then defaults to constants.DEFAULT_SHORTCUTS. If you do not want
353-
any shortcuts, pass an empty dictionary.
354-
:param command_sets: Provide CommandSet instances to load during cmd2 initialization.
355-
This allows CommandSets with custom constructor parameters to be
356-
loaded. This also allows the a set of CommandSets to be provided
357-
when `auto_load_commands` is set to False
358335
:param auto_load_commands: If True, cmd2 will check for all subclasses of `CommandSet`
359336
that are currently loaded by Python and automatically
360337
instantiate and register all commands. If False, CommandSets
361338
must be manually installed with `register_command_set`.
362-
:param allow_clipboard: If False, cmd2 will disable clipboard interactions
363-
:param suggest_similar_command: if ``True``, then when a command is not found,
364-
[cmd2.Cmd][] will look for similar commands and suggest them.
365-
:param intro: introduction to display at startup
339+
:param auto_suggest: If True, cmd2 will provide fish shell style auto-suggestions
340+
based on history. If False, these will not be provided.
366341
:param bottom_toolbar: if ``True``, then a bottom toolbar will be displayed.
342+
:param command_sets: Provide CommandSet instances to load during cmd2 initialization.
343+
This allows CommandSets with custom constructor parameters to be
344+
loaded. This also allows the a set of CommandSets to be provided
345+
when `auto_load_commands` is set to False
367346
:param complete_style: style of prompt-toolkit tab completion to use, 3 valid options are:
368347
1. CompleteStyle.COLUMN (default) - displays hints with help next to them in one big column
369348
2. CompleteStyle.MULTI_COLUMN - displays hints across multiple columns, with help when selected
370349
3. CompleteStyle.READLINE_LIKE - displays like readline, complete_in_thread doesn't work
350+
:param include_ipy: should the "ipy" command be included for an embedded IPython shell
351+
:param include_py: should the "py" command be included for an embedded Python shell
352+
:param intro: introduction to display at startup
371353
:param max_column_completion_items: The maximum number of completion results to display in a single column,
372354
used to provide the initial value for a settable with the same name.
355+
:param multiline_commands: list of commands allowed to accept multi-line input
356+
:param persistent_history_file: file path to load a persistent cmd2 command history from
357+
:param persistent_history_length: max number of history items to write
358+
to the persistent history file
359+
:param shortcuts: dictionary containing shortcuts for commands. If not supplied,
360+
then defaults to constants.DEFAULT_SHORTCUTS. If you do not want
361+
any shortcuts, pass an empty dictionary.
362+
:param silence_startup_script: if ``True``, then the startup script's output will be
363+
suppressed. Anything written to stderr will still display.
364+
:param startup_script: file path to a script to execute at startup
365+
:param suggest_similar_command: if ``True``, then when a command is not found,
366+
[cmd2.Cmd][] will look for similar commands and suggest them.
367+
:param terminators: list of characters that terminate a command. These are mainly
368+
intended for terminating multiline commands, but will also
369+
terminate single-line commands. If not supplied, the default
370+
is a semicolon. If your app only contains single-line commands
371+
and you want terminators to be treated as literals by the parser,
372+
then set this to an empty list.
373+
:param transcript_files: pass a list of transcript files to be run on initialization.
374+
This allows running transcript tests when ``allow_cli_args``
375+
is ``False``. If ``allow_cli_args`` is ``True`` this parameter
376+
is ignored.
373377
"""
374378
# Check if py or ipy need to be disabled in this instance
375379
if not include_py:
@@ -467,30 +471,36 @@ def _(event: Any) -> None: # pragma: no cover
467471
self.lexer = Cmd2Lexer(self)
468472
self.bottom_toolbar = bottom_toolbar
469473

474+
self.auto_suggest = None
475+
if auto_suggest:
476+
self.auto_suggest = AutoSuggestFromHistory()
477+
470478
try:
471479
self.session: PromptSession[str] = PromptSession(
472-
history=self.history_adapter,
473-
completer=self.completer,
474-
lexer=self.lexer,
475-
complete_style=complete_style,
480+
auto_suggest=self.auto_suggest,
476481
complete_in_thread=True,
482+
complete_style=complete_style,
477483
complete_while_typing=False,
484+
completer=self.completer,
485+
history=self.history_adapter,
478486
key_bindings=key_bindings,
487+
lexer=self.lexer,
479488
)
480489
except (NoConsoleScreenBufferError, AttributeError, ValueError):
481490
# Fallback to dummy input/output if PromptSession initialization fails.
482491
# This can happen in some CI environments (like GitHub Actions on Windows)
483492
# where isatty() is True but there is no real console.
484493
self.session = PromptSession(
485-
history=self.history_adapter,
486-
completer=self.completer,
487-
lexer=self.lexer,
488-
input=DummyInput(),
489-
output=DummyOutput(),
490-
complete_style=complete_style,
494+
auto_suggest=self.auto_suggest,
491495
complete_in_thread=True,
496+
complete_style=complete_style,
492497
complete_while_typing=False,
498+
completer=self.completer,
499+
history=self.history_adapter,
500+
input=DummyInput(),
493501
key_bindings=key_bindings,
502+
lexer=self.lexer,
503+
output=DummyOutput(),
494504
)
495505

496506
# Commands to exclude from the history command

examples/basic_completion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
class BasicCompletion(cmd2.Cmd):
3535
def __init__(self) -> None:
36-
super().__init__(complete_style=CompleteStyle.MULTI_COLUMN, include_py=True)
36+
super().__init__(auto_suggest=False, complete_style=CompleteStyle.MULTI_COLUMN, include_py=True)
3737

3838
def do_flag_based(self, statement: cmd2.Statement) -> None:
3939
"""Tab completes arguments based on a preceding flag using flag_based_complete

tests/test_cmd2.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from unittest import mock
1313

1414
import pytest
15+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
1516
from rich.text import Text
1617

1718
import cmd2
@@ -3838,3 +3839,26 @@ def run_alert():
38383839

38393840
assert len(exceptions) == 1
38403841
assert "Event loop not available" in str(exceptions[0])
3842+
3843+
3844+
def test_auto_suggest_true():
3845+
"""Test that auto_suggest=True initializes AutoSuggestFromHistory."""
3846+
app = cmd2.Cmd(auto_suggest=True)
3847+
assert app.auto_suggest is not None
3848+
assert isinstance(app.auto_suggest, AutoSuggestFromHistory)
3849+
assert app.session.auto_suggest is app.auto_suggest
3850+
3851+
3852+
def test_auto_suggest_false():
3853+
"""Test that auto_suggest=False does not initialize AutoSuggestFromHistory."""
3854+
app = cmd2.Cmd(auto_suggest=False)
3855+
assert app.auto_suggest is None
3856+
assert app.session.auto_suggest is None
3857+
3858+
3859+
def test_auto_suggest_default():
3860+
"""Test that auto_suggest defaults to True."""
3861+
app = cmd2.Cmd()
3862+
assert app.auto_suggest is not None
3863+
assert isinstance(app.auto_suggest, AutoSuggestFromHistory)
3864+
assert app.session.auto_suggest is app.auto_suggest

0 commit comments

Comments
 (0)