Add register_teardown, Rename session -> prompt_session, Add sets, tuples to SelectionAction

This commit is contained in:
Roland Thomas Jr 2025-05-08 22:10:05 -04:00
parent 53729f089f
commit ad803e01be
Signed by: roland
GPG Key ID: 7C3C2B085A4C2872
9 changed files with 69 additions and 46 deletions

View File

@ -448,6 +448,8 @@ class ChainedAction(BaseAction, ActionListMixin):
if self.actions and self.auto_inject and not action.inject_last_result: if self.actions and self.auto_inject and not action.inject_last_result:
action.inject_last_result = True action.inject_last_result = True
super().add_action(action) super().add_action(action)
if hasattr(action, "register_teardown") and callable(action.register_teardown):
action.register_teardown(self.hooks)
async def _run(self, *args, **kwargs) -> list[Any]: async def _run(self, *args, **kwargs) -> list[Any]:
if not self.actions: if not self.actions:
@ -617,6 +619,22 @@ class ActionGroup(BaseAction, ActionListMixin):
if actions: if actions:
self.set_actions(actions) self.set_actions(actions)
def _wrap_if_needed(self, action: BaseAction | Any) -> BaseAction:
if isinstance(action, BaseAction):
return action
elif callable(action):
return Action(name=action.__name__, action=action)
else:
raise TypeError(
f"ActionGroup only accepts BaseAction or callable, got {type(action).__name__}"
)
def add_action(self, action: BaseAction | Any) -> None:
action = self._wrap_if_needed(action)
super().add_action(action)
if hasattr(action, "register_teardown") and callable(action.register_teardown):
action.register_teardown(self.hooks)
async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]: async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
shared_context = SharedContext(name=self.name, is_parallel=True) shared_context = SharedContext(name=self.name, is_parallel=True)
if self.shared_context: if self.shared_context:

View File

@ -148,7 +148,7 @@ class Falyx:
self.render_menu: Callable[["Falyx"], None] | None = render_menu self.render_menu: Callable[["Falyx"], None] | None = render_menu
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._session: PromptSession | None = None self._prompt_session: PromptSession | None = None
def validate_options( def validate_options(
self, self,
@ -337,11 +337,11 @@ class Falyx:
move_cursor_to_end=True, move_cursor_to_end=True,
) )
def _invalidate_session_cache(self): def _invalidate_prompt_session_cache(self):
"""Forces the session to be recreated on the next access.""" """Forces the prompt session to be recreated on the next access."""
if hasattr(self, "session"): if hasattr(self, "prompt_session"):
del self.session del self.prompt_session
self._session = None self._prompt_session = None
def add_help_command(self): def add_help_command(self):
"""Adds a help command to the menu if it doesn't already exist.""" """Adds a help command to the menu if it doesn't already exist."""
@ -375,7 +375,7 @@ class Falyx:
raise FalyxError( raise FalyxError(
"Bottom bar must be a string, callable, or BottomBar instance." "Bottom bar must be a string, callable, or BottomBar instance."
) )
self._invalidate_session_cache() self._invalidate_prompt_session_cache()
def _get_bottom_bar_render(self) -> Callable[[], Any] | str | None: def _get_bottom_bar_render(self) -> Callable[[], Any] | str | None:
"""Returns the bottom bar for the menu.""" """Returns the bottom bar for the menu."""
@ -390,10 +390,10 @@ class Falyx:
return None return None
@cached_property @cached_property
def session(self) -> PromptSession: def prompt_session(self) -> PromptSession:
"""Returns the prompt session for the menu.""" """Returns the prompt session for the menu."""
if self._session is None: if self._prompt_session is None:
self._session = PromptSession( self._prompt_session = PromptSession(
message=self.prompt, message=self.prompt,
multiline=False, multiline=False,
completer=self._get_completer(), completer=self._get_completer(),
@ -402,7 +402,7 @@ class Falyx:
bottom_toolbar=self._get_bottom_bar_render(), bottom_toolbar=self._get_bottom_bar_render(),
key_bindings=self.key_bindings, key_bindings=self.key_bindings,
) )
return self._session return self._prompt_session
def register_all_hooks(self, hook_type: HookType, hooks: Hook | list[Hook]) -> None: def register_all_hooks(self, hook_type: HookType, hooks: Hook | list[Hook]) -> None:
"""Registers hooks for all commands in the menu and actions recursively.""" """Registers hooks for all commands in the menu and actions recursively."""
@ -717,7 +717,7 @@ 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.session.prompt_async() choice = await self.prompt_session.prompt_async()
selected_command = self.get_command(choice) 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}'.")

