Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decorator to parse a Sphinx docstring to obtain arg description #109

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)