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,12 @@ 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 (
901+ 'labels_source' , {}
902+ ) # labelname -> source file path
903+
896904 def clear_doc (self , docname : str ) -> None :
897905 to_remove1 = [
898906 key for key , (fn , _l ) in self .progoptions .items () if fn == docname
@@ -911,6 +919,7 @@ def clear_doc(self, docname: str) -> None:
911919 to_remove3 = [key for key , (fn , _l , _l ) in self .labels .items () if fn == docname ]
912920 for key3 in to_remove3 :
913921 del self .labels [key3 ]
922+ self .labels_source .pop (key3 , None )
914923
915924 to_remove3 = [key for key , (fn , _l ) in self .anonlabels .items () if fn == docname ]
916925 for key3 in to_remove3 :
@@ -933,6 +942,9 @@ def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> Non
933942 for key , data in otherdata ['anonlabels' ].items ():
934943 if data [0 ] in docnames :
935944 self .anonlabels [key ] = data
945+ for key , data in otherdata .get ('labels_source' , {}).items ():
946+ if key in self .labels :
947+ self .labels_source [key ] = data
936948
937949 def process_doc (
938950 self , env : BuildEnvironment , docname : str , document : nodes .document
@@ -957,12 +969,30 @@ def process_doc(
957969 # link and object descriptions
958970 continue
959971 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- )
972+ current_source = getattr (node , 'source' , None )
973+ existing_source = self .labels_source .get (name )
974+ existing_docname = self .labels [name ][0 ]
975+
976+ # Check if both labels come from the same source file
977+ if current_source and existing_source :
978+ current_norm = os .path .normcase (os .path .normpath (current_source ))
979+ existing_norm = os .path .normcase (os .path .normpath (existing_source ))
980+ same_source = current_norm == existing_norm
981+ else :
982+ same_source = False
983+
984+ if same_source :
985+ # Same source file included in multiple documents.
986+ # Prefer the including document for proper figure numbering.
987+ if existing_docname not in env .included .get (docname , set ()):
988+ continue
989+ else :
990+ logger .warning (
991+ __ ('duplicate label %s, other instance in %s' ),
992+ name ,
993+ env .doc2path (existing_docname ),
994+ location = node ,
995+ )
966996 self .anonlabels [name ] = docname , labelid
967997 if node .tagname == 'section' :
968998 title = cast ('nodes.title' , node [0 ])
@@ -991,6 +1021,9 @@ def process_doc(
9911021 # anonymous-only labels
9921022 continue
9931023 self .labels [name ] = docname , labelid , sectname
1024+ node_source = getattr (node , 'source' , None )
1025+ if node_source :
1026+ self .labels_source [name ] = node_source
9941027
9951028 def add_program_option (
9961029 self , program : str | None , name : str , docname : str , labelid : str
0 commit comments