From: Vladimír Vondruš Date: Sun, 2 Jan 2022 23:46:59 +0000 (+0100) Subject: documentation/python: support insane pybind 2.6 enum printing. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~cjwatson/git?a=commitdiff_plain;h=c6707e1c85a46e8848ebca077ec60eeb27eb5aab;p=blog.git documentation/python: support insane pybind 2.6 enum printing. What the hell, why. --- diff --git a/documentation/python.py b/documentation/python.py index 430dc81d..ac6028b9 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -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 `` 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 , 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): diff --git a/documentation/test_python/test_pybind.py b/documentation/test_python/test_pybind.py index c7d9122e..47574ca8 100644 --- a/documentation/test_python/test_pybind.py +++ b/documentation/test_python/test_pybind.py @@ -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 = )'), + ('foo', '', [ + ('a', 'bar.Enum', 'bar.Enum', 'Enum.FOO_BAR') + ], None, None)) + + # Nested + self.assertEqual(parse_pybind_signature(self.state, [], + 'foo(a: bar.Enum = (4, [], ))'), + ('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 = )'), + ('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 = )'), bad_signature) + self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: Enum = a)'), bad_signature) + self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: Enum = <)'), 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)