From 68cbf4ee3913634886b927c8af61ecac6e81ec90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Thu, 27 Aug 2026 08:16:16 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20give=20untitled=20argumen?= =?UTF-8?q?t=20groups=20a=20heading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 2 ++ roots/test-group-untitled/conf.py | 8 ++++++++ roots/test-group-untitled/index.rst | 3 +++ roots/test-group-untitled/parser.py | 11 +++++++++++ src/sphinx_argparse_cli/_logic.py | 14 +++++++------- tests/test_logic.py | 13 +++++++++++++ 6 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 roots/test-group-untitled/conf.py create mode 100644 roots/test-group-untitled/index.rst create mode 100644 roots/test-group-untitled/parser.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a48d35..ead2ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ All notable changes to this project will be documented in this file. - Make `:hook:` intercept `parse_intermixed_args()` as well as `parse_args()`. - Keep ANSI color codes out of usage blocks when `PYTHON_COLORS=1` is set on Python 3.14 or newer. - Skip sub-commands added with `help=argparse.SUPPRESS` instead of rendering them with a `==SUPPRESS==` description. +- Use the description, or `arguments`, as the heading of an argument group without a title instead of rendering `None` + and emitting duplicate label warnings. ## 1.13.1 diff --git a/roots/test-group-untitled/conf.py b/roots/test-group-untitled/conf.py new file mode 100644 index 0000000..9f2a54a --- /dev/null +++ b/roots/test-group-untitled/conf.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +extensions = ["sphinx_argparse_cli"] +nitpicky = True diff --git a/roots/test-group-untitled/index.rst b/roots/test-group-untitled/index.rst new file mode 100644 index 0000000..708ad9c --- /dev/null +++ b/roots/test-group-untitled/index.rst @@ -0,0 +1,3 @@ +.. sphinx_argparse_cli:: + :module: parser + :func: make diff --git a/roots/test-group-untitled/parser.py b/roots/test-group-untitled/parser.py new file mode 100644 index 0000000..2f8e14c --- /dev/null +++ b/roots/test-group-untitled/parser.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from argparse import ArgumentParser + + +def make() -> ArgumentParser: + parser = ArgumentParser(prog="tool") + parser.add_argument_group(description="only a description").add_argument("--x", help="x help") + parser.add_argument_group(description="another").add_argument("--y", help="y help") + parser.add_argument_group().add_argument("--z", help="z help") + return parser diff --git a/src/sphinx_argparse_cli/_logic.py b/src/sphinx_argparse_cli/_logic.py index 0ec4a6d..6a5dbc5 100644 --- a/src/sphinx_argparse_cli/_logic.py +++ b/src/sphinx_argparse_cli/_logic.py @@ -211,13 +211,15 @@ def _pre_format(self, block: str | None) -> paragraph | literal_block | None: def _mk_option_group(self, group: _ArgumentGroup, prefix: str, prog: str) -> section: sub_title_prefix: str = self.options.get("group_sub_title_prefix") title_prefix = self.options.get("group_title_prefix") - title_text = self._build_opt_grp_title(group, prefix, prog, sub_title_prefix, title_prefix) - title_ref: str = f"{prefix}{' ' if prefix else ''}{group.title}" + # an untitled group borrows its description as heading so its anchor stays unique + group_title = group.title or group.description or "arguments" + title_text = self._build_opt_grp_title(group_title, prefix, prog, sub_title_prefix, title_prefix) + title_ref: str = f"{prefix}{' ' if prefix else ''}{group_title}" ref_id = self._make_id(title_ref) # the text sadly needs to be prefixed, because otherwise the autosectionlabel will conflict header = title("", Text(title_text)) group_section = section("", header, ids=[ref_id], names=[ref_id]) - if description := self._pre_format(group.description): + if group.title and (description := self._pre_format(group.description)): group_section += description self._register_ref(ref_id, title_text, group_section) opt_group = bullet_list() @@ -230,12 +232,10 @@ def _mk_option_group(self, group: _ArgumentGroup, prefix: str, prog: str) -> sec return group_section def _build_opt_grp_title( - self, group: _ArgumentGroup, prefix: str, prog: str, sub_title_prefix: str, title_prefix: str + self, group_title: str, prefix: str, prog: str, sub_title_prefix: str, title_prefix: str ) -> str: sub_cmd = prefix[len(prog) :].strip() or None if prefix != prog else None - title_text = self._resolve_prefix(prog, sub_cmd, prefix, title_prefix, sub_title_prefix) - title_text += group.title or "" - return title_text + return self._resolve_prefix(prog, sub_cmd, prefix, title_prefix, sub_title_prefix) + group_title def _mk_option_line(self, action: Action, prefix: str) -> list_item: line = paragraph() diff --git a/tests/test_logic.py b/tests/test_logic.py index 860af4f..2353fd3 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -323,6 +323,19 @@ def test_usage_ignores_python_colors(build_outcome: str) -> None: ) +@pytest.mark.sphinx(buildername="html", testroot="group-untitled") +def test_group_untitled(build_outcome: str, warning: StringIO) -> None: + headings = re.findall(r'

(.*?)only a description

" not in build_outcome + assert not warning.getvalue() + + @pytest.mark.sphinx(buildername="text", testroot="ref-duplicate-label") def test_ref_duplicate_label(build_outcome: tuple[str, str], warning: StringIO) -> None: assert build_outcome