Add ExecutionContext.signature, fix partial command matching with arguments, fix passing args to Falyx._create_context helper

This commit is contained in:
Roland Thomas Jr 2025-06-05 17:23:27 -04:00
parent ac82076511
commit b24079ea7e
Signed by: roland
GPG Key ID: 7C3C2B085A4C2872
7 changed files with 63 additions and 31 deletions

View File

@ -70,7 +70,7 @@ class ExecutionContext(BaseModel):
name: str name: str
args: tuple = () args: tuple = ()
kwargs: dict = {} kwargs: dict = Field(default_factory=dict)
action: Any action: Any
result: Any | None = None result: Any | None = None
exception: Exception | None = None exception: Exception | None = None
@ -120,6 +120,17 @@ class ExecutionContext(BaseModel):
def status(self) -> str: def status(self) -> str:
return "OK" if self.success else "ERROR" return "OK" if self.success else "ERROR"
@property
def signature(self) -> str:
"""
Returns a string representation of the action signature, including
its name and arguments.
"""
args = ", ".join(map(repr, self.args))
kwargs = ", ".join(f"{key}={value!r}" for key, value in self.kwargs.items())
signature = ", ".join(filter(None, [args, kwargs]))
return f"{self.name} ({signature})"
def as_dict(self) -> dict: def as_dict(self) -> dict:
return { return {
"name": self.name, "name": self.name,

View File

@ -8,7 +8,7 @@ from falyx.logger import logger
def log_before(context: ExecutionContext): def log_before(context: ExecutionContext):
"""Log the start of an action.""" """Log the start of an action."""
args = ", ".join(map(repr, context.args)) args = ", ".join(map(repr, context.args))
kwargs = ", ".join(f"{k}={v!r}" for k, v in context.kwargs.items()) kwargs = ", ".join(f"{key}={value!r}" for key, value in context.kwargs.items())
signature = ", ".join(filter(None, [args, kwargs])) signature = ", ".join(filter(None, [args, kwargs]))
logger.info("[%s] Starting -> %s(%s)", context.name, context.action, signature) logger.info("[%s] Starting -> %s(%s)", context.name, context.action, signature)

View File

@ -30,7 +30,7 @@ from __future__ import annotations
from collections import defaultdict from collections import defaultdict
from datetime import datetime from datetime import datetime
from threading import Lock from threading import Lock
from typing import Any, Literal from typing import Literal
from rich import box from rich import box
from rich.console import Console from rich.console import Console
@ -111,8 +111,8 @@ class ExecutionRegistry:
def summary( def summary(
cls, cls,
name: str = "", name: str = "",
index: int = -1, index: int | None = None,
result: int = -1, result: int | None = None,
clear: bool = False, clear: bool = False,
last_result: bool = False, last_result: bool = False,
status: Literal["all", "success", "error"] = "all", status: Literal["all", "success", "error"] = "all",
@ -138,7 +138,7 @@ class ExecutionRegistry:
) )
return return
if result and result >= 0: if result is not None and result >= 0:
try: try:
result_context = cls._store_by_index[result] result_context = cls._store_by_index[result]
except KeyError: except KeyError:
@ -146,7 +146,11 @@ class ExecutionRegistry:
f"[{OneColors.DARK_RED}]❌ No execution found for index {index}." f"[{OneColors.DARK_RED}]❌ No execution found for index {index}."
) )
return return
cls._console.print(result_context.result) cls._console.print(f"{result_context.signature}:")
if result_context.exception:
cls._console.print(result_context.exception)
else:
cls._console.print(result_context.result)
return return
if name: if name:
@ -157,9 +161,10 @@ class ExecutionRegistry:
) )
return return
title = f"📊 Execution History for '{contexts[0].name}'" title = f"📊 Execution History for '{contexts[0].name}'"
elif index and index >= 0: elif index is not None and index >= 0:
try: try:
contexts = [cls._store_by_index[index]] contexts = [cls._store_by_index[index]]
print(contexts)
except KeyError: except KeyError:
cls._console.print( cls._console.print(
f"[{OneColors.DARK_RED}]❌ No execution found for index {index}." f"[{OneColors.DARK_RED}]❌ No execution found for index {index}."

View File

@ -796,7 +796,12 @@ class Falyx:
def table(self) -> Table: def table(self) -> Table:
"""Creates or returns a custom table to display the menu commands.""" """Creates or returns a custom table to display the menu commands."""
if callable(self.custom_table): if callable(self.custom_table):
return self.custom_table(self) custom_table = self.custom_table(self)
if not isinstance(custom_table, Table):
raise FalyxError(
"custom_table must return an instance of rich.table.Table."
)
return custom_table
elif isinstance(self.custom_table, Table): elif isinstance(self.custom_table, Table):
return self.custom_table return self.custom_table
else: else:
@ -834,21 +839,31 @@ class Falyx:
choice = choice.upper() choice = choice.upper()
name_map = self._name_map name_map = self._name_map
run_command = None
if name_map.get(choice): if name_map.get(choice):
run_command = name_map[choice]
else:
prefix_matches = [
cmd for key, cmd in name_map.items() if key.startswith(choice)
]
if len(prefix_matches) == 1:
run_command = prefix_matches[0]
if run_command:
if not from_validate: if not from_validate:
logger.info("Command '%s' selected.", choice) logger.info("Command '%s' selected.", run_command.key)
if is_preview: if is_preview:
return True, name_map[choice], args, kwargs return True, run_command, args, kwargs
elif self.mode in {FalyxMode.RUN, FalyxMode.RUN_ALL, FalyxMode.PREVIEW}: elif self.mode in {FalyxMode.RUN, FalyxMode.RUN_ALL, FalyxMode.PREVIEW}:
return False, name_map[choice], args, kwargs return False, run_command, args, kwargs
try: try:
args, kwargs = await name_map[choice].parse_args( args, kwargs = await run_command.parse_args(input_args, from_validate)
input_args, from_validate
)
except (CommandArgumentError, Exception) as error: except (CommandArgumentError, Exception) as error:
if not from_validate: if not from_validate:
name_map[choice].show_help() run_command.show_help()
self.console.print(f"[{OneColors.DARK_RED}]❌ [{choice}]: {error}") self.console.print(
f"[{OneColors.DARK_RED}]❌ [{run_command.key}]: {error}"
)
else: else:
raise ValidationError( raise ValidationError(
message=str(error), cursor_position=len(raw_choices) message=str(error), cursor_position=len(raw_choices)
@ -856,11 +871,7 @@ class Falyx:
return is_preview, None, args, kwargs return is_preview, None, args, kwargs
except HelpSignal: except HelpSignal:
return True, None, args, kwargs return True, None, args, kwargs
return is_preview, name_map[choice], args, kwargs return is_preview, run_command, args, kwargs
prefix_matches = [cmd for key, cmd in name_map.items() if key.startswith(choice)]
if len(prefix_matches) == 1:
return is_preview, prefix_matches[0], args, kwargs
fuzzy_matches = get_close_matches(choice, list(name_map.keys()), n=3, cutoff=0.7) fuzzy_matches = get_close_matches(choice, list(name_map.keys()), n=3, cutoff=0.7)
if fuzzy_matches: if fuzzy_matches:
@ -890,12 +901,14 @@ class Falyx:
) )
return is_preview, None, args, kwargs return is_preview, None, args, kwargs
def _create_context(self, selected_command: Command) -> ExecutionContext: def _create_context(
"""Creates a context dictionary for the selected command.""" self, selected_command: Command, args: tuple, kwargs: dict[str, Any]
) -> ExecutionContext:
"""Creates an ExecutionContext object for the selected command."""
return ExecutionContext( return ExecutionContext(
name=selected_command.description, name=selected_command.description,
args=tuple(), args=args,
kwargs={}, kwargs=kwargs,
action=selected_command, action=selected_command,
) )
@ -929,7 +942,7 @@ class Falyx:
logger.info("Back selected: exiting %s", self.get_title()) logger.info("Back selected: exiting %s", self.get_title())
return False return False
context = self._create_context(selected_command) context = self._create_context(selected_command, args, kwargs)
context.start_timer() context.start_timer()
try: try:
await self.hooks.trigger(HookType.BEFORE, context) await self.hooks.trigger(HookType.BEFORE, context)
@ -974,7 +987,7 @@ class Falyx:
selected_command.description, selected_command.description,
) )
context = self._create_context(selected_command) context = self._create_context(selected_command, args, kwargs)
context.start_timer() context.start_timer()
try: try:
await self.hooks.trigger(HookType.BEFORE, context) await self.hooks.trigger(HookType.BEFORE, context)

View File

@ -629,7 +629,10 @@ class CommandArgumentParser:
consumed_positional_indicies.add(j) consumed_positional_indicies.add(j)
if i < len(args): if i < len(args):
raise CommandArgumentError(f"Unexpected positional argument: {args[i:]}") plural = "s" if len(args[i:]) > 1 else ""
raise CommandArgumentError(
f"Unexpected positional argument{plural}: {', '.join(args[i:])}"
)
return i return i

View File

@ -1 +1 @@
__version__ = "0.1.49" __version__ = "0.1.50"

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "falyx" name = "falyx"
version = "0.1.49" version = "0.1.50"
description = "Reliable and introspectable async CLI action framework." description = "Reliable and introspectable async CLI action framework."
authors = ["Roland Thomas Jr <roland@rtj.dev>"] authors = ["Roland Thomas Jr <roland@rtj.dev>"]
license = "MIT" license = "MIT"