Coverage for phml\core\compile\util.py: 100%

150 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-08 13:16 -0600

1"""Helper methods for processing dynamic python attributes and blocks.""" 

2 

3from __future__ import annotations 

4 

5from copy import deepcopy 

6from re import match, search, sub 

7 

8from phml.nodes import AST, All_Nodes, Element, Root 

9from phml.utils import check, find, path, replace_node, visit_children 

10from phml.virtual_python import VirtualPython, get_vp_result, process_vp_blocks 

11 

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

13CONDITION_PREFIX = "@" 

14 

15# ? Change prefix char for python attributes here 

16ATTR_PREFIX = ":" 

17 

18 

19def replace_components( 

20 node: Root | Element | AST, 

21 components: dict[str, All_Nodes], 

22 virtual_python: VirtualPython, 

23 **kwargs, 

24): 

25 """Replace all nodes in the tree with matching components. 

26 

27 Args: 

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

29 virtual_python (VirtualPython): Temp 

30 """ 

31 

32 if isinstance(node, AST): 

33 node = node.tree 

34 

35 for name, value in components.items(): 

36 curr_node = find(node, ["element", {"tag": name}]) 

37 while curr_node is not None: 

38 new_props = {} 

39 

40 # Retain conditional properties 

41 conditions = py_conditions(curr_node) 

42 if len(conditions) > 0: 

43 for condition in conditions: 

44 new_props[condition] = curr_node[condition] 

45 

46 for prop in curr_node.properties: 

47 if prop not in conditions: 

48 if prop.startswith((ATTR_PREFIX, "py-")): 

49 local_env = {**kwargs} 

50 local_env.update(virtual_python.locals) 

51 new_props[prop.lstrip(ATTR_PREFIX).lstrip("py-")] = get_vp_result( 

52 curr_node[prop], **local_env 

53 ) 

54 elif match(r".*\{.*\}.*", str(curr_node[prop])) is not None: 

55 new_props[prop] = process_vp_blocks( 

56 curr_node[prop], virtual_python, **kwargs 

57 ) 

58 else: 

59 new_props[prop] = curr_node[prop] 

60 

61 props = new_props 

62 props["children"] = curr_node.children 

63 

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

65 rnode.locals.update(props) 

66 rnode.parent = curr_node.parent 

67 

68 idx = curr_node.parent.children.index(curr_node) 

69 curr_node.parent.children = ( 

70 curr_node.parent.children[:idx] 

71 + [ 

72 *components[curr_node.tag]["python"], 

73 *components[curr_node.tag]["script"], 

74 *components[curr_node.tag]["style"], 

75 rnode, 

76 ] 

77 + curr_node.parent.children[idx + 1 :] 

78 ) 

79 curr_node = find(node, ["element", {"tag": name}]) 

80 

81 

82# def __has_py_condition(node: Element) -> Optional[tuple[str, str]]: 

83# for cond in [ 

84# "py-for", 

85# "py-if", 

86# "py-elif", 

87# "py-else", 

88# f"{CONDITION_PREFIX}if", 

89# f"{CONDITION_PREFIX}elif", 

90# f"{CONDITION_PREFIX}else", 

91# f"{CONDITION_PREFIX}for", 

92# ]: 

93# if cond in node.properties.keys(): 

94# return (cond, node[cond]) 

95# return None 

96 

97 

98def apply_conditions(node: Root | Element | AST, virtual_python: VirtualPython, **kwargs): 

99 """Applys all `py-if`, `py-elif`, `py-else`, and `py-for` to the node 

100 recursively. 

101 

102 Args: 

103 node (Root | Element): The node to recursively apply `py-` attributes too. 

104 virtual_python (VirtualPython): All of the data from the python elements. 

105 """ 

106 

107 if isinstance(node, AST): 

108 node = node.tree 

109 

110 process_conditions(node, virtual_python, **kwargs) 

111 for child in node.children: 

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

113 apply_conditions(child, virtual_python, **kwargs) 

114 

115 

116def apply_python( 

117 current: Root | Element | AST, 

118 virtual_python: VirtualPython, 

119 **kwargs, 

120): 

121 """Recursively travers the node and search for python blocks. When found 

122 process them and apply the results. 

123 

124 Args: 

125 current (Root | Element): The node to traverse 

126 virtual_python (VirtualPython): The python elements data 

127 """ 

