Skip to content

Commit

Permalink
Merge pull request #33 from denniskempin/master
Browse files Browse the repository at this point in the history
Support Sphinx inline parameter syntax. (Dennis Kempin, Google)
  • Loading branch information
AndreaCensi committed Feb 7, 2015
2 parents 5657b5e + 8910970 commit 859ecc5
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 39 deletions.
51 changes: 32 additions & 19 deletions src/contracts/docstring_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,42 +80,53 @@ 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

found = {}

for key in keys:
if empty:
regexp = '^\s*:\s*%s\s*:\s*(?P<desc>.*?)\s*$' % key
else:
regexp = ('^\s*:\s*%s\s+(?P<name>\w*?)\s*:\s*(?P<desc>.*?)\s*$'
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):
Expand All @@ -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)
Expand Down
15 changes: 14 additions & 1 deletion src/contracts/testing/test_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
75 changes: 56 additions & 19 deletions src/contracts/testing/test_docstring_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,105 @@
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'),
'max_value': Arg('If specified, everything *above* is clipped.', 'float'),
'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)
"%s" % parsed
"%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)
self.assertEqual(number_of_spaces(' '), 2)
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)

0 comments on commit 859ecc5

Please sign in to comment.