Source code for contracts.docstring_parsing

import re


[docs]class Arg(object): def __init__(self, desc=None, type=None): # @ReservedAssignment if desc is not None: desc = desc.strip() self.desc = desc if type is not None: type = type.strip() # @ReservedAssignment self.type = type def __eq__(self, other): return (self.type == other.type and self.desc == other.desc) def __repr__(self): return "Arg(%r,%r)" % (self.desc, self.type)
[docs]class DocStringInfo(object): def __init__(self, docstring=None, params=None, returns=None): if params is None: params = {} if returns is None: returns = [] self.docstring = docstring self.params = params self.returns = returns def __eq__(self, other): return (self.docstring == other.docstring and self.params == other.params and self.returns == other.returns) def __repr__(self): return ("DocString(\n\t%r,\n\t%r,\n\t%r)" % (self.docstring, self.params, self.returns)) def __str__(self): s = self.docstring valid_lines = [x for x in self.docstring.split('\n') if x] if valid_lines: last_line = [-1] indentation = number_of_spaces(last_line) else: indentation = 0 # ORDER? s += '\n\n' prefix = '\n' + (' ' * indentation) for param in self.params: if self.params[param].desc is not None: s += (prefix + ':param %s: %s' % (param, self.params[param].desc)) if self.params[param].type is not None: s += (prefix + ':type %s: %s' % (param, self.params[param].type)) s += prefix if self.returns: for r in self.returns: if r.desc is not None: s += prefix + ':returns: %s' % (r.desc) if r.type is not None: s += prefix + ':rtype: %s' % (r.type) s += prefix return s
[docs] @staticmethod def parse(docstring): assert docstring is not None param_keys = ['param', 'parameter', 'arg', 'argument', 'key', 'keyword'] type_keys = ['type'] return_keys = ['returns', 'return'] rtype_keys = ['rtype'] # TODO: document state? # var_keys = ['var', 'ivar', 'cvar'] # raises, raise, except, exception docstring, params_ann = parse_annotations(docstring, param_keys, False, True) docstring, types_ann = parse_annotations(docstring, type_keys, False, False) docstring, returns_ann = parse_annotations(docstring, return_keys, True, True) docstring, rtype_ann = parse_annotations(docstring, rtype_keys, True, False) params = {} names = set(list(params_ann.keys()) + list(types_ann.keys())) for name in names: param_type, param_desc = params_ann.get(name, (None, None)) params[name] = Arg(param_desc, param_type or types_ann.get(name, None)) returns = [] for i in range(max(len(returns_ann), len(rtype_ann))): return_type, return_desc = returns_ann.get(i, (None, None)) returns.append(Arg(return_desc, return_type or rtype_ann.get(i, None))) return DocStringInfo(docstring, params=params, returns=returns)
[docs]def parse_annotations(docstring, keys, empty=False, inline_type=False): """ Parses ":key name: description" lines into a dictionary mapping name to a description. If empty is specified, look statements without a name such as ":key: description". If inline_type is specified, allow an optional type to be specified parsing ":key type name: description" or ":key type: description". """ assert docstring is not None found = {} for key in keys: if empty: regexp = ('^\s*:\s*%s(?P<type>[^:]*?)\s*:\s*(?P<desc>.*?)\s*$' % key) else: regexp = ('^\s*:\s*%s\s+(?P<type>[^:]*?)(?P<name>[^\s:]+)\s*:' '\s*(?P<desc>.*?)\s*$' % key) regexp = re.compile(regexp, re.MULTILINE) def replace(match): d = match.groupdict() if empty: name = len(found) else: name = d['name'] or None if inline_type: found[name] = (d['type'] or None, d['desc'] or None) else: found[name] = d['desc'] or None return "" docstring = regexp.sub(repl=replace, string=docstring) return docstring, found
[docs]def number_of_spaces(x): for i in range(1, len(x)): if x[:i] != ' ' * i: return i - 1 return len(x)