Last active
June 10, 2026 15:52
-
-
Save phoerious/46dba8a3181ad903cd7d5e34fbf9bbd1 to your computer and use it in GitHub Desktop.
Sphinx conf.py template for enabling stub file parsing for native submodules. Sphinx doc added support for stubs in 8.2.0, but if you have a non-native __init__.py at the top, it won't pick up the stub files. This conf.py template allows you to specify explicit stub file paths for native submodules.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import importlib | |
| from importlib.abc import MetaPathFinder | |
| from importlib.machinery import EXTENSION_SUFFIXES, ModuleSpec, SourceFileLoader | |
| from pathlib import Path | |
| import sys | |
| # Requires sphinx>=9.0 !!! | |
| # Path to your sources root here | |
| src_dir = Path(...) | |
| # Insert your stubbed modules here... | |
| _mymod_pkg_dir = src_dir.joinpath('mymod') | |
| _STUBBED_NATIVE_MODULES = { | |
| 'mymod.submod1': _mymod_pkg_dir.joinpath('submod1.pyi'), | |
| 'mymod.submod2': _mymod_pkg_dir.joinpath('submod2.pyi'), | |
| # ... | |
| } | |
| # Rest of your config here... | |
| # -- Stub patching ----------------------------------------------------------- | |
| class _NativeStubFinder(MetaPathFinder): | |
| """ | |
| Make PyO3 submodules look like native extension modules to autodoc. | |
| Native Sphinx stub loading only activates when ``find_spec()`` returns a | |
| native-extension origin, for which we need to patch in the .pyi path. | |
| """ | |
| def __init__(self, module_specs): | |
| self.pyi_paths = module_specs | |
| def find_spec(self, fullname, path=None, target=None): | |
| pyi_file: Path = self.pyi_paths.get(fullname) | |
| if pyi_file is None: | |
| return None | |
| mock_native_lib_path = str(pyi_file.parent.joinpath(pyi_file.stem + EXTENSION_SUFFIXES[0])) | |
| return ModuleSpec( | |
| fullname, | |
| SourceFileLoader(fullname, str(pyi_file)), | |
| origin=mock_native_lib_path | |
| ) | |
| def _copy_docstring(src, dst): | |
| if dst is not None and not getattr(dst, '__doc__', None) and getattr(src, '__doc__', None): | |
| dst.__doc__ = src.__doc__ | |
| def _hydrate_member_docstrings(dst, src): | |
| _copy_docstring(src, dst) | |
| dst_dict = getattr(dst, '__dict__', None) | |
| src_dict = getattr(src, '__dict__', None) | |
| if dst_dict is None or src_dict is None: | |
| return | |
| for name, dst_member in dst_dict.items(): | |
| src_member = src_dict.get(name) | |
| if src_member is None: | |
| continue | |
| _copy_docstring(src_member, dst_member) | |
| if isinstance(dst_member, property) and isinstance(src_member, property): | |
| for accessor_name in ('fget', 'fset', 'fdel'): | |
| dst_accessor = getattr(dst_member, accessor_name) | |
| src_accessor = getattr(src_member, accessor_name) | |
| if dst_accessor is not None and src_accessor is not None: | |
| _copy_docstring(src_accessor, dst_accessor) | |
| def setup(_): | |
| from sphinx.ext.autodoc._dynamic import _importer | |
| original_importer = _importer._import_module | |
| native_mods = {} | |
| for m in _STUBBED_NATIVE_MODULES: | |
| # Import the parent package once, capture the native submodule object it | |
| # exposes, then remove the submodule import entry so autodoc can load the | |
| # stub-backed replacement later. | |
| parent, name = m.rsplit('.', 1) | |
| parent_mod = importlib.import_module(parent) | |
| native_mods[m] = getattr(parent_mod, name) | |
| sys.modules.pop(m, None) | |
| if hasattr(parent_mod, name): | |
| delattr(parent_mod, name) | |
| sys.meta_path.insert(0, _NativeStubFinder(_STUBBED_NATIVE_MODULES)) | |
| def import_module(modname, try_reload=False): | |
| # Load new module | |
| module = original_importer(modname, try_reload=try_reload) | |
| if modname not in _STUBBED_NATIVE_MODULES: | |
| return module | |
| # Copy docstrings from original module | |
| _copy_docstring(native_mods[modname], module) | |
| for name, member in vars(module).items(): | |
| native_member = getattr(native_mods[modname], name, None) | |
| if native_member is not None: | |
| _hydrate_member_docstrings(member, native_member) | |
| return module | |
| # Patch _importer._import_module to load stub files properly | |
| _importer._import_module = import_module |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment