Add ActionFactoryAction, Add mode flags for Falyx, Rename inject_last_result_as -> inject_into

This commit is contained in:
Roland Thomas Jr 2025-05-09 23:43:36 -04:00
parent ad803e01be
commit 76e542cfce
Signed by: roland
GPG Key ID: 7C3C2B085A4C2872
10 changed files with 179 additions and 46 deletions

View File

@ -56,7 +56,7 @@ class BaseAction(ABC):
be run independently or as part of Falyx. be run independently or as part of Falyx.
inject_last_result (bool): Whether to inject the previous action's result into kwargs. inject_last_result (bool): Whether to inject the previous action's result into kwargs.
inject_last_result_as (str): The name of the kwarg key to inject the result as inject_into (str): The name of the kwarg key to inject the result as
(default: 'last_result'). (default: 'last_result').
_requires_injection (bool): Whether the action requires input injection. _requires_injection (bool): Whether the action requires input injection.
""" """
@ -66,7 +66,7 @@ class BaseAction(ABC):
name: str, name: str,
hooks: HookManager | None = None, hooks: HookManager | None = None,
inject_last_result: bool = False, inject_last_result: bool = False,
inject_last_result_as: str = "last_result", inject_into: str = "last_result",
never_prompt: bool = False, never_prompt: bool = False,
logging_hooks: bool = False, logging_hooks: bool = False,
) -> None: ) -> None:
@ -75,7 +75,7 @@ class BaseAction(ABC):
self.is_retryable: bool = False self.is_retryable: bool = False
self.shared_context: SharedContext | None = None self.shared_context: SharedContext | None = None
self.inject_last_result: bool = inject_last_result self.inject_last_result: bool = inject_last_result
self.inject_last_result_as: str = inject_last_result_as self.inject_into: str = inject_into
self._never_prompt: bool = never_prompt self._never_prompt: bool = never_prompt
self._requires_injection: bool = False self._requires_injection: bool = False
self._skip_in_chain: bool = False self._skip_in_chain: bool = False
@ -133,7 +133,7 @@ class BaseAction(ABC):
def _maybe_inject_last_result(self, kwargs: dict[str, Any]) -> dict[str, Any]: def _maybe_inject_last_result(self, kwargs: dict[str, Any]) -> dict[str, Any]:
if self.inject_last_result and self.shared_context: if self.inject_last_result and self.shared_context:
key = self.inject_last_result_as key = self.inject_into
if key in kwargs: if key in kwargs:
logger.warning("[%s] ⚠️ Overriding '%s' with last_result", self.name, key) logger.warning("[%s] ⚠️ Overriding '%s' with last_result", self.name, key)
kwargs = dict(kwargs) kwargs = dict(kwargs)
@ -173,7 +173,7 @@ class Action(BaseAction):
kwargs (dict, optional): Static keyword arguments. kwargs (dict, optional): Static keyword arguments.
hooks (HookManager, optional): Hook manager for lifecycle events. hooks (HookManager, optional): Hook manager for lifecycle events.
inject_last_result (bool, optional): Enable last_result injection. inject_last_result (bool, optional): Enable last_result injection.
inject_last_result_as (str, optional): Name of injected key. inject_into (str, optional): Name of injected key.
retry (bool, optional): Enable retry logic. retry (bool, optional): Enable retry logic.
retry_policy (RetryPolicy, optional): Retry settings. retry_policy (RetryPolicy, optional): Retry settings.
""" """
@ -187,11 +187,11 @@ class Action(BaseAction):
kwargs: dict[str, Any] | None = None, kwargs: dict[str, Any] | None = None,
hooks: HookManager | None = None, hooks: HookManager | None = None,
inject_last_result: bool = False, inject_last_result: bool = False,
inject_last_result_as: str = "last_result", inject_into: str = "last_result",
retry: bool = False, retry: bool = False,
retry_policy: RetryPolicy | None = None, retry_policy: RetryPolicy | None = None,
) -> None: ) -> None:
super().__init__(name, hooks, inject_last_result, inject_last_result_as) super().__init__(name, hooks, inject_last_result, inject_into)
self.action = action self.action = action
self.rollback = rollback self.rollback = rollback
self.args = args self.args = args
@ -257,7 +257,7 @@ class Action(BaseAction):
if context.result is not None: if context.result is not None:
logger.info("[%s] ✅ Recovered: %s", self.name, self.name) logger.info("[%s] ✅ Recovered: %s", self.name, self.name)
return context.result return context.result
raise error raise
finally: finally:
context.stop_timer() context.stop_timer()
await self.hooks.trigger(HookType.AFTER, context) await self.hooks.trigger(HookType.AFTER, context)
@ -267,7 +267,7 @@ class Action(BaseAction):
async def preview(self, parent: Tree | None = None): async def preview(self, parent: Tree | None = None):
label = [f"[{OneColors.GREEN_b}]⚙ Action[/] '{self.name}'"] label = [f"[{OneColors.GREEN_b}]⚙ Action[/] '{self.name}'"]
if self.inject_last_result: if self.inject_last_result:
label.append(f" [dim](injects '{self.inject_last_result_as}')[/dim]") label.append(f" [dim](injects '{self.inject_into}')[/dim]")
if self.retry_policy.enabled: if self.retry_policy.enabled:
label.append( label.append(
f"\n[dim]↻ Retries:[/] {self.retry_policy.max_retries}x, " f"\n[dim]↻ Retries:[/] {self.retry_policy.max_retries}x, "
@ -413,7 +413,7 @@ class ChainedAction(BaseAction, ActionListMixin):
actions (list): List of actions or literals to execute. actions (list): List of actions or literals to execute.
hooks (HookManager, optional): Hooks for lifecycle events. hooks (HookManager, optional): Hooks for lifecycle events.
inject_last_result (bool, optional): Whether to inject last results into kwargs by default. inject_last_result (bool, optional): Whether to inject last results into kwargs by default.
inject_last_result_as (str, optional): Key name for injection. inject_into (str, optional): Key name for injection.
auto_inject (bool, optional): Auto-enable injection for subsequent actions. auto_inject (bool, optional): Auto-enable injection for subsequent actions.
return_list (bool, optional): Whether to return a list of all results. False returns the last result. return_list (bool, optional): Whether to return a list of all results. False returns the last result.
""" """
@ -424,11 +424,11 @@ class ChainedAction(BaseAction, ActionListMixin):
actions: list[BaseAction | Any] | None = None, actions: list[BaseAction | Any] | None = None,
hooks: HookManager | None = None, hooks: HookManager | None = None,
inject_last_result: bool = False, inject_last_result: bool = False,
inject_last_result_as: str = "last_result", inject_into: str = "last_result",
auto_inject: bool = False, auto_inject: bool = False,
return_list: bool = False, return_list: bool = False,
) -> None: ) -> None:
super().__init__(name, hooks, inject_last_result, inject_last_result_as) super().__init__(name, hooks, inject_last_result, inject_into)
ActionListMixin.__init__(self) ActionListMixin.__init__(self)
self.auto_inject = auto_inject self.auto_inject = auto_inject
self.return_list = return_list self.return_list = return_list
@ -482,9 +482,7 @@ class ChainedAction(BaseAction, ActionListMixin):
last_result = shared_context.last_result() last_result = shared_context.last_result()
try: try:
if self.requires_io_injection() and last_result is not None: if self.requires_io_injection() and last_result is not None:
result = await prepared( result = await prepared(**{prepared.inject_into: last_result})
**{prepared.inject_last_result_as: last_result}
)
else: else:
result = await prepared(*args, **updated_kwargs) result = await prepared(*args, **updated_kwargs)
except Exception as error: except Exception as error:
@ -559,7 +557,7 @@ class ChainedAction(BaseAction, ActionListMixin):
async def preview(self, parent: Tree | None = None): async def preview(self, parent: Tree | None = None):
label = [f"[{OneColors.CYAN_b}]⛓ ChainedAction[/] '{self.name}'"] label = [f"[{OneColors.CYAN_b}]⛓ ChainedAction[/] '{self.name}'"]
if self.inject_last_result: if self.inject_last_result:
label.append(f" [dim](injects '{self.inject_last_result_as}')[/dim]") label.append(f" [dim](injects '{self.inject_into}')[/dim]")
tree = parent.add("".join(label)) if parent else Tree("".join(label)) tree = parent.add("".join(label)) if parent else Tree("".join(label))
for action in self.actions: for action in self.actions:
await action.preview(parent=tree) await action.preview(parent=tree)
@ -603,7 +601,7 @@ class ActionGroup(BaseAction, ActionListMixin):
actions (list): List of actions or literals to execute. actions (list): List of actions or literals to execute.
hooks (HookManager, optional): Hooks for lifecycle events. hooks (HookManager, optional): Hooks for lifecycle events.
inject_last_result (bool, optional): Whether to inject last results into kwargs by default. inject_last_result (bool, optional): Whether to inject last results into kwargs by default.
inject_last_result_as (str, optional): Key name for injection. inject_into (str, optional): Key name for injection.
""" """
def __init__( def __init__(
@ -612,9 +610,9 @@ class ActionGroup(BaseAction, ActionListMixin):
actions: list[BaseAction] | None = None, actions: list[BaseAction] | None = None,
hooks: HookManager | None = None, hooks: HookManager | None = None,
inject_last_result: bool = False, inject_last_result: bool = False,
inject_last_result_as: str = "last_result", inject_into: str = "last_result",
): ):
super().__init__(name, hooks, inject_last_result, inject_last_result_as) super().__init__(name, hooks, inject_last_result, inject_into)
ActionListMixin.__init__(self) ActionListMixin.__init__(self)
if actions: if actions:
self.set_actions(actions) self.set_actions(actions)
@ -694,7 +692,7 @@ class ActionGroup(BaseAction, ActionListMixin):
async def preview(self, parent: Tree | None = None): async def preview(self, parent: Tree | None = None):
label = [f"[{OneColors.MAGENTA_b}]⏩ ActionGroup (parallel)[/] '{self.name}'"] label = [f"[{OneColors.MAGENTA_b}]⏩ ActionGroup (parallel)[/] '{self.name}'"]
if self.inject_last_result: if self.inject_last_result:
label.append(f" [dim](receives '{self.inject_last_result_as}')[/dim]") label.append(f" [dim](receives '{self.inject_into}')[/dim]")
tree = parent.add("".join(label)) if parent else Tree("".join(label)) tree = parent.add("".join(label)) if parent else Tree("".join(label))
actions = self.actions.copy() actions = self.actions.copy()
random.shuffle(actions) random.shuffle(actions)
@ -726,7 +724,7 @@ class ProcessAction(BaseAction):
hooks (HookManager, optional): Hook manager for lifecycle events. hooks (HookManager, optional): Hook manager for lifecycle events.
executor (ProcessPoolExecutor, optional): Custom executor if desired. executor (ProcessPoolExecutor, optional): Custom executor if desired.
inject_last_result (bool, optional): Inject last result into the function. inject_last_result (bool, optional): Inject last result into the function.
inject_last_result_as (str, optional): Name of the injected key. inject_into (str, optional): Name of the injected key.
""" """
def __init__( def __init__(
@ -738,9 +736,9 @@ class ProcessAction(BaseAction):
hooks: HookManager | None = None, hooks: HookManager | None = None,
executor: ProcessPoolExecutor | None = None, executor: ProcessPoolExecutor | None = None,
inject_last_result: bool = False, inject_last_result: bool = False,
inject_last_result_as: str = "last_result", inject_into: str = "last_result",
): ):
super().__init__(name, hooks, inject_last_result, inject_last_result_as) super().__init__(name, hooks, inject_last_result, inject_into)
self.func = func self.func = func
self.args = args self.args = args
self.kwargs = kwargs or {} self.kwargs = kwargs or {}
@ -800,7 +798,7 @@ class ProcessAction(BaseAction):
f"[{OneColors.DARK_YELLOW_b}]🧠 ProcessAction (new process)[/] '{self.name}'" f"[{OneColors.DARK_YELLOW_b}]🧠 ProcessAction (new process)[/] '{self.name}'"
] ]
if self.inject_last_result: if self.inject_last_result:
label.append(f" [dim](injects '{self.inject_last_result_as}')[/dim]") label.append(f" [dim](injects '{self.inject_into}')[/dim]")
if parent: if parent:
parent.add("".join(label)) parent.add("".join(label))
else: else:

