Skip to content

Commit

Permalink
Added decorator to parse a Sphinx docstring to obtain arg description…
Browse files Browse the repository at this point in the history
… and type information
  • Loading branch information
Josh Levy-Kramer committed Aug 11, 2016
1 parent 4fcd062 commit 4c50110
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 2 deletions.
57 changes: 55 additions & 2 deletions argh/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
ATTR_WRAPPED_EXCEPTIONS,
ATTR_WRAPPED_EXCEPTIONS_PROCESSOR,
ATTR_EXPECTS_NAMESPACE_OBJECT)
from argh.parse_sphinx import parse_sphinx_doc
from argh.utils import func_kwargs_args


__all__ = ['aliases', 'named', 'arg', 'wrap_errors', 'expects_obj']
__all__ = ['aliases', 'named', 'arg', 'wrap_errors', 'expects_obj', 'parse_docstring']


def named(new_name):
Expand Down Expand Up @@ -193,3 +194,55 @@ def foo(bar, quux=123):
"""
setattr(func, ATTR_EXPECTS_NAMESPACE_OBJECT, True)
return func



def parse_docstring(format='sphinx'):
"""
A decorator that automatically adds the parameter description and type to the argh parser
:param format: Which docstring format is being used
:return: A function
"""

if format not in ['sphinx']:
raise NotImplementedError("sphinx is currently only supported")

type_func_map = {
'str': str,
'int': int,
'float': float,
'open': open
}

def wrapper(func):
doc = func.__doc__
doc = parse_sphinx_doc(doc)
declared_args = getattr(func, ATTR_ARGS, [])

kward_args = func_kwargs_args(func)
arguments = doc['arguments']

# For each parsed argument
for name, attr in arguments.items():

add_args_settings = {}
add_args_settings['help'] = attr.get('description', None)

kward = kward_args[name]
if not kward:
name_str = name
else:
name_str = '--' + name

type = attr.get('type_name', None)
if type is not None:
type = type_func_map[type] # Needs to be a function
add_args_settings['type'] = type

declared_args.append(dict(option_strings=[name_str], **add_args_settings))

setattr(func, ATTR_ARGS, declared_args)
return func

return wrapper

107 changes: 107 additions & 0 deletions argh/parse_sphinx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@

import re
from collections import OrderedDict

PARAM_RE = re.compile(':param (\S*):\s*(.*)')
TYPE_RE = re.compile(':type (\S*):\s*(.*)')
RETURN_RE = re.compile(':return:\s*(.*)')
RTYPE_RE = re.compile(':rtype:\s*(.*)')


def parse_sphinx_doc(doc):
"""
Parse sphinx docstring and return a dictionary of attributes. If attributes not found they will not be included.
e.g. for:
'''
This is the description
:param foo: Describe foo
:type foo: bool
:return: bar
'''
The dict returned:
{
'description': 'This is the description',
'parameters': OrderedDict([
('foo', {'description': 'Describe foo', 'type': 'bool'})
]),
'return': {'description': 'bar'}
}
Attributes parsed:
* param
* type
* return
* rtype
:param doc: The docstring of a function or method
:type doc: str
:return: A nested dict containing the attributes
:rtype: dict
"""


lines = doc.expandtabs().splitlines()
found_attribute = False

doc_dict = {}

for line in lines:
line = line.strip()

param_m = PARAM_RE.search(line)
type_m = TYPE_RE.search(line)
return_m = RETURN_RE.search(line)
rtype_m = RTYPE_RE.search(line)

if param_m is not None:
var, description = param_m.groups()
description = description.strip()
if description == '': continue
parameters = doc_dict.get('arguments', OrderedDict())
param = parameters.get(var, {})
param['description'] = description
parameters[var] = param
doc_dict['arguments'] = parameters

elif type_m is not None:
var, type = type_m.groups()
type = type.strip()
if type == '': continue
parameters = doc_dict.get('arguments', OrderedDict())
param = parameters.get(var, {})
param['type'] = type
parameters[var] = param
doc_dict['arguments'] = parameters

elif return_m is not None:
description = return_m.groups()
description = description[0].strip()
if description == '': continue
return_dict = doc_dict.get('return', {})
return_dict['description'] = description
doc_dict['return'] = return_dict

elif rtype_m is not None:
type = rtype_m.groups()
type = type[0].strip()
if type == '': continue
return_dict = doc_dict.get('return', {})
return_dict['type'] = type
doc_dict['return'] = return_dict

if (param_m or type_m or return_m or rtype_m) is not None:
found_attribute = True

if not found_attribute:
func_description = doc_dict.get('description', '')
func_description += line+'\n'
doc_dict['description'] = func_description

if 'description' in doc_dict:
doc_dict['description'] = doc_dict['description'].strip()

return doc_dict
35 changes: 35 additions & 0 deletions argh/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import inspect

from argh import compat
from argh.parse_sphinx import parse_sphinx_doc


def get_subparsers(parser, create=False):
Expand Down Expand Up @@ -53,3 +54,37 @@ def get_arg_spec(function):
if inspect.ismethod(function):
spec = spec._replace(args=spec.args[1:])
return spec


def func_kwargs_args(function):
"""
Return a dict which specifies which arguments of a function are key-word (True) or positional (False)
:param func: A method or function
:return: A dict - keys args/kwargs names : True if keyword arg, False if not
"""
args, varargs, varkw, argspec_defaults = get_arg_spec(function)

defaults = {}
if argspec_defaults is not None:
defaults = dict(zip(reversed(args), reversed(argspec_defaults)))

args_dict = {}
for arg in args:
args_dict[arg] = arg in defaults # If in True, else False

return args_dict


def parse_description(func, format='sphinx'):
"""
Returns the function description from the docstring
:param func: A function
:param format: Which docstring format
:return: String of the description
"""

if format not in ['sphinx']:
raise NotImplementedError("sphinx is currently only supported")

return parse_sphinx_doc(func.__doc__).get('description', None)

0 comments on commit 4c50110

Please sign in to comment.