Coverage for phml\virtual_python\vp.py: 48%

89 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-30 09:38 -0600

1from __future__ import annotations 

2 

3from ast import Assign, Name, parse, walk 

4from typing import Any, Optional 

5 

6from .ImportObjects import Import, ImportFrom 

7 

8__all__ = ["VirtualPython", "get_vp_result", "process_vp_blocks"] 

9 

10 

11class VirtualPython: 

12 """Represents a python string. Extracts the imports along 

13 with the locals. 

14 """ 

15 

16 def __init__( 

17 self, 

18 content: Optional[str] = None, 

19 imports: Optional[list] = None, 

20 local_env: Optional[dict] = None, 

21 ): 

22 self.content = content or "" 

23 self.imports = imports or [] 

24 self.locals = local_env or {} 

25 

26 if self.content != "": 

27 import ast 

28 

29 self.__normalize_indent() 

30 

31 # Extract imports from content 

32 for node in ast.parse(self.content).body: 

33 if isinstance(node, ast.ImportFrom): 

34 self.imports.append(ImportFrom.from_node(node)) 

35 elif isinstance(node, ast.Import): 

36 self.imports.append(Import.from_node(node)) 

37 

38 # Retreive locals from content 

39 exec(self.content, globals(), self.locals) 

40 

41 def __normalize_indent(self): 

42 self.content = self.content.split("\n") 

43 offset = len(self.content[0]) - len(self.content[0].lstrip()) 

44 lines = [line[offset:] for line in self.content] 

45 joiner = "\n" 

46 self.content = joiner.join(lines) 

47 

48 def __add__(self, obj: VirtualPython) -> VirtualPython: 

49 local_env = {**self.locals} 

50 local_env.update(obj.locals) 

51 return VirtualPython( 

52 imports=[*self.imports, *obj.imports], 

53 local_env=local_env, 

54 ) 

55 

56 def __repr__(self) -> str: 

57 return f"VP(imports: {len(self.imports)}, locals: {len(self.locals.keys())})" 

58 

59 

60def parse_ast_assign(vals: list[Name | tuple[Name]]) -> list[str]: 

61 values = vals[0] 

62 if isinstance(values, Name): 

63 return [values.id] 

64 elif isinstance(values, tuple): 

65 return [name.id for name in values] 

66 

67 

68def get_vp_result(expr: str, **kwargs) -> Any: 

69 """Execute the given python expression, while using 

70 the kwargs as the local variables. 

71 

72 This will collect the result of the expression and return it. 

73 """ 

74 # Find all assigned vars in expression 

75 avars = [] 

76 for assign in walk(parse(expr)): 

77 if isinstance(assign, Assign): 

78 avars.extend(parse_ast_assign(assign.targets)) 

79 

80 # Find all variables being used that are not are not assigned 

81 used_vars = [ 

82 name.id for name in walk(parse(expr)) if isinstance(name, Name) and name.id not in avars 

83 ] 

84 

85 # For all variables used if they are not in kwargs then they == None 

86 for uv in used_vars: 

87 if uv not in kwargs: 

88 kwargs[uv] = None 

89 

90 if len(expr.split("\n")) > 1: 

91 exec(expr, {}, kwargs) 

92 return kwargs["result"] or kwargs["results"] 

93 else: 

94 try: 

95 exec(f"phml_vp_result = {expr}", {}, kwargs) 

96 return kwargs["phml_vp_result"] 

97 except NameError as e: 

98 print(e, expr, kwargs) 

99 

100 

101def extract_expressions(data: str) -> str: 

102 """Extract a phml python expr from a string. 

103 This method also handles multiline strings, 

104 strings with `\\n` 

105 

106 Note: 

107 phml python blocks/expressions are indicated 

108 with curly brackets, {}. 

109 """ 

110 from re import findall 

111 

112 expressions = findall(r"\{(.*)\}", data) 

113 if expressions is not None: 

114 for expression in expressions: 

115 expression = expression.lstrip("{").rstrip("}") 

116 expr = expression.split("\n") 

117 if len(expr) > 1: 

118 offset = len(expr[0]) - len(expr[0].lstrip()) 

119 lines = [line[offset:] for line in expr] 

120 joiner = "\n" 

121 expression = joiner.join(lines) 

122 else: 

123 expression = expr[0] 

124 return expressions 

125 

126 

127def process_vp_blocks(line: str, vp: VirtualPython, **kwargs) -> str: 

128 """Process a lines python blocks. Use the VirtualPython locals, 

129 and kwargs as local variables for each python block. Import 

130 VirtualPython imports in this methods scope. 

131 

132 Args: 

133 line (str): The line to process 

134 **kwargs (Any): The extra data to pass to the exec function 

135 

136 Returns: 

137 str: The processed line as str. 

138 """ 

139 from re import sub 

140 

141 # Bring vp imports into scope 

142 for imp in vp.imports: 

143 exec(str(imp)) 

144 

145 expr = extract_expressions(line) 

146 kwargs.update(vp.locals) 

147 if expr is not None: 

148 for e in expr: 

149 result = get_vp_result(e, **kwargs) 

150 if isinstance(result, bool): 

151 line = result # sub(r"\{.*\}", "yes" if result else "no", line) 

152 else: 

153 line = sub(r"\{.*\}", str(result), line) 

154 

155 return line 

156 

157 

158if __name__ == "__main__": 

159 # Extra data to similuate extra info a user may pass 

160 date = "11/14/2022" 

161 

162 with open("sample_python.txt", "r") as sample_python: 

163 # Extract python and process as vp 

164 vp = VirtualPython(sample_python.read()) 

165 

166 # Read source file and export to output file 

167 with open("sample.phml", "r", encoding="utf-8") as source_file: 

168 with open("output.html", "+w", encoding="utf-8") as out_file: 

169 # If line has a inline python block process it 

170 for line in source_file.readlines(): 

171 out_file.write(process_vp_blocks(line, vp, date=date))