128 

129 if isinstance(current, AST): 

130 current = current.tree 

131 

132 def process_children(node: Root | Element, local_env: dict): 

133 

134 for child in node.children: 

135 if check(child, "element"): 

136 if "children" in child.locals.keys(): 

137 replace_node(child, ["element", {"tag": "slot"}], child.locals["children"]) 

138 

139 local_vars = {**local_env} 

140 local_vars.update(child.locals) 

141 new_props = {} 

142 

143 for prop in child.properties: 

144 if prop.startswith((ATTR_PREFIX, "py-")): 

145 local_env = {**virtual_python.locals} 

146 local_env.update(local_vars) 

147 new_props[prop.lstrip(ATTR_PREFIX).lstrip("py-")] = get_vp_result( 

148 child[prop], **local_env 

149 ) 

150 elif match(r".*\{.*\}.*", str(child[prop])) is not None: 

151 new_props[prop] = process_vp_blocks( 

152 child[prop], virtual_python, **local_vars 

153 ) 

154 else: 

155 new_props[prop] = child[prop] 

156 

157 child.properties = new_props 

158 process_children(child, {**local_vars}) 

159 elif ( 

160 check(child, "text") 

161 and child.parent.tag not in ["script", "style"] 

162 and search(r".*\{.*\}.*", child.value) is not None 

163 ): 

164 child.value = process_vp_blocks(child.value, virtual_python, **local_env) 

165 

166 process_children(current, {**kwargs}) 

167 

168 

169def py_conditions(node: Element) -> bool: 

170 """Return all python condition attributes on an element.""" 

171 return [ 

172 k 

173 for k in node.properties.keys() 

174 if k 

175 in [ 

176 "py-for", 

177 "py-if", 

178 "py-elif", 

179 "py-else", 

180 f"{CONDITION_PREFIX}if", 

181 f"{CONDITION_PREFIX}elif", 

182 f"{CONDITION_PREFIX}else", 

183 f"{CONDITION_PREFIX}for", 

184 ] 

185 ] 

186 

187 

188def process_conditions(tree: Root | Element, virtual_python: VirtualPython, **kwargs): 

189 """Process all python condition attributes in the phml tree. 

190 

191 Args: 

192 tree (Root | Element): The tree to process conditions on. 

193 virtual_python (VirtualPython): The collection of information from the python blocks. 

194 """ 

195 

196 conditions = [] 

197 for child in visit_children(tree): 

198 if check(child, "element"): 

199 if len(py_conditions(child)) == 1: 

200 if py_conditions(child)[0] not in [ 

201 "py-for", 

202 "py-if", 

203 f"{CONDITION_PREFIX}for", 

204 f"{CONDITION_PREFIX}if", 

205 ]: 

206 idx = child.parent.children.index(child) 

207 previous = child.parent.children[idx - 1] if idx > 0 else None 

208 prev_cond = ( 

209 py_conditions(previous) 

210 if previous is not None and isinstance(previous, Element) 

211 else None 

212 ) 

213 if ( 

214 prev_cond is not None 

215 and len(prev_cond) == 1 

216 and prev_cond[0] 

217 in ["py-elif", "py-if", f"{CONDITION_PREFIX}elif", f"{CONDITION_PREFIX}if"] 

218 ): 

219 conditions.append((py_conditions(child)[0], child)) 

220 else: 

221 raise Exception( 

222 f"Condition statements that are not py-if or py-for must have py-if or\ 

223 py-elif as a prevous sibling.\n{child.start_tag()}{f' at {child.position}' or ''}" 

224 ) 

225 else: 

226 conditions.append((py_conditions(child)[0], child)) 

227 elif len(py_conditions(child)) > 1: 

228 raise Exception( 

229 f"There can only be one python condition statement at a time:\n{repr(child)}" 

230 ) 

231 

232 tree.children = execute_conditions(conditions, tree.children, virtual_python, **kwargs) 

233 

234 

235def execute_conditions( 

236 cond: list[tuple], 

237 children: list, 

238 virtual_python: VirtualPython, 

239 **kwargs, 

240) -> list: 

