Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions src/winml/modelkit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
67 changes: 66 additions & 1 deletion tests/cli/test_help_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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
# ===========================================================================
Expand Down
Loading