Coverage for phml\core\compile\util.py: 100%
150 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-08 13:16 -0600
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-08 13:16 -0600
1"""Helper methods for processing dynamic python attributes and blocks."""
3from __future__ import annotations
5from copy import deepcopy
6from re import match, search, sub
8from phml.nodes import AST, All_Nodes, Element, Root
9from phml.utils import check, find, path, replace_node, visit_children
10from phml.virtual_python import VirtualPython, get_vp_result, process_vp_blocks
12# ? Change prefix char for `if`, `elif`, `else`, and `fore` here
13CONDITION_PREFIX = "@"
15# ? Change prefix char for python attributes here
16ATTR_PREFIX = ":"
19def replace_components(
20 node: Root | Element | AST,
21 components: dict[str, All_Nodes],
22 virtual_python: VirtualPython,
23 **kwargs,
24):
25 """Replace all nodes in the tree with matching components.
27 Args:
28 node (Root | Element | AST): The starting point.
29 virtual_python (VirtualPython): Temp
30 """
32 if isinstance(node, AST):
33 node = node.tree
35 for name, value in components.items():
36 curr_node = find(node, ["element", {"tag": name}])
37 while curr_node is not None:
38 new_props = {}
40 # Retain conditional properties
41 conditions = py_conditions(curr_node)
42 if len(conditions) > 0:
43 for condition in conditions:
44 new_props[condition] = curr_node[condition]
46 for prop in curr_node.properties:
47 if prop not in conditions:
48 if prop.startswith((ATTR_PREFIX, "py-")):
49 local_env = {**kwargs}
50 local_env.update(virtual_python.locals)
51 new_props[prop.lstrip(ATTR_PREFIX).lstrip("py-")] = get_vp_result(
52 curr_node[prop], **local_env
53 )
54 elif match(r".*\{.*\}.*", str(curr_node[prop])) is not None:
55 new_props[prop] = process_vp_blocks(
56 curr_node[prop], virtual_python, **kwargs
57 )
58 else:
59 new_props[prop] = curr_node[prop]
61 props = new_props
62 props["children"] = curr_node.children
64 rnode = deepcopy(value["component"])
65 rnode.locals.update(props)
66 rnode.parent = curr_node.parent
68 idx = curr_node.parent.children.index(curr_node)
69 curr_node.parent.children = (
70 curr_node.parent.children[:idx]
71 + [
72 *components[curr_node.tag]["python"],
73 *components[curr_node.tag]["script"],
74 *components[curr_node.tag]["style"],
75 rnode,
76 ]
77 + curr_node.parent.children[idx + 1 :]
78 )
79 curr_node = find(node, ["element", {"tag": name}])
82# def __has_py_condition(node: Element) -> Optional[tuple[str, str]]:
83# for cond in [
84# "py-for",
85# "py-if",
86# "py-elif",
87# "py-else",
88# f"{CONDITION_PREFIX}if",
89# f"{CONDITION_PREFIX}elif",
90# f"{CONDITION_PREFIX}else",
91# f"{CONDITION_PREFIX}for",
92# ]:
93# if cond in node.properties.keys():
94# return (cond, node[cond])
95# return None
98def apply_conditions(node: Root | Element | AST, virtual_python: VirtualPython, **kwargs):
99 """Applys all `py-if`, `py-elif`, `py-else`, and `py-for` to the node
100 recursively.
102 Args:
103 node (Root | Element): The node to recursively apply `py-` attributes too.
104 virtual_python (VirtualPython): All of the data from the python elements.
105 """
107 if isinstance(node, AST):
108 node = node.tree
110 process_conditions(node, virtual_python, **kwargs)
111 for child in node.children:
112 if isinstance(child, (Root, Element)):
113 apply_conditions(child, virtual_python, **kwargs)
116def apply_python(
117 current: Root | Element | AST,
118 virtual_python: VirtualPython,
119 **kwargs,
120):
121 """Recursively travers the node and search for python blocks. When found
122 process them and apply the results.
124 Args:
125 current (Root | Element): The node to traverse
126 virtual_python (VirtualPython): The python elements data
127 """
129 if isinstance(current, AST):
130 current = current.tree
132 def process_children(node: Root | Element, local_env: dict):
134 for child in node.children:
135 if check(child, "element"):
136 if "children" in child.locals.keys():
137 replace_node(child, ["element", {"tag": "slot"}], child.locals["children"])
139 local_vars = {**local_env}
140 local_vars.update(child.locals)
141 new_props = {}
143 for prop in child.properties:
144 if prop.startswith((ATTR_PREFIX, "py-")):
145 local_env = {**virtual_python.locals}
146 local_env.update(local_vars)
147 new_props[prop.lstrip(ATTR_PREFIX).lstrip("py-")] = get_vp_result(
148 child[prop], **local_env
149 )
150 elif match(r".*\{.*\}.*", str(child[prop])) is not None:
151 new_props[prop] = process_vp_blocks(
152 child[prop], virtual_python, **local_vars
153 )
154 else:
155 new_props[prop] = child[prop]
157 child.properties = new_props
158 process_children(child, {**local_vars})
159 elif (
160 check(child, "text")
161 and child.parent.tag not in ["script", "style"]
162 and search(r".*\{.*\}.*", child.value) is not None
163 ):
164 child.value = process_vp_blocks(child.value, virtual_python, **local_env)
166 process_children(current, {**kwargs})
169def py_conditions(node: Element) -> bool:
170 """Return all python condition attributes on an element."""
171 return [
172 k
173 for k in node.properties.keys()
174 if k
175 in [
176 "py-for",
177 "py-if",
178 "py-elif",
179 "py-else",
180 f"{CONDITION_PREFIX}if",
181 f"{CONDITION_PREFIX}elif",
182 f"{CONDITION_PREFIX}else",
183 f"{CONDITION_PREFIX}for",
184 ]
185 ]
188def process_conditions(tree: Root | Element, virtual_python: VirtualPython, **kwargs):
189 """Process all python condition attributes in the phml tree.
191 Args:
192 tree (Root | Element): The tree to process conditions on.
193 virtual_python (VirtualPython): The collection of information from the python blocks.
194 """
196 conditions = []
197 for child in visit_children(tree):
198 if check(child, "element"):
199 if len(py_conditions(child)) == 1:
200 if py_conditions(child)[0] not in [
201 "py-for",
202 "py-if",
203 f"{CONDITION_PREFIX}for",
204 f"{CONDITION_PREFIX}if",
205 ]:
206 idx = child.parent.children.index(child)
207 previous = child.parent.children[idx - 1] if idx > 0 else None
208 prev_cond = (
209 py_conditions(previous)
210 if previous is not None and isinstance(previous, Element)
211 else None
212 )
213 if (
214 prev_cond is not None
215 and len(prev_cond) == 1
216 and prev_cond[0]
217 in ["py-elif", "py-if", f"{CONDITION_PREFIX}elif", f"{CONDITION_PREFIX}if"]
218 ):
219 conditions.append((py_conditions(child)[0], child))
220 else:
221 raise Exception(
222 f"Condition statements that are not py-if or py-for must have py-if or\
223 py-elif as a prevous sibling.\n{child.start_tag()}{f' at {child.position}' or ''}"
224 )
225 else:
226 conditions.append((py_conditions(child)[0], child))
227 elif len(py_conditions(child)) > 1:
228 raise Exception(
229 f"There can only be one python condition statement at a time:\n{repr(child)}"
230 )
232 tree.children = execute_conditions(conditions, tree.children, virtual_python, **kwargs)
235def execute_conditions(
236 cond: list[tuple],
237 children: list,
238 virtual_python: VirtualPython,
239 **kwargs,
240) -> list:
241 """Execute all the conditions. If the condition is a `for` then generate more nodes.
242 All other conditions determine if the node stays or is removed.
244 Args:
245 cond (list[tuple]): The list of conditions to apply. Holds tuples of (condition, node).
246 children (list): List of current nodes children.
247 virtual_python (VirtualPython): The collection of information from the python blocks.
249 Raises:
250 Exception: An unkown conditional attribute is being parsed.
251 Exception: Condition requirements are not met.
253 Returns:
254 list: The newly generated/modified list of children.
255 """
257 valid_prev = {
258 "py-for": [
259 "py-if",
260 "py-elif",
261 "py-else",
262 "py-for",
263 f"{CONDITION_PREFIX}if",
264 f"{CONDITION_PREFIX}elif",
265 f"{CONDITION_PREFIX}else",
266 f"{CONDITION_PREFIX}for",
267 ],
268 "py-if": [
269 "py-if",
270 "py-elif",
271 "py-else",
272 "py-for",
273 f"{CONDITION_PREFIX}if",
274 f"{CONDITION_PREFIX}elif",
275 f"{CONDITION_PREFIX}else",
276 f"{CONDITION_PREFIX}for",
277 ],
278 "py-elif": ["py-if", "py-elif", f"{CONDITION_PREFIX}if", f"{CONDITION_PREFIX}elif"],
279 "py-else": ["py-if", "py-elif", f"{CONDITION_PREFIX}if", f"{CONDITION_PREFIX}elif"],
280 f"{CONDITION_PREFIX}for": [
281 "py-if",
282 "py-elif",
283 "py-else",
284 "py-for",
285 f"{CONDITION_PREFIX}if",
286 f"{CONDITION_PREFIX}elif",
287 f"{CONDITION_PREFIX}else",
288 f"{CONDITION_PREFIX}for",
289 ],
290 f"{CONDITION_PREFIX}if": [
291 "py-if",
292 "py-elif",
293 "py-else",
294 "py-for",
295 f"{CONDITION_PREFIX}if",
296 f"{CONDITION_PREFIX}elif",
297 f"{CONDITION_PREFIX}else",
298 f"{CONDITION_PREFIX}for",
299 ],
300 f"{CONDITION_PREFIX}elif": [
301 "py-if",
302 "py-elif",
303 f"{CONDITION_PREFIX}if",
304 f"{CONDITION_PREFIX}elif",
305 ],
306 f"{CONDITION_PREFIX}else": [
307 "py-if",
308 "py-elif",
309 f"{CONDITION_PREFIX}if",
310 f"{CONDITION_PREFIX}elif",
311 ],
312 }
314 # Whether the current conditional branch began with an `if` condition.
315 first_cond = False
317 # Previous condition that was run and whether it was successful.
318 previous = (f"{CONDITION_PREFIX}for", True)
320 # Add the python blocks locals to kwargs dict
321 kwargs.update(virtual_python.locals)
323 # Bring python blocks imports into scope
324 for imp in virtual_python.imports:
325 exec(str(imp)) # pylint: disable=exec-used
327 # For each element with a python condition
328 for condition, child in cond:
329 if condition in ["py-for", f"{CONDITION_PREFIX}for"]:
331 children = run_py_for(condition, child, children, **kwargs)
333 previous = (f"{CONDITION_PREFIX}for", False)
335 # End any condition branch
336 first_cond = False
338 elif condition in ["py-if", f"{CONDITION_PREFIX}if"]:
339 previous = run_py_if(child, condition, children, **kwargs)
341 # Start of condition branch
342 first_cond = True
344 elif condition in ["py-elif", f"{CONDITION_PREFIX}elif"]:
345 # Can only exist if previous condition in branch failed
346 previous = run_py_elif(
347 child,
348 children,
349 condition,
350 {
351 "previous": previous,
352 "valid_prev": valid_prev,
353 "first_cond": first_cond,
354 },
355 **kwargs,
356 )
357 elif condition in ["py-else", f"{CONDITION_PREFIX}else"]:
359 # Can only exist if previous condition in branch failed
360 previous = run_py_else(
361 child,
362 children,
363 condition,
364 {
365 "previous": previous,
366 "valid_prev": valid_prev,
367 "first_cond": first_cond,
368 },
369 )
371 # End any condition branch
372 first_cond = False
374 return children
377def build_locals(child, **kwargs) -> dict:
378 """Build a dictionary of local variables from a nodes inherited locals and
379 the passed kwargs.
380 """
382 clocals = {**kwargs}
384 # Inherit locals from top down
385 for parent in path(child):
386 if parent.type == "element":
387 clocals.update(parent.locals)
389 clocals.update(child.locals)
390 return clocals
393def run_py_if(child: Element, condition: str, children: list, **kwargs):
394 """Run the logic for manipulating the children on a `if` condition."""
396 clocals = build_locals(child, **kwargs)
397 result = get_vp_result(sub(r"\{|\}", "", child[condition].strip()), **clocals)
399 if result:
400 del child[condition]
401 return (f"{CONDITION_PREFIX}if", True)
403 # Condition failed, so remove the node
404 children.remove(child)
405 return (f"{CONDITION_PREFIX}if", False)
408def run_py_elif(
409 child: Element,
410 children: list,
411 condition: str,
412 variables: dict,
413 **kwargs,
414):
415 """Run the logic for manipulating the children on a `elif` condition."""
417 clocals = build_locals(child, **kwargs)
419 if variables["previous"][0] in variables["valid_prev"][condition] and variables["first_cond"]:
420 if not variables["previous"][1]:
421 result = get_vp_result(sub(r"\{|\}", "", child[condition].strip()), **clocals)
422 if result:
423 del child[condition]
424 return (f"{CONDITION_PREFIX}elif", True)
426 children.remove(child)
427 return variables["previous"]
430def run_py_else(child: Element, children: list, condition: str, variables: dict):
431 """Run the logic for manipulating the children on a `else` condition."""
433 if variables["previous"][0] in variables["valid_prev"][condition] and variables["first_cond"]:
434 if not variables["previous"][1]:
435 del child[condition]
436 return (f"{CONDITION_PREFIX}else", True)
438 # Condition failed so remove element
439 children.remove(child)
440 return (f"{CONDITION_PREFIX}else", False)
443def run_py_for(condition: str, child: All_Nodes, children: list, **kwargs) -> list:
444 """Take a for loop condition, child node, and the list of children and
445 generate new nodes.
447 Nodes are duplicates from the child node with variables provided
448 from the for loop and child's locals.
449 """
450 clocals = build_locals(child)
452 # Format for loop condition
453 for_loop = sub(r"for |:", "", child[condition]).strip()
455 # Get local var names from for loop condition
456 new_locals = [
457 item.strip()
458 for item in sub(
459 r"\s+",
460 " ",
461 match(r"(for )?(.*)in", for_loop).group(2),
462 ).split(",")
463 ]
465 # Formatter for key value pairs
466 key_value = "\"{key}\": {key}"
468 # Start index on where to insert generated children
469 insert = children.index(child)
471 # Construct dynamic for loop
472 # Uses for loop condition from above
473 # Creates deepcopy of looped element
474 # Adds locals from what was passed to exec and what is from for loop condition
475 # concat and generate new list of children
476 for_loop = f'''\
477new_children = []
478for {for_loop}:
479 new_child = deepcopy(child)
480 new_child.locals = {{{", ".join([f"{key_value.format(key=key)}" for key in new_locals])}, **local_vals}}
481 children = [*children[:insert], new_child, *children[insert+1:]]
482 insert += 1\
483'''
485 # Prep the child to be used as a copy for new children
487 # Delete the py-for condition from child's props
488 del child[condition]
489 # Set the position to None since the copies are generated
490 child.position = None
492 # Construct locals for dynamic for loops execution
493 local_env = {
494 "children": children,
495 "insert": insert,
496 "child": child,
497 "local_vals": clocals,
498 **kwargs,
499 }
501 # Execute dynamic for loop
502 exec( # pylint: disable=exec-used
503 for_loop,
504 {**globals()},
505 local_env,
506 )
508 # Return the new complete list of children after generation
509 return local_env["children"]