diff --git a/src/winml/modelkit/cli.py b/src/winml/modelkit/cli.py index 2e4745950..dca562839 100644 --- a/src/winml/modelkit/cli.py +++ b/src/winml/modelkit/cli.py @@ -218,19 +218,23 @@ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> Non super().format_help(ctx, formatter) def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: - """Format command list using AST-parsed help (no module imports).""" - commands = [] - for cmd_name in self.list_commands(ctx): - help_text = _parse_click_help(_COMMANDS_DIR / f"{cmd_name}.py") - commands.append((cmd_name, help_text)) - - if commands: - limit = max(1, formatter.width - 6 - max(len(name) for name, _ in commands)) - rows = [] - for name, help_text in commands: - short = help_text[:limit].rstrip() if help_text else "" - rows.append((name, short)) - + """Format command list using AST-parsed help (no module imports). + + Each command's first docstring line is handed verbatim to + :meth:`click.HelpFormatter.write_dl`, which wraps long descriptions + onto continuation lines at word boundaries with a hanging indent, + respecting the terminal width. + + We deliberately do *not* pre-truncate the help text: a fixed + character slice would cut mid-word (e.g. ``…model or .on``), whereas + ``write_dl`` wraps cleanly and keeps the full summary visible. + """ + rows = [ + (cmd_name, _parse_click_help(_COMMANDS_DIR / f"{cmd_name}.py")) + for cmd_name in self.list_commands(ctx) + ] + + if rows: with formatter.section("Commands"): formatter.write_dl(rows) diff --git a/tests/cli/test_help_cli.py b/tests/cli/test_help_cli.py index 346594311..1c0365a54 100644 --- a/tests/cli/test_help_cli.py +++ b/tests/cli/test_help_cli.py @@ -38,7 +38,12 @@ from click.testing import CliRunner, Result from winml.modelkit import __version__ -from winml.modelkit.cli import _COMMANDS_DIR, _DISABLED_COMMANDS, main +from winml.modelkit.cli import ( + _COMMANDS_DIR, + _DISABLED_COMMANDS, + _parse_click_help, + main, +) # --------------------------------------------------------------------------- @@ -192,6 +197,66 @@ def test_enabled_command_has_help_text(self, cmd: str) -> None: ) +# =========================================================================== +# Command summary truncation (regression for #511) +# =========================================================================== + + +class TestNoMidWordTruncation: + """Subcommand summaries must never be cut mid-word in ``winml --help``. + + Regression for issue #511: ``LazyGroup.format_commands`` used to slice + each help string at a fixed character count, which landed mid-token + (e.g. ``…HuggingFace model or .on``). The fix hands the full first + docstring line to Click's ``write_dl``, which wraps at word boundaries + onto continuation lines. The complete summary must therefore survive in + the rendered output (modulo the whitespace ``write_dl`` inserts when + wrapping), and no rendered line may exceed the formatter width. + """ + + @staticmethod + def _normalize(text: str) -> str: + """Collapse all whitespace runs so wrapped text compares to source.""" + return " ".join(text.split()) + + @pytest.mark.parametrize("cmd", ENABLED_COMMANDS) + def test_full_summary_present(self, cmd: str) -> None: + """The complete first docstring line appears in help, never truncated. + + Empty expected text (no docstring) is caught by + ``TestCommandList.test_enabled_command_has_help_text``; here an empty + string is trivially a substring and simply doesn't constrain. + """ + expected = self._normalize(_parse_click_help(_COMMANDS_DIR / f"{cmd}.py")) + rendered = self._normalize(_invoke("--help").output) + assert expected in rendered, f"'{cmd}' summary was truncated in winml --help" + + def test_long_summary_wraps_within_narrow_width(self) -> None: + """At a narrow width the longest summary wraps — full text, no overflow. + + Forces a width that guarantees wrapping regardless of the current + docstrings, proving the formatter wraps rather than hard-truncating. + """ + import click + + width = 50 + formatter = click.HelpFormatter(width=width) + ctx = click.Context(main, info_name="winml") + main.format_commands(ctx, formatter) + rendered = formatter.getvalue() + + for line in rendered.splitlines(): + assert len(line) <= width, f"line exceeds width {width}: {line!r}" + + longest = max( + (_parse_click_help(_COMMANDS_DIR / f"{c}.py") for c in ENABLED_COMMANDS), + key=len, + ) + assert self._normalize(longest) in self._normalize(rendered), ( + "longest summary was truncated instead of wrapped" + ) + + # =========================================================================== # Options section # ===========================================================================