usse/scrape/venv/lib/python3.10/site-packages/sphinx/ext/napoleon/docstring.py
2023-12-22 15:26:01 +01:00

1364 lines
48 KiB
Python

"""Classes for docstring parsing and formatting."""
from __future__ import annotations
import collections
import contextlib
import inspect
import re
from functools import partial
from typing import TYPE_CHECKING, Any, Callable
from sphinx.locale import _, __
from sphinx.util import logging
from sphinx.util.typing import get_type_hints, stringify_annotation
if TYPE_CHECKING:
from sphinx.application import Sphinx
from sphinx.config import Config as SphinxConfig
logger = logging.getLogger(__name__)
_directive_regex = re.compile(r'\.\. \S+::')
_google_section_regex = re.compile(r'^(\s|\w)+:\s*$')
_google_typed_arg_regex = re.compile(r'(.+?)\(\s*(.*[^\s]+)\s*\)')
_numpy_section_regex = re.compile(r'^[=\-`:\'"~^_*+#<>]{2,}\s*$')
_single_colon_regex = re.compile(r'(?<!:):(?!:)')
_xref_or_code_regex = re.compile(
r'((?::(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:`.+?`)|'
r'(?:``.+?``)|'
r'(?::meta .+:.*)|'
r'(?:`.+?\s*(?<!\x00)<.*?>`))')
_xref_regex = re.compile(
r'(?:(?::(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:)?`.+?`)',
)
_bullet_list_regex = re.compile(r'^(\*|\+|\-)(\s+\S|\s*$)')
_enumerated_list_regex = re.compile(
r'^(?P<paren>\()?'
r'(\d+|#|[ivxlcdm]+|[IVXLCDM]+|[a-zA-Z])'
r'(?(paren)\)|\.)(\s+\S|\s*$)')
_token_regex = re.compile(
r"(,\sor\s|\sor\s|\sof\s|:\s|\sto\s|,\sand\s|\sand\s|,\s"
r"|[{]|[}]"
r'|"(?:\\"|[^"])*"'
r"|'(?:\\'|[^'])*')",
)
_default_regex = re.compile(
r"^default[^_0-9A-Za-z].*$",
)
_SINGLETONS = ("None", "True", "False", "Ellipsis")
class Deque(collections.deque):
"""
A subclass of deque that mimics ``pockets.iterators.modify_iter``.
The `.Deque.get` and `.Deque.next` methods are added.
"""
sentinel = object()
def get(self, n: int) -> Any:
"""
Return the nth element of the stack, or ``self.sentinel`` if n is
greater than the stack size.
"""
return self[n] if n < len(self) else self.sentinel
def next(self) -> Any:
if self:
return super().popleft()
else:
raise StopIteration
def _convert_type_spec(_type: str, translations: dict[str, str] | None = None) -> str:
"""Convert type specification to reference in reST."""
if translations is not None and _type in translations:
return translations[_type]
if _type == 'None':
return ':py:obj:`None`'
return f':py:class:`{_type}`'
class GoogleDocstring:
"""Convert Google style docstrings to reStructuredText.
Parameters
----------
docstring : :obj:`str` or :obj:`list` of :obj:`str`
The docstring to parse, given either as a string or split into
individual lines.
config: :obj:`sphinx.ext.napoleon.Config` or :obj:`sphinx.config.Config`
The configuration settings to use. If not given, defaults to the
config object on `app`; or if `app` is not given defaults to the
a new :class:`sphinx.ext.napoleon.Config` object.
Other Parameters
----------------
app : :class:`sphinx.application.Sphinx`, optional
Application object representing the Sphinx process.
what : :obj:`str`, optional
A string specifying the type of the object to which the docstring
belongs. Valid values: "module", "class", "exception", "function",
"method", "attribute".
name : :obj:`str`, optional
The fully qualified name of the object.
obj : module, class, exception, function, method, or attribute
The object to which the docstring belongs.
options : :class:`sphinx.ext.autodoc.Options`, optional
The options given to the directive: an object with attributes
inherited_members, undoc_members, show_inheritance and no_index that
are True if the flag option of same name was given to the auto
directive.
Example
-------
>>> from sphinx.ext.napoleon import Config
>>> config = Config(napoleon_use_param=True, napoleon_use_rtype=True)
>>> docstring = '''One line summary.
...
... Extended description.
...
... Args:
... arg1(int): Description of `arg1`
... arg2(str): Description of `arg2`
... Returns:
... str: Description of return value.
... '''
>>> print(GoogleDocstring(docstring, config))
One line summary.
<BLANKLINE>
Extended description.
<BLANKLINE>
:param arg1: Description of `arg1`
:type arg1: int
:param arg2: Description of `arg2`
:type arg2: str
<BLANKLINE>
:returns: Description of return value.
:rtype: str
<BLANKLINE>
"""
_name_rgx = re.compile(r"^\s*((?::(?P<role>\S+):)?`(?P<name>~?[a-zA-Z0-9_.-]+)`|"
r" (?P<name2>~?[a-zA-Z0-9_.-]+))\s*", re.X)
def __init__(
self,
docstring: str | list[str],
config: SphinxConfig | None = None,
app: Sphinx | None = None,
what: str = '',
name: str = '',
obj: Any = None,
options: Any = None,
) -> None:
self._app = app
if config:
self._config = config
elif app:
self._config = app.config
else:
from sphinx.ext.napoleon import Config
self._config = Config() # type: ignore[assignment]
if not what:
if inspect.isclass(obj):
what = 'class'
elif inspect.ismodule(obj):
what = 'module'
elif callable(obj):
what = 'function'
else:
what = 'object'
self._what = what
self._name = name
self._obj = obj
self._opt = options
if isinstance(docstring, str):
lines = docstring.splitlines()
else:
lines = docstring
self._lines = Deque(map(str.rstrip, lines))
self._parsed_lines: list[str] = []
self._is_in_section = False
self._section_indent = 0
if not hasattr(self, '_directive_sections'):
self._directive_sections: list[str] = []
if not hasattr(self, '_sections'):
self._sections: dict[str, Callable] = {
'args': self._parse_parameters_section,
'arguments': self._parse_parameters_section,
'attention': partial(self._parse_admonition, 'attention'),
'attributes': self._parse_attributes_section,
'caution': partial(self._parse_admonition, 'caution'),
'danger': partial(self._parse_admonition, 'danger'),
'error': partial(self._parse_admonition, 'error'),
'example': self._parse_examples_section,
'examples': self._parse_examples_section,
'hint': partial(self._parse_admonition, 'hint'),
'important': partial(self._parse_admonition, 'important'),
'keyword args': self._parse_keyword_arguments_section,
'keyword arguments': self._parse_keyword_arguments_section,
'methods': self._parse_methods_section,
'note': partial(self._parse_admonition, 'note'),
'notes': self._parse_notes_section,
'other parameters': self._parse_other_parameters_section,
'parameters': self._parse_parameters_section,
'receive': self._parse_receives_section,
'receives': self._parse_receives_section,
'return': self._parse_returns_section,
'returns': self._parse_returns_section,
'raise': self._parse_raises_section,
'raises': self._parse_raises_section,
'references': self._parse_references_section,
'see also': self._parse_see_also_section,
'tip': partial(self._parse_admonition, 'tip'),
'todo': partial(self._parse_admonition, 'todo'),
'warning': partial(self._parse_admonition, 'warning'),
'warnings': partial(self._parse_admonition, 'warning'),
'warn': self._parse_warns_section,
'warns': self._parse_warns_section,
'yield': self._parse_yields_section,
'yields': self._parse_yields_section,
}
self._load_custom_sections()
self._parse()
def __str__(self) -> str:
"""Return the parsed docstring in reStructuredText format.
Returns
-------
unicode
Unicode version of the docstring.
"""
return '\n'.join(self.lines())
def lines(self) -> list[str]:
"""Return the parsed lines of the docstring in reStructuredText format.
Returns
-------
list(str)
The lines of the docstring in a list.
"""
return self._parsed_lines
def _consume_indented_block(self, indent: int = 1) -> list[str]:
lines = []
line = self._lines.get(0)
while (
not self._is_section_break() and
(not line or self._is_indented(line, indent))
):
lines.append(self._lines.next())
line = self._lines.get(0)
return lines
def _consume_contiguous(self) -> list[str]:
lines = []
while (self._lines and
self._lines.get(0) and
not self._is_section_header()):
lines.append(self._lines.next())
return lines
def _consume_empty(self) -> list[str]:
lines = []
line = self._lines.get(0)
while self._lines and not line:
lines.append(self._lines.next())
line = self._lines.get(0)
return lines
def _consume_field(self, parse_type: bool = True, prefer_type: bool = False,
) -> tuple[str, str, list[str]]:
line = self._lines.next()
before, colon, after = self._partition_field_on_colon(line)
_name, _type, _desc = before, '', after
if parse_type:
match = _google_typed_arg_regex.match(before)
if match:
_name = match.group(1).strip()
_type = match.group(2)
_name = self._escape_args_and_kwargs(_name)
if prefer_type and not _type:
_type, _name = _name, _type
if _type and self._config.napoleon_preprocess_types:
_type = _convert_type_spec(_type, self._config.napoleon_type_aliases or {})
indent = self._get_indent(line) + 1
_descs = [_desc] + self._dedent(self._consume_indented_block(indent))
_descs = self.__class__(_descs, self._config).lines()
return _name, _type, _descs
def _consume_fields(self, parse_type: bool = True, prefer_type: bool = False,
multiple: bool = False) -> list[tuple[str, str, list[str]]]:
self._consume_empty()
fields = []
while not self._is_section_break():
_name, _type, _desc = self._consume_field(parse_type, prefer_type)
if multiple and _name:
for name in _name.split(","):
fields.append((name.strip(), _type, _desc))
elif _name or _type or _desc:
fields.append((_name, _type, _desc))
return fields
def _consume_inline_attribute(self) -> tuple[str, list[str]]:
line = self._lines.next()
_type, colon, _desc = self._partition_field_on_colon(line)
if not colon or not _desc:
_type, _desc = _desc, _type
_desc += colon
_descs = [_desc] + self._dedent(self._consume_to_end())
_descs = self.__class__(_descs, self._config).lines()
return _type, _descs
def _consume_returns_section(self, preprocess_types: bool = False,
) -> list[tuple[str, str, list[str]]]:
lines = self._dedent(self._consume_to_next_section())
if lines:
before, colon, after = self._partition_field_on_colon(lines[0])
_name, _type, _desc = '', '', lines
if colon:
if after:
_desc = [after] + lines[1:]
else:
_desc = lines[1:]
_type = before
if (_type and preprocess_types and
self._config.napoleon_preprocess_types):
_type = _convert_type_spec(_type, self._config.napoleon_type_aliases or {})
_desc = self.__class__(_desc, self._config).lines()
return [(_name, _type, _desc)]
else:
return []
def _consume_usage_section(self) -> list[str]:
lines = self._dedent(self._consume_to_next_section())
return lines
def _consume_section_header(self) -> str:
section = self._lines.next()
stripped_section = section.strip(':')
if stripped_section.lower() in self._sections:
section = stripped_section
return section
def _consume_to_end(self) -> list[str]:
lines = []
while self._lines:
lines.append(self._lines.next())
return lines
def _consume_to_next_section(self) -> list[str]:
self._consume_empty()
lines = []
while not self._is_section_break():
lines.append(self._lines.next())
return lines + self._consume_empty()
def _dedent(self, lines: list[str], full: bool = False) -> list[str]:
if full:
return [line.lstrip() for line in lines]
else:
min_indent = self._get_min_indent(lines)
return [line[min_indent:] for line in lines]
def _escape_args_and_kwargs(self, name: str) -> str:
if name.endswith('_') and getattr(self._config, 'strip_signature_backslash', False):
name = name[:-1] + r'\_'
if name[:2] == '**':
return r'\*\*' + name[2:]
elif name[:1] == '*':
return r'\*' + name[1:]
else:
return name
def _fix_field_desc(self, desc: list[str]) -> list[str]:
if self._is_list(desc):
desc = [''] + desc
elif desc[0].endswith('::'):
desc_block = desc[1:]
indent = self._get_indent(desc[0])
block_indent = self._get_initial_indent(desc_block)
if block_indent > indent:
desc = [''] + desc
else:
desc = ['', desc[0]] + self._indent(desc_block, 4)
return desc
def _format_admonition(self, admonition: str, lines: list[str]) -> list[str]:
lines = self._strip_empty(lines)
if len(lines) == 1:
return [f'.. {admonition}:: {lines[0].strip()}', '']
elif lines:
lines = self._indent(self._dedent(lines), 3)
return ['.. %s::' % admonition, ''] + lines + ['']
else:
return ['.. %s::' % admonition, '']
def _format_block(
self, prefix: str, lines: list[str], padding: str | None = None,
) -> list[str]:
if lines:
if padding is None:
padding = ' ' * len(prefix)
result_lines = []
for i, line in enumerate(lines):
if i == 0:
result_lines.append((prefix + line).rstrip())
elif line:
result_lines.append(padding + line)
else:
result_lines.append('')
return result_lines
else:
return [prefix]
def _format_docutils_params(self, fields: list[tuple[str, str, list[str]]],
field_role: str = 'param', type_role: str = 'type',
) -> list[str]:
lines = []
for _name, _type, _desc in fields:
_desc = self._strip_empty(_desc)
if any(_desc):
_desc = self._fix_field_desc(_desc)
field = f':{field_role} {_name}: '
lines.extend(self._format_block(field, _desc))
else:
lines.append(f':{field_role} {_name}:')
if _type:
lines.append(f':{type_role} {_name}: {_type}')
return lines + ['']
def _format_field(self, _name: str, _type: str, _desc: list[str]) -> list[str]:
_desc = self._strip_empty(_desc)
has_desc = any(_desc)
separator = ' -- ' if has_desc else ''
if _name:
if _type:
if '`' in _type:
field = f'**{_name}** ({_type}){separator}'
else:
field = f'**{_name}** (*{_type}*){separator}'
else:
field = f'**{_name}**{separator}'
elif _type:
if '`' in _type:
field = f'{_type}{separator}'
else:
field = f'*{_type}*{separator}'
else:
field = ''
if has_desc:
_desc = self._fix_field_desc(_desc)
if _desc[0]:
return [field + _desc[0]] + _desc[1:]
else:
return [field] + _desc
else:
return [field]
def _format_fields(self, field_type: str, fields: list[tuple[str, str, list[str]]],
) -> list[str]:
field_type = ':%s:' % field_type.strip()
padding = ' ' * len(field_type)
multi = len(fields) > 1
lines: list[str] = []
for _name, _type, _desc in fields:
field = self._format_field(_name, _type, _desc)
if multi:
if lines:
lines.extend(self._format_block(padding + ' * ', field))
else:
lines.extend(self._format_block(field_type + ' * ', field))
else:
lines.extend(self._format_block(field_type + ' ', field))
if lines and lines[-1]:
lines.append('')
return lines
def _get_current_indent(self, peek_ahead: int = 0) -> int:
line = self._lines.get(peek_ahead)
while line is not self._lines.sentinel:
if line:
return self._get_indent(line)
peek_ahead += 1
line = self._lines.get(peek_ahead)
return 0
def _get_indent(self, line: str) -> int:
for i, s in enumerate(line):
if not s.isspace():
return i
return len(line)
def _get_initial_indent(self, lines: list[str]) -> int:
for line in lines:
if line:
return self._get_indent(line)
return 0
def _get_min_indent(self, lines: list[str]) -> int:
min_indent = None
for line in lines:
if line:
indent = self._get_indent(line)
if min_indent is None or indent < min_indent:
min_indent = indent
return min_indent or 0
def _indent(self, lines: list[str], n: int = 4) -> list[str]:
return [(' ' * n) + line for line in lines]
def _is_indented(self, line: str, indent: int = 1) -> bool:
for i, s in enumerate(line): # noqa: SIM110
if i >= indent:
return True
elif not s.isspace():
return False
return False
def _is_list(self, lines: list[str]) -> bool:
if not lines:
return False
if _bullet_list_regex.match(lines[0]):
return True
if _enumerated_list_regex.match(lines[0]):
return True
if len(lines) < 2 or lines[0].endswith('::'):
return False
indent = self._get_indent(lines[0])
next_indent = indent
for line in lines[1:]:
if line:
next_indent = self._get_indent(line)
break
return next_indent > indent
def _is_section_header(self) -> bool:
section = self._lines.get(0).lower()
match = _google_section_regex.match(section)
if match and section.strip(':') in self._sections:
header_indent = self._get_indent(section)
section_indent = self._get_current_indent(peek_ahead=1)
return section_indent > header_indent
elif self._directive_sections:
if _directive_regex.match(section):
for directive_section in self._directive_sections:
if section.startswith(directive_section):
return True
return False
def _is_section_break(self) -> bool:
line = self._lines.get(0)
return (not self._lines or
self._is_section_header() or
(self._is_in_section and
line and
not self._is_indented(line, self._section_indent)))
def _load_custom_sections(self) -> None:
if self._config.napoleon_custom_sections is not None:
for entry in self._config.napoleon_custom_sections:
if isinstance(entry, str):
# if entry is just a label, add to sections list,
# using generic section logic.
self._sections[entry.lower()] = self._parse_custom_generic_section
else:
# otherwise, assume entry is container;
if entry[1] == "params_style":
self._sections[entry[0].lower()] = \
self._parse_custom_params_style_section
elif entry[1] == "returns_style":
self._sections[entry[0].lower()] = \
self._parse_custom_returns_style_section
else:
# [0] is new section, [1] is the section to alias.
# in the case of key mismatch, just handle as generic section.
self._sections[entry[0].lower()] = \
self._sections.get(entry[1].lower(),
self._parse_custom_generic_section)
def _parse(self) -> None:
self._parsed_lines = self._consume_empty()
if self._name and self._what in ('attribute', 'data', 'property'):
res: list[str] = []
with contextlib.suppress(StopIteration):
res = self._parse_attribute_docstring()
self._parsed_lines.extend(res)
return
while self._lines:
if self._is_section_header():
try:
section = self._consume_section_header()
self._is_in_section = True
self._section_indent = self._get_current_indent()
if _directive_regex.match(section):
lines = [section] + self._consume_to_next_section()
else:
lines = self._sections[section.lower()](section)
finally:
self._is_in_section = False
self._section_indent = 0
else:
if not self._parsed_lines:
lines = self._consume_contiguous() + self._consume_empty()
else:
lines = self._consume_to_next_section()
self._parsed_lines.extend(lines)
def _parse_admonition(self, admonition: str, section: str) -> list[str]:
# type (str, str) -> List[str]
lines = self._consume_to_next_section()
return self._format_admonition(admonition, lines)
def _parse_attribute_docstring(self) -> list[str]:
_type, _desc = self._consume_inline_attribute()
lines = self._format_field('', '', _desc)
if _type:
lines.extend(['', ':type: %s' % _type])
return lines
def _parse_attributes_section(self, section: str) -> list[str]:
lines = []
for _name, _type, _desc in self._consume_fields():
if not _type:
_type = self._lookup_annotation(_name)
if self._config.napoleon_use_ivar:
field = ':ivar %s: ' % _name
lines.extend(self._format_block(field, _desc))
if _type:
lines.append(f':vartype {_name}: {_type}')
else:
lines.append('.. attribute:: ' + _name)
if self._opt:
if 'no-index' in self._opt or 'noindex' in self._opt:
lines.append(' :no-index:')
lines.append('')
fields = self._format_field('', '', _desc)
lines.extend(self._indent(fields, 3))
if _type:
lines.append('')
lines.extend(self._indent([':type: %s' % _type], 3))
lines.append('')
if self._config.napoleon_use_ivar:
lines.append('')
return lines
def _parse_examples_section(self, section: str) -> list[str]:
labels = {
'example': _('Example'),
'examples': _('Examples'),
}
use_admonition = self._config.napoleon_use_admonition_for_examples
label = labels.get(section.lower(), section)
return self._parse_generic_section(label, use_admonition)
def _parse_custom_generic_section(self, section: str) -> list[str]:
# for now, no admonition for simple custom sections
return self._parse_generic_section(section, False)
def _parse_custom_params_style_section(self, section: str) -> list[str]:
return self._format_fields(section, self._consume_fields())
def _parse_custom_returns_style_section(self, section: str) -> list[str]:
fields = self._consume_returns_section(preprocess_types=True)
return self._format_fields(section, fields)
def _parse_usage_section(self, section: str) -> list[str]:
header = ['.. rubric:: Usage:', '']
block = ['.. code-block:: python', '']
lines = self._consume_usage_section()
lines = self._indent(lines, 3)
return header + block + lines + ['']
def _parse_generic_section(self, section: str, use_admonition: bool) -> list[str]:
lines = self._strip_empty(self._consume_to_next_section())
lines = self._dedent(lines)
if use_admonition:
header = '.. admonition:: %s' % section
lines = self._indent(lines, 3)
else:
header = '.. rubric:: %s' % section
if lines:
return [header, ''] + lines + ['']
else:
return [header, '']
def _parse_keyword_arguments_section(self, section: str) -> list[str]:
fields = self._consume_fields()
if self._config.napoleon_use_keyword:
return self._format_docutils_params(
fields,
field_role="keyword",
type_role="kwtype")
else:
return self._format_fields(_('Keyword Arguments'), fields)
def _parse_methods_section(self, section: str) -> list[str]:
lines: list[str] = []
for _name, _type, _desc in self._consume_fields(parse_type=False):
lines.append('.. method:: %s' % _name)
if self._opt:
if 'no-index' in self._opt or 'noindex' in self._opt:
lines.append(' :no-index:')
if _desc:
lines.extend([''] + self._indent(_desc, 3))
lines.append('')
return lines
def _parse_notes_section(self, section: str) -> list[str]:
use_admonition = self._config.napoleon_use_admonition_for_notes
return self._parse_generic_section(_('Notes'), use_admonition)
def _parse_other_parameters_section(self, section: str) -> list[str]:
if self._config.napoleon_use_param:
# Allow to declare multiple parameters at once (ex: x, y: int)
fields = self._consume_fields(multiple=True)
return self._format_docutils_params(fields)
else:
fields = self._consume_fields()
return self._format_fields(_('Other Parameters'), fields)
def _parse_parameters_section(self, section: str) -> list[str]:
if self._config.napoleon_use_param:
# Allow to declare multiple parameters at once (ex: x, y: int)
fields = self._consume_fields(multiple=True)
return self._format_docutils_params(fields)
else:
fields = self._consume_fields()
return self._format_fields(_('Parameters'), fields)
def _parse_raises_section(self, section: str) -> list[str]:
fields = self._consume_fields(parse_type=False, prefer_type=True)
lines: list[str] = []
for _name, _type, _desc in fields:
m = self._name_rgx.match(_type)
if m and m.group('name'):
_type = m.group('name')
elif _xref_regex.match(_type):
pos = _type.find('`')
_type = _type[pos + 1:-1]
_type = ' ' + _type if _type else ''
_desc = self._strip_empty(_desc)
_descs = ' ' + '\n '.join(_desc) if any(_desc) else ''
lines.append(f':raises{_type}:{_descs}')
if lines:
lines.append('')
return lines
def _parse_receives_section(self, section: str) -> list[str]:
if self._config.napoleon_use_param:
# Allow to declare multiple parameters at once (ex: x, y: int)
fields = self._consume_fields(multiple=True)
return self._format_docutils_params(fields)
else:
fields = self._consume_fields()
return self._format_fields(_('Receives'), fields)
def _parse_references_section(self, section: str) -> list[str]:
use_admonition = self._config.napoleon_use_admonition_for_references
return self._parse_generic_section(_('References'), use_admonition)
def _parse_returns_section(self, section: str) -> list[str]:
fields = self._consume_returns_section()
multi = len(fields) > 1
use_rtype = False if multi else self._config.napoleon_use_rtype
lines: list[str] = []
for _name, _type, _desc in fields:
if use_rtype:
field = self._format_field(_name, '', _desc)
else:
field = self._format_field(_name, _type, _desc)
if multi:
if lines:
lines.extend(self._format_block(' * ', field))
else:
lines.extend(self._format_block(':returns: * ', field))
else:
if any(field): # only add :returns: if there's something to say
lines.extend(self._format_block(':returns: ', field))
if _type and use_rtype:
lines.extend([':rtype: %s' % _type, ''])
if lines and lines[-1]:
lines.append('')
return lines
def _parse_see_also_section(self, section: str) -> list[str]:
return self._parse_admonition('seealso', section)
def _parse_warns_section(self, section: str) -> list[str]:
return self._format_fields(_('Warns'), self._consume_fields())
def _parse_yields_section(self, section: str) -> list[str]:
fields = self._consume_returns_section(preprocess_types=True)
return self._format_fields(_('Yields'), fields)
def _partition_field_on_colon(self, line: str) -> tuple[str, str, str]:
before_colon = []
after_colon = []
colon = ''
found_colon = False
for i, source in enumerate(_xref_or_code_regex.split(line)):
if found_colon:
after_colon.append(source)
else:
m = _single_colon_regex.search(source)
if (i % 2) == 0 and m:
found_colon = True
colon = source[m.start(): m.end()]
before_colon.append(source[:m.start()])
after_colon.append(source[m.end():])
else:
before_colon.append(source)
return ("".join(before_colon).strip(),
colon,
"".join(after_colon).strip())
def _strip_empty(self, lines: list[str]) -> list[str]:
if lines:
start = -1
for i, line in enumerate(lines):
if line:
start = i
break
if start == -1:
lines = []
end = -1
for i in reversed(range(len(lines))):
line = lines[i]
if line:
end = i
break
if start > 0 or end + 1 < len(lines):
lines = lines[start:end + 1]
return lines
def _lookup_annotation(self, _name: str) -> str:
if self._config.napoleon_attr_annotations:
if self._what in ("module", "class", "exception") and self._obj:
# cache the class annotations
if not hasattr(self, "_annotations"):
localns = getattr(self._config, "autodoc_type_aliases", {})
localns.update(getattr(
self._config, "napoleon_type_aliases", {},
) or {})
self._annotations = get_type_hints(self._obj, None, localns)
if _name in self._annotations:
return stringify_annotation(self._annotations[_name],
'fully-qualified-except-typing')
# No annotation found
return ""
def _recombine_set_tokens(tokens: list[str]) -> list[str]:
token_queue = collections.deque(tokens)
keywords = ("optional", "default")
def takewhile_set(tokens):
open_braces = 0
previous_token = None
while True:
try:
token = tokens.popleft()
except IndexError:
break
if token == ", ":
previous_token = token
continue
if not token.strip():
continue
if token in keywords:
tokens.appendleft(token)
if previous_token is not None:
tokens.appendleft(previous_token)
break
if previous_token is not None:
yield previous_token
previous_token = None
if token == "{":
open_braces += 1
elif token == "}":
open_braces -= 1
yield token
if open_braces == 0:
break
def combine_set(tokens):
while True:
try:
token = tokens.popleft()
except IndexError:
break
if token == "{":
tokens.appendleft("{")
yield "".join(takewhile_set(tokens))
else:
yield token
return list(combine_set(token_queue))
def _tokenize_type_spec(spec: str) -> list[str]:
def postprocess(item):
if _default_regex.match(item):
default = item[:7]
# can't be separated by anything other than a single space
# for now
other = item[8:]
return [default, " ", other]
else:
return [item]
tokens = [
item
for raw_token in _token_regex.split(spec)
for item in postprocess(raw_token)
if item
]
return tokens
def _token_type(token: str, location: str | None = None) -> str:
def is_numeric(token):
try:
# use complex to make sure every numeric value is detected as literal
complex(token)
except ValueError:
return False
else:
return True
if token.startswith(" ") or token.endswith(" "):
type_ = "delimiter"
elif (
is_numeric(token) or
(token.startswith("{") and token.endswith("}")) or
(token.startswith('"') and token.endswith('"')) or
(token.startswith("'") and token.endswith("'"))
):
type_ = "literal"
elif token.startswith("{"):
logger.warning(
__("invalid value set (missing closing brace): %s"),
token,
location=location,
)
type_ = "literal"
elif token.endswith("}"):
logger.warning(
__("invalid value set (missing opening brace): %s"),
token,
location=location,
)
type_ = "literal"
elif token.startswith(("'", '"')):
logger.warning(
__("malformed string literal (missing closing quote): %s"),
token,
location=location,
)
type_ = "literal"
elif token.endswith(("'", '"')):
logger.warning(
__("malformed string literal (missing opening quote): %s"),
token,
location=location,
)
type_ = "literal"
elif token in ("optional", "default"):
# default is not a official keyword (yet) but supported by the
# reference implementation (numpydoc) and widely used
type_ = "control"
elif _xref_regex.match(token):
type_ = "reference"
else:
type_ = "obj"
return type_
def _convert_numpy_type_spec(
_type: str, location: str | None = None, translations: dict | None = None,
) -> str:
if translations is None:
translations = {}
def convert_obj(obj, translations, default_translation):
translation = translations.get(obj, obj)
# use :class: (the default) only if obj is not a standard singleton
if translation in _SINGLETONS and default_translation == ":class:`%s`":
default_translation = ":obj:`%s`"
elif translation == "..." and default_translation == ":class:`%s`":
# allow referencing the builtin ...
default_translation = ":obj:`%s <Ellipsis>`"
if _xref_regex.match(translation) is None:
translation = default_translation % translation
return translation
tokens = _tokenize_type_spec(_type)
combined_tokens = _recombine_set_tokens(tokens)
types = [
(token, _token_type(token, location))
for token in combined_tokens
]
converters = {
"literal": lambda x: "``%s``" % x,
"obj": lambda x: convert_obj(x, translations, ":class:`%s`"),
"control": lambda x: "*%s*" % x,
"delimiter": lambda x: x,
"reference": lambda x: x,
}
converted = "".join(converters.get(type_)(token) # type: ignore[misc]
for token, type_ in types)
return converted
class NumpyDocstring(GoogleDocstring):
"""Convert NumPy style docstrings to reStructuredText.
Parameters
----------
docstring : :obj:`str` or :obj:`list` of :obj:`str`
The docstring to parse, given either as a string or split into
individual lines.
config: :obj:`sphinx.ext.napoleon.Config` or :obj:`sphinx.config.Config`
The configuration settings to use. If not given, defaults to the
config object on `app`; or if `app` is not given defaults to the
a new :class:`sphinx.ext.napoleon.Config` object.
Other Parameters
----------------
app : :class:`sphinx.application.Sphinx`, optional
Application object representing the Sphinx process.
what : :obj:`str`, optional
A string specifying the type of the object to which the docstring
belongs. Valid values: "module", "class", "exception", "function",
"method", "attribute".
name : :obj:`str`, optional
The fully qualified name of the object.
obj : module, class, exception, function, method, or attribute
The object to which the docstring belongs.
options : :class:`sphinx.ext.autodoc.Options`, optional
The options given to the directive: an object with attributes
inherited_members, undoc_members, show_inheritance and no_index that
are True if the flag option of same name was given to the auto
directive.
Example
-------
>>> from sphinx.ext.napoleon import Config
>>> config = Config(napoleon_use_param=True, napoleon_use_rtype=True)
>>> docstring = '''One line summary.
...
... Extended description.
...
... Parameters
... ----------
... arg1 : int
... Description of `arg1`
... arg2 : str
... Description of `arg2`
... Returns
... -------
... str
... Description of return value.
... '''
>>> print(NumpyDocstring(docstring, config))
One line summary.
<BLANKLINE>
Extended description.
<BLANKLINE>
:param arg1: Description of `arg1`
:type arg1: int
:param arg2: Description of `arg2`
:type arg2: str
<BLANKLINE>
:returns: Description of return value.
:rtype: str
<BLANKLINE>
Methods
-------
__str__()
Return the parsed docstring in reStructuredText format.
Returns
-------
str
UTF-8 encoded version of the docstring.
__unicode__()
Return the parsed docstring in reStructuredText format.
Returns
-------
unicode
Unicode version of the docstring.
lines()
Return the parsed lines of the docstring in reStructuredText format.
Returns
-------
list(str)
The lines of the docstring in a list.
"""
def __init__(
self,
docstring: str | list[str],
config: SphinxConfig | None = None,
app: Sphinx | None = None,
what: str = '',
name: str = '',
obj: Any = None,
options: Any = None,
) -> None:
self._directive_sections = ['.. index::']
super().__init__(docstring, config, app, what, name, obj, options)
def _get_location(self) -> str | None:
try:
filepath = inspect.getfile(self._obj) if self._obj is not None else None
except TypeError:
filepath = None
name = self._name
if filepath is None and name is None:
return None
elif filepath is None:
filepath = ""
return ":".join([filepath, "docstring of %s" % name])
def _escape_args_and_kwargs(self, name: str) -> str:
func = super()._escape_args_and_kwargs
if ", " in name:
return ", ".join(func(param) for param in name.split(", "))
else:
return func(name)
def _consume_field(self, parse_type: bool = True, prefer_type: bool = False,
) -> tuple[str, str, list[str]]:
line = self._lines.next()
if parse_type:
_name, _, _type = self._partition_field_on_colon(line)
else:
_name, _type = line, ''
_name, _type = _name.strip(), _type.strip()
_name = self._escape_args_and_kwargs(_name)
if parse_type and not _type:
_type = self._lookup_annotation(_name)
if prefer_type and not _type:
_type, _name = _name, _type
if self._config.napoleon_preprocess_types:
_type = _convert_numpy_type_spec(
_type,
location=self._get_location(),
translations=self._config.napoleon_type_aliases or {},
)
indent = self._get_indent(line) + 1
_desc = self._dedent(self._consume_indented_block(indent))
_desc = self.__class__(_desc, self._config).lines()
return _name, _type, _desc
def _consume_returns_section(self, preprocess_types: bool = False,
) -> list[tuple[str, str, list[str]]]:
return self._consume_fields(prefer_type=True)
def _consume_section_header(self) -> str:
section = self._lines.next()
if not _directive_regex.match(section):
# Consume the header underline
self._lines.next()
return section
def _is_section_break(self) -> bool:
line1, line2 = self._lines.get(0), self._lines.get(1)
return (not self._lines or
self._is_section_header() or
['', ''] == [line1, line2] or
(self._is_in_section and
line1 and
not self._is_indented(line1, self._section_indent)))
def _is_section_header(self) -> bool:
section, underline = self._lines.get(0), self._lines.get(1)
section = section.lower()
if section in self._sections and isinstance(underline, str):
return bool(_numpy_section_regex.match(underline))
elif self._directive_sections:
if _directive_regex.match(section):
for directive_section in self._directive_sections:
if section.startswith(directive_section):
return True
return False
def _parse_see_also_section(self, section: str) -> list[str]:
lines = self._consume_to_next_section()
try:
return self._parse_numpydoc_see_also_section(lines)
except ValueError:
return self._format_admonition('seealso', lines)
def _parse_numpydoc_see_also_section(self, content: list[str]) -> list[str]:
"""
Derived from the NumpyDoc implementation of _parse_see_also.
See Also
--------
func_name : Descriptive text
continued text
another_func_name : Descriptive text
func_name1, func_name2, :meth:`func_name`, func_name3
"""
items = []
def parse_item_name(text: str) -> tuple[str, str | None]:
"""Match ':role:`name`' or 'name'"""
m = self._name_rgx.match(text)
if m:
g = m.groups()
if g[1] is None:
return g[3], None
else:
return g[2], g[1]
raise ValueError("%s is not a item name" % text)
def push_item(name: str | None, rest: list[str]) -> None:
if not name:
return
name, role = parse_item_name(name)
items.append((name, list(rest), role))
del rest[:]
def translate(func, description, role):
translations = self._config.napoleon_type_aliases
if role is not None or not translations:
return func, description, role
translated = translations.get(func, func)
match = self._name_rgx.match(translated)
if not match:
return translated, description, role
groups = match.groupdict()
role = groups["role"]
new_func = groups["name"] or groups["name2"]
return new_func, description, role
current_func = None
rest: list[str] = []
for line in content:
if not line.strip():
continue
m = self._name_rgx.match(line)
if m and line[m.end():].strip().startswith(':'):
push_item(current_func, rest)
current_func, line = line[:m.end()], line[m.end():]
rest = [line.split(':', 1)[1].strip()]
if not rest[0]:
rest = []
elif not line.startswith(' '):
push_item(current_func, rest)
current_func = None
if ',' in line:
for func in line.split(','):
if func.strip():
push_item(func, [])
elif line.strip():
current_func = line
elif current_func is not None:
rest.append(line.strip())
push_item(current_func, rest)
if not items:
return []
# apply type aliases
items = [
translate(func, description, role)
for func, description, role in items
]
lines: list[str] = []
last_had_desc = True
for name, desc, role in items:
if role:
link = f':{role}:`{name}`'
else:
link = ':obj:`%s`' % name
if desc or last_had_desc:
lines += ['']
lines += [link]
else:
lines[-1] += ", %s" % link
if desc:
lines += self._indent([' '.join(desc)])
last_had_desc = True
else:
last_had_desc = False
lines += ['']
return self._format_admonition('seealso', lines)