Enable automatic formatting for `sphinx/ext/napoleon/` (#12972)

This commit is contained in:
Adam Turner 2024-10-04 23:54:45 +01:00 committed by GitHub
parent a6e449094a
commit f6590c5a33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 204 additions and 154 deletions

View File

@ -470,8 +470,6 @@ exclude = [
"sphinx/ext/inheritance_diagram.py", "sphinx/ext/inheritance_diagram.py",
"sphinx/ext/linkcode.py", "sphinx/ext/linkcode.py",
"sphinx/ext/mathjax.py", "sphinx/ext/mathjax.py",
"sphinx/ext/napoleon/__init__.py",
"sphinx/ext/napoleon/docstring.py",
"sphinx/ext/todo.py", "sphinx/ext/todo.py",
"sphinx/ext/viewcode.py", "sphinx/ext/viewcode.py",
"sphinx/registry.py", "sphinx/registry.py",

View File

@ -333,19 +333,26 @@ def setup(app: Sphinx) -> ExtensionMetadata:
def _patch_python_domain() -> None: def _patch_python_domain() -> None:
from sphinx.domains.python._object import PyObject, PyTypedField from sphinx.domains.python._object import PyObject, PyTypedField
from sphinx.locale import _ from sphinx.locale import _
for doc_field in PyObject.doc_field_types: for doc_field in PyObject.doc_field_types:
if doc_field.name == 'parameter': if doc_field.name == 'parameter':
doc_field.names = ('param', 'parameter', 'arg', 'argument') doc_field.names = ('param', 'parameter', 'arg', 'argument')
break break
PyObject.doc_field_types.append( PyObject.doc_field_types.append(
PyTypedField('keyword', label=_('Keyword Arguments'), PyTypedField(
names=('keyword', 'kwarg', 'kwparam'), 'keyword',
typerolename='class', typenames=('paramtype', 'kwtype'), label=_('Keyword Arguments'),
can_collapse=True)) names=('keyword', 'kwarg', 'kwparam'),
typerolename='class',
typenames=('paramtype', 'kwtype'),
can_collapse=True,
)
)
def _process_docstring(app: Sphinx, what: str, name: str, obj: Any, def _process_docstring(
options: Any, lines: list[str]) -> None: app: Sphinx, what: str, name: str, obj: Any, options: Any, lines: list[str]
) -> None:
"""Process the docstring for a given python object. """Process the docstring for a given python object.
Called when autodoc has read and processed a docstring. `lines` is a list Called when autodoc has read and processed a docstring. `lines` is a list
@ -384,18 +391,21 @@ def _process_docstring(app: Sphinx, what: str, name: str, obj: Any,
result_lines = lines result_lines = lines
docstring: GoogleDocstring docstring: GoogleDocstring
if app.config.napoleon_numpy_docstring: if app.config.napoleon_numpy_docstring:
docstring = NumpyDocstring(result_lines, app.config, app, what, name, docstring = NumpyDocstring(
obj, options) result_lines, app.config, app, what, name, obj, options
)
result_lines = docstring.lines() result_lines = docstring.lines()
if app.config.napoleon_google_docstring: if app.config.napoleon_google_docstring:
docstring = GoogleDocstring(result_lines, app.config, app, what, name, docstring = GoogleDocstring(
obj, options) result_lines, app.config, app, what, name, obj, options
)
result_lines = docstring.lines() result_lines = docstring.lines()
lines[:] = result_lines.copy() lines[:] = result_lines.copy()
def _skip_member(app: Sphinx, what: str, name: str, obj: Any, def _skip_member(
skip: bool, options: Any) -> bool | None: app: Sphinx, what: str, name: str, obj: Any, skip: bool, options: Any
) -> bool | None:
"""Determine if private and special class members are included in docs. """Determine if private and special class members are included in docs.
The following settings in conf.py determine if private and special class The following settings in conf.py determine if private and special class
@ -458,22 +468,25 @@ def _skip_member(app: Sphinx, what: str, name: str, obj: Any,
except Exception: except Exception:
cls_is_owner = False cls_is_owner = False
else: else:
cls_is_owner = (cls and hasattr(cls, name) and # type: ignore[assignment] cls_is_owner = (
name in cls.__dict__) cls # type: ignore[assignment]
and hasattr(cls, name)
and name in cls.__dict__
)
else: else:
cls_is_owner = False cls_is_owner = False
if what == 'module' or cls_is_owner: if what == 'module' or cls_is_owner:
is_init = (name == '__init__') is_init = name == '__init__'
is_special = (not is_init and name.startswith('__') and is_special = not is_init and name.startswith('__') and name.endswith('__')
name.endswith('__')) is_private = not is_init and not is_special and name.startswith('_')
is_private = (not is_init and not is_special and
name.startswith('_'))
inc_init = app.config.napoleon_include_init_with_doc inc_init = app.config.napoleon_include_init_with_doc
inc_special = app.config.napoleon_include_special_with_doc inc_special = app.config.napoleon_include_special_with_doc
inc_private = app.config.napoleon_include_private_with_doc inc_private = app.config.napoleon_include_private_with_doc
if ((is_special and inc_special) or if (
(is_private and inc_private) or (is_special and inc_special)
(is_init and inc_init)): or (is_private and inc_private)
or (is_init and inc_init)
):
return False return False
return None return None

View File

@ -31,7 +31,8 @@ _xref_or_code_regex = re.compile(
r'((?::(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:`.+?`)|' r'((?::(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:`.+?`)|'
r'(?:``.+?``)|' r'(?:``.+?``)|'
r'(?::meta .+:.*)|' r'(?::meta .+:.*)|'
r'(?:`.+?\s*(?<!\x00)<.*?>`))') r'(?:`.+?\s*(?<!\x00)<.*?>`))'
)
_xref_regex = re.compile( _xref_regex = re.compile(
r'(?:(?::(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:)?`.+?`)', r'(?:(?::(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:)?`.+?`)',
) )
@ -39,17 +40,18 @@ _bullet_list_regex = re.compile(r'^(\*|\+|\-)(\s+\S|\s*$)')
_enumerated_list_regex = re.compile( _enumerated_list_regex = re.compile(
r'^(?P<paren>\()?' r'^(?P<paren>\()?'
r'(\d+|#|[ivxlcdm]+|[IVXLCDM]+|[a-zA-Z])' r'(\d+|#|[ivxlcdm]+|[IVXLCDM]+|[a-zA-Z])'
r'(?(paren)\)|\.)(\s+\S|\s*$)') r'(?(paren)\)|\.)(\s+\S|\s*$)'
)
_token_regex = re.compile( _token_regex = re.compile(
r"(,\sor\s|\sor\s|\sof\s|:\s|\sto\s|,\sand\s|\sand\s|,\s" r'(,\sor\s|\sor\s|\sof\s|:\s|\sto\s|,\sand\s|\sand\s|,\s'
r"|[{]|[}]" r'|[{]|[}]'
r'|"(?:\\"|[^"])*"' r'|"(?:\\"|[^"])*"'
r"|'(?:\\'|[^'])*')", r"|'(?:\\'|[^'])*')",
) )
_default_regex = re.compile( _default_regex = re.compile(
r"^default[^_0-9A-Za-z].*$", r'^default[^_0-9A-Za-z].*$',
) )
_SINGLETONS = ("None", "True", "False", "Ellipsis") _SINGLETONS = ('None', 'True', 'False', 'Ellipsis')
class Deque(collections.deque[Any]): class Deque(collections.deque[Any]):
@ -147,8 +149,11 @@ class GoogleDocstring:
""" """
_name_rgx = re.compile(r"^\s*((?::(?P<role>\S+):)?`(?P<name>~?[a-zA-Z0-9_.-]+)`|" _name_rgx = re.compile(
r" (?P<name2>~?[a-zA-Z0-9_.-]+))\s*", re.VERBOSE) r'^\s*((?::(?P<role>\S+):)?`(?P<name>~?[a-zA-Z0-9_.-]+)`|'
r' (?P<name2>~?[a-zA-Z0-9_.-]+))\s*',
re.VERBOSE,
)
def __init__( def __init__(
self, self,
@ -261,9 +266,8 @@ class GoogleDocstring:
def _consume_indented_block(self, indent: int = 1) -> list[str]: def _consume_indented_block(self, indent: int = 1) -> list[str]:
lines = [] lines = []
line = self._lines.get(0) line = self._lines.get(0)
while ( while not self._is_section_break() and (
not self._is_section_break() and not line or self._is_indented(line, indent)
(not line or self._is_indented(line, indent))
): ):
lines.append(self._lines.next()) lines.append(self._lines.next())
line = self._lines.get(0) line = self._lines.get(0)
@ -271,9 +275,7 @@ class GoogleDocstring:
def _consume_contiguous(self) -> list[str]: def _consume_contiguous(self) -> list[str]:
lines = [] lines = []
while (self._lines and while self._lines and self._lines.get(0) and not self._is_section_header():
self._lines.get(0) and
not self._is_section_header()):
lines.append(self._lines.next()) lines.append(self._lines.next())
return lines return lines
@ -285,8 +287,11 @@ class GoogleDocstring:
line = self._lines.get(0) line = self._lines.get(0)
return lines return lines
def _consume_field(self, parse_type: bool = True, prefer_type: bool = False, def _consume_field(
) -> tuple[str, str, list[str]]: self,
parse_type: bool = True,
prefer_type: bool = False,
) -> tuple[str, str, list[str]]:
line = self._lines.next() line = self._lines.next()
before, colon, after = self._partition_field_on_colon(line) before, colon, after = self._partition_field_on_colon(line)
@ -311,14 +316,15 @@ class GoogleDocstring:
_descs = self.__class__(_descs, self._config).lines() _descs = self.__class__(_descs, self._config).lines()
return _name, _type, _descs return _name, _type, _descs
def _consume_fields(self, parse_type: bool = True, prefer_type: bool = False, def _consume_fields(
multiple: bool = False) -> list[tuple[str, str, list[str]]]: self, parse_type: bool = True, prefer_type: bool = False, multiple: bool = False
) -> list[tuple[str, str, list[str]]]:
self._consume_empty() self._consume_empty()
fields: list[tuple[str, str, list[str]]] = [] fields: list[tuple[str, str, list[str]]] = []
while not self._is_section_break(): while not self._is_section_break():
_name, _type, _desc = self._consume_field(parse_type, prefer_type) _name, _type, _desc = self._consume_field(parse_type, prefer_type)
if multiple and _name: if multiple and _name:
fields.extend((name.strip(), _type, _desc) for name in _name.split(",")) fields.extend((name.strip(), _type, _desc) for name in _name.split(','))
elif _name or _type or _desc: elif _name or _type or _desc:
fields.append((_name, _type, _desc)) fields.append((_name, _type, _desc))
return fields return fields
@ -333,8 +339,9 @@ class GoogleDocstring:
_descs = self.__class__(_descs, self._config).lines() _descs = self.__class__(_descs, self._config).lines()
return _type, _descs return _type, _descs
def _consume_returns_section(self, preprocess_types: bool = False, def _consume_returns_section(
) -> list[tuple[str, str, list[str]]]: self, preprocess_types: bool = False
) -> list[tuple[str, str, list[str]]]:
lines = self._dedent(self._consume_to_next_section()) lines = self._dedent(self._consume_to_next_section())
if lines: if lines:
before, colon, after = self._partition_field_on_colon(lines[0]) before, colon, after = self._partition_field_on_colon(lines[0])
@ -348,9 +355,10 @@ class GoogleDocstring:
_type = before _type = before
if (_type and preprocess_types and if _type and preprocess_types and self._config.napoleon_preprocess_types:
self._config.napoleon_preprocess_types): _type = _convert_type_spec(
_type = _convert_type_spec(_type, self._config.napoleon_type_aliases or {}) _type, self._config.napoleon_type_aliases or {}
)
_desc = self.__class__(_desc, self._config).lines() _desc = self.__class__(_desc, self._config).lines()
return [(_name, _type, _desc)] return [(_name, _type, _desc)]
@ -389,7 +397,9 @@ class GoogleDocstring:
return [line[min_indent:] for line in lines] return [line[min_indent:] for line in lines]
def _escape_args_and_kwargs(self, name: str) -> str: def _escape_args_and_kwargs(self, name: str) -> str:
if name.endswith('_') and getattr(self._config, 'strip_signature_backslash', False): if name.endswith('_') and getattr(
self._config, 'strip_signature_backslash', False
):
name = name[:-1] + r'\_' name = name[:-1] + r'\_'
if name[:2] == '**': if name[:2] == '**':
@ -423,7 +433,10 @@ class GoogleDocstring:
return ['.. %s::' % admonition, ''] return ['.. %s::' % admonition, '']
def _format_block( def _format_block(
self, prefix: str, lines: list[str], padding: str | None = None, self,
prefix: str,
lines: list[str],
padding: str | None = None,
) -> list[str]: ) -> list[str]:
if lines: if lines:
if padding is None: if padding is None:
@ -440,9 +453,12 @@ class GoogleDocstring:
else: else:
return [prefix] return [prefix]
def _format_docutils_params(self, fields: list[tuple[str, str, list[str]]], def _format_docutils_params(
field_role: str = 'param', type_role: str = 'type', self,
) -> list[str]: fields: list[tuple[str, str, list[str]]],
field_role: str = 'param',
type_role: str = 'type',
) -> list[str]:
lines = [] lines = []
for _name, _type, _desc in fields: for _name, _type, _desc in fields:
_desc = self._strip_empty(_desc) _desc = self._strip_empty(_desc)
@ -486,8 +502,11 @@ class GoogleDocstring:
else: else:
return [field] return [field]
def _format_fields(self, field_type: str, fields: list[tuple[str, str, list[str]]], def _format_fields(
) -> list[str]: self,
field_type: str,
fields: list[tuple[str, str, list[str]]],
) -> list[str]:
field_type = ':%s:' % field_type.strip() field_type = ':%s:' % field_type.strip()
padding = ' ' * len(field_type) padding = ' ' * len(field_type)
multi = len(fields) > 1 multi = len(fields) > 1
@ -579,11 +598,15 @@ class GoogleDocstring:
def _is_section_break(self) -> bool: def _is_section_break(self) -> bool:
line = self._lines.get(0) line = self._lines.get(0)
return (not self._lines or return (
self._is_section_header() or not self._lines
(self._is_in_section and or self._is_section_header()
line and or (
not self._is_indented(line, self._section_indent))) self._is_in_section
and line
and not self._is_indented(line, self._section_indent)
)
)
def _load_custom_sections(self) -> None: def _load_custom_sections(self) -> None:
if self._config.napoleon_custom_sections is not None: if self._config.napoleon_custom_sections is not None:
@ -594,18 +617,20 @@ class GoogleDocstring:
self._sections[entry.lower()] = self._parse_custom_generic_section self._sections[entry.lower()] = self._parse_custom_generic_section
else: else:
# otherwise, assume entry is container; # otherwise, assume entry is container;
if entry[1] == "params_style": if entry[1] == 'params_style':
self._sections[entry[0].lower()] = \ self._sections[entry[0].lower()] = (
self._parse_custom_params_style_section self._parse_custom_params_style_section
elif entry[1] == "returns_style": )
self._sections[entry[0].lower()] = \ elif entry[1] == 'returns_style':
self._sections[entry[0].lower()] = (
self._parse_custom_returns_style_section self._parse_custom_returns_style_section
)
else: else:
# [0] is new section, [1] is the section to alias. # [0] is new section, [1] is the section to alias.
# in the case of key mismatch, just handle as generic section. # in the case of key mismatch, just handle as generic section.
self._sections[entry[0].lower()] = \ self._sections[entry[0].lower()] = self._sections.get(
self._sections.get(entry[1].lower(), entry[1].lower(), self._parse_custom_generic_section
self._parse_custom_generic_section) )
def _parse(self) -> None: def _parse(self) -> None:
self._parsed_lines = self._consume_empty() self._parsed_lines = self._consume_empty()
@ -721,9 +746,8 @@ class GoogleDocstring:
fields = self._consume_fields() fields = self._consume_fields()
if self._config.napoleon_use_keyword: if self._config.napoleon_use_keyword:
return self._format_docutils_params( return self._format_docutils_params(
fields, fields, field_role='keyword', type_role='kwtype'
field_role="keyword", )
type_role="kwtype")
else: else:
return self._format_fields(_('Keyword Arguments'), fields) return self._format_fields(_('Keyword Arguments'), fields)
@ -770,7 +794,7 @@ class GoogleDocstring:
_type = m.group('name') _type = m.group('name')
elif _xref_regex.match(_type): elif _xref_regex.match(_type):
pos = _type.find('`') pos = _type.find('`')
_type = _type[pos + 1:-1] _type = _type[pos + 1 : -1]
_type = ' ' + _type if _type else '' _type = ' ' + _type if _type else ''
_desc = self._strip_empty(_desc) _desc = self._strip_empty(_desc)
_descs = ' ' + '\n '.join(_desc) if any(_desc) else '' _descs = ' ' + '\n '.join(_desc) if any(_desc) else ''
@ -840,15 +864,13 @@ class GoogleDocstring:
m = _single_colon_regex.search(source) m = _single_colon_regex.search(source)
if (i % 2) == 0 and m: if (i % 2) == 0 and m:
found_colon = True found_colon = True
colon = source[m.start(): m.end()] colon = source[m.start() : m.end()]
before_colon.append(source[:m.start()]) before_colon.append(source[: m.start()])
after_colon.append(source[m.end():]) after_colon.append(source[m.end() :])
else: else:
before_colon.append(source) before_colon.append(source)
return ("".join(before_colon).strip(), return ''.join(before_colon).strip(), colon, ''.join(after_colon).strip()
colon,
"".join(after_colon).strip())
def _strip_empty(self, lines: list[str]) -> list[str]: def _strip_empty(self, lines: list[str]) -> list[str]:
if lines: if lines:
@ -866,29 +888,35 @@ class GoogleDocstring:
end = i end = i
break break
if start > 0 or end + 1 < len(lines): if start > 0 or end + 1 < len(lines):
lines = lines[start:end + 1] lines = lines[start : end + 1]
return lines return lines
def _lookup_annotation(self, _name: str) -> str: def _lookup_annotation(self, _name: str) -> str:
if self._config.napoleon_attr_annotations: if self._config.napoleon_attr_annotations:
if self._what in ("module", "class", "exception") and self._obj: if self._what in ('module', 'class', 'exception') and self._obj:
# cache the class annotations # cache the class annotations
if not hasattr(self, "_annotations"): if not hasattr(self, '_annotations'):
localns = getattr(self._config, "autodoc_type_aliases", {}) localns = getattr(self._config, 'autodoc_type_aliases', {})
localns.update(getattr( localns.update(
self._config, "napoleon_type_aliases", {}, getattr(
) or {}) self._config,
'napoleon_type_aliases',
{},
)
or {}
)
self._annotations = get_type_hints(self._obj, None, localns) self._annotations = get_type_hints(self._obj, None, localns)
if _name in self._annotations: if _name in self._annotations:
return stringify_annotation(self._annotations[_name], return stringify_annotation(
'fully-qualified-except-typing') self._annotations[_name], 'fully-qualified-except-typing'
)
# No annotation found # No annotation found
return "" return ''
def _recombine_set_tokens(tokens: list[str]) -> list[str]: def _recombine_set_tokens(tokens: list[str]) -> list[str]:
token_queue = collections.deque(tokens) token_queue = collections.deque(tokens)
keywords = ("optional", "default") keywords = ('optional', 'default')
def takewhile_set(tokens: collections.deque[str]) -> Iterator[str]: def takewhile_set(tokens: collections.deque[str]) -> Iterator[str]:
open_braces = 0 open_braces = 0
@ -899,7 +927,7 @@ def _recombine_set_tokens(tokens: list[str]) -> list[str]:
except IndexError: except IndexError:
break break
if token == ", ": if token == ', ':
previous_token = token previous_token = token
continue continue
@ -916,9 +944,9 @@ def _recombine_set_tokens(tokens: list[str]) -> list[str]:
yield previous_token yield previous_token
previous_token = None previous_token = None
if token == "{": if token == '{':
open_braces += 1 open_braces += 1
elif token == "}": elif token == '}':
open_braces -= 1 open_braces -= 1
yield token yield token
@ -933,9 +961,9 @@ def _recombine_set_tokens(tokens: list[str]) -> list[str]:
except IndexError: except IndexError:
break break
if token == "{": if token == '{':
tokens.appendleft("{") tokens.appendleft('{')
yield "".join(takewhile_set(tokens)) yield ''.join(takewhile_set(tokens))
else: else:
yield token yield token
@ -950,7 +978,7 @@ def _tokenize_type_spec(spec: str) -> list[str]:
# for now # for now
other = item[8:] other = item[8:]
return [default, " ", other] return [default, ' ', other]
else: else:
return [item] return [item]
@ -973,70 +1001,74 @@ def _token_type(token: str, location: str | None = None) -> str:
else: else:
return True return True
if token.startswith(" ") or token.endswith(" "): if token.startswith(' ') or token.endswith(' '):
type_ = "delimiter" type_ = 'delimiter'
elif ( elif (
is_numeric(token) or is_numeric(token)
(token.startswith("{") and token.endswith("}")) or or (token.startswith('{') and token.endswith('}'))
(token.startswith('"') and token.endswith('"')) or or (token.startswith('"') and token.endswith('"'))
(token.startswith("'") and token.endswith("'")) or (token.startswith("'") and token.endswith("'"))
): ):
type_ = "literal" type_ = 'literal'
elif token.startswith("{"): elif token.startswith('{'):
logger.warning( logger.warning(
__("invalid value set (missing closing brace): %s"), __('invalid value set (missing closing brace): %s'),
token, token,
location=location, location=location,
) )
type_ = "literal" type_ = 'literal'
elif token.endswith("}"): elif token.endswith('}'):
logger.warning( logger.warning(
__("invalid value set (missing opening brace): %s"), __('invalid value set (missing opening brace): %s'),
token, token,
location=location, location=location,
) )
type_ = "literal" type_ = 'literal'
elif token.startswith(("'", '"')): elif token.startswith(("'", '"')):
logger.warning( logger.warning(
__("malformed string literal (missing closing quote): %s"), __('malformed string literal (missing closing quote): %s'),
token, token,
location=location, location=location,
) )
type_ = "literal" type_ = 'literal'
elif token.endswith(("'", '"')): elif token.endswith(("'", '"')):
logger.warning( logger.warning(
__("malformed string literal (missing opening quote): %s"), __('malformed string literal (missing opening quote): %s'),
token, token,
location=location, location=location,
) )
type_ = "literal" type_ = 'literal'
elif token in ("optional", "default"): elif token in ('optional', 'default'):
# default is not a official keyword (yet) but supported by the # default is not a official keyword (yet) but supported by the
# reference implementation (numpydoc) and widely used # reference implementation (numpydoc) and widely used
type_ = "control" type_ = 'control'
elif _xref_regex.match(token): elif _xref_regex.match(token):
type_ = "reference" type_ = 'reference'
else: else:
type_ = "obj" type_ = 'obj'
return type_ return type_
def _convert_numpy_type_spec( def _convert_numpy_type_spec(
_type: str, location: str | None = None, translations: dict[str, str] | None = None, _type: str,
location: str | None = None,
translations: dict[str, str] | None = None,
) -> str: ) -> str:
if translations is None: if translations is None:
translations = {} translations = {}
def convert_obj(obj: str, translations: dict[str, str], default_translation: str) -> str: def convert_obj(
obj: str, translations: dict[str, str], default_translation: str
) -> str:
translation = translations.get(obj, obj) translation = translations.get(obj, obj)
# use :class: (the default) only if obj is not a standard singleton # use :class: (the default) only if obj is not a standard singleton
if translation in _SINGLETONS and default_translation == ":class:`%s`": if translation in _SINGLETONS and default_translation == ':class:`%s`':
default_translation = ":obj:`%s`" default_translation = ':obj:`%s`'
elif translation == "..." and default_translation == ":class:`%s`": elif translation == '...' and default_translation == ':class:`%s`':
# allow referencing the builtin ... # allow referencing the builtin ...
default_translation = ":obj:`%s <Ellipsis>`" default_translation = ':obj:`%s <Ellipsis>`'
if _xref_regex.match(translation) is None: if _xref_regex.match(translation) is None:
translation = default_translation % translation translation = default_translation % translation
@ -1045,21 +1077,20 @@ def _convert_numpy_type_spec(
tokens = _tokenize_type_spec(_type) tokens = _tokenize_type_spec(_type)
combined_tokens = _recombine_set_tokens(tokens) combined_tokens = _recombine_set_tokens(tokens)
types = [ types = [(token, _token_type(token, location)) for token in combined_tokens]
(token, _token_type(token, location))
for token in combined_tokens
]
converters = { converters = {
"literal": lambda x: "``%s``" % x, 'literal': lambda x: '``%s``' % x,
"obj": lambda x: convert_obj(x, translations, ":class:`%s`"), 'obj': lambda x: convert_obj(x, translations, ':class:`%s`'),
"control": lambda x: "*%s*" % x, 'control': lambda x: '*%s*' % x,
"delimiter": lambda x: x, 'delimiter': lambda x: x,
"reference": lambda x: x, 'reference': lambda x: x,
} }
converted = "".join(converters.get(type_)(token) # type: ignore[misc] converted = ''.join(
for token, type_ in types) converters.get(type_)(token) # type: ignore[misc]
for token, type_ in types
)
return converted return converted
@ -1181,20 +1212,21 @@ class NumpyDocstring(GoogleDocstring):
if filepath is None and name is None: if filepath is None and name is None:
return None return None
elif filepath is None: elif filepath is None:
filepath = "" filepath = ''
return f"{filepath}:docstring of {name}" return f'{filepath}:docstring of {name}'
def _escape_args_and_kwargs(self, name: str) -> str: def _escape_args_and_kwargs(self, name: str) -> str:
func = super()._escape_args_and_kwargs func = super()._escape_args_and_kwargs
if ", " in name: if ', ' in name:
return ", ".join(map(func, name.split(", "))) return ', '.join(map(func, name.split(', ')))
else: else:
return func(name) return func(name)
def _consume_field(self, parse_type: bool = True, prefer_type: bool = False, def _consume_field(
) -> tuple[str, str, list[str]]: self, parse_type: bool = True, prefer_type: bool = False
) -> tuple[str, str, list[str]]:
line = self._lines.next() line = self._lines.next()
if parse_type: if parse_type:
_name, _, _type = self._partition_field_on_colon(line) _name, _, _type = self._partition_field_on_colon(line)
@ -1221,8 +1253,9 @@ class NumpyDocstring(GoogleDocstring):
_desc = self.__class__(_desc, self._config).lines() _desc = self.__class__(_desc, self._config).lines()
return _name, _type, _desc return _name, _type, _desc
def _consume_returns_section(self, preprocess_types: bool = False, def _consume_returns_section(
) -> list[tuple[str, str, list[str]]]: self, preprocess_types: bool = False
) -> list[tuple[str, str, list[str]]]:
return self._consume_fields(prefer_type=True) return self._consume_fields(prefer_type=True)
def _consume_section_header(self) -> str: def _consume_section_header(self) -> str:
@ -1234,12 +1267,16 @@ class NumpyDocstring(GoogleDocstring):
def _is_section_break(self) -> bool: def _is_section_break(self) -> bool:
line1, line2 = self._lines.get(0), self._lines.get(1) line1, line2 = self._lines.get(0), self._lines.get(1)
return (not self._lines or return (
self._is_section_header() or not self._lines
(line1 == line2 == '') or or self._is_section_header()
(self._is_in_section and or (line1 == line2 == '')
line1 and or (
not self._is_indented(line1, self._section_indent))) self._is_in_section
and line1
and not self._is_indented(line1, self._section_indent)
)
)
def _is_section_header(self) -> bool: def _is_section_header(self) -> bool:
section, underline = self._lines.get(0), self._lines.get(1) section, underline = self._lines.get(0), self._lines.get(1)
@ -1312,7 +1349,7 @@ class NumpyDocstring(GoogleDocstring):
return g[3], None return g[3], None
else: else:
return g[2], g[1] return g[2], g[1]
raise ValueError("%s is not a item name" % text) raise ValueError('%s is not a item name' % text)
def push_item(name: str | None, rest: list[str]) -> None: def push_item(name: str | None, rest: list[str]) -> None:
if not name: if not name:
@ -1322,7 +1359,9 @@ class NumpyDocstring(GoogleDocstring):
rest.clear() rest.clear()
def translate( def translate(
func: str, description: list[str], role: str | None, func: str,
description: list[str],
role: str | None,
) -> tuple[str, list[str], str | None]: ) -> tuple[str, list[str], str | None]:
translations = self._config.napoleon_type_aliases translations = self._config.napoleon_type_aliases
if role is not None or not translations: if role is not None or not translations:
@ -1334,8 +1373,8 @@ class NumpyDocstring(GoogleDocstring):
return translated, description, role return translated, description, role
groups = match.groupdict() groups = match.groupdict()
role = groups["role"] role = groups['role']
new_func = groups["name"] or groups["name2"] new_func = groups['name'] or groups['name2']
return new_func, description, role return new_func, description, role
@ -1347,9 +1386,9 @@ class NumpyDocstring(GoogleDocstring):
continue continue
m = self._name_rgx.match(line) m = self._name_rgx.match(line)
if m and line[m.end():].strip().startswith(':'): if m and line[m.end() :].strip().startswith(':'):
push_item(current_func, rest) push_item(current_func, rest)
current_func, line = line[:m.end()], line[m.end():] current_func, line = line[: m.end()], line[m.end() :]
rest = [line.split(':', 1)[1].strip()] rest = [line.split(':', 1)[1].strip()]
if not rest[0]: if not rest[0]:
rest = [] rest = []
@ -1383,7 +1422,7 @@ class NumpyDocstring(GoogleDocstring):
lines += [''] lines += ['']
lines += [link] lines += [link]
else: else:
lines[-1] += ", %s" % link lines[-1] += ', %s' % link
if desc: if desc:
lines += self._indent([' '.join(desc)]) lines += self._indent([' '.join(desc)])
last_had_desc = True last_had_desc = True