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
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-08 16:56 -0600
1"""phml.virtual_python
3Data strucutures to store the compiled locals and imports from
4python source code.
5"""
6from __future__ import annotations
8from ast import Assign, Name, parse, walk
9from re import finditer, sub, MULTILINE
10from typing import Any, Optional
12from .built_in import built_in_funcs
13from .import_objects import Import, ImportFrom
15__all__ = ["VirtualPython", "get_vp_result", "process_vp_blocks"]
18class VirtualPython:
19 """Represents a python string. Extracts the imports along
20 with the locals.
21 """
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 {}
33 if self.content != "":
34 import ast # pylint: disable=import-outside-toplevel
36 self.__normalize_indent()
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))
45 # Retreive locals from content
46 exec(self.content, globals(), self.locals) # pylint: disable=exec-used
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)
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 )
63 def __repr__(self) -> str:
64 return f"VP(imports: {len(self.imports)}, locals: {len(self.locals.keys())})"
67def parse_ast_assign(vals: list[Name | tuple[Name]]) -> list[str]:
68 """Parse an ast.Assign node."""
70 values = vals[0]
71 if isinstance(values, Name):
72 return [values.id]
74 if isinstance(values, tuple):
75 return [name.id for name in values]
77 return []
80def get_vp_result(expr: str, **kwargs) -> Any:
81 """Execute the given python expression, while using
82 the kwargs as the local variables.
84 This will collect the result of the expression and return it.
85 """
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))
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 ]
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
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]]
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
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
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`
128 Note:
129 phml python blocks/expressions are indicated
130 with curly brackets, {}.
131 """
132 results = []
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])
145 return results
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.
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.
158 Returns:
159 str: The processed line as str.
160 """
162 # Bring vp imports into scope
163 for imp in virtual_python.imports:
164 exec(str(imp)) # pylint: disable=exec-used
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)
176 return pvb_value