241 """Execute all the conditions. If the condition is a `for` then generate more nodes. 

242 All other conditions determine if the node stays or is removed. 

243 

244 Args: 

245 cond (list[tuple]): The list of conditions to apply. Holds tuples of (condition, node). 

246 children (list): List of current nodes children. 

247 virtual_python (VirtualPython): The collection of information from the python blocks. 

248 

249 Raises: 

250 Exception: An unkown conditional attribute is being parsed. 

251 Exception: Condition requirements are not met. 

252 

253 Returns: 

254 list: The newly generated/modified list of children. 

255 """ 

256 

257 valid_prev = { 

258 "py-for": [ 

259 "py-if", 

260 "py-elif", 

261 "py-else", 

262 "py-for", 

263 f"{CONDITION_PREFIX}if", 

264 f"{CONDITION_PREFIX}elif", 

265 f"{CONDITION_PREFIX}else", 

266 f"{CONDITION_PREFIX}for", 

267 ], 

268 "py-if": [ 

269 "py-if", 

270 "py-elif", 

271 "py-else", 

272 "py-for", 

273 f"{CONDITION_PREFIX}if", 

274 f"{CONDITION_PREFIX}elif", 

275 f"{CONDITION_PREFIX}else", 

276 f"{CONDITION_PREFIX}for", 

277 ], 

278 "py-elif": ["py-if", "py-elif", f"{CONDITION_PREFIX}if", f"{CONDITION_PREFIX}elif"], 

279 "py-else": ["py-if", "py-elif", f"{CONDITION_PREFIX}if", f"{CONDITION_PREFIX}elif"], 

280 f"{CONDITION_PREFIX}for": [ 

281 "py-if", 

282 "py-elif", 

283 "py-else", 

284 "py-for", 

285 f"{CONDITION_PREFIX}if", 

286 f"{CONDITION_PREFIX}elif", 

287 f"{CONDITION_PREFIX}else", 

288 f"{CONDITION_PREFIX}for", 

289 ], 

290 f"{CONDITION_PREFIX}if": [ 

291 "py-if", 

292 "py-elif", 

293 "py-else", 

294 "py-for", 

295 f"{CONDITION_PREFIX}if", 

296 f"{CONDITION_PREFIX}elif", 

297 f"{CONDITION_PREFIX}else", 

298 f"{CONDITION_PREFIX}for", 

299 ], 

300 f"{CONDITION_PREFIX}elif": [ 

301 "py-if", 

302 "py-elif", 

303 f"{CONDITION_PREFIX}if", 

304 f"{CONDITION_PREFIX}elif", 

305 ], 

306 f"{CONDITION_PREFIX}else": [ 

307 "py-if", 

308 "py-elif", 

309 f"{CONDITION_PREFIX}if", 

310 f"{CONDITION_PREFIX}elif", 

311 ], 

312 } 

313 

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

315 first_cond = False 

316 

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

318 previous = (f"{CONDITION_PREFIX}for", True) 

319 

320 # Add the python blocks locals to kwargs dict 

321 kwargs.update(virtual_python.locals) 

322 

323 # Bring python blocks imports into scope 

324 for imp in virtual_python.imports: 

325 exec(str(imp)) # pylint: disable=exec-used 

326 

327 # For each element with a python condition 

328 for condition, child in cond: 

329 if condition in ["py-for", f"{CONDITION_PREFIX}for"]: 

330 

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

332 

333 previous = (f"{CONDITION_PREFIX}for", False) 

334 

335 # End any condition branch 

336 first_cond = False 

337 

338 elif condition in ["py-if", f"{CONDITION_PREFIX}if"]: 

339 previous = run_py_if(child, condition, children, **kwargs) 

340 

341 # Start of condition branch 

342 first_cond = True 

343 

344 elif condition in ["py-elif", f"{CONDITION_PREFIX}elif"]: 

345 # Can only exist if previous condition in branch failed 

346 previous = run_py_elif( 

347 child, 

348 children, 

349 condition, 

350 { 

351 "previous": previous, 

352 "valid_prev": valid_prev, 

353 "first_cond": first_cond, 

354 }, 

355 **kwargs, 

356 ) 

357 elif condition in ["py-else", f"{CONDITION_PREFIX}else"]: 

358 

359 # Can only exist if previous condition in branch failed 

360 previous = run_py_else( 

361 child, 

362 children, 

363 condition, 

364 { 

365 "previous": previous, 

366 "valid_prev": valid_prev, 

367 "first_cond": first_cond, 

368 }, 

369 ) 

