@@ -60,49 +60,79 @@ def to_nav(self, title: str) -> NavItem:
6060 return {title : items }
6161
6262
63- def _compact_index (package : griffe .Module , documented : set [str ]) -> str | None :
64- """Build a compact index body for a package that re-exports from outside its own subtree.
63+ def _compact_index (module : griffe .Module , documented : set [str ]) -> str | None :
64+ """Build a compact page body for a module that re-exports from outside its own subtree.
6565
6666 mkdocstrings renders a re-export whose canonical documentation lives on
6767 another page as a full duplicate of it: deterministically for aliases
6868 within one top-level package (`mcp.client.auth` re-exporting from
6969 `mcp.shared.auth`), and order-dependently across top-level packages
7070 (`from mcp_types import y` + `__all__` renders the duplicate only when
7171 the other package happens to be loaded already, and silently omits the
72- member when it isn't). Packages whose exports all live in their own
73- subtree (`mcp_types` re-exporting its private `._types` module) are
74- unaffected and keep the plain `::: package` stub (return `None`): their
75- index is itself the canonical rendering.
72+ member when it isn't). Modules whose exports all live in their own
73+ subtree (`mcp_types` re-exporting its private `._types` module, or a
74+ module whose `__all__` lists only its own definitions) are unaffected
75+ and keep the plain `::: module` stub (return `None`): their page is
76+ itself the canonical rendering.
7677
77- For an affected package , pin the semantics instead of inheriting the
78+ For an affected module , pin the semantics instead of inheriting the
7879 accident: every export whose canonical page exists elsewhere under the API
7980 reference becomes a link to it, and only exports documented nowhere else
8081 (re-exports from private modules) keep their full body here, via an
8182 explicit `members:` list.
8283 """
83- prefix = f"{ package .path } ."
84- exports = {str (export ): package .members [str (export )] for export in package .exports or ()}
84+ prefix = f"{ module .path } ."
85+ exports : dict [str , griffe .Object | griffe .Alias ] = {}
86+ for export in module .exports or ():
87+ name = str (export )
88+ # Listed exports must be statically documentable: a name provided at
89+ # runtime (module `__getattr__`) is a docs error by policy, not a skip.
90+ if (member := module .members .get (name )) is None :
91+ msg = f"gen_ref_pages: export { module .path } .{ name } is not statically visible"
92+ raise SystemExit (msg )
93+ exports [name ] = member
8594 if not any (member .is_alias and not member .target_path .startswith (prefix ) for member in exports .values ()):
8695 return None
8796
97+ # A plain stub also renders the module's own public members that are not
98+ # in `__all__` (`show_if_no_docstring: false` hides the docstring-less
99+ # ones); keep them, so flipping a page to compact drops nothing.
100+ public = dict (exports )
101+ for name , member in module .members .items ():
102+ if (
103+ name not in public
104+ and not name .startswith ("_" )
105+ and not member .is_alias
106+ and member .kind is not griffe .Kind .MODULE
107+ and member .has_docstrings
108+ ):
109+ public [name ] = member
110+
88111 inline : list [str ] = []
89112 sections : dict [str , list [str ]] = {}
90- for name in sorted (exports , key = str .lower ):
91- member = exports [name ]
92- # A target only gets an anchor on its canonical page if it is rendered
93- # there, which the default `show_if_no_docstring: false` limits to
94- # objects with docstrings.
95- if member .is_alias and member .target_path .rpartition ("." )[0 ] in documented and member .has_docstrings :
96- link_target = member .target_path
97- else :
98- inline .append (name )
99- link_target = f"{ package .path } .{ name } "
100- entry = f"- [`{ name } `][{ link_target } ]"
113+ for name in sorted (public , key = str .lower ):
114+ member = public [name ]
101115 try :
102116 target = member .final_target if member .is_alias else member
103117 except griffe .AliasResolutionError as exc :
104- msg = f"gen_ref_pages: export { package .path } .{ name } resolves outside the documented packages"
118+ msg = f"gen_ref_pages: export { module .path } .{ name } resolves outside the documented packages"
105119 raise SystemExit (msg ) from exc
120+ # Link to the anchor another page actually renders: the deepest alias
121+ # hop whose module is documented (the final target may live in a
122+ # private module and only be re-exported by a public one), rendered
123+ # there only when docstringed (`show_if_no_docstring: false`).
124+ anchor = None
125+ hop = member
126+ while hop .is_alias :
127+ if hop .target_path .rpartition ("." )[0 ] in documented :
128+ anchor = hop .target_path
129+ hop = hop .target
130+ if anchor is not None and member .has_docstrings :
131+ link_target = anchor
132+ else :
133+ inline .append (name )
134+ link_target = f"{ module .path } .{ name } "
135+ entry = f"- [`{ name } `][{ link_target } ]"
106136 if docstring := target .docstring :
107137 summary = " " .join (docstring .value .split ("\n \n " , 1 )[0 ].split ("\n " ))
108138 entry += f" — { summary } "
@@ -111,12 +141,15 @@ def _compact_index(package: griffe.Module, documented: set[str]) -> str | None:
111141 # Rendering the stub resolves the cross-package aliases again, in
112142 # mkdocstrings' own collection. On a warm incremental rebuild the target
113143 # package's pages can all be cache hits, so nothing else loads it and the
114- # resolution crashes (AliasResolutionError); preloading pins it.
144+ # resolution crashes (AliasResolutionError); preloading pins it. The
145+ # module's own root package needs no pin: rendering the stub loads it.
115146 preload = sorted (
116- {member .target_path .split ("." )[0 ] for member in exports .values () if member .is_alias } - {package .path }
147+ {member .target_path .split ("." )[0 ] for member in exports .values () if member .is_alias }
148+ - {module .path .split ("." )[0 ]}
117149 )
118- body = [f"::: { package .path } " , " options:" ]
119- body += [" preload_modules:" , * (f" - { module } " for module in preload )]
150+ body = [f"::: { module .path } " , " options:" ]
151+ if preload :
152+ body += [" preload_modules:" , * (f" - { pkg } " for pkg in preload )]
120153 if inline :
121154 body += [" members:" , * (f" - { name } " for name in inline )]
122155 else :
@@ -145,7 +178,7 @@ def generate() -> list[NavItem]:
145178
146179 root = _Node ()
147180 stubs : dict [Path , str ] = {}
148- package_index : dict [str , Path ] = {}
181+ pages : dict [str , Path ] = {}
149182 documented : set [str ] = set ()
150183
151184 for package in PACKAGES :
@@ -155,8 +188,7 @@ def generate() -> list[NavItem]:
155188 doc_path = path .relative_to (base ).with_suffix (".md" )
156189
157190 parts = tuple (module_path .parts )
158- is_package = parts [- 1 ] == "__init__"
159- if is_package :
191+ if parts [- 1 ] == "__init__" :
160192 parts = parts [:- 1 ]
161193 doc_path = doc_path .with_name ("index.md" )
162194 # A private component anywhere makes the module private: checking
@@ -167,21 +199,25 @@ def generate() -> list[NavItem]:
167199 ident = "." .join (parts )
168200 documented .add (ident )
169201 stubs [API_DIR / doc_path ] = _stub (parts [- 1 ], f"::: { ident } " )
170- if is_package :
171- package_index [ident ] = API_DIR / doc_path
202+ pages [ident ] = API_DIR / doc_path
172203
173204 node = root
174205 for part in parts :
175206 node = node .child (part )
176207 node .url = f"api/{ doc_path .as_posix ()} "
177208
178- # Load every package before inspecting any of them : aliases only resolve
179- # once the module they point at is in the loader's collection.
209+ # Load the root packages before inspecting any module : aliases only
210+ # resolve once the module they point at is in the loader's collection.
180211 loader = griffe .GriffeLoader (search_paths = [str (package .parent ) for package in PACKAGES ])
181- modules = {ident : loader .load (ident ) for ident in package_index }
182- for ident , doc_path in package_index .items ():
183- module = modules [ident ]
184- assert isinstance (module , griffe .Module )
212+ for package in PACKAGES :
213+ loader .load (package .name )
214+ for ident , doc_path in pages .items ():
215+ try :
216+ module = loader .modules_collection [ident ]
217+ except KeyError as exc :
218+ raise SystemExit (f"gen_ref_pages: cannot find { ident } in the loaded packages" ) from exc
219+ if not isinstance (module , griffe .Module ):
220+ raise SystemExit (f"gen_ref_pages: { ident } is shadowed by a non-module member" )
185221 if body := _compact_index (module , documented ):
186222 stubs [doc_path ] = _stub (ident .rpartition ("." )[2 ], body )
187223
0 commit comments