Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LaTeX: Fix 'multiply-defined references' PDF build warning when one or more reST labels directly precede a py:module or automodule directive #11333

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES
Expand Up @@ -75,6 +75,9 @@ Bugs fixed

* #11079: LaTeX: figures with align attribute may disappear and strangely impact
following lists
* #11093: LaTeX: fix "multiply-defined references" PDF build warnings when one or
more reST labels directly precede an :rst:dir:`py:module` or :rst:dir:`automodule`
picnixz marked this conversation as resolved.
Show resolved Hide resolved
directive. Patch by Bénédikt Tran (picnixz)
* #11110: LaTeX: Figures go missing from latex pdf if their files have the same
base name and they use a post transform. Patch by aaron-cooper
* LaTeX: fix potential color leak from shadow to border of rounded boxes, if
Expand Down
21 changes: 20 additions & 1 deletion sphinx/writers/latex.py
Expand Up @@ -1499,7 +1499,26 @@ def add_target(id: str) -> None:
pass
else:
add_target(node['refid'])
for id in node['ids']:
# Temporary fix for https://github.com/sphinx-doc/sphinx/issues/11093
# TODO: investigate if a more elegant solution exists (see comments of #11093)
if node.get('ismod', False):
# Detect if the previous nodes are label targets. If so, remove
# the refid thereof from node['ids'] to avoid duplicated ids.
def has_dup_label(sib: Node | None) -> bool:
return isinstance(sib, nodes.target) and sib.get('refid') in node['ids']

prev = get_prev_node(node)
if has_dup_label(prev):
ids = node['ids'][:] # copy to avoid side-effects
while has_dup_label(prev):
ids.remove(prev['refid']) # type: ignore
prev = get_prev_node(prev)
else:
ids = iter(node['ids']) # read-only iterator
else:
ids = iter(node['ids']) # read-only iterator

for id in ids:
add_target(id)

def depart_target(self, node: Element) -> None:
Expand Down
2 changes: 2 additions & 0 deletions tests/roots/test-latex-labels-before-module/automodule1.py
@@ -0,0 +1,2 @@
"""docstring"""

2 changes: 2 additions & 0 deletions tests/roots/test-latex-labels-before-module/automodule2a.py
@@ -0,0 +1,2 @@
"""docstring"""

2 changes: 2 additions & 0 deletions tests/roots/test-latex-labels-before-module/automodule2b.py
@@ -0,0 +1,2 @@
"""docstring"""

2 changes: 2 additions & 0 deletions tests/roots/test-latex-labels-before-module/automodule3.py
@@ -0,0 +1,2 @@
"""docstring"""

8 changes: 8 additions & 0 deletions tests/roots/test-latex-labels-before-module/conf.py
@@ -0,0 +1,8 @@
import os
import sys

sys.path.insert(0, os.path.abspath('.'))

extensions = ['sphinx.ext.autodoc']

nitpicky = True
48 changes: 48 additions & 0 deletions tests/roots/test-latex-labels-before-module/index.rst
@@ -0,0 +1,48 @@
latex-labels-before-module
==========================

.. _label_1a:
.. _label_1b:

.. module:: module1

text

.. _label_2:

.. module:: module2a

text

.. module:: module2b

text

.. _label_3:

.. module:: module3

text

.. _label_auto_1a:
.. _label_auto_1b:

.. automodule:: automodule1

text

.. _label_auto_2:

.. automodule:: automodule2a

text

.. automodule:: automodule2b

text

.. _label_auto_3:

.. automodule:: automodule3

text
30 changes: 30 additions & 0 deletions tests/test_build_latex.py
Expand Up @@ -1708,3 +1708,33 @@ def test_copy_images(app, status, warning):
'rimg.png',
'testimäge.png',
}


@pytest.mark.sphinx('latex', testroot='latex-labels-before-module')
def test_duplicated_labels_before_module(app, status, warning):
app.build()
content: str = (app.outdir / 'python.tex').read_text()

def count_label(name):
text = r'\phantomsection\label{\detokenize{%s}}' % name
return content.count(text)

pattern = r'\\phantomsection\\label\{\\detokenize\{index:label-(?:auto-)?\d+[a-z]*}}'
# labels found in the TeX output
output_labels = frozenset(match.group() for match in re.finditer(pattern, content))
# labels that have been tested and occurring exactly once in the output
tested_labels = set()

# iterate over the (explicit) labels in the corresponding index.rst
for rst_label_name in [
'label_1a', 'label_1b', 'label_2', 'label_3',
'label_auto_1a', 'label_auto_1b', 'label_auto_2', 'label_auto_3',
]:
tex_label_name = 'index:' + rst_label_name.replace('_', '-')
tex_label_code = r'\phantomsection\label{\detokenize{%s}}' % tex_label_name
assert content.count(tex_label_code) == 1, f'duplicated label: {tex_label_name!r}'
tested_labels.add(tex_label_code)

# ensure that we did not forget any label to check
# and if so, report them nicely in case of failure
assert sorted(tested_labels) == sorted(output_labels)