How do I make sphinx understand the documentation information in static methods? #707
-
|
Hi any and all, We decided to switch from pybind11 to nanobind. This is a growth pain question. I have a problem getting my sphinx documentation for static data and static methods working in nanobind. In pybind11, static methods produces nice documentation. You can see this comparing our stable new and stable documentation. This page gives the method Note that if I go into I am wondering if there are workarounds here so that sphinx can see the documentation? |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 10 replies
-
|
How do you list the classes and their members? Have you tried registering them explicitly, like so? It's possible that Sphinx thinks that the static methods are data, not functions. |
Beta Was this translation helpful? Give feedback.
-
|
I have the same issue. I noticed that when I added a listener in sphinx in my def autodoc_process_handler(app, what, name, obj, options, lines, huh):
print(what, name, obj, lines, obj.__doc__)
def setup(app):
app.connect('autodoc-process-signature', autodoc_process_handler)sphinx gives me output like this: but it is inferring incorrectly that the methods are attributes and thus gives no documentation. |
Beta Was this translation helpful? Give feedback.
-
|
My solution is also brittle, I wrote a script to parse, since my object hierarchy is flat enough: import inspect
class AutodocCollector:
entries = []
def __init__(self, module, dunder=False):
for key in dir(module):
thing = getattr(module, key)
if key.startswith('__') and not dunder:
continue
if 'nanobind.nb_func' in str(type(thing)):
self.parse_func(thing)
elif 'enum.EnumType' in str(type(thing)):
self.parse_enum(thing)
elif 'nanobind.nb_type_0' in str(type(thing)):
self.parse_class(thing)
else:
print(key, type(thing), thing.__name__, inspect.getmembers(thing))
def add_entry(self, e):
self.entries.append(e)
def to_file(self, path, title):
with open(path, 'w') as fp:
fp.write(title+'\n')
fp.write('='*len(title)+'\n\n')
fp.write('\n\n'.join(self.entries))
def parse_func(self, thing):
mother = inspect.getmodule(thing)
self.add_entry(f'.. autofunction:: {mother.__name__}.{thing.__name__}\n\n :teqpflsh:`{thing.__name__}`')
def parse_enum(self, thing):
mother = inspect.getmodule(thing)
self.add_entry(f'.. autoclass:: {mother.__name__}.{thing.__name__}\n :undoc-members:\n :members:\n :show-inheritance:\n\n :teqpflsh:`{thing.__name__}`')
def parse_class(self, thing):
mother = inspect.getmodule(thing)
members = inspect.getmembers(thing)
e = f'.. autoclass:: {mother.__name__}.{thing.__name__}\n :show-inheritance:'
e += f'\n\n C++ docs: :teqpflsh:`{thing.__name__}`'
for name, member in members:
if name.startswith('__'): continue
if isinstance(member, property):
e += '\n\n' + ' '*4 + f'.. autoproperty:: {name}\n'
else:
e += '\n\n' + ' '*4 + f'.. automethod:: {name}\n'
self.add_entry(e)
if __name__ == '__main__':
import teqpflsh
ac = AutodocCollector(teqpflsh._teqpflsh_impl)
ac.to_file('api/teqpflsh.rst', title='teqpflsh Package') |
Beta Was this translation helpful? Give feedback.
-
|
UPDATE: Sorry for the noise, my issue was different and not for static method |
Beta Was this translation helpful? Give feedback.
-
|
Hi, I am the developer of symusic, a library using nanobind, and I am trying to make the API Reference work. I found that previous solutions are not enough for my project, and I would like to paste my solution here. It might not work for old or new version of sphinx, and I test it under Demo here: https://symusic.readthedocs.io/en/latest/api/core/scores.html#symusic.core.ScoreTick.from_abc Nanobind autodoc patch cheat sheetMethod detection
def isfunction(obj: Any) -> bool:
return _orig_isfunction(obj) or isnanobind(obj)
def isroutine(obj: Any) -> bool:
return _orig_isroutine(obj) or isnanobind(obj)
def ismethoddescriptor(obj: Any) -> bool:
return _orig_ismethoddescriptor(obj) or isnanobind(obj)Property type hints
def add_directive_header(self, sig: str) -> None:
orig_add_directive_header(self, sig)
if self.config.autodoc_typehints == "none":
return
func = self._get_property_getter()
fallback = _nanobind_property_return_from_doc(func) if func else None
if fallback:
self.add_line(' :type: ' + fallback, self.get_sourcename())Static factories and groupwise ordering
def import_object(self, raiseerror: bool = False) -> bool:
ret = orig_import_object(self, raiseerror)
if not ret:
return ret
obj = self.parent.__dict__.get(self.object_name)
if isinstance(obj, type(self.object)) and type(obj).__name__ == "nb_func" and isnanobind(obj):
self._is_nanobind_static = True
self.member_order -= 1
return ret
def add_directive_header(self, sig: str) -> None:
orig_add_directive_header(self, sig)
if getattr(self, "_is_nanobind_static", False):
self.add_line(' :staticmethod:', self.get_sourcename())def sort_members(self, documenters, order):
if order == "groupwise" and isinstance(self, ClassDocumenter):
for documenter, _ in documenters:
if isinstance(documenter, MethodDocumenter):
resolved = _resolve_class_member(documenter)
if resolved:
owner, _, raw = resolved
if type(raw).__name__ == "nb_func" and isnanobind(raw):
documenter.member_order = MethodDocumenter.member_order - 1
return orig_sort_members(self, documenters, order)Full patch referencefrom __future__ import annotations
import importlib
from inspect import Parameter
from typing import Any, Optional
import sphinx.util.inspect as sphinx_inspect
from sphinx.ext.autodoc import (
ClassDocumenter,
Documenter,
MethodDocumenter,
PropertyDocumenter,
)
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.typing import stringify_annotation
logger = logging.getLogger(__name__)
def isnanobind(obj) -> bool:
return (
hasattr(type(obj), "__module__")
and type(obj).__module__ == "nanobind"
and type(obj).__name__ in ("nb_func", "nb_method")
)
_orig_isfunction = sphinx_inspect.isfunction
def isfunction(obj: Any) -> bool:
return _orig_isfunction(obj) or isnanobind(obj)
sphinx_inspect.isfunction = isfunction
_orig_isroutine = sphinx_inspect.isroutine
def isroutine(obj: Any) -> bool:
return _orig_isroutine(obj) or isnanobind(obj)
sphinx_inspect.isroutine = isroutine
_orig_ismethoddescriptor = getattr(sphinx_inspect, "ismethoddescriptor", None)
if _orig_ismethoddescriptor:
def ismethoddescriptor(obj: Any) -> bool:
return _orig_ismethoddescriptor(obj) or isnanobind(obj)
sphinx_inspect.ismethoddescriptor = ismethoddescriptor
def _nanobind_property_return_from_doc(func: Any) -> Optional[str]:
if not isnanobind(func):
return None
doc = (getattr(func, "__doc__", "") or "").strip()
if not doc:
return None
first_line = doc.splitlines()[0]
if "->" not in first_line:
return None
return first_line.split("->", 1)[1].strip()
def _patch_property_documenter() -> None:
orig_add_directive_header = PropertyDocumenter.add_directive_header
def add_directive_header(self, sig: str) -> None: # type: ignore[override]
orig_add_directive_header(self, sig)
if self.config.autodoc_typehints == "none":
return
func = self._get_property_getter()
fallback_type = _nanobind_property_return_from_doc(func) if func else None
if fallback_type:
self.add_line(' :type: ' + fallback_type, self.get_sourcename())
PropertyDocumenter.add_directive_header = add_directive_header
def _patch_method_documenter() -> None:
orig_import_object = MethodDocumenter.import_object
orig_add_directive_header = MethodDocumenter.add_directive_header
def import_object(self, raiseerror: bool = False) -> bool: # type: ignore[override]
ret = orig_import_object(self, raiseerror)
self._is_nanobind_static = False # type: ignore[attr-defined]
if not ret:
return ret
obj = self.parent.__dict__.get(self.object_name)
if (
isinstance(obj, type(self.object))
and type(obj).__name__ == "nb_func"
and isnanobind(obj)
):
self._is_nanobind_static = True # type: ignore[attr-defined]
self.member_order -= 1
return ret
def add_directive_header(self, sig: str) -> None: # type: ignore[override]
orig_add_directive_header(self, sig)
if getattr(self, "_is_nanobind_static", False): # type: ignore[attr-defined]
self.add_line(' :staticmethod:', self.get_sourcename())
MethodDocumenter.import_object = import_object
MethodDocumenter.add_directive_header = add_directive_header
def _resolve_class_member(documenter: MethodDocumenter) -> tuple[object, str, Any] | None:
if "::" not in documenter.name:
return None
modname, qualname = documenter.name.split("::", 1)
if not qualname:
return None
chunks = qualname.split(".")
if len(chunks) < 2:
return None
try:
module = importlib.import_module(modname)
except Exception: # pragma: no cover
return None
owner: Any = module
for chunk in chunks[:-1]:
owner = getattr(owner, chunk, None)
if owner is None:
return None
member_name = chunks[-1]
owner_dict = getattr(owner, "__dict__", {})
raw = owner_dict.get(member_name)
if raw is None:
raw = getattr(owner, member_name, None)
if raw is None:
return None
return owner, member_name, raw
def _patch_member_sorting() -> None:
orig_sort_members = Documenter.sort_members
def sort_members(self, documenters, order): # type: ignore[override]
if order == "groupwise" and isinstance(self, ClassDocumenter):
for documenter, _ in documenters:
if isinstance(documenter, MethodDocumenter):
resolved = _resolve_class_member(documenter)
if not resolved:
continue
_, _, raw = resolved
if type(raw).__name__ == "nb_func" and isnanobind(raw):
documenter.member_order = MethodDocumenter.member_order - 1
return orig_sort_members(self, documenters, order)
Documenter.sort_members = sort_members
_patch_property_documenter()
_patch_method_documenter()
_patch_member_sorting() |
Beta Was this translation helpful? Give feedback.
How do you list the classes and their members? Have you tried registering them explicitly, like so?
It's possible that Sphinx thinks that the static methods are data, not functions.