chiark / gitweb /
documentation/python: support insane pybind 2.6 enum printing.
authorVladimír Vondruš <mosra@centrum.cz>
Sun, 2 Jan 2022 23:46:59 +0000 (00:46 +0100)
committerVladimír Vondruš <mosra@centrum.cz>
Mon, 3 Jan 2022 01:36:44 +0000 (02:36 +0100)
What the hell, why.

documentation/python.py
documentation/test_python/test_pybind.py

index 430dc81decc38b66a2a57a079e68699fec2abb9b..ac6028b9cd697d2f679370b178ba62967825f7d5 100755 (executable)
@@ -785,12 +785,40 @@ _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_.]+')
 
-def _pybind11_default_argument_length(string):
-    """Returns length of balanced []()-expression at begin of input string until `,` or `)`"""
+def _pybind11_extract_default_argument(string):
+    """Consumes a balanced []()-expression at begin of input string until `,`
+    or `)`, while also replacing all `<Enum.FOO: -12354>` with just
+    `Enum.FOO`."""
     stack = []
-    for i, c in enumerate(string):
+    default = ''
+    i = 0
+    while i < len(string):
+        c = string[i]
+
+        # At the end, what follows is the next argument or end of the argument
+        # list, exit with what we got so far
         if len(stack) == 0 and (c == ',' or c == ')'):
-            return i
+            return string[i:], default
+
+        # Pybind 2.6+ enum in the form of <Enum.FOO_BAR: -2986>, extract
+        # everything until the colon and discard the rest. It can however be a
+        # part of a rogue C++ type name, so pick the < only if directly at the
+        # start or after a space or bracket, and the > only if at the end
+        # again.
+        if c == '<' and (i == 0 or string[i - 1] in [' ', '(', '[']):
+            name_end = string.find(':', i)
+            if name_end == -1:
+                raise SyntaxError("Enum expected to have a value: `{}`".format(string))
+
+            default += string[i + 1:name_end]
+            i = string.find('>', name_end + 1) + 1
+
+            if i == -1 or (i < len(string) and string[i] not in [',', ')', ']']):
+                raise SyntaxError("Unexpected content after enum value: `{}`".format(string[i:]))
+
+            continue
+
+        # Brackets
         if c == '(':
             stack.append(')')
         elif c == '[':
@@ -798,6 +826,12 @@ def _pybind11_default_argument_length(string):
         elif c == ')' or c == ']':
             if len(stack) == 0 or c != stack.pop():
                 raise SyntaxError("Unmatched {} at pos {} in `{}`".format(c, i, string))
+
+        # If there would be find_first_not_of(), I wouldn't have to iterate
+        # byte by byte LIKE A CAVEMAN
+        default += string[i]
+        i += 1
+
     raise SyntaxError("Unexpected end of `{}`".format(string))
 
 def _pybind_map_name_prefix_or_add_typing_suffix(state: State, input_type: str):
index c7d9122e1e56c8e86362522b258b55b200b19b5e..47574ca80f93098d784b4b4f5c2c5d5c58f47931 100644 (file)
@@ -210,6 +210,47 @@ class Signature(unittest.TestCase):
         self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: float = ))'), bad_signature)
         self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: float = ])'), bad_signature)
 
+    # https://github.com/pybind/pybind11/pull/2126, extremely stupid and
+    # annoying but what can I do. Want to support both this and the original
+    # behavior in case they revert the insanity again, so test that both
+    # variants give the same output.
+    def test_default_values_pybind26(self):
+        # Before the insane change
+        self.assertEqual(parse_pybind_signature(self.state, [],
+            'foo(a: bar.Enum = Enum.FOO_BAR)'),
+            ('foo', '', [
+                ('a', 'bar.Enum', 'bar.Enum', 'Enum.FOO_BAR')
+            ], None, None))
+
+        # After the insane change
+        self.assertEqual(parse_pybind_signature(self.state, [],
+            'foo(a: bar.Enum = <Enum.FOO_BAR: -13376>)'),
+            ('foo', '', [
+                ('a', 'bar.Enum', 'bar.Enum', 'Enum.FOO_BAR')
+            ], None, None))
+
+        # Nested
+        self.assertEqual(parse_pybind_signature(self.state, [],
+            'foo(a: bar.Enum = (4, [<Enum.FOO_BAR: -13376>], <Enum.FIZZ_PISS: 1>))'),
+            ('foo', '', [
+                ('a', 'bar.Enum', 'bar.Enum', '(4, [Enum.FOO_BAR], Enum.FIZZ_PISS)')
+            ], None, None))
+
+        # This isn't really expected to happen but yeah it still treats it as
+        # an enum
+        self.assertEqual(parse_pybind_signature(self.state, [],
+            'foo(a: Enum = <Enum_MISSING_DOT:>)'),
+            ('foo', '', [
+                ('a', 'Enum', 'Enum', 'Enum_MISSING_DOT')
+            ], None, None))
+
+        # This will fail
+        bad_signature = ('foo', '', [('…', None, None, None)], None, None)
+        self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: Enum = <Enum.MISSING_COLON>)'), bad_signature)
+        self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: Enum = <Enum.MISSING_GT)'), bad_signature)
+        self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: Enum = <Enum.CHARACTERS_AFTER>a)'), bad_signature)
+        self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: Enum = <Enum.CHARACTERS_AFTER><)'), bad_signature)
+
     def test_bad_return_type(self):
         bad_signature = ('foo', '', [('…', None, None, None)], None, None)
         self.assertEqual(parse_pybind_signature(self.state, [], 'foo() -> List[[]'), bad_signature)