Skip to content

Commit 0ca72a3

Browse files
authored
[ENH] Add prog-name input and start deprecation cycle for click-prog-name (#8)
* bump argudmp to >= 0.1.3 * revert pre-commit to 4.3.0 * update tests * ignore .vscode * update tests * update action * use Python >=3.10 (3.14 for dev) * update README * fix prog name
1 parent 41b3382 commit 0ca72a3

10 files changed

Lines changed: 153 additions & 508 deletions

File tree

‎.github/workflows/run_tests.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
14+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
1515

1616
steps:
1717
- uses: actions/checkout@v7

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
__pycache__
22
.python-version
3+
.vscode

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ GitHub Action to create a [Boutiques descriptor](https://boutiques.github.io/) f
88
|------|-------------|----------|---------|
99
| `parser-type` | Type of parser to serialize. Either `"argparse"` or `"click"`. | yes | `argparse` |
1010
| `parser-location` | Module path to and name of function that produces the `argparse.ArgumentParser` or `click.Command` object. E.g., `"my_package.my_module:my_func_name"`. | yes | — |
11+
| `prog-name` | Program name to use. **Highly recommended** if the name is not explicitly set in the parser source code. | no | — |
1112
| `output-path` | Where the output Boutiques description file should be written. | yes | — |
1213
| `open-pr` | Whether to open a pull request with the generated descriptor. A GitHub token must be provided as well. | no | `true` |
1314
| `token` | GitHub token with write permissions for `contents` and `pull-requests`. Required if `open-pr` is true. | no | — |
14-
| `click-prog-name` | Program name to use for the Click parser type. By default this will be the name of the decorated function. | no | — |
1515
| `click-parent-location` | Module path to and name of function that produces the `click.Group` object that contains the `click.Command` object to serialize. E.g., `"my_package.my_module:my_func_name"`. Needed for nested commands. | no | — |
1616
| `exclude-version` | Whether to exclude the `tool-version` field in the Boutiques descriptor even if version information is available. Enabled by default because dynamic versioning schemes can cause the descriptor to be updated on every commit. | no | `true` |
1717
| `updates-file` | Path to a JSON file with updates to apply to the generated Boutiques descriptor. The file should contain a map of dot/bracket paths to values (e.g. `{"description": "new desc"}`). | no | — |
@@ -46,6 +46,7 @@ jobs:
4646
with:
4747
parser-type: argparse
4848
parser-location: my_tool.cli:get_parser
49+
prog-name: my_tool
4950
output-path: descriptor.json
5051
token: ${{ secrets.TOKEN }}
5152
```
@@ -62,6 +63,7 @@ This can be used for example to add a `"container-image"` field:
6263
with:
6364
parser-type: argparse
6465
parser-location: my_tool.cli:get_parser
66+
prog-name: my_tool
6567
output-path: descriptor.json
6668
token: ${{ secrets.TOKEN }}
6769
updates-str: '{"container-image.image": "my-container-image:tag", "container-image.type": "docker"}'
@@ -77,7 +79,7 @@ This can be used for example to add a `"container-image"` field:
7779
with:
7880
parser-type: click
7981
parser-location: my_tool.cli:cli
82+
prog-name: my-tool
8083
output-path: descriptor.json
8184
token: ${{ secrets.TOKEN }}
82-
click-prog-name: my-tool
8385
```

‎action.yml‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ inputs:
88
parser-location:
99
description: Module path to and name of function that produces the argparse.ArgumentParser or click.Command objects. E.g., "my_package.my_module:my_func_name"
1010
required: true
11+
prog-name:
12+
description: Program name to use. Highly recommended if the name is not explicitly set in the parser source code.
13+
required: false
1114
output-path:
1215
description: Where the output Boutiques description file should be written
1316
required: true
@@ -18,9 +21,6 @@ inputs:
1821
token:
1922
description: GitHub token with write permissions for "contents" and "pull-requests". Required if open-pr is true.
2023
required: false
21-
click-prog-name:
22-
description: Program name to use for the Click parser type. By default this will be the name of the decorated function.
23-
required: false
2424
click-parent-location:
2525
description: Module path to and name of function that produces the click.Group object that contains the click.Command object to serialize. E.g., "my_package.my_module:my_func_name". This is needed for nested commands.
2626
required: false
@@ -38,6 +38,11 @@ inputs:
3838
description: Whether to validate the generated Boutiques descriptor. If this is set to true, the action will fail if the descriptor is invalid.
3939
required: false
4040
default: false
41+
# TODO deprecate in v2
42+
click-prog-name:
43+
description: Program name to use for the Click parser type. By default this will be the name of the decorated function.
44+
deprecationMessage: This input is deprecated and will be removed in a future release. Use the "prog-name" input instead.
45+
required: false
4146

4247
runs:
4348
using: composite
@@ -53,6 +58,9 @@ runs:
5358
echo "::error::token must be provided if open-pr is true"
5459
exit 2
5560
fi
61+
if [[ ! -z "${{ inputs.click-prog-name }}" && ! -z "${{ inputs.prog-name }}" ]]; then
62+
echo "::warning::Both click-prog-name and prog-name were provided: using prog-name."
63+
fi
5664
shell: bash
5765

5866
- name: Set up Node.js
@@ -82,9 +90,9 @@ runs:
8290
env:
8391
PARSER_TYPE: ${{ inputs.parser-type }}
8492
PARSER_LOCATION: ${{ inputs.parser-location }}
93+
PROG_NAME: ${{ inputs.prog-name || inputs.click-prog-name }}
8594
OUTPUT_PATH: ${{ github.workspace }}/${{ inputs.output-path }}
8695
DUMP_FILE: dump.json
87-
CLICK_PROG_NAME: ${{ inputs.click-prog-name }}
8896
CLICK_PARENT_LOCATION: ${{ inputs.click-parent-location }}
8997
EXCLUDE_VERSION: ${{ inputs.exclude-version }}
9098
UPDATES_FILE: ${{ inputs.updates-file && format('{0}/{1}', github.workspace, inputs.updates-file) || '' }}

‎pyproject.toml‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ name = "python-cli-to-boutiques"
33
version = "1.1.0"
44
description = "Add your description here"
55
readme = "README.md"
6-
requires-python = ">=3.9"
6+
requires-python = ">=3.10"
77
dependencies = [
8-
"argdump>=0.1.2",
8+
"argdump>=0.1.3",
99
"clickdump>=0.1.0",
1010
]
1111

@@ -19,6 +19,9 @@ pythonpath = ["scripts", "tests"]
1919

2020
[dependency-groups]
2121
test = [
22-
"pytest>=8.4.2",
22+
"pytest>=9.0.3",
23+
]
24+
dev = [
25+
"pre-commit>=4.6.0",
26+
{include-group = "test"},
2327
]
24-
dev = ["pre-commit", {include-group = "test"}]

‎scripts/create_descriptor.sh‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ set -euo pipefail
44
# -- Create dump file --
55
if [[ "$PARSER_TYPE" == "click" ]]; then
66
CLICK_ARGS=(--output "$DUMP_FILE")
7-
if [[ -n "$CLICK_PROG_NAME" ]]; then
8-
CLICK_ARGS+=(--prog "$CLICK_PROG_NAME")
7+
if [[ -n "$PROG_NAME" ]]; then
8+
CLICK_ARGS+=(--prog "$PROG_NAME")
99
fi
1010
if [[ -n "$CLICK_PARENT_LOCATION" ]]; then
1111
CLICK_ARGS+=(--parent "$CLICK_PARENT_LOCATION")
1212
fi
1313
python ./scripts/run_clickdump.py "${CLICK_ARGS[@]}" "$PARSER_LOCATION"
1414
elif [[ "$PARSER_TYPE" == "argparse" ]]; then
15-
python ./scripts/run_argdump.py --output "$DUMP_FILE" "$PARSER_LOCATION"
15+
ARGPARSE_ARGS=(--output "$DUMP_FILE")
16+
if [[ -n "$PROG_NAME" ]]; then
17+
ARGPARSE_ARGS+=(--prog "$PROG_NAME")
18+
fi
19+
python ./scripts/run_argdump.py "${ARGPARSE_ARGS[@]}" "$PARSER_LOCATION"
1620
else
1721
echo "Error: PARSER_TYPE must be 'argparse' or 'click', got '$PARSER_TYPE'" >&2
1822
exit 1

‎scripts/run_argdump.py‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import argparse
55
import importlib
66
from pathlib import Path
7+
from typing import Optional
78

89
import argdump
910

@@ -29,11 +30,13 @@ def load_parser(module_path: str, parser_func_name: str) -> argparse.ArgumentPar
2930
return parser
3031

3132

32-
def run_argdump(location: str, output_path: Path, indent=DEFAULT_INDENT):
33+
def run_argdump(
34+
location: str, output_path: Path, indent=DEFAULT_INDENT, prog: Optional[str] = None
35+
):
3336
"""Serialize an `argparse.ArgumentParser` to JSON using argdump."""
3437
module_path, parser_name = location.split(":")
3538
parser_to_serialize = load_parser(module_path, parser_name)
36-
serialized_parser = argdump.dumps(parser_to_serialize, indent=indent)
39+
serialized_parser = argdump.dumps(parser_to_serialize, indent=indent, prog=prog)
3740
output_path.parent.mkdir(parents=True, exist_ok=True)
3841
output_path.write_text(f"{serialized_parser}\n")
3942

@@ -58,13 +61,18 @@ def build_parser() -> argparse.ArgumentParser:
5861
default=DEFAULT_INDENT,
5962
help="JSON indentation. Default: %(default)s",
6063
)
64+
parser.add_argument(
65+
"--prog",
66+
default=None,
67+
help="Override the program name.",
68+
)
6169
return parser
6270

6371

6472
def main():
6573
parser = build_parser()
6674
args = parser.parse_args()
67-
run_argdump(args.location, args.output_path, indent=args.indent)
75+
run_argdump(args.location, args.output_path, indent=args.indent, prog=args.prog)
6876

6977

7078
if __name__ == "__main__":

‎tests/test_create_descriptor.py‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ def _base_env(tmp_path: Path) -> dict[str, str]:
1515
env["PYTHONPATH"] = str(REPO_ROOT / "tests")
1616
env["PARSER_TYPE"] = "argparse"
1717
env["PARSER_LOCATION"] = "conftest:make_parser"
18+
env["PROG_NAME"] = ""
1819
env["DUMP_FILE"] = "dump.json"
1920
env["OUTPUT_PATH"] = str(tmp_path / "desc.json")
20-
env["CLICK_PROG_NAME"] = ""
2121
env["CLICK_PARENT_LOCATION"] = ""
2222
env["EXCLUDE_VERSION"] = "false"
2323
env["UPDATES_FILE"] = ""
@@ -56,6 +56,19 @@ def test_does_not_use_clickdump(self, tmp_path):
5656
result = _run(tmp_path)
5757
assert "run_clickdump" not in result.stderr
5858

59+
def test_custom_args(self, tmp_path):
60+
result = _run(
61+
tmp_path,
62+
{
63+
"PROG_NAME": "custom_name",
64+
},
65+
)
66+
assert result.returncode == 0
67+
out_file = tmp_path / "desc.json"
68+
assert out_file.exists()
69+
data = json.loads(out_file.read_text())
70+
assert data["name"] == "custom_name"
71+
5972

6073
class TestClickFlow:
6174
def test_creates_output_file(self, tmp_path):
@@ -77,7 +90,7 @@ def test_with_prog(self, tmp_path):
7790
{
7891
"PARSER_TYPE": "click",
7992
"PARSER_LOCATION": "conftest:test_cli",
80-
"CLICK_PROG_NAME": "myprog",
93+
"PROG_NAME": "myprog",
8194
},
8295
)
8396
assert result.returncode == 0

