Coverage for phml\compiler\steps\loops.py: 100%
73 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-06 14:03 -0500
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-06 14:03 -0500
1import re
2from copy import deepcopy
3from typing import Any
5from phml.embedded import exec_embedded
6from phml.helpers import build_recursive_context
7from phml.nodes import Element, Literal, Parent
9from .base import comp_step
12def _update_fallbacks(node: Element, exc: Exception):
13 fallbacks = _get_fallbacks(node)
14 for fallback in fallbacks:
15 fallback.context["_loop_fail_"] = exc
18def _remove_fallbacks(node: Element):
19 fallbacks = _get_fallbacks(node)
20 for fallback in fallbacks:
21 if fallback.parent is not None:
22 fallback.parent.remove(fallback)
25def _get_fallbacks(node: Element) -> list[Element]:
26 fallbacks = []
27 if node.parent is not None:
28 idx = node.parent.index(node)
29 for i in range(idx + 1, len(node.parent)):
30 if isinstance(node.parent[i], Element):
31 if "@elif" in node.parent[i]:
32 fallbacks.append(node.parent[i])
33 continue
34 elif "@else" in node.parent[i]:
35 fallbacks.append(node.parent[i])
37 # Ignore comments
38 if not Literal.is_comment(node.parent[i]):
39 break
40 return fallbacks
43def replace_default(
44 node: Element, exc: Exception, sub: Element = Element("", {"@if": "False"})
45):
46 """Set loop node to a False if condition and update all sibling fallbacks with the
47 loop failure exception.
48 """
49 if node.parent is not None and len(node.parent) > 0:
50 node.attributes.pop("@elif", None)
51 node.attributes.pop("@else", None)
52 node.attributes["@if"] = "False"
54 _update_fallbacks(node, exc)
57@comp_step
58def step_expand_loop_tags(
59 node: Parent,
60 _,
61 context: dict[str, Any],
62):
63 """Step to process and expand all loop (<For/>) elements. Will also set loop elements
64 to have a false condition attribute to allow for fallback sibling elements."""
65 if len(node) == 0:
66 return
68 for_loops = [
69 child
70 for child in node
71 if isinstance(child, Element) and child.tag == "For" and len(node) > 0
72 ]
74 def gen_new_children(node: Parent, context: dict[str, Any]) -> list:
75 new_children = deepcopy(node[:])
76 for child in new_children:
77 if isinstance(child, Element):
78 child.context.update(context)
79 child.parent = None
80 child._position = None
81 return new_children
83 for loop in for_loops:
84 parsed_loop = re.match(
85 r"(?:for\\s*)?(?P<captures>.+) in (?P<source>.+):?",
86 str(loop.get(":each", loop.get("each", ""))),
87 )
89 if parsed_loop is None:
90 raise ValueError(
91 "Expected expression in 'each' attribute for <For/> to be a valid list comprehension.",
92 )
94 parsed_loop = parsed_loop.groupdict()
96 captures = re.findall(r"([^\s,]+)", parsed_loop["captures"])
97 parsed_loop["source"].strip()
99 def dict_key(a):
100 return f"'{a}':{a}"
102 process = f"""\
103__children__ = []
104__iterations__ = 0
105for {loop.get(":each", loop.get("each", ""))}:
106 __children__.extend(
107 __gen_new_children__(
108 __node__,
109 {{{','.join(dict_key(key) for key in captures)}}}
110 )
111 )
112 __iterations__ += 1
113(__iterations__, __children__)
114"""
116 if ":each" in loop:
117 _each = f':each="{loop[":each"]}"'
118 else:
119 _each = f'each="{loop["each"]}"'
121 try:
122 iterations, new_nodes = exec_embedded(
123 process,
124 f"<For {_each}>",
125 **build_recursive_context(loop, context),
126 __gen_new_children__=gen_new_children,
127 __node__=loop,
128 )
130 if iterations == 0:
131 replace_default(
132 loop,
133 Exception("No iterations occured. Expected non empty iterator."),
134 )
135 elif loop.parent is not None:
136 _remove_fallbacks(loop)
138 idx = loop.parent.index(loop)
139 loop.parent.remove(loop)
140 loop.parent.insert(idx, new_nodes)
141 except Exception as exec:
142 replace_default(loop, exec)