extract presentation layer

This commit is contained in:
2026-01-30 20:52:52 +03:00
parent d03ce5061b
commit 5f6b3368e1
7 changed files with 239 additions and 161 deletions
-14
View File
@@ -1,14 +0,0 @@
from rich.console import Console
from argenta.app.presentation.renderers import RichRenderer, Renderer, PlainRenderer, RendererMixin
def main(rend: Renderer):
pass
def mm(tr) -> str | None:
pass
main(RichRenderer(print, mm))
main(PlainRenderer(print, mm))
main()
@@ -0,0 +1,62 @@
from rich.markup import escape
from argenta import Response
from argenta.app.presentation.renderers import Renderer
from argenta.app.protocols import (
NonStandardBehaviorHandler,
EmptyCommandHandler,
Printer,
MostSimilarCommandGetter,
DescriptionMessageGenerator,
)
from argenta.command import InputCommand
class BehaviorHandlersFabric:
def __init__(
self,
printer: Printer,
renderer: Renderer,
most_similar_command_getter: MostSimilarCommandGetter,
) -> None:
self._printer = printer
self._renderer = renderer
self._most_similar_command_getter = most_similar_command_getter
def generate_incorrect_input_syntax_handler(self) -> NonStandardBehaviorHandler[str]:
return lambda raw_command: self._printer(
self._renderer.render_text_for_incorrect_input_syntax_handler(
raw_command=escape(raw_command)
)
)
def generate_repeated_input_flags_handler(self) -> NonStandardBehaviorHandler[str]:
return lambda raw_command: self._printer(
self._renderer.render_text_for_repeated_input_flags_handler(
raw_command=escape(raw_command)
)
)
def generate_empty_input_command_handler(self) -> EmptyCommandHandler:
return lambda: self._printer(self._renderer.render_text_for_empty_input_command_handler())
def generate_unknown_command_handler(self) -> NonStandardBehaviorHandler[InputCommand]:
def unknown_command_handler(command: InputCommand) -> None:
command_trigger: str = command.trigger
most_similar_command_trigger: str | None = self._most_similar_command_getter(command_trigger)
self._printer(
self._renderer.render_text_for_unknown_command_handler(
command_trigger=command_trigger,
most_similar_command_trigger=most_similar_command_trigger
)
)
return unknown_command_handler
def generate_exit_command_handler(self, farewell_message: str) -> NonStandardBehaviorHandler[Response]:
return lambda _: self._printer(self._renderer.render_farewell_message(farewell_message))
def generate_description_message_generator(self) -> DescriptionMessageGenerator:
return lambda command, description: self._renderer.render_text_for_description_message_generator(
command=command,
description=description
)
+25 -38
View File
@@ -8,8 +8,10 @@ from typing import Callable, Never, TypeAlias
from rich.console import Console
from argenta.app.autocompleter import AutoCompleter
from argenta.app.behavior_handlers.entity import BehaviorHandlersFabric
from argenta.app.presentation.renderers import PlainRenderer, RichRenderer, Renderer
from argenta.app.dividing_line.models import DynamicDividingLine, StaticDividingLine
from argenta.app.presentation.viewers import Viewer
from argenta.app.protocols import (
DescriptionMessageGenerator,
EmptyCommandHandler,
@@ -61,24 +63,25 @@ class BaseApp:
self._messages_on_startup: list[str] = []
if self._override_system_messages:
self.view: Renderer = PlainRenderer(
print_func=self._print_func,
most_similar_command_getter=self._most_similar_command
)
self._renderer: Renderer = PlainRenderer()
else:
self.view: Renderer = RichRenderer(
print_func=self._print_func,
most_similar_command_getter=self._most_similar_command
self._renderer: Renderer = RichRenderer()
self._viewer: Viewer = Viewer(self._print_func, self._renderer)
self._handlers_fabric: BehaviorHandlersFabric = BehaviorHandlersFabric(
self._print_func,
self._renderer,
self._most_similar_command
)
self._initial_message: str = self.view.render_initial_message(initial_message)
self._farewell_message: str = self.view.render_farewell_message(farewell_message)
self._description_message_gen: DescriptionMessageGenerator = self.view.generate_formatted_description_message_gen()
self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = self.view.generate_formatted_incorrect_input_syntax_handler()
self._repeated_input_flags_handler: NonStandardBehaviorHandler[str] = self.view.generate_formatted_repeated_input_flags_handler()
self._empty_input_command_handler: EmptyCommandHandler = self.view.generate_formatted_empty_input_command_handler()
self._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = self.view.generate_formatted_unknown_command_handler()
self._exit_command_handler: NonStandardBehaviorHandler[Response] = self.view.generate_formatted_exit_command_handler(self._farewell_message)
self._initial_message: str = self._renderer.render_initial_message(initial_message)
self._farewell_message: str = self._renderer.render_farewell_message(farewell_message)
self._description_message_generator: DescriptionMessageGenerator = self._handlers_fabric.generate_description_message_generator()
self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = self._handlers_fabric.generate_incorrect_input_syntax_handler()
self._repeated_input_flags_handler: NonStandardBehaviorHandler[str] = self._handlers_fabric.generate_repeated_input_flags_handler()
self._empty_input_command_handler: EmptyCommandHandler = self._handlers_fabric.generate_empty_input_command_handler()
self._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = self._handlers_fabric.generate_unknown_command_handler()
self._exit_command_handler: NonStandardBehaviorHandler[Response] = self._handlers_fabric.generate_exit_command_handler(self._farewell_message)
def set_description_message_pattern(self, _: DescriptionMessageGenerator, /) -> None:
"""
@@ -86,7 +89,7 @@ class BaseApp:
:param _: output pattern of the available commands
:return: None
"""
self._description_message_gen = _
self._description_message_generator = _
def set_incorrect_input_syntax_handler(self, _: NonStandardBehaviorHandler[str], /) -> None:
"""
@@ -128,22 +131,6 @@ class BaseApp:
"""
self._exit_command_handler = _
def _print_command_group_description(self) -> None:
"""
Private. Prints the description of the available commands
:return: None
"""
for registered_router in self.registered_routers:
self._print_func("\n" + registered_router.title)
for command_handler in registered_router.command_handlers:
handled_command = command_handler.handled_command
self._print_func(
self._description_message_gen(
handled_command.trigger,
handled_command.description,
)
)
def _print_static_framed_text(self, text: str) -> None:
"""
Private. Outputs text by framing it in a static or dynamic split strip
@@ -278,10 +265,10 @@ class BaseApp:
self._autocompleter.initial_setup(self.registered_routers.get_triggers())
if self._messages_on_startup:
self._print_func()
self._viewer.view_messages_on_startup(self._messages_on_startup)
if not self._repeat_command_groups_printing:
self._print_command_group_description()
self._viewer.view_command_groups_description(self._description_message_generator, self.registered_routers)
def _process_exist_and_valid_command(self, input_command: InputCommand) -> None:
processing_router = self.registered_routers.get_router_by_trigger(input_command.trigger.lower())
@@ -290,6 +277,8 @@ class BaseApp:
raise RuntimeError(f"Router for '{input_command.trigger}' not found. Panic!")
match (self._dividing_line, processing_router.disable_redirect_stdout):
case (None, bool()):
processing_router.finds_appropriate_handler(input_command)
case (DynamicDividingLine(), False):
stdout_result = self._capture_stdout(lambda: processing_router.finds_appropriate_handler(input_command))
clear_text = _ANSI_ESCAPE_RE.sub("", stdout_result)
@@ -321,8 +310,6 @@ class BaseApp:
is_override=self._override_system_messages
)
)
case (None, bool()):
processing_router.finds_appropriate_handler(input_command)
case _:
raise NotImplementedError(f"Dividing line with type {self._dividing_line} is not implemented")
@@ -384,10 +371,10 @@ class App(BaseApp):
self._pre_cycle_setup()
while True:
if self._repeat_command_groups_printing:
self._print_command_group_description()
self._viewer.view_command_groups_description(self._description_message_generator, self.registered_routers)
print() # pre-prompt gap
raw_command: str = self._autocompleter.prompt(self.view.render_prompt(self._prompt))
raw_command: str = self._autocompleter.prompt(self._renderer.render_prompt(self._prompt))
print() # post-prompt gap
try:
+122 -106
View File
@@ -1,86 +1,56 @@
from abc import ABC, abstractmethod
from typing import Protocol, Iterable
from typing import Iterable, Protocol
from art import text2art
from rich.markup import escape
from argenta.app.protocols import DescriptionMessageGenerator
from argenta.app.registered_routers.entity import RegisteredRouters
from argenta.response.entity import Response
from argenta.command.models import InputCommand
from argenta.app.protocols import (
DescriptionMessageGenerator,
EmptyCommandHandler,
MostSimilarCommandGetter,
NonStandardBehaviorHandler,
Printer
)
class RendererMixin(ABC):
def __init__(self, print_func: Printer, most_similar_command_getter: MostSimilarCommandGetter) -> None:
self._print_func = print_func
self._most_similar_command_getter = most_similar_command_getter
self._cached_command_groups_description: str | None = None
@staticmethod
@abstractmethod
def generate_formatted_description_message_gen() -> DescriptionMessageGenerator:
raise NotImplementedError
@staticmethod
def render_messages_on_startup(messages: Iterable[str]) -> str:
return "\n".join(messages)
def render_command_groups_description(self, registered_routers: RegisteredRouters) -> str:
if self._cached_command_groups_description:
return self._cached_command_groups_description
command_groups_description = ""
for registered_router in registered_routers:
command_groups_description += "\n" + registered_router.title
for command_handler in registered_router.command_handlers:
handled_command = command_handler.handled_command
command_groups_description += self.generate_formatted_description_message_gen()(
handled_command.trigger,
handled_command.description,
)
self._cached_command_groups_description = command_groups_description
return command_groups_description
def generate_formatted_exit_command_handler(self, farewell_message: str) -> NonStandardBehaviorHandler[Response]:
return lambda _: self._print_func(farewell_message)
class Renderer(Protocol):
@staticmethod
def render_prompt(text: str) -> str: ...
def render_prompt(
text: str
) -> str: ...
@staticmethod
def render_initial_message(text: str) -> str: ...
def render_initial_message(
text: str
) -> str: ...
@staticmethod
def render_farewell_message(text: str) -> str: ...
def render_farewell_message(
text: str
) -> str: ...
@staticmethod
def render_messages_on_startup(messages: Iterable[str]) -> str: ...
def render_messages_on_startup(
messages: Iterable[str]
) -> str: ...
@staticmethod
def generate_formatted_description_message_gen() -> DescriptionMessageGenerator: ...
def render_command_groups_description(self, registered_routers: RegisteredRouters) -> str: ...
def generate_formatted_incorrect_input_syntax_handler(self) -> NonStandardBehaviorHandler[str]: ...
def generate_formatted_repeated_input_flags_handler(self) -> NonStandardBehaviorHandler[str]: ...
def generate_formatted_empty_input_command_handler(self) -> EmptyCommandHandler: ...
def generate_formatted_unknown_command_handler(self) -> NonStandardBehaviorHandler[InputCommand]: ...
def generate_formatted_exit_command_handler(self, farewell_message: str) -> NonStandardBehaviorHandler[Response]: ...
def render_text_for_description_message_generator(
command: str,
description: str
) -> str: ...
@staticmethod
def render_command_groups_description(
description_message_generator: DescriptionMessageGenerator,
registered_routers: RegisteredRouters
) -> str: ...
@staticmethod
def render_text_for_incorrect_input_syntax_handler(
raw_command: str
) -> str: ...
@staticmethod
def render_text_for_repeated_input_flags_handler(
raw_command: str
) -> str: ...
@staticmethod
def render_text_for_empty_input_command_handler() -> str: ...
@staticmethod
def render_text_for_unknown_command_handler(
command_trigger: str,
most_similar_command_trigger: str | None
) -> str: ...
class RichRenderer(RendererMixin):
class RichRenderer(Renderer):
@staticmethod
def render_prompt(text: str) -> str:
return f"<gray><b>{text}</b></gray>"
@@ -99,35 +69,58 @@ class RichRenderer(RendererMixin):
)
@staticmethod
def generate_formatted_description_message_gen() -> DescriptionMessageGenerator:
return lambda command, description: (
f"[bold red]{escape('[' + command + ']')}[/bold red] "
def render_text_for_description_message_generator(command: str, description: str) -> str:
return (
f"[bold red]<{command}>[/bold red] "
f"[blue dim]*=*=*[/blue dim] "
f"[bold yellow italic]{escape(description)}[/bold yellow italic]"
f"[bold yellow italic]{description}[/bold yellow italic]"
)
def generate_formatted_incorrect_input_syntax_handler(self) -> NonStandardBehaviorHandler[str]:
return lambda raw_command: self._print_func(f"[red bold]Incorrect flag syntax: {escape(raw_command)}[/red bold]")
@staticmethod
def render_messages_on_startup(messages: Iterable[str]) -> str:
return "\n".join(messages)
def generate_formatted_repeated_input_flags_handler(self) -> NonStandardBehaviorHandler[str]:
return lambda raw_command: self._print_func(f"[red bold]Repeated input flags: {escape(raw_command)}[/red bold]")
@staticmethod
def render_command_groups_description(
description_message_generator: DescriptionMessageGenerator,
registered_routers: RegisteredRouters
) -> str:
command_groups_description = ""
for registered_router in registered_routers:
command_groups_description += "\n" + registered_router.title
for command_handler in registered_router.command_handlers:
handled_command = command_handler.handled_command
command_groups_description += description_message_generator(
handled_command.trigger,
handled_command.description,
)
return command_groups_description
def generate_formatted_empty_input_command_handler(self) -> EmptyCommandHandler:
return lambda: self._print_func("[red bold]Empty input command[/red bold]")
@staticmethod
def render_text_for_incorrect_input_syntax_handler(raw_command: str) -> str:
return f"[red bold]Incorrect flag syntax: {raw_command}[/red bold]"
def generate_formatted_unknown_command_handler(self) -> NonStandardBehaviorHandler[InputCommand]:
def unknown_command_handler(command: InputCommand) -> None:
cmd_trg: str = command.trigger
mst_sim_cmd: str | None = self._most_similar_command_getter(cmd_trg)
self._print_func(
f"[red]Unknown command:[/red][blue]{escape(cmd_trg)}[/blue][red]"
+ (f", most similar:[/red][blue]{mst_sim_cmd}[/blue]" if mst_sim_cmd else "")
@staticmethod
def render_text_for_repeated_input_flags_handler(raw_command: str) -> str:
return f"[red bold]Repeated input flags: {raw_command}[/red bold]"
@staticmethod
def render_text_for_empty_input_command_handler() -> str:
return "[red bold]Empty input command[/red bold]"
@staticmethod
def render_text_for_unknown_command_handler(
command_trigger: str,
most_similar_command_trigger: str | None
) -> str:
return (
f"[red]Unknown command:[/red][blue]{command_trigger}[/blue]"
+ (f"[red], most similar:[/red][blue]{most_similar_command_trigger}[/blue]"
if most_similar_command_trigger else "")
)
return unknown_command_handler
class PlainRenderer(RendererMixin):
class PlainRenderer(Renderer):
@staticmethod
def render_prompt(text: str) -> str:
return text
@@ -138,28 +131,51 @@ class PlainRenderer(RendererMixin):
@staticmethod
def render_farewell_message(text: str) -> str:
return f"{text} | https://github.com/koloideal/Argenta | made by kolo"
return f"{text} | [https://github.com/koloideal/Argenta](https://github.com/koloideal/Argenta) | made by kolo"
@staticmethod
def generate_formatted_description_message_gen() -> DescriptionMessageGenerator:
return lambda command, description: f"{command} *=*=* {description}"
def render_text_for_description_message_generator(command: str, description: str) -> str:
return f"{command} *=*=* {description}"
def generate_formatted_incorrect_input_syntax_handler(self) -> NonStandardBehaviorHandler[str]:
return lambda raw_command: self._print_func(f"Incorrect flag syntax: {escape(raw_command)}")
def render_messages_on_startup(self, messages: Iterable[str]) -> str:
return "\n".join(messages)
def generate_formatted_repeated_input_flags_handler(self) -> NonStandardBehaviorHandler[str]:
return lambda raw_command: self._print_func(f"Repeated input flags: {escape(raw_command)}")
@staticmethod
def render_command_groups_description(
description_message_generator: DescriptionMessageGenerator,
registered_routers: RegisteredRouters,
) -> str:
command_groups_description = ""
for registered_router in registered_routers:
command_groups_description += "\n" + registered_router.title
for command_handler in registered_router.command_handlers:
handled_command = command_handler.handled_command
command_groups_description += description_message_generator(
handled_command.trigger,
handled_command.description,
)
return command_groups_description
def generate_formatted_empty_input_command_handler(self) -> EmptyCommandHandler:
return lambda: self._print_func("Empty input command")
@staticmethod
def render_text_for_incorrect_input_syntax_handler(raw_command: str) -> str:
return f"Incorrect flag syntax: {raw_command}"
def generate_formatted_unknown_command_handler(self) -> NonStandardBehaviorHandler[InputCommand]:
def unknown_command_handler(command: InputCommand) -> None:
cmd_trg: str = command.trigger
mst_sim_cmd: str | None = self._most_similar_command_getter(cmd_trg)
self._print_func(
f"Unknown command: {escape(cmd_trg)}"
+ (f", most similar:{mst_sim_cmd}" if mst_sim_cmd else "")
@staticmethod
def render_text_for_repeated_input_flags_handler(raw_command: str) -> str:
return f"Repeated input flags: {raw_command}"
@staticmethod
def render_text_for_empty_input_command_handler() -> str:
return "Empty input command"
@staticmethod
def render_text_for_unknown_command_handler(
command_trigger: str,
most_similar_command_trigger: str | None
) -> str:
return (
f"Unknown command: {command_trigger}"
+ (f", most similar:{most_similar_command_trigger}"
if most_similar_command_trigger else "")
)
return unknown_command_handler
+27
View File
@@ -0,0 +1,27 @@
from typing import Iterable
from argenta.app.presentation.renderers import Renderer
from argenta.app.protocols import Printer, DescriptionMessageGenerator
from argenta.app.registered_routers.entity import RegisteredRouters
class Viewer:
def __init__(self, printer: Printer, renderer: Renderer):
self._printer = printer
self._renderer = renderer
def view_messages_on_startup(self, messages: Iterable[str]) -> None:
self._printer(self._renderer.render_messages_on_startup(messages))
def view_command_groups_description(
self,
description_message_generator: DescriptionMessageGenerator,
registered_routers: RegisteredRouters
) -> None:
self._printer(
self._renderer.render_command_groups_description(
description_message_generator,
registered_routers
)
)
+1 -1
View File
@@ -343,7 +343,7 @@ def test_set_description_message_pattern_stores_generator() -> None:
descr_gen: DescriptionMessageGenerator = lambda command, description: command + '-+-' + description
app.set_description_message_pattern(descr_gen)
assert app._description_message_gen is descr_gen
assert app._description_message_generator is descr_gen
def test_set_exit_command_handler_stores_handler() -> None: