From b2273c39bab43b049de39d4f98706c3ab84974cc Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 17 Nov 2025 09:50:29 -0800 Subject: [PATCH 01/11] Reference doc generator tool --- smart_tests/args4p/command.py | 87 +++++++++++++++++++++++++++++++++++ tools/README.md | 70 ++++++++++++++++++++++++++++ tools/generate_reference.py | 81 ++++++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 tools/README.md create mode 100755 tools/generate_reference.py diff --git a/smart_tests/args4p/command.py b/smart_tests/args4p/command.py index 22e61adfb..07a3cb6c9 100644 --- a/smart_tests/args4p/command.py +++ b/smart_tests/args4p/command.py @@ -416,6 +416,93 @@ def _format_options(self, caption: str, lines: list[str]): if self.parent: self.parent._format_options(f"Options (common to {self.parent.name})", lines) + def format_asciidoc_table(self) -> str: + """ + Generate an AsciiDoc table for the command's options and arguments. + Returns the table as a string. + """ + lines = [] + + # Add options table + options = [opt for opt in self.options if not opt.hidden] + if options or self.arguments: + lines.append("[cols=\"1,2,1\"]") + lines.append("|===") + lines.append("|Option |Description |Required") + lines.append("") + + # Add arguments first + for arg in self.arguments: + # Format argument name with angle brackets + if arg.multiple: + arg_name = f"`<{arg.metavar}>...`" + else: + arg_name = f"`<{arg.metavar}>`" + + # Build description + desc_parts = [] + if arg.help: + desc_parts.append(arg.help) + + # Add type info if not string + if arg.type != str: + type_name = getattr(arg.type, '__name__', str(arg.type)) + desc_parts.append(f"(type: {type_name})") + + # Add default value + if arg.default != NO_DEFAULT: + desc_parts.append(f"Default: `{arg.default}`") + + description = " ".join(desc_parts) if desc_parts else "" + + # Required column + required = "Yes" if arg.required else "No" + + lines.append(f"|{arg_name}") + lines.append(f"|{description}") + lines.append(f"|{required}") + lines.append("") + + # Add options + for opt in options: + # Format option names + opt_names = ", ".join([f"`{name}`" for name in opt.option_names]) + + # Add metavar for non-boolean options + if opt.type != bool: + if opt.metavar: + opt_names += f" {opt.metavar}" + else: + type_name = getattr(opt.type, '__name__', str(opt.type)) + opt_names += f" {type_name.upper()}" + + # Build description + desc_parts = [] + if opt.help: + desc_parts.append(opt.help) + + # Add default value info + if opt.default != NO_DEFAULT and opt.type != bool: + desc_parts.append(f"Default: `{opt.default}`") + + # Add multiple indicator + if opt.multiple: + desc_parts.append("(can be specified multiple times)") + + description = " ".join(desc_parts) if desc_parts else "" + + # Required column + required = "Yes" if opt.required else "No" + + lines.append(f"|{opt_names}") + lines.append(f"|{description}") + lines.append(f"|{required}") + lines.append("") + + lines.append("|===") + + return "\n".join(lines) + def __repr__(self): return f"" diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 000000000..29992e74e --- /dev/null +++ b/tools/README.md @@ -0,0 +1,70 @@ +# Documentation Generation Tools + +## generate_reference.py + +This script automatically generates AsciiDoc tables for CLI command options and arguments from the args4p command definitions. +This is meant to be used with https://github.com/cloudbees/docsite-cloudbees-smart-tests/blob/main/docs/modules/resources/pages/cli-reference.adoc + +### Usage + +```bash +uv run python tools/generate_reference.py path/to/reference.adoc +``` + +### How It Works + +The script looks for special markers in the AsciiDoc file: + +```asciidoc +// [generate:COMMAND_PATH] +... content to be replaced ... +// [/generate] +``` + +Where `COMMAND_PATH` is a space-separated command path like: +- `inspect subset` +- `record build` +- `subset` + +The script will: +1. Parse the AsciiDoc file to find all `[generate:...]` markers +2. Resolve each command path to the actual Command object +3. Generate an AsciiDoc table from the command's options and arguments +4. Replace the content between the markers with the generated table + +### Marking Sections for Generation + +To mark a section for automatic generation, wrap it with markers: + +```asciidoc +=== inspect subset + +Display details of a subset request. + +`smart-tests inspect subset --subset-id 26876` + +// [generate:inspect subset] +[cols="1,2,1"] +|=== +|Option |Description |Required + +|`--subset-id` INT +|subset id +|Yes + +|`--json` +|display JSON format +|No + +|=== +// [/generate] + +Additional documentation... +``` + +### Benefits + +- **Single Source of Truth**: Option/argument definitions live in code +- **Always Up-to-Date**: Regenerate docs whenever code changes +- **Human Control**: Manually written documentation is preserved +- **Easy Maintenance**: Just run the script to update all marked sections diff --git a/tools/generate_reference.py b/tools/generate_reference.py new file mode 100755 index 000000000..8b227f92e --- /dev/null +++ b/tools/generate_reference.py @@ -0,0 +1,81 @@ +#!/usr/bin/env uv run python +""" +Generate AsciiDoc documentation tables from args4p command definitions. + +This script parses reference.adoc and replaces sections marked with: + // [generate:COMMAND_PATH] + ... + // [/generate] + +Where COMMAND_PATH is a space-separated command path like "inspect subset" or "record build". +""" + +import re +import sys +from pathlib import Path +from typing import Annotated + +import smart_tests.args4p.typer as typer +from smart_tests import args4p +from smart_tests.args4p.command import Command, Group +from smart_tests.args4p.converters import path +from smart_tests.args4p.exceptions import BadCmdLineException + +# Add parent directory to path to import smart_tests modules +sys.path.insert(0, str(Path(__file__).parent.parent)) + + +def resolve_command(root: Group, command_path: str) -> Command: + current = root + + for part in command_path.strip().split(): + if not isinstance(current, Group): + raise BadCmdLineException(f"Command '{command_path}' is invalid: '{part}' is not a group command") + current = current.find_subcommand(part) + + return current + + +def process_reference_file(reference_path: Path, cli_root: Group): + """ + Process the reference.adoc file and replace marked sections with generated tables. + + Returns: + The new content of the file + """ + content = reference_path.read_text() + + # Pattern to match: // [generate:COMMAND_PATH] + # Captures everything until // [/generate] + pattern = r'// \[generate:([^\]]+)\]\n(.*?)// \[/generate\]' + + def replace_section(match): + command_path = match.group(1).strip() + + # Resolve the command + command = resolve_command(cli_root, command_path) + + # Generate the table + table = command.format_asciidoc_table() + + # Return the marker with new content + return f"// [generate:{command_path}]\n{table}\n// [/generate]" + + # Replace all marked sections + new_content = re.sub(pattern, replace_section, content, flags=re.DOTALL) + + reference_path.write_text(new_content) + print(f"Updated {reference_path}") + + +@args4p.command(help="Generate AsciiDoc documentation tables from args4p commands") +def main(reference_file: Annotated[Path, typer.Argument(type=path(exists=True, file_okay=True, dir_okay=False))]): + """Main entry point for the script.""" + + from smart_tests.__main__ import cli + + process_reference_file(reference_file, cli) + + +if __name__ == "__main__": + main() From f8ce9ea46d9ef2c690ad923b5b3d5ebe8bbc2271 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 17 Nov 2025 10:29:49 -0800 Subject: [PATCH 02/11] Start with the usage line --- smart_tests/args4p/command.py | 83 ++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/smart_tests/args4p/command.py b/smart_tests/args4p/command.py index 07a3cb6c9..c86a12b08 100644 --- a/smart_tests/args4p/command.py +++ b/smart_tests/args4p/command.py @@ -264,6 +264,45 @@ def error(msg: str) -> BadConfigException: for c in self.commands: c.check_consistency() + def _usage_line(self, program_name) -> str: + parts = ["Usage:"] + + # Program name + parts.append(program_name) + + # Build command path (for subcommands) + command_path: List[str] = [] + current = self + while current.parent is not None: + command_path.insert(0, current.name) + current = current.parent + if len(command_path) > 0: + parts.append(" ".join(command_path)) + + # Add options placeholder if we have options + if self.options: + parts.append("[OPTIONS]") + + # Add arguments + for a in self.arguments: + if a.required: + if a.multiple: + parts.append(f"<{a.metavar}>...") + else: + parts.append(f"<{a.metavar}>") + else: + if a.multiple: + parts.append(f"[{a.metavar}...]") + else: + parts.append(f"[{a.metavar}]") + if isinstance(self, Group): + if len(self.arguments) == 0: + # Add subcommand placeholder for groups + parts.append("COMMAND") + parts.append("...") + + return " ".join(parts) + def format_help(self, program_name: str = os.path.basename(sys.argv[0])) -> str: """ Generate and return a formatted help message for this command. @@ -271,46 +310,8 @@ def format_help(self, program_name: str = os.path.basename(sys.argv[0])) -> str: :param program_name Name of the program to display in the usage line. Defaults to the name of the running script. """ - def usage_line() -> str: - parts = ["Usage:"] - - # Program name - parts.append(program_name) - - # Build command path (for subcommands) - command_path: List[str] = [] - current = self - while current.parent is not None: - command_path.insert(0, current.name) - current = current.parent - if len(command_path) > 0: - parts.append(" ".join(command_path)) - - # Add options placeholder if we have options - if self.options: - parts.append("[OPTIONS]") - - # Add arguments - for a in self.arguments: - if a.required: - if a.multiple: - parts.append(f"<{a.metavar}>...") - else: - parts.append(f"<{a.metavar}>") - else: - if a.multiple: - parts.append(f"[{a.metavar}...]") - else: - parts.append(f"[{a.metavar}]") - if isinstance(self, Group): - if len(self.arguments) == 0: - # Add subcommand placeholder for groups - parts.append("COMMAND") - parts.append("...") - - return " ".join(parts) - lines = [usage_line()] + lines = [self._usage_line(program_name)] # Description from docstring if self.callback.__doc__: @@ -416,12 +417,12 @@ def _format_options(self, caption: str, lines: list[str]): if self.parent: self.parent._format_options(f"Options (common to {self.parent.name})", lines) - def format_asciidoc_table(self) -> str: + def format_asciidoc_table(self, program_name: str) -> str: """ Generate an AsciiDoc table for the command's options and arguments. Returns the table as a string. """ - lines = [] + lines = [f"`{self._usage_line(program_name)}`", ""] # Add options table options = [opt for opt in self.options if not opt.hidden] From d8c86f54cf7ac2066d780606b72b8197616eef9d Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 17 Nov 2025 10:30:17 -0800 Subject: [PATCH 03/11] Simplification and bug fix --- tools/generate_reference.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/tools/generate_reference.py b/tools/generate_reference.py index 8b227f92e..78e653407 100755 --- a/tools/generate_reference.py +++ b/tools/generate_reference.py @@ -1,4 +1,4 @@ -#!/usr/bin/env uv run python +#!/usr/bin/env -S uv run --script """ Generate AsciiDoc documentation tables from args4p command definitions. @@ -31,7 +31,11 @@ def resolve_command(root: Group, command_path: str) -> Command: for part in command_path.strip().split(): if not isinstance(current, Group): raise BadCmdLineException(f"Command '{command_path}' is invalid: '{part}' is not a group command") - current = current.find_subcommand(part) + + try: + current = current.find_subcommand(part) + except BadCmdLineException: + raise BadCmdLineException(f"Command '{command_path}' is invalid: no such subcommand '{part}'") return current @@ -52,13 +56,8 @@ def process_reference_file(reference_path: Path, cli_root: Group): def replace_section(match): command_path = match.group(1).strip() - # Resolve the command - command = resolve_command(cli_root, command_path) - - # Generate the table - table = command.format_asciidoc_table() + table = resolve_command(cli_root, command_path).format_asciidoc_table("smart-tests") - # Return the marker with new content return f"// [generate:{command_path}]\n{table}\n// [/generate]" # Replace all marked sections @@ -69,7 +68,7 @@ def replace_section(match): @args4p.command(help="Generate AsciiDoc documentation tables from args4p commands") -def main(reference_file: Annotated[Path, typer.Argument(type=path(exists=True, file_okay=True, dir_okay=False))]): +def main(reference_file: Annotated[Path, typer.Argument(type=path(exists=True, file_okay=True, dir_okay=False), required=True)]): """Main entry point for the script.""" from smart_tests.__main__ import cli @@ -78,4 +77,4 @@ def main(reference_file: Annotated[Path, typer.Argument(type=path(exists=True, f if __name__ == "__main__": - main() + main(*sys.argv[1:]) From ffc56f502a6b54eef11a8fc6716df4e9e30d2e5a Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 17 Nov 2025 10:35:24 -0800 Subject: [PATCH 04/11] Report options alphabetically --- smart_tests/args4p/command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smart_tests/args4p/command.py b/smart_tests/args4p/command.py index c86a12b08..a4a122466 100644 --- a/smart_tests/args4p/command.py +++ b/smart_tests/args4p/command.py @@ -425,7 +425,8 @@ def format_asciidoc_table(self, program_name: str) -> str: lines = [f"`{self._usage_line(program_name)}`", ""] # Add options table - options = [opt for opt in self.options if not opt.hidden] + options = sorted([opt for opt in self.options if not opt.hidden], key=lambda o: o.name) + if options or self.arguments: lines.append("[cols=\"1,2,1\"]") lines.append("|===") From 170b65f4d2e0f66c9db45d6434da48f74f0e0312 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 17 Nov 2025 12:15:56 -0800 Subject: [PATCH 05/11] Imported metavar from reference CLI doc I noticed most metavars are empty, resulting in undesirable place holders like STR. I had Claude Code reverse import metavar from the reference docs --- smart_tests/commands/inspect/subset.py | 4 ++-- smart_tests/commands/record/build.py | 27 +++++++++++++++----------- smart_tests/commands/record/commit.py | 11 +++++++---- smart_tests/commands/record/session.py | 14 ++++++++----- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/smart_tests/commands/inspect/subset.py b/smart_tests/commands/inspect/subset.py index 88691839d..fe180c75c 100644 --- a/smart_tests/commands/inspect/subset.py +++ b/smart_tests/commands/inspect/subset.py @@ -99,11 +99,11 @@ def display(self): def subset( app: Application, subset_id: Annotated[int, typer.Option( - help="subset id", + help="Subset id", required=True )], json: Annotated[bool, typer.Option( - help="display JSON format" + help="Display JSON format" )] = False, ): is_json_format = json # Map parameter name diff --git a/smart_tests/commands/record/build.py b/smart_tests/commands/record/build.py index 3d8243d85..57bbfce17 100644 --- a/smart_tests/commands/record/build.py +++ b/smart_tests/commands/record/build.py @@ -35,34 +35,37 @@ def build( app: Application, build_name: Annotated[str, typer.Option( "--build", - help="build name", - metavar="BUILD_NAME", + help="Build name", + metavar="NAME", required=True )], branch: Annotated[str | None, typer.Option( "--branch", - help="Branch name. A branch is a set of test sessions grouped and this option value will be used for a lineage name." + help="Set branch name. A branch is a set of test sessions grouped and this option value will be used for a branch name.", + metavar="NAME" )] = None, repositories: Annotated[List[str], typer.Option( "--repo-branch-map", multiple=True, help="Set repository name and branch name when you use --no-commit-collection option. " - "Please use the same repository name with a commit option" + "Please use the same repository name with a commit option", + metavar="REPO_NAME=BRANCH_NAME" )] = [], source: Annotated[List[str], typer.Option( multiple=True, - help="path to local Git workspace, optionally prefixed by a label. " + help="Path to local Git workspace, optionally prefixed by a label. " "like --source path/to/ws or --source main=path/to/ws", - metavar="REPO_NAME" + metavar="DIR" )] = ["."], max_days: Annotated[int, typer.Option( - help="the maximum number of days to collect commits retroactively" + help="The maximum number of days to collect commits retroactively", + metavar="DAYS" )] = 30, no_submodules: Annotated[bool, typer.Option( - help="stop collecting information from Git Submodules" + help="Stop collecting information from Git Submodules" )] = False, no_commit_collection: Annotated[bool, typer.Option( - help="do not collect commit data. " + help="Do not collect commit data. " "This is useful if the repository is a shallow clone and the RevWalk is not " "possible. The commit data must be collected with a separate fully-cloned " "repository." @@ -70,11 +73,13 @@ def build( commits: Annotated[List[str], typer.Option( "--commit", multiple=True, - help="set repository name and commit hash when you use --no-commit-collection option" + help="Set repository name and commit hash when you use --no-commit-collection option", + metavar="REPO_NAME=COMMIT_HASH" )] = [], timestamp: Annotated[str | None, typer.Option( help="Used to overwrite the build time when importing historical data. " - "Note: Format must be `YYYY-MM-DDThh:mm:ssTZD` or `YYYY-MM-DDThh:mm:ss` (local timezone applied)" + "Note: Format must be `YYYY-MM-DDThh:mm:ssTZD` or `YYYY-MM-DDThh:mm:ss` (local timezone applied)", + metavar="TIMESTAMP" )] = None, links: Annotated[List[KeyValue], typer.Option( "--link", diff --git a/smart_tests/commands/record/commit.py b/smart_tests/commands/record/commit.py index 94c529300..908b57eb7 100644 --- a/smart_tests/commands/record/commit.py +++ b/smart_tests/commands/record/commit.py @@ -28,20 +28,23 @@ def commit( app: Application, name: Annotated[str | None, typer.Option( - help="repository name" + help="Repository name" )] = None, source: Annotated[str, typer.Option( - help="repository path" + help="Repository path", + metavar="DIR", )] = os.getcwd(), executable: Annotated[str, typer.Option( help="[Obsolete] it was to specify how to perform commit collection but has been removed", hidden=True )] = "jar", max_days: Annotated[int, typer.Option( - help="the maximum number of days to collect commits retroactively" + help="The maximum number of days to collect commits retroactively", + metavar="DAYS", )] = 30, import_git_log_output: Annotated[str | None, typer.Option( - help="import from the git-log output" + help="Import from the git-log output", + metavar="FILE", )] = None, ): if executable == 'docker': diff --git a/smart_tests/commands/record/session.py b/smart_tests/commands/record/session.py index c734d98ea..c9f315a8f 100644 --- a/smart_tests/commands/record/session.py +++ b/smart_tests/commands/record/session.py @@ -26,29 +26,32 @@ def session( app: Application, build_name: Annotated[str, typer.Option( "--build", - help="build name", + help="Build name", + metavar="NAME", required=True )], test_suite: Annotated[str, typer.Option( "--test-suite", help="Set test suite name. A test suite is a collection of test sessions. Setting a test suite allows you to " "manage data over test sessions and lineages.", + metavar="NAME", required=True )], flavors: Annotated[List[KeyValue], typer.Option( "--flavor", - help="flavors", + help="Flavors", multiple=True, metavar="KEY=VALUE", type=parse_key_value )] = [], is_observation: Annotated[bool, typer.Option( "--observation", - help="enable observation mode" + help="Enable observation mode" )] = False, links: Annotated[List[KeyValue], typer.Option( "--link", - help="Set external link of a title and url", + help="Set external link of title and url", + metavar="TITLE=URL", multiple=True, type=parse_key_value, )] = [], @@ -58,7 +61,8 @@ def session( )] = False, timestamp: Annotated[str | None, typer.Option( help="Used to overwrite the session time when importing historical data. Note: Format must be " - "`YYYY-MM-DDThh:mm:ssTZD` or `YYYY-MM-DDThh:mm:ss` (local timezone applied)" + "`YYYY-MM-DDThh:mm:ssTZD` or `YYYY-MM-DDThh:mm:ss` (local timezone applied)", + metavar="TIMESTAMP" )] = None, ): From 225a0215d8ccaecd3e17e4d8aa8dfea5e4976ffa Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 17 Nov 2025 12:57:54 -0800 Subject: [PATCH 06/11] Added rest of metavars --- smart_tests/commands/record/tests.py | 3 ++- smart_tests/commands/subset.py | 28 ++++++++++++++++++---------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/smart_tests/commands/record/tests.py b/smart_tests/commands/record/tests.py index c1776a61f..e8fc0ff71 100644 --- a/smart_tests/commands/record/tests.py +++ b/smart_tests/commands/record/tests.py @@ -149,7 +149,8 @@ def __init__( hidden=True )] = False, group: Annotated[str | None, typer.Option( - help="Grouping name for test results" + help="Grouping name for test results", + metavar="NAME" )] = "", is_allow_test_before_build: Annotated[bool, typer.Option( "--allow-test-before-build", diff --git a/smart_tests/commands/subset.py b/smart_tests/commands/subset.py index 83f57457c..0a67ea386 100644 --- a/smart_tests/commands/subset.py +++ b/smart_tests/commands/subset.py @@ -101,18 +101,22 @@ def __init__( session: Annotated[SessionId, SessionId.as_option()], target: Annotated[Percentage | None, typer.Option( type=parse_percentage, - help="subsetting target from 0% to 100%" + help="Subsetting target from 0% to 100%", + metavar="PERCENTAGE" )] = None, time: Annotated[Duration | None, typer.Option( type=parse_duration, - help="subsetting by absolute time, in seconds e.g) 300, 5m" + help="Subsetting by absolute time, in seconds e.g) 300, 5m", + metavar="TIME" )] = None, confidence: Annotated[Percentage | None, typer.Option( type=parse_percentage, - help="subsetting by confidence from 0% to 100%" + help="Subsetting by confidence from 0% to 100%", + metavar="PERCENTAGE" )] = None, goal_spec: Annotated[str | None, typer.Option( - help="subsetting by programmatic goal definition" + help="Subsetting by programmatic goal definition", + metavar="GOAL_SPEC" )] = None, base_path: Annotated[str | None, typer.Option( '--base', @@ -120,7 +124,8 @@ def __init__( metavar="DIR" )] = None, rest: Annotated[str | None, typer.Option( - help="Output the subset remainder to a file, e.g. `--rest=remainder.txt`" + help="Output the subset remainder to a file, e.g. --rest=remainder.txt", + metavar="FILE" )] = None, # TODO(Konboi): omit from the smart-tests command initial release # split: Annotated[bool, typer.Option( @@ -142,11 +147,11 @@ def __init__( )] = False, is_get_tests_from_previous_sessions: Annotated[bool, typer.Option( "--get-tests-from-previous-sessions", - help="get subset list from previous full tests" + help="Get subset list from previous full tests" )] = False, is_output_exclusion_rules: Annotated[bool, typer.Option( "--output-exclusion-rules", - help="outputs the exclude test list. Switch the subset and rest." + help="Outputs the exclude test list. Switch the subset and rest." )] = False, is_non_blocking: Annotated[bool, typer.Option( "--non-blocking", @@ -155,16 +160,19 @@ def __init__( )] = False, ignore_flaky_tests_above: Annotated[float | None, typer.Option( help="Ignore flaky tests above the value set by this option. You can confirm flaky scores in WebApp", - type=floatType(min=0.0, max=1.0) + type=floatType(min=0.0, max=1.0), + metavar="N" )] = None, prioritize_tests_failed_within_hours: Annotated[int | None, typer.Option( help="Prioritize tests that failed within the specified hours; maximum 720 hours (= 24 hours * 30 days)", - type=intType(min=0, max=24 * 30) + type=intType(min=0, max=24 * 30), + metavar="N" )] = None, prioritized_tests_mapping_file: Annotated[TextIOWrapper | None, typer.Option( "--prioritized-tests-mapping", help="Prioritize tests based on test mapping file", - type=fileText(mode="r") + type=fileText(mode="r"), + metavar="FILE" )] = None, is_get_tests_from_guess: Annotated[bool, typer.Option( "--get-tests-from-guess", From 698ab4b8228245f3ec12ead3543b1c406a9e396f Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 17 Nov 2025 13:09:55 -0800 Subject: [PATCH 07/11] Massaged machine generated code --- smart_tests/args4p/command.py | 94 ++++++++++++++++------------------- 1 file changed, 43 insertions(+), 51 deletions(-) diff --git a/smart_tests/args4p/command.py b/smart_tests/args4p/command.py index a4a122466..67b1313ff 100644 --- a/smart_tests/args4p/command.py +++ b/smart_tests/args4p/command.py @@ -424,7 +424,6 @@ def format_asciidoc_table(self, program_name: str) -> str: """ lines = [f"`{self._usage_line(program_name)}`", ""] - # Add options table options = sorted([opt for opt in self.options if not opt.hidden], key=lambda o: o.name) if options or self.arguments: @@ -433,72 +432,65 @@ def format_asciidoc_table(self, program_name: str) -> str: lines.append("|Option |Description |Required") lines.append("") - # Add arguments first - for arg in self.arguments: - # Format argument name with angle brackets - if arg.multiple: - arg_name = f"`<{arg.metavar}>...`" - else: - arg_name = f"`<{arg.metavar}>`" + def _print_required(p: Parameter) -> str: + return "Yes" if p.required else "No" - # Build description - desc_parts = [] - if arg.help: - desc_parts.append(arg.help) + for arg in self.arguments: + def _print_name() -> str: + if arg.multiple: + return f"`<{arg.metavar}>...`" + else: + return f"`<{arg.metavar}>`" - # Add type info if not string - if arg.type != str: - type_name = getattr(arg.type, '__name__', str(arg.type)) - desc_parts.append(f"(type: {type_name})") + def _print_description() -> str: + desc_parts = [] + if arg.help: + desc_parts.append(arg.help) - # Add default value - if arg.default != NO_DEFAULT: - desc_parts.append(f"Default: `{arg.default}`") + if arg.type != str: + type_name = getattr(arg.type, '__name__', str(arg.type)) + desc_parts.append(f"(type: {type_name})") - description = " ".join(desc_parts) if desc_parts else "" + if arg.default != NO_DEFAULT: + desc_parts.append(f"Default: `{arg.default}`") - # Required column - required = "Yes" if arg.required else "No" + return " ".join(desc_parts) - lines.append(f"|{arg_name}") - lines.append(f"|{description}") - lines.append(f"|{required}") + lines.append(f"|{_print_name()}") + lines.append(f"|{_print_description()}") + lines.append(f"|{_print_required(arg)}") lines.append("") - # Add options for opt in options: - # Format option names - opt_names = ", ".join([f"`{name}`" for name in opt.option_names]) + def _print_name() -> str: + names = ", ".join([f"`{name}`" for name in opt.option_names]) - # Add metavar for non-boolean options - if opt.type != bool: - if opt.metavar: - opt_names += f" {opt.metavar}" - else: - type_name = getattr(opt.type, '__name__', str(opt.type)) - opt_names += f" {type_name.upper()}" + # Add metavar for non-boolean options + if opt.type != bool: + if opt.metavar: + names += f" {opt.metavar}" + else: + type_name = getattr(opt.type, '__name__', str(opt.type)) + names += f" {type_name.upper()}" - # Build description - desc_parts = [] - if opt.help: - desc_parts.append(opt.help) + return names - # Add default value info - if opt.default != NO_DEFAULT and opt.type != bool: - desc_parts.append(f"Default: `{opt.default}`") + def _print_description() -> str: + desc_parts = [] + if opt.help: + desc_parts.append(opt.help) - # Add multiple indicator - if opt.multiple: - desc_parts.append("(can be specified multiple times)") + if opt.default != NO_DEFAULT and opt.type != bool: + desc_parts.append(f"Default: `{opt.default}`") - description = " ".join(desc_parts) if desc_parts else "" + if opt.multiple: + desc_parts.append("(can be specified multiple times)") - # Required column - required = "Yes" if opt.required else "No" + return " ".join(desc_parts) - lines.append(f"|{opt_names}") - lines.append(f"|{description}") - lines.append(f"|{required}") + lines.append(f"|{_print_name()}") + lines.append(f"|{_print_description()}") + lines.append(f"|{_print_required(opt)}") lines.append("") lines.append("|===") From 223ba5b2f96e07fd9840a85e6e33a05cac0f09fe Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 17 Nov 2025 13:16:12 -0800 Subject: [PATCH 08/11] Split the arguments section and the options section --- smart_tests/args4p/command.py | 20 +++++++++++++------- smart_tests/commands/record/commit.py | 3 ++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/smart_tests/args4p/command.py b/smart_tests/args4p/command.py index 67b1313ff..1b2ac48ca 100644 --- a/smart_tests/args4p/command.py +++ b/smart_tests/args4p/command.py @@ -424,17 +424,15 @@ def format_asciidoc_table(self, program_name: str) -> str: """ lines = [f"`{self._usage_line(program_name)}`", ""] - options = sorted([opt for opt in self.options if not opt.hidden], key=lambda o: o.name) + def _print_required(p: Parameter) -> str: + return "Yes" if p.required else "No" - if options or self.arguments: + if self.arguments: lines.append("[cols=\"1,2,1\"]") lines.append("|===") - lines.append("|Option |Description |Required") + lines.append("|Argument |Description |Required") lines.append("") - def _print_required(p: Parameter) -> str: - return "Yes" if p.required else "No" - for arg in self.arguments: def _print_name() -> str: if arg.multiple: @@ -461,7 +459,15 @@ def _print_description() -> str: lines.append(f"|{_print_required(arg)}") lines.append("") - for opt in options: + lines.append("|===") + + if self.options: + lines.append("[cols=\"1,2,1\"]") + lines.append("|===") + lines.append("|Option |Description |Required") + lines.append("") + + for opt in sorted([opt for opt in self.options if not opt.hidden], key=lambda o: o.name): def _print_name() -> str: names = ", ".join([f"`{name}`" for name in opt.option_names]) diff --git a/smart_tests/commands/record/commit.py b/smart_tests/commands/record/commit.py index 908b57eb7..dffe3be4d 100644 --- a/smart_tests/commands/record/commit.py +++ b/smart_tests/commands/record/commit.py @@ -28,7 +28,8 @@ def commit( app: Application, name: Annotated[str | None, typer.Option( - help="Repository name" + help="Repository name", + metavar="NAME", )] = None, source: Annotated[str, typer.Option( help="Repository path", From 915a61cadcfd29646061118c4a31600d36dd0df5 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 17 Nov 2025 13:19:36 -0800 Subject: [PATCH 09/11] Adjusting table width --- smart_tests/args4p/command.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smart_tests/args4p/command.py b/smart_tests/args4p/command.py index 1b2ac48ca..f9ba8ed64 100644 --- a/smart_tests/args4p/command.py +++ b/smart_tests/args4p/command.py @@ -428,7 +428,7 @@ def _print_required(p: Parameter) -> str: return "Yes" if p.required else "No" if self.arguments: - lines.append("[cols=\"1,2,1\"]") + lines.append("[cols=\"2,4,1\"]") lines.append("|===") lines.append("|Argument |Description |Required") lines.append("") @@ -462,7 +462,7 @@ def _print_description() -> str: lines.append("|===") if self.options: - lines.append("[cols=\"1,2,1\"]") + lines.append("[cols=\"2,4,1\"]") lines.append("|===") lines.append("|Option |Description |Required") lines.append("") From 233abb7f2b3d9152bd9f9bd01bbb50fd84dec782 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 17 Nov 2025 13:20:35 -0800 Subject: [PATCH 10/11] Added ample warning that these are generated --- smart_tests/args4p/command.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/smart_tests/args4p/command.py b/smart_tests/args4p/command.py index f9ba8ed64..f1523b1ab 100644 --- a/smart_tests/args4p/command.py +++ b/smart_tests/args4p/command.py @@ -454,6 +454,7 @@ def _print_description() -> str: return " ".join(desc_parts) + lines.append("// GENERATED. MODIFY IN CLI SOURCE CODE") lines.append(f"|{_print_name()}") lines.append(f"|{_print_description()}") lines.append(f"|{_print_required(arg)}") @@ -494,6 +495,7 @@ def _print_description() -> str: return " ".join(desc_parts) + lines.append("// GENERATED. MODIFY IN CLI SOURCE CODE") lines.append(f"|{_print_name()}") lines.append(f"|{_print_description()}") lines.append(f"|{_print_required(opt)}") From 9ec0e72697813c6e70075b6bc80e08fbcc60886b Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 17 Nov 2025 14:26:54 -0800 Subject: [PATCH 11/11] Better way to invoke the main method --- tools/generate_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/generate_reference.py b/tools/generate_reference.py index 78e653407..8fe5f036e 100755 --- a/tools/generate_reference.py +++ b/tools/generate_reference.py @@ -77,4 +77,4 @@ def main(reference_file: Annotated[Path, typer.Argument(type=path(exists=True, f if __name__ == "__main__": - main(*sys.argv[1:]) + main.main()