View File

@ -15,6 +15,7 @@ from rich.tree import Tree
from falyx.action import Action from falyx.action import Action
from falyx.context import ExecutionContext, SharedContext from falyx.context import ExecutionContext, SharedContext
from falyx.hook_manager import HookManager, HookType
from falyx.themes.colors import OneColors from falyx.themes.colors import OneColors
from falyx.utils import logger from falyx.utils import logger
@ -97,7 +98,6 @@ class HTTPAction(Action):
) )
async def _request(self, *args, **kwargs) -> dict[str, Any]: async def _request(self, *args, **kwargs) -> dict[str, Any]:
# TODO: Add check for HOOK registration
if self.shared_context: if self.shared_context:
context: SharedContext = self.shared_context context: SharedContext = self.shared_context
session = context.get("http_session") session = context.get("http_session")
@ -128,6 +128,9 @@ class HTTPAction(Action):
if not self.shared_context: if not self.shared_context:
await session.close() await session.close()
def register_teardown(self, hooks: HookManager):
hooks.register(HookType.ON_TEARDOWN, close_shared_http_session)
async def preview(self, parent: Tree | None = None): async def preview(self, parent: Tree | None = None):
label = [ label = [
f"[{OneColors.CYAN_b}]🌐 HTTPAction[/] '{self.name}'", f"[{OneColors.CYAN_b}]🌐 HTTPAction[/] '{self.name}'",

View File

@ -33,8 +33,10 @@ async def cleanup():
""" """
GLOBAL_CONFIG = """\ GLOBAL_CONFIG = """\
async def cleanup(): - key: C
print("🧹 Cleaning temp files...") description: Cleanup temp files
action: tasks.cleanup
aliases: [clean, cleanup]
""" """
console = Console(color_system="auto") console = Console(color_system="auto")

View File

@ -168,15 +168,13 @@ class MenuAction(BaseAction):
await self.hooks.trigger(HookType.BEFORE, context) await self.hooks.trigger(HookType.BEFORE, context)
key = effective_default key = effective_default
if not self.never_prompt: if not self.never_prompt:
console = self.console
session = self.prompt_session
table = self._build_table() table = self._build_table()
key = await prompt_for_selection( key = await prompt_for_selection(
self.menu_options.keys(), self.menu_options.keys(),
table, table,
default_selection=self.default_selection, default_selection=self.default_selection,
console=console, console=self.console,
session=session, prompt_session=self.prompt_session,
prompt_message=self.prompt_message, prompt_message=self.prompt_message,
show_table=self.show_table, show_table=self.show_table,
) )

View File

@ -203,17 +203,17 @@ async def prompt_for_index(
min_index: int = 0, min_index: int = 0,
default_selection: str = "", default_selection: str = "",
console: Console | None = None, console: Console | None = None,
session: PromptSession | None = None, prompt_session: PromptSession | None = None,
prompt_message: str = "Select an option > ", prompt_message: str = "Select an option > ",
show_table: bool = True, show_table: bool = True,
): ):
session = session or PromptSession() prompt_session = prompt_session or PromptSession()
console = console or Console(color_system="auto") console = console or Console(color_system="auto")
if show_table: if show_table:
console.print(table) console.print(table)
selection = await session.prompt_async( selection = await prompt_session.prompt_async(
message=prompt_message, message=prompt_message,
validator=int_range_validator(min_index, max_index), validator=int_range_validator(min_index, max_index),
default=default_selection, default=default_selection,
@ -226,18 +226,18 @@ async def prompt_for_selection(
table: Table, table: Table,
default_selection: str = "", default_selection: str = "",
console: Console | None = None, console: Console | None = None,
session: PromptSession | None = None, prompt_session: PromptSession | None = None,
prompt_message: str = "Select an option > ", prompt_message: str = "Select an option > ",
show_table: bool = True, show_table: bool = True,
) -> str: ) -> str:
"""Prompt the user to select a key from a set of options. Return the selected key.""" """Prompt the user to select a key from a set of options. Return the selected key."""
session = session or PromptSession() prompt_session = prompt_session or PromptSession()
console = console or Console(color_system="auto") console = console or Console(color_system="auto")
if show_table: if show_table:
console.print(table, justify="center") console.print(table, justify="center")
selected = await session.prompt_async( selected = await prompt_session.prompt_async(
message=prompt_message, message=prompt_message,
validator=key_validator(keys), validator=key_validator(keys),
default=default_selection, default=default_selection,
@ -250,7 +250,7 @@ async def select_value_from_list(
title: str, title: str,
selections: Sequence[str], selections: Sequence[str],
console: Console | None = None, console: Console | None = None,
session: PromptSession | None = None, prompt_session: PromptSession | None = None,
prompt_message: str = "Select an option > ", prompt_message: str = "Select an option > ",
default_selection: str = "", default_selection: str = "",
columns: int = 4, columns: int = 4,
@ -283,7 +283,7 @@ async def select_value_from_list(
caption_style, caption_style,
highlight, highlight,
) )
session = session or PromptSession() prompt_session = prompt_session or PromptSession()
console = console or Console(color_system="auto") console = console or Console(color_system="auto")
selection_index = await prompt_for_index( selection_index = await prompt_for_index(
@ -291,7 +291,7 @@ async def select_value_from_list(
table, table,
default_selection=default_selection, default_selection=default_selection,
console=console, console=console,
session=session, prompt_session=prompt_session,
prompt_message=prompt_message, prompt_message=prompt_message,
) )
@ -302,12 +302,12 @@ async def select_key_from_dict(
selections: dict[str, SelectionOption], selections: dict[str, SelectionOption],
table: Table, table: Table,
console: Console | None = None, console: Console | None = None,
session: PromptSession | None = None, prompt_session: PromptSession | None = None,
prompt_message: str = "Select an option > ", prompt_message: str = "Select an option > ",
default_selection: str = "", default_selection: str = "",
) -> Any: ) -> Any:
"""Prompt for a key from a dict, returns the key.""" """Prompt for a key from a dict, returns the key."""
session = session or PromptSession() prompt_session = prompt_session or PromptSession()
console = console or Console(color_system="auto") console = console or Console(color_system="auto")
console.print(table) console.print(table)
@ -317,7 +317,7 @@ async def select_key_from_dict(
table, table,
default_selection=default_selection, default_selection=default_selection,
console=console, console=console,
session=session, prompt_session=prompt_session,
prompt_message=prompt_message, prompt_message=prompt_message,
) )
@ -326,12 +326,12 @@ async def select_value_from_dict(
selections: dict[str, SelectionOption], selections: dict[str, SelectionOption],
table: Table, table: Table,
console: Console | None = None, console: Console | None = None,
session: PromptSession | None = None, prompt_session: PromptSession | None = None,
prompt_message: str = "Select an option > ", prompt_message: str = "Select an option > ",
default_selection: str = "", default_selection: str = "",
) -> Any: ) -> Any:
"""Prompt for a key from a dict, but return the value.""" """Prompt for a key from a dict, but return the value."""
session = session or PromptSession() prompt_session = prompt_session or PromptSession()
console = console or Console(color_system="auto") console = console or Console(color_system="auto")
console.print(table) console.print(table)
@ -341,7 +341,7 @@ async def select_value_from_dict(
table, table,
default_selection=default_selection, default_selection=default_selection,
console=console, console=console,
session=session, prompt_session=prompt_session,
prompt_message=prompt_message, prompt_message=prompt_message,
) )
@ -352,7 +352,7 @@ async def get_selection_from_dict_menu(
title: str, title: str,
selections: dict[str, SelectionOption], selections: dict[str, SelectionOption],
console: Console | None = None, console: Console | None = None,
session: PromptSession | None = None, prompt_session: PromptSession | None = None,
prompt_message: str = "Select an option > ", prompt_message: str = "Select an option > ",
default_selection: str = "", default_selection: str = "",
): ):
@ -366,7 +366,7 @@ async def get_selection_from_dict_menu(
selections, selections,
table, table,
console, console,
session, prompt_session,
prompt_message, prompt_message,
default_selection, default_selection,
) )

