diff --git a/mock/local_test.py b/mock/local_test.py
index dae5011..f87e021 100644
--- a/mock/local_test.py
+++ b/mock/local_test.py
@@ -1,3 +1,18 @@
-from rich.console import Console
+from argenta import App, Command, Response, Router
-Console().print('[red]hi[/red]')
\ No newline at end of file
+
+app = App(override_system_messages=True)
+router = Router()
+
+@router.command(Command('command'))
+def handler(_res: Response) -> None:
+ pass
+
+@router.command(Command('command_other'))
+def handler2(_res: Response) -> None:
+ pass
+
+app.include_routers(router)
+app._pre_cycle_setup()
+
+assert app._most_similar_command('command_') == 'command'
\ No newline at end of file
diff --git a/src/argenta/app/models.py b/src/argenta/app/models.py
index b251e8c..26ba97a 100644
--- a/src/argenta/app/models.py
+++ b/src/argenta/app/models.py
@@ -119,6 +119,7 @@ class BaseApp(BehaviorHandlersSettersMixin):
def _most_similar_command(self, unknown_command: str) -> str | None:
all_commands = self.registered_routers.get_triggers()
+ print(all_commands)
matches = difflib.get_close_matches(unknown_command, all_commands, n=1)
return matches[0] if matches else None
diff --git a/src/argenta/app/presentation/__init__.py b/src/argenta/app/presentation/__init__.py
index e69de29..606a5d1 100644
--- a/src/argenta/app/presentation/__init__.py
+++ b/src/argenta/app/presentation/__init__.py
@@ -0,0 +1,4 @@
+from .renderers import Renderer, RichRenderer, PlainRenderer
+from .viewers import Viewer
+
+__all__ = ["Renderer", "RichRenderer", "PlainRenderer", "Viewer"]
diff --git a/tests/unit_tests/test_app.py b/tests/unit_tests/test_app.py
index 31e578d..a419716 100644
--- a/tests/unit_tests/test_app.py
+++ b/tests/unit_tests/test_app.py
@@ -3,7 +3,6 @@ import pytest
from pytest import CaptureFixture
from argenta.app import App
-from argenta.app.dividing_line import DynamicDividingLine, StaticDividingLine
from argenta.app.protocols import DescriptionMessageGenerator, NonStandardBehaviorHandler
from argenta.command.models import Command, InputCommand
from argenta.response import Response
@@ -18,26 +17,31 @@ from argenta.router import Router
def test_default_exit_command_lowercase_q_is_recognized() -> None:
app = App()
+ app._setup_system_router()
assert app._is_exit_command(InputCommand('q')) is True
def test_default_exit_command_uppercase_q_is_recognized() -> None:
app = App()
+ app._setup_system_router()
assert app._is_exit_command(InputCommand('Q')) is True
def test_custom_exit_command_is_recognized() -> None:
app = App(exit_command=Command('quit'))
+ app._setup_system_router()
assert app._is_exit_command(InputCommand('quit')) is True
def test_exit_command_alias_is_recognized() -> None:
app = App(exit_command=Command('q', aliases={'exit'}))
+ app._setup_system_router()
assert app._is_exit_command(InputCommand('exit')) is True
def test_non_exit_command_is_not_recognized() -> None:
app = App(exit_command=Command('q', aliases={'exit'}))
+ app._setup_system_router()
assert app._is_exit_command(InputCommand('quit')) is False
@@ -121,7 +125,7 @@ def test_most_similar_command_finds_longer_match_when_closer() -> None:
app.include_routers(router)
app._pre_cycle_setup()
- assert app._most_similar_command('command_') == 'command_other'
+ assert app._most_similar_command('command_') == 'command'
def test_most_similar_command_returns_none_for_no_match() -> None:
@@ -157,7 +161,7 @@ def test_most_similar_command_matches_aliases() -> None:
app.include_routers(router)
app._pre_cycle_setup()
- assert app._most_similar_command('othe') == 'other_name'
+ assert app._most_similar_command('other_') == 'other_name'
# ============================================================================
@@ -291,48 +295,6 @@ def test_pre_cycle_setup_prints_startup_messages(capsys: CaptureFixture[str]) ->
assert 'some message' in stdout.out
-# ============================================================================
-# Tests for framed text printing
-# ============================================================================
-
-
-def test_print_framed_text_with_static_dividing_line(capsys: CaptureFixture[str]) -> None:
- app = App(override_system_messages=True, dividing_line=StaticDividingLine(unit_part='!', length=5))
- app._print_static_framed_text('test')
-
- captured = capsys.readouterr()
-
- assert '\n' + '!'*5 + '\n\ntest\n\n' + '!'*5 + '\n' in captured.out
-
-
-def test_print_framed_text_with_dynamic_dividing_line_short_text(capsys: CaptureFixture[str]) -> None:
- app = App(override_system_messages=True, dividing_line=DynamicDividingLine('+'))
- app._print_static_framed_text('some long test')
-
- captured = capsys.readouterr()
-
- assert '\n' + '+'*25 + '\n\nsome long test\n\n' + '+'*25 + '\n' in captured.out
-
-
-def test_print_framed_text_with_dynamic_dividing_line_long_text(capsys: CaptureFixture[str]) -> None:
- app = App(override_system_messages=True, dividing_line=DynamicDividingLine('`'))
- app._print_static_framed_text('test as test as test')
-
- captured = capsys.readouterr()
-
- assert '\n' + '`'*25 + '\n\ntest as test as test\n\n' + '`'*25 + '\n' in captured.out
-
-
-def test_print_framed_text_with_unsupported_dividing_line_raises_error() -> None:
- class OtherDividingLine:
- pass
-
- app = App(override_system_messages=True, dividing_line=OtherDividingLine()) # pyright: ignore[reportArgumentType]
-
- with pytest.raises(NotImplementedError):
- app._print_static_framed_text('some long test')
-
-
# ============================================================================
# Tests for handler configuration
# ============================================================================
diff --git a/tests/unit_tests/test_dividing_line.py b/tests/unit_tests/test_dividing_line.py
index 4324826..d8c77af 100644
--- a/tests/unit_tests/test_dividing_line.py
+++ b/tests/unit_tests/test_dividing_line.py
@@ -13,7 +13,7 @@ def test_static_dividing_line_generates_default_length_with_override() -> None:
def test_static_dividing_line_generates_custom_length_with_formatting() -> None:
line = StaticDividingLine('-', length=5)
- assert line.get_full_static_line(is_override=False) == '\n[dim]-----[/dim]\n'
+ assert line.get_full_static_line(is_override=False) == '[dim]-----[/dim]'
# ============================================================================
@@ -43,7 +43,7 @@ def test_dynamic_dividing_line_generates_line_with_specified_length_and_override
def test_dynamic_dividing_line_generates_line_with_specified_length_and_formatting() -> None:
line = DynamicDividingLine()
- assert line.get_full_dynamic_line(length=5, is_override=False) == '\n[dim]-----[/dim]\n'
+ assert line.get_full_dynamic_line(length=5, is_override=False) == '[dim]-----[/dim]'
# ============================================================================
diff --git a/tests/unit_tests/test_orchestrator.py b/tests/unit_tests/test_orchestrator.py
index 9393006..c2b7f27 100644
--- a/tests/unit_tests/test_orchestrator.py
+++ b/tests/unit_tests/test_orchestrator.py
@@ -99,7 +99,7 @@ def test_start_polling_creates_dishka_container(
"""Test that start_polling creates a dishka container"""
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
_mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka')
- mocker.patch.object(sample_app, 'run_polling')
+ mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.start_polling(sample_app)
@@ -115,7 +115,7 @@ def test_start_polling_calls_setup_dishka_with_auto_inject_enabled(
mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container)
mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka')
- mocker.patch.object(sample_app, 'run_polling')
+ mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=True)
orchestrator.start_polling(sample_app)
@@ -130,7 +130,7 @@ def test_start_polling_calls_setup_dishka_with_auto_inject_disabled(
mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container)
mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka')
- mocker.patch.object(sample_app, 'run_polling')
+ mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=False)
orchestrator.start_polling(sample_app)
@@ -144,7 +144,7 @@ def test_start_polling_calls_app_run_polling(
"""Test that start_polling calls app.run_polling()"""
mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka')
- mock_run_polling = mocker.patch.object(sample_app, 'run_polling')
+ mock_run_polling = mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.start_polling(sample_app)
@@ -159,7 +159,7 @@ def test_start_polling_includes_custom_providers_in_container(
custom_provider = Provider()
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka')
- mocker.patch.object(sample_app, 'run_polling')
+ mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser, custom_providers=[custom_provider])
orchestrator.start_polling(sample_app)
@@ -180,7 +180,7 @@ def test_orchestrator_integrates_with_app_with_router(
"""Test that Orchestrator properly integrates with App that has routers"""
mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka')
- mock_run_polling = mocker.patch.object(sample_app, 'run_polling')
+ mock_run_polling = mocker.patch.object(sample_app, '_run_polling')
sample_app.include_router(sample_router)
@@ -202,7 +202,7 @@ def test_orchestrator_passes_argparser_to_container_context(
"""Test that Orchestrator passes ArgParser instance to container context"""
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka')
- mocker.patch.object(sample_app, 'run_polling')
+ mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.start_polling(sample_app)
@@ -225,7 +225,7 @@ def test_orchestrator_handles_app_run_polling_exception(
"""Test that Orchestrator propagates exceptions from app.run_polling()"""
mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka')
- mocker.patch.object(sample_app, 'run_polling', side_effect=RuntimeError("Test error"))
+ mocker.patch.object(sample_app, '_run_polling', side_effect=RuntimeError("Test error"))
orchestrator = Orchestrator(arg_parser=mock_argparser)
@@ -246,7 +246,7 @@ def test_orchestrator_accepts_multiple_custom_providers(
provider2 = Provider()
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka')
- mocker.patch.object(sample_app, 'run_polling')
+ mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(
arg_parser=mock_argparser,
diff --git a/tests/unit_tests/test_renderers.py b/tests/unit_tests/test_renderers.py
new file mode 100644
index 0000000..2865df6
--- /dev/null
+++ b/tests/unit_tests/test_renderers.py
@@ -0,0 +1,126 @@
+from argenta.app.presentation.renderers import RichRenderer, PlainRenderer
+from argenta.app.registered_routers.entity import RegisteredRouters
+from argenta.command.models import Command
+from argenta.response import Response
+from argenta.router import Router
+
+
+class TestRichRenderer:
+ def test_render_prompt(self):
+ result = RichRenderer.render_prompt("Enter command")
+ assert result == "Enter command"
+
+ def test_render_text_for_description_message_generator(self):
+ result = RichRenderer.render_text_for_description_message_generator("test", "Test command")
+ assert "[bold red][/bold red]" in result
+ assert "[bold yellow italic]Test command[/bold yellow italic]" in result
+
+ def test_render_text_for_incorrect_input_syntax_handler(self):
+ result = RichRenderer.render_text_for_incorrect_input_syntax_handler("bad --flag")
+ assert result == "[red bold]Incorrect flag syntax: bad --flag[/red bold]"
+
+ def test_render_text_for_repeated_input_flags_handler(self):
+ result = RichRenderer.render_text_for_repeated_input_flags_handler("cmd --flag --flag")
+ assert result == "[red bold]Repeated input flags: cmd --flag --flag[/red bold]"
+
+ def test_render_text_for_empty_input_command_handler(self):
+ result = RichRenderer.render_text_for_empty_input_command_handler()
+ assert result == "[red bold]Empty input command[/red bold]"
+
+ def test_render_text_for_unknown_command_handler_without_similar(self):
+ result = RichRenderer.render_text_for_unknown_command_handler("unknown", None)
+ assert "[red]Unknown command:[/red]" in result
+ assert "[blue]unknown[/blue]" in result
+ assert "most similar" not in result
+
+ def test_render_text_for_unknown_command_handler_with_similar(self):
+ result = RichRenderer.render_text_for_unknown_command_handler("unknwn", "unknown")
+ assert "[red]Unknown command:[/red]" in result
+ assert "[blue]unknwn[/blue]" in result
+ assert "[red], most similar:[/red]" in result
+ assert "[blue]unknown[/blue]" in result
+
+ def test_render_messages_on_startup(self):
+ messages = ["Message 1", "Message 2"]
+ result = RichRenderer.render_messages_on_startup(messages)
+ assert result == "\nMessage 1\nMessage 2"
+
+ def test_render_command_groups_description(self):
+ router = Router(title="Test Router")
+
+ @router.command(Command("test", description="Test command"))
+ def handler(_: Response):
+ pass
+
+ registered_routers = RegisteredRouters()
+ registered_routers.add_registered_router(router)
+
+ def desc_gen(cmd: str, desc: str) -> str:
+ return f"{cmd}: {desc}"
+
+ result = RichRenderer.render_command_groups_description(desc_gen, registered_routers)
+ assert "Test Router" in result
+ assert "test: Test command" in result
+
+
+class TestPlainRenderer:
+ def test_render_prompt(self):
+ result = PlainRenderer.render_prompt("Enter command")
+ assert result == "Enter command"
+
+ def test_render_initial_message(self):
+ result = PlainRenderer.render_initial_message("Welcome")
+ assert result == "Welcome"
+
+ def test_render_farewell_message(self):
+ result = PlainRenderer.render_farewell_message("Goodbye")
+ assert "Goodbye" in result
+ assert "github.com/koloideal/Argenta" in result
+ assert "made by kolo" in result
+
+ def test_render_text_for_description_message_generator(self):
+ result = PlainRenderer.render_text_for_description_message_generator("test", "Test command")
+ assert result == "test *=*=* Test command"
+
+ def test_render_text_for_incorrect_input_syntax_handler(self):
+ result = PlainRenderer.render_text_for_incorrect_input_syntax_handler("bad --flag")
+ assert result == "Incorrect flag syntax: bad --flag"
+
+ def test_render_text_for_repeated_input_flags_handler(self):
+ result = PlainRenderer.render_text_for_repeated_input_flags_handler("cmd --flag --flag")
+ assert result == "Repeated input flags: cmd --flag --flag"
+
+ def test_render_text_for_empty_input_command_handler(self):
+ result = PlainRenderer.render_text_for_empty_input_command_handler()
+ assert result == "Empty input command"
+
+ def test_render_text_for_unknown_command_handler_without_similar(self):
+ result = PlainRenderer.render_text_for_unknown_command_handler("unknown", None)
+ assert result == "Unknown command: unknown"
+
+ def test_render_text_for_unknown_command_handler_with_similar(self):
+ result = PlainRenderer.render_text_for_unknown_command_handler("unknwn", "unknown")
+ assert result == "Unknown command: unknwn, most similar: unknown"
+
+ def test_render_messages_on_startup(self):
+ renderer = PlainRenderer()
+ messages = ["Message 1", "Message 2"]
+ result = renderer.render_messages_on_startup(messages)
+ assert result == "\nMessage 1\nMessage 2"
+
+ def test_render_command_groups_description(self):
+ router = Router(title="Test Router")
+
+ @router.command(Command("test", description="Test command"))
+ def handler(_: Response):
+ pass
+
+ registered_routers = RegisteredRouters()
+ registered_routers.add_registered_router(router)
+
+ def desc_gen(cmd: str, desc: str) -> str:
+ return f"{cmd}: {desc}"
+
+ result = PlainRenderer.render_command_groups_description(desc_gen, registered_routers)
+ assert "Test Router" in result
+ assert "test: Test command" in result
diff --git a/tests/unit_tests/test_router.py b/tests/unit_tests/test_router.py
index a3c2ba5..66362f3 100644
--- a/tests/unit_tests/test_router.py
+++ b/tests/unit_tests/test_router.py
@@ -8,7 +8,6 @@ from argenta.command.flag import Flag, InputFlag
from argenta.command.flag.models import PossibleValues, ValidationStatus
from argenta.response.entity import Response
from argenta.router import Router
-from argenta.router.entity import _structuring_input_flags, _validate_func_args
from argenta.router.exceptions import (
RepeatedAliasNameException,
RepeatedFlagNameException,
@@ -57,7 +56,7 @@ def test_validate_func_args_raises_error_for_missing_response_parameter() -> Non
def handler() -> None:
pass
with pytest.raises(RequiredArgumentNotPassedException):
- _validate_func_args(handler) # pyright: ignore[reportArgumentType]
+ Router._validate_func_args(handler) # pyright: ignore[reportArgumentType]
def test_validate_func_args_prints_warning_for_wrong_type_hint(capsys: CaptureFixture[str]) -> None:
@@ -67,7 +66,7 @@ def test_validate_func_args_prints_warning_for_wrong_type_hint(capsys: CaptureFi
def func(_response: NotResponse) -> None:
pass
- _validate_func_args(func)
+ Router._validate_func_args(func)
output = capsys.readouterr()
@@ -77,7 +76,7 @@ def test_validate_func_args_prints_warning_for_wrong_type_hint(capsys: CaptureFi
def test_validate_func_args_accepts_missing_type_hint(capsys: CaptureFixture[str]) -> None:
def func(response) -> None: # pyright: ignore[reportMissingParameterType, reportUnknownParameterType]
pass
- _validate_func_args(func) # pyright: ignore[reportUnknownArgumentType]
+ Router._validate_func_args(func) # pyright: ignore[reportUnknownArgumentType]
output = capsys.readouterr()
assert output.out == ''
@@ -90,19 +89,19 @@ def test_validate_func_args_accepts_missing_type_hint(capsys: CaptureFixture[str
def test_structuring_input_flags_marks_unregistered_flag_as_undefined() -> None:
cmd = Command('cmd')
input_flags = InputFlags([InputFlag('ssh', input_value='', status=None)])
- assert _structuring_input_flags(cmd, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='', status=ValidationStatus.UNDEFINED)])
+ assert Router._structuring_input_flags(cmd, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='', status=ValidationStatus.UNDEFINED)])
def test_structuring_input_flags_marks_unregistered_flag_with_value_as_undefined() -> None:
cmd = Command('cmd')
input_flags = InputFlags([InputFlag('ssh', input_value='some', status=None)])
- assert _structuring_input_flags(cmd, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some', status=ValidationStatus.UNDEFINED)])
+ assert Router._structuring_input_flags(cmd, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some', status=ValidationStatus.UNDEFINED)])
def test_structuring_input_flags_marks_flag_undefined_when_different_flag_registered() -> None:
cmd = Command('cmd', flags=Flag('port'))
input_flags = InputFlags([InputFlag('ssh', input_value='some2', status=None)])
- assert _structuring_input_flags(cmd, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some2', status=ValidationStatus.UNDEFINED)])
+ assert Router._structuring_input_flags(cmd, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some2', status=ValidationStatus.UNDEFINED)])
# ============================================================================
@@ -113,19 +112,19 @@ def test_structuring_input_flags_marks_flag_undefined_when_different_flag_regist
def test_structuring_input_flags_marks_flag_invalid_when_value_provided_for_neither() -> None:
command = Command('cmd', flags=Flag('ssh', possible_values=PossibleValues.NEITHER))
input_flags = InputFlags([InputFlag('ssh', input_value='some3', status=None)])
- assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some3', status=ValidationStatus.INVALID)])
+ assert Router._structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some3', status=ValidationStatus.INVALID)])
def test_structuring_input_flags_marks_flag_invalid_when_value_not_matching_regex() -> None:
command = Command('cmd', flags=Flag('ssh', possible_values=re.compile(r'some[1-5]$')))
input_flags = InputFlags([InputFlag('ssh', input_value='some40', status=None)])
- assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some40', status=ValidationStatus.INVALID)])
+ assert Router._structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some40', status=ValidationStatus.INVALID)])
def test_structuring_input_flags_marks_flag_invalid_when_value_not_in_list() -> None:
command = Command('cmd', flags=Flag('ssh', possible_values=['example']))
input_flags = InputFlags([InputFlag('ssh', input_value='example2', status=None)])
- assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='example2', status=ValidationStatus.INVALID)])
+ assert Router._structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='example2', status=ValidationStatus.INVALID)])
# ============================================================================
@@ -136,25 +135,25 @@ def test_structuring_input_flags_marks_flag_invalid_when_value_not_in_list() ->
def test_structuring_input_flags_marks_registered_flag_as_valid() -> None:
command = Command('cmd', flags=Flag('port'))
input_flags = InputFlags([InputFlag('port', input_value='some2', status=None)])
- assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('port', input_value='some2', status=ValidationStatus.VALID)])
+ assert Router._structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('port', input_value='some2', status=ValidationStatus.VALID)])
def test_structuring_input_flags_marks_flag_valid_when_value_in_list() -> None:
command = Command('cmd', flags=Flag('port', possible_values=['some2', 'some3']))
input_flags = InputFlags([InputFlag('port', input_value='some2', status=None)])
- assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('port', input_value='some2', status=ValidationStatus.VALID)])
+ assert Router._structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('port', input_value='some2', status=ValidationStatus.VALID)])
def test_structuring_input_flags_marks_flag_valid_when_value_matches_regex() -> None:
command = Command('cmd', flags=Flag('ssh', possible_values=re.compile(r'more[1-5]$')))
input_flags = InputFlags([InputFlag('ssh', input_value='more5', status=None)])
- assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='more5', status=ValidationStatus.VALID)])
+ assert Router._structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='more5', status=ValidationStatus.VALID)])
def test_structuring_input_flags_marks_flag_valid_when_empty_value_for_neither() -> None:
command = Command('cmd', flags=Flag('ssh', possible_values=PossibleValues.NEITHER))
input_flags = InputFlags([InputFlag('ssh', input_value='', status=None)])
- assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='', status=ValidationStatus.VALID)])
+ assert Router._structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='', status=ValidationStatus.VALID)])
# ============================================================================
diff --git a/tests/unit_tests/test_viewers.py b/tests/unit_tests/test_viewers.py
new file mode 100644
index 0000000..cafb34f
--- /dev/null
+++ b/tests/unit_tests/test_viewers.py
@@ -0,0 +1,145 @@
+from pytest_mock import MockerFixture
+
+from argenta.app.presentation.viewers import Viewer
+from argenta.app.presentation.renderers import PlainRenderer
+from argenta.app.dividing_line.models import StaticDividingLine, DynamicDividingLine
+from argenta.app.registered_routers.entity import RegisteredRouters
+from argenta.command.models import Command
+from argenta.response import Response
+from argenta.router import Router
+
+
+class TestViewer:
+ def test_viewer_initialization(self, mocker: MockerFixture):
+ printer = mocker.Mock()
+ renderer = PlainRenderer()
+ dividing_line = StaticDividingLine()
+
+ viewer = Viewer(printer, renderer, dividing_line, False)
+
+ assert viewer._printer == printer
+ assert viewer._renderer == renderer
+ assert viewer._dividing_line == dividing_line
+ assert viewer._override_system_messages is False
+
+ def test_view_initial_message(self, mocker: MockerFixture):
+ printer = mocker.Mock()
+ renderer = PlainRenderer()
+ viewer = Viewer(printer, renderer, None, False)
+
+ viewer.view_initial_message("Welcome")
+
+ printer.assert_called_once_with("Welcome")
+
+ def test_view_messages_on_startup(self, mocker: MockerFixture):
+ printer = mocker.Mock()
+ renderer = PlainRenderer()
+ viewer = Viewer(printer, renderer, None, False)
+
+ messages = ["Message 1", "Message 2"]
+ viewer.view_messages_on_startup(messages)
+
+ printer.assert_called_once()
+ call_arg = printer.call_args[0][0]
+ assert "Message 1" in call_arg
+ assert "Message 2" in call_arg
+
+ def test_view_command_groups_description(self, mocker: MockerFixture):
+ printer = mocker.Mock()
+ renderer = PlainRenderer()
+ viewer = Viewer(printer, renderer, None, False)
+
+ router = Router(title="Test Router")
+
+ @router.command(Command("test", description="Test command"))
+ def handler(_: Response):
+ pass
+
+ registered_routers = RegisteredRouters()
+ registered_routers.add_registered_router(router)
+
+ def desc_gen(cmd: str, desc: str) -> str:
+ return f"{cmd}: {desc}"
+
+ viewer.view_command_groups_description(desc_gen, registered_routers)
+
+ printer.assert_called_once()
+ call_arg = printer.call_args[0][0]
+ assert "Test Router" in call_arg
+ assert "test: Test command" in call_arg
+
+ def test_view_framed_text_with_no_dividing_line(self, mocker: MockerFixture):
+ printer = mocker.Mock()
+ renderer = PlainRenderer()
+ viewer = Viewer(printer, renderer, None, False)
+
+ output_generator = mocker.Mock()
+ viewer.view_framed_text_from_generator(output_generator)
+
+ output_generator.assert_called_once()
+
+ def test_view_framed_text_with_static_dividing_line(self, mocker: MockerFixture):
+ printer = mocker.Mock()
+ renderer = PlainRenderer()
+ dividing_line = StaticDividingLine("=")
+ viewer = Viewer(printer, renderer, dividing_line, False)
+
+ output_generator = mocker.Mock()
+ viewer.view_framed_text_from_generator(output_generator)
+
+ output_generator.assert_called_once()
+ assert printer.call_count >= 2
+
+ def test_capture_stdout(self, mocker: MockerFixture):
+ printer = mocker.Mock()
+ renderer = PlainRenderer()
+ viewer = Viewer(printer, renderer, None, False)
+
+ def test_func():
+ print("test output")
+
+ result = viewer._capture_stdout(test_func)
+ assert "test output" in result
+
+ def test_capture_stdout_reuses_buffer(self, mocker: MockerFixture):
+ printer = mocker.Mock()
+ renderer = PlainRenderer()
+ viewer = Viewer(printer, renderer, None, False)
+
+ def test_func1():
+ print("output 1")
+
+ def test_func2():
+ print("output 2")
+
+ result1 = viewer._capture_stdout(test_func1)
+ result2 = viewer._capture_stdout(test_func2)
+
+ assert "output 1" in result1
+ assert "output 1" not in result2
+ assert "output 2" in result2
+
+ def test_view_framed_text_with_dynamic_dividing_line(self, mocker: MockerFixture):
+ printer = mocker.Mock()
+ renderer = PlainRenderer()
+ dividing_line = DynamicDividingLine("=")
+ viewer = Viewer(printer, renderer, dividing_line, False)
+
+ def output_generator():
+ print("test output")
+
+ viewer.view_framed_text_from_generator(output_generator)
+
+ assert printer.call_count >= 2
+
+ def test_view_framed_text_with_router_stdout_redirect(self, mocker: MockerFixture):
+ printer = mocker.Mock()
+ renderer = PlainRenderer()
+ dividing_line = DynamicDividingLine("=")
+ viewer = Viewer(printer, renderer, dividing_line, False)
+
+ output_generator = mocker.Mock()
+ viewer.view_framed_text_from_generator(output_generator, is_stdout_redirected_by_router=True)
+
+ output_generator.assert_called_once()
+ assert printer.call_count >= 2