33from __future__ import annotations
44
55import operator
6+ import os .path
67import re
78from copy import copy
89from 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
0 commit comments