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

This commit is contained in:
Rob Ruana 2014-03-21 19:21:18 -04:00
parent 3e7ce5d3a1
commit c430f7f4c6
2 changed files with 130 additions and 62 deletions

View File

@ -24,7 +24,8 @@ if sys.version_info[0] >= 3:
_directive_regex = re.compile(r'\.\. \S+::') _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): class GoogleDocstring(object):
@ -213,12 +214,22 @@ class GoogleDocstring(object):
def _consume_field(self, parse_type=True, prefer_type=False): def _consume_field(self, parse_type=True, prefer_type=False):
line = self._line_iter.next() line = self._line_iter.next()
_name, _, _desc = line.partition(':')
_name, _type, _desc = _name.strip(), '', _desc.strip() match = None
match = _field_parens_regex.match(_name) _name, _type, _desc = line.strip(), '', ''
if parse_type and match: if parse_type:
_name = match.group(1) match = _google_typed_arg_regex.match(line)
_type = match.group(2) 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: if prefer_type and not _type:
_type, _name = _name, _type _type, _name = _name, _type
indent = self._get_indent(line) + 1 indent = self._get_indent(line) + 1
@ -238,17 +249,21 @@ class GoogleDocstring(object):
def _consume_returns_section(self): def _consume_returns_section(self):
lines = self._dedent(self._consume_to_next_section()) lines = self._dedent(self._consume_to_next_section())
if lines: if lines:
if ':' in lines[0]: _name, _type, _desc = '', '', lines
_type, _, _desc = lines[0].partition(':') match = _google_typed_arg_regex.match(lines[0])
_name, _type, _desc = '', _type.strip(), _desc.strip() if match:
match = _field_parens_regex.match(_type) _name = match.group(1)
_type = match.group(2)
_desc = match.group(3)
else:
match = _google_untyped_arg_regex.match(lines[0])
if match: if match:
_name = match.group(1) _type = match.group(1)
_type = match.group(2) _desc = match.group(2)
if match:
lines[0] = _desc lines[0] = _desc
_desc = lines _desc = lines
else:
_name, _type, _desc = '', '', lines
_desc = self.__class__(_desc, self._config).lines() _desc = self.__class__(_desc, self._config).lines()
return [(_name, _type, _desc,)] return [(_name, _type, _desc,)]
else: else:

View File

@ -146,6 +146,19 @@ class GoogleDocstringTest(BaseDocstringTest):
:returns: *str* -- :returns: *str* --
Extended Extended
description of return value""" 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): def test_docstrings(self):
@ -155,6 +168,43 @@ class GoogleDocstringTest(BaseDocstringTest):
expected = textwrap.dedent(expected) expected = textwrap.dedent(expected)
self.assertEqual(expected, actual) 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): class NumpyDocstringTest(BaseDocstringTest):
docstrings = [( docstrings = [(
@ -268,95 +318,98 @@ class NumpyDocstringTest(BaseDocstringTest):
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
def test_parameters_with_class_reference(self): def test_parameters_with_class_reference(self):
docstring = """ docstring = """\
Parameters Parameters
---------- ----------
param1 : :class:`MyClass <name.space.MyClass>` instance param1 : :class:`MyClass <name.space.MyClass>` instance
""" """
config = Config(napoleon_use_param=False) config = Config(napoleon_use_param=False)
actual = str(NumpyDocstring(textwrap.dedent(docstring), config)) actual = str(NumpyDocstring(docstring, config))
expected = textwrap.dedent(""" expected = """\
:Parameters: **param1** (:class:`MyClass <name.space.MyClass>` instance) :Parameters: **param1** (:class:`MyClass <name.space.MyClass>` instance)
""") """
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
config = Config(napoleon_use_param=True) config = Config(napoleon_use_param=True)
actual = str(NumpyDocstring(textwrap.dedent(docstring), config)) actual = str(NumpyDocstring(docstring, config))
expected = textwrap.dedent(""" expected = """\
:type param1: :class:`MyClass <name.space.MyClass>` instance :type param1: :class:`MyClass <name.space.MyClass>` instance
""") """
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
def test_parameters_without_class_reference(self): def test_parameters_without_class_reference(self):
docstring = """ docstring = """\
Parameters Parameters
---------- ----------
param1 : MyClass instance param1 : MyClass instance
""" """
config = Config(napoleon_use_param=False) config = Config(napoleon_use_param=False)
actual = str(NumpyDocstring(textwrap.dedent(docstring), config)) actual = str(NumpyDocstring(docstring, config))
expected = textwrap.dedent(""" expected = """\
:Parameters: **param1** (*MyClass instance*) :Parameters: **param1** (*MyClass instance*)
""") """
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
config = Config(napoleon_use_param=True) config = Config(napoleon_use_param=True)
actual = str(NumpyDocstring(textwrap.dedent(docstring), config)) actual = str(NumpyDocstring(textwrap.dedent(docstring), config))
expected = textwrap.dedent(""" expected = """\
:type param1: MyClass instance :type param1: MyClass instance
""") """
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
def test_see_also_refs(self): def test_see_also_refs(self):
docstring = """ docstring = """\
numpy.multivariate_normal(mean, cov, shape=None, spam=None) numpy.multivariate_normal(mean, cov, shape=None, spam=None)
See Also See Also
-------- --------
some, other, funcs some, other, funcs
otherfunc : relationship otherfunc : relationship
""" """
actual = str(NumpyDocstring(textwrap.dedent(docstring))) actual = str(NumpyDocstring(docstring))
expected = """ expected = """\
numpy.multivariate_normal(mean, cov, shape=None, spam=None) numpy.multivariate_normal(mean, cov, shape=None, spam=None)
.. seealso:: .. seealso::
\n :obj:`some`, :obj:`other`, :obj:`funcs`
\n :obj:`otherfunc` :obj:`some`, :obj:`other`, :obj:`funcs`
\n\
:obj:`otherfunc`
relationship relationship
""" """
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
docstring = """ docstring = """\
numpy.multivariate_normal(mean, cov, shape=None, spam=None) numpy.multivariate_normal(mean, cov, shape=None, spam=None)
See Also See Also
-------- --------
some, other, funcs some, other, funcs
otherfunc : relationship otherfunc : relationship
""" """
config = Config() config = Config()
app = Mock() app = Mock()
actual = str(NumpyDocstring(textwrap.dedent(docstring), actual = str(NumpyDocstring(docstring, config, app, "method"))
config, app, "method"))
expected = """ expected = """\
numpy.multivariate_normal(mean, cov, shape=None, spam=None) numpy.multivariate_normal(mean, cov, shape=None, spam=None)
.. seealso:: .. seealso::
\n :meth:`some`, :meth:`other`, :meth:`funcs`
\n :meth:`otherfunc` :meth:`some`, :meth:`other`, :meth:`funcs`
\n\
:meth:`otherfunc`
relationship relationship
""" """
self.assertEqual(expected, actual) self.assertEqual(expected, actual)