diff --git a/docs/tutorial/commands/help.md b/docs/tutorial/commands/help.md index 3a0e7170ad..b9f9d45070 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.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.py b/docs_src/commands/help/tutorial009.py new file mode 100644 index 0000000000..373e14c2e3 --- /dev/null +++ b/docs_src/commands/help/tutorial009.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..ab75d40d66 --- /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 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 e9631e56cf..a7ee068889 100644 --- a/typer/core.py +++ b/typer/core.py @@ -166,6 +166,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 @@ -212,7 +213,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 @@ -674,6 +675,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, @@ -691,6 +693,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 @@ -724,6 +727,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, ) @@ -736,6 +740,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, ) @@ -750,12 +755,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( @@ -808,6 +815,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, ) @@ -820,6 +828,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 2e3b1223cc..f20af63cba 100644 --- a/typer/main.py +++ b/typer/main.py @@ -136,6 +136,7 @@ def __init__( add_completion: bool = True, # Rich settings rich_markup_mode: MarkupMode = Default(DEFAULT_MARKUP_MODE), + rich_expand: bool = True, rich_help_panel: Union[str, None] = Default(None), suggest_commands: bool = True, pretty_exceptions_enable: bool = True, @@ -144,6 +145,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 @@ -334,6 +336,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 @@ -367,6 +370,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) @@ -463,6 +467,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" @@ -473,6 +478,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 @@ -481,6 +487,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: @@ -528,6 +535,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, @@ -563,6 +571,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__) @@ -597,6 +606,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 d4c3676aea..d1408bd8da 100644 --- a/typer/rich_utils.py +++ b/typer/rich_utils.py @@ -234,7 +234,8 @@ def _get_parameter_help( param: Union[click.Option, click.Argument, click.Parameter], ctx: click.Context, markup_mode: MarkupMode, -) -> 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 @@ -313,9 +314,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( @@ -351,6 +359,7 @@ def _print_options_panel( params: Union[List[click.Option], List[click.Argument]], ctx: click.Context, markup_mode: MarkupMode, + expand: bool, console: Console, ) -> None: options_rows: List[List[RenderableType]] = [] @@ -433,6 +442,7 @@ class MetavarHighlighter(RegexHighlighter): param=param, ctx=ctx, markup_mode=markup_mode, + rich_expand=expand, ), ] ) @@ -468,6 +478,7 @@ class MetavarHighlighter(RegexHighlighter): options_table, border_style=STYLE_OPTIONS_PANEL_BORDER, title=name, + expand=expand, title_align=ALIGN_OPTIONS_PANEL, ) ) @@ -478,6 +489,7 @@ def _print_commands_panel( name: str, commands: List[click.Command], markup_mode: MarkupMode, + expand: bool, console: Console, cmd_len: int, ) -> None: @@ -544,6 +556,7 @@ def _print_commands_panel( commands_table, border_style=STYLE_COMMANDS_PANEL_BORDER, title=name, + expand=expand, title_align=ALIGN_COMMANDS_PANEL, ) ) @@ -554,6 +567,7 @@ def rich_format_help( obj: Union[click.Command, click.Group], ctx: click.Context, markup_mode: MarkupMode, + expand: bool, ) -> None: """Print nicely formatted help text using rich. @@ -607,6 +621,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(): @@ -618,6 +633,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, []) @@ -626,6 +642,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(): @@ -637,6 +654,7 @@ def rich_format_help( params=options, ctx=ctx, markup_mode=markup_mode, + expand=expand, console=console, ) @@ -667,6 +685,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, ) @@ -678,6 +697,7 @@ def rich_format_help( name=panel_name, commands=commands, markup_mode=markup_mode, + expand=expand, console=console, cmd_len=max_cmd_len, ) @@ -691,7 +711,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. @@ -718,6 +738,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, )