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:

  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_python_result, process_python_blocks
55
56__all__ = ["VirtualPython", "get_python_result", "process_python_blocks", "Import", "ImportFrom"]
class VirtualPython:
 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.

VirtualPython( content: Optional[str] = None, imports: Optional[list] = None, context: Optional[dict] = None, *, file_name: Optional[str] = None)
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)
def get_python_context(self, source: str, context: dict) -> dict[str, typing.Any]:
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.

def get_python_result(expr: str, **kwargs) -> Any:
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.

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

class Import(phml.core.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.core.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.core.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.core.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.