2020class CrossRefsMixin (Mixin ):
2121 """Auto-link backtick references to mkdocstrings anchors.
2222
23- When mkdocstrings is configured, this mixin inspects the packages found
24- under the handler's `paths` config and auto-converts backtick references
25- like `pkg.SomeClass` into mkdocstrings cross-reference links
26- [`pkg.SomeClass`][pkg.SomeClass].
27-
28- Supports an `aliases` dict in the theme config for project-specific
29- name mappings (e.g. `compute_modes` -> `pkg.compute_modes_impl`).
23+ When mkdocstrings is configured, this mixin auto-converts backtick
24+ references like `SomeClass` into mkdocstrings cross-reference links
25+ [`SomeClass`][pkg.SomeClass].
26+
27+ Configure via theme config:
28+ crossref_modules: list of dotted module names to search
29+ (default: auto-discovered top-level packages from mkdocstrings paths)
30+ crossref_aliases: dict mapping short names to fully qualified names
3031 """
3132
3233 crossrefs_modules : List = []
3334 crossrefs_aliases : Dict [str , str ] = {}
3435 crossrefs_activated : bool = False
35- crossrefs_package_name : Optional [str ] = None
3636
3737 def on_config (self , config : MkDocsConfig ):
3838 plugin = config ["plugins" ].get ("mkdocstrings" , None )
3939 if plugin is None :
4040 return super ().on_config (config )
4141
42- # Get the aliases from theme config
4342 self .crossrefs_aliases = config .theme .get ("crossref_aliases" , {})
44-
45- # Find package names from mkdocstrings paths
46- handler_config = plugin .config .get ("handlers" , {}).get ("python" , {})
47- paths = handler_config .get ("paths" , [])
48-
4943 self .crossrefs_modules = []
50- self .crossrefs_package_name = None
5144
52- for path in paths :
53- # Try to discover top-level packages in the given paths
45+ # If user specified explicit modules, use those
46+ explicit_modules = config .theme .get ("crossref_modules" , None )
47+
48+ if explicit_modules is not None :
49+ for mod_name in explicit_modules :
50+ try :
51+ mod = importlib .import_module (mod_name )
52+ self .crossrefs_modules .append ((mod_name , mod ))
53+ logger .info (f"Cross-refs: loaded module '{ mod_name } '." )
54+ except Exception as e :
55+ logger .warning (f"Cross-refs: could not import '{ mod_name } ': { e } " )
56+ else :
57+ # Auto-discover top-level packages from mkdocstrings paths
5458 import os
5559
56- if not os .path .isdir (path ):
57- continue
58- for entry in sorted (os .listdir (path )):
59- entry_path = os .path .join (path , entry )
60- if os .path .isdir (entry_path ) and os .path .isfile (
61- os .path .join (entry_path , "__init__.py" )
62- ):
63- try :
64- mod = importlib .import_module (entry )
65- self .crossrefs_modules .append ((entry , mod ))
66- if self .crossrefs_package_name is None :
67- self .crossrefs_package_name = entry
68- logger .info (
69- f"Cross-refs: loaded module '{ entry } ' for auto-linking."
70- )
71- except Exception as e :
72- logger .debug (
73- f"Cross-refs: could not import '{ entry } ': { e } "
74- )
60+ handler_config = plugin .config .get ("handlers" , {}).get ("python" , {})
61+ paths = handler_config .get ("paths" , [])
62+
63+ for path in paths :
64+ if not os .path .isdir (path ):
65+ continue
66+ for entry in sorted (os .listdir (path )):
67+ entry_path = os .path .join (path , entry )
68+ if os .path .isdir (entry_path ) and os .path .isfile (
69+ os .path .join (entry_path , "__init__.py" )
70+ ):
71+ try :
72+ mod = importlib .import_module (entry )
73+ self .crossrefs_modules .append ((entry , mod ))
74+ logger .info (f"Cross-refs: loaded module '{ entry } '." )
75+ except Exception as e :
76+ logger .debug (f"Cross-refs: could not import '{ entry } ': { e } " )
7577
7678 self .crossrefs_activated = len (self .crossrefs_modules ) > 0
7779
@@ -85,7 +87,6 @@ def on_config(self, config: MkDocsConfig):
8587
8688 def _resolve_ref (self , name : str ) -> Optional [str ]:
8789 """Resolve a backtick name to its fully qualified mkdocstrings anchor."""
88- # Strip leading package prefix if present
8990 * prefix , short = name .split ("." )
9091 if prefix and prefix [0 ] not in {n for n , _ in self .crossrefs_modules }:
9192 return None
@@ -94,22 +95,10 @@ def _resolve_ref(self, name: str) -> Optional[str]:
9495 if short in self .crossrefs_aliases :
9596 return self .crossrefs_aliases [short ]
9697
97- # Search through loaded modules and their submodules
98+ # Check each module directly — no recursion
9899 for mod_name , mod in self .crossrefs_modules :
99100 if hasattr (mod , short ):
100101 return f"{ mod_name } .{ short } "
101- # Check submodules
102- for attr_name in dir (mod ):
103- attr = getattr (mod , attr_name , None )
104- if (
105- attr is not None
106- and hasattr (attr , "__module__" )
107- and hasattr (attr , short )
108- ):
109- # It's a submodule that has this name
110- submod_name = f"{ mod_name } .{ attr_name } "
111- if hasattr (attr , short ):
112- return f"{ submod_name } .{ short } "
113102
114103 return None
115104
@@ -131,10 +120,8 @@ def on_page_markdown(
131120 blocks = markdown .split ("```" )
132121 for i , block in enumerate (blocks ):
133122 if i % 2 == 0 :
134- # Process non-code blocks
135123 blocks [i ] = self ._insert_cross_refs (block )
136124 else :
137- # Restore code block delimiters
138125 blocks [i ] = f"```{ block } ```"
139126
140127 markdown = "" .join (blocks )
0 commit comments