Summary
exemplar.make_section_id in sphinx-argparse-neo derives the example section ID from the term text (e.g. "add examples:" → "add-examples") and only falls back to the caller-supplied page_prefix when the term-derived prefix is empty. In multi-page docs where the same subcommand name appears in more than one page's parser epilog — typically a top-level index page rendering .. argparse:: :nosubcommands: alongside leaf pages rendering .. argparse:: :path: <sub> — both pages emit the same section ID, docutils promotes the name to an explicit cross-document target (via MyST {eval-rst}), and Sphinx's std domain logs duplicate label <sub>-examples.
Relationship to #15 / PR #16
#15 fixed render_usage_section, render_group_section, and the top of _create_example_section so that section["names"] mirrors section["ids"] with the correct id_prefix. That eliminated 58 of the 64 duplicate-label warnings in vcspull's api-styling docs (-91%, verified via editable install of PR #16). This issue tracks the remaining 6 warnings, which come from a separate mechanism inside make_section_id that #16 intentionally did not touch.
Reproduction
On vcspull with sphinx-argparse-neo built from the vcspull-fixes branch of this repo (PR #16), build the docs:
$ cd ~/work/python/vcspull
$ uv run sphinx-build -a -E -b html docs docs/_build/html 2>&1 | grep "duplicate label"
The remaining warnings form a clean pattern:
docs/cli/index.md: WARNING: duplicate label add-examples, other instance in docs/cli/add.md
docs/cli/index.md: WARNING: duplicate label discover-examples, other instance in docs/cli/discover.md
docs/cli/index.md: WARNING: duplicate label fmt-examples, other instance in docs/cli/fmt.md
docs/cli/list.md: WARNING: duplicate label list-examples, other instance in docs/cli/index.md
docs/cli/search.md: WARNING: duplicate label search-examples, other instance in docs/cli/index.md
docs/cli/sync.md: WARNING: duplicate label sync-examples, other instance in docs/cli/index.md
Every warning is a <subcommand>-examples collision between a leaf CLI page and the aggregate index page.
Root cause
packages/sphinx-argparse-neo/src/sphinx_argparse_neo/exemplar.py:405-421:
# Extract prefix before the term suffix (e.g., "Machine-readable output")
lower_text = term_text.lower().rstrip(":")
if term_suffix in lower_text:
prefix = lower_text.rsplit(term_suffix, 1)[0].strip()
# Remove trailing colon from prefix (handles ": examples" pattern)
prefix = prefix.rstrip(":").strip()
if prefix:
normalized_prefix = prefix.replace(" ", "-")
if is_subsection:
section_id = normalized_prefix
else:
section_id = f"{normalized_prefix}-{term_suffix}"
else:
# Plain "examples" - add page prefix if provided for uniqueness
section_id = f"{page_prefix}-{term_suffix}" if page_prefix else term_suffix
else:
section_id = term_suffix
Two observations:
- When the term text contains a prefix (e.g.
"add examples:"), the function uses it and ignores page_prefix entirely.
- The "plain
examples:" case is the only one where page_prefix contributes to section_id.
This means two pages whose argparse directives both surface a "<sub> examples:" term (which is natural for an index page aggregating all subcommands + the leaf page showing its own subcommand) both produce the same section_id and therefore collide on the implicit target that docutils wires into section["names"].
CleanArgParseDirective.run() at exemplar.py:1208-1214 already computes page_prefix = docname.split("/")[-1] and threads it through process_node → _create_example_section → make_section_id, but the value is only consulted in the no-term-prefix branch.
Proposed fix direction
Make page_prefix authoritative when it's non-empty. Two variants, picking between them is a design call:
(a) Always prepend page_prefix when present
if prefix:
normalized_prefix = prefix.replace(" ", "-")
base = normalized_prefix if is_subsection else f"{normalized_prefix}-{term_suffix}"
section_id = f"{page_prefix}-{base}" if page_prefix else base
Pros: preserves the term-derived prefix in the final ID, so existing cross-references like :ref:add-examples can be migrated mechanically to `:ref:`cli-add-add-examples. Cons: IDs become verbose (cli-add-add-examples), and the term prefix duplicates information already in page_prefix when the page and the term are named the same.
(b) Prefer page_prefix over the term-derived prefix entirely
if prefix:
normalized_prefix = prefix.replace(" ", "-")
effective_prefix = page_prefix or normalized_prefix
section_id = effective_prefix if is_subsection else f"{effective_prefix}-{term_suffix}"
Pros: IDs stay short (add-examples) and are guaranteed unique across pages. Cons: when two leaf pages contain the same term texts with different subcommand names (unlikely but possible), the page-prefix alone might not be expressive enough — but in practice the page name itself already encodes the subcommand.
I'd lean (b) if the plan is still to drop sphinx.ext.napoleon / sphinx_autodoc_typehints from gp-sphinx's default stack, because (b) is the minimal-surface change and produces the same IDs users are already seeing under the common case.
Test coverage requested
A new regression test in the same spirit as tests/ext/argparse_neo/test_multi_page_integration.py, but with a parser whose top-level epilog contains example definition terms naming individual subcommands — mirroring vcspull's pattern. The test should fail on main at tip (post-#16) with three duplicate-label warnings and pass under the proposed fix.
Downstream impact
Same 13 consumers listed in #15. Vcspull is the one where this has been empirically measured; the others likely hit the same pattern whenever their index/aggregate CLI page aggregates subcommands that are also documented on their own leaf pages.
Scope comparison (for this issue only)
Fresh sphinx-build -a -E on vcspull api-styling:
| gp-sphinx source |
duplicate label |
published 0.0.1a6 |
64 |
vcspull-fixes / PR #16 editable |
6 |
| hypothetical fix for this issue on top of #16 |
0 |
PR #16 already does the bulk of the work; this issue is the mop-up.
Summary
exemplar.make_section_idinsphinx-argparse-neoderives the example section ID from the term text (e.g."add examples:"→"add-examples") and only falls back to the caller-suppliedpage_prefixwhen the term-derived prefix is empty. In multi-page docs where the same subcommand name appears in more than one page's parser epilog — typically a top-level index page rendering.. argparse:: :nosubcommands:alongside leaf pages rendering.. argparse:: :path: <sub>— both pages emit the same section ID, docutils promotes the name to an explicit cross-document target (via MyST{eval-rst}), and Sphinx's std domain logsduplicate label <sub>-examples.Relationship to #15 / PR #16
#15 fixed
render_usage_section,render_group_section, and the top of_create_example_sectionso thatsection["names"]mirrorssection["ids"]with the correctid_prefix. That eliminated 58 of the 64 duplicate-label warnings in vcspull's api-styling docs (-91%, verified via editable install of PR #16). This issue tracks the remaining 6 warnings, which come from a separate mechanism insidemake_section_idthat #16 intentionally did not touch.Reproduction
On vcspull with
sphinx-argparse-neobuilt from thevcspull-fixesbranch of this repo (PR #16), build the docs:The remaining warnings form a clean pattern:
Every warning is a
<subcommand>-examplescollision between a leaf CLI page and the aggregate index page.Root cause
packages/sphinx-argparse-neo/src/sphinx_argparse_neo/exemplar.py:405-421:Two observations:
"add examples:"), the function uses it and ignorespage_prefixentirely.examples:" case is the only one wherepage_prefixcontributes tosection_id.This means two pages whose argparse directives both surface a
"<sub> examples:"term (which is natural for an index page aggregating all subcommands + the leaf page showing its own subcommand) both produce the samesection_idand therefore collide on the implicit target that docutils wires intosection["names"].CleanArgParseDirective.run()atexemplar.py:1208-1214already computespage_prefix = docname.split("/")[-1]and threads it throughprocess_node→_create_example_section→make_section_id, but the value is only consulted in the no-term-prefix branch.Proposed fix direction
Make
page_prefixauthoritative when it's non-empty. Two variants, picking between them is a design call:(a) Always prepend
page_prefixwhen presentPros: preserves the term-derived prefix in the final ID, so existing cross-references like
:ref:add-examplescan be migrated mechanically to `:ref:`cli-add-add-examples. Cons: IDs become verbose (cli-add-add-examples), and the term prefix duplicates information already inpage_prefixwhen the page and the term are named the same.(b) Prefer
page_prefixover the term-derived prefix entirelyPros: IDs stay short (
add-examples) and are guaranteed unique across pages. Cons: when two leaf pages contain the same term texts with different subcommand names (unlikely but possible), the page-prefix alone might not be expressive enough — but in practice the page name itself already encodes the subcommand.I'd lean (b) if the plan is still to drop
sphinx.ext.napoleon/sphinx_autodoc_typehintsfrom gp-sphinx's default stack, because (b) is the minimal-surface change and produces the same IDs users are already seeing under the common case.Test coverage requested
A new regression test in the same spirit as
tests/ext/argparse_neo/test_multi_page_integration.py, but with a parser whose top-level epilog contains example definition terms naming individual subcommands — mirroring vcspull's pattern. The test should fail onmainat tip (post-#16) with three duplicate-label warnings and pass under the proposed fix.Downstream impact
Same 13 consumers listed in #15. Vcspull is the one where this has been empirically measured; the others likely hit the same pattern whenever their index/aggregate CLI page aggregates subcommands that are also documented on their own leaf pages.
Scope comparison (for this issue only)
Fresh
sphinx-build -a -Eon vcspull api-styling:0.0.1a6vcspull-fixes/ PR #16 editablePR #16 already does the bulk of the work; this issue is the mop-up.