370 

371 # End any condition branch 

372 first_cond = False 

373 

374 return children 

375 

376 

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

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

379 the passed kwargs. 

380 """ 

381 

382 clocals = {**kwargs} 

383 

384 # Inherit locals from top down 

385 for parent in path(child): 

386 if parent.type == "element": 

387 clocals.update(parent.locals) 

388 

389 clocals.update(child.locals) 

390 return clocals 

391 

392 

393def run_py_if(child: Element, condition: str, children: list, **kwargs): 

394 """Run the logic for manipulating the children on a `if` condition.""" 

395 

396 clocals = build_locals(child, **kwargs) 

397 result = get_vp_result(sub(r"\{|\}", "", child[condition].strip()), **clocals) 

398 

399 if result: 

400 del child[condition] 

401 return (f"{CONDITION_PREFIX}if", True) 

402 

403 # Condition failed, so remove the node 

404 children.remove(child) 

405 return (f"{CONDITION_PREFIX}if", False) 

406 

407 

408def run_py_elif( 

409 child: Element, 

410 children: list, 

411 condition: str, 

412 variables: dict, 

413 **kwargs, 

414): 

415 """Run the logic for manipulating the children on a `elif` condition.""" 

416 

417 clocals = build_locals(child, **kwargs) 

418 

419 if variables["previous"][0] in variables["valid_prev"][condition] and variables["first_cond"]: 

420 if not variables["previous"][1]: 

421 result = get_vp_result(sub(r"\{|\}", "", child[condition].strip()), **clocals) 

422 if result: 

423 del child[condition] 

424 return (f"{CONDITION_PREFIX}elif", True) 

425 

426 children.remove(child) 

427 return variables["previous"] 

428 

429 

430def run_py_else(child: Element, children: list, condition: str, variables: dict): 

431 """Run the logic for manipulating the children on a `else` condition.""" 

432 

433 if variables["previous"][0] in variables["valid_prev"][condition] and variables["first_cond"]: 

434 if not variables["previous"][1]: 

435 del child[condition] 

436 return (f"{CONDITION_PREFIX}else", True) 

437 

438 # Condition failed so remove element 

439 children.remove(child) 

440 return (f"{CONDITION_PREFIX}else", False) 

441 

442 

443def run_py_for(condition: str, child: All_Nodes, children: list, **kwargs) -> list: 

444 """Take a for loop condition, child node, and the list of children and 

445 generate new nodes. 

446 

447 Nodes are duplicates from the child node with variables provided 

448 from the for loop and child's locals. 

449 """ 

450 clocals = build_locals(child) 

451 

452 # Format for loop condition 

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

454 

455 # Get local var names from for loop condition 

456 new_locals = [ 

457 item.strip() 

458 for item in sub( 

459 r"\s+", 

460 " ", 

461 match(r"(for )?(.*)in", for_loop).group(2), 

462 ).split(",") 

463 ] 

464 

465 # Formatter for key value pairs 

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

467 

468 # Start index on where to insert generated children 

469 insert = children.index(child) 

470 

471 # Construct dynamic for loop 

472 # Uses for loop condition from above 

473 # Creates deepcopy of looped element 

474 # Adds locals from what was passed to exec and what is from for loop condition 

475 # concat and generate new list of children 

476 for_loop = f'''\ 

477new_children = [] 

478for {for_loop}: 

479 new_child = deepcopy(child) 

480 new_child.locals = {{{", ".join([f"{key_value.format(key=key)}" for key in new_locals])}, **local_vals}} 

481 children = [*children[:insert], new_child, *children[insert+1:]] 

482 insert += 1\ 

483''' 

484 

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

486 

487 # Delete the py-for condition from child's props 

488 del child[condition] 

489 # Set the position to None since the copies are generated 

490 child.position = None 

491 

492 # Construct locals for dynamic for loops execution 

493 local_env = { 

494 "children": children, 

495 "insert": insert, 

496 "child": child, 

497 "local_vals": clocals, 

498 **kwargs, 

499 } 

500 

501 # Execute dynamic for loop 

502 exec( # pylint: disable=exec-used 

503 for_loop, 

504 {**globals()}, 

505 local_env, 

506 ) 

507 

508 # Return the new complete list of children after generation 

509 return local_env["children"]