Coverage for phml\core\compile\util.py: 46%
149 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-30 09:38 -0600
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-30 09:38 -0600
1from __future__ import annotations
3from copy import deepcopy
4from re import match, search, sub
5from typing import Optional
7from phml.nodes import *
8from phml.utils import path, replace_node, test, visit_children
9from phml.virtual_python import VirtualPython, get_vp_result, process_vp_blocks
11# ? Change prefix char for `if`, `elif`, `else`, and `fore` here
12condition_prefix = "@"
14# ? Change prefix char for python attributes here
15python_attr_prefix = ":"
18def replace_components(
19 node: Root | Element | AST, components: dict[str, All_Nodes], vp: VirtualPython, **kwargs
20):
21 """Replace all nodes in the tree with matching components.
23 Args:
24 node (Root | Element | AST): The starting point.
25 vp (VirtualPython): Temp
26 """
27 from phml.utils import find
29 if isinstance(node, AST):
30 node = node.tree
32 for name, value in components.items():
33 curr_node = find(node, ["element", {"tag": name}])
34 while curr_node is not None:
35 new_props = {}
36 for prop in curr_node.properties:
37 if prop.startswith((python_attr_prefix, "py-")):
38 new_props[prop.lstrip(python_attr_prefix).lstrip("py-")] = get_vp_result(
39 curr_node.properties[prop], **vp.locals, **kwargs
40 )
41 elif match(r".*\{.*\}.*", str(curr_node.properties[prop])) is not None:
42 new_props[prop] = process_vp_blocks(curr_node.properties[prop], vp, **kwargs)
43 else:
44 new_props[prop] = curr_node.properties[prop]
46 props = new_props
47 props["children"] = curr_node.children
49 rnode = deepcopy(value["component"])
50 rnode.locals.update(props)
51 rnode.parent = curr_node.parent
53 # Retain conditional properties
54 condition = __has_py_condition(curr_node)
55 if condition is not None:
56 rnode.properties[condition[0]] = condition[1]
57 rnode.locals.pop(condition[0], None)
59 idx = curr_node.parent.children.index(curr_node)
60 curr_node.parent.children = (
61 curr_node.parent.children[:idx]
62 + [
63 *components[curr_node.tag]["python"],
64 *components[curr_node.tag]["script"],
65 *components[curr_node.tag]["style"],
66 rnode,
67 ]
68 + curr_node.parent.children[idx + 1 :]
69 )
70 curr_node = find(node, ["element", {"tag": name}])
73def __has_py_condition(node: Element) -> Optional[tuple[str, str]]:
74 for cond in [
75 "py-for",
76 "py-if",
77 "py-elif",
78 "py-else",
79 f"{condition_prefix}if",
80 f"{condition_prefix}elif",
81 f"{condition_prefix}else",
82 f"{condition_prefix}for",
83 ]:
84 if cond in node.properties.keys():
85 return (cond, node.properties[cond])
86 return None
89def apply_conditions(node: Root | Element | AST, vp: VirtualPython, **kwargs):
90 """Applys all `py-if`, `py-elif`, `py-else`, and `py-for` to the node
91 recursively.
93 Args:
94 node (Root | Element): The node to recursively apply `py-` attributes too.
95 vp (VirtualPython): All of the data from the python elements.
96 """
98 if isinstance(node, AST):
99 node = node.tree
101 process_conditions(node, vp, **kwargs)
102 for child in node.children:
103 if isinstance(child, (Root, Element)):
104 apply_conditions(child, vp, **kwargs)
107def apply_python(node: Root | Element | AST, vp: VirtualPython, **kwargs):
108 """Recursively travers the node and search for python blocks. When found
109 process them and apply the results.
111 Args:
112 node (Root | Element): The node to traverse
113 vp (VirtualPython): The python elements data
114 """
116 if isinstance(node, AST):
117 node = node.tree
119 def process_children(n: Root | Element, local_env: dict):
121 for child in n.children:
122 if test(child, "element"):
123 if "children" in child.locals.keys():
124 replace_node(child, ["element", {"tag": "slot"}], child.locals["children"])
126 le = {**local_env}
127 le.update(child.locals)
128 new_props = {}
129 for prop in child.properties:
130 if prop.startswith((python_attr_prefix, "py-")):
131 new_props[prop.lstrip(python_attr_prefix).lstrip("py-")] = get_vp_result(
132 child.properties[prop], **le, **vp.locals
133 )
134 elif match(r".*\{.*\}.*", str(child.properties[prop])) is not None:
135 new_props[prop] = process_vp_blocks(child.properties[prop], vp, **le)
136 else:
137 new_props[prop] = child.properties[prop]
139 child.properties = new_props
140 process_children(child, {**le})
141 elif (
142 test(child, "text")
143 and child.parent.tag not in ["script", "style"]
144 and search(r".*\{.*\}.*", child.value) is not None
145 ):
146 child.value = process_vp_blocks(child.value, vp, **local_env)
148 process_children(node, {**kwargs})
151def process_conditions(tree: Root | Element, vp: VirtualPython, **kwargs):
152 def py_conditions(node) -> bool:
153 return [
154 k
155 for k in node.properties.keys()
156 if k
157 in [
158 "py-for",
159 "py-if",
160 "py-elif",
161 "py-else",
162 f"{condition_prefix}if",
163 f"{condition_prefix}elif",
164 f"{condition_prefix}else",
165 f"{condition_prefix}for",
166 ]
167 ]
169 conditions = []
170 for child in visit_children(tree):
171 if test(child, "element"):
172 if len(py_conditions(child)) == 1:
173 if py_conditions(child)[0] not in [
174 "py-for",
175 "py-if",
176 f"{condition_prefix}for",
177 f"{condition_prefix}if",
178 ]:
179 idx = child.parent.children.index(child)
180 previous = child.parent.children[idx - 1] if idx > 0 else None
181 prev_cond = py_conditions(previous) if previous is not None else None
182 if (
183 prev_cond is not None
184 and len(prev_cond) == 1
185 and prev_cond[0]
186 in ["py-elif", "py-if", f"{condition_prefix}elif", f"{condition_prefix}if"]
187 ):
188 conditions.append((py_conditions(child)[0], child))
189 else:
190 raise Exception(
191 f"Condition statements that are not py-if or py-for must have py-if or py-elif\
192 as a prevous sibling.\n{child.start_tag()}{f' at {child.position}' or ''}"
193 )
194 else:
195 conditions.append((py_conditions(child)[0], child))
196 elif len(py_conditions(child)) > 1:
197 raise Exception(
198 f"There can only be one python condition statement at a time:\n{repr(child)}"
199 )
201 tree.children = execute_conditions(conditions, tree.children, vp, **kwargs)
204def execute_conditions(cond: list[tuple], children: list, vp: VirtualPython, **kwargs) -> list:
206 valid_prev = {
207 "py-for": [
208 "py-if",
209 "py-elif",
210 "py-else",
211 "py-for",
212 f"{condition_prefix}if",
213 f"{condition_prefix}elif",
214 f"{condition_prefix}else",
215 f"{condition_prefix}for",
216 ],
217 "py-if": [
218 "py-if",
219 "py-elif",
220 "py-else",
221 "py-for",
222 f"{condition_prefix}if",
223 f"{condition_prefix}elif",
224 f"{condition_prefix}else",
225 f"{condition_prefix}for",
226 ],
227 "py-elif": ["py-if", "py-elif", f"{condition_prefix}if", f"{condition_prefix}elif"],
228 "py-else": ["py-if", "py-elif", f"{condition_prefix}if", f"{condition_prefix}elif"],
229 f"{condition_prefix}for": [
230 "py-if",
231 "py-elif",
232 "py-else",
233 "py-for",
234 f"{condition_prefix}if",
235 f"{condition_prefix}elif",
236 f"{condition_prefix}else",
237 f"{condition_prefix}for",
238 ],
239 f"{condition_prefix}if": [
240 "py-if",
241 "py-elif",
242 "py-else",
243 "py-for",
244 f"{condition_prefix}if",
245 f"{condition_prefix}elif",
246 f"{condition_prefix}else",
247 f"{condition_prefix}for",
248 ],
249 f"{condition_prefix}elif": [
250 "py-if",
251 "py-elif",
252 f"{condition_prefix}if",
253 f"{condition_prefix}elif",
254 ],
255 f"{condition_prefix}else": [
256 "py-if",
257 "py-elif",
258 f"{condition_prefix}if",
259 f"{condition_prefix}elif",
260 ],
261 }
263 # Whether the current conditional branch began with an `if` condition.
264 first_cond = False
266 # Previous condition that was run and whether it was successful.
267 previous = (f"{condition_prefix}for", True)
269 # Add the python blocks locals to kwargs dict
270 kwargs.update(vp.locals)
272 # Bring python blocks imports into scope
273 for imp in vp.imports:
274 exec(str(imp))
276 # For each element with a python condition
277 for condition, child in cond:
278 if condition in ["py-for", f"{condition_prefix}for"]:
280 children = run_py_for(condition, child, children, **kwargs)
282 previous = (f"{condition_prefix}for", False)
284 # End any condition branch
285 first_cond = False
287 elif condition in ["py-if", f"{condition_prefix}if"]:
289 clocals = build_locals(child, **kwargs)
290 result = get_vp_result(
291 sub(r"\{|\}", "", child.properties[condition].strip()), **clocals
292 )
294 if result:
295 del child.properties[condition]
296 previous = (f"{condition_prefix}if", True)
297 else:
298 # Condition failed so remove element
299 children.remove(child)
300 previous = (f"{condition_prefix}if", False)
302 # Start of condition branch
303 first_cond = True
305 elif condition in ["py-elif", f"{condition_prefix}elif"]:
306 clocals = build_locals(child, **kwargs)
308 # Can only exist if previous condition in branch failed
309 if previous[0] in valid_prev[condition] and first_cond:
310 if not previous[1]:
311 result = get_vp_result(
312 sub(r"\{|\}", "", child.properties[condition].strip()), **clocals
313 )
314 if result:
315 del child.properties[condition]
316 previous = (f"{condition_prefix}elif", True)
317 else:
319 # Condition failed so remove element
320 children.remove(child)
321 previous = (f"{condition_prefix}elif", False)
322 else:
323 children.remove(child)
324 else:
325 raise Exception(
326 f"py-elif must follow a py-if. It may follow a py-elif if the first condition was a py-if.\n{child}"
327 )
328 elif condition in ["py-else", f"{condition_prefix}else"]:
330 # Can only exist if previous condition in branch failed
331 if previous[0] in valid_prev[condition] and first_cond:
332 if not previous[1]:
333 del child.properties[condition]
334 previous = (f"{condition_prefix}else", True)
335 else:
337 # Condition failed so remove element
338 children.remove(child)
340 # End of condition branch
341 first_cond = False
342 else:
343 raise Exception(
344 f"py-else must follow a py-if. It may follow a py-elif if the first condition was a py-if.\n{child.parent.type}.{child.tag} at {child.position}"
345 )
346 else:
347 raise Exception(f"Unkown condition property: {condition}")
349 return children
352def build_locals(child, **kwargs) -> dict:
353 """Build a dictionary of local variables from a nodes inherited locals and
354 the passed kwargs.
355 """
357 clocals = {**kwargs}
359 # Inherit locals from top down
360 for parent in path(child):
361 if parent.type == "element":
362 clocals.update(parent.locals)
364 clocals.update(child.locals)
365 return clocals
368def run_py_for(condition: str, child: All_Nodes, children: list, **kwargs) -> list:
369 """Take a for loop condition, child node, and the list of children and
370 generate new nodes.
372 Nodes are duplicates from the child node with variables provided
373 from the for loop and child's locals.
374 """
375 clocals = build_locals(child)
377 # Format for loop condition
378 for_loop = sub(r"for |:", "", child.properties[condition]).strip()
380 # Get local var names from for loop condition
381 new_locals = [
382 item.strip()
383 for item in sub(
384 r"\s+",
385 " ",
386 match(r"(for )?(.*)in", for_loop).group(2),
387 ).split(",")
388 ]
390 # Formatter for key value pairs
391 key_value = "\"{key}\": {key}"
393 # Start index on where to insert generated children
394 insert = children.index(child)
396 # Construct dynamic for loop
397 # Uses for loop condition from above
398 # Creates deepcopy of looped element
399 # Adds locals from what was passed to exec and what is from for loop condition
400 # concat and generate new list of children
401 for_loop = f'''\
402new_children = []
403for {for_loop}:
404 new_children.append(deepcopy(child))
405 new_children[-1].locals = {{{", ".join([f"{key_value.format(key=key)}" for key in new_locals])}, **local_vals}}
406 children = [*children[:insert], *new_children, *children[insert+1:]]\
407'''
409 # Prep the child to be used as a copy for new children
411 # Delete the py-for condition from child's props
412 del child.properties[condition]
413 # Set the position to None since the copies are generated
414 child.position = None
416 # Construct locals for dynamic for loops execution
417 local_env = {
418 "children": children,
419 "insert": insert,
420 "child": child,
421 "local_vals": clocals,
422 **kwargs,
423 }
425 # Execute dynamic for loop
426 exec(
427 for_loop,
428 {**globals()},
429 local_env,
430 )
432 # Return the new complete list of children after generation
433 return local_env["children"]