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

1import re 

2from copy import deepcopy 

3from typing import Any 

4 

5from phml.embedded import exec_embedded 

6from phml.helpers import build_recursive_context 

7from phml.nodes import Element, Literal, Parent 

8 

9from .base import comp_step 

10 

11 

12def _update_fallbacks(node: Element, exc: Exception): 

13 fallbacks = _get_fallbacks(node) 

14 for fallback in fallbacks: 

15 fallback.context["_loop_fail_"] = exc 

16 

17 

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) 

23 

24 

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]) 

36 

37 # Ignore comments 

38 if not Literal.is_comment(node.parent[i]): 

39 break 

40 return fallbacks 

41 

42 

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" 

53 

54 _update_fallbacks(node, exc) 

55 

56 

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 

67 

68 for_loops = [ 

69 child 

70 for child in node 

71 if isinstance(child, Element) and child.tag == "For" and len(node) > 0 

72 ] 

73 

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 

82 

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 ) 

88 

89 if parsed_loop is None: 

90 raise ValueError( 

91 "Expected expression in 'each' attribute for <For/> to be a valid list comprehension.", 

92 ) 

93 

94 parsed_loop = parsed_loop.groupdict() 

95 

96 captures = re.findall(r"([^\s,]+)", parsed_loop["captures"]) 

97 parsed_loop["source"].strip() 

98 

99 def dict_key(a): 

100 return f"'{a}':{a}" 

101 

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""" 

115 

116 if ":each" in loop: 

117 _each = f':each="{loop[":each"]}"' 

118 else: 

119 _each = f'each="{loop["each"]}"' 

120 

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 ) 

129 

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) 

137 

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)