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 pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator | ||||
| from rich.padding import Padding | ||||
| from rich.panel import Panel | ||||
| from rich.tree import Tree | ||||
|  | ||||
| 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.hook_manager import HookManager, HookType | ||||
| from falyx.logger import logger | ||||
| from falyx.mode import FalyxMode | ||||
| from falyx.options_manager import OptionsManager | ||||
| from falyx.parser.command_argument_parser import CommandArgumentParser | ||||
| from falyx.parser.signature import infer_args_from_func | ||||
| @@ -348,20 +351,37 @@ class Command(BaseModel): | ||||
|         return f"  {command_keys_text:<20}  {options_text} " | ||||
|  | ||||
|     @property | ||||
|     def help_signature(self) -> str: | ||||
|     def help_signature(self) -> tuple[Panel, str]: | ||||
|         """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: | ||||
|             signature = [self.arg_parser.get_usage()] | ||||
|             signature.append(f"  {self.help_text or self.description}") | ||||
|             usage = Panel( | ||||
|                 f"[{self.style}]{program}[/]{self.arg_parser.get_usage()}", | ||||
|                 expand=False, | ||||
|             ) | ||||
|             description = [f"  {self.help_text or self.description}"] | ||||
|             if self.tags: | ||||
|                 signature.append(f"  [dim]Tags: {', '.join(self.tags)}[/dim]") | ||||
|             return "\n".join(signature).strip() | ||||
|                 description.append(f"  [dim]Tags: {', '.join(self.tags)}[/dim]") | ||||
|             return usage, "\n".join(description) | ||||
|  | ||||
|         command_keys = " | ".join( | ||||
|             [f"[{self.style}]{self.key}[/{self.style}]"] | ||||
|             + [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: | ||||
|         if self._context: | ||||
|   | ||||
| @@ -125,6 +125,7 @@ class Falyx: | ||||
|         title: str | Markdown = "Menu", | ||||
|         *, | ||||
|         program: str | None = "falyx", | ||||
|         program_style: str = OneColors.WHITE, | ||||
|         usage: str | None = None, | ||||
|         description: str | None = "Falyx CLI - Run structured async command workflows.", | ||||
|         epilog: str | None = None, | ||||
| @@ -158,13 +159,6 @@ class Falyx: | ||||
|         self.prompt: str | StyleAndTextTuples = rich_text_to_prompt_text(prompt) | ||||
|         self.columns: int = columns | ||||
|         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.welcome_message: str | Markdown | dict[str, Any] = welcome_message | ||||
|         self.exit_message: str | Markdown | dict[str, Any] = exit_message | ||||
| @@ -182,6 +176,13 @@ class Falyx: | ||||
|         self.validate_options(cli_args, options) | ||||
|         self._prompt_session: PromptSession | None = None | ||||
|         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( | ||||
|         self, | ||||
| @@ -262,6 +263,8 @@ class Falyx: | ||||
|             style=OneColors.DARK_RED, | ||||
|             simple_help_signature=True, | ||||
|             ignore_in_history=True, | ||||
|             options_manager=self.options, | ||||
|             program=self.program, | ||||
|         ) | ||||
|  | ||||
|     def _get_history_command(self) -> Command: | ||||
| @@ -271,6 +274,7 @@ class Falyx: | ||||
|             command_description="History", | ||||
|             command_style=OneColors.DARK_YELLOW, | ||||
|             aliases=["HISTORY"], | ||||
|             program=self.program, | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             "-n", | ||||
| @@ -314,17 +318,19 @@ class Falyx: | ||||
|             arg_parser=parser, | ||||
|             help_text="View the execution history of commands.", | ||||
|             ignore_in_history=True, | ||||
|             options_manager=self.options, | ||||
|             program=self.program, | ||||
|         ) | ||||
|  | ||||
|     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: | ||||
|             table = Table( | ||||
|                 title=tag.upper(), | ||||
|                 title_justify="left", | ||||
|                 show_header=False, | ||||
|                 box=box.SIMPLE, | ||||
|                 show_footer=False, | ||||
|             ) | ||||
|             tag_lower = tag.lower() | ||||
|             commands = [ | ||||
|                 command | ||||
| @@ -332,27 +338,31 @@ class Falyx: | ||||
|                 if any(tag_lower == tag.lower() for tag in command.tags) | ||||
|             ] | ||||
|             for command in commands: | ||||
|                 table.add_row(command.help_signature) | ||||
|             self.console.print(table) | ||||
|                 usage, description = command.help_signature | ||||
|                 self.console.print(usage) | ||||
|                 if description: | ||||
|                     self.console.print(description) | ||||
|             return | ||||
|         else: | ||||
|             table = Table( | ||||
|                 title="Help", | ||||
|                 title_justify="left", | ||||
|                 title_style=OneColors.LIGHT_YELLOW_b, | ||||
|                 show_header=False, | ||||
|                 show_footer=False, | ||||
|                 box=box.SIMPLE, | ||||
|             ) | ||||
|             for command in self.commands.values(): | ||||
|                 table.add_row(command.help_signature) | ||||
|  | ||||
|         for command in self.commands.values(): | ||||
|             usage, description = command.help_signature | ||||
|             self.console.print(usage) | ||||
|             if description: | ||||
|                 self.console.print(description) | ||||
|         if self.help_command: | ||||
|             table.add_row(self.help_command.help_signature) | ||||
|         if self.history_command: | ||||
|             table.add_row(self.history_command.help_signature) | ||||
|         table.add_row(self.exit_command.help_signature) | ||||
|         table.add_row(f"Tip: '[{OneColors.LIGHT_YELLOW}]?[KEY][/]' to preview a command ") | ||||
|         self.console.print(table) | ||||
|             usage, description = self.help_command.help_signature | ||||
|             self.console.print(usage) | ||||
|             self.console.print(description) | ||||
|         if not is_cli_mode: | ||||
|             if self.history_command: | ||||
|                 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: | ||||
|         """Returns the help command for the menu.""" | ||||
| @@ -361,6 +371,7 @@ class Falyx: | ||||
|             command_description="Help", | ||||
|             command_style=OneColors.LIGHT_YELLOW, | ||||
|             aliases=["?", "HELP", "LIST"], | ||||
|             program=self.program, | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             "-t", | ||||
| @@ -378,6 +389,8 @@ class Falyx: | ||||
|             style=OneColors.LIGHT_YELLOW, | ||||
|             arg_parser=parser, | ||||
|             ignore_in_history=True, | ||||
|             options_manager=self.options, | ||||
|             program=self.program, | ||||
|         ) | ||||
|  | ||||
|     def _get_completer(self) -> FalyxCompleter: | ||||
| @@ -549,6 +562,8 @@ class Falyx: | ||||
|             confirm=confirm, | ||||
|             confirm_message=confirm_message, | ||||
|             ignore_in_history=True, | ||||
|             options_manager=self.options, | ||||
|             program=self.program, | ||||
|         ) | ||||
|  | ||||
|     def add_submenu( | ||||
| @@ -729,7 +744,7 @@ class Falyx: | ||||
|  | ||||
|     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] | ||||
|         if not visible_commands: | ||||
|   | ||||
| @@ -142,12 +142,13 @@ class CommandArgumentParser: | ||||
|         Args: | ||||
|             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 | ||||
|         ): | ||||
|             raise CommandArgumentError( | ||||
|                 "TLDR examples must be a list of (usage, description) tuples" | ||||
|             ) | ||||
|  | ||||
|         for usage, description in examples: | ||||
|             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 | ||||
|         if is_cli_mode: | ||||
|             command = ( | ||||
|                 f"{program} run [{self.command_style}]{command}[/{self.command_style}]" | ||||
|                 f"[{self.command_style}]{program} run {command}[/{self.command_style}]" | ||||
|             ) | ||||
|         else: | ||||
|             command = f"[{self.command_style}]{command}[/{self.command_style}]" | ||||
|   | ||||
| @@ -59,9 +59,7 @@ def get_root_parser( | ||||
|     prog: str | None = "falyx", | ||||
|     usage: str | None = None, | ||||
|     description: str | None = "Falyx CLI - Run structured async command workflows.", | ||||
|     epilog: ( | ||||
|         str | None | ||||
|     ) = "Tip: Use 'falyx run ?[COMMAND]' to preview any command from the CLI.", | ||||
|     epilog: str | None = "Tip: Use 'falyx run ?' to show available commands.", | ||||
|     parents: Sequence[ArgumentParser] | None = None, | ||||
|     prefix_chars: str = "-", | ||||
|     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. | ||||
|     """ | ||||
|     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: | ||||
|         parser = get_root_parser( | ||||
|             prog=prog, | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| __version__ = "0.1.72" | ||||
| __version__ = "0.1.73" | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| [tool.poetry] | ||||
| name = "falyx" | ||||
| version = "0.1.72" | ||||
| version = "0.1.73" | ||||
| description = "Reliable and introspectable async CLI action framework." | ||||
| authors = ["Roland Thomas Jr <roland@rtj.dev>"] | ||||
| license = "MIT" | ||||
|   | ||||
| @@ -88,3 +88,11 @@ def test_argument_equality(): | ||||
|     assert arg != "not an argument" | ||||
|     assert arg is not None | ||||
|     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