95
falyx/action_factory.py Normal file
View File

@ -0,0 +1,95 @@
from typing import Any
from rich.tree import Tree
from falyx.action import BaseAction
from falyx.context import ExecutionContext
from falyx.execution_registry import ExecutionRegistry as er
from falyx.hook_manager import HookType
from falyx.protocols import ActionFactoryProtocol
from falyx.themes.colors import OneColors
class ActionFactoryAction(BaseAction):
"""
Dynamically creates and runs another Action at runtime using a factory function.
This is useful for generating context-specific behavior (e.g., dynamic HTTPActions)
where the structure of the next action depends on runtime values.
Args:
name (str): Name of the action.
factory (Callable): A function that returns a BaseAction given args/kwargs.
inject_last_result (bool): Whether to inject last_result into the factory.
inject_into (str): The name of the kwarg to inject last_result as.
"""
def __init__(
self,
name: str,
factory: ActionFactoryProtocol,
*,
inject_last_result: bool = False,
inject_into: str = "last_result",
preview_args: tuple[Any, ...] = (),
preview_kwargs: dict[str, Any] = {},
):
super().__init__(
name=name,
inject_last_result=inject_last_result,
inject_into=inject_into,
)
self.factory = factory
self.preview_args = preview_args
self.preview_kwargs = preview_kwargs
async def _run(self, *args, **kwargs) -> Any:
updated_kwargs = self._maybe_inject_last_result(kwargs)
context = ExecutionContext(
name=f"{self.name} (factory)",
args=args,
kwargs=updated_kwargs,
action=self,
)
context.start_timer()
try:
await self.hooks.trigger(HookType.BEFORE, context)
generated_action = self.factory(*args, **updated_kwargs)
if not isinstance(generated_action, BaseAction):
raise TypeError(
f"[{self.name}] Factory must return a BaseAction, got {type(generated_action).__name__}"
)
if self.shared_context:
generated_action.set_shared_context(self.shared_context)
if self.options_manager:
generated_action.set_options_manager(self.options_manager)
context.result = await generated_action(*args, **kwargs)
await self.hooks.trigger(HookType.ON_SUCCESS, context)
return context.result
except Exception as error:
context.exception = error
await self.hooks.trigger(HookType.ON_ERROR, context)
raise
finally:
context.stop_timer()
await self.hooks.trigger(HookType.AFTER, context)
await self.hooks.trigger(HookType.ON_TEARDOWN, context)
er.record(context)
async def preview(self, parent: Tree | None = None):
label = f"[{OneColors.CYAN_b}]🏗️ ActionFactory[/] '{self.name}'"
tree = parent.add(label) if parent else Tree(label)
try:
generated = self.factory(*self.preview_args, **self.preview_kwargs)
if isinstance(generated, BaseAction):
await generated.preview(parent=tree)
else:
tree.add(
f"[{OneColors.DARK_RED}]⚠️ Factory did not return a BaseAction[/]"
)
except Exception as error:
tree.add(f"[{OneColors.DARK_RED}]⚠️ Preview failed: {error}[/]")
if not parent:
self.console.print(tree)

