diff --git a/falyx/context.py b/falyx/context.py index c35d5c4..5f39731 100644 --- a/falyx/context.py +++ b/falyx/context.py @@ -70,7 +70,7 @@ class ExecutionContext(BaseModel): name: str args: tuple = () - kwargs: dict = {} + kwargs: dict = Field(default_factory=dict) action: Any result: Any | None = None exception: Exception | None = None @@ -120,6 +120,17 @@ class ExecutionContext(BaseModel): def status(self) -> str: 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: return { "name": self.name, diff --git a/falyx/debug.py b/falyx/debug.py index 554bbf6..2377c38 100644 --- a/falyx/debug.py +++ b/falyx/debug.py @@ -8,7 +8,7 @@ from falyx.logger import logger def log_before(context: ExecutionContext): """Log the start of an action.""" 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])) logger.info("[%s] Starting -> %s(%s)", context.name, context.action, signature) diff --git a/falyx/execution_registry.py b/falyx/execution_registry.py index d78aa6b..4a87ed2 100644 --- a/falyx/execution_registry.py +++ b/falyx/execution_registry.py @@ -30,7 +30,7 @@ from __future__ import annotations from collections import defaultdict from datetime import datetime from threading import Lock -from typing import Any, Literal +from typing import Literal from rich import box from rich.console import Console @@ -111,8 +111,8 @@ class ExecutionRegistry: def summary( cls, name: str = "", - index: int = -1, - result: int = -1, + index: int | None = None, + result: int | None = None, clear: bool = False, last_result: bool = False, status: Literal["all", "success", "error"] = "all", @@ -138,7 +138,7 @@ class ExecutionRegistry: ) return - if result and result >= 0: + if result is not None and result >= 0: try: result_context = cls._store_by_index[result] except KeyError: @@ -146,7 +146,11 @@ class ExecutionRegistry: f"[{OneColors.DARK_RED}]❌ No execution found for index {index}." ) 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 if name: @@ -157,9 +161,10 @@ class ExecutionRegistry: ) return title = f"📊 Execution History for '{contexts[0].name}'" - elif index and index >= 0: + elif index is not None and index >= 0: try: contexts = [cls._store_by_index[index]] + print(contexts) except KeyError: cls._console.print( f"[{OneColors.DARK_RED}]❌ No execution found for index {index}." diff --git a/falyx/falyx.py b/falyx/falyx.py index f5183d5..c7dbe79 100644 --- a/falyx/falyx.py +++ b/falyx/falyx.py @@ -796,7 +796,12 @@ class Falyx: def table(self) -> Table: """Creates or returns a custom table to display the menu commands.""" 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): return self.custom_table else: @@ -834,21 +839,31 @@ class Falyx: choice = choice.upper() name_map = self._name_map + run_command = None 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: - logger.info("Command '%s' selected.", choice) + logger.info("Command '%s' selected.", run_command.key) 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}: - return False, name_map[choice], args, kwargs + return False, run_command, args, kwargs try: - args, kwargs = await name_map[choice].parse_args( - input_args, from_validate - ) + args, kwargs = await run_command.parse_args(input_args, from_validate) except (CommandArgumentError, Exception) as error: if not from_validate: - name_map[choice].show_help() - self.console.print(f"[{OneColors.DARK_RED}]❌ [{choice}]: {error}") + run_command.show_help() + self.console.print( + f"[{OneColors.DARK_RED}]❌ [{run_command.key}]: {error}" + ) else: raise ValidationError( message=str(error), cursor_position=len(raw_choices) @@ -856,11 +871,7 @@ class Falyx: return is_preview, None, args, kwargs except HelpSignal: return True, None, args, kwargs - return is_preview, name_map[choice], 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 + return is_preview, run_command, args, kwargs fuzzy_matches = get_close_matches(choice, list(name_map.keys()), n=3, cutoff=0.7) if fuzzy_matches: @@ -890,12 +901,14 @@ class Falyx: ) return is_preview, None, args, kwargs - def _create_context(self, selected_command: Command) -> ExecutionContext: - """Creates a context dictionary for the selected command.""" + def _create_context( + self, selected_command: Command, args: tuple, kwargs: dict[str, Any] + ) -> ExecutionContext: + """Creates an ExecutionContext object for the selected command.""" return ExecutionContext( name=selected_command.description, - args=tuple(), - kwargs={}, + args=args, + kwargs=kwargs, action=selected_command, ) @@ -929,7 +942,7 @@ class Falyx: logger.info("Back selected: exiting %s", self.get_title()) return False - context = self._create_context(selected_command) + context = self._create_context(selected_command, args, kwargs) context.start_timer() try: await self.hooks.trigger(HookType.BEFORE, context) @@ -974,7 +987,7 @@ class Falyx: selected_command.description, ) - context = self._create_context(selected_command) + context = self._create_context(selected_command, args, kwargs) context.start_timer() try: await self.hooks.trigger(HookType.BEFORE, context) diff --git a/falyx/parsers/argparse.py b/falyx/parsers/argparse.py index 751de27..e4d6651 100644 --- a/falyx/parsers/argparse.py +++ b/falyx/parsers/argparse.py @@ -629,7 +629,10 @@ class CommandArgumentParser: consumed_positional_indicies.add(j) 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 diff --git a/falyx/version.py b/falyx/version.py index a9d181d..a0036bc 100644 --- a/falyx/version.py +++ b/falyx/version.py @@ -1 +1 @@ -__version__ = "0.1.49" +__version__ = "0.1.50" diff --git a/pyproject.toml b/pyproject.toml index c9a764e..744b499 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "falyx" -version = "0.1.49" +version = "0.1.50" description = "Reliable and introspectable async CLI action framework." authors = ["Roland Thomas Jr "] license = "MIT"