@@ -42,6 +42,7 @@ class MethodInfo:
4242 examples : List [str ] = field (default_factory = list )
4343 notes : str = "" # Notes/More Information section
4444 see_also : List [Tuple [str , str ]] = field (default_factory = list ) # (name, description)
45+ calls : List [str ] = field (default_factory = list ) # Functions this method calls (Uses)
4546 source_file : str = "" # Relative path to source file
4647 source_line : int = 0 # Line number in source
4748 is_async : bool = False
@@ -77,6 +78,7 @@ class FunctionInfo:
7778 examples : List [str ] = field (default_factory = list )
7879 notes : str = "" # Notes/More Information section
7980 see_also : List [Tuple [str , str ]] = field (default_factory = list ) # (name, description)
81+ calls : List [str ] = field (default_factory = list ) # Functions this function calls (Uses)
8082 source_file : str = "" # Relative path to source file
8183 source_line : int = 0 # Line number in source
8284 is_async : bool = False
@@ -687,6 +689,112 @@ def render_see_also_section(see_also: list) -> str:
687689"""
688690
689691
692+ def render_uses_section (calls : list ) -> str :
693+ """Render a 'Uses' section showing what functions this code calls.
694+
695+ Args:
696+ calls: List of function/method names that are called
697+
698+ Returns:
699+ MDX string with uses section, or empty string if none
700+ """
701+ if not calls :
702+ return ""
703+
704+ items = [f"- `{ call } `" for call in calls ]
705+
706+ return f"""
707+ ## Uses
708+
709+ { chr (10 ).join (items )}
710+ """
711+
712+
713+ def render_used_by_section (used_by : list ) -> str :
714+ """Render a 'Used By' section showing what code calls this function.
715+
716+ Args:
717+ used_by: List of (caller_name, caller_path) tuples
718+
719+ Returns:
720+ MDX string with used by section, or empty string if none
721+ """
722+ if not used_by :
723+ return ""
724+
725+ items = []
726+ for caller_name , caller_path in used_by :
727+ if caller_path :
728+ items .append (f"- [`{ caller_name } `]({ caller_path } )" )
729+ else :
730+ items .append (f"- `{ caller_name } `" )
731+
732+ return f"""
733+ ## Used By
734+
735+ { chr (10 ).join (items )}
736+ """
737+
738+
739+ # =============================================================================
740+ # AST FUNCTION CALL EXTRACTION
741+ # =============================================================================
742+
743+ class FunctionCallVisitor (ast .NodeVisitor ):
744+ """Extract function/method calls from an AST node."""
745+
746+ def __init__ (self ):
747+ self .calls = []
748+
749+ def visit_Call (self , node ):
750+ """Extract the name of a function call."""
751+ if isinstance (node .func , ast .Name ):
752+ # Simple function call: func_name()
753+ self .calls .append (node .func .id )
754+ elif isinstance (node .func , ast .Attribute ):
755+ # Method call: obj.method() or self.method()
756+ if isinstance (node .func .value , ast .Name ):
757+ if node .func .value .id == 'self' :
758+ # self.method() - just use method name
759+ self .calls .append (node .func .attr )
760+ else :
761+ # obj.method()
762+ self .calls .append (f"{ node .func .value .id } .{ node .func .attr } " )
763+ else :
764+ # Chained or complex: just get the attribute
765+ self .calls .append (node .func .attr )
766+ self .generic_visit (node )
767+
768+ def get_unique_calls (self ) -> List [str ]:
769+ """Return unique function calls, excluding common built-ins."""
770+ # Filter out common built-ins and private methods
771+ excluded = {
772+ 'print' , 'len' , 'str' , 'int' , 'float' , 'bool' , 'list' , 'dict' , 'set' ,
773+ 'tuple' , 'range' , 'enumerate' , 'zip' , 'map' , 'filter' , 'sorted' ,
774+ 'isinstance' , 'issubclass' , 'hasattr' , 'getattr' , 'setattr' , 'delattr' ,
775+ 'super' , 'type' , 'id' , 'repr' , 'format' , 'open' , 'input' , 'round' ,
776+ 'abs' , 'min' , 'max' , 'sum' , 'any' , 'all' , 'next' , 'iter' , 'reversed' ,
777+ 'append' , 'extend' , 'insert' , 'remove' , 'pop' , 'clear' , 'copy' ,
778+ 'keys' , 'values' , 'items' , 'get' , 'update' , 'join' , 'split' , 'strip' ,
779+ 'lower' , 'upper' , 'replace' , 'format' , 'startswith' , 'endswith' ,
780+ }
781+ seen = set ()
782+ result = []
783+ for call in self .calls :
784+ call_name = call .split ('.' )[- 1 ] if '.' in call else call
785+ if call_name not in excluded and not call_name .startswith ('_' ) and call not in seen :
786+ seen .add (call )
787+ result .append (call )
788+ return result [:10 ] # Limit to top 10 calls
789+
790+
791+ def extract_function_calls (node ) -> List [str ]:
792+ """Extract function calls from a function/method AST node."""
793+ visitor = FunctionCallVisitor ()
794+ visitor .visit (node )
795+ return visitor .get_unique_calls ()
796+
797+
690798# =============================================================================
691799# CONFIGURATION
692800# =============================================================================
@@ -1009,6 +1117,7 @@ def _parse_method(self, node) -> Optional[MethodInfo]:
10091117 examples = parsed_doc ["examples" ],
10101118 notes = parsed_doc ["notes" ],
10111119 see_also = parsed_doc ["see_also" ],
1120+ calls = extract_function_calls (node ),
10121121 source_line = node .lineno ,
10131122 is_async = is_async ,
10141123 is_static = is_static ,
@@ -1042,6 +1151,7 @@ def _parse_function(self, node) -> Optional[FunctionInfo]:
10421151 examples = parsed_doc ["examples" ],
10431152 notes = parsed_doc ["notes" ],
10441153 see_also = parsed_doc ["see_also" ],
1154+ calls = extract_function_calls (node ),
10451155 source_line = node .lineno ,
10461156 is_async = is_async ,
10471157 )
@@ -1261,6 +1371,39 @@ def __init__(self, output_dir: Path, package_name: str, config: Dict[str, Any],
12611371 self .config = config
12621372 self .layout = layout
12631373 self .generated_files = set () # Use set for faster lookups and cleanup
1374+ # Index for "Used By" cross-references: {function_name: [(caller_name, caller_path), ...]}
1375+ self .used_by_index : Dict [str , List [Tuple [str , str ]]] = {}
1376+
1377+ def _build_used_by_index (self , modules : List [ModuleInfo ]) -> None :
1378+ """Build reverse index of function calls for 'Used By' section."""
1379+ for mod in modules :
1380+ for cls in mod .classes :
1381+ for method in cls .methods + cls .class_methods :
1382+ caller_name = f"{ cls .name } .{ method .name } "
1383+ caller_path = f"../functions/{ cls .name } -{ method .name } "
1384+ for called in method .calls :
1385+ # Get just the function name (without module prefix)
1386+ call_name = called .split ("." )[- 1 ] if "." in called else called
1387+ if call_name not in self .used_by_index :
1388+ self .used_by_index [call_name ] = []
1389+ self .used_by_index [call_name ].append ((caller_name , caller_path ))
1390+
1391+ for func in mod .functions :
1392+ caller_name = func .name
1393+ caller_path = f"../functions/{ func .name } "
1394+ for called in func .calls :
1395+ call_name = called .split ("." )[- 1 ] if "." in called else called
1396+ if call_name not in self .used_by_index :
1397+ self .used_by_index [call_name ] = []
1398+ self .used_by_index [call_name ].append ((caller_name , caller_path ))
1399+
1400+ def get_used_by (self , func_name : str ) -> List [Tuple [str , str ]]:
1401+ """Get list of callers for a function."""
1402+ # Try exact match first, then short name
1403+ if func_name in self .used_by_index :
1404+ return self .used_by_index [func_name ][:5 ] # Limit to top 5
1405+ short_name = func_name .split ("." )[- 1 ] if "." in func_name else func_name
1406+ return self .used_by_index .get (short_name , [])[:5 ]
12641407
12651408 def generate_module_doc (self , info : ModuleInfo , dry_run : bool = False ) -> List [Path ]:
12661409 """Generate MDX documentation for a module (potentially multiple files)."""
@@ -1760,6 +1903,11 @@ def _render_function_page(self, func: FunctionInfo, module_info: ModuleInfo, cls
17601903 dedented_ex = textwrap .dedent (func .examples [0 ])
17611904 lines .append (f"```python\n { dedented_ex } \n ```\n " )
17621905
1906+ # Add Uses section if function calls other functions
1907+ uses_section = render_uses_section (func .calls )
1908+ if uses_section :
1909+ lines .append (uses_section )
1910+
17631911 # Add Notes section if available
17641912 notes_section = render_notes_section (func .notes )
17651913 if notes_section :
@@ -1770,6 +1918,13 @@ def _render_function_page(self, func: FunctionInfo, module_info: ModuleInfo, cls
17701918 if see_also_section :
17711919 lines .append (see_also_section )
17721920
1921+ # Add Used By section (what calls this function)
1922+ used_by = self .get_used_by (func .name )
1923+ if used_by :
1924+ used_by_section = render_used_by_section (used_by )
1925+ if used_by_section :
1926+ lines .append (used_by_section )
1927+
17731928 # Add Source link
17741929 github_repo = self .config .get ("github_repo" , "" )
17751930 source_section = render_source_link (func .source_file , func .source_line , github_repo )
@@ -1986,23 +2141,35 @@ def generate_package(self, package_name: str, dry_run: bool = False):
19862141 modules = parser .get_modules ()
19872142 print (f"Found { len (modules )} modules" )
19882143
1989- generated = 0
2144+ # Two-pass generation for Used By cross-references:
2145+ # Pass 1: Parse all modules and build the index
2146+ print (f" Pass 1: Building cross-reference index..." )
2147+ parsed_modules = []
19902148 for module in modules :
19912149 module_name = module .split ("." )[- 1 ] if "." in module else module
19922150 if module_name in SKIP_MODULES :
19932151 continue
1994-
1995- print (f" Processing: { module } " )
19962152 info = parser .parse_module (module )
19972153 if info :
1998- results = generator .generate_module_doc (info , dry_run = dry_run )
1999- if results :
2000- generated += 1
2001- for r in results :
2002- if dry_run :
2003- print (f" Would generate: { r .name } " )
2004- else :
2005- print (f" Generated: { r .name } " )
2154+ parsed_modules .append (info )
2155+
2156+ # Build the Used By index from all parsed modules
2157+ generator ._build_used_by_index (parsed_modules )
2158+ print (f" Built index with { len (generator .used_by_index )} functions tracked" )
2159+
2160+ # Pass 2: Generate documentation with cross-references
2161+ print (f" Pass 2: Generating documentation..." )
2162+ generated = 0
2163+ for info in parsed_modules :
2164+ print (f" Processing: { info .name } " )
2165+ results = generator .generate_module_doc (info , dry_run = dry_run )
2166+ if results :
2167+ generated += 1
2168+ for r in results :
2169+ if dry_run :
2170+ print (f" Would generate: { r .name } " )
2171+ else :
2172+ print (f" Generated: { r .name } " )
20062173
20072174 if generator .generated_files and not dry_run :
20082175 # NOTE: Cleanup disabled - was incorrectly deleting valid files
0 commit comments