diff --git a/AUTHORS b/AUTHORS index 713082e51a2..a35c27044f0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -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 diff --git a/CHANGES b/CHANGES index 90e8b382869..2516243a624 100644 --- a/CHANGES +++ b/CHANGES @@ -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 ---------- diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 99901d2b2b1..b83e22b8f75 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -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) diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 26f12e48ceb..9e9cd4fb3a2 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -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" diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index 6db8979320a..c2704ebce26 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -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): @@ -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'", @@ -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'"], @@ -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'", @@ -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'``", @@ -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 @@ -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:`... ` 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 `",