phml.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_vp_result, process_vp_blocks 55 56__all__ = ["VirtualPython", "get_vp_result", "process_vp_blocks", "Import", "ImportFrom"]
19class VirtualPython: 20 """Represents a python string. Extracts the imports along 21 with the locals. 22 """ 23 24 def __init__( 25 self, 26 content: Optional[str] = None, 27 imports: Optional[list] = None, 28 local_env: Optional[dict] = None, 29 ): 30 self.content = content or "" 31 self.imports = imports or [] 32 self.locals = local_env or {} 33 34 if self.content != "": 35 self.__normalize_indent() 36 37 # Extract imports from content 38 for node in ast.parse(self.content).body: 39 if isinstance(node, ast.ImportFrom): 40 self.imports.append(ImportFrom.from_node(node)) 41 elif isinstance(node, ast.Import): 42 self.imports.append(Import.from_node(node)) 43 44 # Retreive locals from content 45 exec(self.content, globals(), self.locals) # pylint: disable=exec-used 46 47 def __normalize_indent(self): 48 self.content = self.content.split("\n") 49 offset = len(self.content[0]) - len(self.content[0].lstrip()) 50 lines = [line[offset:] for line in self.content] 51 joiner = "\n" 52 self.content = joiner.join(lines) 53 54 def __add__(self, obj: VirtualPython) -> VirtualPython: 55 local_env = {**self.locals} 56 local_env.update(obj.locals) 57 return VirtualPython( 58 imports=[*self.imports, *obj.imports], 59 local_env=local_env, 60 ) 61 62 def __repr__(self) -> str: 63 return f"VP(imports: {len(self.imports)}, locals: {len(self.locals.keys())})"
Represents a python string. Extracts the imports along with the locals.
24 def __init__( 25 self, 26 content: Optional[str] = None, 27 imports: Optional[list] = None, 28 local_env: Optional[dict] = None, 29 ): 30 self.content = content or "" 31 self.imports = imports or [] 32 self.locals = local_env or {} 33 34 if self.content != "": 35 self.__normalize_indent() 36 37 # Extract imports from content 38 for node in ast.parse(self.content).body: 39 if isinstance(node, ast.ImportFrom): 40 self.imports.append(ImportFrom.from_node(node)) 41 elif isinstance(node, ast.Import): 42 self.imports.append(Import.from_node(node)) 43 44 # Retreive locals from content 45 exec(self.content, globals(), self.locals) # pylint: disable=exec-used
79def get_vp_result(expr: str, **kwargs) -> Any: 80 """Execute the given python expression, while using 81 the kwargs as the local variables. 82 83 This will collect the result of the expression and return it. 84 """ 85 from phml.utils import ( # pylint: disable=import-outside-toplevel,unused-import 86 ClassList, 87 blank, 88 classnames, 89 ) 90 91 kwargs.update({"classnames": classnames, "blank": blank}) 92 93 if len(expr.split("\n")) > 1: 94 # Find all assigned vars in expression 95 avars = [] 96 assignment = None 97 for assign in ast.walk(ast.parse(expr)): 98 if isinstance(assign, ast.Assign): 99 assignment = parse_ast_assign(assign.targets) 100 avars.extend(parse_ast_assign(assign.targets)) 101 102 # Find all variables being used that are not are not assigned 103 used_vars = [ 104 name.id 105 for name in ast.walk(ast.parse(expr)) 106 if isinstance(name, ast.Name) and name.id not in avars and name.id not in built_in_funcs 107 ] 108 109 # For all variables used if they are not in kwargs then they == None 110 for var in used_vars: 111 if var not in kwargs: 112 kwargs[var] = None 113 114 source = compile(f"{expr}\n", f"{expr}", "exec") 115 exec(source, globals(), kwargs) # pylint: disable=exec-used 116 # Get the last assignment and use it as the result 117 return kwargs[assignment[-1]] 118 119 # For all variables used if they are not in kwargs then they == None 120 for var in [name.id for name in ast.walk(ast.parse(expr)) if isinstance(name, ast.Name)]: 121 if var not in kwargs: 122 kwargs[var] = None 123 124 source = compile(f"phml_vp_result = {expr}", expr, "exec") 125 exec(source, globals(), kwargs) # pylint: disable=exec-used 126 return kwargs["phml_vp_result"] if "phml_vp_result" in kwargs else None
Execute the given python expression, while using the kwargs as the local variables.
This will collect the result of the expression and return it.
154def process_vp_blocks(pvb_value: str, virtual_python: VirtualPython, **kwargs) -> str: 155 """Process a lines python blocks. Use the VirtualPython locals, 156 and kwargs as local variables for each python block. Import 157 VirtualPython imports in this methods scope. 158 159 Args: 160 value (str): The line to process. 161 virtual_python (VirtualPython): Parsed locals and imports from all python blocks. 162 **kwargs (Any): The extra data to pass to the exec function. 163 164 Returns: 165 str: The processed line as str. 166 """ 167 168 # Bring vp imports into scope 169 for imp in virtual_python.imports: 170 exec(str(imp)) # pylint: disable=exec-used 171 172 expressions = extract_expressions(pvb_value) 173 kwargs.update(virtual_python.locals) 174 if expressions is not None: 175 for expr in expressions: 176 result = get_vp_result(expr, **kwargs) 177 if isinstance(result, bool): 178 pvb_value = result 179 else: 180 pvb_value = sub(r"\{[^}]+\}", str(result), pvb_value, 1) 181 182 return pvb_value
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.