Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 29 additions & 16 deletions sphinx/ext/napoleon/docstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def __init__(self, docstring: Union[str, List[str]], config: SphinxConfig = None
if not hasattr(self, '_directive_sections'):
self._directive_sections = [] # type: List[str]
if not hasattr(self, '_sections'):
self._sections = {
self._sections: Dict[str, Callable] = {
'args': self._parse_parameters_section,
'arguments': self._parse_parameters_section,
'attention': partial(self._parse_admonition, 'attention'),
Expand Down Expand Up @@ -191,7 +191,7 @@ def __init__(self, docstring: Union[str, List[str]], config: SphinxConfig = None
'warns': self._parse_warns_section,
'yield': self._parse_yields_section,
'yields': self._parse_yields_section,
} # type: Dict[str, Callable]
}

self._load_custom_sections()

Expand Down Expand Up @@ -222,8 +222,10 @@ def lines(self) -> List[str]:
def _consume_indented_block(self, indent: int = 1) -> List[str]:
lines = []
line = self._line_iter.peek()
while(not self._is_section_break() and
(not line or self._is_indented(line, indent))):
while (
not self._is_section_break() and
(not line or self._is_indented(line, indent))
):
lines.append(next(self._line_iter))
line = self._line_iter.peek()
return lines
Expand Down Expand Up @@ -337,15 +339,25 @@ def _dedent(self, lines: List[str], full: bool = False) -> List[str]:
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'\_'
def _escape_single(token: str) -> str:
if token.endswith('_') and getattr(
self._config, 'strip_signature_backslash', False
):
token = token[:-1] + r'\_'

if token[:2] == '**':
return r'\*\*' + token[2:]
elif token[:1] == '*':
return r'\*' + token[1:]
else:
return token

if name[:2] == '**':
return r'\*\*' + name[2:]
elif name[:1] == '*':
return r'\*' + name[1:]
else:
return name
parts = [part for part in re.split(r'\s*,\s*', name.strip()) if part]
if not parts:
return ''

escaped = [_escape_single(part) for part in parts]
return ', '.join(escaped)

def _fix_field_desc(self, desc: List[str]) -> List[str]:
if self._is_list(desc):
Expand Down Expand Up @@ -1081,11 +1093,12 @@ def _get_location(self) -> str:

def _escape_args_and_kwargs(self, name: str) -> str:
func = super()._escape_args_and_kwargs
parts = [part for part in re.split(r'\s*,\s*', name.strip()) if part]

if ", " in name:
return ", ".join(func(param) for param in name.split(", "))
else:
return func(name)
if len(parts) <= 1:
return func(parts[0] if parts else '')

return ', '.join(func(part) for part in parts)

def _consume_field(self, parse_type: bool = True, prefer_type: bool = False
) -> Tuple[str, str, List[str]]:
Expand Down
76 changes: 75 additions & 1 deletion tests/test_ext_napoleon_docstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -1230,7 +1230,7 @@ class NumpyDocstringTest(BaseDocstringTest):
"""
Single line summary

:Parameters: * **arg1** (*str*) -- Extended description of arg1
:Parameters: * **arg1** (:class:`str`) -- Extended description of arg1
* **\\*args, \\*\\*kwargs** -- Variable length argument list and arbitrary keyword arguments.
"""
), (
Expand Down Expand Up @@ -1363,6 +1363,80 @@ def test_parameters_without_class_reference(self):
"""
self.assertEqual(expected, actual)

def test_parameters_multi_name_optional_use_param_true(self):
docstring = """\
Parameters
----------
x1, x2 : array_like, optional
Input arrays.
"""

config = Config(napoleon_use_param=True)
actual = str(NumpyDocstring(docstring, config))
expected = """\
:param x1, x2: Input arrays.
:type x1, x2: :class:`array_like`, *optional*
"""
self.assertEqual(expected, actual)

for raw_name in ("x1,x2", "x1, x2"):
variant = f"""\
Parameters
----------
{raw_name} : array_like, optional
Input arrays.
"""
actual = str(NumpyDocstring(variant, config))
self.assertEqual(expected, actual)

def test_parameters_multi_name_optional_use_param_false(self):
docstring = """\
Parameters
----------
x1, x2 : array_like, optional
Input arrays.
"""

config = Config(napoleon_use_param=False)
actual = str(NumpyDocstring(docstring, config))
expected = """\
:Parameters: **x1, x2** (:class:`array_like`, *optional*) -- Input arrays.
"""
self.assertEqual(expected, actual)

for raw_name in ("x1,x2", "x1, x2"):
variant = f"""\
Parameters
----------
{raw_name} : array_like, optional
Input arrays.
"""
actual = str(NumpyDocstring(variant, config))
self.assertEqual(expected, actual)

def test_parameters_multi_name_without_optional(self):
docstring = """\
Parameters
----------
x1, x2 : array_like
Input arrays.
"""

expected_param = """\
:param x1, x2: Input arrays.
:type x1, x2: :class:`array_like`
"""
config = Config(napoleon_use_param=True)
actual = str(NumpyDocstring(docstring, config))
self.assertEqual(expected_param, actual)

config = Config(napoleon_use_param=False)
actual = str(NumpyDocstring(docstring, config))
expected_list = """\
:Parameters: **x1, x2** (:class:`array_like`) -- Input arrays.
"""
self.assertEqual(expected_list, actual)

def test_see_also_refs(self):
docstring = """\
numpy.multivariate_normal(mean, cov, shape=None, spam=None)
Expand Down