From 9bd82f72465737d8d1621591300dfe6457596be0 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Mon, 24 Aug 2026 11:01:50 +0200 Subject: [PATCH] doc: Replace fork with extensions. Use clean upstream Sphinx and use extensions for the awaitable types and work around a Sphinx docstring parsing bug. --- doc/common/conf.py | 7 ++- doc/common/extensions/awaitable.py | 61 ++++++++++++++++++++ doc/common/extensions/docstring_signature.py | 51 ++++++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 doc/common/extensions/awaitable.py create mode 100644 doc/common/extensions/docstring_signature.py diff --git a/doc/common/conf.py b/doc/common/conf.py index bcd48d3..50da3ab 100644 --- a/doc/common/conf.py +++ b/doc/common/conf.py @@ -57,9 +57,11 @@ extensions = [ "sphinx.ext.napoleon", "sphinx.ext.todo", # Custom Pybricks extensions + "awaitable", "blockimg", "color", "classlink", + "docstring_signature", "requirements", "requirements-static", "versionchanged", @@ -133,8 +135,9 @@ nitpick_ignore = [ # not sure why, but this is needed for typing.IO in uselect nitpick_ignore.append(("py:obj", "typing.IO")) -# MaybeAwaitable* stub types have no documented target; rendering hacks were -# dropped with the Sphinx upgrade (may be revisited later). +# MaybeAwaitable* stub types have no documented target; the awaitable +# extension renders them as an "await" prefix instead, but the raw names can +# still leak into signatures (e.g. overloads), so suppress those warnings. nitpick_ignore_regex = [ ("py:class", r"MaybeAwaitable\w*"), ] diff --git a/doc/common/extensions/awaitable.py b/doc/common/extensions/awaitable.py new file mode 100644 index 0000000..2f54541 --- /dev/null +++ b/doc/common/extensions/awaitable.py @@ -0,0 +1,61 @@ +"""Show ``await`` in front of multitasking functions and methods. + +The Pybricks API returns awaitable objects when a run loop is active but +blocks otherwise. Such functions are annotated with ``MaybeAwaitable*`` +return types. This extension: + +* makes autodoc treat functions/methods returning ``MaybeAwaitable*`` as + async (there is no public hook for this, so ``is_async`` is patched); +* renders the signature prefix as ``await`` (linked to the multitasking + section in tools) instead of ``async``, which better matches how users + call these functions. +""" + +from typing import Sequence + +from docutils import nodes +from sphinx.addnodes import desc_sig_keyword +from sphinx.application import Sphinx +from sphinx.domains.python import PyFunction, PyMethod, type_to_xref +from sphinx.ext.autodoc._property_types import _FunctionDefProperties + + +def _is_async(self: _FunctionDefProperties) -> bool: + if "async" in self.properties: + return True + + try: + return_type = self._obj.__annotations__["return"] + except Exception: + return False + + return "MaybeAwaitable" in str(return_type) + + +class _AwaitPrefixMixin: + """Replaces the ``async`` keyword prefix with a linked ``await``.""" + + def get_signature_prefix(self, sig: str) -> Sequence[nodes.Node]: + prefix = [] + for node in super().get_signature_prefix(sig): + if isinstance(node, desc_sig_keyword) and node.astext() == "async": + node = type_to_xref("await", self.env, suppress_prefix=True) + prefix.append(node) + return prefix + + +class PybricksPyFunction(_AwaitPrefixMixin, PyFunction): + pass + + +class PybricksPyMethod(_AwaitPrefixMixin, PyMethod): + pass + + +def setup(app: Sphinx): + _FunctionDefProperties.is_async = property(_is_async) + + app.add_directive_to_domain("py", "function", PybricksPyFunction, override=True) + app.add_directive_to_domain("py", "method", PybricksPyMethod, override=True) + + return {"parallel_read_safe": True} diff --git a/doc/common/extensions/docstring_signature.py b/doc/common/extensions/docstring_signature.py new file mode 100644 index 0000000..c1cc983 --- /dev/null +++ b/doc/common/extensions/docstring_signature.py @@ -0,0 +1,51 @@ +"""Make explicit docstring signatures win over ``@overload`` signatures. + +Workaround for https://github.com/sphinx-doc/sphinx/issues/10436: when a +function or method has overloads, autodoc unconditionally replaces the +signature(s) found in the docstring with the overload signatures. Classes +already behave correctly, so this only patches the function/method path. There +is no event hook for this in autodoc's new (Sphinx 9) pipeline, so +``_format_signatures`` is wrapped: when a docstring signature is present, +``autodoc_typehints`` is set to ``'none'`` for that single call, which +disables only the overload substitution branch. +""" + +from sphinx.application import Sphinx +from sphinx.ext.autodoc._dynamic import _loader, _signatures +from sphinx.ext.autodoc._shared import _AutodocConfig + +_orig_format_signatures = _signatures._format_signatures + + +def _format_signatures(**kwargs): + config: _AutodocConfig = kwargs["config"] + docstrings = kwargs.get("docstrings") + options = kwargs["options"] + props = kwargs["props"] + + if ( + kwargs.get("args") is None + and docstrings + and config.autodoc_docstring_signature + and config.autodoc_typehints != "none" + and props.obj_type in {"function", "method", "decorator"} + ): + # Probe on a copy: extraction strips signature lines from docstrings. + docstring_signatures = _signatures._extract_signatures_from_docstrings( + [list(lines) for lines in docstrings], + props=props, + tab_width=options._tab_width, + ) + if docstring_signatures: + values = {name: getattr(config, name) for name in _AutodocConfig.__slots__} + values["autodoc_typehints"] = "none" + kwargs["config"] = _AutodocConfig(**values) + + return _orig_format_signatures(**kwargs) + + +def setup(app: Sphinx): + _signatures._format_signatures = _format_signatures + _loader._format_signatures = _format_signatures + + return {"parallel_read_safe": True}