perf boooooooooooooost

This commit is contained in:
2026-03-13 16:50:38 +03:00
parent 1cd5c3759e
commit b9b83540e2
9 changed files with 297 additions and 296 deletions
+1
View File
@@ -322,3 +322,4 @@ http-client.private.env.json
.idea/ApifoxUploaderProjectSetting.xml .idea/ApifoxUploaderProjectSetting.xml
.zed .zed
test.py
@@ -52,24 +52,30 @@ class EntrypointResolver:
@overload @overload
def parse_entrypoint_with_type( def parse_entrypoint_with_type(
self, entrypoint_type: type[CallableEntryPoint] self,
entrypoint_object_name: str,
entrypoint_type: type[CallableEntryPoint]
) -> EntryPoint[Callable[[], None]]: ... ) -> EntryPoint[Callable[[], None]]: ...
@overload @overload
def parse_entrypoint_with_type( def parse_entrypoint_with_type(
self, entrypoint_type: type[EntryPointAsApp] self,
entrypoint_object_name: str,
entrypoint_type: type[EntryPointAsApp]
) -> EntryPoint[App]: ... ) -> EntryPoint[App]: ...
def parse_entrypoint_with_type( def parse_entrypoint_with_type(
self, entrypoint_type: type[CallableEntryPoint] | type[EntryPointAsApp] self,
entrypoint_object_name: str,
entrypoint_type: type[CallableEntryPoint] | type[EntryPointAsApp]
) -> EntryPoint[Callable[[], None]] | EntryPoint[App]: ) -> EntryPoint[Callable[[], None]] | EntryPoint[App]:
if entrypoint_type is CallableEntryPoint: if entrypoint_type is CallableEntryPoint:
return self._parse_callable_entrypoint() return self._parse_callable_entrypoint(entrypoint_object_name)
elif entrypoint_type is EntryPointAsApp: elif entrypoint_type is EntryPointAsApp:
return self._parse_entrypoint_as_app() return self._parse_entrypoint_as_app(entrypoint_object_name)
raise NotImplementedError raise NotImplementedError
def _parse_callable_entrypoint(self) -> CallableEntryPoint: def _parse_callable_entrypoint(self, entrypoint_object_name: str) -> CallableEntryPoint:
resolved_entrypoint = self._resolve_from_string() resolved_entrypoint = self._resolve_from_string(entrypoint_object_name)
instance_object = resolved_entrypoint[1] instance_object = resolved_entrypoint[1]
if not callable(instance_object): if not callable(instance_object):
raise EntrypointNotCallableError(repr(instance_object)) raise EntrypointNotCallableError(repr(instance_object))
@@ -82,20 +88,21 @@ class EntrypointResolver:
instance_object = cast(Callable[[], None], instance_object) instance_object = cast(Callable[[], None], instance_object)
return CallableEntryPoint(raw_path=resolved_entrypoint[0], instance_object=instance_object) return CallableEntryPoint(raw_path=resolved_entrypoint[0], instance_object=instance_object)
def _parse_entrypoint_as_app(self) -> EntryPointAsApp: def _parse_entrypoint_as_app(self, entrypoint_object_name: str) -> EntryPointAsApp:
resolved_entrypoint = self._resolve_from_string() resolved_entrypoint = self._resolve_from_string(entrypoint_object_name)
instance_object = resolved_entrypoint[1] instance_object = resolved_entrypoint[1]
if not isinstance(instance_object, App): if not isinstance(instance_object, App):
raise EntrypointNotAppInstanceError(repr(instance_object)) raise EntrypointNotAppInstanceError(repr(instance_object))
return EntryPointAsApp(raw_path=resolved_entrypoint[0], instance_object=instance_object) return EntryPointAsApp(raw_path=resolved_entrypoint[0], instance_object=instance_object)
def _resolve_from_string(self) -> tuple[str, object]: def _resolve_from_string(self, entrypoint_object_name: str) -> tuple[str, object]:
file_path, _, attr_name = self._path_to_entrypoint.partition(":") file_path: str = self._path_to_entrypoint
attr_name: str = entrypoint_object_name
if not file_path or not attr_name: if not file_path or not attr_name:
raise ResolveFromStringError( raise ResolveFromStringError(
f'"{self._path_to_entrypoint}" must be in format "<path/to/file.py>:<attribute>"' f'"{self._path_to_entrypoint}" must be in format "<path/to/file.py>"'
) )
path = Path(file_path).resolve() path = Path(file_path).resolve()
@@ -0,0 +1,122 @@
__all__ = ['build_session', 'do_prompt']
from typing import Callable, Iterable
from prompt_toolkit import HTML, PromptSession
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import CompleteEvent, Completer, Completion, ThreadedCompleter
from prompt_toolkit.cursor_shapes import CursorShape
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.history import FileHistory, History, InMemoryHistory, ThreadedHistory
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
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: Document) -> Callable[[int], StyleAndTextTuples]:
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):
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: CompleteEvent
) -> Iterable[Completion]:
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
def build_session(
history_filename: str | None,
autocomplete_button: str,
command_highlighting: bool,
auto_suggestions: bool,
all_commands: set[str],
) -> PromptSession[str]:
kb = KeyBindings()
def _(event: KeyPressEvent) -> None:
buff = event.app.current_buffer
if buff.complete_state:
buff.complete_next()
return
comps_gen = iter(buff.completer.get_completions(buff.document, CompleteEvent()))
try:
first = next(comps_gen)
except StopIteration:
return
try:
_ = next(comps_gen)
buff.start_completion(select_first=False)
except StopIteration:
buff.apply_completion(first)
kb.add(autocomplete_button)(_)
history: InMemoryHistory | ThreadedHistory
if history_filename:
history = ThreadedHistory(FileHistory(history_filename))
else:
history = InMemoryHistory()
style = Style.from_dict({"valid": "#00ff00", "invalid": "#ff0000"})
return PromptSession(
history=history,
completer=ThreadedCompleter(HistoryCompleter(history, all_commands)),
complete_while_typing=False,
key_bindings=kb,
auto_suggest=AutoSuggestFromHistory() if auto_suggestions else None,
style=style if command_highlighting else None,
lexer=CommandLexer(all_commands) if command_highlighting else None,
)
def do_prompt(session: PromptSession[str], prompt_text: str | HTML) -> str:
return session.prompt(
HTML(prompt_text) if isinstance(prompt_text, str) else prompt_text,
cursor=CursorShape.BLINKING_BEAM,
)
+16 -108
View File
@@ -1,77 +1,12 @@
from __future__ import annotations
__all__ = ["AutoCompleter"] __all__ = ["AutoCompleter"]
import sys import sys
from typing import Callable, Iterable from typing import TYPE_CHECKING
from prompt_toolkit import HTML, PromptSession if TYPE_CHECKING:
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit import PromptSession, HTML
from prompt_toolkit.completion import (CompleteEvent, Completer, Completion,
ThreadedCompleter)
from prompt_toolkit.cursor_shapes import CursorShape
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.history import FileHistory, History, InMemoryHistory, ThreadedHistory
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
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: Document) -> Callable[[int], StyleAndTextTuples]:
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):
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: CompleteEvent) -> Iterable[Completion]:
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: class AutoCompleter:
@@ -95,41 +30,14 @@ class AutoCompleter:
self._fallback_mode = True self._fallback_mode = True
return return
kb = KeyBindings() from ._ext_features_impl import build_session
def _(event: KeyPressEvent) -> None: self._session = build_session(
buff = event.app.current_buffer self.history_filename,
if buff.complete_state: self.autocomplete_button,
buff.complete_next() self.command_highlighting,
return self.auto_suggestions,
comps_gen = iter(buff.completer.get_completions(buff.document, CompleteEvent())) all_commands
try:
first = next(comps_gen)
except StopIteration:
return
try:
_ = next(comps_gen)
buff.start_completion(select_first=False)
except StopIteration:
buff.apply_completion(first)
kb.add(self.autocomplete_button)(_)
history: InMemoryHistory | ThreadedHistory
if self.history_filename:
history = ThreadedHistory(FileHistory(self.history_filename))
else:
history = InMemoryHistory()
style = Style.from_dict({'valid': '#00ff00', 'invalid': '#ff0000'})
self._session = PromptSession(
history=history,
completer=ThreadedCompleter(HistoryCompleter(history, all_commands)),
complete_while_typing=False,
key_bindings=kb,
auto_suggest=AutoSuggestFromHistory() if self.auto_suggestions else None,
style=style if self.command_highlighting else None,
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:
@@ -137,7 +45,7 @@ class AutoCompleter:
return input(prompt_text if isinstance(prompt_text, str) else ">>> ") 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(
HTML(prompt_text) if isinstance(prompt_text, str) else prompt_text, from ._ext_features_impl import do_prompt
cursor=CursorShape.BLINKING_BEAM
) return do_prompt(self._session, prompt_text)
+3 -4
View File
@@ -3,8 +3,6 @@ __all__ = ["App"]
import difflib import difflib
from typing import Never, TypeAlias from typing import Never, TypeAlias
from rich.console import Console
from argenta.app.autocompleter import AutoCompleter from argenta.app.autocompleter import AutoCompleter
from argenta.app.behavior_handlers.models import (BehaviorHandlersFabric, from argenta.app.behavior_handlers.models import (BehaviorHandlersFabric,
BehaviorHandlersSettersMixin) BehaviorHandlersSettersMixin)
@@ -189,7 +187,7 @@ class App(BaseApp):
repeat_command_groups_printing: bool = False, repeat_command_groups_printing: bool = False,
override_system_messages: bool = False, override_system_messages: bool = False,
autocompleter: AutoCompleter | None = None, autocompleter: AutoCompleter | None = None,
printer: Printer = Console().print, printer: Printer | None = None,
) -> None: ) -> None:
""" """
Public. The essence of the application itself. Public. The essence of the application itself.
@@ -206,6 +204,7 @@ class App(BaseApp):
:param printer: system messages text output function :param printer: system messages text output function
:return: None :return: None
""" """
from rich.console import Console
super().__init__( super().__init__(
prompt=prompt, prompt=prompt,
initial_message=initial_message, initial_message=initial_message,
@@ -216,7 +215,7 @@ class App(BaseApp):
repeat_command_groups_printing=repeat_command_groups_printing, repeat_command_groups_printing=repeat_command_groups_printing,
override_system_messages=override_system_messages, override_system_messages=override_system_messages,
autocompleter=autocompleter or AutoCompleter(), autocompleter=autocompleter or AutoCompleter(),
printer=printer, printer=printer or Console().print,
) )
def include_router(self, router: Router) -> None: def include_router(self, router: Router) -> None:
+2 -2
View File
@@ -28,7 +28,7 @@ class Orchestrator:
self._auto_inject_handlers: bool = auto_inject_handlers self._auto_inject_handlers: bool = auto_inject_handlers
if self._arg_parser is not None: if self._arg_parser is not None:
self._arg_parser._parse_args() self._arg_parser._parse_args() # pyright: ignore[reportPrivateUsage]
def run_repl(self, app: App) -> None: def run_repl(self, app: App) -> None:
""" """
@@ -41,4 +41,4 @@ class Orchestrator:
) )
setup_dishka(app, container, auto_inject=self._auto_inject_handlers) setup_dishka(app, container, auto_inject=self._auto_inject_handlers)
app._run_repl() app._run_repl() # pyright: ignore[reportPrivateUsage]
+6 -1
View File
@@ -1,6 +1,11 @@
from __future__ import annotations
__all__ = ["Response"] __all__ = ["Response"]
from dishka import Container from typing import TYPE_CHECKING
if TYPE_CHECKING:
from dishka import Container
from argenta.command import InputFlags from argenta.command import InputFlags
from argenta.response.status import ResponseStatus from argenta.response.status import ResponseStatus
+2 -3
View File
@@ -3,8 +3,6 @@ __all__ = ["Router"]
from inspect import get_annotations, getfullargspec, getsourcefile, getsourcelines from inspect import get_annotations, getfullargspec, getsourcefile, getsourcelines
from typing import Callable from typing import Callable
from rich.console import Console
from argenta.app.protocols import HandlerFunc from argenta.app.protocols import HandlerFunc
from argenta.command import Command, InputCommand, InputFlags from argenta.command import Command, InputCommand, InputFlags
from argenta.command.flag import ValidationStatus from argenta.command.flag import ValidationStatus
@@ -20,7 +18,7 @@ from argenta.router.exceptions import (RepeatedAliasNameException,
class Router: class Router:
def __init__( def __init__(
self, self,
title: str = "Default title", title: str = "Title",
*, *,
disable_redirect_stdout: bool = False, disable_redirect_stdout: bool = False,
): ):
@@ -175,6 +173,7 @@ class Router:
response_arg_annotation = func_annotations.get(response_arg) response_arg_annotation = func_annotations.get(response_arg)
if response_arg_annotation is not None and response_arg_annotation is not Response: if response_arg_annotation is not None and response_arg_annotation is not Response:
from rich.console import Console
source_line: int = getsourcelines(func)[1] source_line: int = getsourcelines(func)[1]
Console().print( Console().print(
f'\nFile "{getsourcefile(func)}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint ' f'\nFile "{getsourcefile(func)}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint '
+63 -103
View File
@@ -10,14 +10,11 @@ from prompt_toolkit.completion import CompleteEvent
from prompt_toolkit.document import Document from prompt_toolkit.document import Document
from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.history import InMemoryHistory
from argenta.app.autocompleter.entity import ( from argenta.app.autocompleter._ext_features_impl import CommandLexer, HistoryCompleter
AutoCompleter, from argenta.app.autocompleter.entity import AutoCompleter
CommandLexer,
HistoryCompleter
)
COMMANDS: set[str] = {"start", "stop", "status"} COMMANDS: set[str] = {"start", "stop", "status"}
_IMPL = "argenta.app.autocompleter._ext_features_impl"
def test_autocompleter_initializes_with_default_params() -> None: def test_autocompleter_initializes_with_default_params() -> None:
@@ -33,7 +30,7 @@ def test_autocompleter_initializes_with_custom_params() -> None:
history_filename="test.txt", history_filename="test.txt",
autocomplete_button="c-space", autocomplete_button="c-space",
command_highlighting=False, command_highlighting=False,
auto_suggestions=False auto_suggestions=False,
) )
assert completer.history_filename == "test.txt" assert completer.history_filename == "test.txt"
assert completer.autocomplete_button == "c-space" assert completer.autocomplete_button == "c-space"
@@ -191,8 +188,10 @@ def test_history_completer_returns_early_when_no_matches() -> None:
def test_autocompleter_initial_setup_with_commands() -> None: def test_autocompleter_initial_setup_with_commands() -> None:
completer = AutoCompleter() completer = AutoCompleter()
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session: patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession") as mock_session,
):
completer.initial_setup({"start", "stop", "status"}) completer.initial_setup({"start", "stop", "status"})
assert completer._session is not None assert completer._session is not None
@@ -201,15 +200,17 @@ def test_autocompleter_initial_setup_with_commands() -> None:
def test_autocompleter_initial_setup_with_history_file() -> None: def test_autocompleter_initial_setup_with_history_file() -> None:
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f: with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as f:
history_file = f.name history_file = f.name
try: try:
completer = AutoCompleter(history_filename=history_file) completer = AutoCompleter(history_filename=history_file)
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession'), \ patch.object(sys.stdin, "isatty", return_value=True),
patch('argenta.app.autocompleter.entity.ThreadedHistory') as mock_threaded_history: patch(f"{_IMPL}.PromptSession"),
patch(f"{_IMPL}.ThreadedHistory") as mock_threaded_history,
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
@@ -223,9 +224,11 @@ def test_autocompleter_initial_setup_with_history_file() -> None:
def test_autocompleter_initial_setup_without_history_file() -> None: def test_autocompleter_initial_setup_without_history_file() -> None:
completer = AutoCompleter(history_filename=None) completer = AutoCompleter(history_filename=None)
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession'), \ patch.object(sys.stdin, "isatty", return_value=True),
patch('argenta.app.autocompleter.entity.InMemoryHistory') as mock_in_memory: patch(f"{_IMPL}.PromptSession"),
patch(f"{_IMPL}.InMemoryHistory") as mock_in_memory,
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
@@ -236,8 +239,10 @@ def test_autocompleter_initial_setup_without_history_file() -> None:
def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None: def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None:
completer = AutoCompleter(autocomplete_button="c-space") completer = AutoCompleter(autocomplete_button="c-space")
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession'): patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession"),
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
@@ -247,31 +252,34 @@ def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None:
def test_autocompleter_initial_setup_without_auto_suggestions() -> None: def test_autocompleter_initial_setup_without_auto_suggestions() -> None:
completer = AutoCompleter(auto_suggestions=False) completer = AutoCompleter(auto_suggestions=False)
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session: patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession") as mock_session,
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
call_kwargs = mock_session.call_args[1] call_kwargs = mock_session.call_args[1]
assert call_kwargs['auto_suggest'] is None assert call_kwargs["auto_suggest"] is None
def test_autocompleter_initial_setup_without_command_highlighting() -> None: def test_autocompleter_initial_setup_without_command_highlighting() -> None:
completer = AutoCompleter(command_highlighting=False) completer = AutoCompleter(command_highlighting=False)
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session: patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession") as mock_session,
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
call_kwargs = mock_session.call_args[1] call_kwargs = mock_session.call_args[1]
assert call_kwargs['style'] is None assert call_kwargs["style"] is None
assert call_kwargs['lexer'] is None assert call_kwargs["lexer"] is None
def test_autocompleter_key_binding_handler_with_complete_state() -> None: def _setup_captured_handler(completer: AutoCompleter) -> Callable[[Any], None] | None:
completer = AutoCompleter() """Вспомогательная функция: поднимает initial_setup и захватывает kb-хендлер."""
captured_handler: Callable[[Any], None] | None = None captured_handler: Callable[[Any], None] | None = None
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]: def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
@@ -281,16 +289,22 @@ def test_autocompleter_key_binding_handler_with_complete_state() -> None:
return func return func
return decorator return decorator
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession'), \ patch.object(sys.stdin, "isatty", return_value=True),
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class: patch(f"{_IMPL}.PromptSession"),
patch(f"{_IMPL}.KeyBindings") as mock_kb_class,
):
mock_kb = MagicMock() mock_kb = MagicMock()
mock_kb.add = capture_kb_add mock_kb.add = capture_kb_add
mock_kb_class.return_value = mock_kb mock_kb_class.return_value = mock_kb
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
return captured_handler
def test_autocompleter_key_binding_handler_with_complete_state() -> None:
completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
assert captured_handler is not None assert captured_handler is not None
mock_event = MagicMock() mock_event = MagicMock()
@@ -305,25 +319,8 @@ def test_autocompleter_key_binding_handler_with_complete_state() -> None:
def test_autocompleter_key_binding_handler_no_completions() -> None: def test_autocompleter_key_binding_handler_no_completions() -> None:
completer = AutoCompleter() completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
captured_handler: Callable[[Any], None] | None = None assert captured_handler is not None
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
nonlocal captured_handler
captured_handler = func
return func
return decorator
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
mock_kb = MagicMock()
mock_kb.add = capture_kb_add
mock_kb_class.return_value = mock_kb
completer.initial_setup({"start", "stop"})
mock_event = MagicMock() mock_event = MagicMock()
mock_buff = MagicMock() mock_buff = MagicMock()
@@ -333,7 +330,6 @@ def test_autocompleter_key_binding_handler_no_completions() -> None:
mock_buff.completer = mock_completer mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event) captured_handler(mock_event)
mock_buff.start_completion.assert_not_called() mock_buff.start_completion.assert_not_called()
@@ -342,25 +338,8 @@ def test_autocompleter_key_binding_handler_no_completions() -> None:
def test_autocompleter_key_binding_handler_single_completion() -> None: def test_autocompleter_key_binding_handler_single_completion() -> None:
completer = AutoCompleter() completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
captured_handler: Callable[[Any], None] | None = None assert captured_handler is not None
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
nonlocal captured_handler
captured_handler = func
return func
return decorator
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
mock_kb = MagicMock()
mock_kb.add = capture_kb_add
mock_kb_class.return_value = mock_kb
completer.initial_setup({"start", "stop"})
mock_event = MagicMock() mock_event = MagicMock()
mock_buff = MagicMock() mock_buff = MagicMock()
@@ -371,7 +350,6 @@ def test_autocompleter_key_binding_handler_single_completion() -> None:
mock_buff.completer = mock_completer mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event) captured_handler(mock_event)
mock_buff.apply_completion.assert_called_once_with(mock_completion) mock_buff.apply_completion.assert_called_once_with(mock_completion)
@@ -380,25 +358,8 @@ def test_autocompleter_key_binding_handler_single_completion() -> None:
def test_autocompleter_key_binding_handler_multiple_completions() -> None: def test_autocompleter_key_binding_handler_multiple_completions() -> None:
completer = AutoCompleter() completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
captured_handler: Callable[[Any], None] | None = None assert captured_handler is not None
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
nonlocal captured_handler
captured_handler = func
return func
return decorator
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
mock_kb = MagicMock()
mock_kb.add = capture_kb_add
mock_kb_class.return_value = mock_kb
completer.initial_setup({"start", "stop"})
mock_event = MagicMock() mock_event = MagicMock()
mock_buff = MagicMock() mock_buff = MagicMock()
@@ -410,7 +371,6 @@ def test_autocompleter_key_binding_handler_multiple_completions() -> None:
mock_buff.completer = mock_completer mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event) captured_handler(mock_event)
mock_buff.start_completion.assert_called_once_with(select_first=False) mock_buff.start_completion.assert_called_once_with(select_first=False)
@@ -420,43 +380,43 @@ def test_autocompleter_key_binding_handler_multiple_completions() -> None:
def test_autocompleter_prompt_in_fallback_mode_with_string() -> None: def test_autocompleter_prompt_in_fallback_mode_with_string() -> None:
completer = AutoCompleter() completer = AutoCompleter()
with patch.object(sys.stdin, 'isatty', return_value=False): with patch.object(sys.stdin, "isatty", return_value=False):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._fallback_mode is True assert completer._fallback_mode is True
with patch('builtins.input', return_value='test input'): with patch("builtins.input", return_value="test input"):
result = completer.prompt(">>> ") result = completer.prompt(">>> ")
assert result == 'test input' assert result == "test input"
def test_autocompleter_prompt_in_fallback_mode_with_html() -> None: def test_autocompleter_prompt_in_fallback_mode_with_html() -> None:
completer = AutoCompleter() completer = AutoCompleter()
with patch.object(sys.stdin, 'isatty', return_value=False): with patch.object(sys.stdin, "isatty", return_value=False):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._fallback_mode is True assert completer._fallback_mode is True
with patch('builtins.input', return_value='test input'): with patch("builtins.input", return_value="test input"):
result = completer.prompt(HTML("<b>>>> </b>")) result = completer.prompt(HTML("<b>>>> </b>"))
assert result == 'test input' assert result == "test input"
def test_autocompleter_prompt_with_html_in_normal_mode() -> None: def test_autocompleter_prompt_with_html_in_normal_mode() -> None:
completer = AutoCompleter() completer = AutoCompleter()
mock_session = MagicMock() mock_session = MagicMock()
mock_session.prompt.return_value = 'test result' mock_session.prompt.return_value = "test result"
completer._session = mock_session completer._session = mock_session
completer._fallback_mode = False completer._fallback_mode = False
html_prompt = HTML("<b>>>> </b>") html_prompt = HTML("<b>>>> </b>")
result = completer.prompt(html_prompt) result = completer.prompt(html_prompt)
assert result == 'test result' assert result == "test result"
mock_session.prompt.assert_called_once() mock_session.prompt.assert_called_once()
call_args = mock_session.prompt.call_args call_args = mock_session.prompt.call_args
assert call_args[0][0] == html_prompt assert call_args[0][0] == html_prompt
@@ -466,13 +426,13 @@ def test_autocompleter_prompt_with_string_in_normal_mode() -> None:
completer = AutoCompleter() completer = AutoCompleter()
mock_session = MagicMock() mock_session = MagicMock()
mock_session.prompt.return_value = 'test result' mock_session.prompt.return_value = "test result"
completer._session = mock_session completer._session = mock_session
completer._fallback_mode = False completer._fallback_mode = False
result = completer.prompt(">>> ") result = completer.prompt(">>> ")
assert result == 'test result' assert result == "test result"
mock_session.prompt.assert_called_once() mock_session.prompt.assert_called_once()
call_args = mock_session.prompt.call_args call_args = mock_session.prompt.call_args
assert isinstance(call_args[0][0], HTML) assert isinstance(call_args[0][0], HTML)