Commits

Rob Ruana committed a8de74e

Closes #1429: Adds smarter Args parsing for Google style docstrings.

Comments (0)

Files changed (2)

sphinx/ext/napoleon/docstring.py

 
 
 _directive_regex = re.compile(r'\.\. \S+::')
-_field_parens_regex = re.compile(r'\s*(\w+)\s*\(\s*(.+?)\s*\)')
+_google_untyped_arg_regex = re.compile(r'\s*(\w+)\s*:\s*(.*)')
+_google_typed_arg_regex = re.compile(r'\s*(\w+)\s*\(\s*(.+?)\s*\)\s*:\s*(.*)')
 
 
 class GoogleDocstring(object):
 
     def _consume_field(self, parse_type=True, prefer_type=False):
         line = self._line_iter.next()
-        _name, _, _desc = line.partition(':')
-        _name, _type, _desc = _name.strip(), '', _desc.strip()
-        match = _field_parens_regex.match(_name)
-        if parse_type and match:
-            _name = match.group(1)
-            _type = match.group(2)
+
+        match = None
+        _name, _type, _desc = line.strip(), '', ''
+        if parse_type:
+            match = _google_typed_arg_regex.match(line)
+            if match:
+                _name = match.group(1)
+                _type = match.group(2)
+                _desc = match.group(3)
+
+        if not match:
+            match = _google_untyped_arg_regex.match(line)
+            if match:
+                _name = match.group(1)
+                _desc = match.group(2)
+
         if prefer_type and not _type:
             _type, _name = _name, _type
         indent = self._get_indent(line) + 1
     def _consume_returns_section(self):
         lines = self._dedent(self._consume_to_next_section())
         if lines:
-            if ':' in lines[0]:
-                _type, _, _desc = lines[0].partition(':')
-                _name, _type, _desc = '', _type.strip(), _desc.strip()
-                match = _field_parens_regex.match(_type)
+            _name, _type, _desc = '', '', lines
+            match = _google_typed_arg_regex.match(lines[0])
+            if match:
+                _name = match.group(1)
+                _type = match.group(2)
+                _desc = match.group(3)
+            else:
+                match = _google_untyped_arg_regex.match(lines[0])
                 if match:
-                    _name = match.group(1)
-                    _type = match.group(2)
+                    _type = match.group(1)
+                    _desc = match.group(2)
+            if match:
                 lines[0] = _desc
                 _desc = lines
-            else:
-                _name, _type, _desc = '', '', lines
+
             _desc = self.__class__(_desc, self._config).lines()
             return [(_name, _type, _desc,)]
         else:

