feat(help): enhance CLI help rendering with Panels and TLDR validation
- Updated `Command.help_signature` to return a `(Panel, description)` tuple, enabling richer Rich-based help output with formatted panels. - Integrated `program` context into commands to display accurate CLI invocation (`falyx run …`) depending on mode (RUN, PREVIEW, RUN_ALL). - Refactored `_show_help` to print `Panel`-styled usage and descriptions instead of table rows. - Added `program` and `options_manager` propagation to built-in commands (Exit, History, Help) for consistent CLI display. - Improved `CommandArgumentParser.add_tldr_examples()` with stricter validation (`all()` instead of `any()`), and added new TLDR tests for coverage. - Simplified parser epilog text to `Tip: Use 'falyx run ?' to show available commands.` - Added tests for required `Argument` fields and TLDR examples. - Bumped version to 0.1.73.
This commit is contained in:
@ -22,6 +22,8 @@ from typing import Any, Awaitable, Callable
|
|||||||
|
|
||||||
from prompt_toolkit.formatted_text import FormattedText
|
from prompt_toolkit.formatted_text import FormattedText
|
||||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
||||||
|
from rich.padding import Padding
|
||||||
|
from rich.panel import Panel
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from falyx.action.action import Action
|
from falyx.action.action import Action
|
||||||
@ -32,6 +34,7 @@ from falyx.debug import register_debug_hooks
|
|||||||
from falyx.execution_registry import ExecutionRegistry as er
|
from falyx.execution_registry import ExecutionRegistry as er
|
||||||
from falyx.hook_manager import HookManager, HookType
|
from falyx.hook_manager import HookManager, HookType
|
||||||
from falyx.logger import logger
|
from falyx.logger import logger
|
||||||
|
from falyx.mode import FalyxMode
|
||||||
from falyx.options_manager import OptionsManager
|
from falyx.options_manager import OptionsManager
|
||||||
from falyx.parser.command_argument_parser import CommandArgumentParser
|
from falyx.parser.command_argument_parser import CommandArgumentParser
|
||||||
from falyx.parser.signature import infer_args_from_func
|
from falyx.parser.signature import infer_args_from_func
|
||||||
@ -348,20 +351,37 @@ class Command(BaseModel):
|
|||||||
return f" {command_keys_text:<20} {options_text} "
|
return f" {command_keys_text:<20} {options_text} "
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def help_signature(self) -> str:
|
def help_signature(self) -> tuple[Panel, str]:
|
||||||
"""Generate a help signature for the command."""
|
"""Generate a help signature for the command."""
|
||||||
|
is_cli_mode = self.options_manager.get("mode") in {
|
||||||
|
FalyxMode.RUN,
|
||||||
|
FalyxMode.PREVIEW,
|
||||||
|
FalyxMode.RUN_ALL,
|
||||||
|
}
|
||||||
|
|
||||||
|
program = f"{self.program} run " if is_cli_mode else ""
|
||||||
|
|
||||||
if self.arg_parser and not self.simple_help_signature:
|
if self.arg_parser and not self.simple_help_signature:
|
||||||
signature = [self.arg_parser.get_usage()]
|
usage = Panel(
|
||||||
signature.append(f" {self.help_text or self.description}")
|
f"[{self.style}]{program}[/]{self.arg_parser.get_usage()}",
|
||||||
|
expand=False,
|
||||||
|
)
|
||||||
|
description = [f" {self.help_text or self.description}"]
|
||||||
if self.tags:
|
if self.tags:
|
||||||
signature.append(f" [dim]Tags: {', '.join(self.tags)}[/dim]")
|
description.append(f" [dim]Tags: {', '.join(self.tags)}[/dim]")
|
||||||
return "\n".join(signature).strip()
|
return usage, "\n".join(description)
|
||||||
|
|
||||||
command_keys = " | ".join(
|
command_keys = " | ".join(
|
||||||
[f"[{self.style}]{self.key}[/{self.style}]"]
|
[f"[{self.style}]{self.key}[/{self.style}]"]
|
||||||
+ [f"[{self.style}]{alias}[/{self.style}]" for alias in self.aliases]
|
+ [f"[{self.style}]{alias}[/{self.style}]" for alias in self.aliases]
|
||||||
)
|
)
|
||||||
return f"{command_keys} {self.description}"
|
return (
|
||||||
|
Panel(
|
||||||
|
f"[{self.style}]{program}[/]{command_keys} {self.description}",
|
||||||
|
expand=False,
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
def log_summary(self) -> None:
|
def log_summary(self) -> None:
|
||||||
if self._context:
|
if self._context:
|
||||||
|
@ -125,6 +125,7 @@ class Falyx:
|
|||||||
title: str | Markdown = "Menu",
|
title: str | Markdown = "Menu",
|
||||||
*,
|
*,
|
||||||
program: str | None = "falyx",
|
program: str | None = "falyx",
|
||||||
|
program_style: str = OneColors.WHITE,
|
||||||
usage: str | None = None,
|
usage: str | None = None,
|
||||||
description: str | None = "Falyx CLI - Run structured async command workflows.",
|
description: str | None = "Falyx CLI - Run structured async command workflows.",
|
||||||
epilog: str | None = None,
|
epilog: str | None = None,
|
||||||
@ -158,13 +159,6 @@ class Falyx:
|
|||||||
self.prompt: str | StyleAndTextTuples = rich_text_to_prompt_text(prompt)
|
self.prompt: str | StyleAndTextTuples = rich_text_to_prompt_text(prompt)
|
||||||
self.columns: int = columns
|
self.columns: int = columns
|
||||||
self.commands: dict[str, Command] = CaseInsensitiveDict()
|
self.commands: dict[str, Command] = CaseInsensitiveDict()
|
||||||
self.exit_command: Command = self._get_exit_command()
|
|
||||||
self.history_command: Command | None = (
|
|
||||||
self._get_history_command() if include_history_command else None
|
|
||||||
)
|
|
||||||
self.help_command: Command | None = (
|
|
||||||
self._get_help_command() if include_help_command else None
|
|
||||||
)
|
|
||||||
self.console: Console = console
|
self.console: Console = console
|
||||||
self.welcome_message: str | Markdown | dict[str, Any] = welcome_message
|
self.welcome_message: str | Markdown | dict[str, Any] = welcome_message
|
||||||
self.exit_message: str | Markdown | dict[str, Any] = exit_message
|
self.exit_message: str | Markdown | dict[str, Any] = exit_message
|
||||||
@ -182,6 +176,13 @@ class Falyx:
|
|||||||
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.options.set("mode", FalyxMode.MENU)
|
self.options.set("mode", FalyxMode.MENU)
|
||||||
|
self.exit_command: Command = self._get_exit_command()
|
||||||
|
self.history_command: Command | None = (
|
||||||
|
self._get_history_command() if include_history_command else None
|
||||||
|
)
|
||||||
|
self.help_command: Command | None = (
|
||||||
|
self._get_help_command() if include_help_command else None
|
||||||
|
)
|
||||||
|
|
||||||
def validate_options(
|
def validate_options(
|
||||||
self,
|
self,
|
||||||
@ -262,6 +263,8 @@ class Falyx:
|
|||||||
style=OneColors.DARK_RED,
|
style=OneColors.DARK_RED,
|
||||||
simple_help_signature=True,
|
simple_help_signature=True,
|
||||||
ignore_in_history=True,
|
ignore_in_history=True,
|
||||||
|
options_manager=self.options,
|
||||||
|
program=self.program,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_history_command(self) -> Command:
|
def _get_history_command(self) -> Command:
|
||||||
@ -271,6 +274,7 @@ class Falyx:
|
|||||||
command_description="History",
|
command_description="History",
|
||||||
command_style=OneColors.DARK_YELLOW,
|
command_style=OneColors.DARK_YELLOW,
|
||||||
aliases=["HISTORY"],
|
aliases=["HISTORY"],
|
||||||
|
program=self.program,
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-n",
|
"-n",
|
||||||
@ -314,17 +318,19 @@ class Falyx:
|
|||||||
arg_parser=parser,
|
arg_parser=parser,
|
||||||
help_text="View the execution history of commands.",
|
help_text="View the execution history of commands.",
|
||||||
ignore_in_history=True,
|
ignore_in_history=True,
|
||||||
|
options_manager=self.options,
|
||||||
|
program=self.program,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _show_help(self, tag: str = "") -> None:
|
async def _show_help(self, tag: str = "") -> None:
|
||||||
|
is_cli_mode = self.options.get("mode") in {
|
||||||
|
FalyxMode.RUN,
|
||||||
|
FalyxMode.PREVIEW,
|
||||||
|
FalyxMode.RUN_ALL,
|
||||||
|
}
|
||||||
|
|
||||||
|
program = f"{self.program} run " if is_cli_mode else ""
|
||||||
if tag:
|
if tag:
|
||||||
table = Table(
|
|
||||||
title=tag.upper(),
|
|
||||||
title_justify="left",
|
|
||||||
show_header=False,
|
|
||||||
box=box.SIMPLE,
|
|
||||||
show_footer=False,
|
|
||||||
)
|
|
||||||
tag_lower = tag.lower()
|
tag_lower = tag.lower()
|
||||||
commands = [
|
commands = [
|
||||||
command
|
command
|
||||||
@ -332,27 +338,31 @@ class Falyx:
|
|||||||
if any(tag_lower == tag.lower() for tag in command.tags)
|
if any(tag_lower == tag.lower() for tag in command.tags)
|
||||||
]
|
]
|
||||||
for command in commands:
|
for command in commands:
|
||||||
table.add_row(command.help_signature)
|
usage, description = command.help_signature
|
||||||
self.console.print(table)
|
self.console.print(usage)
|
||||||
|
if description:
|
||||||
|
self.console.print(description)
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
table = Table(
|
for command in self.commands.values():
|
||||||
title="Help",
|
usage, description = command.help_signature
|
||||||
title_justify="left",
|
self.console.print(usage)
|
||||||
title_style=OneColors.LIGHT_YELLOW_b,
|
if description:
|
||||||
show_header=False,
|
self.console.print(description)
|
||||||
show_footer=False,
|
|
||||||
box=box.SIMPLE,
|
|
||||||
)
|
|
||||||
for command in self.commands.values():
|
|
||||||
table.add_row(command.help_signature)
|
|
||||||
if self.help_command:
|
if self.help_command:
|
||||||
table.add_row(self.help_command.help_signature)
|
usage, description = self.help_command.help_signature
|
||||||
if self.history_command:
|
self.console.print(usage)
|
||||||
table.add_row(self.history_command.help_signature)
|
self.console.print(description)
|
||||||
table.add_row(self.exit_command.help_signature)
|
if not is_cli_mode:
|
||||||
table.add_row(f"Tip: '[{OneColors.LIGHT_YELLOW}]?[KEY][/]' to preview a command ")
|
if self.history_command:
|
||||||
self.console.print(table)
|
usage, description = self.history_command.help_signature
|
||||||
|
self.console.print(usage)
|
||||||
|
self.console.print(description)
|
||||||
|
usage, _ = self.exit_command.help_signature
|
||||||
|
self.console.print(usage)
|
||||||
|
self.console.print(
|
||||||
|
f"Tip: '{program}[{OneColors.LIGHT_YELLOW}]?[KEY][/]' to preview a command "
|
||||||
|
)
|
||||||
|
|
||||||
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."""
|
||||||
@ -361,6 +371,7 @@ class Falyx:
|
|||||||
command_description="Help",
|
command_description="Help",
|
||||||
command_style=OneColors.LIGHT_YELLOW,
|
command_style=OneColors.LIGHT_YELLOW,
|
||||||
aliases=["?", "HELP", "LIST"],
|
aliases=["?", "HELP", "LIST"],
|
||||||
|
program=self.program,
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-t",
|
"-t",
|
||||||
@ -378,6 +389,8 @@ class Falyx:
|
|||||||
style=OneColors.LIGHT_YELLOW,
|
style=OneColors.LIGHT_YELLOW,
|
||||||
arg_parser=parser,
|
arg_parser=parser,
|
||||||
ignore_in_history=True,
|
ignore_in_history=True,
|
||||||
|
options_manager=self.options,
|
||||||
|
program=self.program,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_completer(self) -> FalyxCompleter:
|
def _get_completer(self) -> FalyxCompleter:
|
||||||
@ -549,6 +562,8 @@ class Falyx:
|
|||||||
confirm=confirm,
|
confirm=confirm,
|
||||||
confirm_message=confirm_message,
|
confirm_message=confirm_message,
|
||||||
ignore_in_history=True,
|
ignore_in_history=True,
|
||||||
|
options_manager=self.options,
|
||||||
|
program=self.program,
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_submenu(
|
def add_submenu(
|
||||||
@ -729,7 +744,7 @@ class Falyx:
|
|||||||
|
|
||||||
def build_placeholder_menu(self) -> StyleAndTextTuples:
|
def build_placeholder_menu(self) -> StyleAndTextTuples:
|
||||||
"""
|
"""
|
||||||
Builds a menu placeholder for prompt_menu mode.
|
Builds a menu placeholder for show_placeholder_menu.
|
||||||
"""
|
"""
|
||||||
visible_commands = [item for item in self.commands.items() if not item[1].hidden]
|
visible_commands = [item for item in self.commands.items() if not item[1].hidden]
|
||||||
if not visible_commands:
|
if not visible_commands:
|
||||||
|
@ -142,12 +142,13 @@ class CommandArgumentParser:
|
|||||||
Args:
|
Args:
|
||||||
examples (list[tuple[str, str]]): List of (usage, description) tuples.
|
examples (list[tuple[str, str]]): List of (usage, description) tuples.
|
||||||
"""
|
"""
|
||||||
if not any(
|
if not all(
|
||||||
isinstance(example, tuple) and len(example) == 2 for example in examples
|
isinstance(example, tuple) and len(example) == 2 for example in examples
|
||||||
):
|
):
|
||||||
raise CommandArgumentError(
|
raise CommandArgumentError(
|
||||||
"TLDR examples must be a list of (usage, description) tuples"
|
"TLDR examples must be a list of (usage, description) tuples"
|
||||||
)
|
)
|
||||||
|
|
||||||
for usage, description in examples:
|
for usage, description in examples:
|
||||||
self._tldr_examples.append(TLDRExample(usage=usage, description=description))
|
self._tldr_examples.append(TLDRExample(usage=usage, description=description))
|
||||||
|
|
||||||
@ -1362,7 +1363,7 @@ class CommandArgumentParser:
|
|||||||
command = self.aliases[0] if self.aliases else self.command_key
|
command = self.aliases[0] if self.aliases else self.command_key
|
||||||
if is_cli_mode:
|
if is_cli_mode:
|
||||||
command = (
|
command = (
|
||||||
f"{program} run [{self.command_style}]{command}[/{self.command_style}]"
|
f"[{self.command_style}]{program} run {command}[/{self.command_style}]"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
command = f"[{self.command_style}]{command}[/{self.command_style}]"
|
command = f"[{self.command_style}]{command}[/{self.command_style}]"
|
||||||
|
@ -59,9 +59,7 @@ def get_root_parser(
|
|||||||
prog: str | None = "falyx",
|
prog: str | None = "falyx",
|
||||||
usage: str | None = None,
|
usage: str | None = None,
|
||||||
description: str | None = "Falyx CLI - Run structured async command workflows.",
|
description: str | None = "Falyx CLI - Run structured async command workflows.",
|
||||||
epilog: (
|
epilog: str | None = "Tip: Use 'falyx run ?' to show available commands.",
|
||||||
str | None
|
|
||||||
) = "Tip: Use 'falyx run ?[COMMAND]' to preview any command from the CLI.",
|
|
||||||
parents: Sequence[ArgumentParser] | None = None,
|
parents: Sequence[ArgumentParser] | None = None,
|
||||||
prefix_chars: str = "-",
|
prefix_chars: str = "-",
|
||||||
fromfile_prefix_chars: str | None = None,
|
fromfile_prefix_chars: str | None = None,
|
||||||
@ -242,7 +240,7 @@ def get_arg_parsers(
|
|||||||
- Use `falyx run ?[COMMAND]` from the CLI to preview a command.
|
- Use `falyx run ?[COMMAND]` from the CLI to preview a command.
|
||||||
"""
|
"""
|
||||||
if epilog is None:
|
if epilog is None:
|
||||||
epilog = f"Tip: Use '{prog} run ?[COMMAND]' to preview any command from the CLI."
|
epilog = f"Tip: Use '{prog} run ?' to show available commands."
|
||||||
if root_parser is None:
|
if root_parser is None:
|
||||||
parser = get_root_parser(
|
parser = get_root_parser(
|
||||||
prog=prog,
|
prog=prog,
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = "0.1.72"
|
__version__ = "0.1.73"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "falyx"
|
name = "falyx"
|
||||||
version = "0.1.72"
|
version = "0.1.73"
|
||||||
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"
|
||||||
|
@ -88,3 +88,11 @@ def test_argument_equality():
|
|||||||
assert arg != "not an argument"
|
assert arg != "not an argument"
|
||||||
assert arg is not None
|
assert arg is not None
|
||||||
assert arg != object()
|
assert arg != object()
|
||||||
|
|
||||||
|
|
||||||
|
def test_argument_required():
|
||||||
|
arg = Argument("--foo", dest="foo", required=True)
|
||||||
|
assert arg.required is True
|
||||||
|
|
||||||
|
arg2 = Argument("--bar", dest="bar", required=False)
|
||||||
|
assert arg2.required is False
|
||||||
|
47
tests/test_parsers/test_tldr.py
Normal file
47
tests/test_parsers/test_tldr.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from falyx.exceptions import CommandArgumentError
|
||||||
|
from falyx.parser.command_argument_parser import CommandArgumentParser
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_tldr_examples():
|
||||||
|
parser = CommandArgumentParser()
|
||||||
|
parser.add_tldr_examples(
|
||||||
|
[
|
||||||
|
("example1", "This is the first example."),
|
||||||
|
("example2", "This is the second example."),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert len(parser._tldr_examples) == 2
|
||||||
|
assert parser._tldr_examples[0].usage == "example1"
|
||||||
|
assert parser._tldr_examples[0].description == "This is the first example."
|
||||||
|
assert parser._tldr_examples[1].usage == "example2"
|
||||||
|
assert parser._tldr_examples[1].description == "This is the second example."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bad_tldr_examples():
|
||||||
|
parser = CommandArgumentParser()
|
||||||
|
with pytest.raises(CommandArgumentError):
|
||||||
|
parser.add_tldr_examples(
|
||||||
|
[
|
||||||
|
("example1", "This is the first example.", "extra_arg"),
|
||||||
|
("example2", "This is the second example."),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_tldr_examples_in_init():
|
||||||
|
parser = CommandArgumentParser(
|
||||||
|
tldr_examples=[
|
||||||
|
("example1", "This is the first example."),
|
||||||
|
("example2", "This is the second example."),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert len(parser._tldr_examples) == 2
|
||||||
|
assert parser._tldr_examples[0].usage == "example1"
|
||||||
|
assert parser._tldr_examples[0].description == "This is the first example."
|
||||||
|
assert parser._tldr_examples[1].usage == "example2"
|
||||||
|
assert parser._tldr_examples[1].description == "This is the second example."
|
Reference in New Issue
Block a user