@@ -666,6 +666,130 @@ def linkcode_resolve(domain, info) -> str | None:
666
666
except AttributeError :
667
667
return None
668
668
669
+ # -- Custom logic for inherited properties --
670
+ # The @inherit_names decorator creates property wrappers at runtime that
671
+ # delegate to the underlying Array class. These wrappers confuse Sphinx's
672
+ # introspection, causing all inherited properties to link to extension.py
673
+ # instead of their actual implementations. We detect this pattern and
674
+ # redirect to the true source location.
675
+ try :
676
+ if (
677
+ isinstance (obj , property )
678
+ and hasattr (obj , "fget" )
679
+ and hasattr (obj .fget , "__qualname__" )
680
+ and "_inherit_from_data.<locals>.fget" in obj .fget .__qualname__
681
+ ):
682
+ class_name , prop_name = fullname .rsplit ("." , 1 )
683
+ array_class_name = class_name .replace ("Index" , "Array" )
684
+
685
+ # Dynamically import the corresponding Array class
686
+ ArrayClass = None
687
+ if "Datetime" in array_class_name :
688
+ from pandas .core .arrays .datetimes import DatetimeArray
689
+
690
+ ArrayClass = DatetimeArray
691
+ elif "Timedelta" in array_class_name :
692
+ from pandas .core .arrays .timedeltas import TimedeltaArray
693
+
694
+ ArrayClass = TimedeltaArray
695
+ elif "Period" in array_class_name :
696
+ from pandas .core .arrays .period import PeriodArray
697
+
698
+ ArrayClass = PeriodArray
699
+
700
+ if ArrayClass and hasattr (ArrayClass , prop_name ):
701
+ fn = inspect .getsourcefile (ArrayClass )
702
+ if not fn :
703
+ raise RuntimeError ("Could not get source file for ArrayClass." )
704
+
705
+ with open (fn , encoding = "utf-8" ) as f :
706
+ lines = f .read ().splitlines ()
707
+
708
+ start_line , end_line = None , None
709
+
710
+ # Strategy 1:
711
+ # Look for direct definition (prop_name = _field_accessor(...))
712
+ p_accessor = re .compile (
713
+ rf"^\s*{ re .escape (prop_name )} \s*=\s*_field_accessor\("
714
+ )
715
+ for i , line in enumerate (lines ):
716
+ if p_accessor .match (line ):
717
+ start_line = i
718
+ paren_count = line .count ("(" ) - line .count (")" )
719
+ for j in range (i + 1 , len (lines )):
720
+ paren_count += lines [j ].count ("(" ) - lines [j ].count (")" )
721
+ if paren_count <= 0 :
722
+ end_line = j
723
+ break
724
+ break
725
+
726
+ # Strategy 2:
727
+ # Look for @property definition
728
+ if start_line is None :
729
+ p_decorator = re .compile (r"^\s*@property" )
730
+ p_def = re .compile (rf"^\s*def\s+{ re .escape (prop_name )} \s*\(" )
731
+ for i , line in enumerate (lines ):
732
+ if (
733
+ p_decorator .match (line )
734
+ and (i + 1 < len (lines ))
735
+ and p_def .match (lines [i + 1 ])
736
+ ):
737
+ start_line = i
738
+ base_indent = len (lines [i + 1 ]) - len (lines [i + 1 ].lstrip ())
739
+ for j in range (i + 2 , len (lines )):
740
+ line_strip = lines [j ].strip ()
741
+ if line_strip :
742
+ current_indent = len (lines [j ]) - len (
743
+ lines [j ].lstrip ()
744
+ )
745
+ if current_indent <= base_indent and (
746
+ line_strip .startswith (("def " , "class " , "@" ))
747
+ ):
748
+ end_line = j - 1
749
+ break
750
+ else :
751
+ end_line = len (lines ) - 1
752
+ break
753
+
754
+ # Strategy 3:
755
+ # Look for alias (prop_name = canonical_name)
756
+ if start_line is None :
757
+ alias_pattern = re .compile (
758
+ rf"^\s*{ re .escape (prop_name )} \s*=\s*(\w+)\s*$"
759
+ )
760
+ for i , line in enumerate (lines ):
761
+ match = alias_pattern .match (line )
762
+ if match :
763
+ canonical_name = match .group (1 )
764
+ # Search for canonical definition
765
+ p_canonical = re .compile (
766
+ rf"^\s*{ re .escape (canonical_name )} \s*=\s*_field_accessor\("
767
+ )
768
+ for j , line2 in enumerate (lines ):
769
+ if p_canonical .match (line2 ):
770
+ start_line = j
771
+ paren_count = line2 .count ("(" ) - line2 .count (")" )
772
+ for k in range (j + 1 , len (lines )):
773
+ paren_count += lines [k ].count ("(" ) - lines [
774
+ k
775
+ ].count (")" )
776
+ if paren_count <= 0 :
777
+ end_line = k
778
+ break
779
+ break
780
+ break
781
+
782
+ if start_line is not None and end_line is not None :
783
+ linespec = f"#L{ start_line + 1 } -L{ end_line + 1 } "
784
+ rel_fn = os .path .relpath (fn , start = os .path .dirname (pandas .__file__ ))
785
+ if "+" in pandas .__version__ :
786
+ return f"https://github.com/pandas-dev/pandas/blob/main/pandas/{ rel_fn } { linespec } "
787
+ else :
788
+ return f"https://github.com/pandas-dev/pandas/blob/v{ pandas .__version__ } /pandas/{ rel_fn } { linespec } "
789
+ except Exception :
790
+ # If custom logic fails, fall through to the default implementation
791
+ pass
792
+
669
793
try :
670
794
fn = inspect .getsourcefile (inspect .unwrap (obj ))
671
795
except TypeError :
0 commit comments