diff --git a/tests/assets/cli/multi_app.py b/tests/assets/cli/multi_app.py index 1442aa2442..2c3e33ae26 100644 --- a/tests/assets/cli/multi_app.py +++ b/tests/assets/cli/multi_app.py @@ -13,14 +13,14 @@ def hello(name: str = "World", age: int = typer.Option(0, help="The age of the u typer.echo(f"Hello {name}") -@sub_app.command() +@sub_app.command(rich_help_panel="Greet") def hi(user: str = typer.Argument("World", help="The name of the user to greet")): """ Say Hi """ -@sub_app.command() +@sub_app.command(rich_help_panel="Farewell") def bye(): """ Say bye @@ -32,7 +32,7 @@ def bye(): app.add_typer(sub_app, name="sub") -@app.command() +@app.command(rich_help_panel="") def top(): """ Top command diff --git a/tests/assets/cli/multiapp-docs-title.md b/tests/assets/cli/multiapp-docs-title.md index ffde843736..8c2f941c79 100644 --- a/tests/assets/cli/multiapp-docs-title.md +++ b/tests/assets/cli/multiapp-docs-title.md @@ -50,7 +50,13 @@ $ multiapp sub [OPTIONS] COMMAND [ARGS]... **Commands**: * `hello`: Say Hello + +**Greet**: + * `hi`: Say Hi + +**Farewell**: + * `bye`: Say bye ### `multiapp sub hello` diff --git a/tests/assets/cli/multiapp-docs.md b/tests/assets/cli/multiapp-docs.md index 67d02568db..d02c4d5187 100644 --- a/tests/assets/cli/multiapp-docs.md +++ b/tests/assets/cli/multiapp-docs.md @@ -50,7 +50,13 @@ $ multiapp sub [OPTIONS] COMMAND [ARGS]... **Commands**: * `hello`: Say Hello + +**Greet**: + * `hi`: Say Hi + +**Farewell**: + * `bye`: Say bye ### `multiapp sub hello` diff --git a/typer/cli.py b/typer/cli.py index 16b37c515d..346ef86576 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -1,8 +1,10 @@ import importlib.util import re import sys +from collections import defaultdict +from itertools import chain from pathlib import Path -from typing import Any, List, Optional +from typing import Any, Dict, List, Optional import click import typer @@ -240,26 +242,50 @@ def get_docs_for_click( if isinstance(obj, Group): group = obj commands = group.list_commands(ctx) + default_panel_name = "Commands" + if HAS_RICH: + from . import rich_utils + + default_panel_name = rich_utils.COMMANDS_PANEL_TITLE if commands: - docs += "**Commands**:\n\n" + panel_to_commands: Dict[str, List[click.Command]] = defaultdict(list) for command in commands: command_obj = group.get_command(ctx, command) assert command_obj - docs += f"* `{command_obj.name}`" - command_help = command_obj.get_short_help_str() - if command_help: - docs += f": {_parse_html(command_help)}" + panel_name = default_panel_name + if HAS_RICH: + from . import rich_utils + + panel_name = rich_utils.get_panel_name( + command_obj, default_panel_name + ) + panel_to_commands[panel_name].append(command_obj) + if HAS_RICH: + # Ensure that the ungrouped commands show up first + default_command_objs = panel_to_commands.pop(default_panel_name, []) + if len(default_command_objs) > 0: + panel_to_commands = { + default_panel_name: default_command_objs, + **panel_to_commands, + } + for panel_name, command_objs in panel_to_commands.items(): + docs += f"**{panel_name}**:\n\n" + for command_obj in command_objs: + docs += f"* `{command_obj.name}`" + command_help = command_obj.get_short_help_str() + if command_help: + docs += f": {_parse_html(command_help)}" + docs += "\n" docs += "\n" - docs += "\n" - for command in commands: - command_obj = group.get_command(ctx, command) - assert command_obj - use_prefix = "" - if command_name: - use_prefix += f"{command_name}" - docs += get_docs_for_click( - obj=command_obj, ctx=ctx, indent=indent + 1, call_prefix=use_prefix - ) + for command_obj in chain.from_iterable( + command_objs for command_objs in panel_to_commands.values() + ): + use_prefix = "" + if command_name: + use_prefix += f"{command_name}" + docs += get_docs_for_click( + obj=command_obj, ctx=ctx, indent=indent + 1, call_prefix=use_prefix + ) return docs diff --git a/typer/rich_utils.py b/typer/rich_utils.py index d4c3676aea..bace5be83a 100644 --- a/typer/rich_utils.py +++ b/typer/rich_utils.py @@ -592,14 +592,10 @@ def rich_format_help( if getattr(param, "hidden", False): continue if isinstance(param, click.Argument): - panel_name = ( - getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE - ) + panel_name = get_panel_name(param, ARGUMENTS_PANEL_TITLE) panel_to_arguments[panel_name].append(param) elif isinstance(param, click.Option): - panel_name = ( - getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE - ) + panel_name = get_panel_name(param, OPTIONS_PANEL_TITLE) panel_to_options[panel_name].append(param) default_arguments = panel_to_arguments.get(ARGUMENTS_PANEL_TITLE, []) _print_options_panel( @@ -645,10 +641,7 @@ def rich_format_help( for command_name in obj.list_commands(ctx): command = obj.get_command(ctx, command_name) if command and not command.hidden: - panel_name = ( - getattr(command, _RICH_HELP_PANEL_NAME, None) - or COMMANDS_PANEL_TITLE - ) + panel_name = get_panel_name(command, COMMANDS_PANEL_TITLE) panel_to_commands[panel_name].append(command) # Identify the longest command name in all panels @@ -768,3 +761,9 @@ def get_traceback( width=MAX_WIDTH, ) return rich_tb + + +def get_panel_name( + obj: Union[click.Command, click.Argument, click.Option], default_name: str +) -> str: + return getattr(obj, _RICH_HELP_PANEL_NAME, None) or default_name