Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Python docstring type handling bug #10738

Merged
merged 13 commits into from Aug 2, 2022
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -30,6 +30,7 @@ Contributors
* Antonio Valentino -- qthelp builder, docstring inheritance
* Antti Kaihola -- doctest extension (skipif option)
* Barry Warsaw -- setup command improvements
* Ben Egan -- Napoleon improvements
* Benjamin Peterson -- unittests
* Blaise Laflamme -- pyramid theme
* Bruce Mitchener -- Minor epub improvement
Expand Down
3 changes: 3 additions & 0 deletions CHANGES
Expand Up @@ -13,6 +13,9 @@ Deprecated
Features added
--------------

* #10738: napoleon: Add support for docstring types using 'of', like
``type of type``. Example: ``tuple of int``.

Bugs fixed
----------

Expand Down
2 changes: 1 addition & 1 deletion sphinx/domains/python.py
Expand Up @@ -380,7 +380,7 @@ def make_xrefs(self, rolename: str, domain: str, target: str,
innernode: Type[TextlikeNode] = nodes.emphasis,
contnode: Node = None, env: BuildEnvironment = None,
inliner: Inliner = None, location: Node = None) -> List[Node]:
delims = r'(\s*[\[\]\(\),](?:\s*or\s)?\s*|\s+or\s+|\s*\|\s*|\.\.\.)'
delims = r'(\s*[\[\]\(\),](?:\s*o[rf]\s)?\s*|\s+o[rf]\s+|\s*\|\s*|\.\.\.)'
delims_re = re.compile(delims)
sub_targets = re.split(delims, target)

Expand Down
72 changes: 72 additions & 0 deletions tests/test_domain_py.py
Expand Up @@ -1214,6 +1214,78 @@ def test_info_field_list_var(app):
refdomain="py", reftype="class", reftarget="int", **{"py:class": "Class"})


def test_info_field_list_napoleon_deliminator_of(app):
text = (".. py:module:: example\n"
".. py:class:: Class\n"
"\n"
" :param list_str_var: example description.\n"
" :type list_str_var: list of str\n"
" :param tuple_int_var: example description.\n"
" :type tuple_int_var: tuple of tuple of int\n"
)
doctree = restructuredtext.parse(app, text)

# :param list of str list_str_var:
assert_node(doctree[3][1][0][0][1][0][0][0],
([addnodes.literal_strong, "list_str_var"],
" (",
[pending_xref, addnodes.literal_emphasis, "list"],
[addnodes.literal_emphasis, " of "],
[pending_xref, addnodes.literal_emphasis, "str"],
")",
" -- ",
"example description."))

# :param tuple of tuple of int tuple_int_var:
assert_node(doctree[3][1][0][0][1][0][1][0],
([addnodes.literal_strong, "tuple_int_var"],
" (",
[pending_xref, addnodes.literal_emphasis, "tuple"],
[addnodes.literal_emphasis, " of "],
[pending_xref, addnodes.literal_emphasis, "tuple"],
[addnodes.literal_emphasis, " of "],
[pending_xref, addnodes.literal_emphasis, "int"],
")",
" -- ",
"example description."))


def test_info_field_list_napoleon_deliminator_or(app):
text = (".. py:module:: example\n"
".. py:class:: Class\n"
"\n"
" :param bool_str_var: example description.\n"
" :type bool_str_var: bool or str\n"
" :param str_float_int_var: example description.\n"
" :type str_float_int_var: str or float or int\n"
)
doctree = restructuredtext.parse(app, text)

# :param bool or str bool_str_var:
assert_node(doctree[3][1][0][0][1][0][0][0],
([addnodes.literal_strong, "bool_str_var"],
" (",
[pending_xref, addnodes.literal_emphasis, "bool"],
[addnodes.literal_emphasis, " or "],
[pending_xref, addnodes.literal_emphasis, "str"],
")",
" -- ",
"example description."))

# :param str or float or int str_float_int_var:
assert_node(doctree[3][1][0][0][1][0][1][0],
([addnodes.literal_strong, "str_float_int_var"],
" (",
[pending_xref, addnodes.literal_emphasis, "str"],
[addnodes.literal_emphasis, " or "],
[pending_xref, addnodes.literal_emphasis, "float"],
[addnodes.literal_emphasis, " or "],
[pending_xref, addnodes.literal_emphasis, "int"],
")",
" -- ",
"example description."))


