phml.core.virtual_python
Virtual Python
This module serves to solve the problem of processing python in scopes and to evaluate python expressions.
These expressions and scopes are python "blocks" that are injected into html which then creates my language phml.
Here are examples of the python blocks:
- Python element. This is treated as python files similarly to how
<script>
elements are treated as javascript files.
<python>
from datetime import datetime
current_time = datetime.now().strftime('%H:%M:%S')
</python>
- Inline python block. Mainly used for retreiving values or creating conditions. The local variables in the blocks are given from the python elements and from kwargs passed to the parser
<p>{current_time}</p>
- Multiline python blocks. Same as inline python blocks just that they take up multiple lines. You can write more logic in these blocks, but there local variables are not retained. By default phml will return the last local variable similar to how Jupyter or the python in cli works.
<p>
Hello, everyone my name is {firstname}. I
am a {work_position}.
<p>
<p>Here is a list of people and what they like</p>
<p>
{
result = []
for i, person, like in enumerate(zip(people, likes)):
result.append(f"{i}. {person} likes {like}")
result = "\n".join(result)
}
</p>
1# pylint: skip-file 2'''Virtual Python 3 4This module serves to solve the problem of processing python 5in scopes and to evaluate python expressions. 6 7These expressions and scopes are python "blocks" that are injected 8into html which then creates my language phml. 9 10Here are examples of the python blocks: 11 121. Python element. This is treated as python files similarly to how 13`<script>` elements are treated as javascript files. 14 15```html 16<python> 17 from datetime import datetime 18 19 current_time = datetime.now().strftime('%H:%M:%S') 20</python> 21``` 22 232. Inline python block. Mainly used for retreiving values 24or creating conditions. The local variables in the blocks are given 25from the python elements and from kwargs passed to the parser 26 27```html 28<p>{current_time}</p> 29``` 30 313. Multiline python blocks. Same as inline python blocks just that they 32take up multiple lines. You can write more logic in these blocks, but 33there local variables are not retained. By default phml will return the last 34local variable similar to how Jupyter or the python in cli works. 35 36```html 37<p> 38 Hello, everyone my name is {firstname}. I 39 am a {work_position}. 40<p> 41<p>Here is a list of people and what they like</p> 42<p> 43 { 44 result = [] 45 for i, person, like in enumerate(zip(people, likes)): 46 result.append(f"{i}. {person} likes {like}") 47 result = "\\n".join(result) 48 } 49</p> 50``` 51''' 52 53from .import_objects import Import, ImportFrom 54from .vp import VirtualPython, get_python_result, process_python_blocks 55 56__all__ = ["VirtualPython", "get_python_result", "process_python_blocks", "Import", "ImportFrom"]
22class VirtualPython: 23 """Represents a python string. Extracts the imports along 24 with the locals. 25 """ 26 27 def __init__( 28 self, 29 content: Optional[str] = None, 30 imports: Optional[list] = None, 31 context: Optional[dict] = None, 32 *, 33 file_name: Optional[str] = None, 34 ): 35 self.content = content or "" 36 self.imports = imports or [] 37 self.context = context or {} 38 self.file_name = file_name or "" 39 40 if self.content != "": 41 42 self.content = normalize_indent(content) 43 44 # Extract imports from content 45 for node in ast.parse(self.content).body: 46 if isinstance(node, ast.ImportFrom): 47 self.imports.append(ImportFrom.from_node(node)) 48 elif isinstance(node, ast.Import): 49 self.imports.append(Import.from_node(node)) 50 51 # Extract context from python source with additional context 52 self.context = self.get_python_context(self.content, self.context) 53 54 def get_python_context(self, source: str, context: dict) -> dict[str, Any]: 55 """Get the locals built from the python source code string. 56 Splits the def's and classes into their own chunks and passes in 57 all other local context to allow for outer scope to be seen in inner scope. 58 """ 59 60 chunks = [[]] 61 lines = source.split("\n") 62 i = 0 63 64 # Split the python source code into chunks 65 # This is a way of exposing outer most scope to functions and classes 66 while i < len(lines): 67 if lines[i].startswith(("def","class")): 68 chunks.append([lines[i]]) 69 i+=1 70 while i < len(lines) and lines[i].startswith((" ", "\t")): 71 chunks[-1].append(lines[i]) 72 i+=1 73 chunks.append([]) 74 continue 75 76 chunks[-1].append(lines[i]) 77 i+=1 78 79 chunks = [compile("\n".join(chunk), self.file_name, "exec") for chunk in chunks] 80 local_env = dict(self.context) 81 82 # Process each chunk and build locals 83 for chunk in chunks: 84 exec(chunk, {**local_env}, local_env) 85 86 return local_env 87 88 89 def __add__(self, obj: VirtualPython) -> VirtualPython: 90 local_env = {**self.context} 91 local_imports = set(self.imports) 92 local_env.update(obj.context) 93 for _import in obj.imports: 94 local_imports.add(_import) 95 return VirtualPython( 96 imports=list(local_imports), 97 context=local_env, 98 ) 99 100 def __repr__(self) -> str: 101 return f"VP(imports: {len(self.imports)}, locals: {len(self.context.keys())})"
Represents a python string. Extracts the imports along with the locals.
27 def __init__( 28 self, 29 content: Optional[str] = None, 30 imports: Optional[list] = None, 31 context: Optional[dict] = None, 32 *, 33 file_name: Optional[str] = None, 34 ): 35 self.content = content or "" 36 self.imports = imports or [] 37 self.context = context or {} 38 self.file_name = file_name or "" 39 40 if self.content != "": 41 42 self.content = normalize_indent(content) 43 44 # Extract imports from content 45 for node in ast.parse(self.content).body: 46 if isinstance(node, ast.ImportFrom): 47 self.imports.append(ImportFrom.from_node(node)) 48 elif isinstance(node, ast.Import): 49 self.imports.append(Import.from_node(node)) 50 51 # Extract context from python source with additional context 52 self.context = self.get_python_context(self.content, self.context)
54 def get_python_context(self, source: str, context: dict) -> dict[str, Any]: 55 """Get the locals built from the python source code string. 56 Splits the def's and classes into their own chunks and passes in 57 all other local context to allow for outer scope to be seen in inner scope. 58 """ 59 60 chunks = [[]] 61 lines = source.split("\n") 62 i = 0 63 64 # Split the python source code into chunks 65 # This is a way of exposing outer most scope to functions and classes 66 while i < len(lines): 67 if lines[i].startswith(("def","class")): 68 chunks.append([lines[i]]) 69 i+=1 70 while i < len(lines) and lines[i].startswith((" ", "\t")): 71 chunks[-1].append(lines[i]) 72 i+=1 73 chunks.append([]) 74 continue 75 76 chunks[-1].append(lines[i]) 77 i+=1 78 79 chunks = [compile("\n".join(chunk), self.file_name, "exec") for chunk in chunks] 80 local_env = dict(self.context) 81 82 # Process each chunk and build locals 83 for chunk in chunks: 84 exec(chunk, {**local_env}, local_env) 85 86 return local_env
Get the locals built from the python source code string. Splits the def's and classes into their own chunks and passes in all other local context to allow for outer scope to be seen in inner scope.
144def get_python_result(expr: str, **kwargs) -> Any: 145 """Execute the given python expression, while using 146 the kwargs as the global variables. 147 148 This will collect the result of the expression and return it. 149 """ 150 from phml.utilities import ( # pylint: disable=import-outside-toplevel,unused-import 151 blank, 152 classnames, 153 ) 154 155 # Data being passed is concidered to be safe and shouldn't be sanatized 156 safe_vars = kwargs.pop("safe_vars", None) or False 157 158 # Global utilities provided by phml 159 kwargs.update({"classnames": classnames, "blank": blank}) 160 161 avars = [] 162 result = "phml_vp_result" 163 expression = f"{result} = {expr}\n" 164 165 if "\n" in expr: 166 # Find all assigned vars in expression 167 avars = [] 168 assignment = None 169 for assign in ast.walk(ast.parse(expr)): 170 if isinstance(assign, ast.Assign): 171 assignment = parse_ast_assign(assign.targets) 172 avars.extend(parse_ast_assign(assign.targets)) 173 174 result = assignment[-1] 175 expression = f"{expr}\n" 176 177 # validate kwargs and replace missing variables with None 178 __validate_kwargs(kwargs, expr, safe_vars=safe_vars) 179 180 try: 181 # Compile and execute python source 182 source = compile(expression, expr, "exec") 183 184 local_env = {**kwargs} 185 global_env = {**kwargs} 186 exec(source, global_env, local_env) # pylint: disable=exec-used 187 return local_env[result] if result in local_env else None 188 except Exception as exception: # pylint: disable=broad-except 189 from saimll import SAIML # pylint: disable=import-outside-toplevel 190 191 # print_exc() 192 SAIML.print(f"[@F red]*Error[]: [$]{exception}: {expr}") 193 194 return False
Execute the given python expression, while using the kwargs as the global variables.
This will collect the result of the expression and return it.
259def process_python_blocks(python_value: str, virtual_python: VirtualPython, **kwargs) -> str: 260 """Process a lines python blocks. Use the VirtualPython locals, 261 and kwargs as local variables for each python block. Import 262 VirtualPython imports in this methods scope. 263 264 Args: 265 value (str): The line to process. 266 virtual_python (VirtualPython): Parsed locals and imports from all python blocks. 267 **kwargs (Any): The extra data to pass to the exec function. 268 269 Returns: 270 str: The processed line as str. 271 """ 272 273 # Bring vp imports into scope 274 for imp in virtual_python.imports: 275 exec(str(imp)) # pylint: disable=exec-used 276 277 expressions = extract_expressions(python_value) 278 kwargs.update(virtual_python.context) 279 for idx, expression in enumerate(expressions): 280 if isinstance(expression, PythonBlock): 281 expressions[idx] = expression.exec(**kwargs) 282 if isinstance(expressions[idx], bool): 283 return expressions[idx] 284 285 return "".join([str(expression) for expression in expressions])
Process a lines python blocks. Use the VirtualPython locals, and kwargs as local variables for each python block. Import VirtualPython imports in this methods scope.
Arguments:
- value (str): The line to process.
- virtual_python (VirtualPython): Parsed locals and imports from all python blocks.
- **kwargs (Any): The extra data to pass to the exec function.
Returns:
str: The processed line as str.
13class Import(PythonImport): 14 """Helper object that stringifies the python ast Import. 15 This is mainly to locally import things dynamically. 16 """ 17 18 def __init__(self, modules: list[str]): 19 super().__init__() 20 self.modules = modules 21 22 @classmethod 23 def from_node(cls, imp) -> Import: 24 """Generates a new import object from a python ast Import. 25 26 Args: 27 imp (ast.Import): Python ast object 28 29 Returns: 30 Import: A new import object. 31 """ 32 return Import([alias.name for alias in imp.names]) 33 34 def __repr__(self) -> str: 35 return f"Import(modules=[{', '.join(self.modules)}])" 36 37 def __str__(self) -> str: 38 return f"import {', '.join(self.modules)}"
Helper object that stringifies the python ast Import. This is mainly to locally import things dynamically.
22 @classmethod 23 def from_node(cls, imp) -> Import: 24 """Generates a new import object from a python ast Import. 25 26 Args: 27 imp (ast.Import): Python ast object 28 29 Returns: 30 Import: A new import object. 31 """ 32 return Import([alias.name for alias in imp.names])
Generates a new import object from a python ast Import.
Arguments:
- imp (ast.Import): Python ast object
Returns:
Import: A new import object.
41class ImportFrom(PythonImport): 42 """Helper object that stringifies the python ast ImportFrom. 43 This is mainly to locally import things dynamically. 44 """ 45 46 def __init__(self, module: str, names: list[str]): 47 super().__init__() 48 self.module = module 49 self.names = names 50 51 @classmethod 52 def from_node(cls, imp) -> Import: 53 """Generates a new import object from a python ast Import. 54 55 Args: 56 imp (ast.Import): Python ast object 57 58 Returns: 59 Import: A new import object. 60 """ 61 return ImportFrom(imp.module, [alias.name for alias in imp.names]) 62 63 def __repr__(self) -> str: 64 return f"ImportFrom(module='{self.module}', names=[{', '.join(self.names)}])" 65 66 def __str__(self) -> str: 67 return f"from {self.module} import {', '.join(self.names)}"
Helper object that stringifies the python ast ImportFrom. This is mainly to locally import things dynamically.
51 @classmethod 52 def from_node(cls, imp) -> Import: 53 """Generates a new import object from a python ast Import. 54 55 Args: 56 imp (ast.Import): Python ast object 57 58 Returns: 59 Import: A new import object. 60 """ 61 return ImportFrom(imp.module, [alias.name for alias in imp.names])
Generates a new import object from a python ast Import.
Arguments:
- imp (ast.Import): Python ast object
Returns:
Import: A new import object.