‎tests/test_run_argdump.py‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ def test_custom_indent(self, parser, _make_fake_module, tmp_path):
5757
text = out.read_text()
5858
assert " " in text
5959

60+
@pytest.mark.parametrize("prog", ["custom_name1", "custom_name2"])
61+
def test_with_prog_override(self, parser, _make_fake_module, tmp_path, prog):
62+
_make_fake_module("dump_mod4", "make_parser", lambda: parser)
63+
out = tmp_path / "out.json"
64+
run_argdump("dump_mod4:make_parser", out, prog=prog)
65+
data = json.loads(out.read_text())
66+
assert data["prog"] == prog
67+
6068

6169
class TestBuildParser:
6270
def test_returns_argument_parser(self):
@@ -68,9 +76,21 @@ def test_defaults(self):
6876
args = parser.parse_args(["mymod:myfunc"])
6977
assert args.output_path == Path("argdump.json")
7078
assert args.indent == 2
79+
assert args.prog is None
7180

72-
def test_custom_output_and_indent(self):
81+
def test_custom_args(self):
7382
parser = build_parser()
74-
args = parser.parse_args(["mymod:myfunc", "-o", "custom.json", "--indent", "4"])
83+
args = parser.parse_args(
84+
[
85+
"mymod:myfunc",
86+
"-o",
87+
"custom.json",
88+
"--indent",
89+
"4",
90+
"--prog",
91+
"custom_name",
92+
]
93+
)
7594
assert args.output_path == Path("custom.json")
7695
assert args.indent == 4
96+
assert args.prog == "custom_name"

0 commit comments

Comments
 (0)