tests/test_napoleon_docstring.py

         :returns: *str* --
                   Extended
                   description of return value"""
+    ), (
+        """
+        Single line summary
+
+        Returns:
+          Extended
+          description of return value
+        """,
+        """
+        Single line summary
+
+        :returns: Extended
+                  description of return value"""
     )]
 
     def test_docstrings(self):
             expected = textwrap.dedent(expected)
             self.assertEqual(expected, actual)
 
+    def test_parameters_with_class_reference(self):
+        docstring = """\
+Construct a new XBlock.
+
+This class should only be used by runtimes.
+
+Arguments:
+    runtime (:class:`Runtime`): Use it to access the environment.
+        It is available in XBlock code as ``self.runtime``.
+
+    field_data (:class:`FieldData`): Interface used by the XBlock
+        fields to access their data from wherever it is persisted.
+
+    scope_ids (:class:`ScopeIds`): Identifiers needed to resolve scopes.
+
+"""
+
+        actual = str(GoogleDocstring(docstring))
+        expected = """\
+Construct a new XBlock.
+
+This class should only be used by runtimes.
+
+:param runtime: Use it to access the environment.
+                It is available in XBlock code as ``self.runtime``.
+
+:type runtime: :class:`Runtime`
+:param field_data: Interface used by the XBlock
+                   fields to access their data from wherever it is persisted.
+
+:type field_data: :class:`FieldData`
+:param scope_ids: Identifiers needed to resolve scopes.
+
+:type scope_ids: :class:`ScopeIds`
+"""
+        self.assertEqual(expected, actual)
+
 
 class NumpyDocstringTest(BaseDocstringTest):
     docstrings = [(
             self.assertEqual(expected, actual)
 
     def test_parameters_with_class_reference(self):
-        docstring = """
-            Parameters
-            ----------
-            param1 : :class:`MyClass <name.space.MyClass>` instance
+        docstring = """\
+Parameters
+----------
+param1 : :class:`MyClass <name.space.MyClass>` instance
 
-            """
+"""
 
         config = Config(napoleon_use_param=False)
-        actual = str(NumpyDocstring(textwrap.dedent(docstring), config))
-        expected = textwrap.dedent("""
+        actual = str(NumpyDocstring(docstring, config))
+        expected = """\
 :Parameters: **param1** (:class:`MyClass <name.space.MyClass>` instance)
-""")
+"""
+        self.assertEqual(expected, actual)
+
+        config = Config(napoleon_use_param=True)
+        actual = str(NumpyDocstring(docstring, config))
+        expected = """\
+
+:type param1: :class:`MyClass <name.space.MyClass>` instance
+"""
+        self.assertEqual(expected, actual)
+
+    def test_parameters_without_class_reference(self):
+        docstring = """\
+Parameters
+----------
+param1 : MyClass instance
+
+"""
+
+        config = Config(napoleon_use_param=False)
+        actual = str(NumpyDocstring(docstring, config))
+        expected = """\
+:Parameters: **param1** (*MyClass instance*)
+"""
         self.assertEqual(expected, actual)
 
         config = Config(napoleon_use_param=True)
         actual = str(NumpyDocstring(textwrap.dedent(docstring), config))
-        expected = textwrap.dedent("""
+        expected = """\
 
-            :type param1: :class:`MyClass <name.space.MyClass>` instance
-            """)
-        self.assertEqual(expected, actual)
-
-    def test_parameters_without_class_reference(self):
-        docstring = """
-            Parameters
-            ----------
-            param1 : MyClass instance
-
-            """
-
-        config = Config(napoleon_use_param=False)
-        actual = str(NumpyDocstring(textwrap.dedent(docstring), config))
-        expected = textwrap.dedent("""
-            :Parameters: **param1** (*MyClass instance*)
-            """)
-        self.assertEqual(expected, actual)
-
-        config = Config(napoleon_use_param=True)
-        actual = str(NumpyDocstring(textwrap.dedent(docstring), config))
-        expected = textwrap.dedent("""
-
-            :type param1: MyClass instance
-            """)
+:type param1: MyClass instance
+"""
         self.assertEqual(expected, actual)
 
     def test_see_also_refs(self):
-        docstring = """
-            numpy.multivariate_normal(mean, cov, shape=None, spam=None)
+        docstring = """\
+numpy.multivariate_normal(mean, cov, shape=None, spam=None)
 
-            See Also
-            --------
-            some, other, funcs
-            otherfunc : relationship
+See Also
+--------
+some, other, funcs
+otherfunc : relationship
 
-            """
+"""
 
-        actual = str(NumpyDocstring(textwrap.dedent(docstring)))
+        actual = str(NumpyDocstring(docstring))
 
-        expected = """
+        expected = """\
 numpy.multivariate_normal(mean, cov, shape=None, spam=None)
 
 .. seealso::
-\n   :obj:`some`, :obj:`other`, :obj:`funcs`
-   \n   :obj:`otherfunc`
+
+   :obj:`some`, :obj:`other`, :obj:`funcs`
+   \n\
+   :obj:`otherfunc`
        relationship
 """
         self.assertEqual(expected, actual)
 
-        docstring = """
-            numpy.multivariate_normal(mean, cov, shape=None, spam=None)
+        docstring = """\
+numpy.multivariate_normal(mean, cov, shape=None, spam=None)
 
-            See Also
-            --------
-            some, other, funcs
-            otherfunc : relationship
+See Also
+--------
+some, other, funcs
+otherfunc : relationship
 
-            """
+"""
 
         config = Config()
         app = Mock()
-        actual = str(NumpyDocstring(textwrap.dedent(docstring),
-                                    config, app, "method"))
+        actual = str(NumpyDocstring(docstring, config, app, "method"))
 
-        expected = """
+        expected = """\
 numpy.multivariate_normal(mean, cov, shape=None, spam=None)
 
 .. seealso::
-\n   :meth:`some`, :meth:`other`, :meth:`funcs`
-   \n   :meth:`otherfunc`
+
+   :meth:`some`, :meth:`other`, :meth:`funcs`
+   \n\
+   :meth:`otherfunc`
        relationship
 """
         self.assertEqual(expected, actual)