Skip to content

Instantly share code, notes, and snippets.

@phoerious
Last active June 10, 2026 15:52
Show Gist options
  • Select an option

  • Save phoerious/46dba8a3181ad903cd7d5e34fbf9bbd1 to your computer and use it in GitHub Desktop.

Select an option

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.
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