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
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-30 09:38 -0600
1from __future__ import annotations
3from ast import Assign, Name, parse, walk
4from typing import Any, Optional
6from .ImportObjects import Import, ImportFrom
8__all__ = ["VirtualPython", "get_vp_result", "process_vp_blocks"]
11class VirtualPython:
12 """Represents a python string. Extracts the imports along
13 with the locals.
14 """
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 {}
26 if self.content != "":
27 import ast
29 self.__normalize_indent()
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))
38 # Retreive locals from content
39 exec(self.content, globals(), self.locals)
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)
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 )
56 def __repr__(self) -> str:
57 return f"VP(imports: {len(self.imports)}, locals: {len(self.locals.keys())})"
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]
68def get_vp_result(expr: str, **kwargs) -> Any:
69 """Execute the given python expression, while using
70 the kwargs as the local variables.
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))
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 ]
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
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)
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`
106 Note:
107 phml python blocks/expressions are indicated
108 with curly brackets, {}.
109 """
110 from re import findall
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
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.
132 Args:
133 line (str): The line to process
134 **kwargs (Any): The extra data to pass to the exec function
136 Returns:
137 str: The processed line as str.
138 """
139 from re import sub
141 # Bring vp imports into scope
142 for imp in vp.imports:
143 exec(str(imp))
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)
155 return line
158if __name__ == "__main__":
159 # Extra data to similuate extra info a user may pass
160 date = "11/14/2022"
162 with open("sample_python.txt", "r") as sample_python:
163 # Extract python and process as vp
164 vp = VirtualPython(sample_python.read())
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))