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
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-06 14:03 -0500
1from enum import EnumType
2from typing import Any
4from phml.embedded import exec_embedded
5from phml.helpers import build_recursive_context
6from phml.nodes import Element, Parent
8from .base import comp_step
11class Condition(EnumType):
12 """Variants of valid conditions.
14 Options:
15 NONE (-1): No condition
16 IF (0): If condition
17 ELIF (1): Else if condition
18 ELSE (2): Else condition
19 """
21 NONE: int = -1
22 IF: int = 0
23 ELIF: int = 1
24 ELSE: int = 2
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
37def get_element_condition(node: Element) -> int:
38 """Get the single condition attribute on a given element.
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)
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 )
57 if len(conditions) == 0:
58 return Condition.NONE
60 return conditions[0]
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 )
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
97 return condition_trees
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.
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()
112 result = exec_embedded(
113 code,
114 f"<{cond[1].tag} {condition}='{code}'>",
115 **build_recursive_context(cond[1], context),
116 )
118 if not isinstance(result, bool):
119 raise ValueError(
120 "Expected boolean expression in condition "
121 + f"attribute '{condition}' at {position!r}",
122 )
124 return result
125 return True
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
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)