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:

  1. 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>
  1. 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>
  1. 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"]
class VirtualPython:
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.

VirtualPython( content: Optional[str] = None, imports: Optional[list] = None, local_env: Optional[dict] = None)
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
def get_vp_result(expr: str, **kwargs) -> Any:
 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.

def process_vp_blocks( pvb_value: str, virtual_python: phml.virtual_python.VirtualPython, **kwargs) -> str:
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.

class Import(phml.virtual_python.import_objects.PythonImport):
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.

Import(modules: list[str])
18    def __init__(self, modules: list[str]):
19        super().__init__()
20        self.modules = modules
@classmethod
def from_node(cls, imp) -> phml.virtual_python.Import:
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.

class ImportFrom(phml.virtual_python.import_objects.PythonImport):
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.

ImportFrom(module: str, names: list[str])
46    def __init__(self, module: str, names: list[str]):
47        super().__init__()
48        self.module = module
49        self.names = names
@classmethod
def from_node(cls, imp) -> phml.virtual_python.Import:
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.