mirror of
https://github.com/koloideal/Argenta.git
synced 2026-06-10 10:05:28 +03:00
benchs
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
<a id='changelog-1.2.0'></a>
|
||||||
|
## 1.2.0 — 2026-02-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 100% coverage of the code base with tests
|
||||||
|
- 100% coverage with typhints
|
||||||
|
- 100% coverage of public API documentation in two languages - Russian and English
|
||||||
|
- cli attributes: highlighting valid commands, redesigned input history with auto-completion, interactive autocomplete selection menu for multiple candidates
|
||||||
|
- a metrics module that allows you to test the performance of various library units
|
||||||
|
- implementing a dependency injection pattern through an ioc container
|
||||||
|
- implementation of a context object for transferring data between handlers within a session
|
||||||
|
- adding a changelog
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- increased performance by several times (there will be real numbers in the next releases)
|
||||||
|
- reworking the internal API, highlighting different layers and reducing connectivity
|
||||||
|
- reworking the README and adding a translation for it
|
||||||
+22
-2
@@ -1,9 +1,9 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "argenta"
|
name = "argenta"
|
||||||
version = "1.1.2"
|
version = "1.2.0"
|
||||||
description = "Python library for building modular CLI applications"
|
description = "Python library for building modular CLI applications"
|
||||||
authors = [{ name = "kolo", email = "kolo.is.main@gmail.com" }]
|
authors = [{ name = "kolo", email = "kolo.is.main@gmail.com" }]
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12,<3.15"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -14,6 +14,13 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
{include-group = "linters"},
|
||||||
|
{include-group = "typecheckers"},
|
||||||
|
{include-group = "docs"},
|
||||||
|
{include-group = "tests"},
|
||||||
|
"scriv>=1.8.0",
|
||||||
|
]
|
||||||
linters = [
|
linters = [
|
||||||
"isort>=7.0.0",
|
"isort>=7.0.0",
|
||||||
"ruff>=0.12.12",
|
"ruff>=0.12.12",
|
||||||
@@ -65,6 +72,19 @@ omit = [
|
|||||||
"src/argenta/metrics/*"
|
"src/argenta/metrics/*"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.scriv]
|
||||||
|
format = "md"
|
||||||
|
output_file = "CHANGELOG.md"
|
||||||
|
fragment_directory = "changelog.d"
|
||||||
|
categories = [
|
||||||
|
"Added",
|
||||||
|
"Changed",
|
||||||
|
"Deprecated",
|
||||||
|
"Removed",
|
||||||
|
"Fixed",
|
||||||
|
]
|
||||||
|
md_header_level = "2"
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
disable_error_code = "import-untyped"
|
disable_error_code = "import-untyped"
|
||||||
|
|
||||||
|
|||||||
@@ -52,10 +52,7 @@ class BaseApp(BehaviorHandlersSettersMixin):
|
|||||||
self.registered_routers: RegisteredRouters = RegisteredRouters()
|
self.registered_routers: RegisteredRouters = RegisteredRouters()
|
||||||
self._messages_on_startup: list[str] = []
|
self._messages_on_startup: list[str] = []
|
||||||
|
|
||||||
if self._override_system_messages:
|
self._renderer: Renderer = PlainRenderer() if self._override_system_messages else RichRenderer()
|
||||||
self._renderer: Renderer = PlainRenderer()
|
|
||||||
else:
|
|
||||||
self._renderer: Renderer = RichRenderer()
|
|
||||||
|
|
||||||
self._viewer: Viewer = Viewer(
|
self._viewer: Viewer = Viewer(
|
||||||
printer=self._printer,
|
printer=self._printer,
|
||||||
|
|||||||
@@ -137,7 +137,8 @@ class PlainRenderer(Renderer):
|
|||||||
def render_text_for_description_message_generator(command: str, description: str) -> str:
|
def render_text_for_description_message_generator(command: str, description: str) -> str:
|
||||||
return f"{command} *=*=* {description}"
|
return f"{command} *=*=* {description}"
|
||||||
|
|
||||||
def render_messages_on_startup(self, messages: Iterable[str]) -> str:
|
@staticmethod
|
||||||
|
def render_messages_on_startup(messages: Iterable[str]) -> str:
|
||||||
return "\n" + "\n".join(messages)
|
return "\n" + "\n".join(messages)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -77,19 +77,19 @@ class Viewer:
|
|||||||
if max_length_line > 100
|
if max_length_line > 100
|
||||||
else 10
|
else 10
|
||||||
)
|
)
|
||||||
dividing_line_as_str: str = self._dividing_line.get_full_dynamic_line(
|
dynamic_dividing_line_as_str: str = self._dividing_line.get_full_dynamic_line(
|
||||||
length=max_length_line, is_override=self._override_system_messages
|
length=max_length_line, is_override=self._override_system_messages
|
||||||
)
|
)
|
||||||
self._printer(dividing_line_as_str + "\n")
|
self._printer(dynamic_dividing_line_as_str + "\n")
|
||||||
self._printer(Text.from_ansi(stdout_result.strip("\n")).markup)
|
self._printer(Text.from_ansi(stdout_result.strip("\n")).markup)
|
||||||
self._printer('\n' + dividing_line_as_str)
|
self._printer('\n' + dynamic_dividing_line_as_str)
|
||||||
|
|
||||||
case (StaticDividingLine() as dividing_line, bool()) | (DynamicDividingLine() as dividing_line, True):
|
case (StaticDividingLine() as dividing_line, bool()) | (DynamicDividingLine() as dividing_line, True):
|
||||||
dividing_line_as_str: str = StaticDividingLine(dividing_line.get_unit_part()).get_full_static_line(
|
static_dividing_line_as_str: str = StaticDividingLine(dividing_line.get_unit_part()).get_full_static_line(
|
||||||
is_override=self._override_system_messages
|
is_override=self._override_system_messages
|
||||||
)
|
)
|
||||||
self._printer(dividing_line_as_str + '\n')
|
self._printer(static_dividing_line_as_str + '\n')
|
||||||
output_text_generator()
|
output_text_generator()
|
||||||
self._printer('\n' + dividing_line_as_str)
|
self._printer('\n' + static_dividing_line_as_str)
|
||||||
case _:
|
case _:
|
||||||
raise NotImplementedError(f"Dividing line with type {self._dividing_line} is not implemented")
|
raise NotImplementedError(f"Dividing line with type {self._dividing_line} is not implemented")
|
||||||
|
|||||||
@@ -618,11 +618,17 @@ def test_app_handlers_work_with_multiple_routers() -> None:
|
|||||||
|
|
||||||
app.set_unknown_command_handler(custom_handler)
|
app.set_unknown_command_handler(custom_handler)
|
||||||
|
|
||||||
# Both commands should be known
|
|
||||||
assert not app._is_unknown_command(InputCommand('cmd1'))
|
assert not app._is_unknown_command(InputCommand('cmd1'))
|
||||||
assert not app._is_unknown_command(InputCommand('cmd2'))
|
assert not app._is_unknown_command(InputCommand('cmd2'))
|
||||||
|
|
||||||
# Unknown command should trigger handler
|
|
||||||
assert app._is_unknown_command(InputCommand('unknown'))
|
assert app._is_unknown_command(InputCommand('unknown'))
|
||||||
app._unknown_command_handler(InputCommand('unknown'))
|
app._unknown_command_handler(InputCommand('unknown'))
|
||||||
assert call_tracker['called']
|
assert call_tracker['called']
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_exist_and_valid_command_raises_runtime_error_when_router_not_found() -> None:
|
||||||
|
app = App()
|
||||||
|
app._pre_cycle_setup()
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="Router for 'nonexistent' not found. Panic!"):
|
||||||
|
app._process_exist_and_valid_command(InputCommand('nonexistent'))
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from typing import Any, Callable
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from prompt_toolkit import HTML
|
||||||
|
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
|
||||||
|
|
||||||
@@ -75,7 +83,7 @@ def test_history_completer_returns_matching_commands() -> None:
|
|||||||
completer = HistoryCompleter(history, {"status"})
|
completer = HistoryCompleter(history, {"status"})
|
||||||
doc = Document("sta")
|
doc = Document("sta")
|
||||||
|
|
||||||
completions = list(completer.get_completions(doc, None))
|
completions = list(completer.get_completions(doc, CompleteEvent()))
|
||||||
completion_texts = [c.text for c in completions]
|
completion_texts = [c.text for c in completions]
|
||||||
|
|
||||||
assert "start server" in completion_texts
|
assert "start server" in completion_texts
|
||||||
@@ -91,7 +99,7 @@ def test_history_completer_returns_all_when_empty_input() -> None:
|
|||||||
completer = HistoryCompleter(history, {"status"})
|
completer = HistoryCompleter(history, {"status"})
|
||||||
doc = Document("")
|
doc = Document("")
|
||||||
|
|
||||||
completions = list(completer.get_completions(doc, None))
|
completions = list(completer.get_completions(doc, CompleteEvent()))
|
||||||
completion_texts = [c.text for c in completions]
|
completion_texts = [c.text for c in completions]
|
||||||
|
|
||||||
assert len(completion_texts) == 3
|
assert len(completion_texts) == 3
|
||||||
@@ -107,7 +115,7 @@ def test_history_completer_returns_empty_when_no_matches() -> None:
|
|||||||
completer = HistoryCompleter(history, {"stop"})
|
completer = HistoryCompleter(history, {"stop"})
|
||||||
doc = Document("xyz")
|
doc = Document("xyz")
|
||||||
|
|
||||||
completions = list(completer.get_completions(doc, None))
|
completions = list(completer.get_completions(doc, CompleteEvent()))
|
||||||
assert len(completions) == 0
|
assert len(completions) == 0
|
||||||
|
|
||||||
|
|
||||||
@@ -119,7 +127,7 @@ def test_history_completer_deduplicates_commands() -> None:
|
|||||||
completer = HistoryCompleter(history, {"start"})
|
completer = HistoryCompleter(history, {"start"})
|
||||||
doc = Document("sta")
|
doc = Document("sta")
|
||||||
|
|
||||||
completions = list(completer.get_completions(doc, None))
|
completions = list(completer.get_completions(doc, CompleteEvent()))
|
||||||
assert len(completions) == 1
|
assert len(completions) == 1
|
||||||
|
|
||||||
|
|
||||||
@@ -132,7 +140,7 @@ def test_history_completer_sorts_results() -> None:
|
|||||||
completer = HistoryCompleter(history, set())
|
completer = HistoryCompleter(history, set())
|
||||||
doc = Document("st")
|
doc = Document("st")
|
||||||
|
|
||||||
completions = list(completer.get_completions(doc, None))
|
completions = list(completer.get_completions(doc, CompleteEvent()))
|
||||||
completion_texts = [c.text for c in completions]
|
completion_texts = [c.text for c in completions]
|
||||||
|
|
||||||
assert completion_texts == ["start", "status", "stop"]
|
assert completion_texts == ["start", "status", "stop"]
|
||||||
@@ -160,3 +168,311 @@ def test_find_common_prefix_with_empty_list() -> None:
|
|||||||
matches: list[str] = []
|
matches: list[str] = []
|
||||||
prefix = HistoryCompleter._find_common_prefix(matches)
|
prefix = HistoryCompleter._find_common_prefix(matches)
|
||||||
assert prefix == ""
|
assert prefix == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_lexer_handles_out_of_range_lineno() -> None:
|
||||||
|
lexer = CommandLexer({"start", "stop"})
|
||||||
|
doc = Document("start")
|
||||||
|
get_line_tokens = lexer.lex_document(doc)
|
||||||
|
tokens = get_line_tokens(1)
|
||||||
|
assert tokens == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_history_completer_returns_early_when_no_matches() -> None:
|
||||||
|
history = InMemoryHistory()
|
||||||
|
completer = HistoryCompleter(history, {"start", "stop"})
|
||||||
|
doc = Document("xyz")
|
||||||
|
|
||||||
|
result = completer.get_completions(doc, CompleteEvent())
|
||||||
|
completions = list(result)
|
||||||
|
assert completions == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_initial_setup_with_commands() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
|
||||||
|
completer.initial_setup({"start", "stop", "status"})
|
||||||
|
|
||||||
|
assert completer._session is not None
|
||||||
|
assert completer._fallback_mode is False
|
||||||
|
mock_session.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_initial_setup_with_history_file() -> None:
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
|
||||||
|
history_file = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
completer = AutoCompleter(history_filename=history_file)
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession'), \
|
||||||
|
patch('argenta.app.autocompleter.entity.ThreadedHistory') as mock_threaded_history:
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._session is not None
|
||||||
|
assert completer._fallback_mode is False
|
||||||
|
mock_threaded_history.assert_called_once()
|
||||||
|
finally:
|
||||||
|
if os.path.exists(history_file):
|
||||||
|
os.unlink(history_file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_initial_setup_without_history_file() -> None:
|
||||||
|
completer = AutoCompleter(history_filename=None)
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession'), \
|
||||||
|
patch('argenta.app.autocompleter.entity.InMemoryHistory') as mock_in_memory:
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._session is not None
|
||||||
|
assert completer._fallback_mode is False
|
||||||
|
mock_in_memory.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None:
|
||||||
|
completer = AutoCompleter(autocomplete_button="c-space")
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession'):
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._session is not None
|
||||||
|
assert completer.autocomplete_button == "c-space"
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_initial_setup_without_auto_suggestions() -> None:
|
||||||
|
completer = AutoCompleter(auto_suggestions=False)
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._session is not None
|
||||||
|
call_kwargs = mock_session.call_args[1]
|
||||||
|
assert call_kwargs['auto_suggest'] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_initial_setup_without_command_highlighting() -> None:
|
||||||
|
completer = AutoCompleter(command_highlighting=False)
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._session is not None
|
||||||
|
call_kwargs = mock_session.call_args[1]
|
||||||
|
assert call_kwargs['style'] is None
|
||||||
|
assert call_kwargs['lexer'] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_key_binding_handler_with_complete_state() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
captured_handler: Callable[[Any], None] | None = 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"})
|
||||||
|
|
||||||
|
assert captured_handler is not None
|
||||||
|
|
||||||
|
mock_event = MagicMock()
|
||||||
|
mock_buff = MagicMock()
|
||||||
|
mock_buff.complete_state = True
|
||||||
|
mock_event.app.current_buffer = mock_buff
|
||||||
|
|
||||||
|
captured_handler(mock_event)
|
||||||
|
|
||||||
|
mock_buff.complete_next.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_key_binding_handler_no_completions() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
captured_handler: Callable[[Any], None] | None = 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_buff = MagicMock()
|
||||||
|
mock_buff.complete_state = False
|
||||||
|
mock_completer = MagicMock()
|
||||||
|
mock_completer.get_completions.return_value = iter([])
|
||||||
|
mock_buff.completer = mock_completer
|
||||||
|
mock_event.app.current_buffer = mock_buff
|
||||||
|
|
||||||
|
assert captured_handler is not None
|
||||||
|
captured_handler(mock_event)
|
||||||
|
|
||||||
|
mock_buff.start_completion.assert_not_called()
|
||||||
|
mock_buff.apply_completion.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_key_binding_handler_single_completion() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
captured_handler: Callable[[Any], None] | None = 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_buff = MagicMock()
|
||||||
|
mock_buff.complete_state = False
|
||||||
|
mock_completion = MagicMock()
|
||||||
|
mock_completer = MagicMock()
|
||||||
|
mock_completer.get_completions.return_value = iter([mock_completion])
|
||||||
|
mock_buff.completer = mock_completer
|
||||||
|
mock_event.app.current_buffer = mock_buff
|
||||||
|
|
||||||
|
assert captured_handler is not None
|
||||||
|
captured_handler(mock_event)
|
||||||
|
|
||||||
|
mock_buff.apply_completion.assert_called_once_with(mock_completion)
|
||||||
|
mock_buff.start_completion.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_key_binding_handler_multiple_completions() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
captured_handler: Callable[[Any], None] | None = 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_buff = MagicMock()
|
||||||
|
mock_buff.complete_state = False
|
||||||
|
mock_completion1 = MagicMock()
|
||||||
|
mock_completion2 = MagicMock()
|
||||||
|
mock_completer = MagicMock()
|
||||||
|
mock_completer.get_completions.return_value = iter([mock_completion1, mock_completion2])
|
||||||
|
mock_buff.completer = mock_completer
|
||||||
|
mock_event.app.current_buffer = mock_buff
|
||||||
|
|
||||||
|
assert captured_handler is not None
|
||||||
|
captured_handler(mock_event)
|
||||||
|
|
||||||
|
mock_buff.start_completion.assert_called_once_with(select_first=False)
|
||||||
|
mock_buff.apply_completion.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_prompt_in_fallback_mode_with_string() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=False):
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._fallback_mode is True
|
||||||
|
|
||||||
|
with patch('builtins.input', return_value='test input'):
|
||||||
|
result = completer.prompt(">>> ")
|
||||||
|
|
||||||
|
assert result == 'test input'
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_prompt_in_fallback_mode_with_html() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=False):
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._fallback_mode is True
|
||||||
|
|
||||||
|
with patch('builtins.input', return_value='test input'):
|
||||||
|
result = completer.prompt(HTML("<b>>>> </b>"))
|
||||||
|
|
||||||
|
assert result == 'test input'
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_prompt_with_html_in_normal_mode() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
mock_session = MagicMock()
|
||||||
|
mock_session.prompt.return_value = 'test result'
|
||||||
|
completer._session = mock_session
|
||||||
|
completer._fallback_mode = False
|
||||||
|
|
||||||
|
html_prompt = HTML("<b>>>> </b>")
|
||||||
|
result = completer.prompt(html_prompt)
|
||||||
|
|
||||||
|
assert result == 'test result'
|
||||||
|
mock_session.prompt.assert_called_once()
|
||||||
|
call_args = mock_session.prompt.call_args
|
||||||
|
assert call_args[0][0] == html_prompt
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_prompt_with_string_in_normal_mode() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
mock_session = MagicMock()
|
||||||
|
mock_session.prompt.return_value = 'test result'
|
||||||
|
completer._session = mock_session
|
||||||
|
completer._fallback_mode = False
|
||||||
|
|
||||||
|
result = completer.prompt(">>> ")
|
||||||
|
|
||||||
|
assert result == 'test result'
|
||||||
|
mock_session.prompt.assert_called_once()
|
||||||
|
call_args = mock_session.prompt.call_args
|
||||||
|
assert isinstance(call_args[0][0], HTML)
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ def test_parse_raises_error_for_empty_command() -> None:
|
|||||||
InputCommand.parse('')
|
InputCommand.parse('')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_raises_error_slash_on_the_end() -> None:
|
||||||
|
with pytest.raises(UnprocessedInputFlagException):
|
||||||
|
InputCommand.parse('ssh --host 192.168.0.3\\')
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Tests for flag validation - valid flags
|
# Tests for flag validation - valid flags
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -164,6 +164,49 @@ def test_input_flags_get_by_name_returns_none_for_missing_flag() -> None:
|
|||||||
assert input_flags.get_flag_by_name('case') is None
|
assert input_flags.get_flag_by_name('case') is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_input_flags_get_by_name_with_status_finds_matching_flag() -> None:
|
||||||
|
from argenta.command.flag import ValidationStatus
|
||||||
|
|
||||||
|
flag1 = InputFlag(name='test', input_value='valid', status=ValidationStatus.VALID)
|
||||||
|
flag2 = InputFlag(name='other', input_value='invalid', status=ValidationStatus.INVALID)
|
||||||
|
input_flags = InputFlags([flag1, flag2])
|
||||||
|
|
||||||
|
result = input_flags.get_flag_by_name('test', with_status=ValidationStatus.VALID)
|
||||||
|
assert result == flag1
|
||||||
|
|
||||||
|
|
||||||
|
def test_input_flags_get_by_name_with_status_returns_none_when_status_mismatch() -> None:
|
||||||
|
from argenta.command.flag import ValidationStatus
|
||||||
|
|
||||||
|
flag = InputFlag(name='test', input_value='value', status=ValidationStatus.VALID)
|
||||||
|
input_flags = InputFlags([flag])
|
||||||
|
|
||||||
|
result = input_flags.get_flag_by_name('test', with_status=ValidationStatus.INVALID)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_input_flags_get_by_name_with_status_returns_default_when_not_found() -> None:
|
||||||
|
from argenta.command.flag import ValidationStatus
|
||||||
|
|
||||||
|
flag = InputFlag(name='test', input_value='value', status=ValidationStatus.VALID)
|
||||||
|
input_flags = InputFlags([flag])
|
||||||
|
|
||||||
|
result = input_flags.get_flag_by_name('missing', with_status=ValidationStatus.VALID, default='default_value')
|
||||||
|
assert result == 'default_value'
|
||||||
|
|
||||||
|
|
||||||
|
def test_input_flags_get_by_name_with_status_filters_by_both_name_and_status() -> None:
|
||||||
|
from argenta.command.flag import ValidationStatus
|
||||||
|
|
||||||
|
flag1 = InputFlag(name='test', input_value='value1', status=ValidationStatus.VALID)
|
||||||
|
flag2 = InputFlag(name='test', input_value='value2', status=ValidationStatus.INVALID)
|
||||||
|
flag3 = InputFlag(name='other', input_value='value3', status=ValidationStatus.VALID)
|
||||||
|
input_flags = InputFlags([flag1, flag2, flag3])
|
||||||
|
|
||||||
|
result = input_flags.get_flag_by_name('test', with_status=ValidationStatus.INVALID)
|
||||||
|
assert result == flag2
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Tests for InputFlags collection - equality and containment
|
# Tests for InputFlags collection - equality and containment
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -248,6 +248,17 @@ def test_finds_appropriate_handler_executes_handler_with_flags_by_alias(capsys:
|
|||||||
assert "Hello World!" in output.out
|
assert "Hello World!" in output.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_finds_appropriate_handler_raises_runtime_error_when_handler_not_found() -> None:
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
@router.command('hello')
|
||||||
|
def handler(_res: Response) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="Handler for 'unknown' command not found. Panic!"):
|
||||||
|
router.finds_appropriate_handler(InputCommand('unknown'))
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Tests for alias and trigger collision detection
|
# Tests for alias and trigger collision detection
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -141,3 +141,15 @@ class TestViewer:
|
|||||||
|
|
||||||
mock_output_generator.assert_called_once()
|
mock_output_generator.assert_called_once()
|
||||||
assert mock_printer.call_count >= 2
|
assert mock_printer.call_count >= 2
|
||||||
|
|
||||||
|
def test_view_framed_text_with_unimplemented_dividing_line(self, mock_printer: Mock, mock_output_generator: Mock):
|
||||||
|
class NotImplementedDividingLine:
|
||||||
|
pass
|
||||||
|
|
||||||
|
renderer = PlainRenderer()
|
||||||
|
dividing_line = NotImplementedDividingLine()
|
||||||
|
viewer = Viewer(mock_printer, renderer, dividing_line, False)
|
||||||
|
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
viewer.view_framed_text_from_generator(mock_output_generator, is_stdout_redirected_by_router=True)
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,22 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "esbonio" },
|
||||||
|
{ name = "isort" },
|
||||||
|
{ name = "mypy" },
|
||||||
|
{ name = "pyfakefs" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-cov" },
|
||||||
|
{ name = "pytest-mock" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
{ name = "scriv" },
|
||||||
|
{ name = "shibuya" },
|
||||||
|
{ name = "sphinx" },
|
||||||
|
{ name = "sphinx-autobuild" },
|
||||||
|
{ name = "sphinx-intl" },
|
||||||
|
{ name = "wemake-python-styleguide" },
|
||||||
|
]
|
||||||
docs = [
|
docs = [
|
||||||
{ name = "esbonio" },
|
{ name = "esbonio" },
|
||||||
{ name = "shibuya" },
|
{ name = "shibuya" },
|
||||||
@@ -85,6 +101,22 @@ requires-dist = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "esbonio", specifier = ">=1.0.0" },
|
||||||
|
{ name = "isort", specifier = ">=7.0.0" },
|
||||||
|
{ name = "mypy", specifier = ">=1.14.1" },
|
||||||
|
{ name = "pyfakefs", specifier = ">=5.5.0" },
|
||||||
|
{ name = "pytest", specifier = ">=8.3.2" },
|
||||||
|
{ name = "pytest-cov", specifier = ">=7.0.0" },
|
||||||
|
{ name = "pytest-mock", specifier = ">=3.15.1" },
|
||||||
|
{ name = "ruff", specifier = ">=0.12.12" },
|
||||||
|
{ name = "scriv", specifier = ">=1.8.0" },
|
||||||
|
{ name = "shibuya", specifier = ">=2025.9.25" },
|
||||||
|
{ name = "sphinx", specifier = ">=8.2.3" },
|
||||||
|
{ name = "sphinx-autobuild", specifier = ">=2025.8.25" },
|
||||||
|
{ name = "sphinx-intl", specifier = ">=2.3.2" },
|
||||||
|
{ name = "wemake-python-styleguide", specifier = ">=0.17.0" },
|
||||||
|
]
|
||||||
docs = [
|
docs = [
|
||||||
{ name = "esbonio", specifier = ">=1.0.0" },
|
{ name = "esbonio", specifier = ">=1.0.0" },
|
||||||
{ name = "shibuya", specifier = ">=2025.9.25" },
|
{ name = "shibuya", specifier = ">=2025.9.25" },
|
||||||
@@ -228,6 +260,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
|
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click-log"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/32/32/228be4f971e4bd556c33d52a22682bfe318ffe57a1ddb7a546f347a90260/click-log-0.4.0.tar.gz", hash = "sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975", size = 9985, upload-time = "2022-03-13T11:10:15.262Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl", hash = "sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756", size = 4273, upload-time = "2022-03-13T11:10:17.594Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -1208,6 +1252,23 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c6/2a/65880dfd0e13f7f13a775998f34703674a4554906167dce02daf7865b954/ruff-0.14.0-py3-none-win_arm64.whl", hash = "sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730", size = 12565142, upload-time = "2025-10-07T18:21:53.577Z" },
|
{ url = "https://files.pythonhosted.org/packages/c6/2a/65880dfd0e13f7f13a775998f34703674a4554906167dce02daf7865b954/ruff-0.14.0-py3-none-win_arm64.whl", hash = "sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730", size = 12565142, upload-time = "2025-10-07T18:21:53.577Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scriv"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "click-log" },
|
||||||
|
{ name = "jinja2" },
|
||||||
|
{ name = "markdown-it-py" },
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/14/9a/2ef2209e0672b264a2f2574dc88ea3cd9cfc9adfecbfd3165a900980ec8c/scriv-1.8.0.tar.gz", hash = "sha256:7b1a105dd411ac541998250fc8594742419f94cee984ca1257c5ebf5af21918b", size = 98160, upload-time = "2025-12-30T00:01:10.13Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/e7/062480ede84ecb56ee0f8f2e5b5a3b2a5bceeb73bbdf909d3c13f5438749/scriv-1.8.0-py3-none-any.whl", hash = "sha256:f00f51325b2f4bc96b16fbb1239d4ab577cc2422301a5dd4f5f9378aae2549e0", size = 39085, upload-time = "2025-12-30T00:01:08.599Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shibuya"
|
name = "shibuya"
|
||||||
version = "2025.9.25"
|
version = "2025.9.25"
|
||||||
|
|||||||
Reference in New Issue
Block a user