Debugging support
* Support for dumping parsed programs as strings (both a basic, repr-like version and an indented version that is easier to read) * Display the parsed program if verbosity is high enough * Program tracing if verbosity is high enough
This commit is contained in:
parent
7b44cdc731
commit
5ef2ffc55c
1 changed files with 179 additions and 24 deletions
|
@ -93,9 +93,10 @@ class RcInstruction:
|
||||||
COMMON_FIELDS = ("when", "loop", "loop_var", "action")
|
COMMON_FIELDS = ("when", "loop", "loop_var", "action")
|
||||||
DEFAULT_LOOP_VAR = "item"
|
DEFAULT_LOOP_VAR = "item"
|
||||||
|
|
||||||
def __init__(self, inventory, templar, action, allowed_fields=()):
|
def __init__(self, inventory, templar, display, action, allowed_fields=()):
|
||||||
self._inventory = inventory
|
self._inventory = inventory
|
||||||
self._templar = templar
|
self._templar = templar
|
||||||
|
self._display = display
|
||||||
self._condition = None
|
self._condition = None
|
||||||
self._loop = None
|
self._loop = None
|
||||||
self._loop_var = None
|
self._loop_var = None
|
||||||
|
@ -103,6 +104,43 @@ class RcInstruction:
|
||||||
self._allowed_fields = set(allowed_fields)
|
self._allowed_fields = set(allowed_fields)
|
||||||
self._allowed_fields.update(RcInstruction.COMMON_FIELDS)
|
self._allowed_fields.update(RcInstruction.COMMON_FIELDS)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
flow = []
|
||||||
|
if self._condition is not None:
|
||||||
|
flow.append(
|
||||||
|
"when=%s"
|
||||||
|
% (
|
||||||
|
repr(
|
||||||
|
self._condition,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if self._loop is not None:
|
||||||
|
flow.append(
|
||||||
|
"loop=%s, loop_var=%s" % (repr(self._loop), repr(self._loop_var))
|
||||||
|
)
|
||||||
|
if flow:
|
||||||
|
output = "{%s}" % (", ".join(flow),)
|
||||||
|
else:
|
||||||
|
output = ""
|
||||||
|
output += self.repr_instruction_only()
|
||||||
|
return output
|
||||||
|
|
||||||
|
def repr_instruction_only(self):
|
||||||
|
return "%s()" % (self._action,)
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
output = []
|
||||||
|
if self._condition is not None:
|
||||||
|
output.append("when: %s" % (repr(self._condition),))
|
||||||
|
if self._loop is not None:
|
||||||
|
output.append("loop[%s]: %s" % (self._loop_var, repr(self._loop)))
|
||||||
|
output.extend(self.dump_instruction())
|
||||||
|
return output
|
||||||
|
|
||||||
|
def dump_instruction(self):
|
||||||
|
return [self.repr_instruction_only()]
|
||||||
|
|
||||||
def parse(self, record):
|
def parse(self, record):
|
||||||
assert "action" in record and record["action"] == self._action
|
assert "action" in record and record["action"] == self._action
|
||||||
# Ensure there are no unsupported fields
|
# Ensure there are no unsupported fields
|
||||||
|
@ -169,10 +207,15 @@ class RcInstruction:
|
||||||
merged_vars = host_vars.copy()
|
merged_vars = host_vars.copy()
|
||||||
merged_vars.update(script_vars)
|
merged_vars.update(script_vars)
|
||||||
if self._loop is None:
|
if self._loop is None:
|
||||||
|
self._display.vvvv("%s : running action %s" % (host_name, self._action))
|
||||||
return self.run_once(host_name, merged_vars, host_vars, script_vars)
|
return self.run_once(host_name, merged_vars, host_vars, script_vars)
|
||||||
loop_values = self.evaluate_loop(host_name, merged_vars)
|
loop_values = self.evaluate_loop(host_name, merged_vars)
|
||||||
script_vars = script_vars.copy()
|
script_vars = script_vars.copy()
|
||||||
for value in loop_values:
|
for value in loop_values:
|
||||||
|
self._display.vvvv(
|
||||||
|
"%s : running action %s for item %s"
|
||||||
|
% (host_name, self._action, repr(value))
|
||||||
|
)
|
||||||
merged_vars[self._loop_var] = value
|
merged_vars[self._loop_var] = value
|
||||||
script_vars[self._loop_var] = value
|
script_vars[self._loop_var] = value
|
||||||
if not self.run_once(host_name, merged_vars, host_vars, script_vars):
|
if not self.run_once(host_name, merged_vars, host_vars, script_vars):
|
||||||
|
@ -181,9 +224,15 @@ class RcInstruction:
|
||||||
|
|
||||||
def run_once(self, host_name, merged_vars, host_vars, script_vars):
|
def run_once(self, host_name, merged_vars, host_vars, script_vars):
|
||||||
if self.evaluate_condition(host_name, merged_vars):
|
if self.evaluate_condition(host_name, merged_vars):
|
||||||
return self.execute_action(host_name, merged_vars, host_vars, script_vars)
|
rv = self.execute_action(host_name, merged_vars, host_vars, script_vars)
|
||||||
|
if not rv:
|
||||||
|
self._display.vvvvv(
|
||||||
|
"%s : action %s returned False, stopping"
|
||||||
|
% (host_name, self._action)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return True
|
rv = True
|
||||||
|
return rv
|
||||||
|
|
||||||
def evaluate_condition(self, host_name, variables):
|
def evaluate_condition(self, host_name, variables):
|
||||||
if self._condition is None:
|
if self._condition is None:
|
||||||
|
@ -195,12 +244,21 @@ class RcInstruction:
|
||||||
self._condition,
|
self._condition,
|
||||||
t.environment.variable_end_string,
|
t.environment.variable_end_string,
|
||||||
)
|
)
|
||||||
return boolean(t.template(template, disable_lookups=False))
|
rv = boolean(t.template(template, disable_lookups=False))
|
||||||
|
self._display.vvvvv(
|
||||||
|
"host %s, action %s, condition %s evaluating to %s"
|
||||||
|
% (host_name, self._action, repr(self._condition), repr(rv))
|
||||||
|
)
|
||||||
|
return rv
|
||||||
|
|
||||||
def evaluate_loop(self, host_name, variables):
|
def evaluate_loop(self, host_name, variables):
|
||||||
if isinstance(self._loop, list):
|
if isinstance(self._loop, list):
|
||||||
return self._loop
|
return self._loop
|
||||||
assert isinstance(self._loop, string_types)
|
assert isinstance(self._loop, string_types)
|
||||||
|
self._display.vvvvv(
|
||||||
|
"host %s, action %s, evaluating loop template %s"
|
||||||
|
% (host_name, self._action, repr(self._loop))
|
||||||
|
)
|
||||||
self._templar.available_variables = variables
|
self._templar.available_variables = variables
|
||||||
value = self._templar.template(self._loop, disable_lookups=False)
|
value = self._templar.template(self._loop, disable_lookups=False)
|
||||||
if not isinstance(value, list):
|
if not isinstance(value, list):
|
||||||
|
@ -235,9 +293,9 @@ class RcInstruction:
|
||||||
|
|
||||||
|
|
||||||
class RciCreateGroup(RcInstruction):
|
class RciCreateGroup(RcInstruction):
|
||||||
def __init__(self, inventory, templar):
|
def __init__(self, inventory, templar, display):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
inventory, templar, "create_group", ("group", "parent", "add_host")
|
inventory, templar, display, "create_group", ("group", "parent", "add_host")
|
||||||
)
|
)
|
||||||
self._group_mbt = None
|
self._group_mbt = None
|
||||||
self._group_name = None
|
self._group_name = None
|
||||||
|
@ -245,6 +303,13 @@ class RciCreateGroup(RcInstruction):
|
||||||
self._parent_name = None
|
self._parent_name = None
|
||||||
self._add_host = None
|
self._add_host = None
|
||||||
|
|
||||||
|
def repr_instruction_only(self):
|
||||||
|
output = "%s(group=%s" % (self._action, repr(self._group_name))
|
||||||
|
if self._parent_name is not None:
|
||||||
|
output += ",parent=" + repr(self._parent_name)
|
||||||
|
output += ",add_host=" + repr(self._add_host) + ")"
|
||||||
|
return output
|
||||||
|
|
||||||
def parse_action(self, record):
|
def parse_action(self, record):
|
||||||
assert self._group_mbt is None and self._group_name is None
|
assert self._group_mbt is None and self._group_name is None
|
||||||
assert self._parent_mbt is None and self._parent_name is None
|
assert self._parent_mbt is None and self._parent_name is None
|
||||||
|
@ -268,19 +333,25 @@ class RciCreateGroup(RcInstruction):
|
||||||
)
|
)
|
||||||
name = self.get_templated_group(merged_vars, self._group_mbt, self._group_name)
|
name = self.get_templated_group(merged_vars, self._group_mbt, self._group_name)
|
||||||
self._inventory.add_group(name)
|
self._inventory.add_group(name)
|
||||||
|
self._display.vvv("- created group %s" % (name,))
|
||||||
if self._parent_name is not None:
|
if self._parent_name is not None:
|
||||||
self._inventory.add_child(parent, name)
|
self._inventory.add_child(parent, name)
|
||||||
|
self._display.vvv("- added group %s to %s" % (name, parent))
|
||||||
if self._add_host:
|
if self._add_host:
|
||||||
self._inventory.add_child(name, host_name)
|
self._inventory.add_child(name, host_name)
|
||||||
|
self._display.vvv("- added host %s to %s" % (host_name, name))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class RciAddHost(RcInstruction):
|
class RciAddHost(RcInstruction):
|
||||||
def __init__(self, inventory, templar):
|
def __init__(self, inventory, templar, display):
|
||||||
super().__init__(inventory, templar, "add_host", ("group",))
|
super().__init__(inventory, templar, display, "add_host", ("group",))
|
||||||
self._may_be_template = None
|
self._may_be_template = None
|
||||||
self._group = None
|
self._group = None
|
||||||
|
|
||||||
|
def repr_instruction_only(self):
|
||||||
|
return "%s(group=%s)" % (self._action, repr(self._group))
|
||||||
|
|
||||||
def parse_action(self, record):
|
def parse_action(self, record):
|
||||||
assert self._may_be_template is None and self._group is None
|
assert self._may_be_template is None and self._group is None
|
||||||
self._may_be_template, self._group = self.parse_group_name(record, "group")
|
self._may_be_template, self._group = self.parse_group_name(record, "group")
|
||||||
|
@ -291,17 +362,25 @@ class RciAddHost(RcInstruction):
|
||||||
merged_vars, self._may_be_template, self._group, must_exist=True
|
merged_vars, self._may_be_template, self._group, must_exist=True
|
||||||
)
|
)
|
||||||
self._inventory.add_child(name, host_name)
|
self._inventory.add_child(name, host_name)
|
||||||
|
self._display.vvv("- added host %s to %s" % (host_name, name))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class RciAddChild(RcInstruction):
|
class RciAddChild(RcInstruction):
|
||||||
def __init__(self, inventory, templar):
|
def __init__(self, inventory, templar, display):
|
||||||
super().__init__(inventory, templar, "add_child", ("group", "child"))
|
super().__init__(inventory, templar, display, "add_child", ("group", "child"))
|
||||||
self._group_mbt = None
|
self._group_mbt = None
|
||||||
self._group_name = None
|
self._group_name = None
|
||||||
self._child_mbt = None
|
self._child_mbt = None
|
||||||
self._child_name = None
|
self._child_name = None
|
||||||
|
|
||||||
|
def repr_instruction_only(self):
|
||||||
|
return "%s(group=%s, child=%s)" % (
|
||||||
|
self._action,
|
||||||
|
repr(self._group_name),
|
||||||
|
repr(self._child_name),
|
||||||
|
)
|
||||||
|
|
||||||
def parse_action(self, record):
|
def parse_action(self, record):
|
||||||
assert self._group_mbt is None and self._group_name is None
|
assert self._group_mbt is None and self._group_name is None
|
||||||
assert self._child_mbt is None and self._child_name is None
|
assert self._child_mbt is None and self._child_name is None
|
||||||
|
@ -318,18 +397,26 @@ class RciAddChild(RcInstruction):
|
||||||
merged_vars, self._child_mbt, self._child_name, must_exist=True
|
merged_vars, self._child_mbt, self._child_name, must_exist=True
|
||||||
)
|
)
|
||||||
self._inventory.add_child(group, child)
|
self._inventory.add_child(group, child)
|
||||||
|
self._display.vvv("- added group %s to %s" % (child, group))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class RciSetVarOrFact(RcInstruction):
|
class RciSetVarOrFact(RcInstruction):
|
||||||
def __init__(self, inventory, templar, is_fact):
|
def __init__(self, inventory, templar, display, is_fact):
|
||||||
action = "set_" + ("fact" if is_fact else "var")
|
action = "set_" + ("fact" if is_fact else "var")
|
||||||
super().__init__(inventory, templar, action, ("name", "value"))
|
super().__init__(inventory, templar, display, action, ("name", "value"))
|
||||||
self._is_fact = is_fact
|
self._is_fact = is_fact
|
||||||
self._var_name = None
|
self._var_name = None
|
||||||
self._name_may_be_template = None
|
self._name_may_be_template = None
|
||||||
self._var_value = None
|
self._var_value = None
|
||||||
|
|
||||||
|
def repr_instruction_only(self):
|
||||||
|
return "%s(name=%s, value=%s)" % (
|
||||||
|
self._action,
|
||||||
|
repr(self._var_name),
|
||||||
|
repr(self._var_value),
|
||||||
|
)
|
||||||
|
|
||||||
def parse_action(self, record):
|
def parse_action(self, record):
|
||||||
assert (
|
assert (
|
||||||
self._var_name is None
|
self._var_name is None
|
||||||
|
@ -379,25 +466,36 @@ class RciSetVarOrFact(RcInstruction):
|
||||||
host_vars[name] = value
|
host_vars[name] = value
|
||||||
else:
|
else:
|
||||||
script_vars[name] = value
|
script_vars[name] = value
|
||||||
|
self._display.vvv(
|
||||||
|
"- set %s %s to %s"
|
||||||
|
% ("fact" if self._is_fact else "var", name, repr(value))
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class RciStop(RcInstruction):
|
class RciStop(RcInstruction):
|
||||||
def __init__(self, inventory, templar):
|
def __init__(self, inventory, templar, display):
|
||||||
super().__init__(inventory, templar, "stop")
|
super().__init__(inventory, templar, display, "stop")
|
||||||
|
|
||||||
def parse_action(self, record):
|
def parse_action(self, record):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def execute_action(self, host_name, merged_vars, host_vars, script_vars):
|
def execute_action(self, host_name, merged_vars, host_vars, script_vars):
|
||||||
|
self._display.vvv("- stopped execution")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class RciFail(RcInstruction):
|
class RciFail(RcInstruction):
|
||||||
def __init__(self, inventory, templar):
|
def __init__(self, inventory, templar, display):
|
||||||
super().__init__(inventory, templar, "fail", ("msg",))
|
super().__init__(inventory, templar, display, "fail", ("msg",))
|
||||||
self._message = None
|
self._message = None
|
||||||
|
|
||||||
|
def repr_instruction_only(self):
|
||||||
|
if self._message is None:
|
||||||
|
return "%s()" % (self._action,)
|
||||||
|
else:
|
||||||
|
return "%s(%s)" % (self._action, self._message)
|
||||||
|
|
||||||
def parse_action(self, record):
|
def parse_action(self, record):
|
||||||
self._message = record.get("msg", None)
|
self._message = record.get("msg", None)
|
||||||
|
|
||||||
|
@ -407,19 +505,53 @@ class RciFail(RcInstruction):
|
||||||
else:
|
else:
|
||||||
self._templar.available_variables = merged_vars
|
self._templar.available_variables = merged_vars
|
||||||
message = self._templar.template(self._message)
|
message = self._templar.template(self._message)
|
||||||
|
self._display.vvv("- failed with message %s" % (message,))
|
||||||
raise AnsibleRuntimeError(message)
|
raise AnsibleRuntimeError(message)
|
||||||
|
|
||||||
|
|
||||||
class RciBlock(RcInstruction):
|
class RciBlock(RcInstruction):
|
||||||
def __init__(self, inventory, templar):
|
def __init__(self, inventory, templar, display):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
inventory, templar, "block", ("block", "rescue", "always", "locals")
|
inventory,
|
||||||
|
templar,
|
||||||
|
display,
|
||||||
|
"block",
|
||||||
|
("block", "rescue", "always", "locals"),
|
||||||
)
|
)
|
||||||
self._block = None
|
self._block = None
|
||||||
self._rescue = None
|
self._rescue = None
|
||||||
self._always = None
|
self._always = None
|
||||||
self._locals = None
|
self._locals = None
|
||||||
|
|
||||||
|
def repr_instruction_only(self):
|
||||||
|
return "%s(block=%s, rescue=%s, always=%s, locals=%s)" % (
|
||||||
|
self._action,
|
||||||
|
repr(self._block),
|
||||||
|
repr(self._rescue),
|
||||||
|
repr(self._always),
|
||||||
|
repr(self._locals),
|
||||||
|
)
|
||||||
|
|
||||||
|
def dump_instruction(self):
|
||||||
|
output = ["%s(...):" % (self._action,)]
|
||||||
|
self.dump_block(output, "block", self._block)
|
||||||
|
self.dump_block(output, "rescue", self._rescue)
|
||||||
|
self.dump_block(output, "always", self._always)
|
||||||
|
if self._locals:
|
||||||
|
output.append(" locals:")
|
||||||
|
for k, v in self._locals.items():
|
||||||
|
output.append(" " + repr(k) + "=" + repr(v))
|
||||||
|
return output
|
||||||
|
|
||||||
|
def dump_block(self, output, block_name, block_contents):
|
||||||
|
if not block_contents:
|
||||||
|
return
|
||||||
|
output.append(" " + block_name + ":")
|
||||||
|
for pos, instr in enumerate(block_contents):
|
||||||
|
if pos != 0:
|
||||||
|
output.append("")
|
||||||
|
output.extend(" " + s for s in instr.dump())
|
||||||
|
|
||||||
def parse_action(self, record):
|
def parse_action(self, record):
|
||||||
assert (
|
assert (
|
||||||
self._block is None
|
self._block is None
|
||||||
|
@ -465,7 +597,7 @@ class RciBlock(RcInstruction):
|
||||||
instructions = []
|
instructions = []
|
||||||
for record in record[key]:
|
for record in record[key]:
|
||||||
instructions.append(
|
instructions.append(
|
||||||
parse_instruction(self._inventory, self._templar, record)
|
parse_instruction(self._inventory, self._templar, self._display, record)
|
||||||
)
|
)
|
||||||
return instructions
|
return instructions
|
||||||
|
|
||||||
|
@ -483,18 +615,25 @@ class RciBlock(RcInstruction):
|
||||||
result = self._templar.template(value)
|
result = self._templar.template(value)
|
||||||
script_vars[key] = result
|
script_vars[key] = result
|
||||||
merged_vars[key] = result
|
merged_vars[key] = result
|
||||||
|
self._display.vvv("- set block-local %s to %s" % (key, result))
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
self._display.vvv("- running 'block' instructions")
|
||||||
return self.run_block(
|
return self.run_block(
|
||||||
self._block, host_name, merged_vars, host_vars, script_vars
|
self._block, host_name, merged_vars, host_vars, script_vars
|
||||||
)
|
)
|
||||||
except AnsibleError as e:
|
except AnsibleError as e:
|
||||||
|
if not self._rescue:
|
||||||
|
self._display.vvv("- block failed")
|
||||||
|
raise
|
||||||
|
self._display.vvv("- block failed, running 'rescue' instructions")
|
||||||
script_vars["reconstructed_error"] = str(e)
|
script_vars["reconstructed_error"] = str(e)
|
||||||
merged_vars["reconstructed_error"] = str(e)
|
merged_vars["reconstructed_error"] = str(e)
|
||||||
return self.run_block(
|
return self.run_block(
|
||||||
self._rescue, host_name, merged_vars, host_vars, script_vars
|
self._rescue, host_name, merged_vars, host_vars, script_vars
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
self._display.vvv("- block exited, running 'always' instructions")
|
||||||
self.run_block(self._always, host_name, merged_vars, host_vars, script_vars)
|
self.run_block(self._always, host_name, merged_vars, host_vars, script_vars)
|
||||||
|
|
||||||
def run_block(self, block, host_name, merged_vars, host_vars, script_vars):
|
def run_block(self, block, host_name, merged_vars, host_vars, script_vars):
|
||||||
|
@ -510,17 +649,17 @@ INSTRUCTIONS = {
|
||||||
"block": RciBlock,
|
"block": RciBlock,
|
||||||
"create_group": RciCreateGroup,
|
"create_group": RciCreateGroup,
|
||||||
"fail": RciFail,
|
"fail": RciFail,
|
||||||
"set_fact": lambda i, t: RciSetVarOrFact(i, t, True),
|
"set_fact": lambda i, t, d: RciSetVarOrFact(i, t, d, True),
|
||||||
"set_var": lambda i, t: RciSetVarOrFact(i, t, False),
|
"set_var": lambda i, t, d: RciSetVarOrFact(i, t, d, False),
|
||||||
"stop": RciStop,
|
"stop": RciStop,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def parse_instruction(inventory, templar, record):
|
def parse_instruction(inventory, templar, display, record):
|
||||||
action = record["action"]
|
action = record["action"]
|
||||||
if action not in INSTRUCTIONS:
|
if action not in INSTRUCTIONS:
|
||||||
raise AnsibleParserError("Unknown action '%s'" % (action,))
|
raise AnsibleParserError("Unknown action '%s'" % (action,))
|
||||||
instruction = INSTRUCTIONS[action](inventory, templar)
|
instruction = INSTRUCTIONS[action](inventory, templar, display)
|
||||||
instruction.parse(record)
|
instruction.parse(record)
|
||||||
return instruction
|
return instruction
|
||||||
|
|
||||||
|
@ -539,8 +678,12 @@ class InventoryModule(BaseInventoryPlugin):
|
||||||
instr_src = self.get_option("instructions")
|
instr_src = self.get_option("instructions")
|
||||||
instructions = []
|
instructions = []
|
||||||
for record in instr_src:
|
for record in instr_src:
|
||||||
instructions.append(parse_instruction(self.inventory, self.templar, record))
|
instructions.append(
|
||||||
|
parse_instruction(self.inventory, self.templar, self.display, record)
|
||||||
|
)
|
||||||
|
self.dump_program(instructions)
|
||||||
for host in inventory.hosts:
|
for host in inventory.hosts:
|
||||||
|
self.display.vvv("executing reconstructed script for %s" % (host,))
|
||||||
try:
|
try:
|
||||||
self.exec_for_host(host, instructions)
|
self.exec_for_host(host, instructions)
|
||||||
except AnsibleError as e:
|
except AnsibleError as e:
|
||||||
|
@ -556,3 +699,15 @@ class InventoryModule(BaseInventoryPlugin):
|
||||||
for instruction in instructions:
|
for instruction in instructions:
|
||||||
if not instruction.run_for(host, host_vars, script_vars):
|
if not instruction.run_for(host, host_vars, script_vars):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def dump_program(self, instructions):
|
||||||
|
if self.display.verbosity < 4:
|
||||||
|
if self.display.verbosity == 3:
|
||||||
|
self.display.vvv("parsed program: " + repr(instructions))
|
||||||
|
return
|
||||||
|
output = []
|
||||||
|
for pos, instr in enumerate(instructions):
|
||||||
|
if pos:
|
||||||
|
output.append("")
|
||||||
|
output.extend(instr.dump())
|
||||||
|
self.display.vvvv("parsed program:\n\n" + "\n".join(" " + s for s in output))
|
||||||
|
|
Loading…
Reference in a new issue