Merge pull request #8 from koloideal/benchmarks

Benchmarks
This commit is contained in:
kolo
2026-02-07 01:30:03 +03:00
committed by GitHub
111 changed files with 4405 additions and 1253 deletions
+2
View File
@@ -1,5 +1,7 @@
#### joe made this: http://goel.io/joe #### joe made this: http://goel.io/joe
metrics/reports/diagrams
#### python #### #### python ####
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
+20
View File
@@ -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
+8 -4
View File
@@ -1,13 +1,17 @@
from argenta import App, Orchestrator from argenta import App, Orchestrator
from argenta.orchestrator.argparser import ArgParser, BooleanArgument, ValueArgument from argenta.orchestrator.argparser import ArgParser, BooleanArgument
arg_parser = ArgParser(processed_args=[BooleanArgument("dev"), ValueArgument('some', possible_values=['fuck', 'cruck'])]) arg_parser = ArgParser(
processed_args=[
BooleanArgument("dev")
]
)
orchestrator = Orchestrator( orchestrator = Orchestrator(
arg_parser=arg_parser, arg_parser=arg_parser,
) )
if __name__ == "__main__": if __name__ == "__main__":
if arg_parser.parsed_argspace.get_by_name('dev'): if arg_parser.parsed_argspace.get_by_name("dev"):
orchestrator.start_polling(App(initial_message='ArgentaDev')) orchestrator.start_polling(App(initial_message="ArgentaDev"))
else: else:
orchestrator.start_polling(App()) orchestrator.start_polling(App())
+1 -5
View File
@@ -6,11 +6,7 @@ arguments = [
ValueArgument("port", help="Server port", is_required=True), ValueArgument("port", help="Server port", is_required=True),
] ]
argparser = ArgParser( argparser = ArgParser(processed_args=arguments, name="WebServer", description="Simple web server")
processed_args=arguments,
name="WebServer",
description="Simple web server"
)
app = App() app = App()
orchestrator = Orchestrator(argparser) orchestrator = Orchestrator(argparser)
+9
View File
@@ -1,3 +1,12 @@
from argenta import Response, Router
from argenta.di import FromDishka
from argenta.orchestrator.argparser import ArgSpace
router = Router()
@router.command("get_args")
def get_args(response: Response, argspace: FromDishka[ArgSpace]):
config_arg = argspace.get_by_name("config") config_arg = argspace.get_by_name("config")
if config_arg: if config_arg:
print(f"Config path: {config_arg.value}") print(f"Config path: {config_arg.value}")
+4 -12
View File
@@ -1,28 +1,20 @@
from argenta.orchestrator.argparser import ArgParser, ValueArgument from argenta.orchestrator.argparser import ArgParser, ValueArgument
# Create arguments # Create arguments
config_arg = ValueArgument( config_arg = ValueArgument("config", help="Path to configuration file", default="config.yaml")
"config",
help="Path to configuration file",
default="config.yaml"
)
log_level_arg = ValueArgument( log_level_arg = ValueArgument(
"log-level", "log-level",
help="Logging level", help="Logging level",
possible_values=["DEBUG", "INFO", "WARNING", "ERROR"], possible_values=["DEBUG", "INFO", "WARNING", "ERROR"],
default="INFO" default="INFO",
) )
host_arg = ValueArgument( host_arg = ValueArgument("host", help="Server host address", is_required=True)
"host",
help="Server host address",
is_required=True
)
# Register in ArgParser # Register in ArgParser
parser = ArgParser( parser = ArgParser(
processed_args=[config_arg, log_level_arg, host_arg], processed_args=[config_arg, log_level_arg, host_arg],
name="MyApp", name="MyApp",
description="My application with CLI arguments" description="My application with CLI arguments",
) )
+4 -18
View File
@@ -1,23 +1,9 @@
from argenta.orchestrator.argparser import ArgParser, BooleanArgument from argenta.orchestrator.argparser import ArgParser, BooleanArgument
# Create boolean arguments # Create boolean arguments
verbose_arg = BooleanArgument( verbose_arg = BooleanArgument("verbose", help="Enable verbose output")
"verbose", debug_arg = BooleanArgument("debug", help="Enable debug mode")
help="Enable verbose output" no_cache_arg = BooleanArgument("no-cache", help="Disable caching")
)
debug_arg = BooleanArgument(
"debug",
help="Enable debug mode"
)
no_cache_arg = BooleanArgument(
"no-cache",
help="Disable caching"
)
# Register in ArgParser # Register in ArgParser
parser = ArgParser( parser = ArgParser(processed_args=[verbose_arg, debug_arg, no_cache_arg], name="MyApp")
processed_args=[verbose_arg, debug_arg, no_cache_arg],
name="MyApp"
)
+5 -2
View File
@@ -2,10 +2,13 @@ from argenta import Router, Command, Response
router = Router(title="System") router = Router(title="System")
@router.command(Command(
@router.command(
Command(
"shutdown", "shutdown",
description="Shutdown the system", description="Shutdown the system",
aliases=["poweroff", "halt", "stop"] aliases=["poweroff", "halt", "stop"]
)) )
)
def handle_shutdown(response: Response): def handle_shutdown(response: Response):
print("Shutting down the system...") print("Shutting down the system...")
+1 -1
View File
@@ -3,7 +3,7 @@ from argenta.command import Flag
verbose_flag = Flag(name="verbose", prefix="--") verbose_flag = Flag(name="verbose", prefix="--")
short_flag = Flag(name="v", prefix="-") short_flag = Flag(name="v", prefix="-")
# Debug view # Debug presentation
print(repr(verbose_flag)) # Flag<prefix=--, name=verbose> print(repr(verbose_flag)) # Flag<prefix=--, name=verbose>
print(repr(short_flag)) # Flag<prefix=-, name=v> print(repr(short_flag)) # Flag<prefix=-, name=v>
+1 -4
View File
@@ -5,10 +5,7 @@ from argenta.command.flag import ValidationStatus
router = Router() router = Router()
@router.command(Command( @router.command(Command("deploy", flags=Flag("verbose", possible_values=PossibleValues.NEITHER)))
"deploy",
flags=Flag("verbose", possible_values=PossibleValues.NEITHER)
))
def deploy_handler(response: Response): def deploy_handler(response: Response):
# Check for toggle flag presence # Check for toggle flag presence
verbose_flag = response.input_flags.get_flag_by_name("verbose") verbose_flag = response.input_flags.get_flag_by_name("verbose")
+1 -4
View File
@@ -8,10 +8,7 @@ router = Router(title="Example")
Command( Command(
"example", "example",
description="Example command with flags", description="Example command with flags",
flags=Flags([ flags=Flags([Flag("name"), Flag("age")]),
Flag("name"),
Flag("age")
]),
) )
) )
def example_handler(response: Response): def example_handler(response: Response):
+1 -5
View File
@@ -8,11 +8,7 @@ router = Router(title="Get Flag Example")
Command( Command(
"config", "config",
description="Configure settings", description="Configure settings",
flags=Flags([ flags=Flags([Flag("host"), Flag("port"), Flag("debug")]),
Flag("host"),
Flag("port"),
Flag("debug")
]),
) )
) )
def config_handler(response: Response): def config_handler(response: Response):
+2 -1
View File
@@ -1,5 +1,6 @@
from argenta import Command, Response, Router from argenta import Command, Response, Router
from argenta.command.flag import InputFlag, InputFlags, ValidationStatus from argenta.command.flag import InputFlag, ValidationStatus
from argenta.command import InputFlags
router = Router(title="Add Flag Example") router = Router(title="Add Flag Example")
+5 -10
View File
@@ -1,20 +1,15 @@
from argenta.command.flag import InputFlag, InputFlags, ValidationStatus from argenta.command.flag import InputFlag, ValidationStatus
from argenta.command import InputFlags
# Create InputFlags collection # Create InputFlags collection
flags = InputFlags() flags = InputFlags()
# Create several flags # Create several flags
flag1 = InputFlag( flag1 = InputFlag(name="option1", prefix="--", input_value="value1", status=ValidationStatus.VALID)
name="option1", prefix="--", input_value="value1", status=ValidationStatus.VALID
)
flag2 = InputFlag( flag2 = InputFlag(name="option2", prefix="--", input_value="value2", status=ValidationStatus.VALID)
name="option2", prefix="--", input_value="value2", status=ValidationStatus.VALID
)
flag3 = InputFlag( flag3 = InputFlag(name="option3", prefix="---", input_value="value3", status=ValidationStatus.VALID)
name="option3", prefix="---", input_value="value3", status=ValidationStatus.VALID
)
# Add all flags in one call # Add all flags in one call
flags.add_flags([flag1, flag2, flag3]) flags.add_flags([flag1, flag2, flag3])
+3 -7
View File
@@ -1,5 +1,5 @@
from argenta.command.flag import InputFlag, ValidationStatus from argenta.command.flag import InputFlag, ValidationStatus
from argenta.command.flag.flags.models import InputFlags from argenta.command import InputFlags
# Create first collection # Create first collection
flags1 = InputFlags( flags1 = InputFlags(
@@ -26,12 +26,8 @@ flags3 = InputFlags(
) )
print(f"flags1 == flags2: {flags1 == flags2}") # True (same names) print(f"flags1 == flags2: {flags1 == flags2}") # True (same names)
print( print(f"flags1 == flags3: {flags1 == flags3}") # True (same names, values are not considered)
f"flags1 == flags3: {flags1 == flags3}"
) # True (same names, values are not considered)
# Different collections # Different collections
flags4 = InputFlags( flags4 = InputFlags([InputFlag(name="flag3", input_value="value3", status=ValidationStatus.VALID)])
[InputFlag(name="flag3", input_value="value3", status=ValidationStatus.VALID)]
)
print(f"flags1 == flags4: {flags1 == flags4}") # False (different flags) print(f"flags1 == flags4: {flags1 == flags4}") # False (different flags)
@@ -0,0 +1,9 @@
from metrics.benchmarks.entity import benchmarks
@benchmarks.register(
type_="my_category",
description="Description of what is being measured"
)
def benchmark_my_operation() -> None:
# Code whose performance is being measured
pass
@@ -8,11 +8,8 @@ from argenta.response.status import ResponseStatus
router = Router("Calculator") router = Router("Calculator")
operations = { operations = {"mul": operator.mul, "sub": operator.sub, "add": operator.add}
'mul': operator.mul,
'sub': operator.sub,
'add': operator.add
}
@router.command( @router.command(
Command( Command(
@@ -22,7 +19,9 @@ operations = {
[ [
Flag("a", possible_values=re.compile(r"^\d{,5}$")), # First number Flag("a", possible_values=re.compile(r"^\d{,5}$")), # First number
Flag("b", possible_values=re.compile(r"^\d{,5}$")), # Second number Flag("b", possible_values=re.compile(r"^\d{,5}$")), # Second number
Flag("operation", possible_values=["add", "sub", "mul"]), # Operation: add, sub, mul Flag(
"operation", possible_values=["add", "sub", "mul"]
), # Operation: add, sub, mul
] ]
), ),
) )
+2 -6
View File
@@ -6,7 +6,7 @@ app = App(
prompt=">> ", prompt=">> ",
initial_message="Simple App", initial_message="Simple App",
farewell_message="Goodbye!", farewell_message="Goodbye!",
repeat_command_groups_printing=False repeat_command_groups_printing=False,
) )
orchestrator = Orchestrator() orchestrator = Orchestrator()
@@ -15,11 +15,7 @@ main_router = Router(title="Main commands")
# 3. Define command and its handler # 3. Define command and its handler
@main_router.command(Command( @main_router.command(Command("hello", description="Prints greeting message", flags=Flag("name")))
"hello",
description="Prints greeting message",
flags=Flag("name")
))
def hello_handler(response: Response): def hello_handler(response: Response):
"""This handler will be called for 'hello' command.""" """This handler will be called for 'hello' command."""
name = response.input_flags.get_flag_by_name("name") name = response.input_flags.get_flag_by_name("name")
@@ -1,7 +1,8 @@
from typing import cast from typing import cast
from argenta import Command, Response, Router from argenta import Command, Response, Router
from argenta.command.flag import Flag, Flags, ValidationStatus from argenta.command.flag import Flag, ValidationStatus
from argenta.command import Flags
from argenta.di import FromDishka from argenta.di import FromDishka
from .repository import Priority, Task, TaskRepository from .repository import Priority, Task, TaskRepository
@@ -9,14 +10,18 @@ from .repository import Priority, Task, TaskRepository
router = Router(title="Task Manager") router = Router(title="Task Manager")
@router.command(Command( @router.command(
Command(
"add-task", "add-task",
description="Add a new task", description="Add a new task",
flags=Flags([ flags=Flags(
[
Flag("description"), Flag("description"),
Flag("priority", possible_values=["low", "medium", "high"]), Flag("priority", possible_values=["low", "medium", "high"]),
]), ]
)) ),
)
)
def add_task(response: Response, repo: FromDishka[TaskRepository]): def add_task(response: Response, repo: FromDishka[TaskRepository]):
description_flag = response.input_flags.get_flag_by_name("description") description_flag = response.input_flags.get_flag_by_name("description")
@@ -4,6 +4,7 @@ from argenta.di import FromDishka
router = Router(title="Authentication") router = Router(title="Authentication")
def authenticate_user(username: str) -> str: def authenticate_user(username: str) -> str:
return f"token_for_{username}" return f"token_for_{username}"
-25
View File
@@ -1,25 +0,0 @@
from argenta import Command, Response, Router
router = Router(title="Data Example")
@router.command(Command("set", description="Set data"))
def set_handler(response: Response):
# Update global data storage
response.update_data(
{
"user_name": "John",
"timestamp": "2024-01-01",
"settings": {"theme": "dark", "language": "ru"},
}
)
print("Data updated successfully")
@router.command(Command("show", description="Show data"))
def show_handler(response: Response):
# Get data from global storage
data = response.get_data()
if "user_name" in data:
print(f"User: {data['user_name']}")
print(f"Settings: {data.get('settings', {})}")
-16
View File
@@ -1,16 +0,0 @@
from argenta import Command, Response, Router
router = Router(title="Get Data Example")
@router.command(Command("info", description="Show all stored data"))
def info_handler(response: Response):
# Get all data from global storage
all_data = response.get_data()
if all_data:
print("Stored data:")
for key, value in all_data.items():
print(f" {key}: {value}")
else:
print("No data stored")
-19
View File
@@ -1,19 +0,0 @@
from argenta import Command, Response, Router
router = Router(title="Clear Data Example")
@router.command(Command("clear", description="Clear all stored data"))
def clear_handler(response: Response):
# Clear all data storage
response.clear_data()
print("All data cleared")
@router.command(Command("check", description="Check if data exists"))
def check_handler(response: Response):
data = response.get_data()
if data:
print(f"Storage contains {len(data)} item(s)")
else:
print("Storage is empty")
-29
View File
@@ -1,29 +0,0 @@
from argenta import Command, Response, Router
router = Router(title="Delete Data Example")
@router.command(Command("store", description="Store data"))
def store_handler(response: Response):
response.update_data(
{
"temp_key": "temporary value",
"important_key": "important value",
"another_key": "another value",
}
)
print("Data stored")
@router.command(Command("remove", description="Remove specific key"))
def remove_handler(response: Response):
# Delete specific key from storage
try:
response.delete_from_data("temp_key")
print("Key 'temp_key' deleted")
# Check what remains
remaining = response.get_data()
print(f"Remaining keys: {list(remaining.keys())}")
except KeyError:
print("Key not found")
+1 -4
View File
@@ -10,10 +10,7 @@ router = Router(title="Flags Example")
Command( Command(
"process", "process",
description="Process with flags", description="Process with flags",
flags=Flags([ flags=Flags([Flag("format", possible_values=["json", "xml"]), Flag("verbose")]),
Flag("format", possible_values=["json", "xml"]),
Flag("verbose")
]),
) )
) )
def process_handler(response: Response): def process_handler(response: Response):
+6 -7
View File
@@ -8,22 +8,21 @@ from argenta import App, Orchestrator, Router, Command, Response
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def patched_argv(): def patched_argv():
with patch.object(sys, 'argv', ['program.py']): with patch.object(sys, "argv", ["program.py"]):
yield yield
def test_input_incorrect_command(capsys: CaptureFixture[str]): def test_input_incorrect_command(capsys: CaptureFixture[str]):
router = Router() router = Router()
orchestrator = Orchestrator() orchestrator = Orchestrator()
@router.command(Command('test')) @router.command(Command("test"))
def test(response: Response) -> None: def test(response: Response) -> None:
print('test command') print("test command")
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler( app.set_unknown_command_handler(lambda command: print(f"Unknown command: {command.trigger}"))
lambda command: print(f'Unknown command: {command.trigger}')
)
with patch("builtins.input", side_effect=["help", "q"]): with patch("builtins.input", side_effect=["help", "q"]):
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -12,12 +12,14 @@ class Service:
def hello(self) -> str: def hello(self) -> str:
return "world" return "world"
def get_service() -> Service: def get_service() -> Service:
return Service() return Service()
router = Router(title="DI") router = Router(title="DI")
@router.command("HELLO") @router.command("HELLO")
def hello(response: Response, service: FromDishka[Service]) -> None: def hello(response: Response, service: FromDishka[Service]) -> None:
print(f"hello {service.hello()}") print(f"hello {service.hello()}")
@@ -37,6 +39,6 @@ def test_hello_uses_service():
# Call handler # Call handler
with redirect_stdout(io.StringIO()) as stdout: with redirect_stdout(io.StringIO()) as stdout:
router.finds_appropriate_handler(InputCommand.parse('HELLO')) router.finds_appropriate_handler(InputCommand.parse("HELLO"))
assert "hello world" in stdout.getvalue() assert "hello world" in stdout.getvalue()
@@ -7,6 +7,7 @@ from argenta.command import InputCommand
router = Router(title="Demo") router = Router(title="Demo")
@router.command(Command("PING", description="Ping command")) @router.command(Command("PING", description="Ping command"))
def ping(response: Response): def ping(response: Response):
print("PONG") print("PONG")
+1
View File
@@ -64,6 +64,7 @@ Argenta предназначена для создания приложений,
root/contributing root/contributing
root/code_of_conduct root/code_of_conduct
root/metrics
.. toctree:: .. toctree::
:hidden: :hidden:
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Argenta \n" "Project-Id-Version: Argenta \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-13 21:50+0300\n" "POT-Creation-Date: 2026-02-06 23:44+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n" "Language: en\n"
@@ -29,32 +29,32 @@ msgid ""
"взаимодействие с пользователем, координируя работу всех компонентов: " "взаимодействие с пользователем, координируя работу всех компонентов: "
"роутеров, обработчиков и системных сообщений." "роутеров, обработчиков и системных сообщений."
msgstr "" msgstr ""
"The ``App`` object is the core of your console application. It handles " "The ``App`` object is the implementations of your console application. It"
"configuration, lifecycle management, command processing, and user " " handles configuration, lifecycle management, command processing, and "
"interaction, coordinating the work of all components: routers, handlers, " "user interaction, coordinating the work of all components: routers, "
"and system messages." "handlers, and system messages."
#: ../../root/api/app/index.rst:11 #: ../../root/api/app/index.rst:11
msgid "Инициализация" msgid "Инициализация"
msgstr "Initialization" msgstr "Initialization"
#: ../../root/api/app/index.rst:37 #: ../../root/api/app/index.rst:31
msgid "Создаёт и настраивает экземпляр приложения." msgid "Создаёт и настраивает экземпляр приложения."
msgstr "Creates and configures an application instance." msgstr "Creates and configures an application instance."
#: ../../root/api/app/index.rst:39 #: ../../root/api/app/index.rst:33
msgid "``prompt``: Приглашение к вводу, отображаемое перед каждой командой." msgid "``prompt``: Приглашение к вводу, отображаемое перед каждой командой."
msgstr "``prompt``: Input prompt displayed before each command." msgstr "``prompt``: Input prompt displayed before each command."
#: ../../root/api/app/index.rst:40 #: ../../root/api/app/index.rst:34
msgid "``initial_message``: Сообщение, выводимое при запуске приложения." msgid "``initial_message``: Сообщение, выводимое при запуске приложения."
msgstr "``initial_message``: Message displayed when the application starts." msgstr "``initial_message``: Message displayed when the application starts."
#: ../../root/api/app/index.rst:41 #: ../../root/api/app/index.rst:35
msgid "``farewell_message``: Сообщение, выводимое при выходе из приложения." msgid "``farewell_message``: Сообщение, выводимое при выходе из приложения."
msgstr "``farewell_message``: Message displayed when exiting the application." msgstr "``farewell_message``: Message displayed when exiting the application."
#: ../../root/api/app/index.rst:42 #: ../../root/api/app/index.rst:36
msgid "" msgid ""
"``exit_command``: Команда, которая маркируется как триггер для выхода из " "``exit_command``: Команда, которая маркируется как триггер для выхода из "
"приложения." "приложения."
@@ -62,7 +62,7 @@ msgstr ""
"``exit_command``: Command that is marked as a trigger for exiting the " "``exit_command``: Command that is marked as a trigger for exiting the "
"application." "application."
#: ../../root/api/app/index.rst:43 #: ../../root/api/app/index.rst:37
msgid "" msgid ""
"``system_router_title``: Заголовок для системного роутера (содержит " "``system_router_title``: Заголовок для системного роутера (содержит "
"команду выхода)." "команду выхода)."
@@ -70,7 +70,7 @@ msgstr ""
"``system_router_title``: Title for the system router (contains the exit " "``system_router_title``: Title for the system router (contains the exit "
"command)." "command)."
#: ../../root/api/app/index.rst:44 #: ../../root/api/app/index.rst:38
msgid "" msgid ""
"``dividing_line``: Тип разделительной линии (``StaticDividingLine`` или " "``dividing_line``: Тип разделительной линии (``StaticDividingLine`` или "
"``DynamicDividingLine``)." "``DynamicDividingLine``)."
@@ -78,7 +78,7 @@ msgstr ""
"``dividing_line``: Type of dividing line (``StaticDividingLine`` or " "``dividing_line``: Type of dividing line (``StaticDividingLine`` or "
"``DynamicDividingLine``)." "``DynamicDividingLine``)."
#: ../../root/api/app/index.rst:45 #: ../../root/api/app/index.rst:39
msgid "" msgid ""
"``repeat_command_groups_printing``: Если ``True``, список доступных " "``repeat_command_groups_printing``: Если ``True``, список доступных "
"команд выводится перед каждым вводом." "команд выводится перед каждым вводом."
@@ -86,7 +86,7 @@ msgstr ""
"``repeat_command_groups_printing``: If ``True``, the list of available " "``repeat_command_groups_printing``: If ``True``, the list of available "
"commands is displayed before each input." "commands is displayed before each input."
#: ../../root/api/app/index.rst:46 #: ../../root/api/app/index.rst:40
msgid "" msgid ""
"``override_system_messages``: Если ``True``, стандартное форматирование " "``override_system_messages``: Если ``True``, стандартное форматирование "
"(цвета, ASCII-арт) отключается." "(цвета, ASCII-арт) отключается."
@@ -94,7 +94,7 @@ msgstr ""
"``override_system_messages``: If ``True``, standard formatting (colors, " "``override_system_messages``: If ``True``, standard formatting (colors, "
"ASCII art) is disabled." "ASCII art) is disabled."
#: ../../root/api/app/index.rst:47 #: ../../root/api/app/index.rst:41
msgid "" msgid ""
"``autocompleter``: Экземпляр класса :ref:`AutoCompleter " "``autocompleter``: Экземпляр класса :ref:`AutoCompleter "
"<root_api_app_autocompleter>`, отвечающий за автодополнение команд." "<root_api_app_autocompleter>`, отвечающий за автодополнение команд."
@@ -103,29 +103,28 @@ msgstr ""
"<root_api_app_autocompleter>` class responsible for command " "<root_api_app_autocompleter>` class responsible for command "
"autocompletion." "autocompletion."
#: ../../root/api/app/index.rst:48 #: ../../root/api/app/index.rst:42
msgid "" #, fuzzy
"``print_func``: Функция для вывода всех системных сообщений (по умолчанию" msgid "``printer``: Функция для вывода всех системных сообщений."
" ``rich.Console().print``)."
msgstr "" msgstr ""
"``print_func``: Function for outputting all system messages (defaults to " "``print_func``: Function for outputting all system messages (defaults to "
"``rich.Console().print``)." "``rich.Console().print``)."
#: ../../root/api/app/index.rst:53 #: ../../root/api/app/index.rst:47
msgid "" msgid ""
"В приложениях на Argenta регистр вводимых команд не важен, проверка на " "В приложениях на Argenta регистр вводимых команд не важен, проверка на "
"существование и роутинг команд производится на основании триггеров, " "существование и роутинг команд производится на основании триггеров, "
"приведённых к нижнему регистру." "приведённых к нижнему регистру."
msgstr "" msgstr ""
"In applications on Argenta, the case of the entered commands is not important, checking for the " "In applications on Argenta, the case of the entered commands is not "
" existence and routing of commands is performed based on triggers " "important, checking for the existence and routing of commands is "
"reduced to lowercase." "performed based on triggers reduced to lowercase."
#: ../../root/api/app/index.rst:56 #: ../../root/api/app/index.rst:50
msgid "Основные методы" msgid "Основные методы"
msgstr "Main Methods" msgstr "Main Methods"
#: ../../root/api/app/index.rst:60 #: ../../root/api/app/index.rst:54
msgid "" msgid ""
"Регистрирует роутер в приложении. Все команды из этого роутера становятся" "Регистрирует роутер в приложении. Все команды из этого роутера становятся"
" доступными для вызова." " доступными для вызова."
@@ -137,19 +136,19 @@ msgstr ""
msgid "Parameters" msgid "Parameters"
msgstr "Parameters" msgstr "Parameters"
#: ../../root/api/app/index.rst:62 #: ../../root/api/app/index.rst:56
msgid "Экземпляр ``Router`` для регистрации." msgid "Экземпляр ``Router`` для регистрации."
msgstr "``Router`` instance to register." msgstr "``Router`` instance to register."
#: ../../root/api/app/index.rst:66 #: ../../root/api/app/index.rst:60
msgid "Регистрирует несколько роутеров одновременно." msgid "Регистрирует несколько роутеров одновременно."
msgstr "Registers multiple routers simultaneously." msgstr "Registers multiple routers simultaneously."
#: ../../root/api/app/index.rst:68 #: ../../root/api/app/index.rst:62
msgid "Последовательность экземпляров ``Router`` для регистрации." msgid "Последовательность экземпляров ``Router`` для регистрации."
msgstr "Sequence of ``Router`` instances to register." msgstr "Sequence of ``Router`` instances to register."
#: ../../root/api/app/index.rst:72 #: ../../root/api/app/index.rst:66
msgid "" msgid ""
"Добавляет текстовое сообщение, которое выводится при запуске приложения " "Добавляет текстовое сообщение, которое выводится при запуске приложения "
"после ``initial_message``." "после ``initial_message``."
@@ -157,11 +156,11 @@ msgstr ""
"Adds a text message that is displayed when the application starts after " "Adds a text message that is displayed when the application starts after "
"``initial_message``." "``initial_message``."
#: ../../root/api/app/index.rst:74 #: ../../root/api/app/index.rst:68
msgid "Строка с сообщением." msgid "Строка с сообщением."
msgstr "String with the message." msgstr "String with the message."
#: ../../root/api/app/index.rst:77 #: ../../root/api/app/index.rst:71
msgid "" msgid ""
"Для вывода стандартных сообщений можно использовать готовые шаблоны из " "Для вывода стандартных сообщений можно использовать готовые шаблоны из "
":ref:`PredefinedMessages <root_api_predefined_messages>`." ":ref:`PredefinedMessages <root_api_predefined_messages>`."
@@ -169,11 +168,11 @@ msgstr ""
"For outputting standard messages, you can use ready-made templates from " "For outputting standard messages, you can use ready-made templates from "
":ref:`PredefinedMessages <root_api_predefined_messages>`." ":ref:`PredefinedMessages <root_api_predefined_messages>`."
#: ../../root/api/app/index.rst:82 #: ../../root/api/app/index.rst:76
msgid "Методы установки обработчиков" msgid "Методы установки обработчиков"
msgstr "Handler Setup Methods" msgstr "Handler Setup Methods"
#: ../../root/api/app/index.rst:84 #: ../../root/api/app/index.rst:78
msgid "" msgid ""
"``App`` позволяет настраивать реакцию на различные события, такие как " "``App`` позволяет настраивать реакцию на различные события, такие как "
"ошибки ввода или неизвестные команды." "ошибки ввода или неизвестные команды."
@@ -181,7 +180,7 @@ msgstr ""
"``App`` allows you to configure responses to various events, such as " "``App`` allows you to configure responses to various events, such as "
"input errors or unknown commands." "input errors or unknown commands."
#: ../../root/api/app/index.rst:87 #: ../../root/api/app/index.rst:81
msgid "" msgid ""
"Подробнее об исключениях и их обработке в соответствующем :ref:`разделе " "Подробнее об исключениях и их обработке в соответствующем :ref:`разделе "
"документации <root_error_handling>`." "документации <root_error_handling>`."
@@ -189,59 +188,59 @@ msgstr ""
"For more details on exceptions and their handling, see the corresponding " "For more details on exceptions and their handling, see the corresponding "
":ref:`documentation section <root_error_handling>`." ":ref:`documentation section <root_error_handling>`."
#: ../../root/api/app/index.rst:93 #: ../../root/api/app/index.rst:87
msgid "Устанавливает шаблон для форматирования описания команды." msgid "Устанавливает шаблон для форматирования описания команды."
msgstr "Sets the template for formatting command descriptions." msgstr "Sets the template for formatting command descriptions."
#: ../../root/api/app/index.rst:95 #: ../../root/api/app/index.rst:89
msgid "Обработчик принимает триггер команды (``str``) и её описание (``str``)." msgid "Обработчик принимает триггер команды (``str``) и её описание (``str``)."
msgstr "" msgstr ""
"The handler accepts the command trigger (``str``) and its description " "The handler accepts the command trigger (``str``) and its description "
"(``str``)." "(``str``)."
#: ../../root/api/app/index.rst:101 #: ../../root/api/app/index.rst:95
msgid "Устанавливает обработчик при некорректном введённом синтаксисе флагов." msgid "Устанавливает обработчик при некорректном введённом синтаксисе флагов."
msgstr "Sets the handler for incorrect flag syntax input." msgstr "Sets the handler for incorrect flag syntax input."
#: ../../root/api/app/index.rst:103 ../../root/api/app/index.rst:111 #: ../../root/api/app/index.rst:97 ../../root/api/app/index.rst:105
msgid "Обработчик принимает строку, введённую пользователем." msgid "Обработчик принимает строку, введённую пользователем."
msgstr "The handler accepts the string entered by the user." msgstr "The handler accepts the string entered by the user."
#: ../../root/api/app/index.rst:109 #: ../../root/api/app/index.rst:103
msgid "Устанавливает обработчик при повторяющихся флагах в введённой команде." msgid "Устанавливает обработчик при повторяющихся флагах в введённой команде."
msgstr "Sets the handler for duplicate flags in the entered command." msgstr "Sets the handler for duplicate flags in the entered command."
#: ../../root/api/app/index.rst:117 #: ../../root/api/app/index.rst:111
msgid "Устанавливает обработчик при вводе неизвестной команды." msgid "Устанавливает обработчик при вводе неизвестной команды."
msgstr "Sets the handler for entering an unknown command." msgstr "Sets the handler for entering an unknown command."
#: ../../root/api/app/index.rst:119 #: ../../root/api/app/index.rst:113
msgid "Обработчик принимает объект ``InputCommand`` - объект введённой команды." msgid "Обработчик принимает объект ``InputCommand`` - объект введённой команды."
msgstr "" msgstr ""
"The handler accepts an ``InputCommand`` object - the entered command " "The handler accepts an ``InputCommand`` object - the entered command "
"object." "object."
#: ../../root/api/app/index.rst:125 #: ../../root/api/app/index.rst:119
msgid "Устанавливает обработчик при вводе пустой строки." msgid "Устанавливает обработчик при вводе пустой строки."
msgstr "Sets the handler for entering an empty string." msgstr "Sets the handler for entering an empty string."
#: ../../root/api/app/index.rst:127 #: ../../root/api/app/index.rst:121
msgid "Обработчик не принимает аргументов." msgid "Обработчик не принимает аргументов."
msgstr "The handler accepts no arguments." msgstr "The handler accepts no arguments."
#: ../../root/api/app/index.rst:133 #: ../../root/api/app/index.rst:127
msgid "Переопределяет стандартное поведение при вызове команды выхода." msgid "Переопределяет стандартное поведение при вызове команды выхода."
msgstr "Overrides the default behavior when the exit command is invoked." msgstr "Overrides the default behavior when the exit command is invoked."
#: ../../root/api/app/index.rst:135 #: ../../root/api/app/index.rst:129
msgid "Обработчик принимает объект ``Response``." msgid "Обработчик принимает объект ``Response``."
msgstr "The handler accepts a ``Response`` object." msgstr "The handler accepts a ``Response`` object."
#: ../../root/api/app/index.rst:148 #: ../../root/api/app/index.rst:142
msgid "PredefinedMessages" msgid "PredefinedMessages"
msgstr "PredefinedMessages" msgstr "PredefinedMessages"
#: ../../root/api/app/index.rst:150 #: ../../root/api/app/index.rst:144
msgid "" msgid ""
"``PredefinedMessages`` — это контейнер, содержащий набор готовых к " "``PredefinedMessages`` — это контейнер, содержащий набор готовых к "
"использованию сообщений. Они отформатированы с использованием синтаксиса " "использованию сообщений. Они отформатированы с использованием синтаксиса "
@@ -252,31 +251,31 @@ msgstr ""
"messages. They are formatted using ``rich`` syntax and are intended for " "messages. They are formatted using ``rich`` syntax and are intended for "
"displaying standard information, such as usage hints." "displaying standard information, such as usage hints."
#: ../../root/api/app/index.rst:152 #: ../../root/api/app/index.rst:146
msgid "Рекомендуется использовать их при старте приложения." msgid "Рекомендуется использовать их при старте приложения."
msgstr "It is recommended to use them when starting the application." msgstr "It is recommended to use them when starting the application."
#: ../../root/api/app/index.rst:179 #: ../../root/api/app/index.rst:173
msgid "Строка: ``[b dim]Usage[/b dim]: [i]<command> <[green]flags[/green]>[/i]``" msgid "Строка: ``[b dim]Usage[/b dim]: [i]<command> <[green]flags[/green]>[/i]``"
msgstr "String: ``[b dim]Usage[/b dim]: [i]<command> <[green]flags[/green]>[/i]``" msgstr "String: ``[b dim]Usage[/b dim]: [i]<command> <[green]flags[/green]>[/i]``"
#: ../../root/api/app/index.rst:181 #: ../../root/api/app/index.rst:175
msgid "Отображается как: ``Usage: <command> <flags>``" msgid "Отображается как: ``Usage: <command> <flags>``"
msgstr "Displayed as: ``Usage: <command> <flags>``" msgstr "Displayed as: ``Usage: <command> <flags>``"
#: ../../root/api/app/index.rst:185 #: ../../root/api/app/index.rst:179
msgid "Строка: ``[b dim]Help[/b dim]: [i]<command>[/i] [b red]--help[/b red]``" msgid "Строка: ``[b dim]Help[/b dim]: [i]<command>[/i] [b red]--help[/b red]``"
msgstr "String: ``[b dim]Help[/b dim]: [i]<command>[/i] [b red]--help[/b red]``" msgstr "String: ``[b dim]Help[/b dim]: [i]<command>[/i] [b red]--help[/b red]``"
#: ../../root/api/app/index.rst:187 #: ../../root/api/app/index.rst:181
msgid "Отображается как: ``Help: <command> --help``" msgid "Отображается как: ``Help: <command> --help``"
msgstr "Displayed as: ``Help: <command> --help``" msgstr "Displayed as: ``Help: <command> --help``"
#: ../../root/api/app/index.rst:191 #: ../../root/api/app/index.rst:185
msgid "Строка: ``[b dim]Autocomplete[/b dim]: [i]<part>[/i] [bold]<tab>``" msgid "Строка: ``[b dim]Autocomplete[/b dim]: [i]<part>[/i] [bold]<tab>``"
msgstr "String: ``[b dim]Autocomplete[/b dim]: [i]<part>[/i] [bold]<tab>``" msgstr "String: ``[b dim]Autocomplete[/b dim]: [i]<part>[/i] [bold]<tab>``"
#: ../../root/api/app/index.rst:193 #: ../../root/api/app/index.rst:187
msgid "Отображается как: ``Autocomplete: <part> <tab>``" msgid "Отображается как: ``Autocomplete: <part> <tab>``"
msgstr "Displayed as: ``Autocomplete: <part> <tab>``" msgstr "Displayed as: ``Autocomplete: <part> <tab>``"
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Argenta \n" "Project-Id-Version: Argenta \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-02 22:29+0300\n" "POT-Creation-Date: 2026-02-06 23:44+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n" "Language: en\n"
@@ -30,10 +30,11 @@ msgid ""
"позволяет получать внешнюю конфигурацию в момент старта (например, путь к" "позволяет получать внешнюю конфигурацию в момент старта (например, путь к"
" файлу настроек, флаги отладки или режим запуска)." " файлу настроек, флаги отладки или режим запуска)."
msgstr "" msgstr ""
"``ArgParser`` is designed for processing **command-line arguments** passed to the " "``ArgParser`` is designed for processing **command-line arguments** "
"application at startup. It's important not to confuse them with flags that the user " "passed to the application at startup. It's important not to confuse them "
"enters in interactive mode. ``ArgParser`` allows receiving external configuration at " "with flags that the user enters in interactive mode. ``ArgParser`` allows"
"startup (e.g., path to settings file, debug flags, or launch mode)." " receiving external configuration at startup (e.g., path to settings "
"file, debug flags, or launch mode)."
#: ../../root/api/orchestrator/argparser.rst:11 #: ../../root/api/orchestrator/argparser.rst:11
msgid "Инициализация" msgid "Инициализация"
@@ -81,8 +82,9 @@ msgid ""
"экземпляр ``ArgParser``, атрибут ``parsed_argspace`` будет содержать " "экземпляр ``ArgParser``, атрибут ``parsed_argspace`` будет содержать "
"пустой ``ArgSpace``." "пустой ``ArgSpace``."
msgstr "" msgstr ""
"Before initializing ``Orchestrator``, to whose constructor an ``ArgParser`` instance " "Before initializing ``Orchestrator``, to whose constructor an "
"was passed, the ``parsed_argspace`` attribute will contain an empty ``ArgSpace``." "``ArgParser`` instance was passed, the ``parsed_argspace`` attribute will"
" contain an empty ``ArgSpace``."
#: ../../root/api/orchestrator/argparser.rst:40 #: ../../root/api/orchestrator/argparser.rst:40
msgid "" msgid ""
@@ -90,8 +92,9 @@ msgid ""
"``Orchestrator``, поэтому использовать ``parsed_argspace`` " "``Orchestrator``, поэтому использовать ``parsed_argspace`` "
"**целесообразно только после** этого." "**целесообразно только после** этого."
msgstr "" msgstr ""
"Parsing and validation of arguments occur during ``Orchestrator`` initialization, " "Parsing and validation of arguments occur during ``Orchestrator`` "
"so using ``parsed_argspace`` is **advisable only after** that." "initialization, so using ``parsed_argspace`` is **advisable only after** "
"that."
#: ../../root/api/orchestrator/argparser.rst:45 #: ../../root/api/orchestrator/argparser.rst:45
msgid "Лучшие практики" msgid "Лучшие практики"
@@ -104,9 +107,10 @@ msgid ""
"``ArgSpace`` через DI. Подробнее см. :ref:`здесь " "``ArgSpace`` через DI. Подробнее см. :ref:`здесь "
"<root_dependency_injection>`." "<root_dependency_injection>`."
msgstr "" msgstr ""
"Using the ``parsed_argspace`` attribute is recommended only during the application " "Using the ``parsed_argspace`` attribute is recommended only during the "
"setup phase. In handlers, the best practice is to obtain ``ArgSpace`` through DI. " "application setup phase. In handlers, the best practice is to obtain "
"For more details, see :ref:`here <root_dependency_injection>`." "``ArgSpace`` through DI. For more details, see :ref:`here "
"<root_dependency_injection>`."
#: ../../root/api/orchestrator/argparser.rst:49 #: ../../root/api/orchestrator/argparser.rst:49
msgid "**Пример использования:**" msgid "**Пример использования:**"
@@ -149,6 +153,12 @@ msgid ""
"При использовании аргумента с ``is_deprecated=True`` выводится " "При использовании аргумента с ``is_deprecated=True`` выводится "
"предупреждение, но выполнение продолжается:" "предупреждение, но выполнение продолжается:"
msgstr "" msgstr ""
"When using an argument with ``is_deprecated=True``, a warning is displayed, " "When using an argument with ``is_deprecated=True``, a warning is "
"but execution continues:" "displayed, but execution continues:"
#: ../../root/api/orchestrator/argparser.rst:90
msgid ""
"Параметр поддерживается начиная с версии CPython 3.13, если версия ниже, "
"то параметр будет игнорироваться."
msgstr ""
@@ -579,7 +579,7 @@ msgstr ""
msgid "" msgid ""
"Откройте `127.0.0.1:8000` в браузере, чтобы просмотреть сгенерированную " "Откройте `127.0.0.1:8000` в браузере, чтобы просмотреть сгенерированную "
"документацию." "документацию."
msgstr "Open `127.0.0.1:8000` in your browser to view the generated documentation." msgstr "Open `127.0.0.1:8000` in your browser to presentation the generated documentation."
#: ../../root/contributing.rst:233 #: ../../root/contributing.rst:233
msgid "" msgid ""
@@ -109,7 +109,7 @@ msgstr "How Does It Work?"
#: ../../root/dependency_injection.rst:51 #: ../../root/dependency_injection.rst:51
msgid "В основе DI в Argenta лежат **провайдеры** и **контейнер**." msgid "В основе DI в Argenta лежат **провайдеры** и **контейнер**."
msgstr "At the core of DI in Argenta are **providers** and a **container**." msgstr "At the implementations of DI in Argenta are **providers** and a **container**."
#: ../../root/dependency_injection.rst:53 #: ../../root/dependency_injection.rst:53
msgid "" msgid ""
+1 -1
View File
@@ -49,7 +49,7 @@ msgstr ""
"The main purpose of flags is to provide a way to change the command's " "The main purpose of flags is to provide a way to change the command's "
"logic without reworking it. A command can operate in several modes: " "logic without reworking it. A command can operate in several modes: "
"standard, verbose, debug, or simplified. Flags switch these modes on user" "standard, verbose, debug, or simplified. Flags switch these modes on user"
" demand, keeping the core functionality unchanged." " demand, keeping the implementations functionality unchanged."
#: ../../root/flags.rst:17 #: ../../root/flags.rst:17
msgid "Опциональность и удобство" msgid "Опциональность и удобство"
+293
View File
@@ -0,0 +1,293 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) 2025, kolo
# This file is distributed under the same license as the Argenta package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2026.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Argenta \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-06 23:44+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
#: ../../root/metrics.rst:2
msgid "Метрики"
msgstr "Metrics"
#: ../../root/metrics.rst:4
msgid ""
"Система метрик ``Argenta`` предоставляет инструменты для измерения "
"производительности ключевых компонентов библиотеки. Это позволяет "
"отслеживать регрессию/прогрессию производительности между релизами и "
"оптимизировать критические участки кода."
msgstr ""
"The ``Argenta`` metrics system provides tools for measuring the performance "
"of key library components. This allows tracking performance regression/progression "
"between releases and optimizing critical code sections."
#: ../../root/metrics.rst:9
msgid "Запуск метрик"
msgstr "Running Metrics"
#: ../../root/metrics.rst:11
msgid ""
"Для работы с метриками необходимо склонировать репозиторий и установить "
"зависимости:"
msgstr ""
"To work with metrics, you need to clone the repository and install "
"dependencies:"
#: ../../root/metrics.rst:19
msgid "Запуск системы метрик:"
msgstr "Running the metrics system:"
#: ../../root/metrics.rst:25
msgid ""
"После запуска откроется интерактивная сессия с доступными командами для "
"работы с бенчмарками."
msgstr ""
"After launch, an interactive session will open with available commands for "
"working with benchmarks."
#: ../../root/metrics.rst:30
msgid "Доступные команды"
msgstr "Available Commands"
#: ../../root/metrics.rst:33
msgid "run-all"
msgstr "run-all"
#: ../../root/metrics.rst:35
msgid ""
"Запускает все зарегистрированные бенчмарки и выводит результаты в виде "
"таблиц."
msgstr ""
"Runs all registered benchmarks and outputs results as tables."
#: ../../root/metrics.rst:37 ../../root/metrics.rst:55
#: ../../root/metrics.rst:78 ../../root/metrics.rst:97
#: ../../root/metrics.rst:117
msgid "**Синтаксис:**"
msgstr "**Syntax:**"
#: ../../root/metrics.rst:43 ../../root/metrics.rst:84
#: ../../root/metrics.rst:103
msgid "**Флаги:**"
msgstr "**Flags:**"
#: ../../root/metrics.rst:45
msgid ""
"``--without-gc`` — отключает сборщик мусора во время выполнения "
"бенчмарков для более стабильных результатов"
msgstr ""
"``--without-gc`` — disables garbage collector during benchmark execution "
"for more stable results"
#: ../../root/metrics.rst:46
msgid "``--without-system-info`` — скрывает информацию о системе в выводе"
msgstr "``--without-system-info`` — hides system information in output"
#: ../../root/metrics.rst:51
msgid "list-types"
msgstr "list-types"
#: ../../root/metrics.rst:53
msgid ""
"Выводит список всех доступных типов бенчмарков с количеством тестов в "
"каждой категории."
msgstr ""
"Displays a list of all available benchmark types with the number of tests "
"in each category."
#: ../../root/metrics.rst:61
msgid "**Пример вывода:**"
msgstr "**Example output:**"
#: ../../root/metrics.rst:74
msgid "run-type"
msgstr "run-type"
#: ../../root/metrics.rst:76
msgid "Запускает бенчмарки определённого типа."
msgstr "Runs benchmarks of a specific type."
#: ../../root/metrics.rst:86
msgid "``--type`` — тип бенчмарков для запуска (обязательный)"
msgstr "``--type`` — benchmark type to run (required)"
#: ../../root/metrics.rst:87 ../../root/metrics.rst:106
msgid "``--without-gc`` — отключает сборщик мусора"
msgstr "``--without-gc`` — disables garbage collector"
#: ../../root/metrics.rst:88
msgid "``--without-system-info`` — скрывает информацию о системе"
msgstr "``--without-system-info`` — hides system information"
#: ../../root/metrics.rst:93
msgid "diagrams-generate"
msgstr "diagrams-generate"
#: ../../root/metrics.rst:95
msgid ""
"Генерирует визуальные диаграммы сравнения производительности для всех "
"бенчмарков."
msgstr ""
"Generates visual performance comparison diagrams for all benchmarks."
#: ../../root/metrics.rst:105
msgid ""
"``--iterations`` — количество итераций для каждого бенчмарка (по "
"умолчанию 100)"
msgstr ""
"``--iterations`` — number of iterations for each benchmark (default 100)"
#: ../../root/metrics.rst:108
msgid ""
"Диаграммы сохраняются в директорию "
"``metrics/reports/diagrams/<timestamp>/``."
msgstr ""
"Diagrams are saved to the ``metrics/reports/diagrams/<timestamp>/`` directory."
#: ../../root/metrics.rst:113
msgid "release-generate"
msgstr "release-generate"
#: ../../root/metrics.rst:115
msgid ""
"Генерирует полный отчёт о производительности для текущей версии "
"библиотеки. Используется при подготовке релизов."
msgstr ""
"Generates a complete performance report for the current library version. "
"Used when preparing releases."
#: ../../root/metrics.rst:123
msgid "Команда автоматически:"
msgstr "The command automatically:"
#: ../../root/metrics.rst:125
msgid "Определяет текущую версию библиотеки"
msgstr "Determines the current library version"
#: ../../root/metrics.rst:126
msgid "Запускает все бенчмарки с 1000 итераций и отключённым GC"
msgstr "Runs all benchmarks with 1000 iterations and disabled GC"
#: ../../root/metrics.rst:127
msgid "Генерирует JSON-отчёты и диаграммы сравнения"
msgstr "Generates JSON reports and comparison diagrams"
#: ../../root/metrics.rst:128
msgid "Сохраняет результаты в ``metrics/reports/releases/<version>/``"
msgstr "Saves results to ``metrics/reports/releases/<version>/``"
#: ../../root/metrics.rst:133
msgid "Интерпретация результатов"
msgstr "Interpreting Results"
#: ../../root/metrics.rst:135
msgid "Результаты бенчмарков включают следующие метрики:"
msgstr "Benchmark results include the following metrics:"
#: ../../root/metrics.rst:137
msgid "**Среднее время (mean)**"
msgstr "**Mean time (mean)**"
#: ../../root/metrics.rst:138
msgid ""
"Среднее время выполнения операции. Основная метрика для сравнения "
"производительности."
msgstr ""
"Average operation execution time. The primary metric for performance comparison."
#: ../../root/metrics.rst:140
msgid "**Медиана (median)**"
msgstr "**Median (median)**"
#: ../../root/metrics.rst:141
msgid ""
"Медианное значение времени выполнения. Менее чувствительна к выбросам, "
"чем среднее."
msgstr ""
"Median execution time value. Less sensitive to outliers than the mean."
#: ../../root/metrics.rst:143
msgid "**Стандартное отклонение (std)**"
msgstr "**Standard deviation (std)**"
#: ../../root/metrics.rst:144
msgid ""
"Показывает стабильность измерений. Меньшее значение означает более "
"предсказуемую производительность."
msgstr ""
"Shows measurement stability. A lower value means more predictable performance."
#: ../../root/metrics.rst:149
msgid "Рекомендации по использованию"
msgstr "Usage Recommendations"
#: ../../root/metrics.rst:151
msgid "**Для оптимизации**"
msgstr "**For optimization**"
#: ../../root/metrics.rst:152
msgid ""
"Используйте ``run-type`` для фокусировки на конкретной области и "
"``--without-gc`` для более точных измерений."
msgstr ""
"Use ``run-type`` to focus on a specific area and ``--without-gc`` for more "
"accurate measurements."
#: ../../root/metrics.rst:154
msgid "**Для визуализации**"
msgstr "**For visualization**"
#: ../../root/metrics.rst:155
msgid ""
"Команда ``diagrams-generate`` создаёт наглядные графики, удобные для "
"презентаций и документации."
msgstr ""
"The ``diagrams-generate`` command creates clear charts suitable for "
"presentations and documentation."
#: ../../root/metrics.rst:157
msgid "**Для стабильных результатов**"
msgstr "**For stable results**"
#: ../../root/metrics.rst:158
msgid ""
"Закройте ресурсоёмкие приложения, используйте флаг ``--without-gc`` и "
"увеличивайте количество итераций через ``--iterations``."
msgstr ""
"Close resource-intensive applications, use the ``--without-gc`` flag, and "
"increase the number of iterations via ``--iterations``."
#: ../../root/metrics.rst:163
msgid "Добавление новых бенчмарков"
msgstr "Adding New Benchmarks"
#: ../../root/metrics.rst:165
msgid ""
"Вы можете реализовать свои бенчмарки для тестирования специфичных юнитов "
"библиотеки. Новые бенчмарки добавляются через декоратор "
"``@benchmarks.register``:"
msgstr ""
"You can implement your own benchmarks to test specific library units. "
"New benchmarks are added via the ``@benchmarks.register`` decorator:"
#: ../../root/metrics.rst:173
msgid ""
"Бенчмарк должен быть импортирован в ``metrics/benchmarks/__init__.py`` "
"для автоматической регистрации."
msgstr ""
"The benchmark must be imported in ``metrics/benchmarks/__init__.py`` for "
"automatic registration."
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Argenta \n" "Project-Id-Version: Argenta \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-04 20:39+0300\n" "POT-Creation-Date: 2026-02-06 23:44+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n" "Language: en\n"
@@ -90,20 +90,20 @@ msgstr "Output Customization"
#: ../../root/overriding_formatting.rst:32 #: ../../root/overriding_formatting.rst:32
msgid "" msgid ""
"Для полной замены логики вывода текста в конструкторе ``App`` " "Для полной замены логики вывода текста в конструкторе ``App`` "
"предусмотрен параметр ``print_func``." "предусмотрен параметр ``printer``."
msgstr "" msgstr ""
"For complete replacement of text output logic, the ``App`` constructor " "For complete replacement of text output logic, the ``App`` constructor "
"provides the ``print_func`` parameter." "provides the ``printer`` parameter."
#: ../../root/overriding_formatting.rst:34 #: ../../root/overriding_formatting.rst:34
msgid "" msgid ""
"**print_func**: ``Callable[[str], None]`` Этот параметр позволяет " "**printer**: ``Callable[[str], None]`` Этот параметр позволяет передать "
"передать любую вызываемую сущность (например, функцию), которая будет " "любую вызываемую сущность (например, функцию), которая будет "
"использоваться для вывода всех системных сообщений. По умолчанию это " "использоваться для вывода всех системных сообщений. По умолчанию это "
"``rich.console.Console().print``. Вы можете передать сюда свою функцию, " "``rich.console.Console().print``. Вы можете передать сюда свою функцию, "
"чтобы, например, логировать вывод в файл или отправлять его по сети." "чтобы, например, логировать вывод в файл или отправлять его по сети."
msgstr "" msgstr ""
"**print_func**: ``Callable[[str], None]`` This parameter allows passing " "**printer**: ``Callable[[str], None]`` This parameter allows passing "
"any callable entity (for example, a function) that will be used to output" "any callable entity (for example, a function) that will be used to output"
" all system messages. By default, this is " " all system messages. By default, this is "
"``rich.console.Console().print``. You can pass your own function here to," "``rich.console.Console().print``. You can pass your own function here to,"
+13 -19
View File
@@ -13,26 +13,20 @@ App
.. code-block:: python .. code-block:: python
:linenos: :linenos:
AVAILABLE_DIVIDING_LINES: TypeAlias = StaticDividingLine | DynamicDividingLine def __init__(
DEFAULT_DIVIDING_LINE: StaticDividingLine = StaticDividingLine() self,
*,
DEFAULT_PRINT_FUNC: Printer = Console().print prompt: str = ">>> ",
DEFAULT_AUTOCOMPLETER: AutoCompleter = AutoCompleter() initial_message: str = "Argenta",
DEFAULT_EXIT_COMMAND: Command = Command("Q", description="Exit command") farewell_message: str = "See you",
exit_command: Command = Command("q", description="Exit command"),
.. code-block:: python system_router_title: str = "System points:",
:linenos: dividing_line: StaticDividingLine | DynamicDividingLine | None = None,
def __init__(self, *, prompt: str = "What do you want to do?\n\n",
initial_message: str = "Argenta\n",
farewell_message: str = "\nSee you\n",
exit_command: Command = DEFAULT_EXIT_COMMAND,
system_router_title: str | None = "System points:",
dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE,
repeat_command_groups_printing: bool = False, repeat_command_groups_printing: bool = False,
override_system_messages: bool = False, override_system_messages: bool = False,
autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER, autocompleter: AutoCompleter | None = None,
print_func: Printer = DEFAULT_PRINT_FUNC) -> None printer: Printer = Console().print,
) -> None:
Создаёт и настраивает экземпляр приложения. Создаёт и настраивает экземпляр приложения.
@@ -45,7 +39,7 @@ App
* ``repeat_command_groups_printing``: Если ``True``, список доступных команд выводится перед каждым вводом. * ``repeat_command_groups_printing``: Если ``True``, список доступных команд выводится перед каждым вводом.
* ``override_system_messages``: Если ``True``, стандартное форматирование (цвета, ASCII-арт) отключается. * ``override_system_messages``: Если ``True``, стандартное форматирование (цвета, ASCII-арт) отключается.
* ``autocompleter``: Экземпляр класса :ref:`AutoCompleter <root_api_app_autocompleter>`, отвечающий за автодополнение команд. * ``autocompleter``: Экземпляр класса :ref:`AutoCompleter <root_api_app_autocompleter>`, отвечающий за автодополнение команд.
* ``print_func``: Функция для вывода всех системных сообщений (по умолчанию ``rich.Console().print``). * ``printer``: Функция для вывода всех системных сообщений.
----- -----
+5
View File
@@ -84,3 +84,8 @@ ArgParser
$ python app.py --old-param value $ python app.py --old-param value
Warning: argument --old-param is deprecated Warning: argument --old-param is deprecated
.. warning::
Параметр поддерживается начиная с версии CPython 3.13, если версия ниже, то параметр будет игнорироваться.
+173
View File
@@ -0,0 +1,173 @@
Метрики
=======
Система метрик ``Argenta`` предоставляет инструменты для измерения производительности ключевых компонентов библиотеки. Это позволяет отслеживать регрессию/прогрессию производительности между релизами и оптимизировать критические участки кода.
-----
Запуск метрик
-------------
Для работы с метриками необходимо склонировать репозиторий и установить зависимости:
.. code-block:: bash
git clone https://github.com/koloideal/Argenta.git
cd Argenta
uv sync --group metrics
Запуск системы метрик:
.. code-block:: bash
python -m metrics
После запуска откроется интерактивная сессия с доступными командами для работы с бенчмарками.
-----
Доступные команды
-----------------
run-all
~~~~~~~
Запускает все зарегистрированные бенчмарки и выводит результаты в виде таблиц.
**Синтаксис:**
.. code-block:: shell
run-all [--without-gc] [--without-system-info]
**Флаги:**
- ``--without-gc`` — отключает сборщик мусора во время выполнения бенчмарков для более стабильных результатов
- ``--without-system-info`` — скрывает информацию о системе в выводе
-----
list-types
~~~~~~~~~~
Выводит список всех доступных типов бенчмарков с количеством тестов в каждой категории.
**Синтаксис:**
.. code-block:: shell
list-types
**Пример вывода:**
.. code-block:: text
Available benchmark types:
• flag_validation (9 benchmarks)
• input_command_parse (7 benchmarks)
• finds_appropriate_handler (5 benchmarks)
-----
run-type
~~~~~~~~
Запускает бенчмарки определённого типа.
**Синтаксис:**
.. code-block:: shell
run-type --type <type_name> [--without-gc] [--without-system-info]
**Флаги:**
- ``--type`` — тип бенчмарков для запуска (обязательный)
- ``--without-gc`` — отключает сборщик мусора
- ``--without-system-info`` — скрывает информацию о системе
-----
diagrams-generate
~~~~~~~~~~~~~~~~~
Генерирует визуальные диаграммы сравнения производительности для всех бенчмарков.
**Синтаксис:**
.. code-block:: shell
diagrams-generate [--iterations <number>] [--without-gc]
**Флаги:**
- ``--iterations`` — количество итераций для каждого бенчмарка (по умолчанию 100)
- ``--without-gc`` — отключает сборщик мусора
Диаграммы сохраняются в директорию ``metrics/reports/diagrams/<timestamp>/``.
-----
release-generate
~~~~~~~~~~~~~~~~
Генерирует полный отчёт о производительности для текущей версии библиотеки. Используется при подготовке релизов.
**Синтаксис:**
.. code-block:: shell
release-generate
Команда автоматически:
1. Определяет текущую версию библиотеки
2. Запускает все бенчмарки с 1000 итераций и отключённым GC
3. Генерирует JSON-отчёты и диаграммы сравнения
4. Сохраняет результаты в ``metrics/reports/releases/<version>/``
-----
Интерпретация результатов
-------------------------
Результаты бенчмарков включают следующие метрики:
**Среднее время (mean)**
Среднее время выполнения операции. Основная метрика для сравнения производительности.
**Медиана (median)**
Медианное значение времени выполнения. Менее чувствительна к выбросам, чем среднее.
**Стандартное отклонение (std)**
Показывает стабильность измерений. Меньшее значение означает более предсказуемую производительность.
-----
Рекомендации по использованию
------------------------------
**Для оптимизации**
Используйте ``run-type`` для фокусировки на конкретной области и ``--without-gc`` для более точных измерений.
**Для визуализации**
Команда ``diagrams-generate`` создаёт наглядные графики, удобные для презентаций и документации.
**Для стабильных результатов**
Закройте ресурсоёмкие приложения, используйте флаг ``--without-gc`` и увеличивайте количество итераций через ``--iterations``.
-----
Добавление новых бенчмарков
----------------------------
Вы можете реализовать свои бенчмарки для тестирования специфичных юнитов библиотеки. Новые бенчмарки добавляются через декоратор ``@benchmarks.register``:
.. literalinclude:: ../code_snippets/metrics/add_new_benchmark.py
:language: python
:linenos:
.. important::
Бенчмарк должен быть импортирован в ``metrics/benchmarks/__init__.py`` для автоматической регистрации.
+2 -2
View File
@@ -29,9 +29,9 @@
Кастомизация вывода Кастомизация вывода
------------------- -------------------
Для полной замены логики вывода текста в конструкторе ``App`` предусмотрен параметр ``print_func``. Для полной замены логики вывода текста в конструкторе ``App`` предусмотрен параметр ``printer``.
* **print_func**: ``Callable[[str], None]`` * **printer**: ``Callable[[str], None]``
Этот параметр позволяет передать любую вызываемую сущность (например, функцию), которая будет использоваться для вывода всех системных сообщений. По умолчанию это ``rich.console.Console().print``. Вы можете передать сюда свою функцию, чтобы, например, логировать вывод в файл или отправлять его по сети. Этот параметр позволяет передать любую вызываемую сущность (например, функцию), которая будет использоваться для вывода всех системных сообщений. По умолчанию это ``rich.console.Console().print``. Вы можете передать сюда свою функцию, чтобы, например, логировать вывод в файл или отправлять его по сети.
.. important:: .. important::
-1
View File
@@ -1 +0,0 @@
from .benchmarks import *
+10 -40
View File
@@ -1,48 +1,18 @@
from concurrent.futures import ProcessPoolExecutor from argenta import App, Orchestrator, Command
import os from argenta.app import DynamicDividingLine
from .handlers import router
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.text import Text
from metrics.utils import run_benchmark, BenchmarkResult
from .registry import Benchmarks, Benchmark
def main(): app = App(initial_message="metrics", exit_command=Command('exit', aliases=['quit']))
console = Console() orchestrator = Orchestrator()
all_benchmarks: list[Benchmark] = Benchmarks.get_benchmarks()
workers = os.cpu_count() or 1
with ProcessPoolExecutor(max_workers=workers) as executor:
results = executor.map(run_benchmark, all_benchmarks)
type_paired_benchmarks: dict[str, list[BenchmarkResult]] = {} def main() -> None:
app.include_router(router)
for result in results: app.set_description_message_pattern(
type_paired_benchmarks.setdefault(result.type_, []).append(result) lambda command, description: f'[bold cyan]▸[/bold cyan] [bold white]{command}[/bold white] [dim]│[/dim] [yellow italic]{description}[/yellow italic]'
for type_, benchmarks in type_paired_benchmarks.items():
header_text = Text(f"TYPE: {type_.upper()}", style="bold magenta")
console.print(Panel(header_text, expand=False, border_style="magenta"))
table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True)
table.add_column("Name", style="green")
table.add_column("Description", style="dim")
table.add_column("Iterations", justify="right")
table.add_column("Avg Time (ms)", justify="right", style="bold yellow")
for benchmark in benchmarks:
table.add_row(
benchmark.name,
benchmark.description,
str(benchmark.iterations),
str(benchmark.avg_time)
) )
orchestrator.start_polling(app)
console.print(table)
console.print()
if __name__ == "__main__": if __name__ == "__main__":
+5
View File
@@ -1 +1,6 @@
from .pre_cycle_setup import * from .pre_cycle_setup import *
from .most_similar_command import *
from .finds_appropriate_handler import *
from .validate_routers_for_collisions import *
from .input_command_parse import *
from .flag_validation import *
View File
+20
View File
@@ -0,0 +1,20 @@
class BenchmarkNotFound(Exception):
def __init__(self, benchmark_name: str):
self.benchmark_name = benchmark_name
def __str__(self) -> str:
return f"Benchmark with name '{self.benchmark_name}' not found"
class BenchmarksNotFound(Exception):
def __init__(self, type_: str):
self.type_ = type_
def __str__(self) -> str:
return f"Benchmarks with type '{self.type_}' not found"
class BenchmarksWithSameNameAlreadyExists(Exception):
def __init__(self, benchmark_name: str):
self.benchmark_name = benchmark_name
def __str__(self) -> str:
return f"Benchmarks with name '{self.benchmark_name}' already exists"
+165
View File
@@ -0,0 +1,165 @@
__all__ = [
"Benchmark",
"Benchmarks",
"BenchmarkResult",
"BenchmarkGroupResult"
]
import io
from contextlib import redirect_stdout
from dataclasses import dataclass
import time
import gc
import statistics
from typing import Callable, override
from .exceptions import BenchmarkNotFound, BenchmarksNotFound, BenchmarksWithSameNameAlreadyExists
FuncForBenchmark = Callable[[], None]
MILLISECONDS_IN_SECONDS = 1000
@dataclass(frozen=True, slots=True)
class BenchmarkResult:
type_: str
name: str
description: str
iterations: int
is_gc_disabled: bool
avg_time: float
median_time: float
std_dev: float
@dataclass(frozen=True, slots=True)
class BenchmarkGroupResult:
type_: str
iterations: int
is_gc_disabled: bool
benchmark_results: list[BenchmarkResult]
class Benchmark:
def __init__(
self,
func: FuncForBenchmark,
*,
type_: str,
name: str,
description: str
) -> None:
self.func = func
self.type_ = type_
self.name = name
self.description = description
def single_run(self) -> float:
with redirect_stdout(io.StringIO()):
start = time.perf_counter()
self.func()
end = time.perf_counter()
return (end - start) * MILLISECONDS_IN_SECONDS
def multiple_runs(self, iterations: int, is_gc_disabled: bool = False) -> tuple[float, ...]:
run_attempts: list[float] = []
if is_gc_disabled:
was_gc_enabled = gc.isenabled()
gc.disable()
for _ in range(iterations):
run_attempts.append(self.single_run())
if was_gc_enabled:
gc.enable()
gc.collect()
return tuple(run_attempts)
else:
for _ in range(iterations):
run_attempts.append(self.single_run())
return tuple(run_attempts)
@override
def __repr__(self) -> str:
return f'Benchmark<{self.type_=}, {self.name=}, {self.description=}>'
@override
def __str__(self) -> str:
return f'benchmark {self.name} with type {self.type_}'
class Benchmarks:
def __init__(self, *benchmarks: Benchmark) -> None:
self._benchmarks: list[Benchmark] = list(benchmarks)
self._benchmarks_grouped_by_type: dict[str, list[Benchmark]] = {}
self._benchmarks_paired_by_name: dict[str, Benchmark] = {}
def register(
self,
type_: str,
description: str = ""
) -> Callable[[FuncForBenchmark], FuncForBenchmark]:
def decorator(func: FuncForBenchmark) -> FuncForBenchmark:
benchmark = Benchmark(
func,
type_=type_,
name=func.__name__,
description=description or f'description for {func.__name__} with type {type_}',
)
if self._benchmarks_paired_by_name.get(func.__name__):
raise BenchmarksWithSameNameAlreadyExists(func.__name__)
self._benchmarks_paired_by_name[func.__name__] = benchmark
self._benchmarks.append(benchmark)
self._benchmarks_grouped_by_type.setdefault(type_, []).append(benchmark)
return func
return decorator
def run_benchmark_by_name(self, name: str, iterations: int = 100, is_gc_disables: bool = False) -> BenchmarkResult:
benchmark = self.get_benchmark_by_name(name)
if not benchmark:
raise BenchmarkNotFound(name)
run_attempts: tuple[float, ...] = benchmark.multiple_runs(iterations, is_gc_disables)
avg = round(statistics.mean(run_attempts), 4)
median = round(statistics.median(run_attempts), 4)
std_dev = round(statistics.stdev(run_attempts) if len(run_attempts) > 1 else 0, 4)
return BenchmarkResult(
type_=benchmark.type_,
name=benchmark.name,
description=benchmark.description,
iterations=iterations,
is_gc_disabled=is_gc_disables,
avg_time=avg,
median_time=median,
std_dev=std_dev
)
def run_benchmarks_by_type(self, type_: str, iterations: int = 100, is_gc_disabled: bool = False) -> BenchmarkGroupResult:
benchmarks = self.get_benchmarks_by_type(type_)
if not benchmarks:
raise BenchmarksNotFound(type_)
benchmark_results: list[BenchmarkResult] = []
for benchmark in benchmarks:
benchmark_results.append(self.run_benchmark_by_name(benchmark.name, iterations, is_gc_disabled))
return BenchmarkGroupResult(
type_=type_,
iterations=iterations,
is_gc_disabled=is_gc_disabled,
benchmark_results=benchmark_results
)
def run_benchmarks_grouped_by_type(self, iterations: int = 100, is_gc_disabled: bool = False) -> list[BenchmarkGroupResult]:
results: list[BenchmarkGroupResult] = []
for type_, benchmarks in self._benchmarks_grouped_by_type.items():
results.append(self.run_benchmarks_by_type(type_, iterations, is_gc_disabled))
return results
def get_benchmarks_by_type(self, type_: str) -> list[Benchmark]:
return self._benchmarks_grouped_by_type.get(type_, [])
def get_benchmark_by_name(self, name: str) -> Benchmark | None:
return self._benchmarks_paired_by_name.get(name)
def get_types(self) -> set[str]:
return set(self._benchmarks_grouped_by_type.keys())
+3
View File
@@ -0,0 +1,3 @@
from .core.models import Benchmarks
benchmarks = Benchmarks()
@@ -0,0 +1,80 @@
__all__ = [
"benchmark_simple_command",
"benchmark_command_with_flags",
"benchmark_many_commands",
"benchmark_command_with_many_flags",
"benchmark_extreme_router"
]
from argenta.command.models import Command, InputCommand
from argenta.command import Flag, Flags
from argenta.response import Response
from argenta.router import Router
from .entity import benchmarks
@benchmarks.register(type_="finds_appropriate_handler", description="Simple command (no flags)")
def benchmark_simple_command() -> None:
router = Router()
@router.command(Command('test'))
def handler(_res: Response) -> None:
pass
input_cmd = InputCommand.parse('test')
router.finds_appropriate_handler(input_cmd)
@benchmarks.register(type_="finds_appropriate_handler", description="Command with flags (3 flags)")
def benchmark_command_with_flags() -> None:
router = Router()
@router.command(Command('test', flags=Flags([Flag('a'), Flag('b'), Flag('c')])))
def handler(_res: Response) -> None:
pass
input_cmd = InputCommand.parse('test -a -b -c')
router.finds_appropriate_handler(input_cmd)
@benchmarks.register(type_="finds_appropriate_handler", description="Many commands (50 commands)")
def benchmark_many_commands() -> None:
router = Router()
for i in range(50):
@router.command(Command(f'cmd{i}'))
def handler(_res: Response) -> None:
pass
input_cmd = InputCommand.parse('cmd25')
router.finds_appropriate_handler(input_cmd)
@benchmarks.register(type_="finds_appropriate_handler", description="Command with many flags (20 flags)")
def benchmark_command_with_many_flags() -> None:
router = Router()
flags = Flags([Flag(f'flag{i}') for i in range(20)])
@router.command(Command('test', flags=flags))
def handler(_res: Response) -> None:
pass
input_cmd = InputCommand.parse('test ' + ' '.join(f'-flag{i}' for i in range(10)))
router.finds_appropriate_handler(input_cmd)
@benchmarks.register(type_="finds_appropriate_handler", description="Extreme (100 commands, 10 flags each)")
def benchmark_extreme_router() -> None:
router = Router()
for i in range(100):
flags = Flags([Flag(f'f{i}_{j}') for j in range(10)])
@router.command(Command(f'cmd{i}', flags=flags))
def handler(_res: Response) -> None:
pass
input_cmd = InputCommand.parse('cmd50 -f50_0 -f50_1 -f50_2')
router.finds_appropriate_handler(input_cmd)
+102
View File
@@ -0,0 +1,102 @@
__all__ = [
"benchmark_validate_all_single_flag",
"benchmark_validate_neither_single_flag",
"benchmark_validate_list_small",
"benchmark_validate_list_large",
"benchmark_validate_regex_simple",
"benchmark_validate_regex_complex",
"benchmark_validate_multiple_flags_10",
"benchmark_validate_multiple_flags_50",
"benchmark_validate_extreme_100_flags"
]
import re
from argenta.command.flag import Flag, InputFlag, PossibleValues
from .entity import benchmarks
@benchmarks.register(type_="flag_validation", description="Single flag with PossibleValues.ALL")
def benchmark_validate_all_single_flag() -> None:
flag = Flag("test", possible_values=PossibleValues.ALL)
flag.validate_input_flag_value("some_value")
@benchmarks.register(type_="flag_validation", description="Single flag with PossibleValues.NEITHER")
def benchmark_validate_neither_single_flag() -> None:
flag = Flag("test", possible_values=PossibleValues.NEITHER)
flag.validate_input_flag_value("")
@benchmarks.register(type_="flag_validation", description="List validation (5 possible values)")
def benchmark_validate_list_small() -> None:
flag = Flag("env", possible_values=["dev", "staging", "prod", "test", "local"])
flag.validate_input_flag_value("prod")
@benchmarks.register(type_="flag_validation", description="List validation (50 possible values)")
def benchmark_validate_list_large() -> None:
possible_values = [f"value{i}" for i in range(50)]
flag = Flag("option", possible_values=possible_values)
flag.validate_input_flag_value("value25")
@benchmarks.register(type_="flag_validation", description="Regex validation (simple pattern)")
def benchmark_validate_regex_simple() -> None:
pattern = re.compile(r"^\d+$")
flag = Flag("port", possible_values=pattern)
flag.validate_input_flag_value("8080")
@benchmarks.register(type_="flag_validation", description="Regex validation (complex pattern)")
def benchmark_validate_regex_complex() -> None:
pattern = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
flag = Flag("email", possible_values=pattern)
flag.validate_input_flag_value("user@example.com")
@benchmarks.register(type_="flag_validation", description="Multiple flags validation (10 flags)")
def benchmark_validate_multiple_flags_10() -> None:
flags = [
Flag(f"flag{i}", possible_values=PossibleValues.ALL)
for i in range(10)
]
input_flags = [
InputFlag(f"flag{i}", input_value=f"value{i}")
for i in range(10)
]
for flag, input_flag in zip(flags, input_flags):
flag.validate_input_flag_value(input_flag.input_value)
@benchmarks.register(type_="flag_validation", description="Multiple flags validation (50 flags)")
def benchmark_validate_multiple_flags_50() -> None:
flags = [
Flag(f"flag{i}", possible_values=PossibleValues.ALL)
for i in range(50)
]
input_flags = [
InputFlag(f"flag{i}", input_value=f"value{i}")
for i in range(50)
]
for flag, input_flag in zip(flags, input_flags):
flag.validate_input_flag_value(input_flag.input_value)
@benchmarks.register(type_="flag_validation", description="Extreme (100 flags with regex validation)")
def benchmark_validate_extreme_100_flags() -> None:
pattern = re.compile(r"^[a-zA-Z0-9_-]+$")
flags = [
Flag(f"flag{i}", possible_values=pattern)
for i in range(100)
]
input_flags = [
InputFlag(f"flag{i}", input_value=f"valid_value_{i}")
for i in range(100)
]
for flag, input_flag in zip(flags, input_flags):
flag.validate_input_flag_value(input_flag.input_value)
+51
View File
@@ -0,0 +1,51 @@
__all__ = [
"benchmark_parse_simple_command",
"benchmark_command_with_few_flags",
"benchmark_command_with_flags_and_values",
"benchmark_command_with_mixed_prefixes",
"benchmark_command_with_long_values",
"benchmark_command_with_quoted_values",
"benchmark_extreme_many_flags"
]
from argenta.command.models import InputCommand
from .entity import benchmarks
@benchmarks.register(type_="input_command_parse", description="Simple command (no flags)")
def benchmark_parse_simple_command() -> None:
InputCommand.parse("start")
@benchmarks.register(type_="input_command_parse", description="Command with few flags (3 flags)")
def benchmark_command_with_few_flags() -> None:
InputCommand.parse("start -a -b -c")
@benchmarks.register(type_="input_command_parse", description="Command with flags and values (5 flags)")
def benchmark_command_with_flags_and_values() -> None:
InputCommand.parse("start --host localhost --port 8080 --debug --verbose -c config.json")
@benchmarks.register(type_="input_command_parse", description="Command with mixed prefixes (-, --, ---)")
def benchmark_command_with_mixed_prefixes() -> None:
InputCommand.parse("cmd -a --bb ---ccc -d value --ee value2 ---fff value3")
@benchmarks.register(type_="input_command_parse", description="Command with long values (10 flags)")
def benchmark_command_with_long_values() -> None:
long_value = "a" * 100
cmd = f"process --data {long_value} --config {long_value} --output {long_value}"
InputCommand.parse(cmd)
@benchmarks.register(type_="input_command_parse", description="Command with quoted values (5 flags)")
def benchmark_command_with_quoted_values() -> None:
InputCommand.parse("cmd --text 'hello world' --path '/usr/local/bin' --msg \"test message\"")
@benchmarks.register(type_="input_command_parse", description="Extreme (50 flags with values)")
def benchmark_extreme_many_flags() -> None:
flags = " ".join(f"--flag{i} value{i}" for i in range(50))
InputCommand.parse(f"command {flags}")
@@ -0,0 +1,59 @@
__all__ = [
"benchmark_few_commands",
"benchmark_many_commands_most_similar",
"benchmark_many_aliases",
"benchmark_partial_match",
"benchmark_extreme_commands"
]
from argenta import App
from argenta.command.models import Command
from argenta.response import Response
from argenta.router import Router
from .entity import benchmarks
def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) -> App:
app = App(override_system_messages=True)
router = Router()
for i in range(command_count):
aliases = {f'alias{i}_{j}' for j in range(aliases_per_command)} if aliases_per_command else set()
@router.command(Command(f'command{i}', aliases=aliases))
def handler(_res: Response) -> None:
pass
app.include_router(router)
return app
@benchmarks.register(type_="most_similar_command", description="Few commands (10 commands, no match)")
def benchmark_few_commands() -> None:
app = setup_app_with_commands(10)
app._most_similar_command("unknown")
@benchmarks.register(type_="most_similar_command", description="Many commands (50 commands, no match)")
def benchmark_many_commands_most_similar() -> None:
app = setup_app_with_commands(50)
app._most_similar_command("unknown")
@benchmarks.register(type_="most_similar_command", description="Many aliases (20 commands, 10 aliases each)")
def benchmark_many_aliases() -> None:
app = setup_app_with_commands(20, aliases_per_command=10)
app._most_similar_command("unknown")
@benchmarks.register(type_="most_similar_command", description="Partial match (50 commands, prefix match)")
def benchmark_partial_match() -> None:
app = setup_app_with_commands(50)
app._most_similar_command("comm")
@benchmarks.register(type_="most_similar_command", description="Extreme (100 commands, 20 aliases each)")
def benchmark_extreme_commands() -> None:
app = setup_app_with_commands(100, aliases_per_command=20)
app._most_similar_command("comm")
+18 -24
View File
@@ -1,22 +1,21 @@
__all__ = [ __all__ = [
"benchmark_no_aliases", "benchmark_no_aliases",
"benchmark_many_aliases", "benchmark_with_many_aliases",
"benchmark_few_aliases", "benchmark_few_aliases",
"benchmark_extreme_aliases", "benchmark_extreme_aliases",
"benchmark_very_many_aliases" "benchmark_very_many_aliases"
] ]
from argenta import App from argenta import App
from argenta.router import Router
from argenta.command.models import Command from argenta.command.models import Command
from argenta.response import Response from argenta.response import Response
from argenta.router import Router
from ..utils import get_time_of_pre_cycle_setup from .entity import benchmarks
from ..registry import benchmark
@benchmark(type_="pre_cycle_setup", description="With no aliases") @benchmarks.register(type_="pre_cycle_setup", description="With no aliases")
def benchmark_no_aliases() -> float: def benchmark_no_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@@ -33,12 +32,11 @@ def benchmark_no_aliases() -> float:
pass pass
app.include_router(router) app.include_router(router)
execution_time = get_time_of_pre_cycle_setup(app) app._pre_cycle_setup()
return execution_time
@benchmark(type_="pre_cycle_setup", description="With few aliases (6 total)") @benchmarks.register(type_="pre_cycle_setup", description="With few aliases (6 total)")
def benchmark_few_aliases() -> float: def benchmark_few_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@@ -55,12 +53,11 @@ def benchmark_few_aliases() -> float:
pass pass
app.include_router(router) app.include_router(router)
execution_time = get_time_of_pre_cycle_setup(app) app._pre_cycle_setup()
return execution_time
@benchmark(type_="pre_cycle_setup", description="With many aliases (15 total)") @benchmarks.register(type_="pre_cycle_setup", description="With many aliases (15 total)")
def benchmark_many_aliases() -> float: def benchmark_with_many_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@@ -77,12 +74,11 @@ def benchmark_many_aliases() -> float:
pass pass
app.include_router(router) app.include_router(router)
execution_time = get_time_of_pre_cycle_setup(app) app._pre_cycle_setup()
return execution_time
@benchmark(type_="pre_cycle_setup", description="With very many aliases (60 total)") @benchmarks.register(type_="pre_cycle_setup", description="With very many aliases (60 total)")
def benchmark_very_many_aliases() -> float: def benchmark_very_many_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@@ -99,12 +95,11 @@ def benchmark_very_many_aliases() -> float:
pass pass
app.include_router(router) app.include_router(router)
execution_time = get_time_of_pre_cycle_setup(app) app._pre_cycle_setup()
return execution_time
@benchmark(type_="pre_cycle_setup", description="With extreme aliases (300 total)") @benchmarks.register(type_="pre_cycle_setup", description="With extreme aliases (300 total)")
def benchmark_extreme_aliases() -> float: def benchmark_extreme_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@@ -121,5 +116,4 @@ def benchmark_extreme_aliases() -> float:
pass pass
app.include_router(router) app.include_router(router)
execution_time = get_time_of_pre_cycle_setup(app) app._pre_cycle_setup()
return execution_time
@@ -0,0 +1,102 @@
__all__ = [
"benchmark_few_routers",
"benchmark_many_routers",
"benchmark_many_commands_per_router",
"benchmark_many_aliases_per_command",
"benchmark_extreme_routers"
]
from argenta import App
from argenta.command.models import Command
from argenta.response import Response
from argenta.router import Router
from .entity import benchmarks
@benchmarks.register(type_="validate_routers_for_collisions", description="With few routers (3 routers, 1 command each)")
def benchmark_few_routers() -> None:
app = App(override_system_messages=True)
for i in range(3):
router = Router()
@router.command(Command(f'cmd{i}'))
def handler(_res: Response) -> None:
pass
app.include_router(router)
app._setup_system_router()
app._validate_routers_for_collisions()
@benchmarks.register(type_="validate_routers_for_collisions", description="With many routers (10 routers, 1 command each)")
def benchmark_many_routers() -> None:
app = App(override_system_messages=True)
for i in range(10):
router = Router()
@router.command(Command(f'cmd{i}'))
def handler(_res: Response) -> None:
pass
app.include_router(router)
app._setup_system_router()
app._validate_routers_for_collisions()
@benchmarks.register(type_="validate_routers_for_collisions", description="With many commands per router (3 routers, 10 commands each)")
def benchmark_many_commands_per_router() -> None:
app = App(override_system_messages=True)
for i in range(3):
router = Router()
for j in range(10):
@router.command(Command(f'cmd{i}_{j}'))
def handler(_res: Response) -> None:
pass
app.include_router(router)
app._setup_system_router()
app._validate_routers_for_collisions()
@benchmarks.register(type_="validate_routers_for_collisions", description="With many aliases (3 routers, 5 commands, 10 aliases each)")
def benchmark_many_aliases_per_command() -> None:
app = App(override_system_messages=True)
for i in range(3):
router = Router()
for j in range(5):
@router.command(Command(f'cmd{i}_{j}', aliases={f'alias{i}_{j}_{k}' for k in range(10)}))
def handler(_res: Response) -> None:
pass
app.include_router(router)
app._setup_system_router()
app._validate_routers_for_collisions()
@benchmarks.register(type_="validate_routers_for_collisions", description="Extreme (20 routers, 10 commands, 20 aliases each)")
def benchmark_extreme_routers() -> None:
app = App(override_system_messages=True)
for i in range(20):
router = Router()
for j in range(10):
@router.command(Command(f'cmd{i}_{j}', aliases={f'alias{i}_{j}_{k}' for k in range(20)}))
def handler(_res: Response) -> None:
pass
app.include_router(router)
app._setup_system_router()
app._validate_routers_for_collisions()
+179
View File
@@ -0,0 +1,179 @@
import re
from datetime import datetime
from importlib.metadata import version
from pathlib import Path
from rich.console import Console
from argenta.command import Flag, PossibleValues, Flags
from argenta.command.flag import ValidationStatus
from argenta.command.models import Command
from argenta.response import Response
from argenta.router import Router
from .benchmarks.core.models import BenchmarkGroupResult
from .benchmarks.entity import benchmarks as registered_benchmarks
from .services.report_table_generator import ReportTableGenerator
from .services.system_info_reader import get_system_info
from .services.diagram_generator import DiagramGenerator
from .services.release_generator import ReleaseGenerator
console = Console()
router = Router(title="Metrics commands:", disable_redirect_stdout=True)
POSITIVE_INTEGER_PATTERN = re.compile(r"^[1-9]\d*$")
@router.command(
Command(
"run-all",
description="Print all benchmarks results",
flags=Flags([
Flag('without-gc', possible_values=PossibleValues.NEITHER),
Flag('without-system-info', possible_values=PossibleValues.NEITHER)
])
)
)
def all_print_handler(response: Response) -> None:
report_generator = ReportTableGenerator(get_system_info())
without_system_info = response.input_flags.get_flag_by_name("without-system-info", with_status=ValidationStatus.VALID)
if not without_system_info:
console.print(report_generator.generate_system_info_header())
console.print(report_generator.generate_system_info_table())
is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID)
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(is_gc_disabled=bool(is_gc_disabled))
for benchmark_group_result in type_grouped_benchmarks:
console.print(report_generator.generate_benchmark_table_header(benchmark_group_result))
console.print(report_generator.generate_benchmark_report_table(benchmark_group_result))
@router.command(Command("list-types", description="List all benchmark types"))
def list_types_handler(_: Response) -> None:
types = registered_benchmarks.get_types()
if not types:
console.print("[yellow]No benchmark types found[/yellow]")
return
console.print("[bold cyan]Available benchmark types:[/bold cyan]\n")
for type_ in types:
benchmarks_count = len(registered_benchmarks.get_benchmarks_by_type(type_))
console.print(f" [green]•[/green] [bold]{type_}[/bold] ({benchmarks_count} benchmarks)")
@router.command(
Command(
"run-type",
description="Run benchmarks by specific type",
flags=Flags([
Flag('type', possible_values=registered_benchmarks.get_types()),
Flag('without-gc', possible_values=PossibleValues.NEITHER),
Flag('without-system-info', possible_values=PossibleValues.NEITHER)
])
)
)
def run_type_handler(response: Response) -> None:
type_flag = response.input_flags.get_flag_by_name("type")
if not type_flag:
console.print("[red]Error: --type flag is required[/red]")
console.print("[yellow]Usage: run-type --type <type_name>[/yellow]")
return
benchmark_type = type_flag.input_value
if not type_flag.status == ValidationStatus.VALID:
console.print(f"[red]Error: No benchmarks found for type '{benchmark_type}'[/red]")
console.print("\n[yellow]Available types:[/yellow]")
types = registered_benchmarks.get_types()
for t in types:
console.print(f"{t}")
return
report_generator = ReportTableGenerator(get_system_info())
without_system_info = response.input_flags.get_flag_by_name("without-system-info", with_status=ValidationStatus.VALID)
if not without_system_info:
console.print(report_generator.generate_system_info_header())
console.print(report_generator.generate_system_info_table())
is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID, default=False)
benchmark_group_result = registered_benchmarks.run_benchmarks_by_type(benchmark_type, is_gc_disabled=bool(is_gc_disabled))
console.print(report_generator.generate_benchmark_table_header(benchmark_group_result))
console.print(report_generator.generate_benchmark_report_table(benchmark_group_result))
@router.command(Command("release-generate", description="Generate release report"))
def release_generate_handler(_: Response) -> None:
lib_version = version("argenta")
console.print(f"[cyan]Generating release report for version:[/cyan] [bold]{lib_version}[/bold]")
console.print("[dim]Running benchmarks (1000 iterations, GC disabled)...[/dim]\n")
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(
iterations=1000,
is_gc_disabled=True
)
release_generator = ReleaseGenerator(lib_version)
output_dir = release_generator.generate_release(type_grouped_benchmarks)
console.print(f"[green]✓[/green] Benchmarks completed. Generating release report...\n")
for benchmark_group in type_grouped_benchmarks:
console.print(f"[cyan]Generated for:[/cyan] [bold]{benchmark_group.type_}[/bold]")
console.print(f" [green]✓[/green] {benchmark_group.type_}_comparison.png")
console.print(f" [green]✓[/green] {benchmark_group.type_}.json\n")
console.print(f"[bold green]✓ Release report generated successfully[/bold green]")
console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]")
@router.command(
Command(
"diagrams-generate",
description="Generate diagrams for all benchmarks",
flags=Flags([
Flag('without-gc', possible_values=PossibleValues.NEITHER),
Flag('iterations', possible_values=POSITIVE_INTEGER_PATTERN)
])
)
)
def diagrams_generate_handler(response: Response) -> None:
iterations = 100
iterations_flag = response.input_flags.get_flag_by_name("iterations", with_status=ValidationStatus.VALID)
if iterations_flag:
iterations = int(iterations_flag.input_value)
is_gc_disabled = bool(response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID))
console.print("[cyan]Running all benchmarks...[/cyan]")
console.print(f"[dim]Iterations: {iterations}, GC Disabled: {is_gc_disabled}[/dim]\n")
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(
iterations=iterations,
is_gc_disabled=is_gc_disabled
)
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
output_dir = Path("metrics/reports/diagrams") / timestamp
output_dir.mkdir(parents=True, exist_ok=True)
diagram_generator = DiagramGenerator(output_dir)
console.print(f"[green]✓[/green] Benchmarks completed. Generating diagrams...\n")
generated_count = 0
for benchmark_group in type_grouped_benchmarks:
console.print(f"[cyan]Generating diagram for:[/cyan] [bold]{benchmark_group.type_}[/bold]")
comparison_path = diagram_generator.generate_comparison_diagram(benchmark_group)
generated_count += 1
console.print(f" [green]✓[/green] {comparison_path.name}\n")
console.print(f"[bold green]✓ Successfully generated {generated_count} diagrams[/bold green]")
console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]")
-98
View File
@@ -1,98 +0,0 @@
__all__ = [
"Benchmark",
"Benchmarks",
"benchmark"
]
from typing import Callable, ClassVar, overload, override
BenchmarkAsFunc = Callable[[], float]
class Benchmark:
def __init__(
self,
func: BenchmarkAsFunc,
*,
type_: str,
name: str,
description: str,
iterations: int
) -> None:
self.func = func
self.type_ = type_
self.name = name
self.description = description
self.iterations = iterations
def run(self) -> float:
return self.func()
@override
def __repr__(self) -> str:
return f'Benchmark<{self.type_=}, {self.name=}, {self.description=}, {self.iterations=}>'
@override
def __str__(self) -> str:
return f'Benchmark({self.type_=}, {self.name=}, {self.description=}, {self.iterations=})'
class Benchmarks:
_benchmarks: ClassVar[list[Benchmark]] = []
@overload
@classmethod
def register(
cls,
call: BenchmarkAsFunc,
*,
type_: str = "",
description: str = "",
iterations: int = 100,
) -> BenchmarkAsFunc:
...
@overload
@classmethod
def register(
cls,
call: None = None,
*,
type_: str = "",
description: str = "",
iterations: int = 100,
) -> Callable[[BenchmarkAsFunc], BenchmarkAsFunc]:
...
@classmethod
def register(
cls,
call: BenchmarkAsFunc | None = None,
*,
type_: str = "",
description: str = "",
iterations: int = 100,
) -> Callable[[BenchmarkAsFunc], BenchmarkAsFunc] | BenchmarkAsFunc:
def decorator(func: BenchmarkAsFunc) -> BenchmarkAsFunc:
cls._benchmarks.append(
Benchmark(
func,
type_=type_,
name=func.__name__,
description=description or f'description for {func.__name__} with {iterations} iterations',
iterations=iterations
)
)
return func
if call is None:
return decorator
else:
return decorator(call)
@classmethod
def get_benchmarks(cls) -> list[Benchmark]:
return cls._benchmarks
benchmark = Benchmarks.register
@@ -0,0 +1,42 @@
{
"type": "finds_appropriate_handler",
"iterations": 1000,
"gc_disabled": true,
"benchmarks": [
{
"name": "benchmark_simple_command",
"description": "Simple command (no flags)",
"avg_time": 0.036,
"median_time": 0.0354,
"std_dev": 0.0087
},
{
"name": "benchmark_command_with_flags",
"description": "Command with flags (3 flags)",
"avg_time": 0.0557,
"median_time": 0.0545,
"std_dev": 0.0171
},
{
"name": "benchmark_many_commands",
"description": "Many commands (50 commands)",
"avg_time": 1.0453,
"median_time": 1.0388,
"std_dev": 0.0322
},
{
"name": "benchmark_command_with_many_flags",
"description": "Command with many flags (20 flags)",
"avg_time": 0.1322,
"median_time": 0.131,
"std_dev": 0.0045
},
{
"name": "benchmark_extreme_router",
"description": "Extreme (100 commands, 10 flags each)",
"avg_time": 3.2471,
"median_time": 3.235,
"std_dev": 0.0814
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

@@ -0,0 +1,70 @@
{
"type": "flag_validation",
"iterations": 1000,
"gc_disabled": true,
"benchmarks": [
{
"name": "benchmark_validate_all_single_flag",
"description": "Single flag with PossibleValues.ALL",
"avg_time": 0.0008,
"median_time": 0.0008,
"std_dev": 0.0002
},
{
"name": "benchmark_validate_neither_single_flag",
"description": "Single flag with PossibleValues.NEITHER",
"avg_time": 0.0008,
"median_time": 0.0008,
"std_dev": 0.0002
},
{
"name": "benchmark_validate_list_small",
"description": "List validation (5 possible values)",
"avg_time": 0.001,
"median_time": 0.0009,
"std_dev": 0.0007
},
{
"name": "benchmark_validate_list_large",
"description": "List validation (50 possible values)",
"avg_time": 0.0079,
"median_time": 0.0078,
"std_dev": 0.0021
},
{
"name": "benchmark_validate_regex_simple",
"description": "Regex validation (simple pattern)",
"avg_time": 0.0017,
"median_time": 0.0016,
"std_dev": 0.0028
},
{
"name": "benchmark_validate_regex_complex",
"description": "Regex validation (complex pattern)",
"avg_time": 0.0018,
"median_time": 0.0016,
"std_dev": 0.0051
},
{
"name": "benchmark_validate_multiple_flags_10",
"description": "Multiple flags validation (10 flags)",
"avg_time": 0.0145,
"median_time": 0.0144,
"std_dev": 0.0013
},
{
"name": "benchmark_validate_multiple_flags_50",
"description": "Multiple flags validation (50 flags)",
"avg_time": 0.0661,
"median_time": 0.0658,
"std_dev": 0.0024
},
{
"name": "benchmark_validate_extreme_100_flags",
"description": "Extreme (100 flags with regex validation)",
"avg_time": 0.1599,
"median_time": 0.1589,
"std_dev": 0.0065
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

@@ -0,0 +1,56 @@
{
"type": "input_command_parse",
"iterations": 1000,
"gc_disabled": true,
"benchmarks": [
{
"name": "benchmark_parse_simple_command",
"description": "Simple command (no flags)",
"avg_time": 0.0096,
"median_time": 0.0095,
"std_dev": 0.0012
},
{
"name": "benchmark_command_with_few_flags",
"description": "Command with few flags (3 flags)",
"avg_time": 0.0216,
"median_time": 0.0213,
"std_dev": 0.0021
},
{
"name": "benchmark_command_with_flags_and_values",
"description": "Command with flags and values (5 flags)",
"avg_time": 0.06,
"median_time": 0.0595,
"std_dev": 0.0025
},
{
"name": "benchmark_command_with_mixed_prefixes",
"description": "Command with mixed prefixes (-, --, ---)",
"avg_time": 0.0542,
"median_time": 0.0538,
"std_dev": 0.0028
},
{
"name": "benchmark_command_with_long_values",
"description": "Command with long values (10 flags)",
"avg_time": 0.2092,
"median_time": 0.2082,
"std_dev": 0.0067
},
{
"name": "benchmark_command_with_quoted_values",
"description": "Command with quoted values (5 flags)",
"avg_time": 0.0481,
"median_time": 0.0477,
"std_dev": 0.0023
},
{
"name": "benchmark_extreme_many_flags",
"description": "Extreme (50 flags with values)",
"avg_time": 0.7907,
"median_time": 0.7884,
"std_dev": 0.0417
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

@@ -0,0 +1,42 @@
{
"type": "most_similar_command",
"iterations": 1000,
"gc_disabled": true,
"benchmarks": [
{
"name": "benchmark_few_commands",
"description": "Few commands (10 commands, no match)",
"avg_time": 0.251,
"median_time": 0.2488,
"std_dev": 0.012
},
{
"name": "benchmark_many_commands_most_similar",
"description": "Many commands (50 commands, no match)",
"avg_time": 1.1933,
"median_time": 1.1878,
"std_dev": 0.0305
},
{
"name": "benchmark_many_aliases",
"description": "Many aliases (20 commands, 10 aliases each)",
"avg_time": 1.2151,
"median_time": 1.2124,
"std_dev": 0.0282
},
{
"name": "benchmark_partial_match",
"description": "Partial match (50 commands, prefix match)",
"avg_time": 1.6781,
"median_time": 1.6689,
"std_dev": 0.0573
},
{
"name": "benchmark_extreme_commands",
"description": "Extreme (100 commands, 20 aliases each)",
"avg_time": 10.5539,
"median_time": 10.5288,
"std_dev": 0.1603
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

@@ -0,0 +1,42 @@
{
"type": "pre_cycle_setup",
"iterations": 1000,
"gc_disabled": true,
"benchmarks": [
{
"name": "benchmark_no_aliases",
"description": "With no aliases",
"avg_time": 7.4799,
"median_time": 7.4576,
"std_dev": 0.1645
},
{
"name": "benchmark_few_aliases",
"description": "With few aliases (6 total)",
"avg_time": 7.4135,
"median_time": 7.4061,
"std_dev": 0.1709
},
{
"name": "benchmark_with_many_aliases",
"description": "With many aliases (15 total)",
"avg_time": 7.4018,
"median_time": 7.3943,
"std_dev": 0.1589
},
{
"name": "benchmark_very_many_aliases",
"description": "With very many aliases (60 total)",
"avg_time": 7.476,
"median_time": 7.4575,
"std_dev": 0.2156
},
{
"name": "benchmark_extreme_aliases",
"description": "With extreme aliases (300 total)",
"avg_time": 7.7167,
"median_time": 7.706,
"std_dev": 0.2052
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

@@ -0,0 +1,42 @@
{
"type": "validate_routers_for_collisions",
"iterations": 1000,
"gc_disabled": true,
"benchmarks": [
{
"name": "benchmark_few_routers",
"description": "With few routers (3 routers, 1 command each)",
"avg_time": 0.0959,
"median_time": 0.0944,
"std_dev": 0.0097
},
{
"name": "benchmark_many_routers",
"description": "With many routers (10 routers, 1 command each)",
"avg_time": 0.2488,
"median_time": 0.2467,
"std_dev": 0.0081
},
{
"name": "benchmark_many_commands_per_router",
"description": "With many commands per router (3 routers, 10 commands each)",
"avg_time": 0.6474,
"median_time": 0.6401,
"std_dev": 0.0304
},
{
"name": "benchmark_many_aliases_per_command",
"description": "With many aliases (3 routers, 5 commands, 10 aliases each)",
"avg_time": 0.5261,
"median_time": 0.5156,
"std_dev": 0.0475
},
{
"name": "benchmark_extreme_routers",
"description": "Extreme (20 routers, 10 commands, 20 aliases each)",
"avg_time": 9.9128,
"median_time": 9.9518,
"std_dev": 0.2373
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

+6
View File
@@ -0,0 +1,6 @@
from .diagram_generator import DiagramGenerator
from .report_table_generator import ReportTableGenerator
from .system_info_reader import get_system_info
from .release_generator import ReleaseGenerator
__all__ = ["DiagramGenerator", "ReportTableGenerator", "get_system_info", "ReleaseGenerator"]
+110
View File
@@ -0,0 +1,110 @@
__all__ = ["DiagramGenerator"]
from pathlib import Path
import matplotlib
import matplotlib.pyplot as plt
from ..benchmarks.core.models import BenchmarkGroupResult
class DiagramGenerator:
def __init__(self, output_dir: Path | str) -> None:
self.output_dir: Path = Path(output_dir) if isinstance(output_dir, str) else output_dir
matplotlib.use('Agg')
plt.style.use('seaborn-v0_8-whitegrid')
def generate_comparison_diagram(self, benchmark_group: BenchmarkGroupResult) -> Path:
results = benchmark_group.benchmark_results
sorted_results = sorted(results, key=lambda br: br.avg_time)
descriptions: list[str] = [br.description for br in sorted_results]
avg_times: list[float] = [br.avg_time for br in sorted_results]
median_times: list[float] = [br.median_time for br in sorted_results]
std_devs: list[float] = [br.std_dev for br in sorted_results]
max_value = max(
max(avg_times) if avg_times else 0,
max(median_times) if median_times else 0,
max(std_devs) if std_devs else 0
)
y_limit = max_value / 0.85 if max_value > 0 else 1.0
items_count = len(descriptions)
x_positions: list[int] = list(range(items_count))
bar_width = 0.25
x_std_dev = [x - bar_width for x in x_positions]
x_avg = [x for x in x_positions]
x_median = [x + bar_width for x in x_positions]
fig, ax = plt.subplots(figsize=(16, 8))
fig.patch.set_facecolor('white')
bars_std = ax.bar(x_std_dev, std_devs, bar_width, label='Std Deviation',
color='#2ecc71', alpha=0.9, edgecolor='#27ae60', linewidth=1.5)
bars_avg = ax.bar(x_avg, avg_times, bar_width, label='Average Time',
color='#3498db', alpha=0.9, edgecolor='#2980b9', linewidth=1.5)
bars_median = ax.bar(x_median, median_times, bar_width, label='Median Time',
color='#e74c3c', alpha=0.9, edgecolor='#c0392b', linewidth=1.5)
for bar_group in [bars_std, bars_avg, bars_median]:
for bar in bar_group:
height = bar.get_height()
ax.text(
bar.get_x() + bar.get_width() / 2.,
height,
f'{height:.3f}',
ha='center', va='bottom', fontsize=9, fontweight='bold'
)
ax.set_ylabel('Time (ms)', fontsize=14, fontweight='bold', labelpad=10)
title_text = f'{benchmark_group.type_.replace("_", " ").title()}'
metadata_text = f'Iterations: {benchmark_group.iterations} | GC: {"Disabled" if benchmark_group.is_gc_disabled else "Enabled"}'
ax.text(0.5, 1.08, title_text, transform=ax.transAxes,
fontsize=18, fontweight='bold', ha='center', color='#2c3e50')
ax.text(0.5, 1.03, metadata_text, transform=ax.transAxes,
fontsize=12, ha='center', color='#7f8c8d', style='italic')
ax.set_xticks(x_positions)
ax.set_xticklabels([])
for i, (pos, desc) in enumerate(zip(x_positions, descriptions)):
text_x_pos = pos - bar_width - (bar_width / 2)
ax.text(
text_x_pos,
y_limit * 0.02,
desc,
rotation=90, va='bottom', ha='right', fontsize=10,
color='#2c3e50'
)
ax.set_ylim(0, y_limit)
legend = ax.legend(loc='upper left', fontsize=12, framealpha=0.95,
edgecolor='#34495e', fancybox=True, shadow=True)
legend.get_frame().set_facecolor('#ecf0f1')
ax.grid(axis='y', alpha=0.4, linestyle='--', linewidth=0.8)
ax.set_axisbelow(True)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color('#7f8c8d')
ax.spines['bottom'].set_color('#7f8c8d')
plt.tight_layout()
filename = f"{benchmark_group.type_}_comparison.png"
output_path = self.output_dir / filename
self.output_dir.mkdir(parents=True, exist_ok=True)
plt.savefig(output_path, dpi=200, bbox_inches='tight', facecolor='white')
plt.close(fig)
return output_path
+49
View File
@@ -0,0 +1,49 @@
__all__ = ["ReleaseGenerator"]
import json
import shutil
from pathlib import Path
from ..benchmarks.core.models import BenchmarkGroupResult
from .diagram_generator import DiagramGenerator
class ReleaseGenerator:
def __init__(self, lib_version: str) -> None:
self.lib_version = lib_version
self.output_dir = Path("metrics/reports/releases") / lib_version
def generate_release(self, benchmark_groups: list[BenchmarkGroupResult]) -> Path:
if self.output_dir.exists():
shutil.rmtree(self.output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
for benchmark_group in benchmark_groups:
type_dir = self.output_dir / benchmark_group.type_
type_dir.mkdir(exist_ok=True)
diagram_generator = DiagramGenerator(type_dir)
diagram_generator.generate_comparison_diagram(benchmark_group)
json_data = {
"type": benchmark_group.type_,
"iterations": benchmark_group.iterations,
"gc_disabled": benchmark_group.is_gc_disabled,
"benchmarks": [
{
"name": br.name,
"description": br.description,
"avg_time": br.avg_time,
"median_time": br.median_time,
"std_dev": br.std_dev
}
for br in benchmark_group.benchmark_results
]
}
json_path = type_dir / f"{benchmark_group.type_}.json"
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(json_data, f, indent=2, ensure_ascii=False)
return self.output_dir
@@ -0,0 +1,72 @@
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from ..benchmarks.core.models import BenchmarkGroupResult
from metrics.services.system_info_reader import SystemInfo
class ReportTableGenerator:
def __init__(self, system_info: SystemInfo):
self.system_info = system_info
self._cached_benchmark_tables: dict[int, Table] = {}
self._cached_system_info_table: Table | None = None
def generate_benchmark_report_table(self, benchmark_group_result: BenchmarkGroupResult) -> Table:
if cached_result := self._cached_benchmark_tables.get(id(benchmark_group_result)):
return cached_result
table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True)
table.add_column("Description", style="dim")
table.add_column("Avg Time", justify="right", style="bold yellow")
table.add_column("Median Time", justify="right", style="bold yellow")
table.add_column("Stdev", justify="right", style="bold yellow")
for benchmark in benchmark_group_result.benchmark_results:
table.add_row(
benchmark.description,
str(benchmark.avg_time),
str(benchmark.median_time),
str(benchmark.std_dev),
)
self._cached_benchmark_tables[id(benchmark_group_result)] = table
return table
@staticmethod
def generate_benchmark_table_header(benchmark_group_result: BenchmarkGroupResult) -> Panel:
header_text = Text(f"TYPE: {benchmark_group_result.type_.upper()} ; "
f"ITERATIONS: {benchmark_group_result.iterations} ; "
f"GC {"DISABLED" if benchmark_group_result.is_gc_disabled else "ENABLED"} ; "
f"ALL TIME IN MS",
style="bold magenta")
return Panel(header_text, expand=False, border_style="magenta")
def generate_system_info_table(self) -> Table:
if self._cached_system_info_table is not None:
return self._cached_system_info_table
table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True)
table.add_column("Parameter", style="green")
table.add_column("Value", style="yellow")
table.add_row("OS Name", self.system_info.os_info.name)
table.add_row("OS Kernel Version", self.system_info.os_info.kernel_version)
table.add_row("Architecture", self.system_info.cpu_info.architecture)
table.add_row("CPU", self.system_info.cpu_info.name)
table.add_row("CPU Physical Cores", str(self.system_info.cpu_info.physical_cores))
table.add_row("CPU Logical Cores", str(self.system_info.cpu_info.logical_cores))
table.add_row("CPU Max Frequency", str(self.system_info.cpu_info.max_frequency) + ' GHz')
table.add_row("Total RAM", str(self.system_info.memory_info.total_ram) + ' GB')
table.add_row("Used RAM", str(self.system_info.memory_info.used_ram) + ' GB')
table.add_row("Available RAM", str(self.system_info.memory_info.available_ram) + ' GB')
table.add_row("Python Version", self.system_info.python_info.version)
table.add_row("Python Implementation", self.system_info.python_info.implementation)
table.add_row("Python Compiler", self.system_info.python_info.compiler)
self._cached_system_info_table = table
return table
@staticmethod
def generate_system_info_header() -> Panel:
header_text = Text("SYSTEM INFO", style="bold magenta")
return Panel(header_text, expand=False, border_style="magenta")
+126
View File
@@ -0,0 +1,126 @@
__all__ = [
"SystemInfo",
"get_system_info"
]
from dataclasses import dataclass
import platform
import sys
import cpuinfo
import psutil
@dataclass(frozen=True, slots=True)
class SystemInfo:
os_info: OSInfo
cpu_info: CPUInfo
memory_info: MemoryInfo
python_info: PythonInfo
@dataclass(frozen=True, slots=True)
class OSInfo:
name: str
kernel_version: str
@dataclass(frozen=True, slots=True)
class CPUInfo:
name: str
architecture: str
physical_cores: int
logical_cores: int
max_frequency: float
@dataclass(frozen=True, slots=True)
class MemoryInfo:
total_ram: float # in GB
used_ram: float # in GB
available_ram: float # in GB
@dataclass(frozen=True, slots=True)
class PythonInfo:
version: str
implementation: str
compiler: str
def get_system_info() -> SystemInfo:
os_info = get_os_info()
cpu_info = get_cpu_info()
memory_info = get_memory_info()
python_info = get_python_info()
return SystemInfo(
os_info=os_info,
cpu_info=cpu_info,
memory_info=memory_info,
python_info=python_info,
)
def get_os_info() -> OSInfo:
system = platform.system()
if system == "Windows":
ver = sys.getwindowsversion()
kernel_version = f"{ver.major}.{ver.minor}.{ver.build}"
if ver.build >= 22000:
product_name = "Windows 11"
else:
product_name = "Windows 10"
return OSInfo(
name=product_name,
kernel_version=kernel_version,
)
elif system == "Darwin":
return OSInfo(
kernel_version=platform.release(),
name=f"macOS {platform.mac_ver()[0]}"
)
else:
return OSInfo(
kernel_version=platform.release(),
name=platform.system()
)
def get_cpu_info() -> CPUInfo:
cpu_info = cpuinfo.get_cpu_info()
cpu_name = cpu_info["brand_raw"]
cpu_architecture = cpu_info["arch"]
cpu_physical_cores = psutil.cpu_count(logical=False)
cpu_logical_cores = psutil.cpu_count(logical=True)
cpu_freq = psutil.cpu_freq()
cpu_max_frequency = cpu_freq.max
return CPUInfo(
name=cpu_name,
architecture=cpu_architecture,
physical_cores=cpu_physical_cores,
logical_cores=cpu_logical_cores,
max_frequency=cpu_max_frequency
)
def get_memory_info() -> MemoryInfo:
mem = psutil.virtual_memory()
total_ram = round(mem.total / (1024**3), 2)
used_ram = round(mem.used / (1024**3), 2)
available_ram = round(mem.available / (1024**3), 2)
return MemoryInfo(
total_ram=total_ram,
used_ram=used_ram,
available_ram=available_ram,
)
def get_python_info() -> PythonInfo:
python_version = platform.python_version()
python_implementation = platform.python_implementation()
python_compiler = platform.python_compiler()
return PythonInfo(
version=python_version,
implementation=python_implementation,
compiler=python_compiler
)
-44
View File
@@ -1,44 +0,0 @@
__all__ = [
"get_time_of_pre_cycle_setup",
"attempts_to_average",
"run_benchmark",
"BenchmarkResult"
]
import io
from contextlib import redirect_stdout
import time
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP
from argenta import App
from metrics.registry import Benchmark
def get_time_of_pre_cycle_setup(app: App) -> float:
start = time.perf_counter()
with redirect_stdout(io.StringIO()):
app._pre_cycle_setup() # pyright: ignore[reportPrivateUsage]
end = time.perf_counter()
return (end - start) * 1000 # as milliseconds
def attempts_to_average(bench_attempts: list[float], iterations: int) -> Decimal:
return Decimal(sum(bench_attempts) / iterations).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
@dataclass(frozen=True)
class BenchmarkResult:
type_: str
name: str
description: str
iterations: int
avg_time: Decimal
def run_benchmark(benchmark: Benchmark) -> BenchmarkResult:
bench_attempts: list[float] = []
for _ in range(benchmark.iterations):
bench_attempts.append(benchmark.run())
avg = attempts_to_average(bench_attempts, benchmark.iterations)
return BenchmarkResult(benchmark.type_, benchmark.name, benchmark.description, benchmark.iterations, avg)
+14 -10
View File
@@ -1,14 +1,18 @@
from argenta.app import AutoCompleter from argenta import App, Command, Response, Router
if __name__ == "__main__": app = App(override_system_messages=True)
test_commands: set[str] = {"start", "qwertyu", "stop", "exit"} router = Router()
hist_file: str = "history.txt"
ac: AutoCompleter = AutoCompleter(autocomplete_button='tab') @router.command(Command('command'))
ac.initial_setup(test_commands) def handler(_res: Response) -> None:
pass
while True: @router.command(Command('command_other'))
inp: str = ac.prompt(">>> ").strip() def handler2(_res: Response) -> None:
if inp == "exit": pass
break
app.include_routers(router)
app._pre_cycle_setup()
assert app._most_similar_command('command_') == 'command'
+1 -1
View File
@@ -4,7 +4,7 @@ from argenta.app import DynamicDividingLine
from .routers import router from .routers import router
app: App = App(prompt='>>> ', dividing_line=DynamicDividingLine('~')) app: App = App(prompt='>>> ', dividing_line=None)
orchestrator: Orchestrator = Orchestrator() orchestrator: Orchestrator = Orchestrator()
def main() -> None: def main() -> None:
+4 -2
View File
@@ -1,10 +1,12 @@
from prompt_toolkit import HTML
from argenta import App, Orchestrator from argenta import App, Orchestrator
from argenta.app import PredefinedMessages from argenta.app import PredefinedMessages, StaticDividingLine, AutoCompleter
from argenta.app.dividing_line.models import DynamicDividingLine from argenta.app.dividing_line.models import DynamicDividingLine
from mock.mock_app.routers import work_router from mock.mock_app.routers import work_router
app: App = App( app: App = App(
dividing_line=DynamicDividingLine('^'), dividing_line=StaticDividingLine('~')
) )
orchestrator: Orchestrator = Orchestrator() orchestrator: Orchestrator = Orchestrator()
+4 -3
View File
@@ -1,7 +1,8 @@
from argenta import Command, Response, Router from argenta import Command, Response, Router
from argenta.command import Flag, Flags from argenta.command import Flag, Flags
from argenta.command.flag import ValidationStatus
work_router: Router = Router(title="Base points:", disable_redirect_stdout=True) work_router: Router = Router(title="Base points:")
@work_router.command( @work_router.command(
@@ -13,5 +14,5 @@ work_router: Router = Router(title="Base points:", disable_redirect_stdout=True)
description="Hello, world!") description="Hello, world!")
) )
def command_help(response: Response): def command_help(response: Response):
c = input("Enter your name: ") n = input('sfgdheth')
print(f"Hello, {c}!") print(f"Hello,{n} {response.input_flags.get_flag_by_name('test', with_status=ValidationStatus.VALID)}")
+30 -13
View File
@@ -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",
@@ -35,17 +42,14 @@ tests = [
"pytest-cov>=7.0.0", "pytest-cov>=7.0.0",
"pytest-mock>=3.15.1", "pytest-mock>=3.15.1",
] ]
metrics = [
"matplotlib>=3.10.8",
"psutil>=7.2.1",
"py-cpuinfo>=9.0.0",
]
[tool.ruff] [tool.ruff]
exclude = [ line-length=100
".idea",
"venv",
".git",
"poetry.lock",
".__pycache__",
"tests"
]
line-length=90
[tool.pyright] [tool.pyright]
typeCheckingMode = "strict" typeCheckingMode = "strict"
@@ -68,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"
@@ -75,5 +92,5 @@ disable_error_code = "import-untyped"
line_length=90 line_length=90
[build-system] [build-system]
requires = ["hatchling"] requires = ["uv_build"]
build-backend = "hatchling.build" build-backend = "uv_build"
+19 -14
View File
@@ -3,12 +3,14 @@ __all__ = ["AutoCompleter"]
import sys import sys
from typing import Callable, Iterable from typing import Callable, Iterable
from prompt_toolkit import PromptSession, HTML from prompt_toolkit import HTML, PromptSession
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import Completer, Completion, CompleteEvent from prompt_toolkit.completion import (CompleteEvent, Completer, Completion,
ThreadedCompleter)
from prompt_toolkit.cursor_shapes import CursorShape
from prompt_toolkit.document import Document from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import StyleAndTextTuples from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.history import History, ThreadedHistory, FileHistory, InMemoryHistory from prompt_toolkit.history import FileHistory, History, InMemoryHistory, ThreadedHistory
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
from prompt_toolkit.lexers import Lexer from prompt_toolkit.lexers import Lexer
from prompt_toolkit.styles import Style from prompt_toolkit.styles import Style
@@ -97,34 +99,36 @@ class AutoCompleter:
def _(event: KeyPressEvent) -> None: def _(event: KeyPressEvent) -> None:
buff = event.app.current_buffer buff = event.app.current_buffer
if buff.complete_state: if buff.complete_state:
buff.complete_next() buff.complete_next()
else: return
completions = list(buff.completer.get_completions(buff.document, CompleteEvent())) comps_gen = iter(buff.completer.get_completions(buff.document, CompleteEvent()))
if len(completions) == 1: try:
buff.apply_completion(completions[0]) first = next(comps_gen)
else: except StopIteration:
return
try:
_ = next(comps_gen)
buff.start_completion(select_first=False) buff.start_completion(select_first=False)
except StopIteration:
buff.apply_completion(first)
kb.add(self.autocomplete_button)(_) kb.add(self.autocomplete_button)(_)
history: InMemoryHistory | ThreadedHistory history: InMemoryHistory | ThreadedHistory
if self.history_filename: if self.history_filename:
history = ThreadedHistory(FileHistory(self.history_filename)) history = ThreadedHistory(FileHistory(self.history_filename))
else: else:
history = InMemoryHistory() history = InMemoryHistory()
style = Style.from_dict({'valid': '#00ff00', 'invalid': '#ff0000'}) style = Style.from_dict({'valid': '#00ff00', 'invalid': '#ff0000'})
self._session = PromptSession( self._session = PromptSession(
history=history, history=history,
completer=HistoryCompleter(history, all_commands), completer=ThreadedCompleter(HistoryCompleter(history, all_commands)),
complete_while_typing=False, complete_while_typing=False,
key_bindings=kb, key_bindings=kb,
auto_suggest=AutoSuggestFromHistory() if self.auto_suggestions else None, auto_suggest=AutoSuggestFromHistory() if self.auto_suggestions else None,
style=style if self.command_highlighting else style, style=style if self.command_highlighting else None,
lexer=CommandLexer(all_commands) if self.command_highlighting else None, lexer=CommandLexer(all_commands) if self.command_highlighting else None,
) )
@@ -134,5 +138,6 @@ class AutoCompleter:
if self._session is None: if self._session is None:
raise RuntimeError("Call initial_setup() before using prompt()") raise RuntimeError("Call initial_setup() before using prompt()")
return self._session.prompt( return self._session.prompt(
HTML(prompt_text) if isinstance(prompt_text, str) else prompt_text HTML(prompt_text) if isinstance(prompt_text, str) else prompt_text,
cursor=CursorShape.BLINKING_BEAM
) )
+104
View File
@@ -0,0 +1,104 @@
from rich.markup import escape
from argenta.app.presentation.renderers import Renderer
from argenta.app.protocols import (DescriptionMessageGenerator, EmptyCommandHandler,
MostSimilarCommandGetter, NonStandardBehaviorHandler,
Printer)
from argenta.command import InputCommand
from argenta.response.entity import Response
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(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
)
class BehaviorHandlersSettersMixin:
def __init__(
self,
description_message_generator: DescriptionMessageGenerator,
incorrect_input_syntax_handler: NonStandardBehaviorHandler[str],
repeated_input_flags_handler: NonStandardBehaviorHandler[str],
empty_input_command_handler: EmptyCommandHandler,
unknown_command_handler: NonStandardBehaviorHandler[InputCommand],
exit_command_handler: NonStandardBehaviorHandler[Response]
):
self._description_message_generator: DescriptionMessageGenerator = description_message_generator
self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = incorrect_input_syntax_handler
self._repeated_input_flags_handler: NonStandardBehaviorHandler[str] = repeated_input_flags_handler
self._empty_input_command_handler: EmptyCommandHandler = empty_input_command_handler
self._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = unknown_command_handler
self._exit_command_handler: NonStandardBehaviorHandler[Response] = exit_command_handler
def set_description_message_pattern(self, _: DescriptionMessageGenerator, /) -> None:
self._description_message_generator = _
def set_incorrect_input_syntax_handler(self, _: NonStandardBehaviorHandler[str], /) -> None:
self._incorrect_input_syntax_handler = _
def set_repeated_input_flags_handler(self, _: NonStandardBehaviorHandler[str], /) -> None:
self._repeated_input_flags_handler = _
def set_unknown_command_handler(self, _: NonStandardBehaviorHandler[InputCommand], /) -> None:
self._unknown_command_handler = _
def set_empty_command_handler(self, _: EmptyCommandHandler, /) -> None:
"""
Public. Sets the handler for empty commands when entering a command
:param _: handler for empty commands when entering a command
:return: None
"""
self._empty_input_command_handler = _
def set_exit_command_handler(self, _: NonStandardBehaviorHandler[Response], /) -> None:
"""
Public. Sets the handler for exit command when entering a command
:param _: handler for exit command when entering a command
:return: None
"""
self._exit_command_handler = _
+4 -4
View File
@@ -41,9 +41,9 @@ class StaticDividingLine(BaseDividingLine):
:return: full line of dividing line as str :return: full line of dividing line as str
""" """
if is_override: if is_override:
return f"\n{self.length * self.get_unit_part()}\n" return self.length * self.get_unit_part()
else: else:
return f"\n[dim]{self.length * self.get_unit_part()}[/dim]\n" return f"[dim]{self.length * self.get_unit_part()}[/dim]"
class DynamicDividingLine(BaseDividingLine): class DynamicDividingLine(BaseDividingLine):
@@ -63,6 +63,6 @@ class DynamicDividingLine(BaseDividingLine):
:return: full line of dividing line as str :return: full line of dividing line as str
""" """
if is_override: if is_override:
return f"\n{length * self.get_unit_part()}\n" return length * self.get_unit_part()
else: else:
return f"\n[dim]{self.get_unit_part() * length}[/dim]\n" return f"[dim]{self.get_unit_part() * length}[/dim]"
+97 -357
View File
@@ -1,246 +1,94 @@
__all__ = ["App"] __all__ = ["App"]
import io import difflib
import re from typing import Never, TypeAlias
from contextlib import redirect_stdout
from typing import Callable, Never, TypeAlias
from art import text2art
from prompt_toolkit import HTML
from rich.console import Console from rich.console import Console
from rich.markup import escape
from argenta.app.autocompleter import AutoCompleter from argenta.app.autocompleter import AutoCompleter
from argenta.app.behavior_handlers.models import (BehaviorHandlersFabric,
BehaviorHandlersSettersMixin)
from argenta.app.dividing_line.models import DynamicDividingLine, StaticDividingLine from argenta.app.dividing_line.models import DynamicDividingLine, StaticDividingLine
from argenta.app.protocols import ( from argenta.app.presentation.renderers import PlainRenderer, Renderer, RichRenderer
DescriptionMessageGenerator, from argenta.app.presentation.viewers import Viewer
EmptyCommandHandler, from argenta.app.protocols import Printer
NonStandardBehaviorHandler,
Printer,
)
from argenta.app.registered_routers.entity import RegisteredRouters from argenta.app.registered_routers.entity import RegisteredRouters
from argenta.command.exceptions import ( from argenta.command.exceptions import (InputCommandException,
InputCommandException,
RepeatedInputFlagsException, RepeatedInputFlagsException,
UnprocessedInputFlagException, UnprocessedInputFlagException)
)
from argenta.router.exceptions import RepeatedAliasNameException, RepeatedTriggerNameException
from argenta.command.models import Command, InputCommand from argenta.command.models import Command, InputCommand
from argenta.response import Response from argenta.response import Response
from argenta.router import Router from argenta.router import Router
from argenta.router.exceptions import (RepeatedAliasNameException,
RepeatedTriggerNameException)
Matches: TypeAlias = list[str] | list[Never] Matches: TypeAlias = list[str] | list[Never]
_ANSI_ESCAPE_RE: re.Pattern[str] = re.compile(r"\u001b\[[0-9;]*m")
class BaseApp(BehaviorHandlersSettersMixin):
class BaseApp:
def __init__( def __init__(
self, self,
*, *,
prompt: str | HTML, prompt: str,
initial_message: str, initial_message: str,
farewell_message: str, farewell_message: str,
exit_command: Command, exit_command: Command,
system_router_title: str, system_router_title: str,
dividing_line: StaticDividingLine | DynamicDividingLine, dividing_line: StaticDividingLine | DynamicDividingLine | None,
repeat_command_groups_printing: bool, repeat_command_groups_printing: bool,
override_system_messages: bool, override_system_messages: bool,
autocompleter: AutoCompleter, autocompleter: AutoCompleter,
print_func: Printer, printer: Printer,
) -> None: ) -> None:
self._prompt: str | HTML = prompt self._prompt: str = prompt
self._print_func: Printer = print_func self._printer: Printer = printer
self._exit_command: Command = exit_command self._exit_command: Command = exit_command
self._dividing_line: StaticDividingLine | DynamicDividingLine = dividing_line self._dividing_line: StaticDividingLine | DynamicDividingLine | None = dividing_line
self._repeat_command_groups_printing: bool = repeat_command_groups_printing self._repeat_command_groups_printing: bool = repeat_command_groups_printing
self._override_system_messages: bool = override_system_messages self._override_system_messages: bool = override_system_messages
self._autocompleter: AutoCompleter = autocompleter self._autocompleter: AutoCompleter = autocompleter
self.system_router: Router = Router(title=system_router_title) self._system_router: Router = Router(title=system_router_title)
self._farewell_message: str = farewell_message
self._initial_message: str = initial_message
self._stdout_buffer: io.StringIO = io.StringIO()
self._description_message_gen: DescriptionMessageGenerator = (
lambda command, description: f"{command} *=*=* {description}"
)
self.registered_routers: RegisteredRouters = RegisteredRouters() self.registered_routers: RegisteredRouters = RegisteredRouters()
self._messages_on_startup: list[str] = [] self._messages_on_startup: list[str] = []
self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = ( self._renderer: Renderer = PlainRenderer() if self._override_system_messages else RichRenderer()
lambda _: print_func(f"Incorrect flag syntax: {_}")
self._viewer: Viewer = Viewer(
printer=self._printer,
renderer=self._renderer,
dividing_line=self._dividing_line,
override_system_messages=self._override_system_messages,
) )
self._repeated_input_flags_handler: NonStandardBehaviorHandler[str] = ( self._handlers_fabric: BehaviorHandlersFabric = BehaviorHandlersFabric(
lambda _: print_func(f"Repeated input flags: {_}") printer=self._printer,
) renderer=self._renderer,
self._empty_input_command_handler: EmptyCommandHandler = lambda: print_func( most_similar_command_getter=self._most_similar_command
"Empty input command"
)
self._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = (
lambda _: print_func(f"Unknown command: {_.trigger}")
)
self._exit_command_handler: NonStandardBehaviorHandler[Response] = (
lambda _: print_func(self._farewell_message)
) )
def set_description_message_pattern(self, _: DescriptionMessageGenerator, /) -> None: self._initial_message: str = self._renderer.render_initial_message(initial_message)
""" self._farewell_message: str = self._renderer.render_farewell_message(farewell_message)
Public. Sets the output pattern of the available commands
:param _: output pattern of the available commands
:return: None
"""
self._description_message_gen = _
def set_incorrect_input_syntax_handler( super().__init__(
self, _: NonStandardBehaviorHandler[str], / description_message_generator = self._handlers_fabric.generate_description_message_generator(),
) -> None: incorrect_input_syntax_handler = self._handlers_fabric.generate_incorrect_input_syntax_handler(),
""" repeated_input_flags_handler = self._handlers_fabric.generate_repeated_input_flags_handler(),
Public. Sets the handler for incorrect flags when entering a command empty_input_command_handler = self._handlers_fabric.generate_empty_input_command_handler(),
:param _: handler for incorrect flags when entering a command unknown_command_handler = self._handlers_fabric.generate_unknown_command_handler(),
:return: None exit_command_handler = self._handlers_fabric.generate_exit_command_handler(self._farewell_message)
"""
self._incorrect_input_syntax_handler = _
def set_repeated_input_flags_handler(
self, _: NonStandardBehaviorHandler[str], /
) -> None:
"""
Public. Sets the handler for repeated flags when entering a command
:param _: handler for repeated flags when entering a command
:return: None
"""
self._repeated_input_flags_handler = _
def set_unknown_command_handler(
self, _: NonStandardBehaviorHandler[InputCommand], /
) -> None:
"""
Public. Sets the handler for unknown commands when entering a command
:param _: handler for unknown commands when entering a command
:return: None
"""
self._unknown_command_handler = _
def set_empty_command_handler(self, _: EmptyCommandHandler, /) -> None:
"""
Public. Sets the handler for empty commands when entering a command
:param _: handler for empty commands when entering a command
:return: None
"""
self._empty_input_command_handler = _
def set_exit_command_handler(
self, _: NonStandardBehaviorHandler[Response], /
) -> None:
"""
Public. Sets the handler for exit command when entering a command
:param _: handler for exit command when entering a command
:return: None
"""
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(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,
) )
)
self._print_func("")
def _print_framed_text(self, text: str) -> None:
"""
Private. Outputs text by framing it in a static or dynamic split strip
:param text: framed text
:return: None
"""
if isinstance(self._dividing_line, DynamicDividingLine):
clear_text = _ANSI_ESCAPE_RE.sub("", text)
max_length_line = max([len(line) for line in clear_text.split("\n")])
max_length_line = (
max_length_line
if 10 <= max_length_line <= 80
else 80
if max_length_line > 80
else 10
)
self._print_func(
self._dividing_line.get_full_dynamic_line(
length=max_length_line, is_override=self._override_system_messages
)
)
print(text.strip("\n"))
self._print_func(
self._dividing_line.get_full_dynamic_line(
length=max_length_line, is_override=self._override_system_messages
)
)
elif isinstance(self._dividing_line, StaticDividingLine): # pyright: ignore[reportUnnecessaryIsInstance]
self._print_func(
self._dividing_line.get_full_static_line(
is_override=self._override_system_messages
)
)
print(text.strip("\n"))
self._print_func(
self._dividing_line.get_full_static_line(
is_override=self._override_system_messages
)
)
else:
raise NotImplementedError
def _is_exit_command(self, command: InputCommand) -> bool: def _is_exit_command(self, command: InputCommand) -> bool:
""" if not self._system_router.command_handlers.get_command_handler_by_trigger(command.trigger.lower()):
Private. Checks if the given command is an exit command
:param command: command to check
:return: is it an exit command or not as bool
"""
trigger = command.trigger
exit_trigger = self._exit_command.trigger
if trigger.lower() == exit_trigger.lower():
return True
elif trigger.lower() in [x.lower() for x in self._exit_command.aliases]:
return True
return False return False
return True
def _is_unknown_command(self, input_command: InputCommand) -> bool: def _is_unknown_command(self, input_command: InputCommand) -> bool:
if not self.registered_routers.get_router_by_trigger(input_command.trigger.lower()): if not self.registered_routers.get_router_by_trigger(input_command.trigger.lower()):
return True return True
return False return False
def _capture_stdout(self, func: Callable[[], None]) -> str:
"""
Private. Captures stdout from a function call using a reusable buffer
:param func: function to execute with captured stdout
:return: captured stdout as string
"""
self._stdout_buffer.seek(0)
self._stdout_buffer.truncate(0)
with redirect_stdout(self._stdout_buffer):
func()
return self._stdout_buffer.getvalue()
def _error_handler(self, error: InputCommandException, raw_command: str) -> None: def _error_handler(self, error: InputCommandException, raw_command: str) -> None:
"""
Private. Handles parsing errors of the entered command
:param error: error being handled
:param raw_command: the raw input command
:return: None
"""
if isinstance(error, UnprocessedInputFlagException): if isinstance(error, UnprocessedInputFlagException):
self._incorrect_input_syntax_handler(raw_command) self._incorrect_input_syntax_handler(raw_command)
elif isinstance(error, RepeatedInputFlagsException): elif isinstance(error, RepeatedInputFlagsException):
@@ -248,123 +96,42 @@ class BaseApp:
else: else:
self._empty_input_command_handler() self._empty_input_command_handler()
def _setup_system_router(self) -> None:
"""
Private. Sets up system router
:return: None
"""
@self.system_router.command(self._exit_command)
def _(response: Response) -> None:
self._exit_command_handler(response)
self.registered_routers.add_registered_router(self.system_router)
def _validate_routers_for_collisions(self) -> None: def _validate_routers_for_collisions(self) -> None:
""" seen_names: set[str] = set()
Private. Validates that there are no trigger/alias collisions between routers
:return: None
:raises: RepeatedTriggerNameException or RepeatedAliasNameException if collision detected
"""
all_triggers: set[str] = set()
all_aliases: set[str] = set()
for router_entity in self.registered_routers: for router_entity in self.registered_routers:
union_units: set[str] = all_triggers | all_aliases if not seen_names.isdisjoint(router_entity.triggers):
trigger_collisions: set[str] = union_units & router_entity.triggers
if trigger_collisions:
raise RepeatedTriggerNameException() raise RepeatedTriggerNameException()
alias_collisions: set[str] = union_units & router_entity.aliases alias_collisions = seen_names.intersection(router_entity.aliases)
if alias_collisions: if alias_collisions:
raise RepeatedAliasNameException(alias_collisions) raise RepeatedAliasNameException(alias_collisions)
all_triggers.update(router_entity.triggers) seen_names.update(router_entity.triggers)
all_aliases.update(router_entity.aliases) seen_names.update(router_entity.aliases)
def _most_similar_command(self, unknown_command: str) -> str | None: def _most_similar_command(self, unknown_command: str) -> str | None:
all_commands = self.registered_routers.get_triggers() all_commands = self.registered_routers.get_triggers()
matches = difflib.get_close_matches(unknown_command, all_commands, n=1)
return matches[0] if matches else None
matches_startswith_unknown_command: Matches = sorted( def _setup_system_router(self) -> None:
cmd for cmd in all_commands if cmd.startswith(unknown_command) @self._system_router.command(self._exit_command)
) def _(response: Response) -> None:
matches_startswith_cmd: Matches = sorted( self._exit_command_handler(response)
cmd for cmd in all_commands if unknown_command.startswith(cmd)
)
matches: Matches = matches_startswith_unknown_command or matches_startswith_cmd self.registered_routers.add_registered_router(self._system_router)
if len(matches) == 1:
return matches[0]
elif len(matches) > 1:
return sorted(matches, key=lambda cmd: len(cmd))[0]
else:
return None
def _setup_default_view(self) -> None:
"""
Private. Sets up default app view
:return: None
"""
self._prompt = f"<gray><b>{self._prompt}</b></gray>"
self._initial_message = (
"\n" + f"[bold red]{text2art(self._initial_message, font='tarty1')}" + "\n"
)
self._farewell_message = (
"[bold red]\n\n"
+ str(text2art(self._farewell_message, font="chanky")) # pyright: ignore[reportUnknownArgumentType]
+ "\n[/bold red]\n"
+ "[red i]github.com/koloideal/Argenta[/red i] | [red bold i]made by kolo[/red bold i]\n"
)
self._description_message_gen = lambda command, description: (
f"[bold red]{escape('[' + command + ']')}[/bold red] "
f"[blue dim]*=*=*[/blue dim] "
f"[bold yellow italic]{escape(description)}"
)
self._incorrect_input_syntax_handler = lambda raw_command: self._print_func(
f"[red bold]Incorrect flag syntax: {escape(raw_command)}"
)
self._repeated_input_flags_handler = lambda raw_command: self._print_func(
f"[red bold]Repeated input flags: {escape(raw_command)}"
)
self._empty_input_command_handler = lambda: self._print_func(
"[red bold]Empty input command"
)
def unknown_command_handler(command: InputCommand) -> None:
cmd_trg: str = command.trigger
mst_sim_cmd: str | None = self._most_similar_command(cmd_trg)
first_part_of_text = (
f"[red]Unknown command:[/red] [blue]{escape(cmd_trg)}[/blue]"
)
second_part_of_text = (
("[red], most similar:[/red] " + ("[blue]" + mst_sim_cmd + "[/blue]"))
if mst_sim_cmd
else ""
)
self._print_func(first_part_of_text + second_part_of_text)
self._unknown_command_handler = unknown_command_handler
def _pre_cycle_setup(self) -> None: def _pre_cycle_setup(self) -> None:
"""
Private. Configures various aspects of the application before the start of the cycle
:return: None
"""
self._setup_system_router() self._setup_system_router()
self._validate_routers_for_collisions() self._validate_routers_for_collisions()
self._autocompleter.initial_setup(self.registered_routers.get_triggers()) self._autocompleter.initial_setup(self.registered_routers.get_triggers())
self._print_func(self._initial_message)
for message in self._messages_on_startup:
self._print_func(message)
if self._messages_on_startup: if self._messages_on_startup:
print("\n") 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())
@@ -372,48 +139,57 @@ class BaseApp:
if not processing_router: if not processing_router:
raise RuntimeError(f"Router for '{input_command.trigger}' not found. Panic!") raise RuntimeError(f"Router for '{input_command.trigger}' not found. Panic!")
if processing_router.disable_redirect_stdout: self._viewer.view_framed_text_from_generator(
dividing_line_unit_part: str = self._dividing_line.get_unit_part() output_text_generator=lambda: processing_router.finds_appropriate_handler(input_command),
self._print_func( is_stdout_redirected_by_router=processing_router.is_redirect_stdout_disabled
StaticDividingLine(dividing_line_unit_part).get_full_static_line(
is_override=self._override_system_messages
) )
)
processing_router.finds_appropriate_handler(input_command)
self._print_func(
StaticDividingLine(dividing_line_unit_part).get_full_static_line(
is_override=self._override_system_messages
)
)
else:
stdout_result = self._capture_stdout(
lambda: processing_router.finds_appropriate_handler(input_command)
)
self._print_framed_text(stdout_result)
def _run_polling(self) -> None:
self._viewer.view_initial_message(self._initial_message)
self._pre_cycle_setup()
while True:
if self._repeat_command_groups_printing:
self._viewer.view_command_groups_description(self._description_message_generator, self.registered_routers)
AVAILABLE_DIVIDING_LINES: TypeAlias = StaticDividingLine | DynamicDividingLine print() # pre-prompt gap
DEFAULT_DIVIDING_LINE: StaticDividingLine = StaticDividingLine() raw_command: str = self._autocompleter.prompt(self._renderer.render_prompt(self._prompt))
print() # post-prompt gap
DEFAULT_PRINT_FUNC: Printer = Console().print try:
DEFAULT_AUTOCOMPLETER: AutoCompleter = AutoCompleter() input_command: InputCommand = InputCommand.parse(raw_command=raw_command)
DEFAULT_EXIT_COMMAND: Command = Command("q", description="Exit command") except InputCommandException as error: # noqa F841
self._viewer.view_framed_text_from_generator(
output_text_generator=lambda: self._error_handler(error, raw_command) # noqa
)
continue
if self._is_unknown_command(input_command):
self._viewer.view_framed_text_from_generator(
output_text_generator=lambda: self._unknown_command_handler(input_command)
)
continue
if self._is_exit_command(input_command):
self._system_router.finds_appropriate_handler(input_command)
return
self._process_exist_and_valid_command(input_command)
class App(BaseApp): class App(BaseApp):
def __init__( def __init__(
self, self,
*, *,
prompt: str | HTML = ">>> ", prompt: str = ">>> ",
initial_message: str = "Argenta\n", initial_message: str = "Argenta",
farewell_message: str = "\nSee you\n", farewell_message: str = "See you",
exit_command: Command = DEFAULT_EXIT_COMMAND, exit_command: Command = Command("q", description="Exit command"),
system_router_title: str = "System points:", system_router_title: str = "System points:",
dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE, dividing_line: StaticDividingLine | DynamicDividingLine | None = None,
repeat_command_groups_printing: bool = False, repeat_command_groups_printing: bool = False,
override_system_messages: bool = False, override_system_messages: bool = False,
autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER, autocompleter: AutoCompleter | None = None,
print_func: Printer = DEFAULT_PRINT_FUNC, printer: Printer = Console().print,
) -> None: ) -> None:
""" """
Public. The essence of the application itself. Public. The essence of the application itself.
@@ -427,7 +203,7 @@ class App(BaseApp):
:param repeat_command_groups_printing: whether to repeat the available commands and their description :param repeat_command_groups_printing: whether to repeat the available commands and their description
:param override_system_messages: whether to redefine the default formatting of system messages :param override_system_messages: whether to redefine the default formatting of system messages
:param autocompleter: the entity of the autocompleter :param autocompleter: the entity of the autocompleter
:param print_func: system messages text output function :param printer: system messages text output function
:return: None :return: None
""" """
super().__init__( super().__init__(
@@ -439,45 +215,9 @@ class App(BaseApp):
dividing_line=dividing_line, dividing_line=dividing_line,
repeat_command_groups_printing=repeat_command_groups_printing, repeat_command_groups_printing=repeat_command_groups_printing,
override_system_messages=override_system_messages, override_system_messages=override_system_messages,
autocompleter=autocompleter, autocompleter=autocompleter or AutoCompleter(),
print_func=print_func, printer=printer,
) )
if not self._override_system_messages:
self._setup_default_view()
def run_polling(self) -> None:
"""
Private. Starts the user input processing cycle
:return: None
"""
self._pre_cycle_setup()
while True:
if self._repeat_command_groups_printing:
self._print_command_group_description()
raw_command: str = self._autocompleter.prompt(self._prompt)
try:
input_command: InputCommand = InputCommand.parse(raw_command=raw_command)
except InputCommandException as error: # noqa F841
stderr_result = self._capture_stdout(
lambda: self._error_handler(error, raw_command) # noqa F821
)
self._print_framed_text(stderr_result)
continue
if self._is_exit_command(input_command):
self.system_router.finds_appropriate_handler(input_command)
return
if self._is_unknown_command(input_command):
stdout_res = self._capture_stdout(
lambda: self._unknown_command_handler(input_command)
)
self._print_framed_text(stdout_res)
continue
self._process_exist_and_valid_command(input_command)
def include_router(self, router: Router) -> None: def include_router(self, router: Router) -> None:
""" """
+4
View File
@@ -0,0 +1,4 @@
from .renderers import PlainRenderer, Renderer, RichRenderer
from .viewers import Viewer
__all__ = ["Renderer", "RichRenderer", "PlainRenderer", "Viewer"]
+182
View File
@@ -0,0 +1,182 @@
from typing import Iterable, Protocol
from art import text2art
from argenta.app.protocols import DescriptionMessageGenerator
from argenta.app.registered_routers.entity import RegisteredRouters
class Renderer(Protocol):
@staticmethod
def render_prompt(
text: str
) -> str: ...
@staticmethod
def render_initial_message(
text: str
) -> str: ...
@staticmethod
def render_farewell_message(
text: str
) -> str: ...
@staticmethod
def render_messages_on_startup(
messages: Iterable[str]
) -> str: ...
@staticmethod
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(Renderer):
@staticmethod
def render_prompt(text: str) -> str:
return f"<gray><b>{text}</b></gray>"
@staticmethod
def render_initial_message(text: str) -> str:
return f"[bold red]{text2art(text, font='tarty1')}[/bold red]"
@staticmethod
def render_farewell_message(text: str) -> str:
return (
"[bold red]"
+ str(text2art(text, font="chanky"))
+ "[/bold red]\n"
+ "[red i]https://github.com/koloideal/Argenta[/red i] | [red bold i]made by kolo[/red bold i]"
)
@staticmethod
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]{description}[/bold yellow italic]"
)
@staticmethod
def render_messages_on_startup(messages: Iterable[str]) -> str:
return "\n" + "\n".join(messages)
@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\n" + registered_router.title
for command_handler in registered_router.command_handlers:
handled_command = command_handler.handled_command
command_groups_description += '\n' + description_message_generator(
handled_command.trigger,
handled_command.description,
)
return command_groups_description
@staticmethod
def render_text_for_incorrect_input_syntax_handler(raw_command: str) -> str:
return f"[red bold]Incorrect flag syntax: {raw_command}[/red bold]"
@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 "")
)
class PlainRenderer(Renderer):
@staticmethod
def render_prompt(text: str) -> str:
return text
@staticmethod
def render_initial_message(text: str) -> str:
return text
@staticmethod
def render_farewell_message(text: str) -> str:
return f"\n{text} | https://github.com/koloideal/Argenta | made by kolo"
@staticmethod
def render_text_for_description_message_generator(command: str, description: str) -> str:
return f"{command} *=*=* {description}"
@staticmethod
def render_messages_on_startup(messages: Iterable[str]) -> str:
return "\n" + "\n".join(messages)
@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\n" + registered_router.title
for command_handler in registered_router.command_handlers:
handled_command = command_handler.handled_command
command_groups_description += "\n" + description_message_generator(
handled_command.trigger,
handled_command.description,
)
return command_groups_description
@staticmethod
def render_text_for_incorrect_input_syntax_handler(raw_command: str) -> str:
return f"Incorrect flag syntax: {raw_command}"
@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 "")
)
+95
View File
@@ -0,0 +1,95 @@
__all__ = ["Viewer"]
import re
from contextlib import redirect_stdout
from io import StringIO
from typing import Callable, Iterable, TypeAlias
from rich.text import Text
from argenta.app import DynamicDividingLine, StaticDividingLine
from argenta.app.presentation.renderers import Renderer
from argenta.app.protocols import DescriptionMessageGenerator, Printer
from argenta.app.registered_routers.entity import RegisteredRouters
AVAILABLE_DIVIDING_LINES: TypeAlias = StaticDividingLine | DynamicDividingLine | None
class Viewer:
ANSI_ESCAPE_RE: re.Pattern[str] = re.compile(r"\u001b\[[0-9;]*m")
def __init__(
self,
printer: Printer,
renderer: Renderer,
dividing_line: AVAILABLE_DIVIDING_LINES,
override_system_messages: bool
):
self._printer = printer
self._renderer = renderer
self._dividing_line = dividing_line
self._override_system_messages = override_system_messages
self._stdout_buffer: StringIO = StringIO()
def _capture_stdout(self, func: Callable[[], None]) -> str:
self._stdout_buffer.seek(0)
self._stdout_buffer.truncate(0)
with redirect_stdout(self._stdout_buffer):
func()
return self._stdout_buffer.getvalue()
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
)
)
def view_initial_message(self, initial_message: str) -> None:
self._printer(initial_message)
def view_framed_text_from_generator(
self,
output_text_generator: Callable[[], None],
is_stdout_redirected_by_router: bool = False,
) -> None:
match (self._dividing_line, is_stdout_redirected_by_router):
case (None, bool()):
output_text_generator()
case (DynamicDividingLine(), False):
stdout_result = self._capture_stdout(
lambda: output_text_generator()
)
clear_text = self.ANSI_ESCAPE_RE.sub("", stdout_result)
max_length_line = max([len(line) for line in clear_text.split("\n")])
max_length_line = (
max_length_line
if 10 <= max_length_line <= 100
else 100
if max_length_line > 100
else 10
)
dynamic_dividing_line_as_str: str = self._dividing_line.get_full_dynamic_line(
length=max_length_line, is_override=self._override_system_messages
)
self._printer(dynamic_dividing_line_as_str + "\n")
self._printer(Text.from_ansi(stdout_result.strip("\n")).markup)
self._printer('\n' + dynamic_dividing_line_as_str)
case (StaticDividingLine() as dividing_line, bool()) | (DynamicDividingLine() as dividing_line, True):
static_dividing_line_as_str: str = StaticDividingLine(dividing_line.get_unit_part()).get_full_static_line(
is_override=self._override_system_messages
)
self._printer(static_dividing_line_as_str + '\n')
output_text_generator()
self._printer('\n' + static_dividing_line_as_str)
case _:
raise NotImplementedError(f"Dividing line with type {self._dividing_line} is not implemented")
+16 -4
View File
@@ -1,10 +1,17 @@
__all__ = ["NonStandardBehaviorHandler", "EmptyCommandHandler", "Printer", "DescriptionMessageGenerator", "HandlerFunc"] __all__ = [
"NonStandardBehaviorHandler",
"EmptyCommandHandler",
"MostSimilarCommandGetter",
"Printer",
"DescriptionMessageGenerator",
"HandlerFunc",
]
from typing import Any, Protocol, TypeVar
from typing import ParamSpec, Protocol, TypeVar
from argenta.response import Response from argenta.response import Response
T = TypeVar("T", contravariant=True) T = TypeVar("T", contravariant=True)
P = ParamSpec("P")
class NonStandardBehaviorHandler(Protocol[T]): class NonStandardBehaviorHandler(Protocol[T]):
@@ -22,11 +29,16 @@ class Printer(Protocol):
raise NotImplementedError raise NotImplementedError
class MostSimilarCommandGetter(Protocol):
def __call__(self, _unknown_trigger: str, /) -> str | None:
raise NotImplementedError
class DescriptionMessageGenerator(Protocol): class DescriptionMessageGenerator(Protocol):
def __call__(self, _command: str, _description: str, /) -> str: def __call__(self, _command: str, _description: str, /) -> str:
raise NotImplementedError raise NotImplementedError
class HandlerFunc(Protocol): class HandlerFunc(Protocol):
def __call__(self, response: Response) -> None: def __call__(self, response: Response, /, *args: Any, **kwargs: Any) -> None:
raise NotImplementedError raise NotImplementedError
+2 -2
View File
@@ -1,8 +1,8 @@
from argenta.command.flag import Flag as Flag from argenta.command.flag import Flag as Flag
from argenta.command.flag import Flags as Flags
from argenta.command.flag import InputFlag as InputFlag from argenta.command.flag import InputFlag as InputFlag
from argenta.command.flag import InputFlags as InputFlags
from argenta.command.flag import PossibleValues as PossibleValues from argenta.command.flag import PossibleValues as PossibleValues
from argenta.command.flag.defaults import PredefinedFlags as PredefinedFlags from argenta.command.flag.defaults import PredefinedFlags as PredefinedFlags
from argenta.command.flag.models import Flags as Flags
from argenta.command.flag.models import InputFlags as InputFlags
from argenta.command.models import Command as Command from argenta.command.models import Command as Command
from argenta.command.models import InputCommand as InputCommand from argenta.command.models import InputCommand as InputCommand
+2 -2
View File
@@ -1,6 +1,6 @@
from argenta.command.flag.flags.models import Flags as Flags
from argenta.command.flag.flags.models import InputFlags as InputFlags
from argenta.command.flag.models import Flag as Flag from argenta.command.flag.models import Flag as Flag
from argenta.command.flag.models import Flags as Flags
from argenta.command.flag.models import InputFlag as InputFlag from argenta.command.flag.models import InputFlag as InputFlag
from argenta.command.flag.models import InputFlags as InputFlags
from argenta.command.flag.models import PossibleValues as PossibleValues from argenta.command.flag.models import PossibleValues as PossibleValues
from argenta.command.flag.models import ValidationStatus as ValidationStatus from argenta.command.flag.models import ValidationStatus as ValidationStatus
@@ -1,2 +0,0 @@
from argenta.command.flag.flags.models import Flags as Flags
from argenta.command.flag.flags.models import InputFlags as InputFlags
-107
View File
@@ -1,107 +0,0 @@
__all__ = ["Flags", "InputFlags"]
from collections.abc import Iterator
from typing import Generic, TypeVar, override
from argenta.command.flag.models import Flag, InputFlag
FlagType = TypeVar("FlagType")
class BaseFlags(Generic[FlagType]):
def __init__(self, flags: list[FlagType] | None = None) -> None:
"""
Public. A model that combines the registered flags
:param flags: the flags that will be registered
:return: None
"""
self.flags: list[FlagType] = flags if flags else []
def add_flag(self, flag: FlagType) -> None:
"""
Public. Adds a flag to the list of flags
:param flag: flag to add
:return: None
"""
self.flags.append(flag)
def add_flags(self, flags: list[FlagType]) -> None:
"""
Public. Adds a list of flags to the list of flags
:param flags: list of flags to add
:return: None
"""
self.flags.extend(flags)
def __len__(self) -> int:
return len(self.flags)
def __iter__(self) -> Iterator[FlagType]:
return iter(self.flags)
def __getitem__(self, flag_index: int) -> FlagType:
return self.flags[flag_index]
def __bool__(self) -> bool:
return bool(self.flags)
class Flags(BaseFlags[Flag]):
def get_flag_by_name(self, name: str) -> Flag | None:
"""
Public. Returns the flag entity by its name or None if not found
:param name: the name of the flag to get
:return: entity of the flag or None
"""
return next((flag for flag in self.flags if flag.name == name), None)
@override
def __eq__(self, other: object) -> bool:
if not isinstance(other, Flags):
return False
if len(self.flags) != len(other.flags):
return False
flag_pairs: zip[tuple[Flag, Flag]] = zip(self.flags, other.flags)
return all(s_flag == o_flag for s_flag, o_flag in flag_pairs)
def __contains__(self, flag_to_check: object) -> bool:
if isinstance(flag_to_check, Flag):
for flag in self.flags:
if flag == flag_to_check:
return True
return False
else:
raise TypeError
class InputFlags(BaseFlags[InputFlag]):
def get_flag_by_name(self, name: str) -> InputFlag | None:
"""
Public. Returns the flag entity by its name or None if not found
:param name: the name of the flag to get
:return: entity of the flag or None
"""
return next((flag for flag in self.flags if flag.name == name), None)
@override
def __eq__(self, other: object) -> bool:
if not isinstance(other, InputFlags):
return False
if len(self.flags) != len(other.flags):
return False
paired_flags: zip[tuple[InputFlag, InputFlag]] = zip(self.flags, other.flags)
return all(my_flag == other_flag for my_flag, other_flag in paired_flags)
def __contains__(self, ingressable_item: object) -> bool:
if isinstance(ingressable_item, InputFlag):
for flag in self.flags:
if flag == ingressable_item:
return True
return False
else:
raise TypeError
+117 -5
View File
@@ -1,8 +1,8 @@
__all__ = ["PossibleValues", "ValidationStatus", "Flag", "InputFlag"] __all__ = ["PossibleValues", "ValidationStatus", "Flag", "InputFlag", "InputFlags", "Flags"]
from enum import Enum from enum import Enum
from re import Pattern from re import Pattern
from typing import Literal, override from typing import Any, Container, Generic, Iterator, Literal, TypeVar, override
PREFIX_TYPE = Literal["-", "--", "---"] PREFIX_TYPE = Literal["-", "--", "---"]
@@ -24,7 +24,7 @@ class Flag:
name: str, name: str,
*, *,
prefix: PREFIX_TYPE = "--", prefix: PREFIX_TYPE = "--",
possible_values: list[str] | Pattern[str] | PossibleValues = PossibleValues.ALL, possible_values: Container[str] | Pattern[str] | PossibleValues = PossibleValues.ALL,
) -> None: ) -> None:
""" """
Public. The entity of the flag being registered for subsequent processing Public. The entity of the flag being registered for subsequent processing
@@ -35,7 +35,7 @@ class Flag:
""" """
self.name: str = name self.name: str = name
self.prefix: PREFIX_TYPE = prefix self.prefix: PREFIX_TYPE = prefix
self.possible_values: list[str] | Pattern[str] | PossibleValues = possible_values self.possible_values: Container[str] | Pattern[str] | PossibleValues = possible_values
def validate_input_flag_value(self, input_flag_value: str) -> bool: def validate_input_flag_value(self, input_flag_value: str) -> bool:
""" """
@@ -91,7 +91,7 @@ class InputFlag:
Public. The entity of the flag of the entered command Public. The entity of the flag of the entered command
:param name: the name of the input flag :param name: the name of the input flag
:param prefix: the prefix of the input flag :param prefix: the prefix of the input flag
:param value: the value of the input flag :param input_value: the value of the input flag
:return: None :return: None
""" """
self.name: str = name self.name: str = name
@@ -122,3 +122,115 @@ class InputFlag:
return self.name == other.name return self.name == other.name
else: else:
raise NotImplementedError raise NotImplementedError
FlagType = TypeVar("FlagType")
class BaseFlags(Generic[FlagType]):
def __init__(self, flags: list[FlagType] | None = None) -> None:
"""
Public. A model that combines the registered flags
:param flags: the flags that will be registered
:return: None
"""
self.flags: list[FlagType] = flags if flags else []
def add_flag(self, flag: FlagType) -> None:
"""
Public. Adds a flag to the list of flags
:param flag: flag to add
:return: None
"""
self.flags.append(flag)
def add_flags(self, flags: list[FlagType]) -> None:
"""
Public. Adds a list of flags to the list of flags
:param flags: list of flags to add
:return: None
"""
self.flags.extend(flags)
def __len__(self) -> int:
return len(self.flags)
def __iter__(self) -> Iterator[FlagType]:
return iter(self.flags)
def __getitem__(self, flag_index: int) -> FlagType:
return self.flags[flag_index]
def __bool__(self) -> bool:
return bool(self.flags)
class Flags(BaseFlags[Flag]):
def get_flag_by_name(self, name: str) -> Flag | None:
"""
Public. Returns the flag entity by its name or None if not found
:param name: the name of the flag to get
:return: entity of the flag or None
"""
return next((flag for flag in self.flags if flag.name == name), None)
@override
def __eq__(self, other: object) -> bool:
if not isinstance(other, Flags):
return False
if len(self.flags) != len(other.flags):
return False
flag_pairs: Iterator[tuple[Flag, Flag]] = zip(self.flags, other.flags)
return all(s_flag == o_flag for s_flag, o_flag in flag_pairs)
def __contains__(self, flag_to_check: object) -> bool:
if isinstance(flag_to_check, Flag):
for flag in self.flags:
if flag == flag_to_check:
return True
return False
else:
raise TypeError
class InputFlags(BaseFlags[InputFlag]):
def get_flag_by_name(
self,
name: str,
with_status: ValidationStatus | None = None,
default: Any = None
) -> InputFlag | None:
"""
Public. Returns the flag entity by its name or None if not found
:param default:
:param with_status:
:param name: the name of the flag to get
:return: entity of the flag or None
"""
if with_status is None:
return next((flag for flag in self.flags if flag.name == name), default)
else:
return next((flag for flag in self.flags if flag.name == name and flag.status == with_status), default)
@override
def __eq__(self, other: object) -> bool:
if not isinstance(other, InputFlags):
return False
if len(self.flags) != len(other.flags):
return False
paired_flags: Iterator[tuple[InputFlag, InputFlag]] = zip(self.flags, other.flags)
return all(my_flag == other_flag for my_flag, other_flag in paired_flags)
def __contains__(self, ingressable_item: object) -> bool:
if isinstance(ingressable_item, InputFlag):
for flag in self.flags:
if flag == ingressable_item:
return True
return False
else:
raise TypeError
+24 -16
View File
@@ -1,14 +1,12 @@
__all__ = ["Command", "InputCommand"] __all__ = ["Command", "InputCommand"]
import shlex import shlex
from typing import Literal, Never, Self, cast from typing import Iterable, Literal, Never, Self, cast
from argenta.command.exceptions import ( from argenta.command import Flags, InputFlags
EmptyInputCommandException, from argenta.command.exceptions import (EmptyInputCommandException,
RepeatedInputFlagsException, RepeatedInputFlagsException,
UnprocessedInputFlagException, UnprocessedInputFlagException)
)
from argenta.command.flag.flags.models import Flags, InputFlags
from argenta.command.flag.models import Flag, InputFlag, ValidationStatus from argenta.command.flag.models import Flag, InputFlag, ValidationStatus
ParseFlagsResult = tuple[InputFlags, str | None, str | None] ParseFlagsResult = tuple[InputFlags, str | None, str | None]
@@ -16,10 +14,6 @@ ParseResult = tuple[str, InputFlags]
MIN_FLAG_PREFIX: str = "-" MIN_FLAG_PREFIX: str = "-"
PREFIX_TYPE = Literal["-", "--", "---"] PREFIX_TYPE = Literal["-", "--", "---"]
DEFAULT_WITHOUT_FLAGS: Flags = Flags()
DEFAULT_WITHOUT_ALIASES: set[Never] = set()
DEFAULT_WITHOUT_INPUT_FLAGS: InputFlags = InputFlags()
class Command: class Command:
@@ -28,8 +22,8 @@ class Command:
trigger: str, trigger: str,
*, *,
description: str = "Some useful command", description: str = "Some useful command",
flags: Flag | Flags = DEFAULT_WITHOUT_FLAGS, flags: Flag | Flags | None = None,
aliases: set[str] | set[Never] = DEFAULT_WITHOUT_ALIASES, aliases: Iterable[str] | None = None,
): ):
""" """
Public. The command that can and should be registered in the Router Public. The command that can and should be registered in the Router
@@ -38,11 +32,16 @@ class Command:
:param flags: processed commands :param flags: processed commands
:param aliases: string synonyms for the main trigger :param aliases: string synonyms for the main trigger
""" """
pretty_flags = flags if isinstance(flags, Flags) else Flags([flags]) pretty_flags: Flags = (
flags if isinstance(flags, Flags)
else Flags([flags])
if flags is not None
else Flags()
)
self.registered_flags: Flags = pretty_flags self.registered_flags: Flags = pretty_flags
self.trigger: str = trigger self.trigger: str = trigger
self.description: str = description self.description: str = description
self.aliases: set[str] | set[Never] = aliases self.aliases: Iterable[str] | Iterable[Never] = aliases or set()
self._paired_string_entity_flag: dict[str, Flag] = { self._paired_string_entity_flag: dict[str, Flag] = {
flag.string_entity: flag for flag in pretty_flags flag.string_entity: flag for flag in pretty_flags
@@ -68,7 +67,7 @@ class InputCommand:
self, self,
trigger: str, trigger: str,
*, *,
input_flags: InputFlag | InputFlags = DEFAULT_WITHOUT_INPUT_FLAGS, input_flags: InputFlag | InputFlags | None = None,
): ):
""" """
Private. The model of the input command, after parsing Private. The model of the input command, after parsing
@@ -81,6 +80,8 @@ class InputCommand:
input_flags input_flags
if isinstance(input_flags, InputFlags) if isinstance(input_flags, InputFlags)
else InputFlags([input_flags]) else InputFlags([input_flags])
if input_flags is not None
else InputFlags()
) )
@classmethod @classmethod
@@ -90,7 +91,14 @@ class InputCommand:
:param raw_command: raw input command :param raw_command: raw input command
:return: model of the input command, after parsing as InputCommand :return: model of the input command, after parsing as InputCommand
""" """
tokens = shlex.split(raw_command) lexer = shlex.shlex(raw_command, posix=True)
lexer.whitespace_split = True
lexer.commenters = ""
try:
tokens = list(lexer)
except ValueError as e:
raise UnprocessedInputFlagException from e
if not tokens: if not tokens:
raise EmptyInputCommandException raise EmptyInputCommandException
+1 -1
View File
@@ -39,4 +39,4 @@ class Orchestrator:
) )
setup_dishka(app, container, auto_inject=self._auto_inject_handlers) setup_dishka(app, container, auto_inject=self._auto_inject_handlers)
app.run_polling() app._run_polling()
+1 -1
View File
@@ -2,7 +2,7 @@ __all__ = ["Response"]
from dishka import Container from dishka import Container
from argenta.command.flag.flags.models import InputFlags from argenta.command import InputFlags
from argenta.response.status import ResponseStatus from argenta.response.status import ResponseStatus
EMPTY_INPUT_FLAGS: InputFlags = InputFlags() EMPTY_INPUT_FLAGS: InputFlags = InputFlags()
+6 -7
View File
@@ -6,9 +6,8 @@ from typing import Callable
from rich.console import Console from rich.console import Console
from argenta.app.protocols import HandlerFunc from argenta.app.protocols import HandlerFunc
from argenta.command import Command, InputCommand from argenta.command import Command, InputCommand, InputFlags
from argenta.command.flag import ValidationStatus from argenta.command.flag import ValidationStatus
from argenta.command.flag.flags import InputFlags
from argenta.response import Response, ResponseStatus from argenta.response import Response, ResponseStatus
from argenta.router.command_handler.entity import CommandHandler, CommandHandlers from argenta.router.command_handler.entity import CommandHandler, CommandHandlers
from argenta.router.exceptions import (RepeatedAliasNameException, from argenta.router.exceptions import (RepeatedAliasNameException,
@@ -36,7 +35,7 @@ class Router:
:return: None :return: None
""" """
self.title: str = title self.title: str = title
self.disable_redirect_stdout: bool = disable_redirect_stdout self.is_redirect_stdout_disabled: bool = disable_redirect_stdout
self.command_handlers: CommandHandlers = CommandHandlers() self.command_handlers: CommandHandlers = CommandHandlers()
self.aliases: set[str] = set() self.aliases: set[str] = set()
@@ -57,7 +56,7 @@ class Router:
self._update_routing_keys(redefined_command) self._update_routing_keys(redefined_command)
def decorator(func: HandlerFunc) -> HandlerFunc: def decorator(func: HandlerFunc) -> HandlerFunc:
_validate_func_args(func) self._validate_func_args(func)
self.command_handlers.add_handler(CommandHandler(func, redefined_command)) self.command_handlers.add_handler(CommandHandler(func, redefined_command))
return func return func
@@ -117,7 +116,7 @@ class Router:
handle_command = command_handler.handled_command handle_command = command_handler.handled_command
if handle_command.registered_flags.flags: if handle_command.registered_flags.flags:
if input_command_flags.flags: if input_command_flags.flags:
response: Response = _structuring_input_flags(handle_command, input_command_flags) response: Response = self._structuring_input_flags(handle_command, input_command_flags)
command_handler.handling(response) command_handler.handling(response)
else: else:
response = Response(ResponseStatus.ALL_FLAGS_VALID) response = Response(ResponseStatus.ALL_FLAGS_VALID)
@@ -134,7 +133,7 @@ class Router:
response = Response(ResponseStatus.ALL_FLAGS_VALID) response = Response(ResponseStatus.ALL_FLAGS_VALID)
command_handler.handling(response) command_handler.handling(response)
@staticmethod
def _structuring_input_flags(handled_command: Command, input_flags: InputFlags) -> Response: def _structuring_input_flags(handled_command: Command, input_flags: InputFlags) -> Response:
""" """
Private. Validates flags of input command Private. Validates flags of input command
@@ -159,7 +158,7 @@ def _structuring_input_flags(handled_command: Command, input_flags: InputFlags)
return Response(status=status, input_flags=input_flags) return Response(status=status, input_flags=input_flags)
@staticmethod
def _validate_func_args(func: HandlerFunc) -> None: def _validate_func_args(func: HandlerFunc) -> None:
""" """
Private. Validates the arguments of the handler Private. Validates the arguments of the handler
@@ -5,8 +5,7 @@ from collections.abc import Iterator
import pytest import pytest
from argenta import App, Orchestrator, Router from argenta import App, Orchestrator, Router
from argenta.command import Command, PredefinedFlags from argenta.command import Command, PredefinedFlags, Flags
from argenta.command.flag.flags.models import Flags
from argenta.command.flag.models import ValidationStatus from argenta.command.flag.models import ValidationStatus
from argenta.response import Response from argenta.response import Response
@@ -36,7 +35,7 @@ def test_empty_input_triggers_empty_command_handler(monkeypatch: pytest.MonkeyPa
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
print('test command') print('test command')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_empty_command_handler(lambda: print('Empty input command')) app.set_empty_command_handler(lambda: print('Empty input command'))
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -62,7 +61,7 @@ def test_unknown_command_triggers_unknown_command_handler(monkeypatch: pytest.Mo
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
print('test command') print('test command')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -83,7 +82,7 @@ def test_mixed_valid_and_unknown_commands_handled_correctly(monkeypatch: pytest.
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
print('test command') print('test command')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -108,7 +107,7 @@ def test_multiple_commands_with_unknown_command_in_between(monkeypatch: pytest.M
def test1(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] def test1(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
print('more command') print('more command')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -136,7 +135,7 @@ def test_unregistered_flag_without_value_is_accessible(monkeypatch: pytest.Monke
if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED: if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED:
print(f'test command with undefined flag: {undefined_flag.string_entity}') print(f'test command with undefined flag: {undefined_flag.string_entity}')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -160,7 +159,7 @@ def test_unregistered_flag_with_value_is_accessible(monkeypatch: pytest.MonkeyPa
else: else:
raise raise
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -183,7 +182,7 @@ def test_registered_and_unregistered_flags_coexist(monkeypatch: pytest.MonkeyPat
if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED: if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED:
print(f'connecting to host with flag: {undefined_flag.string_entity} {undefined_flag.input_value}') print(f'connecting to host with flag: {undefined_flag.string_entity} {undefined_flag.input_value}')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -208,7 +207,7 @@ def test_flag_without_value_triggers_incorrect_syntax_handler(monkeypatch: pytes
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
print('test command') print('test command')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_incorrect_input_syntax_handler(lambda command: print(f'Incorrect flag syntax: "{command}"')) app.set_incorrect_input_syntax_handler(lambda command: print(f'Incorrect flag syntax: "{command}"'))
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -234,7 +233,7 @@ def test_repeated_flags_trigger_repeated_flags_handler(monkeypatch: pytest.Monke
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
print('test command') print('test command')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"')) app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"'))
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -5,9 +5,8 @@ from collections.abc import Iterator
import pytest import pytest
from argenta import App, Orchestrator, Router from argenta import App, Orchestrator, Router
from argenta.command import Command, PredefinedFlags from argenta.command import Command, PredefinedFlags, Flags
from argenta.command.flag import Flag from argenta.command.flag import Flag
from argenta.command.flag.flags import Flags
from argenta.command.flag.models import PossibleValues, ValidationStatus from argenta.command.flag.models import PossibleValues, ValidationStatus
from argenta.response import Response from argenta.response import Response
@@ -37,7 +36,7 @@ def test_simple_command_executes_successfully(monkeypatch: pytest.MonkeyPatch, c
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
print('test command') print('test command')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -61,7 +60,7 @@ def test_two_commands_execute_sequentially(monkeypatch: pytest.MonkeyPatch, caps
def test2(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] def test2(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
print('some command') print('some command')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -89,7 +88,7 @@ def test_three_commands_execute_sequentially(monkeypatch: pytest.MonkeyPatch, ca
def test2(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] def test2(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
print('more command') print('more command')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -117,7 +116,7 @@ def test_custom_flag_without_value_is_recognized(monkeypatch: pytest.MonkeyPatch
if valid_flag and valid_flag.status == ValidationStatus.VALID: if valid_flag and valid_flag.status == ValidationStatus.VALID:
print(f'\nhelp for {valid_flag.name} flag\n') print(f'\nhelp for {valid_flag.name} flag\n')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -140,7 +139,7 @@ def test_custom_flag_with_regex_validation_accepts_valid_value(monkeypatch: pyte
if valid_flag and valid_flag.status == ValidationStatus.VALID: if valid_flag and valid_flag.status == ValidationStatus.VALID:
print(f'flag value for {valid_flag.name} flag : {valid_flag.input_value}') print(f'flag value for {valid_flag.name} flag : {valid_flag.input_value}')
app = App(override_system_messages=True, repeat_command_groups_printing=True, print_func=print) app = App(override_system_messages=True, repeat_command_groups_printing=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -168,7 +167,7 @@ def test_predefined_short_help_flag_is_recognized(monkeypatch: pytest.MonkeyPatc
if valid_flag and valid_flag.status == ValidationStatus.VALID: if valid_flag and valid_flag.status == ValidationStatus.VALID:
print(f'help for {valid_flag.name} flag') print(f'help for {valid_flag.name} flag')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -191,7 +190,7 @@ def test_predefined_info_flag_is_recognized(monkeypatch: pytest.MonkeyPatch, cap
if valid_flag and valid_flag.status == ValidationStatus.VALID: if valid_flag and valid_flag.status == ValidationStatus.VALID:
print('info about test command') print('info about test command')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -214,7 +213,7 @@ def test_predefined_host_flag_with_value_is_recognized(monkeypatch: pytest.Monke
if valid_flag and valid_flag.status == ValidationStatus.VALID: if valid_flag and valid_flag.status == ValidationStatus.VALID:
print(f'connecting to host {valid_flag.input_value}') print(f'connecting to host {valid_flag.input_value}')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.start_polling(app)
@@ -243,7 +242,7 @@ def test_two_predefined_flags_are_recognized_together(monkeypatch: pytest.Monkey
if (host_flag and host_flag.status == ValidationStatus.VALID) and (port_flag and port_flag.status == ValidationStatus.VALID): if (host_flag and host_flag.status == ValidationStatus.VALID) and (port_flag and port_flag.status == ValidationStatus.VALID):
print(f'connecting to host {host_flag.input_value} and port {port_flag.input_value}') print(f'connecting to host {host_flag.input_value} and port {port_flag.input_value}')
app = App(override_system_messages=True, print_func=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.start_polling(app)
+16 -64
View File
@@ -3,7 +3,6 @@ import pytest
from pytest import CaptureFixture from pytest import CaptureFixture
from argenta.app import App from argenta.app import App
from argenta.app.dividing_line import DynamicDividingLine, StaticDividingLine
from argenta.app.protocols import DescriptionMessageGenerator, NonStandardBehaviorHandler from argenta.app.protocols import DescriptionMessageGenerator, NonStandardBehaviorHandler
from argenta.command.models import Command, InputCommand from argenta.command.models import Command, InputCommand
from argenta.response import Response from argenta.response import Response
@@ -18,26 +17,31 @@ from argenta.router import Router
def test_default_exit_command_lowercase_q_is_recognized() -> None: def test_default_exit_command_lowercase_q_is_recognized() -> None:
app = App() app = App()
app._setup_system_router()
assert app._is_exit_command(InputCommand('q')) is True assert app._is_exit_command(InputCommand('q')) is True
def test_default_exit_command_uppercase_q_is_recognized() -> None: def test_default_exit_command_uppercase_q_is_recognized() -> None:
app = App() app = App()
app._setup_system_router()
assert app._is_exit_command(InputCommand('Q')) is True assert app._is_exit_command(InputCommand('Q')) is True
def test_custom_exit_command_is_recognized() -> None: def test_custom_exit_command_is_recognized() -> None:
app = App(exit_command=Command('quit')) app = App(exit_command=Command('quit'))
app._setup_system_router()
assert app._is_exit_command(InputCommand('quit')) is True assert app._is_exit_command(InputCommand('quit')) is True
def test_exit_command_alias_is_recognized() -> None: def test_exit_command_alias_is_recognized() -> None:
app = App(exit_command=Command('q', aliases={'exit'})) app = App(exit_command=Command('q', aliases={'exit'}))
app._setup_system_router()
assert app._is_exit_command(InputCommand('exit')) is True assert app._is_exit_command(InputCommand('exit')) is True
def test_non_exit_command_is_not_recognized() -> None: def test_non_exit_command_is_not_recognized() -> None:
app = App(exit_command=Command('q', aliases={'exit'})) app = App(exit_command=Command('q', aliases={'exit'}))
app._setup_system_router()
assert app._is_exit_command(InputCommand('quit')) is False 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.include_routers(router)
app._pre_cycle_setup() 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: 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.include_routers(router)
app._pre_cycle_setup() 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 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(length=5))
app._print_framed_text('test')
captured = capsys.readouterr()
assert '\n-----\n\ntest\n\n-----\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_framed_text('some long test')
captured = capsys.readouterr()
assert '\n--------------\n\nsome long test\n\n--------------\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_framed_text('test as test as test')
captured = capsys.readouterr()
assert '\n' + '-'*20 + '\n\ntest as test as test\n\n' + '-'*20 + '\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_framed_text('some long test')
# ============================================================================ # ============================================================================
# Tests for handler configuration # Tests for handler configuration
# ============================================================================ # ============================================================================
@@ -343,7 +305,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:
@@ -354,22 +316,6 @@ def test_set_exit_command_handler_stores_handler() -> None:
assert app._exit_command_handler is handler assert app._exit_command_handler is handler
# ============================================================================
# Tests for default view setup
# ============================================================================
def test_setup_default_view_formats_prompt() -> None:
app = App(prompt='>>')
assert app._prompt == '<gray><b>>></b></gray>'
def test_setup_default_view_sets_default_unknown_command_handler() -> None:
app = App()
app._setup_default_view()
assert app._unknown_command_handler(InputCommand('nonexists')) is None
# ============================================================================ # ============================================================================
# Tests for command processing # Tests for command processing
# ============================================================================ # ============================================================================
@@ -672,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'))
+321 -5
View File
@@ -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)

Some files were not shown because too many files have changed in this diff Show More