#!/usr/bin/env python3
"""
A Sphinx directive to specify that a module has extra requirements, and show how to install them.
:copyright: Copyright (c) 2020 by Dominic Davis-Foster <dominic@davis-foster.co.uk>
:license: MIT, see LICENSE for details.
"""
# Based on https://github.com/agronholm/sphinx-autodoc-typehints
# Copyright (c) Alex Grönholm
# MIT Licensed
#
# stdlib
import inspect
import re
import string
import typing
from typing import Any, Dict
# 3rd party
from sphinx.application import Sphinx
from sphinx.util.inspect import signature as Signature
__author__: str = "Dominic Davis-Foster"
__copyright__: str = "2020 Dominic Davis-Foster"
__license__: str = "MIT"
__version__: str = "0.0.7"
__email__: str = "dominic@davis-foster.co.uk"
[docs]def process_docstring(app: Sphinx, what, name, obj, options, lines: typing.List[str]) -> None:
"""
Add default values to the docstring.
:param app:
:param what:
:type what:
:param name:
:type name:
:param obj:
:type obj:
:param options:
:type options:
:param lines: List of strings representing the current contents of the docstring.
"""
if isinstance(obj, property):
return None
if callable(obj):
if inspect.isclass(obj):
obj = getattr(obj, '__init__')
obj = inspect.unwrap(obj)
try:
signature = Signature(obj)
except ValueError:
return None
default_description_format: str = app.config.default_description_format # type: ignore
for argname, param in signature.parameters.items():
if argname.endswith('_'):
argname = f'{argname[:-1]}\\_'
default_value = param.default
formatted_annotation = None
# Get the default value from the signature
if default_value is not inspect._empty: # type: ignore
if isinstance(default_value, bool):
formatted_annotation = f":py:obj:`{default_value}`"
elif default_value is None:
formatted_annotation = f":py:obj:`None`"
elif isinstance(default_value, str):
formatted_annotation = f"``'{default_value.replace(' ', '␣')}'``"
else:
formatted_annotation = f"``{default_value!r}``"
# Check if the user has overridden the default value in the docstring
default_searchfor = [f':{field} {argname}:' for field in ('default', 'Default')]
for i, line in enumerate(lines):
for search_string in default_searchfor:
if line.startswith(search_string):
formatted_annotation = line.split(search_string)[-1].lstrip(" ")
lines.remove(line)
break
# Check the user hasn't turned the default argument off
no_default_searchfor = re.compile(fr"^:(No|no)[-_](default|Default) {argname}:")
for i, line in enumerate(lines):
if no_default_searchfor.match(line):
formatted_annotation = None
break
# Add the default value
searchfor = [f':{field} {argname}:' for field in ('param', 'parameter', 'arg', 'argument')]
insert_index = None
for i, line in enumerate(lines):
if any(line.startswith(search_string) for search_string in searchfor):
insert_index = i
break
if formatted_annotation is not None:
if insert_index is not None:
# Look ahead to find the index of the next unindented line, and insert before it.
for idx, line in enumerate(lines[insert_index + 1:]):
if not line.startswith(" "):
# Ensure the previous line has a fullstop at the end.
if lines[insert_index + idx][-1] not in ".,;:":
lines[insert_index + idx] += '.'
lines.insert(
insert_index + 1 + idx,
f" {default_description_format % formatted_annotation}."
)
break
# Remove all remaining :default *: lines
for i, line in enumerate(lines):
if re.match(r"^:(default|Default) ", line):
lines.remove(line)
# Remove all remaining :no-default *: lines
for i, line in enumerate(lines):
if re.match(r"^:(No|no)[-_](default|Default) ", line):
lines.remove(line)
return None
[docs]def setup(app: Sphinx) -> Dict[str, Any]:
"""
Setup Sphinx Extension.
:param app:
:return:
"""
# Custom formatting for the default value indication
app.add_config_value('default_description_format', "Default %s", 'env')
app.connect('builder-inited', process_default_format)
app.connect('autodoc-process-docstring', process_docstring)
return {
"version": __version__,
"parallel_read_safe": True,
"parallel_write_safe": True,
}