from sphinx.domains.python import PythonDomain # NOQA
# from sphinx.application import Sphinx # NOQA
from typing import Any, List # NOQA
# HACK TO PREVENT EXCESSIVE TIME.
# TODO: FIXME FOR REAL
MAX_TIME_MINUTES = None
if MAX_TIME_MINUTES:
import ubelt # NOQA
TIMER = ubelt.Timer()
TIMER.tic()
[docs]
class PatchedPythonDomain(PythonDomain):
"""
References:
https://github.com/sphinx-doc/sphinx/issues/3866
"""
[docs]
def resolve_xref(
self, env, fromdocname, builder, type, target, node, contnode
):
"""
Helps to resolves cross-references
"""
if target.startswith('ub.'):
target = 'ubelt.' + target[3]
if target.startswith('xdoc.'):
target = 'xdoctest.' + target[3]
return_value = super(PatchedPythonDomain, self).resolve_xref(
env, fromdocname, builder, type, target, node, contnode
)
return return_value
[docs]
class GoogleStyleDocstringProcessor:
"""
A small extension that runs after napoleon and reformats erotemic-flavored
google-style docstrings for sphinx.
"""
def __init__(self, autobuild=1):
self.debug = 0
self.registry = {}
if autobuild:
self._register_builtins()
[docs]
def register_section(self, tag, alias=None):
"""
Decorator that adds a custom processing function for a non-standard
google style tag. The decorated function should accept a list of
docstring lines, where the first one will be the google-style tag that
likely needs to be replaced, and then return the appropriate sphinx
format (TODO what is the name? Is it just RST?).
"""
alias = [] if alias is None else alias
alias = [alias] if not isinstance(alias, (list, tuple, set)) else alias
alias.append(tag)
alias = tuple(alias)
# TODO: better tag patterns
def _wrap(func):
self.registry[tag] = {
'tag': tag,
'alias': alias,
'func': func,
}
return func
return _wrap
[docs]
def _register_builtins(self):
"""
Adds definitions I like of CommandLine, TextArt, and Ignore
"""
@self.register_section(tag='CommandLine')
def commandline(lines):
new_lines = []
new_lines.append('.. rubric:: CommandLine')
new_lines.append('')
new_lines.append('.. code-block:: bash')
new_lines.append('')
new_lines.extend(lines[1:])
return new_lines
@self.register_section(
tag='SpecialExample', alias=['Benchmark', 'Sympy', 'Doctest']
)
def benchmark(lines):
import textwrap
new_lines = []
tag = lines[0].replace(':', '').strip()
# new_lines.append(lines[0]) # TODO: it would be nice to change the tagline.
# new_lines.append('')
new_lines.append('.. rubric:: {}'.format(tag))
new_lines.append('')
new_text = textwrap.dedent('\n'.join(lines[1:]))
redone = new_text.split('\n')
new_lines.extend(redone)
# import ubelt as ub
# print('new_lines = {}'.format(ub.urepr(new_lines, nl=1)))
# new_lines.append('')
return new_lines
@self.register_section(tag='TextArt', alias=['Ascii'])
def text_art(lines):
new_lines = []
new_lines.append('.. rubric:: TextArt')
new_lines.append('')
new_lines.append('.. code-block:: bash')
new_lines.append('')
new_lines.extend(lines[1:])
return new_lines
# @self.register_section(tag='TODO', alias=['.. todo::'])
# def todo_section(lines):
# """
# Fixup todo sections
# """
# import xdev
# xdev.embed()
# import ubelt as ub
# print('lines = {}'.format(ub.urepr(lines, nl=1)))
# return new_lines
@self.register_section(tag='Ignore')
def ignore(lines):
return []
[docs]
def process(self, lines):
"""
Example:
>>> import ubelt as ub
>>> self = GoogleStyleDocstringProcessor()
>>> lines = ['Hello world',
>>> '',
>>> 'CommandLine:',
>>> ' hi',
>>> '',
>>> 'CommandLine:',
>>> '',
>>> ' bye',
>>> '',
>>> 'TextArt:',
>>> '',
>>> ' 1',
>>> ' 2',
>>> '',
>>> ' 345',
>>> '',
>>> 'Foobar:',
>>> '',
>>> 'TextArt:']
>>> new_lines = self.process(lines[:])
>>> print(chr(10).join(new_lines))
"""
orig_lines = lines[:]
new_lines = []
curr_mode = '__doc__'
accum = []
def accept():
"""called when we finish reading a section"""
if curr_mode == '__doc__':
# Keep the lines as-is
new_lines.extend(accum)
else:
# Process this section with the given function
regitem = self.registry[curr_mode]
func = regitem['func']
fixed = func(accum)
new_lines.extend(fixed)
# Reset the accumulator for the next section
accum[:] = []
for line in orig_lines:
found = None
for regitem in self.registry.values():
if line.startswith(regitem['alias']):
found = regitem['tag']
break
if not found and line and not line.startswith(' '):
# if the line startswith anything but a space, we are no longer
# in the previous nested scope. NOTE: This assumption may not
# be general, but it works for my code.
found = '__doc__'
if found:
# New section is found, accept the previous one and start
# accumulating the new one.
accept()
curr_mode = found
accum.append(line)
# Finalize the last section
accept()
lines[:] = new_lines
# make sure there is a blank line at the end
if lines and lines[-1]:
lines.append('')
return lines
[docs]
def process_docstring_callback(
self,
app,
what_: str,
name: str,
obj: Any,
options: Any,
lines: List[str],
) -> None:
"""
Callback to be registered to autodoc-process-docstring
Custom process to transform docstring lines Remove "Ignore" blocks
Args:
app (sphinx.application.Sphinx): the Sphinx application object
what (str):
the type of the object which the docstring belongs to (one of
"module", "class", "exception", "function", "method", "attribute")
name (str): the fully qualified name of the object
obj: the object itself
options: the options given to the directive: an object with
attributes inherited_members, undoc_members, show_inheritance
and noindex that are true if the flag option of same name was
given to the auto directive
lines (List[str]): the lines of the docstring, see above
References:
https://www.sphinx-doc.org/en/1.5.1/_modules/sphinx/ext/autodoc.html
https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html
"""
if self.debug:
print(
f'ProcessDocstring: name={name}, what_={what_}, num_lines={len(lines)}'
)
# print('BEFORE:')
# import ubelt as ub
# print('lines = {}'.format(ub.urepr(lines, nl=1)))
self.process(lines)
# docstr = '\n'.join(lines)
# if 'Convert the Mask' in docstr:
# import xdev
# xdev.embed()
# if 'keys in this dictionary ' in docstr:
# import xdev
# xdev.embed()
render_doc_images = 0
if MAX_TIME_MINUTES and TIMER.toc() > (60 * MAX_TIME_MINUTES):
render_doc_images = False # FIXME too slow on RTD
if render_doc_images:
# DEVELOPING
if any('REQUIRES(--show)' in line for line in lines):
# import xdev
# xdev.embed()
create_doctest_figure(app, obj, name, lines)
FIX_EXAMPLE_FORMATTING = 1
if FIX_EXAMPLE_FORMATTING:
for idx, line in enumerate(lines):
if line == 'Example:':
lines[idx] = '**Example:**'
lines.insert(idx + 1, '')
REFORMAT_SECTIONS = 0
if REFORMAT_SECTIONS:
REFORMAT_RETURNS = 0
REFORMAT_PARAMS = 0
docstr = SphinxDocstring(lines)
if REFORMAT_PARAMS:
for found in docstr.find_tagged_lines('Parameters'):
print(found['text'])
edit_slice = found['edit_slice']
# TODO: figure out how to do this.
# # file = 'foo.rst'
# import rstparse
# rst = rstparse.Parser()
# import io
# rst.read(io.StringIO(found['text']))
# rst.parse()
# for line in rst.lines:
# print(line)
# # found['text']
# import docutils
# settings = docutils.frontend.OptionParser(
# components=(docutils.parsers.rst.Parser,)
# ).get_default_values()
# document = docutils.utils.new_document('<tempdoc>', settings)
# from docutils.parsers import rst
# rst.Parser().parse(found['text'], document)
if REFORMAT_RETURNS:
for found in docstr.find_tagged_lines('returns'):
# FIXME: account for new slice with -2 offset
edit_slice = found['edit_slice']
text = found['text']
new_lines = []
for para in text.split('\n\n'):
indent = para[: len(para) - len(para.lstrip())]
new_paragraph = indent + paragraph(para)
new_lines.append(new_paragraph)
new_lines.append('')
new_lines = new_lines[:-1]
lines[edit_slice] = new_lines
# print('AFTER:')
# print('lines = {}'.format(ub.urepr(lines, nl=1)))
# if name == 'kwimage.Affine.translate':
# import sys
# sys.exit(1)
[docs]
class SphinxDocstring:
"""
Helper to parse and modify sphinx docstrings
"""
def __init__(docstr, lines):
docstr.lines = lines
# FORMAT THE RETURNS SECTION A BIT NICER
import re
tag_pat = re.compile(r'^:(\w*):')
directive_pat = re.compile(r'^.. (\w*)::\s*(\w*)')
# Split by sphinx types, mark the line offset where they start / stop
sphinx_parts = []
for idx, line in enumerate(lines):
tag_match = tag_pat.search(line)
directive_match = directive_pat.search(line)
if tag_match:
tag = tag_match.groups()[0]
sphinx_parts.append(
{
'tag': tag,
'start_offset': idx,
'type': 'tag',
}
)
elif directive_match:
tag = directive_match.groups()[0]
sphinx_parts.append(
{
'tag': tag,
'start_offset': idx,
'type': 'directive',
}
)
prev_offset = len(lines)
for part in sphinx_parts[::-1]:
part['end_offset'] = prev_offset
prev_offset = part['start_offset']
docstr.sphinx_parts = sphinx_parts
if 0:
for line in lines:
print(line)
[docs]
def find_tagged_lines(docstr, tag):
for part in docstr.sphinx_parts[::-1]:
if part['tag'] == tag:
edit_slice = slice(part['start_offset'], part['end_offset'])
return_section = docstr.lines[edit_slice]
text = '\n'.join(return_section)
found = {
'edit_slice': edit_slice,
'text': text,
}
yield found
[docs]
def paragraph(text):
r"""
Wraps multi-line strings and restructures the text to remove all newlines,
heading, trailing, and double spaces.
Useful for writing log messages
Args:
text (str): typically a multiline string
Returns:
str: the reduced text block
"""
import re
out = re.sub(r'\s\s*', ' ', text).strip()
return out
[docs]
def postprocess_hyperlinks(app, doctree, docname):
"""
Extension to fixup hyperlinks.
This should be connected to the Sphinx application's
"autodoc-process-docstring" event.
"""
# Your hyperlink postprocessing logic here
import pathlib
from docutils import nodes
for node in doctree.traverse(nodes.reference):
if 'refuri' in node.attributes:
refuri = node.attributes['refuri']
if '.rst' in refuri:
if 'source' in node.document:
fpath = pathlib.Path(node.document['source'])
parent_dpath = fpath.parent
if (parent_dpath / refuri).exists():
node.attributes['refuri'] = refuri.replace(
'.rst', '.html'
)
else:
raise AssertionError
[docs]
def fix_rst_todo_section(lines):
# new_lines = []
for line in lines:
...
...
[docs]
def setup(app):
import sphinx
import sphinx.application
app: sphinx.application.Sphinx = app
app.add_domain(PatchedPythonDomain, override=True)
app.connect('doctree-resolved', postprocess_hyperlinks)
docstring_processor = GoogleStyleDocstringProcessor()
# https://stackoverflow.com/questions/26534184/can-sphinx-ignore-certain-tags-in-python-docstrings
app.connect(
'autodoc-process-docstring',
docstring_processor.process_docstring_callback,
)
def copy(src, dst):
import shutil
print(f'Copy {src} -> {dst}')
assert src.exists()
if not dst.parent.exists():
dst.parent.mkdir()
shutil.copy(src, dst)
### Hack for kwcoco: TODO: figure out a way for the user to configure this.
HACK_FOR_KWCOCO = 0
if HACK_FOR_KWCOCO:
import pathlib
doc_outdir = pathlib.Path(app.outdir) / 'auto'
doc_srcdir = pathlib.Path(app.srcdir) / 'auto'
mod_dpath = doc_srcdir / '../../../kwcoco'
src_fpath = mod_dpath / 'coco_schema.json'
copy(src_fpath, doc_outdir / src_fpath.name)
copy(src_fpath, doc_srcdir / src_fpath.name)
src_fpath = mod_dpath / 'coco_schema_informal.rst'
copy(src_fpath, doc_outdir / src_fpath.name)
copy(src_fpath, doc_srcdir / src_fpath.name)
return app