Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the autodoc directive to improve overloaded functions #160

Merged
merged 5 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ install:
pip3 install . --use-feature=in-tree-build

black:
black setup.py tests/*.py libsemigroups_pybind11/*.py
black setup.py tests/*.py libsemigroups_pybind11/*.py docs/source/conf.py

check: doctest
pytest -vv tests/test_*.py

lint:
pylint --exit-zero setup.py tests/*.py libsemigroups_pybind11/*.py
pylint --exit-zero setup.py tests/*.py libsemigroups_pybind11/*.py docs/source/conf.py
cpplint src/*.hpp src/*.cpp

coverage:
Expand Down
198 changes: 170 additions & 28 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,150 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# pylint:disable=redefined-builtin, invalid-name, too-many-arguments, unbalanced-tuple-unpacking, unused-argument
"""
This provides configuration for the generation of the docs
"""

import os
import re
import subprocess
import sphinx_rtd_theme
import sys
from sphinx.util import logging
from sphinx.ext.autodoc.directive import AutodocDirective
import sphinx_rtd_theme
from sphinx.addnodes import desc_content, desc, index
from sphinx.ext.autodoc.directive import (
AutodocDirective,
DocumenterBridge,
process_documenter_options,
parse_generated_content,
)
from sphinx.util import logging
from sphinx.util.docutils import StringList

logger = logging.getLogger(__name__)

# Custom Directive


class ExtendedAutodocDirective(AutodocDirective):
"""A an extended directive class for all autodoc directives.

It performs the same as AutodocDirective, with the additional ability to
display only the docstring, everything but the docstring, and also formats
overloaded functions nicely.
"""

def fix_overloads(self, content):
"""Indent overloaded function documentation and format signatures"""
overloading = False
output = StringList(content)
offset = 0 # How many additional lines we have added to output
indent = " " # How much to indent overloaded functions by
directive_seen = False
for i, line in enumerate(content):
if line == "":
continue
# Find what directive to use
directive_match = re.search(r"\.\. py:.+?::", line)
if directive_match is not None:
current_directive = directive_match.group(0)
directive_seen = True

if not directive_seen:
continue

# Stop overloading when we see a new directive
if current_directive in line:
overloading = False
continue

# Start overloading and capture the name of the overloaded function
if "Overloaded function." in line:
overloading = True
m = re.search(r"\s+?\d. (.*?)\(", content[i + 2])
overloaded_function = m.group(1)
overload_counter = 1
continue

if overloading:
if f"{overload_counter}. {overloaded_function}" in line:
# Capture the initial indent and the function signature
m = re.match(r"(\s+?)\d. (.*)", line)
parent_indent = m.group(1)
# Make replacements in signature
signature = change_sig(
name=self.arguments[0], signature=m.group(2)
)[0]

# Add adjusted content to the output
new_line = f"{parent_indent[:-3]}{indent}{current_directive} {signature}"
output.data[i + offset] = new_line
output.insert(
i + offset + 1,
StringList([f"{parent_indent}{indent}:no-index:"]),
)
overload_counter += 1
offset += 1
else:
output.data[i + offset] = indent + line
return output

def basic_run(self):
"""Generate and parse the docstring

This is almost identical to AutodocDirective.run(), with the added step
that allows for better overloaded functions.

See:
https://github.com/sphinx-doc/sphinx/blob/master/sphinx/ext/autodoc/directive.py
"""
reporter = self.state.document.reporter

try:
source, lineno = reporter.get_source_and_line(self.lineno)
except AttributeError:
source, lineno = (None, None)
logger.debug(
"[autodoc] %s:%s: input:\n%s", source, lineno, self.block_text
)

# look up target Documenter
objtype = self.name[4:] # strip prefix (auto-).
doccls = self.env.app.registry.documenters[objtype]

# process the options with the selected documenter's option_spec
try:
documenter_options = process_documenter_options(
doccls, self.config, self.options
)
except (KeyError, ValueError, TypeError) as exc:
# an option is either unknown or has a wrong type
logger.error(
"An option to %s is either unknown or has an invalid value: %s",
self.name,
exc,
location=(self.env.docname, lineno),
)
return []

# generate the output
params = DocumenterBridge(
self.env, reporter, documenter_options, lineno, self.state
)
documenter = doccls(params, self.arguments[0])
documenter.generate(more_content=self.content)
if not params.result:
return []

logger.debug("[autodoc] output:\n%s", "\n".join(params.result))

# record all filenames as dependencies -- this will at least
# partially make automatic invalidation possible
for fn in params.record_dependencies:
self.state.document.settings.record_dependencies.add(fn)

params.result = self.fix_overloads(params.result)

result = parse_generated_content(self.state, params.result, documenter)
return result

def run(self):
# Change the name so that AutodocDirective knows which documenter class
# to use
self.name = "ext_" + self.name[8:]
"""Handle custom options and then generate parsed output"""

if "doc-only" in self.options and "no-doc" in self.options:
logger.warning("Cannot set both 'doc-only' and 'no-doc' options.")
Expand All @@ -29,7 +153,6 @@ def run(self):
if "doc-only" in self.options:
# delete option so Autodoc Directive doesn't complain
del self.options["doc-only"]
self.options["no-index"] = True
self.options["noindex"] = True
return self.doc_only_run()

Expand All @@ -38,11 +161,11 @@ def run(self):
del self.options["no-doc"]
return self.no_doc_run()

# Behave like AutodocDirective if nothing extra is needed
return super().run()
return self.basic_run()

def doc_only_run(self):
content = super().run()
"""Format parsed text to contain only docstring only"""
content = self.basic_run()

if not content:
return []
Expand All @@ -61,7 +184,8 @@ def doc_only_run(self):
return docstring

def no_doc_run(self):
content = super().run()
"""Format parsed text to contain everything but docstring"""
content = self.basic_run()

if not content:
return []
Expand Down Expand Up @@ -144,7 +268,10 @@ def no_doc_run(self):
# replacements will be performed globally. Hyperlinks will be added in the
# signature if "good type" is a valid (potentially user defined) python type
type_replacements = {
r"libsemigroups::Presentation<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >": r"Presentation",
(
r"libsemigroups::Presentation<std::__cxx11::basic_string<char, "
r"std::char_traits<char>, std::allocator<char> > >"
): r"Presentation",
r"libsemigroups::BMat8": r"BMat8",
r"libsemigroups::WordGraph<unsigned int>": r"WordGraph",
r"libsemigroups::Gabow<unsigned int>": r"Gabow",
Expand All @@ -153,36 +280,51 @@ def no_doc_run(self):
# This dictionary should be of the form class_name -> (pattern, repl), where
# "pattern" should be replaced by "repl" in the signature of all functions in
# "class_name"
class_specific_replacements = {"RowActionBMat8": (r"\bBMat8\b", "Element")}
class_specific_replacements = {
"RowActionBMat8": [(r"\bBMat8\b", "Element")],
}


def sub_if_not_none(pattern, repl, *strings):
"""Make regex replacement on inputs that are not None"""
out = []
for string in strings:
if string is None:
out.append(string)
else:
out.append(re.sub(pattern, repl, string))
if len(out) == 1:
return out[0]
return out


def change_sig(app, what, name, obj, options, signature, return_annotation):
# if what in to_replace:
def change_sig(
app=None,
what=None,
name=None,
obj=None,
options=None,
signature=None,
return_annotation=None,
):
"""Make type replacement in function signatures"""
for class_name, repl_pairs in class_specific_replacements.items():
if class_name in name:
for find, repl in repl_pairs:
signature, return_annotation = sub_if_not_none(
find, repl, signature, return_annotation
)

for typename, repl in type_replacements.items():
signature, return_annotation = sub_if_not_none(
typename, repl, signature, return_annotation
)

for class_name, repl_pair in class_specific_replacements.items():
if class_name in name:
find, repl = repl_pair
signature, return_annotation = sub_if_not_none(
find, repl, signature, return_annotation
)
return signature, return_annotation


def setup(app):
"""Add custom behaviour"""
app.connect("autodoc-process-signature", change_sig)
app.add_directive("ext_autoclass", ExtendedAutodocDirective)
app.add_directive("ext_autofunction", ExtendedAutodocDirective)
app.add_directive("autoclass", ExtendedAutodocDirective)
app.add_directive("autofunction", ExtendedAutodocDirective)
app.add_directive("automodule", ExtendedAutodocDirective)
2 changes: 1 addition & 1 deletion docs/source/data-structures/misc/reporter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Reporter
========

.. ext_autoclass:: Reporter
.. autoclass:: Reporter
:doc-only:
:class-doc-from: class

Expand Down
4 changes: 2 additions & 2 deletions docs/source/data-structures/misc/runner.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Runner
======

.. ext_autoclass:: Runner
.. autoclass:: Runner
:doc-only:
:class-doc-from: class

Expand Down Expand Up @@ -38,7 +38,7 @@ Contents
Full API
--------

.. ext_autoclass:: Runner
.. autoclass:: Runner
:no-doc:
:members:

Expand Down