Skip to content

Commit 77c50a5

Browse files
committed
Fix duplicate label warnings for included files (#14413)
1 parent cc7c6f4 commit 77c50a5

11 files changed

Lines changed: 187 additions & 6 deletions

File tree

sphinx/domains/std/__init__.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import operator
6+
import os.path
67
import re
78
from copy import copy
89
from typing import TYPE_CHECKING, cast
@@ -778,6 +779,7 @@ class StandardDomain(Domain):
778779
'modindex': ('py-modindex', ''),
779780
'search': ('search', ''),
780781
},
782+
'labels_source': {}, # labelname -> source file path
781783
}
782784

783785
# labelname -> docname, sectionname
@@ -893,6 +895,10 @@ def labels(self) -> dict[str, tuple[str, str, str]]:
893895
def anonlabels(self) -> dict[str, tuple[str, str]]:
894896
return self.data.setdefault('anonlabels', {}) # labelname -> docname, labelid
895897

898+
@property
899+
def labels_source(self) -> dict[str, str]:
900+
return self.data.setdefault('labels_source', {}) # labelname -> source file path
901+
896902
def clear_doc(self, docname: str) -> None:
897903
to_remove1 = [
898904
key for key, (fn, _l) in self.progoptions.items() if fn == docname
@@ -911,6 +917,7 @@ def clear_doc(self, docname: str) -> None:
911917
to_remove3 = [key for key, (fn, _l, _l) in self.labels.items() if fn == docname]
912918
for key3 in to_remove3:
913919
del self.labels[key3]
920+
self.labels_source.pop(key3, None)
914921

915922
to_remove3 = [key for key, (fn, _l) in self.anonlabels.items() if fn == docname]
916923
for key3 in to_remove3:
@@ -933,6 +940,9 @@ def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> Non
933940
for key, data in otherdata['anonlabels'].items():
934941
if data[0] in docnames:
935942
self.anonlabels[key] = data
943+
for key, data in otherdata.get('labels_source', {}).items():
944+
if key in self.labels:
945+
self.labels_source[key] = data
936946

937947
def process_doc(
938948
self, env: BuildEnvironment, docname: str, document: nodes.document
@@ -957,12 +967,30 @@ def process_doc(
957967
# link and object descriptions
958968
continue
959969
if name in self.labels:
960-
logger.warning(
961-
__('duplicate label %s, other instance in %s'),
962-
name,
963-
env.doc2path(self.labels[name][0]),
964-
location=node,
965-
)
970+
current_source = getattr(node, 'source', None)
971+
existing_source = self.labels_source.get(name)
972+
existing_docname = self.labels[name][0]
973+
974+
# Check if both labels come from the same source file
975+
if current_source and existing_source:
976+
current_norm = os.path.normcase(os.path.normpath(current_source))
977+
existing_norm = os.path.normcase(os.path.normpath(existing_source))
978+
same_source = current_norm == existing_norm
979+
else:
980+
same_source = False
981+
982+
if same_source:
983+
# Same source file included in multiple documents.
984+
# Prefer the including document for proper figure numbering.
985+
if existing_docname not in env.included.get(docname, set()):
986+
continue
987+
else:
988+
logger.warning(
989+
__('duplicate label %s, other instance in %s'),
990+
name,
991+
env.doc2path(existing_docname),
992+
location=node,
993+
)
966994
self.anonlabels[name] = docname, labelid
967995
if node.tagname == 'section':
968996
title = cast('nodes.title', node[0])
@@ -991,6 +1019,9 @@ def process_doc(
9911019
# anonymous-only labels
9921020
continue
9931021
self.labels[name] = docname, labelid, sectname
1022+
node_source = getattr(node, 'source', None)
1023+
if node_source:
1024+
self.labels_source[name] = node_source
9941025

9951026
def add_program_option(
9961027
self, program: str | None, name: str, docname: str, labelid: str
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
project = 'test-numfig-include-duplicate'
2+
exclude_patterns = ['_build']
3+
numfig = True
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Document 1
2+
==========
3+
4+
.. _shared-label:
5+
6+
.. figure:: img.png
7+
8+
Figure in doc1
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Document 2
2+
==========
3+
4+
.. _shared-label:
5+
6+
.. figure:: img.png
7+
8+
Figure in doc2 (DUPLICATE label from different source file)
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Test duplicate labels across documents
2+
======================================
3+
4+
.. toctree::
5+
6+
doc1
7+
doc2
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.. _included-figure:
2+
3+
.. figure:: /img.png
4+
5+
This is an included figure
6+
7+
See :numref:`included-figure` for the figure above.
8+
9+
.. _included-figure-2:
10+
11+
.. figure:: /img.png
12+
13+
This is another included figure
14+
15+
See :numref:`included-figure-2` for figure 2.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
project = 'test-numfig-include'
2+
exclude_patterns = ['_build'] # NOT excluding _includes to test include-aware duplicate handling
3+
numfig = True
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Test numfig with include
2+
========================
3+
4+
This document tests that :numref: works with figures in included files.
5+
6+
.. include:: _includes/figures.rst
7+
8+
Reference to included figure: :numref:`included-figure`
9+
10+
Reference to local figure: :numref:`local-figure`
11+
12+
.. _local-figure:
13+
14+
.. figure:: img.png
15+
16+
This is a local figure

0 commit comments

Comments
 (0)