Hide ioactions, Add doc strings, Add tests
This commit is contained in:
105
falyx/command.py
105
falyx/command.py
@@ -1,14 +1,24 @@
|
||||
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
||||
"""command.py
|
||||
Any Action or Command is callable and supports the signature:
|
||||
result = thing(*args, **kwargs)
|
||||
|
||||
This guarantees:
|
||||
- Hook lifecycle (before/after/error/teardown)
|
||||
- Timing
|
||||
- Consistent return values
|
||||
Defines the Command class for Falyx CLI.
|
||||
|
||||
Commands are callable units representing a menu option or CLI task,
|
||||
wrapping either a BaseAction or a simple function. They provide:
|
||||
|
||||
- Hook lifecycle (before, on_success, on_error, after, on_teardown)
|
||||
- Execution timing and duration tracking
|
||||
- Retry logic (single action or recursively through action trees)
|
||||
- Confirmation prompts and spinner integration
|
||||
- Result capturing and summary logging
|
||||
- Rich-based preview for CLI display
|
||||
|
||||
Every Command is self-contained, configurable, and plays a critical role
|
||||
in building robust interactive menus.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cached_property
|
||||
from typing import Any, Callable
|
||||
|
||||
from prompt_toolkit.formatted_text import FormattedText
|
||||
@@ -16,11 +26,12 @@ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
||||
from rich.console import Console
|
||||
from rich.tree import Tree
|
||||
|
||||
from falyx.action import Action, BaseAction
|
||||
from falyx.action import Action, ActionGroup, BaseAction, ChainedAction
|
||||
from falyx.context import ExecutionContext
|
||||
from falyx.debug import register_debug_hooks
|
||||
from falyx.execution_registry import ExecutionRegistry as er
|
||||
from falyx.hook_manager import HookManager, HookType
|
||||
from falyx.io_action import BaseIOAction
|
||||
from falyx.retry import RetryPolicy
|
||||
from falyx.themes.colors import OneColors
|
||||
from falyx.utils import _noop, ensure_async, logger
|
||||
@@ -29,13 +40,63 @@ console = Console()
|
||||
|
||||
|
||||
class Command(BaseModel):
|
||||
"""Class representing an command in the menu."""
|
||||
"""
|
||||
Represents a selectable command in a Falyx menu system.
|
||||
|
||||
A Command wraps an executable action (function, coroutine, or BaseAction)
|
||||
and enhances it with:
|
||||
|
||||
- Lifecycle hooks (before, success, error, after, teardown)
|
||||
- Retry support (single action or recursive for chained/grouped actions)
|
||||
- Confirmation prompts for safe execution
|
||||
- Spinner visuals during execution
|
||||
- Tagging for categorization and filtering
|
||||
- Rich-based CLI previews
|
||||
- Result tracking and summary reporting
|
||||
|
||||
Commands are built to be flexible yet robust, enabling dynamic CLI workflows
|
||||
without sacrificing control or reliability.
|
||||
|
||||
Attributes:
|
||||
key (str): Primary trigger key for the command.
|
||||
description (str): Short description for the menu display.
|
||||
hidden (bool): Toggles visibility in the menu.
|
||||
aliases (list[str]): Alternate keys or phrases.
|
||||
action (BaseAction | Callable): The executable logic.
|
||||
args (tuple): Static positional arguments.
|
||||
kwargs (dict): Static keyword arguments.
|
||||
help_text (str): Additional help or guidance text.
|
||||
color (str): Color theme for CLI rendering.
|
||||
confirm (bool): Whether to require confirmation before executing.
|
||||
confirm_message (str): Custom confirmation prompt.
|
||||
preview_before_confirm (bool): Whether to preview before confirming.
|
||||
spinner (bool): Whether to show a spinner during execution.
|
||||
spinner_message (str): Spinner text message.
|
||||
spinner_type (str): Spinner style (e.g., dots, line, etc.).
|
||||
spinner_style (str): Color or style of the spinner.
|
||||
spinner_kwargs (dict): Extra spinner configuration.
|
||||
hooks (HookManager): Hook manager for lifecycle events.
|
||||
retry (bool): Enable retry on failure.
|
||||
retry_all (bool): Enable retry across chained or grouped actions.
|
||||
retry_policy (RetryPolicy): Retry behavior configuration.
|
||||
tags (list[str]): Organizational tags for the command.
|
||||
logging_hooks (bool): Whether to attach logging hooks automatically.
|
||||
requires_input (bool | None): Indicates if the action needs input.
|
||||
|
||||
Methods:
|
||||
__call__(): Executes the command, respecting hooks and retries.
|
||||
preview(): Rich tree preview of the command.
|
||||
confirmation_prompt(): Formatted prompt for confirmation.
|
||||
result: Property exposing the last result.
|
||||
log_summary(): Summarizes execution details to the console.
|
||||
"""
|
||||
key: str
|
||||
description: str
|
||||
aliases: list[str] = Field(default_factory=list)
|
||||
action: BaseAction | Callable[[], Any] = _noop
|
||||
args: tuple = ()
|
||||
kwargs: dict[str, Any] = Field(default_factory=dict)
|
||||
hidden: bool = False
|
||||
aliases: list[str] = Field(default_factory=list)
|
||||
help_text: str = ""
|
||||
color: str = OneColors.WHITE
|
||||
confirm: bool = False
|
||||
@@ -52,6 +113,7 @@ class Command(BaseModel):
|
||||
retry_policy: RetryPolicy = Field(default_factory=RetryPolicy)
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
logging_hooks: bool = False
|
||||
requires_input: bool | None = None
|
||||
|
||||
_context: ExecutionContext | None = PrivateAttr(default=None)
|
||||
|
||||
@@ -65,12 +127,32 @@ class Command(BaseModel):
|
||||
self.action.set_retry_policy(self.retry_policy)
|
||||
elif self.retry:
|
||||
logger.warning(f"[Command:{self.key}] Retry requested, but action is not an Action instance.")
|
||||
if self.retry_all:
|
||||
if self.retry_all and isinstance(self.action, BaseAction):
|
||||
self.retry_policy.enabled = True
|
||||
self.action.enable_retries_recursively(self.action, self.retry_policy)
|
||||
elif self.retry_all:
|
||||
logger.warning(f"[Command:{self.key}] Retry all requested, but action is not a BaseAction instance.")
|
||||
|
||||
if self.logging_hooks and isinstance(self.action, BaseAction):
|
||||
register_debug_hooks(self.action.hooks)
|
||||
|
||||
if self.requires_input is None and self.detect_requires_input:
|
||||
self.requires_input = True
|
||||
self.hidden = True
|
||||
elif self.requires_input is None:
|
||||
self.requires_input = False
|
||||
|
||||
@cached_property
|
||||
def detect_requires_input(self) -> bool:
|
||||
"""Detect if the action requires input based on its type."""
|
||||
if isinstance(self.action, BaseIOAction):
|
||||
return True
|
||||
elif isinstance(self.action, ChainedAction):
|
||||
return isinstance(self.action.actions[0], BaseIOAction) if self.action.actions else False
|
||||
elif isinstance(self.action, ActionGroup):
|
||||
return any(isinstance(action, BaseIOAction) for action in self.action.actions)
|
||||
return False
|
||||
|
||||
@field_validator("action", mode="before")
|
||||
@classmethod
|
||||
def wrap_callable_as_async(cls, action: Any) -> Any:
|
||||
@@ -81,7 +163,8 @@ class Command(BaseModel):
|
||||
raise TypeError("Action must be a callable or an instance of BaseAction")
|
||||
|
||||
def __str__(self):
|
||||
return f"Command(key='{self.key}', description='{self.description}')"
|
||||
return (f"Command(key='{self.key}', description='{self.description}' "
|
||||
f"action='{self.action}')")
|
||||
|
||||
async def __call__(self, *args, **kwargs):
|
||||
"""Run the action with full hook lifecycle, timing, and error handling."""
|
||||
|
||||
Reference in New Issue
Block a user