1919from sphinx .ext .autodoc .mock import ismock , mock , undecorate
2020from sphinx .pycode import ModuleAnalyzer
2121from sphinx .util import inspect , logging
22- from sphinx .util .inspect import (
23- isclass ,
24- safe_getattr ,
25- )
22+ from sphinx .util .inspect import isclass , safe_getattr
2623from sphinx .util .typing import get_type_hints
2724
2825if TYPE_CHECKING :
@@ -71,119 +68,47 @@ def __repr__(self) -> str:
7168 return f'<{ self .__class__ .__name__ } { self .__dict__ } >'
7269
7370
74- def mangle (subject : Any , name : str ) -> str :
75- """Mangle the given name."""
76- try :
77- if isclass (subject ) and name .startswith ('__' ) and not name .endswith ('__' ):
78- return f'_{ subject .__name__ } { name } '
79- except AttributeError :
80- pass
81-
82- return name
83-
84-
85- def import_module (modname : str , try_reload : bool = False ) -> Any :
86- if modname in sys .modules :
87- return sys .modules [modname ]
88-
89- skip_pyi = bool (os .getenv ('SPHINX_AUTODOC_IGNORE_NATIVE_MODULE_TYPE_STUBS' , '' ))
90- original_module_names = frozenset (sys .modules )
91- try :
92- spec = find_spec (modname )
93- if spec is None :
94- msg = f'No module named { modname !r} '
95- raise ModuleNotFoundError (msg , name = modname ) # NoQA: TRY301
96- spec , pyi_path = _find_type_stub_spec (spec , modname )
97- if skip_pyi or pyi_path is None :
98- module = importlib .import_module (modname )
99- else :
100- if spec .loader is None :
101- msg = 'missing loader'
102- raise ImportError (msg , name = spec .name ) # NoQA: TRY301
103- sys .modules [modname ] = module = module_from_spec (spec )
104- spec .loader .exec_module (module )
105- except ImportError :
106- raise
107- except BaseException as exc :
108- # Importing modules may cause any side effects, including
109- # SystemExit, so we need to catch all errors.
110- raise ImportError (exc , traceback .format_exc ()) from exc
111- if try_reload and os .environ .get ('SPHINX_AUTODOC_RELOAD_MODULES' ):
112- new_modules = [m for m in sys .modules if m not in original_module_names ]
113- # Try reloading modules with ``typing.TYPE_CHECKING == True``.
114- try :
115- typing .TYPE_CHECKING = True # type: ignore[misc]
116- # Ignore failures; we've already successfully loaded these modules
117- with contextlib .suppress (ImportError , KeyError ):
118- for m in new_modules :
119- mod_path = getattr (sys .modules [m ], '__file__' , '' )
120- if mod_path and mod_path .endswith ('.pyi' ):
121- continue
122- _reload_module (sys .modules [m ])
123- finally :
124- typing .TYPE_CHECKING = False # type: ignore[misc]
125- module = sys .modules [modname ]
126- return module
127-
128-
129- def _find_type_stub_spec (
130- spec : ModuleSpec , modname : str
131- ) -> tuple [ModuleSpec , Path | None ]:
132- """Try finding a spec for a PEP 561 '.pyi' stub file for native modules."""
133- if spec .origin is None :
134- return spec , None
135-
136- for suffix in _NATIVE_SUFFIXES :
137- if not spec .origin .endswith (suffix ):
138- continue
139- pyi_path = Path (spec .origin .removesuffix (suffix ) + '.pyi' )
140- if not pyi_path .is_file ():
141- continue
142- pyi_loader = _StubFileLoader (modname , path = str (pyi_path ))
143- pyi_spec = spec_from_loader (modname , loader = pyi_loader )
144- if pyi_spec is not None :
145- return pyi_spec , pyi_path
146- return spec , None
147-
148-
149- class _StubFileLoader (FileLoader ):
150- """Load modules from ``.pyi`` stub files."""
151-
152- def get_source (self , fullname : str ) -> str :
153- path = self .get_filename (fullname )
154- for suffix in _NATIVE_SUFFIXES :
155- if not path .endswith (suffix ):
156- continue
157- path = path .removesuffix (suffix ) + '.pyi'
158- try :
159- source_bytes = self .get_data (path )
160- except OSError as exc :
161- raise ImportError from exc
162- return decode_source (source_bytes )
163-
164-
165- def _reload_module (module : ModuleType ) -> Any :
166- """Call importlib.reload(module), convert exceptions to ImportError"""
71+ def _import_object (
72+ * ,
73+ get_attr : _AttrGetter = safe_getattr ,
74+ mock_imports : list [str ],
75+ module_name : str ,
76+ obj_path : Sequence [str ],
77+ obj_type : _AutodocObjType ,
78+ type_aliases : dict [str , Any ] | None ,
79+ ) -> _ImportedObject | None :
80+ """Import the module and get the object to document."""
16781 try :
168- return importlib .reload (module )
169- except BaseException as exc :
170- # Importing modules may cause any side effects, including
171- # SystemExit, so we need to catch all errors.
172- raise ImportError (exc , traceback .format_exc ()) from exc
173-
82+ with mock (mock_imports ):
83+ im = _import_from_module_and_path (
84+ module_name = module_name , obj_path = obj_path , get_attr = get_attr
85+ )
86+ except ImportError as exc :
87+ if obj_type == 'data' :
88+ im_ = _import_data_declaration (
89+ module_name = module_name ,
90+ obj_path = obj_path ,
91+ mock_imports = mock_imports ,
92+ type_aliases = type_aliases ,
93+ )
94+ if im_ is not None :
95+ return im_
96+ elif obj_type == 'attribute' :
97+ im_ = _import_attribute_declaration (
98+ module_name = module_name ,
99+ obj_path = obj_path ,
100+ mock_imports = mock_imports ,
101+ type_aliases = type_aliases ,
102+ get_attr = get_attr ,
103+ )
104+ if im_ is not None :
105+ return im_
106+ logger .warning (exc .args [0 ], type = 'autodoc' , subtype = 'import_object' )
107+ return None
174108
175- def import_object (
176- modname : str ,
177- objpath : list [str ],
178- objtype : str = '' ,
179- attrgetter : _AttrGetter = safe_getattr ,
180- ) -> Any :
181- ret = _import_from_module_and_path (
182- module_name = modname , obj_path = objpath , get_attr = attrgetter
183- )
184- if isinstance (ret , _ImportedObject ):
185- return [ret .module , ret .parent , ret .object_name , ret .obj ]
186- return None
109+ if ismock (im .obj ):
110+ im .obj = undecorate (im .obj )
111+ return im
187112
188113
189114def _import_from_module_and_path (
@@ -203,7 +128,7 @@ def _import_from_module_and_path(
203128 try :
204129 while module is None :
205130 try :
206- module = import_module (module_name , try_reload = True )
131+ module = _import_module (module_name , try_reload = True )
207132 logger .debug ('[autodoc] import %s => %r' , module_name , module )
208133 except ImportError as exc :
209134 logger .debug ('[autodoc] import %s => failed' , module_name )
@@ -221,7 +146,7 @@ def _import_from_module_and_path(
221146 for attr_name in obj_path :
222147 parent = obj
223148 logger .debug ('[autodoc] getattr(_, %r)' , attr_name )
224- mangled_name = mangle (obj , attr_name )
149+ mangled_name = _mangle_name (obj , attr_name )
225150 obj = get_attr (obj , mangled_name )
226151
227152 try :
@@ -253,7 +178,7 @@ def _import_from_module_and_path(
253178 err_parts = [f'autodoc: failed to import { module_name !r} ' ]
254179
255180 if isinstance (exc , ImportError ):
256- # import_module () raises ImportError having real exception obj and
181+ # _import_module () raises ImportError having real exception obj and
257182 # traceback
258183 real_exc = exc .args [0 ]
259184 traceback_msg = traceback .format_exception (exc )
@@ -280,52 +205,105 @@ def _import_from_module_and_path(
280205 raise ImportError (errmsg ) from exc
281206
282207
283- def _import_object (
284- * ,
285- module_name : str ,
286- obj_path : Sequence [str ],
287- mock_imports : list [str ],
288- get_attr : _AttrGetter = safe_getattr ,
289- obj_type : _AutodocObjType ,
290- type_aliases : dict [str , Any ] | None ,
291- ) -> _ImportedObject | None :
292- """Import the object given by *module_name* and *obj_path* and set
293- it as *object*.
208+ def _import_module (modname : str , try_reload : bool = False ) -> Any :
209+ if modname in sys .modules :
210+ return sys .modules [modname ]
294211
295- Returns True if successful, False if an error occurred.
296- """
212+ skip_pyi = bool ( os . getenv ( 'SPHINX_AUTODOC_IGNORE_NATIVE_MODULE_TYPE_STUBS' , '' ))
213+ original_module_names = frozenset ( sys . modules )
297214 try :
298- with mock (mock_imports ):
299- im = _import_from_module_and_path (
300- module_name = module_name , obj_path = obj_path , get_attr = get_attr
301- )
302- except ImportError as exc :
303- if obj_type == 'data' :
304- im_ = _import_data_declaration (
305- module_name = module_name ,
306- obj_path = obj_path ,
307- mock_imports = mock_imports ,
308- type_aliases = type_aliases ,
309- )
310- if im_ is not None :
311- return im_
312- elif obj_type == 'attribute' :
313- im_ = _import_attribute_declaration (
314- module_name = module_name ,
315- obj_path = obj_path ,
316- mock_imports = mock_imports ,
317- type_aliases = type_aliases ,
318- get_attr = get_attr ,
319- )
320- if im_ is not None :
321- return im_
215+ spec = find_spec (modname )
216+ if spec is None :
217+ msg = f'No module named { modname !r} '
218+ raise ModuleNotFoundError (msg , name = modname ) # NoQA: TRY301
219+ spec , pyi_path = _find_type_stub_spec (spec , modname )
220+ if skip_pyi or pyi_path is None :
221+ module = importlib .import_module (modname )
222+ else :
223+ if spec .loader is None :
224+ msg = 'missing loader'
225+ raise ImportError (msg , name = spec .name ) # NoQA: TRY301
226+ sys .modules [modname ] = module = module_from_spec (spec )
227+ spec .loader .exec_module (module )
228+ except ImportError :
229+ raise
230+ except BaseException as exc :
231+ # Importing modules may cause any side effects, including
232+ # SystemExit, so we need to catch all errors.
233+ raise ImportError (exc , traceback .format_exc ()) from exc
234+ if try_reload and os .environ .get ('SPHINX_AUTODOC_RELOAD_MODULES' ):
235+ new_modules = [m for m in sys .modules if m not in original_module_names ]
236+ # Try reloading modules with ``typing.TYPE_CHECKING == True``.
237+ try :
238+ typing .TYPE_CHECKING = True # type: ignore[misc]
239+ # Ignore failures; we've already successfully loaded these modules
240+ with contextlib .suppress (ImportError , KeyError ):
241+ for m in new_modules :
242+ mod_path = getattr (sys .modules [m ], '__file__' , '' )
243+ if mod_path and mod_path .endswith ('.pyi' ):
244+ continue
245+ _reload_module (sys .modules [m ])
246+ finally :
247+ typing .TYPE_CHECKING = False # type: ignore[misc]
248+ module = sys .modules [modname ]
249+ return module
322250
323- logger .warning (exc .args [0 ], type = 'autodoc' , subtype = 'import_object' )
324- return None
325251
326- if ismock (im .obj ):
327- im .obj = undecorate (im .obj )
328- return im
252+ def _find_type_stub_spec (
253+ spec : ModuleSpec , modname : str
254+ ) -> tuple [ModuleSpec , Path | None ]:
255+ """Try finding a spec for a PEP 561 '.pyi' stub file for native modules."""
256+ if spec .origin is None :
257+ return spec , None
258+
259+ for suffix in _NATIVE_SUFFIXES :
260+ if not spec .origin .endswith (suffix ):
261+ continue
262+ pyi_path = Path (spec .origin .removesuffix (suffix ) + '.pyi' )
263+ if not pyi_path .is_file ():
264+ continue
265+ pyi_loader = _StubFileLoader (modname , path = str (pyi_path ))
266+ pyi_spec = spec_from_loader (modname , loader = pyi_loader )
267+ if pyi_spec is not None :
268+ return pyi_spec , pyi_path
269+ return spec , None
270+
271+
272+ class _StubFileLoader (FileLoader ):
273+ """Load modules from ``.pyi`` stub files."""
274+
275+ def get_source (self , fullname : str ) -> str :
276+ path = self .get_filename (fullname )
277+ for suffix in _NATIVE_SUFFIXES :
278+ if not path .endswith (suffix ):
279+ continue
280+ path = path .removesuffix (suffix ) + '.pyi'
281+ try :
282+ source_bytes = self .get_data (path )
283+ except OSError as exc :
284+ raise ImportError from exc
285+ return decode_source (source_bytes )
286+
287+
288+ def _reload_module (module : ModuleType ) -> Any :
289+ """Call importlib.reload(module), convert exceptions to ImportError"""
290+ try :
291+ return importlib .reload (module )
292+ except BaseException as exc :
293+ # Importing modules may cause any side effects, including
294+ # SystemExit, so we need to catch all errors.
295+ raise ImportError (exc , traceback .format_exc ()) from exc
296+
297+
298+ def _mangle_name (subject : Any , name : str ) -> str :
299+ """Mangle the given name."""
300+ try :
301+ if isclass (subject ) and name .startswith ('__' ) and not name .endswith ('__' ):
302+ return f'_{ subject .__name__ } { name } '
303+ except AttributeError :
304+ pass
305+
306+ return name
329307
330308
331309def _import_data_declaration (
@@ -338,7 +316,7 @@ def _import_data_declaration(
338316 # annotation only instance variable (PEP-526)
339317 try :
340318 with mock (mock_imports ):
341- parent = import_module (module_name )
319+ parent = _import_module (module_name )
342320 annotations = get_type_hints (parent , None , type_aliases , include_extras = True )
343321 if obj_path [- 1 ] in annotations :
344322 im = _ImportedObject (
0 commit comments