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 rich.console import Console
from argenta.app.autocompleter import AutoCompleter 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.presentation.renderers import PlainRenderer, RichRenderer, Renderer
from argenta.app.dividing_line.models import DynamicDividingLine, StaticDividingLine from argenta.app.dividing_line.models import DynamicDividingLine, StaticDividingLine
from argenta.app.presentation.viewers import Viewer
from argenta.app.protocols import ( from argenta.app.protocols import (
DescriptionMessageGenerator, DescriptionMessageGenerator,
EmptyCommandHandler, EmptyCommandHandler,
@@ -61,24 +63,25 @@ class BaseApp:
self._messages_on_startup: list[str] = [] self._messages_on_startup: list[str] = []
if self._override_system_messages: if self._override_system_messages:
self.view: Renderer = PlainRenderer( self._renderer: Renderer = PlainRenderer()
print_func=self._print_func,
most_similar_command_getter=self._most_similar_command
)
else: else:
self.view: Renderer = RichRenderer( self._renderer: Renderer = RichRenderer()
print_func=self._print_func,
most_similar_command_getter=self._most_similar_command 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._initial_message: str = self._renderer.render_initial_message(initial_message)
self._farewell_message: str = self.view.render_farewell_message(farewell_message) self._farewell_message: str = self._renderer.render_farewell_message(farewell_message)
self._description_message_gen: DescriptionMessageGenerator = self.view.generate_formatted_description_message_gen() self._description_message_generator: DescriptionMessageGenerator = self._handlers_fabric.generate_description_message_generator()
self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = self.view.generate_formatted_incorrect_input_syntax_handler() self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = self._handlers_fabric.generate_incorrect_input_syntax_handler()
self._repeated_input_flags_handler: NonStandardBehaviorHandler[str] = self.view.generate_formatted_repeated_input_flags_handler() self._repeated_input_flags_handler: NonStandardBehaviorHandler[str] = self._handlers_fabric.generate_repeated_input_flags_handler()
self._empty_input_command_handler: EmptyCommandHandler = self.view.generate_formatted_empty_input_command_handler() self._empty_input_command_handler: EmptyCommandHandler = self._handlers_fabric.generate_empty_input_command_handler()
self._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = self.view.generate_formatted_unknown_command_handler() self._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = self._handlers_fabric.generate_unknown_command_handler()
self._exit_command_handler: NonStandardBehaviorHandler[Response] = self.view.generate_formatted_exit_command_handler(self._farewell_message) self._exit_command_handler: NonStandardBehaviorHandler[Response] = self._handlers_fabric.generate_exit_command_handler(self._farewell_message)
def set_description_message_pattern(self, _: DescriptionMessageGenerator, /) -> None: def set_description_message_pattern(self, _: DescriptionMessageGenerator, /) -> None:
""" """
@@ -86,7 +89,7 @@ class BaseApp:
:param _: output pattern of the available commands :param _: output pattern of the available commands
:return: None :return: None
""" """
self._description_message_gen = _ self._description_message_generator = _
def set_incorrect_input_syntax_handler(self, _: NonStandardBehaviorHandler[str], /) -> None: def set_incorrect_input_syntax_handler(self, _: NonStandardBehaviorHandler[str], /) -> None:
""" """
@@ -128,22 +131,6 @@ class BaseApp:
""" """
self._exit_command_handler = _ 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: def _print_static_framed_text(self, text: str) -> None:
""" """
Private. Outputs text by framing it in a static or dynamic split strip 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()) self._autocompleter.initial_setup(self.registered_routers.get_triggers())
if self._messages_on_startup: 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: 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: def _process_exist_and_valid_command(self, input_command: InputCommand) -> None:
processing_router = self.registered_routers.get_router_by_trigger(input_command.trigger.lower()) 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!") raise RuntimeError(f"Router for '{input_command.trigger}' not found. Panic!")
match (self._dividing_line, processing_router.disable_redirect_stdout): match (self._dividing_line, processing_router.disable_redirect_stdout):
case (None, bool()):
processing_router.finds_appropriate_handler(input_command)
case (DynamicDividingLine(), False): case (DynamicDividingLine(), False):
stdout_result = self._capture_stdout(lambda: processing_router.finds_appropriate_handler(input_command)) stdout_result = self._capture_stdout(lambda: processing_router.finds_appropriate_handler(input_command))
clear_text = _ANSI_ESCAPE_RE.sub("", stdout_result) clear_text = _ANSI_ESCAPE_RE.sub("", stdout_result)
@@ -321,8 +310,6 @@ class BaseApp:
is_override=self._override_system_messages is_override=self._override_system_messages
) )
) )
case (None, bool()):
processing_router.finds_appropriate_handler(input_command)
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")
@@ -384,10 +371,10 @@ class App(BaseApp):
self._pre_cycle_setup() self._pre_cycle_setup()
while True: while True:
if self._repeat_command_groups_printing: 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 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 print() # post-prompt gap
try: try:
+122 -106
View File
@@ -1,86 +1,56 @@
from abc import ABC, abstractmethod from typing import Iterable, Protocol
from typing import Protocol, Iterable
from art import text2art 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.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): class Renderer(Protocol):
@staticmethod @staticmethod
def render_prompt(text: str) -> str: ... def render_prompt(
text: str
) -> str: ...
@staticmethod @staticmethod
def render_initial_message(text: str) -> str: ... def render_initial_message(
text: str
) -> str: ...
@staticmethod @staticmethod
def render_farewell_message(text: str) -> str: ... def render_farewell_message(
text: str
) -> str: ...
@staticmethod @staticmethod
def render_messages_on_startup(messages: Iterable[str]) -> str: ... def render_messages_on_startup(
messages: Iterable[str]
) -> str: ...
@staticmethod @staticmethod
def generate_formatted_description_message_gen() -> DescriptionMessageGenerator: ... def render_text_for_description_message_generator(
command: str,
def render_command_groups_description(self, registered_routers: RegisteredRouters) -> str: ... description: str
) -> str: ...
def generate_formatted_incorrect_input_syntax_handler(self) -> NonStandardBehaviorHandler[str]: ... @staticmethod
def render_command_groups_description(
def generate_formatted_repeated_input_flags_handler(self) -> NonStandardBehaviorHandler[str]: ... description_message_generator: DescriptionMessageGenerator,
registered_routers: RegisteredRouters
def generate_formatted_empty_input_command_handler(self) -> EmptyCommandHandler: ... ) -> str: ...
@staticmethod
def generate_formatted_unknown_command_handler(self) -> NonStandardBehaviorHandler[InputCommand]: ... def render_text_for_incorrect_input_syntax_handler(
raw_command: str
def generate_formatted_exit_command_handler(self, farewell_message: str) -> NonStandardBehaviorHandler[Response]: ... ) -> 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 @staticmethod
def render_prompt(text: str) -> str: def render_prompt(text: str) -> str:
return f"<gray><b>{text}</b></gray>" return f"<gray><b>{text}</b></gray>"
@@ -99,35 +69,58 @@ class RichRenderer(RendererMixin):
) )
@staticmethod @staticmethod
def generate_formatted_description_message_gen() -> DescriptionMessageGenerator: def render_text_for_description_message_generator(command: str, description: str) -> str:
return lambda command, description: ( return (
f"[bold red]{escape('[' + command + ']')}[/bold red] " f"[bold red]<{command}>[/bold red] "
f"[blue dim]*=*=*[/blue dim] " 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]: @staticmethod
return lambda raw_command: self._print_func(f"[red bold]Incorrect flag syntax: {escape(raw_command)}[/red bold]") def render_messages_on_startup(messages: Iterable[str]) -> str:
return "\n".join(messages)
def generate_formatted_repeated_input_flags_handler(self) -> NonStandardBehaviorHandler[str]: @staticmethod
return lambda raw_command: self._print_func(f"[red bold]Repeated input flags: {escape(raw_command)}[/red bold]") 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: @staticmethod
return lambda: self._print_func("[red bold]Empty input command[/red bold]") 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]: @staticmethod
def unknown_command_handler(command: InputCommand) -> None: def render_text_for_repeated_input_flags_handler(raw_command: str) -> str:
cmd_trg: str = command.trigger return f"[red bold]Repeated input flags: {raw_command}[/red bold]"
mst_sim_cmd: str | None = self._most_similar_command_getter(cmd_trg)
self._print_func( @staticmethod
f"[red]Unknown command:[/red][blue]{escape(cmd_trg)}[/blue][red]" def render_text_for_empty_input_command_handler() -> str:
+ (f", most similar:[/red][blue]{mst_sim_cmd}[/blue]" if mst_sim_cmd else "") 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(Renderer):
class PlainRenderer(RendererMixin):
@staticmethod @staticmethod
def render_prompt(text: str) -> str: def render_prompt(text: str) -> str:
return text return text
@@ -138,28 +131,51 @@ class PlainRenderer(RendererMixin):
@staticmethod @staticmethod
def render_farewell_message(text: str) -> str: 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 @staticmethod
def generate_formatted_description_message_gen() -> DescriptionMessageGenerator: def render_text_for_description_message_generator(command: str, description: str) -> str:
return lambda command, description: f"{command} *=*=* {description}" return f"{command} *=*=* {description}"
def generate_formatted_incorrect_input_syntax_handler(self) -> NonStandardBehaviorHandler[str]: def render_messages_on_startup(self, messages: Iterable[str]) -> str:
return lambda raw_command: self._print_func(f"Incorrect flag syntax: {escape(raw_command)}") return "\n".join(messages)
def generate_formatted_repeated_input_flags_handler(self) -> NonStandardBehaviorHandler[str]: @staticmethod
return lambda raw_command: self._print_func(f"Repeated input flags: {escape(raw_command)}") 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: @staticmethod
return lambda: self._print_func("Empty input command") 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]: @staticmethod
def unknown_command_handler(command: InputCommand) -> None: def render_text_for_repeated_input_flags_handler(raw_command: str) -> str:
cmd_trg: str = command.trigger return f"Repeated input flags: {raw_command}"
mst_sim_cmd: str | None = self._most_similar_command_getter(cmd_trg)
self._print_func( @staticmethod
f"Unknown command: {escape(cmd_trg)}" def render_text_for_empty_input_command_handler() -> str:
+ (f", most similar:{mst_sim_cmd}" if mst_sim_cmd else "") 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 descr_gen: DescriptionMessageGenerator = lambda command, description: command + '-+-' + description
app.set_description_message_pattern(descr_gen) 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: def test_set_exit_command_handler_stores_handler() -> None: