|
|
|
@ -166,14 +166,23 @@ def _split_keyword_arg(param):
|
|
|
|
|
return None, param
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_recurse(macro, context, macro_instance=None, depth=0):
|
|
|
|
|
def _is_number(value):
|
|
|
|
|
"""Check if the value can be turned into a number."""
|
|
|
|
|
try:
|
|
|
|
|
float(value)
|
|
|
|
|
return True
|
|
|
|
|
except ValueError:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_recurse(code, context, macro_instance=None, depth=0):
|
|
|
|
|
"""Handle a subset of the macro, e.g. one parameter or function call.
|
|
|
|
|
|
|
|
|
|
Not using eval for security reasons.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
macro : string
|
|
|
|
|
code : string
|
|
|
|
|
Just like parse.
|
|
|
|
|
A single parameter of a function or the complete macro as string.
|
|
|
|
|
context : Context
|
|
|
|
@ -182,45 +191,53 @@ def _parse_recurse(macro, context, macro_instance=None, depth=0):
|
|
|
|
|
depth : int
|
|
|
|
|
For logging porposes
|
|
|
|
|
"""
|
|
|
|
|
assert isinstance(macro, str)
|
|
|
|
|
assert isinstance(code, str)
|
|
|
|
|
assert isinstance(depth, int)
|
|
|
|
|
|
|
|
|
|
if macro == "":
|
|
|
|
|
space = " " * depth
|
|
|
|
|
|
|
|
|
|
code = code.strip()
|
|
|
|
|
|
|
|
|
|
if code == "":
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
if macro.startswith('"'):
|
|
|
|
|
# a string, don't parse
|
|
|
|
|
return macro[1:-1]
|
|
|
|
|
if code.startswith('"'):
|
|
|
|
|
# a string, don't parse. remove quotes
|
|
|
|
|
string = code[1:-1]
|
|
|
|
|
logger.spam("%sstring %s", space, string)
|
|
|
|
|
return string
|
|
|
|
|
|
|
|
|
|
if macro.startswith("$"):
|
|
|
|
|
if code.startswith("$"):
|
|
|
|
|
# will be resolved during the macros runtime
|
|
|
|
|
return Variable(macro.split("$", 1)[1])
|
|
|
|
|
|
|
|
|
|
if macro_instance is None:
|
|
|
|
|
macro_instance = Macro(macro, context)
|
|
|
|
|
else:
|
|
|
|
|
assert isinstance(macro_instance, Macro)
|
|
|
|
|
return Variable(code.split("$", 1)[1])
|
|
|
|
|
|
|
|
|
|
macro = macro.strip()
|
|
|
|
|
space = " " * depth
|
|
|
|
|
if _is_number(code):
|
|
|
|
|
if "." in code:
|
|
|
|
|
code = float(code)
|
|
|
|
|
else:
|
|
|
|
|
code = int(code)
|
|
|
|
|
logger.spam("%snumber %s", space, code)
|
|
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
# is it another macro?
|
|
|
|
|
call_match = re.match(r"^(\w+)\(", macro)
|
|
|
|
|
call_match = re.match(r"^(\w+)\(", code)
|
|
|
|
|
call = call_match[1] if call_match else None
|
|
|
|
|
if call is not None:
|
|
|
|
|
# available functions in the macro and the minimum and maximum number
|
|
|
|
|
# of their arguments
|
|
|
|
|
if macro_instance is None:
|
|
|
|
|
macro_instance = Macro(code, context)
|
|
|
|
|
else:
|
|
|
|
|
assert isinstance(macro_instance, Macro)
|
|
|
|
|
|
|
|
|
|
function = FUNCTIONS.get(call)
|
|
|
|
|
if function is None:
|
|
|
|
|
raise Exception(f"Unknown function {call}")
|
|
|
|
|
|
|
|
|
|
# get all the stuff inbetween
|
|
|
|
|
position = _count_brackets(macro)
|
|
|
|
|
|
|
|
|
|
inner = macro[macro.index("(") + 1 : position - 1]
|
|
|
|
|
position = _count_brackets(code)
|
|
|
|
|
inner = code[code.index("(") + 1 : position - 1]
|
|
|
|
|
logger.spam("%scalls %s with %s", space, call, inner)
|
|
|
|
|
|
|
|
|
|
# split "3, foo=k(a).w(10)" into arguments
|
|
|
|
|
# split "3, foo=a(2, k(a).w(10))" into arguments
|
|
|
|
|
raw_string_args = _extract_args(inner)
|
|
|
|
|
|
|
|
|
|
# parse and sort the params
|
|
|
|
@ -256,7 +273,7 @@ def _parse_recurse(macro, context, macro_instance=None, depth=0):
|
|
|
|
|
f"not {num_provided_args} parameters"
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
msg = f"{call} takes {min_args}, " f"not {num_provided_args} parameters"
|
|
|
|
|
msg = f"{call} takes {min_args}, not {num_provided_args} parameters"
|
|
|
|
|
|
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
|
|
|
@ -265,32 +282,18 @@ def _parse_recurse(macro, context, macro_instance=None, depth=0):
|
|
|
|
|
function(macro_instance, *positional_args, **keyword_args)
|
|
|
|
|
|
|
|
|
|
# is after this another call? Chain it to the macro_instance
|
|
|
|
|
if len(macro) > position and macro[position] == ".":
|
|
|
|
|
chain = macro[position + 1 :]
|
|
|
|
|
if len(code) > position and code[position] == ".":
|
|
|
|
|
chain = code[position + 1 :]
|
|
|
|
|
logger.spam("%sfollowed by %s", space, chain)
|
|
|
|
|
_parse_recurse(chain, context, macro_instance, depth)
|
|
|
|
|
|
|
|
|
|
return macro_instance
|
|
|
|
|
|
|
|
|
|
# at this point it is still a string since no previous rule matched
|
|
|
|
|
assert isinstance(macro, str)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# If it can be parsed as number, then it's probably a parameter to a function.
|
|
|
|
|
# It isn't really needed to parse it here, but is nice for the logs.
|
|
|
|
|
macro = int(macro)
|
|
|
|
|
except ValueError:
|
|
|
|
|
try:
|
|
|
|
|
macro = float(macro)
|
|
|
|
|
except ValueError:
|
|
|
|
|
# use as string instead
|
|
|
|
|
# If a string, it is probably either a key name like KEY_A or a variable
|
|
|
|
|
# name as n `set(var, 1)`, both won't contain special characters that can
|
|
|
|
|
# break macro syntax so they don't have to be wrapped in quotes.
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
logger.spam("%s%s %s", space, type(macro), macro)
|
|
|
|
|
return macro
|
|
|
|
|
# It is probably either a key name like KEY_A or a variable name as in `set(var,1)`,
|
|
|
|
|
# both won't contain special characters that can break macro syntax so they don't
|
|
|
|
|
# have to be wrapped in quotes.
|
|
|
|
|
logger.spam("%sstring %s", space, code)
|
|
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def handle_plus_syntax(macro):
|
|
|
|
@ -325,6 +328,7 @@ def _remove_whitespaces(macro, delimiter='"'):
|
|
|
|
|
"""Remove whitespaces, tabs, newlines and such outside of string quotes."""
|
|
|
|
|
result = ""
|
|
|
|
|
for i, chunk in enumerate(macro.split(delimiter)):
|
|
|
|
|
# every second chunk is inside string quotes
|
|
|
|
|
if i % 2 == 0:
|
|
|
|
|
result += re.sub(r"\s", "", chunk)
|
|
|
|
|
else:
|
|
|
|
@ -335,6 +339,31 @@ def _remove_whitespaces(macro, delimiter='"'):
|
|
|
|
|
return result[: -len(delimiter)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _remove_comments(macro):
|
|
|
|
|
"""Remove comments from the macro and return the resulting code."""
|
|
|
|
|
# keep hashtags inside quotes intact
|
|
|
|
|
result = ""
|
|
|
|
|
|
|
|
|
|
for i, line in enumerate(macro.split("\n")):
|
|
|
|
|
for j, chunk in enumerate(line.split('"')):
|
|
|
|
|
if j > 0:
|
|
|
|
|
# add back the string quote
|
|
|
|
|
chunk = f'"{chunk}'
|
|
|
|
|
|
|
|
|
|
# every second chunk is inside string quotes
|
|
|
|
|
if j % 2 == 0 and "#" in chunk:
|
|
|
|
|
# everything from now on is a comment and can be ignored
|
|
|
|
|
result += chunk.split("#")[0]
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
result += chunk
|
|
|
|
|
|
|
|
|
|
if i < macro.count("\n"):
|
|
|
|
|
result += "\n"
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse(macro, context, return_errors=False):
|
|
|
|
|
"""parse and generate a Macro that can be run as often as you want.
|
|
|
|
|
|
|
|
|
@ -354,6 +383,8 @@ def parse(macro, context, return_errors=False):
|
|
|
|
|
"""
|
|
|
|
|
macro = handle_plus_syntax(macro)
|
|
|
|
|
|
|
|
|
|
macro = _remove_comments(macro)
|
|
|
|
|
|
|
|
|
|
macro = _remove_whitespaces(macro, '"')
|
|
|
|
|
|
|
|
|
|
if return_errors:
|
|
|
|
|