summaryrefslogtreecommitdiffstats
path: root/sphinx/ext/napoleon/docstring.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/ext/napoleon/docstring.py')
-rw-r--r--sphinx/ext/napoleon/docstring.py1363
1 files changed, 1363 insertions, 0 deletions
diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py
new file mode 100644
index 0000000..2ffde39
--- /dev/null
+++ b/sphinx/ext/napoleon/docstring.py
@@ -0,0 +1,1363 @@
+"""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)