-
-
Notifications
You must be signed in to change notification settings - Fork 63
Description
Hey @pawamoy ... it's me again
Following up on #116 and #296 — both about pymmcore, the SWIG-wrapped C extension I keep pestering you about. Those fixes solved the structural problems (wildcard expansion before merge, and wrong alias target paths). The stubs now merge successfully. But there's one remaining gap: _merge_stubs_docstring won't overwrite the SWIG-generated docstrings with the proper ones from the .pyi stubs.
The allow_inspection: false workaround from #296 was fine when I was rendering pymmcore.CMMCore directly, but I now need inherited_members: true on a subclass (pymmcore_plus.CMMCorePlus), which requires runtime inspection for MRO resolution — so I can't turn inspection off.
Problem
When merging .pyi stubs into a dynamically-inspected C extension module, _merge_stubs_docstring never overwrites an existing docstring:
def _merge_stubs_docstring(obj: Object, stubs: Object) -> None:
if not obj.docstring and stubs.docstring:
obj.docstring = stubs.docstringThis is fine for pure-Python modules where the runtime docstring is the authoritative one. But for SWIG/pybind11/cython extension modules, the runtime "docstrings" are auto-generated C-level signatures like:
getCurrentFocusScore(CMMCore self) -> double
The real docstrings live in the .pyi stubs.
Since the SWIG methods already have a (not so useful) docstring, the stub docstrings are silently discarded.
There's a second, related issue: For overload-only stub methods (methods where ALL definitions in the .pyi are @overload - standard practice for typed C extensions), _merge_stubs_overloads correctly attaches the overload annotations to the runtime member, but never applies the overload's docstring to the base function. So even though the overloads carry proper docstrings, the base function retains its SWIG signature string.
Concrete example
pymmcore ships an __init__.pyi with proper docstrings for all ~290 methods. When griffe loads it with find_stubs_package=True, none of the stub docstrings are used — every method renders with its SWIG signature instead.
Proposed fix
-
Prefer stub docstrings for dynamically-analyzed objects
Objects loaded via runtime inspection have
analysis = "dynamic". When merging stubs, their auto-generated docstrings should yield to the human-authored stub docstrings:def _merge_stubs_docstring(obj: Object, stubs: Object) -> None: - if not obj.docstring and stubs.docstring: + if stubs.docstring and ( + not obj.docstring or getattr(obj, "analysis", None) == "dynamic" + ): obj.docstring = stubs.docstring
-
Apply overload docstrings to the base function during overload merging
For overload-only stub methods, the base function (from the runtime module) has no stub counterpart in stubs.members, so
_merge_stubs_membersnever touches it. Only_merge_stubs_overloadsruns, and it only merges annotations. Adding a docstring fallback here covers the gap:def _merge_stubs_overloads(obj: Module | Class, stubs: Module | Class) -> None: for function_name, overloads in list(stubs.overloads.items()): if overloads: with suppress(KeyError): - _merge_overload_annotations(obj.get_member(function_name), overloads) + function = obj.get_member(function_name) + _merge_overload_annotations(function, overloads) + _merge_stubs_docstring(function, overloads[0]) del stubs.overloads[function_name]
Together, these two changes ensure that stub docstrings are used for all dynamically-analyzed members — both regular and overloaded.
thoughts?
I can submit a PR