From 5eff29f5e719ad28650e2b6bbd8d1a348fd3d0d3 Mon Sep 17 00:00:00 2001 From: Sergei Izmailov Date: Sat, 16 May 2020 01:34:30 +0300 Subject: [PATCH] documentation/python: improve pybind11 default argument parsing. --- documentation/python.py | 24 +++++++-- documentation/test_python/test_pybind.py | 65 ++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/documentation/python.py b/documentation/python.py index a9cd0663..8aefd153 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -781,7 +781,21 @@ def make_name_link(state: State, referrer_path: List[str], name) -> str: _pybind_name_rx = re.compile('[a-zA-Z0-9_]*') _pybind_arg_name_rx = re.compile('[*a-zA-Z0-9_]+') _pybind_type_rx = re.compile('[a-zA-Z0-9_.]+') -_pybind_default_value_rx = re.compile('[^,)]+') + +def _pybind11_default_argument_length(string): + """Returns length of balanced []()-expression at begin of input string until `,` or `)`""" + stack = [] + for i, c in enumerate(string): + if len(stack) == 0 and (c == ',' or c == ')'): + return i + if c == '(': + stack.append(')') + elif c == '[': + stack.append(']') + elif c == ')' or c == ']': + if len(stack) == 0 or c != stack.pop(): + raise SyntaxError("Unmatched {} at pos {} in `{}`".format(c, i, string)) + raise SyntaxError("Unexpected end of `{}`".format(string)) def parse_pybind_type(state: State, referrer_path: List[str], signature: str) -> str: # If this doesn't match, it's because we're in Callable[[arg, ...], retval] @@ -857,14 +871,14 @@ def parse_pybind_signature(state: State, referrer_path: List[str], signature: st arg_type = None arg_type_link = None - # Default (optional) -- for now take everything until the next comma - # TODO: ugh, do properly + # Default (optional) # The equals has spaces around since 2.3.0, preserve 2.2 compatibility. # https://github.com/pybind/pybind11/commit/0826b3c10607c8d96e1d89dc819c33af3799a7b8 if signature.startswith(('=', ' = ')): signature = signature[1 if signature[0] == '=' else 3:] - default = _pybind_default_value_rx.match(signature).group(0) - signature = signature[len(default):] + default_length = _pybind11_default_argument_length(signature) + default = signature[:default_length] + signature = signature[default_length:] else: default = None diff --git a/documentation/test_python/test_pybind.py b/documentation/test_python/test_pybind.py index 2dbdf765..f3af8028 100644 --- a/documentation/test_python/test_pybind.py +++ b/documentation/test_python/test_pybind.py @@ -136,6 +136,42 @@ class Signature(unittest.TestCase): ('b', 'str', 'str', '\'hello\''), ], None, None)) + self.assertEqual(parse_pybind_signature(self.state, [], + 'foo(a: float=libA.foo(libB.goo(123), libB.bar + 13) + 2, b=3)'), + ('foo', '', [ + ('a', 'float', 'float', 'libA.foo(libB.goo(123), libB.bar + 13) + 2'), + ('b', None, None, '3'), + ], None, None)) + + self.assertEqual(parse_pybind_signature(self.state, [], + 'foo(a: List=[1, 2, 3], b: Tuple=(1, 2, 3, "str"))'), + ('foo', '', [ + ('a', 'typing.List', 'typing.List', '[1, 2, 3]'), + ('b', "typing.Tuple", "typing.Tuple", '(1, 2, 3, "str")'), + ], None, None)) + + self.assertEqual(parse_pybind_signature(self.state, [], + 'foo(a: Tuple[int, ...]=(1,("hello", \'world\'),3,4))'), + ('foo', '', [ + ('a', 'typing.Tuple[int, ...]', + 'typing.Tuple[int, ...]', + '(1,("hello", \'world\'),3,4)') + ], None, None)) + + self.assertEqual(parse_pybind_signature(self.state, [], + 'foo(a: str=[dict(key="A", value=\'B\')["key"][0], None][0])'), + ('foo', '', [ + ('a', 'str', 'str', '[dict(key="A", value=\'B\')["key"][0], None][0]') + ], None, None)) + + bad_signature = ('foo', '', [('…', None, None, None)], None, None) + + self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: float=[0][)'), bad_signature) + self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: float=()'), bad_signature) + self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: float=(()'), bad_signature) + self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: float=))'), bad_signature) + self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: float=])'), bad_signature) + def test_default_values_pybind23(self): self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: float = 1.0, b: str = \'hello\')'), @@ -144,6 +180,35 @@ class Signature(unittest.TestCase): ('b', 'str', 'str', '\'hello\''), ], None, None)) + self.assertEqual(parse_pybind_signature(self.state, [], + 'foo(a: float = libA.foo(libB.goo(123), libB.bar + 13) + 2, b=3)'), + ('foo', '', [ + ('a', 'float', 'float', 'libA.foo(libB.goo(123), libB.bar + 13) + 2'), + ('b', None, None, '3'), + ], None, None)) + + self.assertEqual(parse_pybind_signature(self.state, [], + 'foo(a: Tuple[int, ...] = (1,("hello", \'world\'),3,4))'), + ('foo', '', [ + ('a', 'typing.Tuple[int, ...]', + 'typing.Tuple[int, ...]', + '(1,("hello", \'world\'),3,4)') + ], None, None)) + + self.assertEqual(parse_pybind_signature(self.state, [], + 'foo(a: str = [dict(key="A", value=\'B\')["key"][0], None][0])'), + ('foo', '', [ + ('a', 'str', 'str', '[dict(key="A", value=\'B\')["key"][0], None][0]') + ], None, None)) + + bad_signature = ('foo', '', [('…', None, None, None)], None, None) + + self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: float = [0][)'), bad_signature) + self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: float = ()'), bad_signature) + self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: float = (()'), bad_signature) + self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: float = ))'), bad_signature) + self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: float = ])'), bad_signature) + def test_crazy_stuff(self): self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: int, b: Math::Vector<4, UnsignedInt>)'), -- 2.30.2