diff --git a/docs/tutorial/commands/help.md b/docs/tutorial/commands/help.md index ae330341da..05c3340e9d 100644 --- a/docs/tutorial/commands/help.md +++ b/docs/tutorial/commands/help.md @@ -503,3 +503,42 @@ $ python main.py --help ``` + +## Expand or Fit + +By default, the help panels all expand to match the width of your terminal window. + +Sometimes, you might prefer that all panels fit their contents instead. This means that they will probably have different widths. + +You can do this by initializing your Typer with `rich_expand=False`, like this: + +{* docs_src/commands/help/tutorial009_py39.py hl[5] *} + +When you now check the `--help` option, it will look like: + +
+ +```console +$ python main.py create --help + + Usage: main.py create [OPTIONS] USERNAME [LASTNAME] + + Create a new user. ✨ + +╭─ Arguments ──────────────────────────────────────╮ +* username TEXT The username [required] │ +╰──────────────────────────────────────────────────╯ +╭─ Secondary Arguments ─────────────────────╮ +│ lastname [LASTNAME] The last name │ +╰───────────────────────────────────────────╯ +╭─ Options ────────────────────────────────────────────────╮ +--force --no-force Force the creation [required] │ +--help Show this message and exit. │ +╰──────────────────────────────────────────────────────────╯ +╭─ Additional Data ───────────────────────────────────╮ +--age INTEGER The age │ +--favorite-color TEXT The favorite color │ +╰─────────────────────────────────────────────────────╯ +``` + +
diff --git a/docs_src/commands/help/tutorial009_py39.py b/docs_src/commands/help/tutorial009_py39.py new file mode 100644 index 0000000000..373e14c2e3 --- /dev/null +++ b/docs_src/commands/help/tutorial009_py39.py @@ -0,0 +1,39 @@ +from typing import Union + +import typer + +app = typer.Typer(rich_markup_mode="rich", rich_expand=False) + + +@app.command() +def create( + username: str = typer.Argument(..., help="The username"), + lastname: str = typer.Argument( + "", help="The last name", rich_help_panel="Secondary Arguments" + ), + force: bool = typer.Option(..., help="Force the creation"), + age: Union[int, None] = typer.Option( + None, help="The age", rich_help_panel="Additional Data" + ), + favorite_color: Union[str, None] = typer.Option( + None, + help="The favorite color", + rich_help_panel="Additional Data", + ), +): + """ + [green]Create[/green] a new user. :sparkles: + """ + print(f"Creating user: {username}") + + +@app.command(rich_help_panel="Utils and Configs") +def config(configuration: str): + """ + [blue]Configure[/blue] the system. :gear: + """ + print(f"Configuring the system with: {configuration}") + + +if __name__ == "__main__": + app() diff --git a/tests/test_tutorial/test_commands/test_help/test_tutorial009.py b/tests/test_tutorial/test_commands/test_help/test_tutorial009.py new file mode 100644 index 0000000000..98ffbeb064 --- /dev/null +++ b/tests/test_tutorial/test_commands/test_help/test_tutorial009.py @@ -0,0 +1,39 @@ +import os +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.commands.help import tutorial009_py39 as mod + +app = mod.app + +runner = CliRunner() + + +def test_main_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "create" in result.output + assert "Create a new user. ✨" in result.output + assert "Utils and Configs" in result.output + assert "config" in result.output + assert "Configure the system. ⚙" in result.output + + +def test_call(): + # Mainly for coverage + result = runner.invoke(app, ["create", "Morty", "--force"]) + assert result.exit_code == 0 + result = runner.invoke(app, ["config", "Morty"]) + assert result.exit_code == 0 + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + env={**os.environ, "PYTHONIOENCODING": "utf-8"}, + ) + assert "Usage" in result.stdout diff --git a/typer/core.py b/typer/core.py index d0d888ccf0..2fab8faf33 100644 --- a/typer/core.py +++ b/typer/core.py @@ -164,6 +164,7 @@ def _main( standalone_mode: bool = True, windows_expand_args: bool = True, rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE, + rich_expand: bool, **extra: Any, ) -> Any: # Typer override, duplicated from click.main() to handle custom rich exceptions @@ -210,7 +211,7 @@ def _main( if HAS_RICH and rich_markup_mode is not None: from . import rich_utils - rich_utils.rich_format_error(e) + rich_utils.rich_format_error(e, expand=rich_expand) else: e.show() # Typer override end @@ -681,6 +682,7 @@ def __init__( # Rich settings rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE, rich_help_panel: Union[str, None] = None, + rich_expand: bool = True, ) -> None: super().__init__( name=name, @@ -698,6 +700,7 @@ def __init__( ) self.rich_markup_mode: MarkupMode = rich_markup_mode self.rich_help_panel = rich_help_panel + self.rich_expand = rich_expand def format_options( self, ctx: click.Context, formatter: click.HelpFormatter @@ -731,6 +734,7 @@ def main( standalone_mode=standalone_mode, windows_expand_args=windows_expand_args, rich_markup_mode=self.rich_markup_mode, + rich_expand=self.rich_expand, **extra, ) @@ -747,6 +751,7 @@ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> Non obj=self, ctx=ctx, markup_mode=self.rich_markup_mode, + expand=self.rich_expand, ) @@ -761,12 +766,14 @@ def __init__( # Rich settings rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE, rich_help_panel: Union[str, None] = None, + rich_expand: bool = True, suggest_commands: bool = True, **attrs: Any, ) -> None: super().__init__(name=name, commands=commands, **attrs) self.rich_markup_mode: MarkupMode = rich_markup_mode self.rich_help_panel = rich_help_panel + self.rich_expand = rich_expand self.suggest_commands = suggest_commands def format_options( @@ -819,6 +826,7 @@ def main( standalone_mode=standalone_mode, windows_expand_args=windows_expand_args, rich_markup_mode=self.rich_markup_mode, + rich_expand=self.rich_expand, **extra, ) @@ -831,6 +839,7 @@ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> Non obj=self, ctx=ctx, markup_mode=self.rich_markup_mode, + expand=self.rich_expand, ) def list_commands(self, ctx: click.Context) -> list[str]: diff --git a/typer/main.py b/typer/main.py index e8c6b9e429..ec903d56e7 100644 --- a/typer/main.py +++ b/typer/main.py @@ -137,6 +137,7 @@ def __init__( add_completion: bool = True, # Rich settings rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE, + rich_expand: bool = True, rich_help_panel: Union[str, None] = Default(None), suggest_commands: bool = True, pretty_exceptions_enable: bool = True, @@ -145,6 +146,7 @@ def __init__( ): self._add_completion = add_completion self.rich_markup_mode: MarkupMode = rich_markup_mode + self.rich_expand = rich_expand self.rich_help_panel = rich_help_panel self.suggest_commands = suggest_commands self.pretty_exceptions_enable = pretty_exceptions_enable @@ -347,6 +349,7 @@ def get_group(typer_instance: Typer) -> TyperGroup: TyperInfo(typer_instance), pretty_exceptions_short=typer_instance.pretty_exceptions_short, rich_markup_mode=typer_instance.rich_markup_mode, + rich_expand=typer_instance.rich_expand, suggest_commands=typer_instance.suggest_commands, ) return group @@ -380,6 +383,7 @@ def get_command(typer_instance: Typer) -> click.Command: single_command, pretty_exceptions_short=typer_instance.pretty_exceptions_short, rich_markup_mode=typer_instance.rich_markup_mode, + rich_expand=typer_instance.rich_expand, ) if typer_instance._add_completion: click_command.params.append(click_install_param) @@ -476,6 +480,7 @@ def get_group_from_info( pretty_exceptions_short: bool, suggest_commands: bool, rich_markup_mode: MarkupMode, + rich_expand: bool, ) -> TyperGroup: assert group_info.typer_instance, ( "A Typer instance is needed to generate a Click Group" @@ -486,6 +491,7 @@ def get_group_from_info( command_info=command_info, pretty_exceptions_short=pretty_exceptions_short, rich_markup_mode=rich_markup_mode, + rich_expand=rich_expand, ) if command.name: commands[command.name] = command @@ -494,6 +500,7 @@ def get_group_from_info( sub_group_info, pretty_exceptions_short=pretty_exceptions_short, rich_markup_mode=rich_markup_mode, + rich_expand=rich_expand, suggest_commands=suggest_commands, ) if sub_group.name: @@ -541,6 +548,7 @@ def get_group_from_info( hidden=solved_info.hidden, deprecated=solved_info.deprecated, rich_markup_mode=rich_markup_mode, + rich_expand=rich_expand, # Rich settings rich_help_panel=solved_info.rich_help_panel, suggest_commands=suggest_commands, @@ -576,6 +584,7 @@ def get_command_from_info( *, pretty_exceptions_short: bool, rich_markup_mode: MarkupMode, + rich_expand: bool, ) -> click.Command: assert command_info.callback, "A command must have a callback function" name = command_info.name or get_command_name(command_info.callback.__name__) @@ -610,6 +619,7 @@ def get_command_from_info( hidden=command_info.hidden, deprecated=command_info.deprecated, rich_markup_mode=rich_markup_mode, + rich_expand=rich_expand, # Rich settings rich_help_panel=command_info.rich_help_panel, ) diff --git a/typer/rich_utils.py b/typer/rich_utils.py index ad110cb8d6..443f55689e 100644 --- a/typer/rich_utils.py +++ b/typer/rich_utils.py @@ -216,7 +216,8 @@ def _get_parameter_help( param: Union[click.Option, click.Argument, click.Parameter], ctx: click.Context, markup_mode: MarkupModeStrict, -) -> Columns: + rich_expand: bool, +) -> Union[Table, Columns]: """Build primary help text for a click option or argument. Returns the prose help text for an option or argument, rendered either @@ -295,9 +296,16 @@ def _get_parameter_help( if param.required: items.append(Text(REQUIRED_LONG_STRING, style=STYLE_REQUIRED_LONG)) - # Use Columns - this allows us to group different renderable types - # (Text, Markdown) onto a single line. - return Columns(items) + if rich_expand: + # Use Columns - this allows us to group different renderable types + # (Text, Markdown) onto a single line. + return Columns(items) + + # Use Table - this allows us to group different renderable types + # (Text, Markdown) onto a single line without using the full screen width. + help_table = Table.grid(padding=(0, 1), expand=False) + help_table.add_row(*items) + return help_table def _make_command_help( @@ -333,6 +341,7 @@ def _print_options_panel( params: Union[list[click.Option], list[click.Argument]], ctx: click.Context, markup_mode: MarkupModeStrict, + expand: bool, console: Console, ) -> None: options_rows: list[list[RenderableType]] = [] @@ -415,6 +424,7 @@ class MetavarHighlighter(RegexHighlighter): param=param, ctx=ctx, markup_mode=markup_mode, + rich_expand=expand, ), ] ) @@ -450,6 +460,7 @@ class MetavarHighlighter(RegexHighlighter): options_table, border_style=STYLE_OPTIONS_PANEL_BORDER, title=name, + expand=expand, title_align=ALIGN_OPTIONS_PANEL, ) ) @@ -460,6 +471,7 @@ def _print_commands_panel( name: str, commands: list[click.Command], markup_mode: MarkupModeStrict, + expand: bool, console: Console, cmd_len: int, ) -> None: @@ -526,6 +538,7 @@ def _print_commands_panel( commands_table, border_style=STYLE_COMMANDS_PANEL_BORDER, title=name, + expand=expand, title_align=ALIGN_COMMANDS_PANEL, ) ) @@ -536,6 +549,7 @@ def rich_format_help( obj: Union[click.Command, click.Group], ctx: click.Context, markup_mode: MarkupModeStrict, + expand: bool, ) -> None: """Print nicely formatted help text using rich. @@ -589,6 +603,7 @@ def rich_format_help( params=default_arguments, ctx=ctx, markup_mode=markup_mode, + expand=expand, console=console, ) for panel_name, arguments in panel_to_arguments.items(): @@ -600,6 +615,7 @@ def rich_format_help( params=arguments, ctx=ctx, markup_mode=markup_mode, + expand=expand, console=console, ) default_options = panel_to_options.get(OPTIONS_PANEL_TITLE, []) @@ -608,6 +624,7 @@ def rich_format_help( params=default_options, ctx=ctx, markup_mode=markup_mode, + expand=expand, console=console, ) for panel_name, options in panel_to_options.items(): @@ -619,6 +636,7 @@ def rich_format_help( params=options, ctx=ctx, markup_mode=markup_mode, + expand=expand, console=console, ) @@ -649,6 +667,7 @@ def rich_format_help( name=COMMANDS_PANEL_TITLE, commands=default_commands, markup_mode=markup_mode, + expand=expand, console=console, cmd_len=max_cmd_len, ) @@ -660,6 +679,7 @@ def rich_format_help( name=panel_name, commands=commands, markup_mode=markup_mode, + expand=expand, console=console, cmd_len=max_cmd_len, ) @@ -673,7 +693,7 @@ def rich_format_help( console.print(Padding(Align(epilogue_text, pad=False), 1)) -def rich_format_error(self: click.ClickException) -> None: +def rich_format_error(self: click.ClickException, expand: bool) -> None: """Print richly formatted click errors. Called by custom exception handler to print richly formatted click errors. @@ -700,6 +720,7 @@ def rich_format_error(self: click.ClickException) -> None: Panel( highlighter(self.format_message()), border_style=STYLE_ERRORS_PANEL_BORDER, + expand=expand, title=ERRORS_PANEL_TITLE, title_align=ALIGN_ERRORS_PANEL, )