Skip to content

Commit 7e90655

Browse files
committed
🐛 fix: give untitled argument groups a heading
An argument group created with only a description rendered its heading as the program name followed by nothing, because the title was None, and every such group shared the label tool-None; two of them raised a duplicate label warning that fails builds run with -W. The description now serves as the heading when the title is missing, and a group with neither falls back to "arguments", so each section keeps a distinct, stable anchor. Titled groups are untouched.
1 parent 786fd11 commit 7e90655

6 files changed

Lines changed: 44 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file.
1313
literals.
1414
- Make `:hook:` intercept `parse_intermixed_args()` as well as `parse_args()`.
1515
- Keep ANSI color codes out of usage blocks when `PYTHON_COLORS=1` is set on Python 3.14 or newer.
16+
- Use the description, or `arguments`, as the heading of an argument group without a title instead of rendering `None`
17+
and emitting duplicate label warnings.
1618

1719
## 1.13.1
1820

roots/test-group-untitled/conf.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
from pathlib import Path
5+
6+
sys.path.insert(0, str(Path(__file__).parent))
7+
extensions = ["sphinx_argparse_cli"]
8+
nitpicky = True
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.. sphinx_argparse_cli::
2+
:module: parser
3+
:func: make
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from __future__ import annotations
2+
3+
from argparse import ArgumentParser
4+
5+
6+
def make() -> ArgumentParser:
7+
parser = ArgumentParser(prog="tool")
8+
parser.add_argument_group(description="only a description").add_argument("--x", help="x help")
9+
parser.add_argument_group(description="another").add_argument("--y", help="y help")
10+
parser.add_argument_group().add_argument("--z", help="z help")
11+
return parser

src/sphinx_argparse_cli/_logic.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,15 @@ def _pre_format(self, block: str | None) -> paragraph | literal_block | None:
209209
def _mk_option_group(self, group: _ArgumentGroup, prefix: str, prog: str) -> section:
210210
sub_title_prefix: str = self.options.get("group_sub_title_prefix")
211211
title_prefix = self.options.get("group_title_prefix")
212-
title_text = self._build_opt_grp_title(group, prefix, prog, sub_title_prefix, title_prefix)
213-
title_ref: str = f"{prefix}{' ' if prefix else ''}{group.title}"
212+
# an untitled group borrows its description as heading so its anchor stays unique
213+
group_title = group.title or group.description or "arguments"
214+
title_text = self._build_opt_grp_title(group_title, prefix, prog, sub_title_prefix, title_prefix)
215+
title_ref: str = f"{prefix}{' ' if prefix else ''}{group_title}"
214216
ref_id = self._make_id(title_ref)
215217
# the text sadly needs to be prefixed, because otherwise the autosectionlabel will conflict
216218
header = title("", Text(title_text))
217219
group_section = section("", header, ids=[ref_id], names=[ref_id])
218-
if description := self._pre_format(group.description):
220+
if group.title and (description := self._pre_format(group.description)):
219221
group_section += description
220222
self._register_ref(ref_id, title_text, group_section)
221223
opt_group = bullet_list()
@@ -228,12 +230,10 @@ def _mk_option_group(self, group: _ArgumentGroup, prefix: str, prog: str) -> sec
228230
return group_section
229231

230232
def _build_opt_grp_title(
231-
self, group: _ArgumentGroup, prefix: str, prog: str, sub_title_prefix: str, title_prefix: str
233+
self, group_title: str, prefix: str, prog: str, sub_title_prefix: str, title_prefix: str
232234
) -> str:
233235
sub_cmd = prefix[len(prog) :].strip() or None if prefix != prog else None
234-
title_text = self._resolve_prefix(prog, sub_cmd, prefix, title_prefix, sub_title_prefix)
235-
title_text += group.title or ""
236-
return title_text
236+
return self._resolve_prefix(prog, sub_cmd, prefix, title_prefix, sub_title_prefix) + group_title
237237

238238
def _mk_option_line(self, action: Action, prefix: str) -> list_item:
239239
line = paragraph()

tests/test_logic.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,19 @@ def test_usage_ignores_python_colors(build_outcome: str) -> None:
303303
)
304304

305305

306+
@pytest.mark.sphinx(buildername="html", testroot="group-untitled")
307+
def test_group_untitled(build_outcome: str, warning: StringIO) -> None:
308+
headings = re.findall(r'<h2>(.*?)<a class="headerlink" href="(#[^"]+)"', build_outcome)
309+
assert headings == [
310+
("tool options", "#tool-options"),
311+
("tool only a description", "#tool-only-a-description"),
312+
("tool another", "#tool-another"),
313+
("tool arguments", "#tool-arguments"),
314+
]
315+
assert "<p>only a description</p>" not in build_outcome
316+
assert not warning.getvalue()
317+
318+
306319
@pytest.mark.sphinx(buildername="text", testroot="ref-duplicate-label")
307320
def test_ref_duplicate_label(build_outcome: tuple[str, str], warning: StringIO) -> None:
308321
assert build_outcome

0 commit comments

Comments
 (0)