From 89109708ebc130692902761a6dd8f1387332c281 Mon Sep 17 00:00:00 2001 From: Dennis Kempin Date: Fri, 6 Feb 2015 15:39:44 -0800 Subject: [PATCH] Support Sphinx inline parameter syntax. Sphinx allows types of parameters to be specified in the :param tag without a separate :type line, such as ":param int a: desc", which results in a more compact docstring. This change adds support by extending the docstring parser to parse these lines. Various test cases are included. Note: My IDE removed a couple of trailing whitespaces in the edited files. --- src/contracts/docstring_parsing.py | 51 ++++++++----- src/contracts/testing/test_decorator.py | 15 +++- .../testing/test_docstring_parsing.py | 75 ++++++++++++++----- 3 files changed, 102 insertions(+), 39 deletions(-) diff --git a/src/contracts/docstring_parsing.py b/src/contracts/docstring_parsing.py index 59f0e0d..43aad7f 100644 --- a/src/contracts/docstring_parsing.py +++ b/src/contracts/docstring_parsing.py @@ -80,31 +80,41 @@ def parse(docstring): # var_keys = ['var', 'ivar', 'cvar'] # raises, raise, except, exception - docstring, params_ann = parse_annotations(docstring, param_keys, False) - docstring, types_ann = parse_annotations(docstring, type_keys, False) - docstring, returns_ann = \ - parse_annotations(docstring, return_keys, True) - docstring, rtype_ann = parse_annotations(docstring, rtype_keys, True) + 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: - params[name] = Arg(params_ann.get(name, None), - types_ann.get(name, None)) + 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))): - returns.append(Arg(returns_ann.get(i, None), - rtype_ann.get(i, None))) + 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) -def parse_annotations(docstring, keys, empty=False): +def parse_annotations(docstring, keys, empty=False, inline_type=False): """ - Returns docstring_without, dictionary. - If empty specified, will look for empty statements, and give integers - for names. + 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 @@ -112,10 +122,11 @@ def parse_annotations(docstring, keys, empty=False): for key in keys: if empty: - regexp = '^\s*:\s*%s\s*:\s*(?P.*?)\s*$' % key - else: - regexp = ('^\s*:\s*%s\s+(?P\w*?)\s*:\s*(?P.*?)\s*$' + regexp = ('^\s*:\s*%s(?P[^:]*?)\s*:\s*(?P.*?)\s*$' % key) + else: + regexp = ('^\s*:\s*%s\s+(?P[^:]*?)(?P[^\s:]+)\s*:' + '\s*(?P.*?)\s*$' % key) regexp = re.compile(regexp, re.MULTILINE) def replace(match): @@ -124,10 +135,12 @@ def replace(match): if empty: name = len(found) else: - name = d['name'] - - found[name] = d['desc'] + 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) diff --git a/src/contracts/testing/test_decorator.py b/src/contracts/testing/test_decorator.py index 929abc4..4ac4760 100644 --- a/src/contracts/testing/test_decorator.py +++ b/src/contracts/testing/test_decorator.py @@ -209,7 +209,7 @@ def f1(a, b): # @UnusedVariable # def test_module_as_decorator(self): # import contracts as contract_module - # + # # @contract_module # def f(a, b): #@UnusedVariable # return a + b @@ -229,6 +229,19 @@ def f(a, b): self.assertRaises(ContractNotRespected, f, 1.0, 2) self.assertRaises(ContractNotRespected, f, 1, 2.0) + def test_inline_docstring_format_works(self): + @contract + def f(a, b): + """ This is good + :param int,>0 a: Description + :param int,>0 b: Description + :returns int,>0: Description + """ + return a + b + f(1, 2) + self.assertRaises(ContractNotRespected, f, 1.0, 2) + self.assertRaises(ContractNotRespected, f, -1, 2) + def test_check_docstring_maintained(self): def f1(a, b): """ This is good diff --git a/src/contracts/testing/test_docstring_parsing.py b/src/contracts/testing/test_docstring_parsing.py index 0804017..17e87b1 100644 --- a/src/contracts/testing/test_docstring_parsing.py +++ b/src/contracts/testing/test_docstring_parsing.py @@ -4,27 +4,27 @@ from contracts.interface import add_prefix -examples = {""" - Provides a RGB representation of the values by interpolating the range +examples = {""" + Provides a RGB representation of the values by interpolating the range [min(value),max(value)] into the colorspace [min_color, max_color]. - + :param value: The field to represent. :type value: HxW array - + :param max_value: If specified, everything *above* is clipped. :type max_value: float :param min_value: If specified, everything *below* is clipped. :type min_value: float - + :param min_color: Color to give to the minimum values. - - + + :return: A RGB image. :rtype: HxWx3 uint8 :return: gray - """: DocStringInfo(docstring=' \n Provides a RGB representation of the values by interpolating the range \n' + """: DocStringInfo(docstring='\n Provides a RGB representation of the values by interpolating the range\n' ' [min(value),max(value)] into the colorspace [min_color, max_color].\n', params={ 'value': Arg('The field to represent.', 'HxW array'), @@ -32,13 +32,13 @@ 'min_value': Arg('If specified, everything *below* is clipped.', 'float'), 'min_color': Arg('Color to give to the minimum values.', None), }, - returns=[Arg('A RGB image.', "HxWx3 uint8"), Arg('gray', None)] -) + returns=[Arg('A RGB image.', "HxWx3 uint8"), Arg('gray', None)] +) } class DocStringTest(unittest.TestCase): - + def test_parsing(self): for string in examples: parsed = DocStringInfo.parse(string) @@ -46,7 +46,7 @@ def test_parsing(self): "%r" % parsed result = examples[string] self.assertEqual(result, parsed) - + def test_number_of_spaces(self): self.assertEqual(number_of_spaces(''), 0) self.assertEqual(number_of_spaces(' '), 1) @@ -54,18 +54,55 @@ def test_number_of_spaces(self): self.assertEqual(number_of_spaces('11'), 0) self.assertEqual(number_of_spaces(' 223'), 1) self.assertEqual(number_of_spaces(' 4343'), 2) - + def test_reparsing(self): for string, result in examples.items(): #@UnusedVariable parsed = DocStringInfo.parse(string) converted = "%s" % parsed reparsed = DocStringInfo.parse(converted) - - msg = ('First string:\n%s\nParsed as:\n%s\n' % + + msg = ('First string:\n%s\nParsed as:\n%s\n' % (add_prefix(string, '|'), add_prefix('%r' % parsed, '|'))) - - msg += ('Converted:\n%s\nReparsed as:\n%s\n' % + + msg += ('Converted:\n%s\nReparsed as:\n%s\n' % (add_prefix(converted, '|'), add_prefix('%r' % reparsed, '|'))) - + self.assertEqual(parsed, reparsed, msg=msg) - + + def test_inline_params(self): + def test_inline_parsing(docstring, expected_type="type", + expected_desc="desc"): + info = DocStringInfo.parse(docstring) + self.assertTrue("name" in info.params) + self.assertEqual(info.params["name"].type, expected_type) + self.assertEqual(info.params["name"].desc, expected_desc) + + # Proper syntax + test_inline_parsing(":param type name: desc") + test_inline_parsing(":param name: desc", None) + test_inline_parsing(":param name:", None, None) + + # Weird syntax for people who like to break things. + test_inline_parsing(" : param type name : desc ") + test_inline_parsing(" : param name : desc ", None) + test_inline_parsing(" : param name : ", None, None) + test_inline_parsing(" : param type , > 0 name : ", "type , > 0", None) + + def test_inline_returns(self): + def test_inline_parsing(docstring, expected_type="type", + expected_desc="desc"): + info = DocStringInfo.parse(docstring) + self.assertTrue(len(info.returns) > 0) + self.assertEqual(info.returns[0].type, expected_type) + self.assertEqual(info.returns[0].desc, expected_desc) + + # Proper syntax + test_inline_parsing(":returns type: desc") + test_inline_parsing(":returns: desc", None) + test_inline_parsing(":returns:", None, None) + + # Weird syntax for people who like to break things. + test_inline_parsing(" : returns type : desc ") + test_inline_parsing(" : returns : desc ", None) + test_inline_parsing(" : returns : ", None, None) + test_inline_parsing(" : returns type , > 0 : ", "type , > 0", None)