def test_type_field(app):
text = (".. py:data:: var1\n"
" :type: .int\n"
Expand Down
45 changes: 45 additions & 0 deletions tests/test_ext_napoleon_docstring.py
Expand Up @@ -356,6 +356,41 @@ class GoogleDocstringTest(BaseDocstringTest):
:Yields: Extended
description of yielded value
"""
), (
"""
Single line summary

Args:

arg1 (list of str): Extended
description of arg1.
arg2 (tuple of int): Extended
description of arg2.
arg3 (tuple of list of float): Extended
description of arg3.
arg4 (int, float, or list of bool): Extended
description of arg4.
arg5 (list of int, float, or bool): Extended
description of arg5.
arg6 (list of int or float): Extended
description of arg6.
""",
"""
Single line summary

:Parameters: * **arg1** (*list of str*) -- Extended
description of arg1.
* **arg2** (*tuple of int*) -- Extended
description of arg2.
* **arg3** (*tuple of list of float*) -- Extended
description of arg3.
* **arg4** (*int, float, or list of bool*) -- Extended
description of arg4.
* **arg5** (*list of int, float, or bool*) -- Extended
description of arg5.
* **arg6** (*list of int or float*) -- Extended
description of arg6.
"""
)]

def test_sphinx_admonitions(self):
Expand Down Expand Up @@ -2367,6 +2402,8 @@ def test_tokenize_type_spec(self):
"defaultdict",
"int, float, or complex",
"int or float or None, optional",
"list of list of int or float, optional",
"tuple of list of str, float, or int",
'{"F", "C", "N"}',
"{'F', 'C', 'N'}, default: 'F'",
"{'F', 'C', 'N or C'}, default 'F'",
Expand All @@ -2383,6 +2420,8 @@ def test_tokenize_type_spec(self):
["defaultdict"],
["int", ", ", "float", ", or ", "complex"],
["int", " or ", "float", " or ", "None", ", ", "optional"],
["list", " of ", "list", " of ", "int", " or ", "float", ", ", "optional"],
["tuple", " of ", "list", " of ", "str", ", ", "float", ", or ", "int"],
["{", '"F"', ", ", '"C"', ", ", '"N"', "}"],
["{", "'F'", ", ", "'C'", ", ", "'N'", "}", ", ", "default", ": ", "'F'"],
["{", "'F'", ", ", "'C'", ", ", "'N or C'", "}", ", ", "default", " ", "'F'"],
Expand Down Expand Up @@ -2443,6 +2482,7 @@ def test_convert_numpy_type_spec(self):
"optional",
"str, optional",
"int or float or None, default: None",
"list of tuple of str, optional",
"int, default None",
'{"F", "C", "N"}',
"{'F', 'C', 'N'}, default: 'N'",
Expand All @@ -2455,6 +2495,7 @@ def test_convert_numpy_type_spec(self):
"*optional*",
":class:`str`, *optional*",
":class:`int` or :class:`float` or :obj:`None`, *default*: :obj:`None`",
":class:`list` of :class:`tuple` of :class:`str`, *optional*",
":class:`int`, *default* :obj:`None`",
'``{"F", "C", "N"}``',
"``{'F', 'C', 'N'}``, *default*: ``'N'``",
Expand Down Expand Up @@ -2486,6 +2527,8 @@ def test_parameter_types(self):
a optional mapping
param8 : ... or Ellipsis
ellipsis
param9 : tuple of list of int
a parameter with tuple of list of int
""")
expected = dedent("""\
:param param1: the data to work on
Expand All @@ -2504,6 +2547,8 @@ def test_parameter_types(self):
:type param7: :term:`mapping` of :term:`hashable` to :class:`str`, *optional*
:param param8: ellipsis
:type param8: :obj:`... <Ellipsis>` or :obj:`Ellipsis`
:param param9: a parameter with tuple of list of int
:type param9: :class:`tuple` of :class:`list` of :class:`int`
""")
translations = {
"dict-like": ":term:`dict-like <mapping>`",
Expand Down