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

1from __future__ import annotations 

2 

3from copy import deepcopy 

4from re import match, search, sub 

5from typing import Optional 

6 

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 

10 

11# ? Change prefix char for `if`, `elif`, `else`, and `fore` here 

12condition_prefix = "@" 

13 

14# ? Change prefix char for python attributes here 

15python_attr_prefix = ":" 

16 

17 

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. 

22 

23 Args: 

24 node (Root | Element | AST): The starting point. 

25 vp (VirtualPython): Temp 

26 """ 

27 from phml.utils import find 

28 

29 if isinstance(node, AST): 

30 node = node.tree 

31 

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] 

45 

46 props = new_props 

47 props["children"] = curr_node.children 

48 

49 rnode = deepcopy(value["component"]) 

50 rnode.locals.update(props) 

51 rnode.parent = curr_node.parent 

52 

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) 

58 

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

71 

72 

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 

87 

88 

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. 

92 

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

97 

98 if isinstance(node, AST): 

99 node = node.tree 

100 

101 process_conditions(node, vp, **kwargs) 

102 for child in node.children: 

103 if isinstance(child, (Root, Element)): 

104 apply_conditions(child, vp, **kwargs) 

105 

106 

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. 

110 

111 Args: 

112 node (Root | Element): The node to traverse 

113 vp (VirtualPython): The python elements data 

114 """ 

115 

116 if isinstance(node, AST): 

117 node = node.tree 

118 

119 def process_children(n: Root | Element, local_env: dict): 

120 

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

125 

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] 

138 

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) 

147 

148 process_children(node, {**kwargs}) 

149 

150 

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 ] 

168 

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 ) 

200 

201 tree.children = execute_conditions(conditions, tree.children, vp, **kwargs) 

202 

203 

204def execute_conditions(cond: list[tuple], children: list, vp: VirtualPython, **kwargs) -> list: 

205 

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 } 

262 

263 # Whether the current conditional branch began with an `if` condition. 

264 first_cond = False 

265 

266 # Previous condition that was run and whether it was successful. 

267 previous = (f"{condition_prefix}for", True) 

268 

269 # Add the python blocks locals to kwargs dict 

270 kwargs.update(vp.locals) 

271 

272 # Bring python blocks imports into scope 

273 for imp in vp.imports: 

274 exec(str(imp)) 

275 

276 # For each element with a python condition 

277 for condition, child in cond: 

278 if condition in ["py-for", f"{condition_prefix}for"]: 

279 

280 children = run_py_for(condition, child, children, **kwargs) 

281 

282 previous = (f"{condition_prefix}for", False) 

283 

284 # End any condition branch 

285 first_cond = False 

286 

287 elif condition in ["py-if", f"{condition_prefix}if"]: 

288 

289 clocals = build_locals(child, **kwargs) 

290 result = get_vp_result( 

291 sub(r"\{|\}", "", child.properties[condition].strip()), **clocals 

292 ) 

293 

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) 

301 

302 # Start of condition branch 

303 first_cond = True 

304 

305 elif condition in ["py-elif", f"{condition_prefix}elif"]: 

306 clocals = build_locals(child, **kwargs) 

307 

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: 

318 

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

329 

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: 

336 

337 # Condition failed so remove element 

338 children.remove(child) 

339 

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

348 

349 return children 

350 

351 

352def build_locals(child, **kwargs) -> dict: 

353 """Build a dictionary of local variables from a nodes inherited locals and 

354 the passed kwargs. 

355 """ 

356 

357 clocals = {**kwargs} 

358 

359 # Inherit locals from top down 

360 for parent in path(child): 

361 if parent.type == "element": 

362 clocals.update(parent.locals) 

363 

364 clocals.update(child.locals) 

365 return clocals 

366 

367 

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. 

371 

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) 

376 

377 # Format for loop condition 

378 for_loop = sub(r"for |:", "", child.properties[condition]).strip() 

379 

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 ] 

389 

390 # Formatter for key value pairs 

391 key_value = "\"{key}\": {key}" 

392 

393 # Start index on where to insert generated children 

394 insert = children.index(child) 

395 

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

408 

409 # Prep the child to be used as a copy for new children 

410 

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 

415 

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 } 

424 

425 # Execute dynamic for loop 

426 exec( 

427 for_loop, 

428 {**globals()}, 

429 local_env, 

430 ) 

431 

432 # Return the new complete list of children after generation 

433 return local_env["children"]