chiark / gitweb /
documentation/python: improve pybind11 default argument parsing.
authorSergei Izmailov <sergei.a.izmailov@gmail.com>
Fri, 15 May 2020 22:34:30 +0000 (01:34 +0300)
committerVladimír Vondruš <mosra@centrum.cz>
Mon, 8 Jun 2020 17:42:29 +0000 (19:42 +0200)
documentation/python.py
documentation/test_python/test_pybind.py

index a9cd06630186af45e8bfe768570d109cee799cd3..8aefd15321b0faede9916e2fdabcbdeb307c54f0 100755 (executable)
@@ -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
 
index 2dbdf76520def3ff69c8fad3f203ac98467272b5..f3af802817ec74b1c3085bc9ef527465f1bd7ab7 100644 (file)
@@ -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>)'),