Skip to content

Commit

Permalink
Make custom documenter class which handles SIP overloaded methods
Browse files Browse the repository at this point in the history
Fixes #18
  • Loading branch information
nyalldawson committed Sep 11, 2024
1 parent 2e16026 commit 18bda83
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 4 deletions.
7 changes: 6 additions & 1 deletion conf.in.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,10 @@ def linkcode_resolve(domain, info):
return None
if not info["module"]:
return None
module = info["module"].split(".")[1]
try:
module = info["module"].split(".")[1]
except IndexError:
return None
if module == "_3d":
module = "3d"
try:
Expand All @@ -248,13 +251,15 @@ def setup(app):
try:
from autoautosummary import AutoAutoSummary
from process_links import (
OverloadedPythonMethodDocumenter,
process_bases,
process_docstring,
process_signature,
skip_member,
)

app.add_directive("autoautosummary", AutoAutoSummary)
app.add_autodocumenter(OverloadedPythonMethodDocumenter)
app.connect("autodoc-process-signature", process_signature)
app.connect("autodoc-process-docstring", process_docstring)
app.connect("autodoc-skip-member", skip_member)
Expand Down
135 changes: 132 additions & 3 deletions process_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
# This logic has been copied from the existing extension with some tuning for PyQGIS

import enum
import inspect
import re

import yaml

with open("pyqgis_conf.yml") as f:
cfg = yaml.safe_load(f)

from sphinx.ext.autodoc import AttributeDocumenter, Documenter
from sphinx.ext.autodoc import AttributeDocumenter, Documenter, MethodDocumenter

old_get_doc = Documenter.get_doc

Expand Down Expand Up @@ -183,10 +184,21 @@ def inject_args(_args, _lines):

# add return type and param type
elif what != "class" and not isinstance(obj, enum.EnumMeta) and obj.__doc__:

signature = obj.__doc__.split("\n")[0]
# default to taking the signature from the lines we've already processed.
# This is because we want the output processed earlier via the
# OverloadedPythonMethodDocumenter class, so that we are only
# looking at the docs relevant to the specific overload we are
# currently processing
signature = lines[0]
if signature != "":
match = py_ext_sig_re.match(signature)
if not match:
# fallback to default docstring, just to be safe...
signature = obj.__doc__.split("\n")[0]
match = py_ext_sig_re.match(signature)
else:
del lines[0]

if not match:
# print(obj)
if name not in cfg["non-instantiable"]:
Expand Down Expand Up @@ -241,3 +253,120 @@ def process_bases(app, name, obj, option, bases: list) -> None:
# replace 'sip.wrapper' base class with 'object'
if base.__name__ == "wrapper":
bases[i] = object


class OverloadedPythonMethodDocumenter(MethodDocumenter):
objtype = "method"
priority = MethodDocumenter.priority

@classmethod
def can_document_member(cls, member, membername, isattr, parent):
return MethodDocumenter.can_document_member(member, membername, isattr, parent)

@staticmethod
def parse_signatures(docstring):
"""
Extracts each signature from a sip generated docstring
"""
signature_pattern = r"(\w+\(.*?\))(?:\s*->\s*\(?\w+(?:,\s*\w+)*\)?)?"
res = []
current_signature_docs = []
for line in docstring.split("\n"):

if re.match(signature_pattern, line):
if current_signature_docs:
res.append(current_signature_docs)

# Extract just the parameter part of each signature
params = re.search(r"\((.*?)\)", line).group()
current_signature_docs = [params]
else:
current_signature_docs += [line]
if current_signature_docs:
res.append(current_signature_docs)

return res

def parse_signature_blocks(self, docstring):
"""
Extracts each signature from a sip generated docstring, and
returns each signature in a tuple with the docs for just
that signature.
"""
res = []
current_sig = ""
current_desc = ""
for line in docstring.split("\n"):
match = re.match(
r"^\s*\w+(\([^)]*\)(?:\s*->\s*[^:\n]+)?)\s*((?:(?!\w+\().)*)\s*$", line
)
if match:
if current_sig:
res.append((current_sig, current_desc))
current_sig = match.group(1)
current_desc = match.group(2)
if current_desc:
current_desc += "\n"
else:
current_desc += line + "\n"

if current_sig:
res.append((current_sig, current_desc))

return res

def add_content(self, more_content):
"""
Parse the docstring to get all signatures and their descriptions
"""
sourcename = self.get_sourcename()
docstring = inspect.getdoc(self.object)
if docstring:
# does this method have multiple overrides?
signature_blocks = self.parse_signature_blocks(docstring)

if len(signature_blocks) <= 1:
# nope, just use standard formatter then!
super().add_content(more_content)
return

# add a method output for EVERY override
for i, (signature, description) in enumerate(signature_blocks):
# this pattern is used in the autodoc source!
old_indent = self.indent
new_indent = (
" "
* len(self.content_indent)
* (len(self.indent) // len(self.content_indent) - 1)
)
self.indent = new_indent
# skip this class, go straight to super. The add_directive_header
# implementation from this class will omit the signatures of
# overridden methods
super().add_directive_header(signature)
self.indent = old_indent

if i > 0:
# we can only index the first signature!
self.add_line(":no-index:", sourcename)

self.add_line("", sourcename)

doc_for_this_override = self.object_name + signature + "\n" + description
for line in self.process_doc([doc_for_this_override.split("\n")]):
self.add_line(line, sourcename)

def add_directive_header(self, sig):
# Parse the docstring to get all signatures
docstring = inspect.getdoc(self.object)
if docstring:
signatures = self.parse_signatures(docstring)
else:
signatures = [sig] # Use the original signature if no docstring

if len(signatures) > 1:
# skip overridden method directive headers here, we will generate
# them later when we pass the actual docstring
return

return super().add_directive_header(sig)

0 comments on commit 18bda83

Please sign in to comment.