Update documentation and code snippets

This commit is contained in:
2026-01-22 04:29:13 +03:00
parent 295e260a46
commit b8e9fdcb9c
7 changed files with 234 additions and 287 deletions
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Argenta \n" "Project-Id-Version: Argenta \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-02 22:27+0300\n" "POT-Creation-Date: 2026-01-22 04:26+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n" "Language: en\n"
@@ -29,92 +29,107 @@ msgid ""
"подсказки и завершая ввод на основе истории команд, что ускоряет работу и" "подсказки и завершая ввод на основе истории команд, что ускоряет работу и"
" снижает вероятность опечаток." " снижает вероятность опечаток."
msgstr "" msgstr ""
"``AutoCompleter`` is a component responsible for interactive command autocompletion. " "``AutoCompleter`` is a component responsible for interactive command "
"It improves user experience by offering suggestions and completing input based on " "autocompletion. It improves user experience by offering suggestions and "
"command history, which speeds up work and reduces the likelihood of typos." "completing input based on command history, which speeds up work and "
"reduces the likelihood of typos."
#: ../../root/api/app/autocompleter.rst:11 #: ../../root/api/app/autocompleter.rst:11
msgid "Инициализация" msgid "Инициализация"
msgstr "Initialization" msgstr "Initialization"
#: ../../root/api/app/autocompleter.rst:18 #: ../../root/api/app/autocompleter.rst:21
msgid "Создаёт и настраивает экземпляр ``AutoCompleter``." msgid "Создаёт и настраивает экземпляр ``AutoCompleter``."
msgstr "Creates and configures an ``AutoCompleter`` instance." msgstr "Creates and configures an ``AutoCompleter`` instance."
#: ../../root/api/app/autocompleter.rst:20 #: ../../root/api/app/autocompleter.rst:23
msgid "" msgid ""
"``history_filename``: Имя файла для сохранения истории команд. Если " "``history_filename``: Имя файла для сохранения истории команд. Если "
"указано, история будет сохраняться между сессиями. При значении ``None`` " "указано, история будет сохраняться между сессиями. При значении ``None`` "
"история хранится только в контексте сессии." "история хранится только в контексте сессии."
msgstr "" msgstr ""
"``history_filename``: Filename for saving command history. If specified, history " "``history_filename``: Filename for saving command history. If specified, "
"will be saved between sessions. When set to ``None``, history is stored only within " "history will be saved between sessions. When set to ``None``, history is "
"the session context." "stored only within the session context."
#: ../../root/api/app/autocompleter.rst:21 #: ../../root/api/app/autocompleter.rst:24
msgid "" msgid ""
"``autocomplete_button``: Клавиша, активирующая автодополнение. По " "``autocomplete_button``: Клавиша, активирующая автодополнение. По "
"умолчанию — **Tab**." "умолчанию — **Tab**."
msgstr "" msgstr ""
"``autocomplete_button``: Key that activates autocompletion. Defaults to **Tab**." "``autocomplete_button``: Key that activates autocompletion. Defaults to "
"**Tab**."
#: ../../root/api/app/autocompleter.rst:25
msgid ""
"``command_highlighting``: Если True, то в реальном времени при вводе "
"команды она будет подсвечиваться: зелёным, если такой триггер существует "
"и красный, если нет."
msgstr ""
"``command_highlighting``: If True, then in real time, when entering a "
" command, it will be highlighted: green if such a trigger exists "
"and red if not."
#: ../../root/api/app/autocompleter.rst:26 #: ../../root/api/app/autocompleter.rst:26
msgid ""
"``auto_suggestions``: Если True, то дополнение до раннее введённой "
"команды будет сразу отображаться светло-серым в строке ввода."
msgstr ""
"``auto_suggestions``: If True, the addition to the previously entered "
" command will immediately be displayed in light gray in the input line."
#: ../../root/api/app/autocompleter.rst:31
msgid "Назначение и возможности" msgid "Назначение и возможности"
msgstr "Purpose and Features" msgstr "Purpose and Features"
#: ../../root/api/app/autocompleter.rst:28 #: ../../root/api/app/autocompleter.rst:33
msgid "Основные возможности ``AutoCompleter``:" msgid "Основные возможности ``AutoCompleter``:"
msgstr "Main features of ``AutoCompleter``:" msgstr "Main features of ``AutoCompleter``:"
#: ../../root/api/app/autocompleter.rst:30 #: ../../root/api/app/autocompleter.rst:35
msgid "" msgid ""
"**Автодополнение по истории**: При нажатии клавиши автодополнения (по " "**Автодополнение по истории**: При нажатии клавиши автодополнения (по "
"умолчанию **Tab**) система ищет в истории команды, начинающиеся с уже " "умолчанию **Tab**) система ищет в истории команды, начинающиеся с уже "
"введённого текста." "введённого текста."
msgstr "" msgstr ""
"**History-based autocompletion**: When the autocompletion key is pressed (by default **Tab**), " "**History-based autocompletion**: When the autocompletion key is pressed "
"the system searches history for commands starting with the already entered text." "(by default **Tab**), the system searches history for commands starting "
"with the already entered text."
#: ../../root/api/app/autocompleter.rst:32 #: ../../root/api/app/autocompleter.rst:37
msgid "" msgid ""
"**Общий префикс**: Если найдено несколько команд с общим префиксом, будет" "**Общий префикс**: Если найдено несколько команд с общим префиксом, будет"
" подставлена только общая часть. Например, для команд ``show_users`` и " " подставлена только общая часть. Например, для команд ``show_users`` и "
"``show_profile`` при вводе ``sho`` и нажатии **Tab** ввод дополнится до " "``show_profile`` при вводе ``sho`` и нажатии **Tab** ввод дополнится до "
"``show_``." "``show_``."
msgstr "" msgstr ""
"**Common prefix**: If multiple commands with a common prefix are found, only the common " "**Common prefix**: If multiple commands with a common prefix are found, "
"part will be inserted. For example, for commands ``show_users`` and ``show_profile``, " "only the common part will be inserted. For example, for commands "
"when entering ``sho`` and pressing **Tab**, the input will be completed to ``show_``." "``show_users`` and ``show_profile``, when entering ``sho`` and pressing "
"**Tab**, the input will be completed to ``show_``."
#: ../../root/api/app/autocompleter.rst:34 #: ../../root/api/app/autocompleter.rst:39
msgid "" msgid ""
"**Постоянная история**: Если указан ``history_filename``, история команд " "**Постоянная история**: Если указан ``history_filename``, история команд "
"сохраняется в файл при выходе и загружается при следующем запуске. Это " "сохраняется в файл при выходе и загружается при следующем запуске. Это "
"делает автодополнение со временем «умнее»." "делает автодополнение со временем «умнее»."
msgstr "" msgstr ""
"**Persistent history**: If ``history_filename`` is specified, command history is saved " "**Persistent history**: If ``history_filename`` is specified, command "
"to a file on exit and loaded on the next startup. This makes autocompletion \"smarter\" over time." "history is saved to a file on exit and loaded on the next startup. This "
"makes autocompletion \"smarter\" over time."
#: ../../root/api/app/autocompleter.rst:36 #: ../../root/api/app/autocompleter.rst:41
msgid ""
"**Очистка истории**: При сохранении ``AutoCompleter`` удаляет дубликаты и"
" несуществующие команды, поддерживая историю в актуальном состоянии."
msgstr ""
"**History cleanup**: When saving, ``AutoCompleter`` removes duplicates and non-existent "
"commands, keeping the history up to date."
#: ../../root/api/app/autocompleter.rst:38
msgid "" msgid ""
"**Настройка клавиши**: Клавишу автодополнения можно изменить с помощью " "**Настройка клавиши**: Клавишу автодополнения можно изменить с помощью "
"параметра ``autocomplete_button``." "параметра ``autocomplete_button``."
msgstr "" msgstr ""
"**Key customization**: The autocompletion key can be changed using the ``autocomplete_button`` parameter." "**Key customization**: The autocompletion key can be changed using the "
"``autocomplete_button`` parameter."
#: ../../root/api/app/autocompleter.rst:43 #: ../../root/api/app/autocompleter.rst:46
msgid "Пример использования" msgid "Пример использования"
msgstr "Usage Example" msgstr "Usage Example"
#: ../../root/api/app/autocompleter.rst:45 #: ../../root/api/app/autocompleter.rst:48
msgid "``AutoCompleter`` передаётся как аргумент при инициализации `App`." msgid "``AutoCompleter`` передаётся как аргумент при инициализации `App`."
msgstr "``AutoCompleter`` is passed as an argument when initializing `App`." msgstr "``AutoCompleter`` is passed as an argument when initializing `App`."
+7 -4
View File
@@ -12,13 +12,18 @@ AutoCompleter
.. code-block:: python .. code-block:: python
__init__(self, history_filename: str | None = None, __init__(self,
autocomplete_button: str = "tab") -> None history_filename: str | None = None,
autocomplete_button: str = "tab",
command_highlighting: bool = True,
auto_suggestions: bool = True) -> None:
Создаёт и настраивает экземпляр ``AutoCompleter``. Создаёт и настраивает экземпляр ``AutoCompleter``.
* ``history_filename``: Имя файла для сохранения истории команд. Если указано, история будет сохраняться между сессиями. При значении ``None`` история хранится только в контексте сессии. * ``history_filename``: Имя файла для сохранения истории команд. Если указано, история будет сохраняться между сессиями. При значении ``None`` история хранится только в контексте сессии.
* ``autocomplete_button``: Клавиша, активирующая автодополнение. По умолчанию — **Tab**. * ``autocomplete_button``: Клавиша, активирующая автодополнение. По умолчанию — **Tab**.
* ``command_highlighting``: Если True, то в реальном времени при вводе команды она будет подсвечиваться: зелёным, если такой триггер существует и красный, если нет.
* ``auto_suggestions``: Если True, то дополнение до раннее введённой команды будет сразу отображаться светло-серым в строке ввода.
----- -----
@@ -33,8 +38,6 @@ AutoCompleter
* **Постоянная история**: Если указан ``history_filename``, история команд сохраняется в файл при выходе и загружается при следующем запуске. Это делает автодополнение со временем «умнее». * **Постоянная история**: Если указан ``history_filename``, история команд сохраняется в файл при выходе и загружается при следующем запуске. Это делает автодополнение со временем «умнее».
* **Очистка истории**: При сохранении ``AutoCompleter`` удаляет дубликаты и несуществующие команды, поддерживая историю в актуальном состоянии.
* **Настройка клавиши**: Клавишу автодополнения можно изменить с помощью параметра ``autocomplete_button``. * **Настройка клавиши**: Клавишу автодополнения можно изменить с помощью параметра ``autocomplete_button``.
----- -----
+1 -89
View File
@@ -1,92 +1,4 @@
__all__ = ["AutoCompleter"] from argenta.app import AutoCompleter
from prompt_toolkit import PromptSession, HTML
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.document import Document
from prompt_toolkit.history import History, ThreadedHistory, FileHistory, InMemoryHistory
from prompt_toolkit.key_binding import KeyBindings
class HistoryCompleter(Completer):
def __init__(self, history_container: History, static_commands: set[str]) -> None:
self.history_container: History = history_container
self.static_commands: set[str] = static_commands
def get_completions(self, document: Document, complete_event):
text: str = document.text_before_cursor
history_items: set[str] = set(self.history_container.load_history_strings())
all_candidates: set[str] = history_items.union(self.static_commands)
matches: list[str] = sorted(cmd for cmd in all_candidates if cmd.startswith(text))
if not matches:
return
for match in matches:
yield Completion(
match,
start_position=-len(text),
display=match
)
@staticmethod
def _find_common_prefix(matches: list[str]) -> str:
if not matches:
return ""
common: str = matches[0]
for match in matches[1:]:
i: int = 0
while i < len(common) and i < len(match) and common[i] == match[i]:
i += 1
common = common[:i]
return common
class AutoCompleter:
def __init__(
self,
history_filename: str | None = None,
autocomplete_button: str = "tab"
) -> None:
self.history_filename: str | None = history_filename
self.autocomplete_button: str = autocomplete_button
self._session: PromptSession | None = None
def initial_setup(self, all_commands: set[str]) -> None:
kb = KeyBindings()
def _(event):
buff = event.app.current_buffer
if buff.complete_state:
buff.complete_next()
else:
completions = list(buff.completer.get_completions(buff.document, None))
if len(completions) == 1:
buff.apply_completion(completions[0])
else:
buff.start_completion(select_first=False)
kb.add(self.autocomplete_button)(_)
if self.history_filename:
history = FileHistory(self.history_filename)
history = ThreadedHistory(history)
else:
history = InMemoryHistory()
self._session = PromptSession(
history=history,
completer=HistoryCompleter(history, all_commands),
complete_while_typing=False,
key_bindings=kb,
)
def prompt(self, prompt_text: str | HTML = ">>> ") -> str:
if self._session is None:
raise RuntimeError("Call initial_setup() before using prompt()")
return self._session.prompt(
HTML(f"<b><gray>{prompt_text}</gray></b>") if isinstance(prompt_text, str) else prompt_text
)
if __name__ == "__main__": if __name__ == "__main__":
+49 -2
View File
@@ -1,10 +1,40 @@
__all__ = ["AutoCompleter"] __all__ = ["AutoCompleter"]
import sys
from prompt_toolkit import PromptSession, HTML from prompt_toolkit import PromptSession, HTML
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.document import Document from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.history import History, ThreadedHistory, FileHistory, InMemoryHistory from prompt_toolkit.history import History, ThreadedHistory, FileHistory, InMemoryHistory
from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.styles import Style
class CommandLexer(Lexer):
def __init__(self, valid_commands: set[str]) -> None:
self.valid_commands: set[str] = valid_commands
def lex_document(self, document):
def get_line_tokens(lineno: int) -> StyleAndTextTuples:
if lineno >= len(document.lines):
return []
line_text: str = document.lines[lineno]
if not line_text.strip():
return [("", line_text)]
first_word: str = line_text.split()[0] if line_text.split() else ""
if first_word in self.valid_commands:
return [("class:valid", line_text)]
else:
return [("class:invalid", line_text)]
return get_line_tokens
class HistoryCompleter(Completer): class HistoryCompleter(Completer):
@@ -45,13 +75,23 @@ class AutoCompleter:
def __init__( def __init__(
self, self,
history_filename: str | None = None, history_filename: str | None = None,
autocomplete_button: str = "tab" autocomplete_button: str = "tab",
command_highlighting: bool = True,
auto_suggestions: bool = True,
) -> None: ) -> None:
self.history_filename: str | None = history_filename self.history_filename: str | None = history_filename
self.autocomplete_button: str = autocomplete_button self.autocomplete_button: str = autocomplete_button
self.command_highlighting: bool = command_highlighting
self.auto_suggestions: bool = auto_suggestions
self._session: PromptSession | None = None self._session: PromptSession | None = None
self._fallback_mode: bool = False
def initial_setup(self, all_commands: set[str]) -> None: def initial_setup(self, all_commands: set[str]) -> None:
if not sys.stdin.isatty():
self._session = None
self._fallback_mode = True
return
kb = KeyBindings() kb = KeyBindings()
def _(event): def _(event):
@@ -74,16 +114,23 @@ class AutoCompleter:
else: else:
history = InMemoryHistory() history = InMemoryHistory()
style = Style.from_dict({'valid': '#00ff00', 'invalid': '#ff0000'})
self._session = PromptSession( self._session = PromptSession(
history=history, history=history,
completer=HistoryCompleter(history, all_commands), completer=HistoryCompleter(history, all_commands),
complete_while_typing=False, complete_while_typing=False,
key_bindings=kb, key_bindings=kb,
auto_suggest=AutoSuggestFromHistory() if self.auto_suggestions else None,
style=style if self.command_highlighting else style,
lexer=CommandLexer(all_commands) if self.command_highlighting else None,
) )
def prompt(self, prompt_text: str | HTML = ">>> ") -> str: def prompt(self, prompt_text: str | HTML = ">>> ") -> str:
if self._fallback_mode:
return input(prompt_text if isinstance(prompt_text, str) else ">>> ")
if self._session is None: if self._session is None:
raise RuntimeError("Call initial_setup() before using prompt()") raise RuntimeError("Call initial_setup() before using prompt()")
return self._session.prompt( return self._session.prompt(
HTML(f"<b><gray>{prompt_text}</gray></b>") if isinstance(prompt_text, str) else prompt_text HTML(prompt_text) if isinstance(prompt_text, str) else prompt_text
) )
+4 -4
View File
@@ -6,6 +6,7 @@ from contextlib import redirect_stdout
from typing import Callable, Never, TypeAlias from typing import Callable, Never, TypeAlias
from art import text2art from art import text2art
from prompt_toolkit import HTML
from rich.console import Console from rich.console import Console
from rich.markup import escape from rich.markup import escape
@@ -306,7 +307,7 @@ class BaseApp:
Private. Sets up default app view Private. Sets up default app view
:return: None :return: None
""" """
self._prompt = f"[italic dim bold]{self._prompt}" self._prompt = f"<gray><b>{self._prompt}</b></gray>"
self._initial_message = ( self._initial_message = (
"\n" + f"[bold red]{text2art(self._initial_message, font='tarty1')}" + "\n" "\n" + f"[bold red]{text2art(self._initial_message, font='tarty1')}" + "\n"
) )
@@ -403,7 +404,7 @@ class App(BaseApp):
def __init__( def __init__(
self, self,
*, *,
prompt: str = "What do you want to do?\n\n", prompt: str | HTML = ">>> ",
initial_message: str = "Argenta\n", initial_message: str = "Argenta\n",
farewell_message: str = "\nSee you\n", farewell_message: str = "\nSee you\n",
exit_command: Command = DEFAULT_EXIT_COMMAND, exit_command: Command = DEFAULT_EXIT_COMMAND,
@@ -454,7 +455,7 @@ class App(BaseApp):
if self._repeat_command_groups_printing: if self._repeat_command_groups_printing:
self._print_command_group_description() self._print_command_group_description()
raw_command: str = Console().input(self._prompt) raw_command: str = self._autocompleter.prompt(self._prompt)
try: try:
input_command: InputCommand = InputCommand.parse(raw_command=raw_command) input_command: InputCommand = InputCommand.parse(raw_command=raw_command)
@@ -467,7 +468,6 @@ class App(BaseApp):
if self._is_exit_command(input_command): if self._is_exit_command(input_command):
self.system_router.finds_appropriate_handler(input_command) self.system_router.finds_appropriate_handler(input_command)
self._autocompleter.exit_setup(self.registered_routers.get_triggers())
return return
if self._is_unknown_command(input_command): if self._is_unknown_command(input_command):
+1 -1
View File
@@ -361,7 +361,7 @@ def test_set_exit_command_handler_stores_handler() -> None:
def test_setup_default_view_formats_prompt() -> None: def test_setup_default_view_formats_prompt() -> None:
app = App(prompt='>>') app = App(prompt='>>')
assert app._prompt == '[italic dim bold]>>' assert app._prompt == '<gray><b>>></b></gray>'
def test_setup_default_view_sets_default_unknown_command_handler() -> None: def test_setup_default_view_sets_default_unknown_command_handler() -> None:
+122 -152
View File
@@ -1,192 +1,162 @@
import os
from typing import Any
import pytest import pytest
from pyfakefs.fake_filesystem import FakeFilesystem from prompt_toolkit.document import Document
from pytest_mock import MockerFixture from prompt_toolkit.history import InMemoryHistory
from argenta.app.autocompleter.entity import ( from argenta.app.autocompleter.entity import (
AutoCompleter, AutoCompleter,
_get_history_items CommandLexer,
HistoryCompleter
) )
HISTORY_FILE: str = "test_history.txt"
COMMANDS: set[str] = {"start", "stop", "status"} COMMANDS: set[str] = {"start", "stop", "status"}
# ============================================================================ def test_autocompleter_initializes_with_default_params() -> None:
# Fixtures completer = AutoCompleter()
# ============================================================================ assert completer.history_filename is None
@pytest.fixture
def mock_readline(mocker: MockerFixture) -> Any:
_history: list[str] = []
def add_history(item: str) -> None:
_history.append(item)
def get_history_item(index: int) -> str | None:
if 1 <= index <= len(_history):
return _history[index - 1]
return None
def get_current_history_length() -> int:
return len(_history)
def clear_history() -> None:
_history.clear()
mock: Any = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
mocker.patch('argenta.app.autocompleter.entity.readline', mock) # pyright: ignore[reportUnknownArgumentType]
mock.reset_mock() # pyright: ignore[reportUnknownMemberType]
clear_history()
mock.add_history.side_effect = add_history # pyright: ignore[reportUnknownMemberType]
mock.get_history_item.side_effect = get_history_item # pyright: ignore[reportUnknownMemberType]
mock.get_current_history_length.side_effect = get_current_history_length # pyright: ignore[reportUnknownMemberType]
mock.get_completer_delims.return_value = " " # pyright: ignore[reportUnknownMemberType]
return mock # pyright: ignore[reportReturnType, reportUnknownVariableType]
# ============================================================================
# Tests for AutoCompleter initialization
# ============================================================================
def test_autocompleter_initializes_with_history_file_and_button() -> None:
completer: AutoCompleter = AutoCompleter(history_filename=HISTORY_FILE, autocomplete_button="tab")
assert completer.history_filename == HISTORY_FILE
assert completer.autocomplete_button == "tab" assert completer.autocomplete_button == "tab"
assert completer.command_highlighting is True
assert completer.auto_suggestions is True
# ============================================================================ def test_autocompleter_initializes_with_custom_params() -> None:
# Tests for initial setup completer = AutoCompleter(
# ============================================================================ history_filename="test.txt",
autocomplete_button="c-space",
command_highlighting=False,
auto_suggestions=False
)
assert completer.history_filename == "test.txt"
assert completer.autocomplete_button == "c-space"
assert completer.command_highlighting is False
assert completer.auto_suggestions is False
def test_initial_setup_creates_history_when_file_does_not_exist(fs: FakeFilesystem, mock_readline: Any) -> None: def test_prompt_raises_error_without_initial_setup() -> None:
if os.path.exists(HISTORY_FILE): completer = AutoCompleter()
os.remove(HISTORY_FILE) with pytest.raises(RuntimeError, match="Call initial_setup"):
completer.prompt(">>> ")
completer: AutoCompleter = AutoCompleter(history_filename=HISTORY_FILE)
completer.initial_setup(COMMANDS)
mock_readline.read_history_file.assert_not_called()
assert mock_readline.add_history.call_count == len(COMMANDS)
mock_readline.set_completer.assert_called_with(completer._complete)
mock_readline.parse_and_bind.assert_called_with("tab: complete")
def test_initial_setup_reads_existing_history_file(fs: FakeFilesystem, mock_readline: Any) -> None: def test_command_lexer_highlights_valid_command() -> None:
fs.create_file(HISTORY_FILE, contents="previous_command\n") # pyright: ignore[reportUnknownMemberType] lexer = CommandLexer({"start", "stop"})
doc = Document("start server")
completer: AutoCompleter = AutoCompleter(history_filename=HISTORY_FILE) tokens = lexer.lex_document(doc)(0)
completer.initial_setup(COMMANDS) assert tokens == [("class:valid", "start server")]
mock_readline.read_history_file.assert_called_once_with(HISTORY_FILE)
mock_readline.add_history.assert_not_called()
mock_readline.set_completer.assert_called_once()
mock_readline.parse_and_bind.assert_called_once()
def test_initial_setup_works_without_history_filename(mock_readline: Any) -> None: def test_command_lexer_highlights_invalid_command() -> None:
completer: AutoCompleter = AutoCompleter(history_filename=None) lexer = CommandLexer({"start", "stop"})
completer.initial_setup(COMMANDS) doc = Document("invalid command")
tokens = lexer.lex_document(doc)(0)
mock_readline.read_history_file.assert_not_called() assert tokens == [("class:invalid", "invalid command")]
assert mock_readline.add_history.call_count == len(COMMANDS)
# ============================================================================ def test_command_lexer_handles_empty_line() -> None:
# Tests for exit setup and history filtering lexer = CommandLexer({"start", "stop"})
# ============================================================================ doc = Document("")
tokens = lexer.lex_document(doc)(0)
assert tokens == [("", "")]
def test_exit_setup_writes_and_filters_duplicate_commands(fs: FakeFilesystem, mock_readline: Any) -> None: def test_command_lexer_handles_whitespace_only() -> None:
mock_readline.add_history.side_effect = None lexer = CommandLexer({"start", "stop"})
mock_readline.add_history("start server") doc = Document(" ")
mock_readline.add_history("stop client") tokens = lexer.lex_document(doc)(0)
mock_readline.add_history("invalid command") assert tokens == [("", " ")]
mock_readline.add_history("start server")
raw_history_content: str = "\n".join(["start server", "stop client", "invalid command", "start server"])
fs.create_file(HISTORY_FILE, contents=raw_history_content) # pyright: ignore[reportUnknownMemberType]
completer: AutoCompleter = AutoCompleter(history_filename=HISTORY_FILE)
completer.exit_setup(all_commands={"start", "stop"})
mock_readline.write_history_file.assert_called_once_with(HISTORY_FILE)
with open(HISTORY_FILE) as f:
content: str = f.read()
lines: list[str] = sorted(content.strip().split("\n"))
assert lines == ["start server", "stop client"]
def test_exit_setup_skips_writing_when_no_history_filename(mock_readline: Any) -> None: def test_history_completer_returns_matching_commands() -> None:
completer: AutoCompleter = AutoCompleter(history_filename=None) history = InMemoryHistory()
completer.exit_setup(all_commands=COMMANDS) history.append_string("start server")
mock_readline.write_history_file.assert_not_called() history.append_string("stop server")
completer = HistoryCompleter(history, {"status"})
doc = Document("sta")
completions = list(completer.get_completions(doc, None))
completion_texts = [c.text for c in completions]
assert "start server" in completion_texts
assert "status" in completion_texts
assert "stop server" not in completion_texts
# ============================================================================ def test_history_completer_returns_all_when_empty_input() -> None:
# Tests for autocomplete functionality history = InMemoryHistory()
# ============================================================================ history.append_string("start")
history.append_string("stop")
completer = HistoryCompleter(history, {"status"})
doc = Document("")
completions = list(completer.get_completions(doc, None))
completion_texts = [c.text for c in completions]
assert len(completion_texts) == 3
assert "start" in completion_texts
assert "stop" in completion_texts
assert "status" in completion_texts
def test_complete_returns_none_when_no_matches_found(mock_readline: Any) -> None: def test_history_completer_returns_empty_when_no_matches() -> None:
cmd: str history = InMemoryHistory()
for cmd in ["start", "stop"]: history.append_string("start")
mock_readline.add_history(cmd)
completer = HistoryCompleter(history, {"stop"})
completer: AutoCompleter = AutoCompleter() doc = Document("xyz")
assert completer._complete("run", 0) is None
assert completer._complete("run", 1) is None completions = list(completer.get_completions(doc, None))
assert len(completions) == 0
def test_complete_returns_single_match(mock_readline: Any) -> None: def test_history_completer_deduplicates_commands() -> None:
mock_readline.add_history("start server") history = InMemoryHistory()
mock_readline.add_history("stop server") history.append_string("start")
history.append_string("start")
completer: AutoCompleter = AutoCompleter()
assert completer._complete("start", 0) == "start server" completer = HistoryCompleter(history, {"start"})
assert completer._complete("start", 1) is None doc = Document("sta")
completions = list(completer.get_completions(doc, None))
assert len(completions) == 1
def test_complete_inserts_common_prefix_for_multiple_matches(mock_readline: Any) -> None: def test_history_completer_sorts_results() -> None:
mock_readline.add_history("status client") history = InMemoryHistory()
mock_readline.add_history("status server") history.append_string("stop")
mock_readline.add_history("stop") history.append_string("start")
history.append_string("status")
completer: AutoCompleter = AutoCompleter()
completer = HistoryCompleter(history, set())
result: str | None = completer._complete("stat", 0) doc = Document("st")
assert result is None
mock_readline.insert_text.assert_called_once_with("us ") completions = list(completer.get_completions(doc, None))
mock_readline.redisplay.assert_called_once() completion_texts = [c.text for c in completions]
mock_readline.reset_mock() assert completion_texts == ["start", "status", "stop"]
result_state_1: str | None = completer._complete("stat", 1)
assert result_state_1 is None
mock_readline.insert_text.assert_not_called()
# ============================================================================ def test_find_common_prefix_with_multiple_matches() -> None:
# Tests for helper functions matches = ["start server", "start client", "start process"]
# ============================================================================ prefix = HistoryCompleter._find_common_prefix(matches)
assert prefix == "start "
def test_get_history_items_returns_empty_list_initially(mock_readline: Any) -> None: def test_find_common_prefix_with_no_common() -> None:
assert _get_history_items() == [] matches = ["start", "stop", "status"]
prefix = HistoryCompleter._find_common_prefix(matches)
assert prefix == "st"
def test_get_history_items_returns_all_added_items(mock_readline: Any) -> None: def test_find_common_prefix_with_single_match() -> None:
mock_readline.add_history("first item") matches = ["start"]
mock_readline.add_history("second item") prefix = HistoryCompleter._find_common_prefix(matches)
assert prefix == "start"
assert _get_history_items() == ["first item", "second item"]
def test_find_common_prefix_with_empty_list() -> None:
matches: list[str] = []
prefix = HistoryCompleter._find_common_prefix(matches)
assert prefix == ""