Coverage for phml\compiler\steps\conditional.py: 100%

71 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-04-06 14:03 -0500

1from enum import EnumType 

2from typing import Any 

3 

4from phml.embedded import exec_embedded 

5from phml.helpers import build_recursive_context 

6from phml.nodes import Element, Parent 

7 

8from .base import comp_step 

9 

10 

11class Condition(EnumType): 

12 """Variants of valid conditions. 

13 

14 Options: 

15 NONE (-1): No condition 

16 IF (0): If condition 

17 ELIF (1): Else if condition 

18 ELSE (2): Else condition 

19 """ 

20 

21 NONE: int = -1 

22 IF: int = 0 

23 ELIF: int = 1 

24 ELSE: int = 2 

25 

26 @staticmethod 

27 def to_str(condition: int): 

28 if condition == 0: 

29 return "@if" 

30 elif condition == 1: 

31 return "@elif" 

32 elif condition == 2: 

33 return "@else" 

34 return "No Condition" # pragma: no cover 

35 

36 

37def get_element_condition(node: Element) -> int: 

38 """Get the single condition attribute on a given element. 

39 

40 Returns: 

41 int: -1 - 2 for: No condition, If, Elif, and Else 

42 """ 

43 conditions = [] 

44 if "@if" in node: 

45 conditions.append(Condition.IF) 

46 if "@elif" in node: 

47 conditions.append(Condition.ELIF) 

48 if "@else" in node: 

49 conditions.append(Condition.ELSE) 

50 

51 if len(conditions) > 1: 

52 raise ValueError( 

53 f"More that one condition attribute found at {node.position!r}" 

54 + ". Expected at most one condition", 

55 ) 

56 

57 if len(conditions) == 0: 

58 return Condition.NONE 

59 

60 return conditions[0] 

61 

62 

63def validate_condition(prev: int, cond: int, position) -> bool: 

64 """Validate that the new condition element is valid following the previous element.""" 

65 if ( 

66 cond > Condition.NONE and cond <= Condition.ELSE 

67 ) and ( # pattern: if, elif, else 

68 cond == Condition.IF # pattern: else -> if, elif -> if, if -> if, None -> if 

69 or (prev == Condition.ELIF and cond == Condition.ELIF) # pattern: elif -> elif 

70 or ( 

71 prev > Condition.NONE and cond > prev 

72 ) # pattern: if -> else, if -> elif, elif -> else 

73 ): 

74 return True 

75 raise ValueError( 

76 f"Invalid condition element order at {position!r}. Expected if -> (elif -> else) | else" 

77 ) 

78 

79 

80def build_condition_trees(node: Element) -> list[list[Element]]: 

81 """Iterates sibling nodes and creates condition trees from adjacent nodes with condition attributes.""" 

82 condition_trees = [] 

83 # 0 == if, 1 == elif, 2 == else 

84 previous = Condition.NONE 

85 for child in node: 

86 if isinstance(child, Element): 

87 condition = get_element_condition(child) 

88 if condition > Condition.NONE and validate_condition( 

89 previous, condition, node.position 

90 ): 

91 if condition == Condition.IF: 

92 condition_trees.append([(condition, child)]) 

93 else: 

94 condition_trees[-1].append((condition, child)) 

95 previous = condition 

96 

97 return condition_trees 

98 

99 

100def get_condition_result( 

101 cond: tuple[int, Element], context: dict[str, Any], position 

102) -> bool: 

103 """Parse the python condition in the attribute and return the result. 

104 

105 Raises: 

106 ValueError: When the condition result is not a boolean 

107 """ 

108 if cond[0] != Condition.ELSE: 

109 condition = Condition.to_str(cond[0]) 

110 code = str(cond[1].get(condition)).strip() 

111 

112 result = exec_embedded( 

113 code, 

114 f"<{cond[1].tag} {condition}='{code}'>", 

115 **build_recursive_context(cond[1], context), 

116 ) 

117 

118 if not isinstance(result, bool): 

119 raise ValueError( 

120 "Expected boolean expression in condition " 

121 + f"attribute '{condition}' at {position!r}", 

122 ) 

123 

124 return result 

125 return True 

126 

127 

128def compile_condition_trees(node, trees: list[list[tuple[int, Element]]], context): 

129 """Compiles the conditions. This will removed False condition nodes and keep True condition nodes.""" 

130 for tree in trees: 

131 for i, cond in enumerate(tree): 

132 result = get_condition_result(cond, context, node.position) 

133 if not result: 

134 cond[1].parent.remove(cond[1]) 

135 else: 

136 cond[1].pop(Condition.to_str(cond[0]), None) 

137 for c in tree[i + 1 :]: 

138 c[1].parent.remove(c[1]) 

139 break 

140 

141 

142@comp_step 

143def step_execute_conditions( 

144 node: Parent, 

145 _, 

146 context: dict[str, Any], 

147): 

148 """Step to process and compile condition attributes in sibling nodes.""" 

149 cond_trees = build_condition_trees(node) 

150 compile_condition_trees(node, cond_trees, context)