View File

@ -24,6 +24,7 @@ import logging
import sys import sys
from argparse import Namespace from argparse import Namespace
from difflib import get_close_matches from difflib import get_close_matches
from enum import Enum
from functools import cached_property from functools import cached_property
from typing import Any, Callable from typing import Any, Callable
@ -59,6 +60,13 @@ from falyx.utils import CaseInsensitiveDict, chunks, get_program_invocation, log
from falyx.version import __version__ from falyx.version import __version__
class FalyxMode(str, Enum):
MENU = "menu"
RUN = "run"
PREVIEW = "preview"
RUN_ALL = "run-all"
class Falyx: class Falyx:
""" """
Main menu controller for Falyx CLI applications. Main menu controller for Falyx CLI applications.
@ -149,6 +157,7 @@ class Falyx:
self.custom_table: Callable[["Falyx"], Table] | Table | None = custom_table self.custom_table: Callable[["Falyx"], Table] | Table | None = custom_table
self.validate_options(cli_args, options) self.validate_options(cli_args, options)
self._prompt_session: PromptSession | None = None self._prompt_session: PromptSession | None = None
self.mode = FalyxMode.MENU
def validate_options( def validate_options(
self, self,
@ -272,6 +281,11 @@ class Falyx:
) )
self.console.print(table, justify="center") self.console.print(table, justify="center")
if self.mode == FalyxMode.MENU:
self.console.print(
f"📦 Tip: Type '[{OneColors.LIGHT_YELLOW}]?[KEY][/]' to preview a command before running it.\n",
justify="center",
)
def _get_help_command(self) -> Command: def _get_help_command(self) -> Command:
"""Returns the help command for the menu.""" """Returns the help command for the menu."""
@ -329,7 +343,8 @@ class Falyx:
error_message = " ".join(message_lines) error_message = " ".join(message_lines)
def validator(text): def validator(text):
return True if self.get_command(text, from_validate=True) else False _, choice = self.get_command(text, from_validate=True)
return True if choice else False
return Validator.from_callable( return Validator.from_callable(
validator, validator,
@ -668,17 +683,25 @@ class Falyx:
else: else:
return self.build_default_table() return self.build_default_table()
def get_command(self, choice: str, from_validate=False) -> Command | None: def parse_preview_command(self, input_str: str) -> tuple[bool, str]:
if input_str.startswith("?"):
return True, input_str[1:].strip()
return False, input_str.strip()
def get_command(
self, choice: str, from_validate=False
) -> tuple[bool, Command | None]:
"""Returns the selected command based on user input. Supports keys, aliases, and abbreviations.""" """Returns the selected command based on user input. Supports keys, aliases, and abbreviations."""
is_preview, choice = self.parse_preview_command(choice)
choice = choice.upper() choice = choice.upper()
name_map = self._name_map name_map = self._name_map
if choice in name_map: if choice in name_map:
return name_map[choice] return is_preview, name_map[choice]
prefix_matches = [cmd for key, cmd in name_map.items() if key.startswith(choice)] prefix_matches = [cmd for key, cmd in name_map.items() if key.startswith(choice)]
if len(prefix_matches) == 1: if len(prefix_matches) == 1:
return prefix_matches[0] return is_preview, prefix_matches[0]
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:
@ -694,7 +717,7 @@ class Falyx:
self.console.print( self.console.print(
f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown command '{choice}'[/]" f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown command '{choice}'[/]"
) )
return None return is_preview, None
def _create_context(self, selected_command: Command) -> ExecutionContext: def _create_context(self, selected_command: Command) -> ExecutionContext:
"""Creates a context dictionary for the selected command.""" """Creates a context dictionary for the selected command."""
@ -718,11 +741,16 @@ class Falyx:
async def process_command(self) -> bool: async def process_command(self) -> bool:
"""Processes the action of the selected command.""" """Processes the action of the selected command."""
choice = await self.prompt_session.prompt_async() choice = await self.prompt_session.prompt_async()
selected_command = self.get_command(choice) is_preview, selected_command = self.get_command(choice)
if not selected_command: if not selected_command:
logger.info(f"Invalid command '{choice}'.") logger.info(f"Invalid command '{choice}'.")
return True return True
if is_preview:
logger.info(f"Preview command '{selected_command.key}' selected.")
await selected_command.preview()
return True
if selected_command.requires_input: if selected_command.requires_input:
program = get_program_invocation() program = get_program_invocation()
self.console.print( self.console.print(
@ -759,7 +787,7 @@ class Falyx:
async def run_key(self, command_key: str, return_context: bool = False) -> Any: async def run_key(self, command_key: str, return_context: bool = False) -> Any:
"""Run a command by key without displaying the menu (non-interactive mode).""" """Run a command by key without displaying the menu (non-interactive mode)."""
self.debug_hooks() self.debug_hooks()
selected_command = self.get_command(command_key) _, selected_command = self.get_command(command_key)
self.last_run_command = selected_command self.last_run_command = selected_command
if not selected_command: if not selected_command:
@ -899,7 +927,8 @@ class Falyx:
sys.exit(0) sys.exit(0)
if self.cli_args.command == "preview": if self.cli_args.command == "preview":
command = self.get_command(self.cli_args.name) self.mode = FalyxMode.PREVIEW
_, command = self.get_command(self.cli_args.name)
if not command: if not command:
self.console.print( self.console.print(
f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found.[/]" f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found.[/]"
@ -912,7 +941,8 @@ class Falyx:
sys.exit(0) sys.exit(0)
if self.cli_args.command == "run": if self.cli_args.command == "run":
command = self.get_command(self.cli_args.name) self.mode = FalyxMode.RUN
_, command = self.get_command(self.cli_args.name)
if not command: if not command:
self.console.print( self.console.print(
f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found.[/]" f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found.[/]"
@ -927,6 +957,7 @@ class Falyx:
sys.exit(0) sys.exit(0)
if self.cli_args.command == "run-all": if self.cli_args.command == "run-all":
self.mode = FalyxMode.RUN_ALL
matching = [ matching = [
cmd cmd
for cmd in self.commands.values() for cmd in self.commands.values()

View File

@ -56,7 +56,7 @@ class HTTPAction(Action):
data (Any, optional): Raw data or form-encoded body. data (Any, optional): Raw data or form-encoded body.
hooks (HookManager, optional): Hook manager for lifecycle events. hooks (HookManager, optional): Hook manager for lifecycle events.
inject_last_result (bool): Enable last_result injection. inject_last_result (bool): Enable last_result injection.
inject_last_result_as (str): Name of injected key. inject_into (str): Name of injected key.
retry (bool): Enable retry logic. retry (bool): Enable retry logic.
retry_policy (RetryPolicy): Retry settings. retry_policy (RetryPolicy): Retry settings.
""" """
@ -74,7 +74,7 @@ class HTTPAction(Action):
data: Any = None, data: Any = None,
hooks=None, hooks=None,
inject_last_result: bool = False, inject_last_result: bool = False,
inject_last_result_as: str = "last_result", inject_into: str = "last_result",
retry: bool = False, retry: bool = False,
retry_policy=None, retry_policy=None,
): ):
@ -92,7 +92,7 @@ class HTTPAction(Action):
kwargs={}, kwargs={},
hooks=hooks, hooks=hooks,
inject_last_result=inject_last_result, inject_last_result=inject_last_result,
inject_last_result_as=inject_last_result_as, inject_into=inject_into,
retry=retry, retry=retry,
retry_policy=retry_policy, retry_policy=retry_policy,
) )
@ -138,7 +138,7 @@ class HTTPAction(Action):
f"\n[dim]URL:[/] {self.url}", f"\n[dim]URL:[/] {self.url}",
] ]
if self.inject_last_result: if self.inject_last_result:
label.append(f"\n[dim]Injects:[/] '{self.inject_last_result_as}'") label.append(f"\n[dim]Injects:[/] '{self.inject_into}'")
if self.retry_policy and self.retry_policy.enabled: if self.retry_policy and self.retry_policy.enabled:
label.append( label.append(
f"\n[dim]↻ Retries:[/] {self.retry_policy.max_retries}x, " f"\n[dim]↻ Retries:[/] {self.retry_policy.max_retries}x, "

View File

@ -83,7 +83,7 @@ class BaseIOAction(BaseAction):
raise NotImplementedError raise NotImplementedError
async def _resolve_input(self, kwargs: dict[str, Any]) -> str | bytes: async def _resolve_input(self, kwargs: dict[str, Any]) -> str | bytes:
last_result = kwargs.pop(self.inject_last_result_as, None) last_result = kwargs.pop(self.inject_into, None)
data = await self._read_stdin() data = await self._read_stdin()
if data: if data:
@ -168,7 +168,7 @@ class BaseIOAction(BaseAction):
async def preview(self, parent: Tree | None = None): async def preview(self, parent: Tree | None = None):
label = [f"[{OneColors.GREEN_b}]⚙ IOAction[/] '{self.name}'"] label = [f"[{OneColors.GREEN_b}]⚙ IOAction[/] '{self.name}'"]
if self.inject_last_result: if self.inject_last_result:
label.append(f" [dim](injects '{self.inject_last_result_as}')[/dim]") label.append(f" [dim](injects '{self.inject_into}')[/dim]")
if parent: if parent:
parent.add("".join(label)) parent.add("".join(label))
else: else:
@ -243,7 +243,7 @@ class ShellAction(BaseIOAction):
async def preview(self, parent: Tree | None = None): async def preview(self, parent: Tree | None = None):
label = [f"[{OneColors.GREEN_b}]⚙ ShellAction[/] '{self.name}'"] label = [f"[{OneColors.GREEN_b}]⚙ ShellAction[/] '{self.name}'"]
if self.inject_last_result: if self.inject_last_result:
label.append(f" [dim](injects '{self.inject_last_result_as}')[/dim]") label.append(f" [dim](injects '{self.inject_into}')[/dim]")
if parent: if parent:
parent.add("".join(label)) parent.add("".join(label))
else: else:

View File

@ -101,7 +101,7 @@ class MenuAction(BaseAction):
prompt_message: str = "Select > ", prompt_message: str = "Select > ",
default_selection: str = "", default_selection: str = "",
inject_last_result: bool = False, inject_last_result: bool = False,
inject_last_result_as: str = "last_result", inject_into: str = "last_result",
console: Console | None = None, console: Console | None = None,
prompt_session: PromptSession | None = None, prompt_session: PromptSession | None = None,
never_prompt: bool = False, never_prompt: bool = False,
@ -111,7 +111,7 @@ class MenuAction(BaseAction):
super().__init__( super().__init__(
name, name,
inject_last_result=inject_last_result, inject_last_result=inject_last_result,
inject_last_result_as=inject_last_result_as, inject_into=inject_into,
never_prompt=never_prompt, never_prompt=never_prompt,
) )
self.menu_options = menu_options self.menu_options = menu_options

9
falyx/protocols.py Normal file
View File

@ -0,0 +1,9 @@
from __future__ import annotations
from typing import Any, Protocol
from falyx.action import BaseAction
class ActionFactoryProtocol(Protocol):
def __call__(self, *args: Any, **kwargs: Any) -> BaseAction: ...

View File

@ -33,7 +33,7 @@ class SelectionAction(BaseAction):
prompt_message: str = "Select > ", prompt_message: str = "Select > ",
default_selection: str = "", default_selection: str = "",
inject_last_result: bool = False, inject_last_result: bool = False,
inject_last_result_as: str = "last_result", inject_into: str = "last_result",
return_key: bool = False, return_key: bool = False,
console: Console | None = None, console: Console | None = None,
prompt_session: PromptSession | None = None, prompt_session: PromptSession | None = None,
@ -43,7 +43,7 @@ class SelectionAction(BaseAction):
super().__init__( super().__init__(
name, name,
inject_last_result=inject_last_result, inject_last_result=inject_last_result,
inject_last_result_as=inject_last_result_as, inject_into=inject_into,
never_prompt=never_prompt, never_prompt=never_prompt,
) )
self.selections: list[str] | CaseInsensitiveDict = selections self.selections: list[str] | CaseInsensitiveDict = selections

View File

@ -1 +1 @@
__version__ = "0.1.19" __version__ = "0.1.20"

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "falyx" name = "falyx"
version = "0.1.19" version = "0.1.20"
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"