From eac1c1bb3f509955edbe07103a070c07fe20381d Mon Sep 17 00:00:00 2001 From: CaxiLee <3578795101@qq.com> Date: Sat, 16 May 2026 10:26:29 +0800 Subject: [PATCH] fix: omit hidden commands/options from utils docs markdown --- tests/test_cli_docs_hidden.py | 54 +++++++++++++++++++++++++++++++++++ typer/cli.py | 34 ++++++++++++++++------ 2 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 tests/test_cli_docs_hidden.py diff --git a/tests/test_cli_docs_hidden.py b/tests/test_cli_docs_hidden.py new file mode 100644 index 0000000000..be5cc7bcf2 --- /dev/null +++ b/tests/test_cli_docs_hidden.py @@ -0,0 +1,54 @@ +"""Tests for `typer ... utils docs` Markdown output respecting hidden parameters/commands. + +`utils docs` walks the Click command tree and must stay consistent with the interactive +`--help` text (Rich): anything marked `hidden=True` should not appear in generated docs. +""" + +import click +import typer +from typer.cli import get_docs_for_click +from typer.main import get_command + + +def test_get_docs_for_click_omits_hidden_subcommands() -> None: + """Hidden subcommands must not appear in the command list or get nested sections.""" + + app = typer.Typer() + + @app.command() + def public_cmd() -> None: + """Visible to users.""" + pass + + @app.command(hidden=True) + def secret_cmd() -> None: + """Internal helper; should not be exported to Markdown.""" + pass + + click_obj = get_command(app) + with click.Context(click_obj, info_name="demo") as ctx: + md = get_docs_for_click(obj=click_obj, ctx=ctx, name="demo") + + # Typer/Click expose CLI names with hyphens by default. + assert "`public-cmd`" in md + assert "secret-cmd" not in md + + +def test_get_docs_for_click_omits_hidden_options() -> None: + """Hidden options should not appear under **Options** in generated Markdown.""" + + app = typer.Typer() + + @app.command() + def main( + visible: str = typer.Option("", "--visible", help="Shown."), + _legacy: str = typer.Option("", "--legacy", hidden=True, help="Deprecated."), + ) -> None: + pass + + click_obj = get_command(app) + with click.Context(click_obj, info_name="demo") as ctx: + md = get_docs_for_click(obj=click_obj, ctx=ctx, name="demo") + + assert "--visible" in md + assert "--legacy" not in md diff --git a/typer/cli.py b/typer/cli.py index 2a7d78c3a4..8ee92d6300 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -215,9 +215,15 @@ def get_docs_for_click( docs += f"{command_name} " docs += f"{' '.join(usage_pieces)}\n" docs += "```\n\n" + # Parameters marked with hidden=True are intentionally omitted from the live CLI + # help rendered by Rich (see rich_format_help in rich_utils.py). Generated Markdown + # from `typer ... utils docs` should follow the same rules so published docs do not + # leak internal or experimental flags/commands that authors hid from --help. args = [] opts = [] for param in obj.get_params(ctx): + if getattr(param, "hidden", False): + continue rv = param.get_help_record(ctx) if rv is not None: if param.param_type_name == "argument": @@ -244,21 +250,33 @@ def get_docs_for_click( docs += f"{obj.epilog}\n\n" if isinstance(obj, Group): group = obj - commands = group.list_commands(ctx) - if commands: + # Subcommands registered with hidden=True still appear in list_commands() but + # are excluded from the command panels in rich_format_help. Mirror that here so + # the Markdown outline and nested sections match what users see when they run + # --help on each group. + visible_commands: list[str] = [] + for cmd_name in group.list_commands(ctx): + command_obj = group.get_command(ctx, cmd_name) + if command_obj is None: + continue + if getattr(command_obj, "hidden", False): + continue + visible_commands.append(cmd_name) + + if visible_commands: docs += "**Commands**:\n\n" - for command in commands: - command_obj = group.get_command(ctx, command) - assert command_obj + for cmd_name in visible_commands: + command_obj = group.get_command(ctx, cmd_name) + assert command_obj is not None docs += f"* `{command_obj.name}`" command_help = command_obj.get_short_help_str() if command_help: docs += f": {_parse_html(to_parse, command_help)}" docs += "\n" docs += "\n" - for command in commands: - command_obj = group.get_command(ctx, command) - assert command_obj + for cmd_name in visible_commands: + command_obj = group.get_command(ctx, cmd_name) + assert command_obj is not None use_prefix = "" if command_name: use_prefix += f"{command_name}"