From 70c627c04618eb1ba7ebf43cad267c3921dcda98 Mon Sep 17 00:00:00 2001 From: Andreas Maier Date: Sun, 12 Nov 2023 21:38:33 +0100 Subject: [PATCH] Support for parsing complex name-value options Signed-off-by: Andreas Maier --- minimum-constraints.txt | 2 +- requirements.txt | 2 + tests/function/test_name_value_parser.py | 757 +++++++++++++++++++++++ zhmccli/__init__.py | 1 + zhmccli/_name_value_parser.py | 260 ++++++++ 5 files changed, 1021 insertions(+), 1 deletion(-) create mode 100644 tests/function/test_name_value_parser.py create mode 100644 zhmccli/_name_value_parser.py diff --git a/minimum-constraints.txt b/minimum-constraints.txt index d077c395..d8961a2b 100644 --- a/minimum-constraints.txt +++ b/minimum-constraints.txt @@ -56,6 +56,7 @@ prompt-toolkit==2.0.1 # PyYAML is pulled in by zhmccli, zhmcclient, yamlloader PyYAML==5.3.1 +pyparsing==3.1.0 jsonschema==3.0.1 yamlloader==0.5.5 @@ -208,7 +209,6 @@ pathlib2==2.3.3; python_version < '3.4' and sys_platform != 'win32' # twine 3.0.0 depends on pkginfo>=1.4.2 pkginfo==1.4.2 py==1.11.0 -pyparsing==3.0.7 # tox 4.0.0 started using pyproject-api and requires pyproject-api>=1.2.1 pyproject-api==1.2.1; python_version >= '3.7' # build is using pyproject-hooks diff --git a/requirements.txt b/requirements.txt index c1faceb8..e154d58c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,6 +39,8 @@ prompt-toolkit>=2.0.1 PyYAML>=5.3.1,!=5.4.0,!=5.4.1; python_version <= '3.11' PyYAML>=5.3.1,!=5.4.0,!=5.4.1,!=6.0.0; python_version >= '3.12' +pyparsing>=3.1.0 + jsonschema>=3.0.1 yamlloader>=0.5.5 diff --git a/tests/function/test_name_value_parser.py b/tests/function/test_name_value_parser.py new file mode 100644 index 00000000..496fd176 --- /dev/null +++ b/tests/function/test_name_value_parser.py @@ -0,0 +1,757 @@ +""" +Function test for '_name_value_parser' module. +""" + +from __future__ import absolute_import, print_function + +import re +import pytest + +from zhmccli import NameValueParseError, NameValueParser + + +TESTCASES_NAME_VALUE_PARSER_PARSE = [ + # Testcases for test_name_value_parser_parse() + # + # Each list item is a testcase with the following tuple items: + # * desc (str) - Testcase description. + # * init_kwargs (dict) - Keyword arguments for NameValueParser(). + # * input_value (str) - Input value for NameValueParser.parse(). + # * exp_result (object) - Expected return value from NameValueParser.parse() + # if expected to succeed, or None if expected to fail. + # * exp_exc_type (exception) - Expected exception type if expected to fail, + # or None if expected to succeed. + # * exp_exc_message (str) - Regexp pattern to match expected exception + # message if expected to fail, or None if expected to succeed. + + ( + r"""Simple null value 'null'.""", + dict(), + r'null', + None, + None, None + ), + + ( + r"""Simple bool 'true'.""", + dict(), + r'true', + True, + None, None + ), + ( + r"""Simple bool 'false'.""", + dict(), + r'false', + False, + None, None + ), + + ( + r"""Simple int '42'.""", + dict(), + r'42', + 42, + None, None + ), + + ( + r"""Simple float '42.0'.""", + dict(), + r'42.0', + 42.0, + None, None + ), + ( + r"""Simple float '.1'.""", + dict(), + r'.1', + 0.1, + None, None + ), + ( + r"""Simple float '1.'.""", + dict(), + r'1.', + 1.0, + None, None + ), + + ( + r"""Simple quoted string '"abc"'.""", + dict(), + r'"abc"', + 'abc', + None, None + ), + ( + r"""Simple quoted string '"42"'.""", + dict(), + r'"42"', + '42', + None, None + ), + ( + r"""Simple quoted string '"true"'.""", + dict(), + r'"true"', + 'true', + None, None + ), + ( + r"""Simple quoted string '"false"'.""", + dict(), + r'"false"', + 'false', + None, None + ), + ( + r"""Simple quoted string '"null"'.""", + dict(), + r'"null"', + 'null', + None, None + ), + ( + r"""Simple unquoted string 'abc'.""", + dict(), + r'abc', + 'abc', + None, None + ), + ( + r"""Simple unquoted string 'a'.""", + dict(), + r'a', + 'a', + None, None + ), + ( + r"""Simple quoted string with blank '"a b"'.""", + dict(), + r'"a b"', + 'a b', + None, None + ), + ( + r"""Simple quoted string with escaped newline '"a\nb"'.""", + dict(), + r'"a\nb"', + 'a\nb', + None, None + ), + ( + r"""Simple quoted string with escaped carriage return '"a\rb"'.""", + dict(), + r'"a\rb"', + 'a\rb', + None, None + ), + ( + r"""Simple quoted string with escaped tab '"a\tb"'.""", + dict(), + r'"a\tb"', + 'a\tb', + None, None + ), + ( + r"""Simple quoted string with escaped backslash '"a\\b"'.""", + dict(), + r'"a\\b"', + 'a\\b', + None, None + ), + ( + r"""Invalid simple unquoted string with blank 'a b'.""", + dict(), + r'a b', + None, + NameValueParseError, r".*: Expected end of text, found 'b'.*" + ), + ( + r"""Invalid simple unquoted string with leading blank ' b'.""", + dict(), + r' b', + None, + NameValueParseError, r".*: , found 'b'.*" + ), + ( + r"""Simple unquoted string with trailing blank 'a '.""", + # TODO: Find out why this is not invalid. + dict(), + r'a ', + 'a', + None, None + ), + ( + r"""Invalid simple unquoted string with escaped newline 'a\nb'.""", + dict(), + r'a\nb', + None, + NameValueParseError, r".*: Expected end of text, found '\\'.*" + ), + + ( + r"""Empty array.""", + dict(), + r'[]', + [], + None, None + ), + ( + r"""Array with one null value 'null'.""", + dict(), + r'[null]', + [None], + None, None + ), + ( + r"""Array with one boolean 'true'.""", + dict(), + r'[true]', + [True], + None, None + ), + ( + r"""Array with one boolean 'false'.""", + dict(), + r'[false]', + [False], + None, None + ), + ( + r"""Array with one int '42'.""", + dict(), + r'[42]', + [42], + None, None + ), + ( + r"""Array with one float '42.0'.""", + dict(), + r'[42.0]', + [42.0], + None, None + ), + ( + r"""Array with one quoted string '"null"'.""", + dict(), + r'["null"]', + ['null'], + None, None + ), + ( + r"""Array with one quoted string '"true"'.""", + dict(), + r'["true"]', + ['true'], + None, None + ), + ( + r"""Array with one quoted string '"false"'.""", + dict(), + r'["false"]', + ['false'], + None, None + ), + ( + r"""Array with one quoted string '"a"'.""", + dict(), + r'["a"]', + ['a'], + None, None + ), + ( + r"""Array with one unquoted string 'a'.""", + dict(), + r'[a]', + ['a'], + None, None + ), + ( + r"""Array with one quoted string '"a]"'.""", + dict(), + r'["a]"]', + ['a]'], + None, None + ), + ( + r"""Array with one quoted string '"[a"'.""", + dict(), + r'["[a"]', + ['[a'], + None, None + ), + ( + r"""Array with two int items.""", + dict(), + r'[42,43]', + [42, 43], + None, None + ), + ( + r"""Invalid array with unquoted string with blank 'a b'.""", + dict(), + r'[a b]', + None, + NameValueParseError, r".*: , found 'b'.*" + ), + ( + r"""Array with unquoted string with leading blank ' b'.""", + # TODO: Find out why this is not invalid. + dict(), + r'[ b]', + ['b'], + None, None + ), + ( + r"""Array with unquoted string with trailing blank 'a '.""", + # TODO: Find out why this is not invalid. + dict(), + r'[a ]', + ['a'], + None, None + ), + ( + r"""Invalid array with trailing comma after one item.""", + dict(), + r'[42,]', + None, + NameValueParseError, r".*: , found ','.*" + ), + ( + r"""Invalid array with leading comma before one item.""", + dict(), + r'[,42]', + None, + NameValueParseError, r".*: , found ','.*" + ), + ( + r"""Invalid array with two commas between two items.""", + dict(), + r'[42,,43]', + None, + NameValueParseError, r".*: , found ','.*" + ), + ( + r"""Invalid array with missing closing ']'.""", + dict(), + r'[42', + None, + NameValueParseError, r".*: , found end of text.*" + ), + ( + r"""Invalid array with missing opening '['.""", + dict(), + r'42]', + None, + NameValueParseError, r".*: Expected end of text, found '\]'.*" + ), + ( + r"""Invalid array with additional '[' after begin.""", + dict(), + r'[[42]', + None, + NameValueParseError, r".*: , found end of text.*" + ), + ( + r"""Invalid array with additional ']'after begin.""", + dict(), + r'[]42]', + None, + NameValueParseError, r".*: Expected end of text, found '42'.*" + ), + ( + r"""Invalid array with additional '[' before end.""", + dict(), + r'[42[]', + None, + NameValueParseError, r".*: , found '\['.*" + ), + ( + r"""Invalid array with additional ']' before end.""", + dict(), + r'[42]]', + None, + NameValueParseError, r".*: Expected end of text, found '\]'.*" + ), + + ( + r"""Array with one nested empty array.""", + dict(), + r'[[]]', + [[]], + None, None + ), + ( + r"""Array with one nested empty object.""", + dict(), + r'[{}]', + [dict()], + None, None + ), + ( + r"""Array with one nested array that has one item.""", + dict(), + r'[[42]]', + [[42]], + None, None + ), + ( + r"""Array with one nested object that has one item.""", + dict(), + r'[{x=42}]', + [dict(x=42)], + None, None + ), + ( + r"""Array with two nested array items.""", + dict(), + r'[[42,43],[]]', + [[42, 43], []], + None, None + ), + ( + r"""Array with deeply nested array items.""", + dict(), + r'[a,[b,[c,[d,e]]]]', + ['a', ['b', ['c', ['d', 'e']]]], + None, None + ), + + ( + r"""Empty object.""", + dict(), + r'{}', + dict(), + None, None + ), + ( + r"""Object with one item of value 'null'.""", + dict(), + r'{x=null}', + dict(x=None), + None, None + ), + ( + r"""Object with one item of boolean value 'true'.""", + dict(), + r'{x=true}', + dict(x=True), + None, None + ), + ( + r"""Object with one item of boolean value 'false'.""", + dict(), + r'{x=false}', + dict(x=False), + None, None + ), + ( + r"""Object with one item of int value '42'.""", + dict(), + r'{x=42}', + dict(x=42), + None, None + ), + ( + r"""Object with one item of float value '42.0'.""", + dict(), + r'{x=42.0}', + dict(x=42.0), + None, None + ), + ( + r"""Object with one item of quoted string value '"null"'.""", + dict(), + r'{x="null"}', + dict(x='null'), + None, None + ), + ( + r"""Object with one item of quoted string value '"true"'.""", + dict(), + r'{x="true"}', + dict(x='true'), + None, None + ), + ( + r"""Object with one item of quoted string value '"false"'.""", + dict(), + r'{x="false"}', + dict(x='false'), + None, None + ), + ( + r"""Object with one item of quoted string value '"a"'.""", + dict(), + r'{x="a"}', + dict(x='a'), + None, None + ), + ( + r"""Object with one item of unquoted string value 'a'.""", + dict(), + r'{x=a}', + dict(x='a'), + None, None + ), + ( + r"""Object with one item of quoted string value '"a}"'.""", + dict(), + r'{x="a}"}', + dict(x='a}'), + None, None + ), + ( + r"""Object with one item of quoted string value '"{a"'.""", + dict(), + r'{x="{a"}', + dict(x='{a'), + None, None + ), + ( + r"""Object with two items of int values.""", + dict(), + r'{x=42,y=43}', + dict(x=42, y=43), + None, None + ), + ( + r"""Object with one item of unquoted string value with blank 'a b'.""", + dict(), + r'{x=a b}', + None, + NameValueParseError, r".*: , found 'b'.*" + ), + ( + r"""Object with one item of unquoted string value with leading """ + r"""blank ' b'.""", + dict(), + r'{x= b}', + None, + NameValueParseError, r".*: , found 'x'.*" + ), + ( + r"""Object with one item of unquoted string value with trailing """ + r"""blank 'a '.""", + # TODO: Find out why this is not invalid. + dict(), + r'{x=a }', + dict(x='a'), + None, None + ), + + ( + r"""Object with one item of name with hyphen.""", + dict(), + r'{abc-def=42}', + {'abc-def': 42}, + None, None + ), + ( + r"""Object with one item of name with underscore.""", + dict(), + r'{abc_def=42}', + {'abc_def': 42}, + None, None + ), + ( + r"""Object with one item of name with numeric.""", + dict(), + r'{a0=42}', + {'a0': 42}, + None, None + ), + ( + r"""Object with one item of name with leading hyphen.""", + dict(), + r'{-a=42}', + None, + NameValueParseError, r".*: , found '-'.*" + ), + ( + r"""Object with one item of name with leading underscore.""", + dict(), + r'{_a=42}', + None, + NameValueParseError, r".*: , found '_'.*" + ), + ( + r"""Object with one item of name with leading numeric.""", + dict(), + r'{0x=42}', + None, + NameValueParseError, r".*: , found '0x'.*" + ), + ( + r"""Invalid object with one item of name/value with '='""", + dict(), + r'{a=b=42}', + None, + NameValueParseError, r".*: , found '='.*" + ), + ( + r"""Invalid object with one item with trailing comma.""", + dict(), + r'{x=42,}', + None, + NameValueParseError, r".*: , found ','.*" + ), + ( + r"""Invalid object with one item with leading comma.""", + dict(), + r'{x=,42}', + None, + NameValueParseError, r".*: , found 'x'.*" + ), + ( + r"""Invalid object with two commas between two items.""", + dict(), + r'{x=42,,y=43}', + None, + NameValueParseError, r".*: , found ','.*" + ), + ( + r"""Invalid object with missing closing '}'.""", + dict(), + r'{x=42', + None, + NameValueParseError, r".*: , found end of text.*" + ), + ( + r"""Invalid object with missing opening '{'.""", + dict(), + r'x=42}', + None, + NameValueParseError, r".*: Expected end of text, found '='.*" + ), + ( + r"""Invalid object with additional '{' after begin.""", + dict(), + r'{{x=42}', + None, + NameValueParseError, r".*: , found '\{'.*" + ), + ( + r"""Invalid object with additional '}' after begin.""", + dict(), + r'{}x=42}', + None, + NameValueParseError, r".*: Expected end of text, found 'x'.*" + ), + ( + r"""Invalid object with additional '{' before end.""", + dict(), + r'{x=42{}', + None, + NameValueParseError, r".*: , found '\{'.*" + ), + ( + r"""Invalid object with additional '}' before end.""", + dict(), + r'{x=42}}', + None, + NameValueParseError, r".*: Expected end of text, found '\}'.*" + ), + + ( + r"""Object with nested empty object.""", + dict(), + r'{x={}}', + dict(x=dict()), + None, None + ), + ( + r"""Object with nested empty array.""", + dict(), + r'{x=[]}', + dict(x=[]), + None, None + ), + ( + r"""Object with nested object that has one item.""", + dict(), + r'{x={y=42}}', + dict(x=dict(y=42)), + None, None + ), + ( + r"""Object with nested array that has one item.""", + dict(), + r'{x=[42]}', + dict(x=[42]), + None, None + ), + ( + r"""Object with deeply nested object items.""", + dict(), + r'{a={b={c={d=x,e=y}}}}', + dict(a=dict(b=dict(c=dict(d='x', e='y')))), + None, None + ), + + ( + r"""Non-default quoting char "'".""", + dict(quoting_char="'"), + r"'a'", + 'a', + None, None + ), + ( + r"""Non-default separator char ';'.""", + dict(separator_char=';'), + r'[42;43]', + [42, 43], + None, None + ), + ( + r"""Invalid non-default separator char ' '.""", + dict(separator_char=' '), + r'[42 43]', + None, + NameValueParseError, r".*: , found '43'.*" + ), + ( + r"""Non-default assignment char ':'.""", + dict(assignment_char=':'), + r'{x:42}', + dict(x=42), + None, None + ), + # TODO: Add more testcases with non-default special characters +] + + +@pytest.mark.parametrize( + "desc, init_kwargs, input_value, exp_result, exp_exc_type, " + "exp_exc_message", + TESTCASES_NAME_VALUE_PARSER_PARSE) +def test_name_value_parser_parse( + desc, init_kwargs, input_value, exp_result, exp_exc_type, + exp_exc_message): + # pylint: disable=unused-argument + """ + Test NameValueParser.parse() method. + """ + + parser = NameValueParser(**init_kwargs) + + if exp_exc_type is None: + + # The code to be tested + result = parser.parse(input_value) + + assert result == exp_result + + else: + + with pytest.raises(exp_exc_type) as exc_info: + + # The code to be tested + parser.parse(input_value) + + exc_value = exc_info.value + exc_message = str(exc_value) + match_str = exp_exc_message + '$' + assert re.match(match_str, exc_message) diff --git a/zhmccli/__init__.py b/zhmccli/__init__.py index f4186b7a..5eb18b1b 100644 --- a/zhmccli/__init__.py +++ b/zhmccli/__init__.py @@ -19,6 +19,7 @@ from __future__ import absolute_import from ._version import * # noqa: F401 +from ._name_value_parser import * # noqa: F401 from ._cmd_info import * # noqa: F401 from ._cmd_session import * # noqa: F401 from ._cmd_console import * # noqa: F401 diff --git a/zhmccli/_name_value_parser.py b/zhmccli/_name_value_parser.py new file mode 100644 index 00000000..17e71516 --- /dev/null +++ b/zhmccli/_name_value_parser.py @@ -0,0 +1,260 @@ +""" +Parser for complex name-value strings. +""" + +from __future__ import absolute_import + +from pyparsing import Forward, Opt, Group, nums, alphas, Literal, Char, Word, \ + CharsNotIn, QuotedString, DelimitedList, Suppress, ParseResults, \ + ParseException + +__all__ = ['NameValueParseError', 'NameValueParser'] + + +class NameValueParseError(Exception): + # pylint: disable=too-few-public-methods + """ + Indicates an error when parsing a complex name-value string. + """ + pass + + +class NameValueParser(object): + # pylint: disable=too-few-public-methods + r""" + Parser for complex name-value strings. + + The syntax for a complex name-value string is as follows: + + * ``complex-value := simple-value | array-value | object-value`` + + * ``simple-value`` is a value of a simple type, as follows: + + - none/null: ``null``. + Will be returned as Python ``None``. + + - bool: ``true`` or ``false``. + Will be returned as Python ``True`` or ``False``. + + - int: decimal string representation of an integer number that is + parseable by ``int()``. + Will be returned as a Python ``int`` value. + + - float: string representation of a floating point number that is + parseable by ``float()``. + Will be returned as a Python ``float`` value. + + - string: Unicode string value, with character range and representation + as for JSON string values, surrounded by double quotes. The quoting + character and the escape character can be configured. The quoting + characters can be omitted if the string value is not escaped, does not + contain any of ``[]{}``, blank, the escape character, the quoting + character, the assignment character, and if it cannot be interpreted + as a value of one of the other simple types. + Will be returned as an unescaped Python Unicode string value. + + The following escape sequences are recognized in quoted strings + (shown using the default escape character): + + - ``\b``: U+0008 (backspace) + - ``\t``: U+0009 (tab) + - ``\n``: U+000A (newline) + - ``\r``: U+000D (carriage return) + - ``\uNNNN``: 4..6 digit Unicode code point + + * ``array-value`` is a value of array/list type, specified as a + comma-separated list of ``complex-value`` items, surrounded by + ``[`` and ``]``. + The item separator character can be configured. + Will be returned as a Python ``list`` value. + + * ``object-value`` is a value of object/dict type, specified as a + comma-separated list of ``name=complex-value`` items, surrounded by + ``{`` and ``}``. + The item separator and assignment characters can be configured. + ``name`` must begin with an alphabetic character, + and may be followed by zero or more alphanumeric characters, + ``-`` (hyphen) and ``_`` (underscore). + Will be returned as a Python ``dict`` value. + + Examples (using the default quoting, escape, separator and assignment + characters):: + + parser = NameValueParser() + + print(parser.parse('null')) # -> None + print(parser.parse('false')) # -> False + print(parser.parse('true')) # -> True + print(parser.parse('"true"')) # -> 'true' + print(parser.parse('42')) # -> 42 + print(parser.parse('42.')) # -> 42.0 + print(parser.parse('.1')) # -> 0.1 + print(parser.parse('abc')) # -> 'abc' + print(parser.parse('"ab c"')) # -> 'ab c' + print(parser.parse('"ab\nc"')) # -> 'ab\nc' + print(parser.parse('[abc,42]')) # -> ['abc', 42] + + print(parser.parse('{interface-name=abc,port=42}')) + # -> {'interface-name': 'abc', 'port': 42} + + print(parser.parse('{title="ab\tc",host-addresses=[10.11.12.13,null]}')) + # -> {'title': 'ab\tc', 'host-addresses': ['10.11.12.13', None]} + """ + + def __init__(self, quoting_char='"', separator_char=',', + assignment_char='='): + """ + Parameters: + + quoting_char(str): Quoting character, used around strings to make + them quoted strings. + + separator_char(str): Separator character, used to separate items + in arrays and objects. + + assignment_char(str): Assignment character, used between name and + value within object items. + """ + assert len(quoting_char) == 1 + assert len(separator_char) == 1 + assert len(assignment_char) == 1 + + self._quoting_char = quoting_char + self._separator_char = separator_char + self._assignment_char = assignment_char + + # Note: Setting escape_char to something else does not work + escape_char = '\\' + + special_chars = quoting_char + separator_char + assignment_char + \ + escape_char + + # pyparsing grammar for complex name-value strings + null_value = Literal('null') + bool_value = Literal('true') | Literal('false') + int_value = Word(nums) + float_value = (Word(nums) + Char('.') + Opt(Word(nums))) | \ + (Char('.') + Word(nums)) + quoted_string_value = \ + QuotedString(quote_char=quoting_char, esc_char=escape_char) + unquoted_string_value = CharsNotIn(' []{}' + special_chars) + simple_value = null_value | bool_value | float_value | int_value | \ + quoted_string_value | unquoted_string_value + complex_value = Forward() + name = Word(alphas, alphas + nums + '-_') + named_value = Group(name + Suppress(assignment_char) + complex_value) + array_value = Suppress('[') + \ + Opt(DelimitedList(complex_value, delim=separator_char)) + \ + Suppress(']') + object_value = Suppress('{') + \ + Opt(DelimitedList(named_value, delim=separator_char)) + \ + Suppress('}') + # pylint: disable=pointless-statement + complex_value << (array_value | object_value | simple_value) + + array_value.add_parse_action(_as_python_list) + object_value.add_parse_action(_as_python_dict) + int_value.add_parse_action(_as_python_int) + float_value.add_parse_action(_as_python_float) + bool_value.add_parse_action(_as_python_bool) + null_value.add_parse_action(_as_python_null) + + self._grammar = complex_value + + @property + def quoting_char(self): + """ + The quoting character used by this parser, used around strings to make + them quoted strings. + """ + return self._quoting_char + + @property + def separator_char(self): + """ + The separator character used by this parser, used to separate items in + arrays and objects. + """ + return self._separator_char + + @property + def assignment_char(self): + """ + The assignment character used by this parser, used between name and + value within object items. + """ + return self._assignment_char + + def parse(self, value): + """ + Parse a complex name-value string and return the Python data object + representing the value. + + Parameters: + + value(str): The complex name-value string to be parsed. + + Returns: + object: Python object representing the complex name-value string. + + Raises: + NameValueParseError: Parsing error. + """ + try: + parse_result = self._grammar.parse_string(value, parse_all=True) + except ParseException as exc: + raise NameValueParseError( + "Cannot parse {!r}: {}".format(value, exc)) + return parse_result[0] + + +# pyparsing parse actions + +# For debugging parse actions, there are two possibilities: +# * Add @pyparsing.trace_parse_action decorator to parse action +# * Invoke set_debug(True) on grammar element + +def _as_python_list(toks): + """Return parsed array as Python list""" + return ParseResults.List(toks.as_list()) + + +def _as_python_dict(toks): + """Return parsed object as Python dict""" + result = {} + for name, value in toks: + result[name] = value + return result + + +def _as_python_int(toks): + """Return parsed int string as Python int""" + assert len(toks) == 1 + return int(toks[0]) + + +def _as_python_float(toks): + """Return parsed float string as Python float""" + float_str = ''.join(toks) + return float(float_str) + + +def _as_python_bool(toks): + """Return parsed bool string as Python bool""" + assert len(toks) == 1 + mapping = { + 'true': True, + 'false': False, + } + return mapping[toks[0]] + + +def _as_python_null(toks): + """Return parsed null string as Python None""" + assert len(toks) == 1 + mapping = { + 'null': None, + } + # `None` can only be returned by using the ability of parse actions to + # modify their ParseResults argument in place. + toks[0] = mapping[toks[0]]