View File

@ -26,7 +26,7 @@ class SelectionAction(BaseAction):
def __init__( def __init__(
self, self,
name: str, name: str,
selections: list[str] | dict[str, SelectionOption], selections: list[str] | set[str] | tuple[str, ...] | dict[str, SelectionOption],
*, *,
title: str = "Select an option", title: str = "Select an option",
columns: int = 2, columns: int = 2,
@ -36,7 +36,7 @@ class SelectionAction(BaseAction):
inject_last_result_as: str = "last_result", inject_last_result_as: str = "last_result",
return_key: bool = False, return_key: bool = False,
console: Console | None = None, console: Console | None = None,
session: PromptSession | None = None, prompt_session: PromptSession | None = None,
never_prompt: bool = False, never_prompt: bool = False,
show_table: bool = True, show_table: bool = True,
): ):
@ -51,7 +51,7 @@ class SelectionAction(BaseAction):
self.title = title self.title = title
self.columns = columns self.columns = columns
self.console = console or Console(color_system="auto") self.console = console or Console(color_system="auto")
self.session = session or PromptSession() self.prompt_session = prompt_session or PromptSession()
self.default_selection = default_selection self.default_selection = default_selection
self.prompt_message = prompt_message self.prompt_message = prompt_message
self.show_table = show_table self.show_table = show_table
@ -61,9 +61,11 @@ class SelectionAction(BaseAction):
return self._selections return self._selections
@selections.setter @selections.setter
def selections(self, value: list[str] | dict[str, SelectionOption]): def selections(
if isinstance(value, list): self, value: list[str] | set[str] | tuple[str, ...] | dict[str, SelectionOption]
self._selections: list[str] | CaseInsensitiveDict = value ):
if isinstance(value, (list, tuple, set)):
self._selections: list[str] | CaseInsensitiveDict = list(value)
elif isinstance(value, dict): elif isinstance(value, dict):
cid = CaseInsensitiveDict() cid = CaseInsensitiveDict()
cid.update(value) cid.update(value)
@ -123,7 +125,7 @@ class SelectionAction(BaseAction):
table, table,
default_selection=effective_default, default_selection=effective_default,
console=self.console, console=self.console,
session=self.session, prompt_session=self.prompt_session,
prompt_message=self.prompt_message, prompt_message=self.prompt_message,
show_table=self.show_table, show_table=self.show_table,
) )
@ -140,7 +142,7 @@ class SelectionAction(BaseAction):
table, table,
default_selection=effective_default, default_selection=effective_default,
console=self.console, console=self.console,
session=self.session, prompt_session=self.prompt_session,
prompt_message=self.prompt_message, prompt_message=self.prompt_message,
show_table=self.show_table, show_table=self.show_table,
) )

View File

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

View File

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