add tags to argument parser

pull/1/head
qwewqa 4 years ago
parent c3baebcca4
commit d199c7e41f
  1. 73
      miyu_bot/commands/common/argument_parsing.py

@ -3,17 +3,18 @@ import re
# https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes # https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes
# https://stackoverflow.com/questions/21105360/regex-find-comma-not-inside-quotes # https://stackoverflow.com/questions/21105360/regex-find-comma-not-inside-quotes
from collections import namedtuple from collections import namedtuple
from typing import Dict, List, Optional, Container, Any, Union, Callable from typing import Dict, List, Optional, Container, Any, Union, Callable, Set, Iterable
# The ` ?` is just so it matches the space after during the replace with blank so there's no double spaces
_param_re = re.compile( _param_re = re.compile(
r'(([a-zA-Z]+)(!=|>=|<=|>|<|==|=)(("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+)(,("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+))*))') r'(([a-zA-Z]+)(!=|>=|<=|>|<|==|=)(("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+)(,("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+))*)) ?')
# The intention of having both = and == is that they might have different behavior # The intention of having both = and == is that they might have different behavior.
# What that means depends on the usage # What that means depends on the usage.
_param_operator_re = re.compile(r'!=|==|=|>|<|>=|<=') _param_operator_re = re.compile(r'!=|==|=|>|<|>=|<=')
_param_argument_re = re.compile(r'("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+)') _param_argument_re = re.compile(r'("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+)')
_param_string_re = re.compile(r'("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\')') _param_string_re = re.compile(r'("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\')')
_param_re_with_post_space = re.compile(
r'([a-zA-Z]+)(!=|==|=|>|<|>=|<=)(("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+)(,("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+))*) ?') _tag_re = re.compile(r'(\$[^\s]+) ?')
NamedArgument = namedtuple('NamedArgument', 'name operator value') NamedArgument = namedtuple('NamedArgument', 'name operator value')
ArgumentValue = namedtuple('ArgumentValue', 'value operator') ArgumentValue = namedtuple('ArgumentValue', 'value operator')
@ -30,13 +31,17 @@ def _parse_named_argument(arg):
def parse_arguments(arg): def parse_arguments(arg):
named_arguments_parsed = [_parse_named_argument(na[0]) for na in _param_re.findall(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) arg = _param_re.sub('', arg)
# Technically, the order (named arguments then tags)
# matters because otherwise a fake tag could appear as a value to a named argument
tags = [t[1:] for t in _tag_re.findall(arg)]
arg = _tag_re.sub('', arg)
named_arguments = {} named_arguments = {}
for na in named_arguments_parsed: for na in named_arguments_parsed:
if na.name not in named_arguments: if na.name not in named_arguments:
named_arguments[na.name] = [] named_arguments[na.name] = []
named_arguments[na.name].append(ArgumentValue(na.value, na.operator)) named_arguments[na.name].append(ArgumentValue(na.value, na.operator))
return ParsedArguments(text_argument.strip(), named_arguments) return ParsedArguments(arg.strip(), set(tags), named_arguments)
class ArgumentError(Exception): class ArgumentError(Exception):
@ -45,12 +50,35 @@ class ArgumentError(Exception):
class ParsedArguments: class ParsedArguments:
text_argument: str text_argument: str
tag_arguments: Set[str]
named_arguments: Dict[str, List[ArgumentValue]] named_arguments: Dict[str, List[ArgumentValue]]
def __init__(self, text, named_arguments): def __init__(self, text, tags, named_arguments):
self.text_argument = text self.text_argument = text
self.tag_arguments = tags
self.named_arguments = named_arguments self.named_arguments = named_arguments
self.used = set() self.used_named_arguments = set()
self.used_tags = set()
def tag(self, name: str):
if name in self.tag_arguments:
self.used_tags.add(name)
return True
return False
def tags(self, names: Optional[Iterable[str]] = None, aliases: Optional[Dict[str, str]] = None):
results = set()
if names is not None:
for name in names:
if name in self.tag_arguments:
results.add(name)
self.used_tags.add(name)
if aliases is not None:
for alias, value in aliases.items():
if alias in self.tag_arguments:
results.add(value)
self.used_tags.add(alias)
return results
def single(self, names: Union[List[str], str], default: Any = None, allowed_operators: Optional[Container] = None, def single(self, names: Union[List[str], str], default: Any = None, allowed_operators: Optional[Container] = None,
is_list=False, numeric=False, converter: Union[dict, Callable] = lambda n: n): is_list=False, numeric=False, converter: Union[dict, Callable] = lambda n: n):
@ -61,7 +89,7 @@ class ParsedArguments:
if not isinstance(names, list): if not isinstance(names, list):
names = [names] names = [names]
for name in names: for name in names:
self.used.add(name) self.used_named_arguments.add(name)
name = f'{names[0]} ({", ".join(names[1:])})' if len(names) > 1 else names[0] name = f'{names[0]} ({", ".join(names[1:])})' if len(names) > 1 else names[0]
value = [arg for args in (self.named_arguments.get(name) for name in names) if args for arg in args] value = [arg for args in (self.named_arguments.get(name) for name in names) if args for arg in args]
if not value: if not value:
@ -90,7 +118,8 @@ class ParsedArguments:
value = ArgumentValue(value.value[0], value.operator) value = ArgumentValue(value.value[0], value.operator)
return value return value
def repeatable(self, names: Union[List[str], str], default: Any = None, allowed_operators: Optional[Container] = None, def repeatable(self, names: Union[List[str], str], default: Any = None,
allowed_operators: Optional[Container] = None,
is_list=False, numeric=False, converter: Union[dict, Callable] = lambda n: n): is_list=False, numeric=False, converter: Union[dict, Callable] = lambda n: n):
if allowed_operators is None: if allowed_operators is None:
allowed_operators = {'>', '<', '>=', '<=', '!=', '==', '='} allowed_operators = {'>', '<', '>=', '<=', '!=', '==', '='}
@ -101,7 +130,7 @@ class ParsedArguments:
if not isinstance(names, list): if not isinstance(names, list):
names = [names] names = [names]
for name in names: for name in names:
self.used.add(name) self.used_named_arguments.add(name)
name = f'{names[0]} ({", ".join(names[1:])})' if len(names) > 1 else names[0] name = f'{names[0]} ({", ".join(names[1:])})' if len(names) > 1 else names[0]
values = [arg for args in (self.named_arguments.get(name) for name in names) if args for arg in args] values = [arg for args in (self.named_arguments.get(name) for name in names) if args for arg in args]
if not values: if not values:
@ -128,15 +157,24 @@ class ParsedArguments:
return values return values
def has_unused(self): def has_unused(self):
return any(name not in self.used for name in self.named_arguments.keys()) return self.has_unused_named_arguments() or self.has_unused_tags()
def has_unused_named_arguments(self):
return any(name not in self.used_named_arguments for name in self.named_arguments.keys())
def has_unused_tags(self):
return any(t not in self.used_tags for t in self.tag_arguments)
def require_all_arguments_used(self): def require_all_arguments_used(self):
def quote(s): def quote(s):
return f'"{s}"' return f'"{s}"'
if self.has_unused(): if self.has_unused_named_arguments():
raise ArgumentError(
f'Unknown arguments with names {", ".join(quote(v) for v in self.named_arguments.keys() if v not in self.used_named_arguments)}.')
if self.has_unused_tags():
raise ArgumentError( raise ArgumentError(
f'Unkown arguments with names {", ".join(quote(v) for v in self.named_arguments.keys() if v not in self.used)}.') f'Unknown tags {", ".join(quote(v) for v in self.tag_arguments if v not in self.used_tags)}.')
_operators = { _operators = {
@ -169,5 +207,6 @@ def list_operator_for(operator: str):
if __name__ == '__main__': if __name__ == '__main__':
a = (parse_arguments(r'sort=default rating>=13.5 a name="a",b," asf,ds ",\'sdf\',dsf')) a = (
parse_arguments(r'sort=default df rating>=13.5,$asd a fds $foo $bar name="a",b," asf,ds ",\'sdf\',dsf $foobar'))
print(a) print(a)

Loading…
Cancel
Save