Skip to content

Commit

Permalink
refactor: update docstring parser and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jnoortheen committed Nov 1, 2020
1 parent 98b0996 commit 3d9d681
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 13 deletions.
32 changes: 19 additions & 13 deletions arger/parser/docstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def parse_params(self, params: str) -> Dict[str, ParamDocTp]:
def parse(self, doc: str) -> DocstringTp:
desc, _, params = self.pattern.split(doc, maxsplit=1)
other_sect, params = self.get_rest_of_section(params)
return DocstringTp(desc, other_sect, params=self.parse_params(params))
return DocstringTp(desc.strip(), other_sect, params=self.parse_params(params))


class GoogleDocParser(NumpyDocParser):
Expand All @@ -113,34 +113,40 @@ class RstDocParser(DocstringParser):

def __init__(self):
self.pattern = re.compile(r':param')
self.section_ptrn = re.compile(
r'\n:[\w]+\s'
) # matches any start of the section
self.param_ptrn = re.compile(r'^\s+(?P<param>\w+):\s+(?P<doc>[\s\S]+)')
self.section_ptrn = re.compile(r'\n:[\w]+') # matches any start of the section
self.param_ptrn = re.compile(r'^[ ]+(?P<tp_param>.+):[ ]*(?P<doc>[\s\S]+)')

def parse(self, doc: str) -> DocstringTp:
lines = self.pattern.split(doc)
long_desc = lines.pop(0)
epilog = ''
params = {}
for lin in lines:
param_doc, _ = self.section_ptrn.split(lin, maxsplit=1)
match = self.param_ptrn.match(param_doc)
for idx, lin in enumerate(lines):
sections = self.section_ptrn.split(lin, maxsplit=1)
if idx + 1 == len(lines) and len(sections) > 1:
epilog = sections[-1]
match = self.param_ptrn.match(sections[0])
if match:
param, doc = match.groups()
params[param] = ParamDocTp(None, *get_flags_from_param_doc(doc))
tp_param, doc = match.groups() # type: str, str
parts = tp_param.strip().rsplit(' ', maxsplit=1)
param = parts[-1].strip()
type_hint = None
if len(parts) > 1:
type_hint = parts[0].strip()
params[param] = ParamDocTp(type_hint, *get_flags_from_param_doc(doc))

return DocstringTp(long_desc, epilog='', params=params)
return DocstringTp(long_desc.strip(), epilog, params)


@functools.lru_cache(None)
def get_parsers():
"""Cache costly init phase per session."""
return [cls() for cls in DocstringParser.__subclasses__()]
return [NumpyDocParser(), GoogleDocParser(), RstDocParser()]


def parse_docstring(doc: Optional[str]) -> DocstringTp:
if doc:
for parser in get_parsers():
if parser.matches(doc):
return parser.parse(doc)
return DocstringTp(description=doc, epilog='', params={})
return DocstringTp(description=doc or '', epilog='', params={})
90 changes: 90 additions & 0 deletions tests/test_docstrings_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import inspect

import pytest

from arger.parser.docstring import parse_docstring, ParamDocTp


def func_numpy():
"""Summary line.
Extended description of function.
Parameters
----------
arg1 : int
Description of arg1
arg2 : str
Description of arg2
a line that extends to next line
arg3 :
arg3 without any type
Returns
-------
bool
Description of return value
"""


def func_google():
"""Summary line.
Extended description of function.
Args:
arg1 (int): Description of arg1
arg2 (str): Description of arg2
a line that extends to next line
arg3 : arg3 without any type
Returns:
bool: Description of return value
"""


# https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html
def func_rst():
"""Summary line.
Extended description of function.
:param int arg1: Description of arg1
:param str arg2: Description of arg2
a line that extends to next line
:param arg3 : arg3 without any type
:return: Description of return value
:rtype: bool
"""


@pytest.mark.parametrize(
'fn',
[
func_numpy,
func_google,
func_rst,
],
)
def test_docstring_parser(fn):
result = parse_docstring(inspect.getdoc(fn))
assert result.description == "Summary line.\n\nExtended description of function."
assert list(result.params) == ['arg1', 'arg2', 'arg3']
assert list(result.params.values()) == [
ParamDocTp(
type_hint='int',
flags=[],
doc='Description of arg1',
),
ParamDocTp(
type_hint='str',
flags=[],
doc='Description of arg2 a line that extends to next line',
),
ParamDocTp(
type_hint=None,
flags=[],
doc='arg3 without any type',
),
]
assert 'Description of return value' in result.epilog

0 comments on commit 3d9d681

Please sign in to comment.