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

85 statements  

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

1"""phml.virtual_python 

2 

3Data strucutures to store the compiled locals and imports from 

4python source code. 

5""" 

6from __future__ import annotations 

7 

8from ast import Assign, Name, parse, walk 

9from re import finditer, sub, MULTILINE 

10from typing import Any, Optional 

11 

12from .built_in import built_in_funcs 

13from .import_objects import Import, ImportFrom 

14 

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

16 

17 

18class VirtualPython: 

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

20 with the locals. 

21 """ 

22 

23 def __init__( 

24 self, 

25 content: Optional[str] = None, 

26 imports: Optional[list] = None, 

27 local_env: Optional[dict] = None, 

28 ): 

29 self.content = content or "" 

30 self.imports = imports or [] 

31 self.locals = local_env or {} 

32 

33 if self.content != "": 

34 import ast # pylint: disable=import-outside-toplevel 

35 

36 self.__normalize_indent() 

37 

38 # Extract imports from content 

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

40 if isinstance(node, ast.ImportFrom): 

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

42 elif isinstance(node, ast.Import): 

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

44 

45 # Retreive locals from content 

46 exec(self.content, globals(), self.locals) # pylint: disable=exec-used 

47 

48 def __normalize_indent(self): 

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

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

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

52 joiner = "\n" 

53 self.content = joiner.join(lines) 

54 

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

56 local_env = {**self.locals} 

57 local_env.update(obj.locals) 

58 return VirtualPython( 

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

60 local_env=local_env, 

61 ) 

62 

63 def __repr__(self) -> str: 

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

65 

66 

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

68 """Parse an ast.Assign node.""" 

69 

70 values = vals[0] 

71 if isinstance(values, Name): 

72 return [values.id] 

73 

74 if isinstance(values, tuple): 

75 return [name.id for name in values] 

76 

77 return [] 

78 

79 

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

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

82 the kwargs as the local variables. 

83 

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

85 """ 

86 

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

88 # Find all assigned vars in expression 

89 avars = [] 

90 assignment = None 

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

92 if isinstance(assign, Assign): 

93 assignment = parse_ast_assign(assign.targets) 

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

95 

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

97 used_vars = [ 

98 name.id 

99 for name in walk(parse(expr)) 

100 if isinstance(name, Name) and name.id not in avars and name.id not in built_in_funcs 

101 ] 

102 

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

104 for var in used_vars: 

105 if var not in kwargs: 

106 kwargs[var] = None 

107 

108 source = compile(f"{expr}\n", f"{expr}", "exec") 

109 exec(source, globals(), kwargs) # pylint: disable=exec-used 

110 # Get the last assignment and use it as the result 

111 return kwargs[assignment[-1]] 

112 

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

114 for var in [name.id for name in walk(parse(expr)) if isinstance(name, Name)]: 

115 if var not in kwargs: 

116 kwargs[var] = None 

117 

118 source = compile(f"phml_vp_result = {expr}", expr, "exec") 

119 exec(source, globals(), kwargs) # pylint: disable=exec-used 

120 return kwargs["phml_vp_result"] if "phml_vp_result" in kwargs else None 

121 

122 

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

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

125 This method also handles multiline strings, 

126 strings with `\\n` 

127 

128 Note: 

129 phml python blocks/expressions are indicated 

130 with curly brackets, {}. 

131 """ 

132 results = [] 

133 

134 for expression in finditer(r"\{[^}]+\}", data): 

135 expression = expression.group().lstrip("{").rstrip("}") 

136 expression = [expr for expr in expression.split("\n") if expr.strip() != ""] 

137 if len(expression) > 1: 

138 offset = len(expression[0]) - len(expression[0].lstrip()) 

139 input([line[offset:] for line in expression]) 

140 lines = [line[offset:] for line in expression] 

141 results.append("\n".join(lines)) 

142 else: 

143 results.append(expression[0]) 

144 

145 return results 

146 

147 

148def process_vp_blocks(pvb_value: str, virtual_python: VirtualPython, **kwargs) -> str: 

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

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

151 VirtualPython imports in this methods scope. 

152 

153 Args: 

154 value (str): The line to process. 

155 virtual_python (VirtualPython): Parsed locals and imports from all python blocks. 

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

157 

158 Returns: 

159 str: The processed line as str. 

160 """ 

161 

162 # Bring vp imports into scope 

163 for imp in virtual_python.imports: 

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

165 

166 expressions = extract_expressions(pvb_value) 

167 kwargs.update(virtual_python.locals) 

168 if expressions is not None: 

169 for expr in expressions: 

170 result = get_vp_result(expr, **kwargs) 

171 if isinstance(result, bool): 

172 pvb_value = result 

173 else: 

174 pvb_value = sub(r"\{[^}]+\}", str(result), pvb_value, 1) 

175 

176 return pvb_value