From 7ea98743dd89142b8c18c8c7de27906a441a5318 Mon Sep 17 00:00:00 2001 From: qwewqa <198e559dbd446d973355f415bdfa34@gmail.com> Date: Wed, 13 Jan 2021 22:33:30 -0500 Subject: [PATCH] add argument parser --- miyu_bot/commands/common/argument_parsing.py | 100 +++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 miyu_bot/commands/common/argument_parsing.py diff --git a/miyu_bot/commands/common/argument_parsing.py b/miyu_bot/commands/common/argument_parsing.py new file mode 100644 index 0000000..55a2b52 --- /dev/null +++ b/miyu_bot/commands/common/argument_parsing.py @@ -0,0 +1,100 @@ +import re + +# https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes +# https://stackoverflow.com/questions/21105360/regex-find-comma-not-inside-quotes +from collections import namedtuple +from typing import Dict, List, Optional, Container, Any, Union, Callable + +_param_re = re.compile( + r'(([a-zA-Z]+)(!=|>=|<=|>|<|==|=)(("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+)(,("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+))*))') +_param_operator_re = re.compile(r'!=|==|=|>|<|>=|<=') +_param_argument_re = re.compile(r'("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+)') +_param_string_re = re.compile(r'("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\')') +_param_re_with_post_space = re.compile( + r'([a-zA-Z]+)(!=|==|=|>|<|>=|<=)(("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+)(,("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+))*) ?') + +NamedArgument = namedtuple('NamedArgument', 'name operator value') +ArgumentValue = namedtuple('ArgumentValue', 'value operator') + + +def _parse_named_argument(arg): + groups = _param_re.fullmatch(arg).groups() + name = groups[1] + operator = groups[2] + values = [value[1:-1] if _param_string_re.fullmatch(value) else value for value in + _param_argument_re.findall(groups[3])] + return NamedArgument(name, operator, values) + + +def parse_arguments(arg): + named_arguments_parsed = [_parse_named_argument(na[0]) for na in _param_re.findall(arg)] + text_argument = _param_re_with_post_space.sub('', arg) + named_arguments = {} + for na in named_arguments_parsed: + if na.name not in named_arguments: + named_arguments[na.name] = [] + named_arguments[na.name].append(ArgumentValue(na.value, na.operator)) + return ParsedArguments(text_argument.strip(), named_arguments) + + +class ArgumentError(Exception): + pass + + +class ParsedArguments: + text_argument: str + named_arguments: Dict[str, List[ArgumentValue]] + + def __init__(self, text, named_arguments): + self.text_argument = text + self.named_arguments = named_arguments + self.used = set() + + def single(self, name: str, default: Any = None, allowed_operators: Optional[Container] = None, + is_list=False, numeric=False, converter: Union[dict, Callable] = lambda n: n): + if allowed_operators is None: + allowed_operators = {'>', '<', '>=', '<=', '!=', '==', '='} + if not isinstance(default, tuple): + default = ArgumentValue(default, '=') + self.used.add(name) + value = self.named_arguments.get(name) + if value is None: + return default + if len(value) != 1: + raise ArgumentError(f'Expected only one value for parameter "{name}".') + value = value[0] + if value.operator not in allowed_operators: + raise ArgumentError( + f'Allowed operators for parameter "{name}" are {", ".join(str(o) for o in allowed_operators)}.') + if numeric: + try: + value = ArgumentValue([float(v) for v in value.value], value.operator) + except ValueError: + raise ArgumentError(f'Expected numerical arguments for parameter "{name}".') + try: + if isinstance(converter, dict): + value = ArgumentValue([converter[v] for v in value.value], value.operator) + else: + value = ArgumentValue([converter(v) for v in value.value], value.operator) + except Exception: + raise ArgumentError(f'Invalid value for parameter "{name}".') + if not is_list: + if len(value.value) != 1: + raise ArgumentError(f'List not allowed for parameter "{name}".') + value = ArgumentValue(value.value[0], value.operator) + return value + + def has_unused(self): + return any(name not in self.used for name in self.named_arguments.keys()) + + def require_all_arguments_used(self): + def quote(s): + return f'"{s}"' + if self.has_unused(): + raise ArgumentError( + f'Unkown arguments with names {", ".join(quote(v) for v in self.named_arguments.keys() if v not in self.used)}.') + + +if __name__ == '__main__': + a = (parse_arguments(r'sort=default rating>=13.5 a name="a",b," asf,ds ",\'sdf\',dsf')) + print(a)