From 4c501102299fa64ecb9d74fb1af9428eb7b069dd Mon Sep 17 00:00:00 2001 From: Josh Levy-Kramer Date: Thu, 11 Aug 2016 14:18:14 +0100 Subject: [PATCH] Added decorator to parse a Sphinx docstring to obtain arg description and type information --- argh/decorators.py | 57 ++++++++++++++++++++++- argh/parse_sphinx.py | 107 +++++++++++++++++++++++++++++++++++++++++++ argh/utils.py | 35 ++++++++++++++ 3 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 argh/parse_sphinx.py diff --git a/argh/decorators.py b/argh/decorators.py index da176e6..19b732e 100644 --- a/argh/decorators.py +++ b/argh/decorators.py @@ -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): @@ -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 + diff --git a/argh/parse_sphinx.py b/argh/parse_sphinx.py new file mode 100644 index 0000000..0dfabf2 --- /dev/null +++ b/argh/parse_sphinx.py @@ -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 \ No newline at end of file diff --git a/argh/utils.py b/argh/utils.py index 8650bb7..6b37894 100644 --- a/argh/utils.py +++ b/argh/utils.py @@ -16,6 +16,7 @@ import inspect from argh import compat +from argh.parse_sphinx import parse_sphinx_doc def get_subparsers(parser, create=False): @@ -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) \ No newline at end of file