From 61576516d4ed6c4e10d38f959e8625201abcc7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 17 Apr 2023 12:07:32 +0200 Subject: [PATCH 1/2] Fix duplicated labels in TeX output (#11093) --- sphinx/writers/latex.py | 21 +++++++- .../automodule1.py | 2 + .../automodule2a.py | 2 + .../automodule2b.py | 2 + .../automodule3.py | 2 + .../test-latex-labels-before-module/conf.py | 8 ++++ .../test-latex-labels-before-module/index.rst | 48 +++++++++++++++++++ tests/test_build_latex.py | 27 +++++++++++ 8 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 tests/roots/test-latex-labels-before-module/automodule1.py create mode 100644 tests/roots/test-latex-labels-before-module/automodule2a.py create mode 100644 tests/roots/test-latex-labels-before-module/automodule2b.py create mode 100644 tests/roots/test-latex-labels-before-module/automodule3.py create mode 100644 tests/roots/test-latex-labels-before-module/conf.py create mode 100644 tests/roots/test-latex-labels-before-module/index.rst diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 215fd4c765e..684164a1f7d 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -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: Element | None) -> bool: + return isinstance(sib, nodes.target) and sib.get('refid') in node['ids'] + + prev: Element | None = 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']) + 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: diff --git a/tests/roots/test-latex-labels-before-module/automodule1.py b/tests/roots/test-latex-labels-before-module/automodule1.py new file mode 100644 index 00000000000..0545aa42705 --- /dev/null +++ b/tests/roots/test-latex-labels-before-module/automodule1.py @@ -0,0 +1,2 @@ +"""docstring""" + diff --git a/tests/roots/test-latex-labels-before-module/automodule2a.py b/tests/roots/test-latex-labels-before-module/automodule2a.py new file mode 100644 index 00000000000..0545aa42705 --- /dev/null +++ b/tests/roots/test-latex-labels-before-module/automodule2a.py @@ -0,0 +1,2 @@ +"""docstring""" + diff --git a/tests/roots/test-latex-labels-before-module/automodule2b.py b/tests/roots/test-latex-labels-before-module/automodule2b.py new file mode 100644 index 00000000000..0545aa42705 --- /dev/null +++ b/tests/roots/test-latex-labels-before-module/automodule2b.py @@ -0,0 +1,2 @@ +"""docstring""" + diff --git a/tests/roots/test-latex-labels-before-module/automodule3.py b/tests/roots/test-latex-labels-before-module/automodule3.py new file mode 100644 index 00000000000..0545aa42705 --- /dev/null +++ b/tests/roots/test-latex-labels-before-module/automodule3.py @@ -0,0 +1,2 @@ +"""docstring""" + diff --git a/tests/roots/test-latex-labels-before-module/conf.py b/tests/roots/test-latex-labels-before-module/conf.py new file mode 100644 index 00000000000..25193b0ddb7 --- /dev/null +++ b/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 diff --git a/tests/roots/test-latex-labels-before-module/index.rst b/tests/roots/test-latex-labels-before-module/index.rst new file mode 100644 index 00000000000..e6df749223e --- /dev/null +++ b/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 diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 7fb59dbc983..de45f5f0c5e 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -1708,3 +1708,30 @@ 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]*}}' + expect_labels = {match.group() for match in re.finditer(pattern, content)} + result_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}' + result_labels.add(tex_label_code) + + # sort the labels for a better visual diff, if any + assert sorted(result_labels) == sorted(expect_labels) From e2f66cea4997b6d8c588d3509adb68d4e9108ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 17 Apr 2023 12:13:29 +0200 Subject: [PATCH 2/2] Update CHANGES for PR #11333 --- CHANGES | 3 +++ sphinx/writers/latex.py | 6 +++--- tests/test_build_latex.py | 17 ++++++++++------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CHANGES b/CHANGES index e7099805500..da414f1e2fe 100644 --- a/CHANGES +++ b/CHANGES @@ -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` + 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 diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 684164a1f7d..efb84735398 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -1504,14 +1504,14 @@ def add_target(id: str) -> None: 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: Element | None) -> bool: + def has_dup_label(sib: Node | None) -> bool: return isinstance(sib, nodes.target) and sib.get('refid') in node['ids'] - prev: Element | None = get_prev_node(node) + 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']) + ids.remove(prev['refid']) # type: ignore prev = get_prev_node(prev) else: ids = iter(node['ids']) # read-only iterator diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index de45f5f0c5e..faf1a47b70b 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -1720,18 +1720,21 @@ def count_label(name): return content.count(text) pattern = r'\\phantomsection\\label\{\\detokenize\{index:label-(?:auto-)?\d+[a-z]*}}' - expect_labels = {match.group() for match in re.finditer(pattern, content)} - result_labels = set() + # 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 { + 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}' - result_labels.add(tex_label_code) + tested_labels.add(tex_label_code) - # sort the labels for a better visual diff, if any - assert sorted(result_labels) == sorted(expect_labels) + # 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)