Source code for k1lib.kast

# AUTOGENERATED FILE! PLEASE DON'T EDIT HERE. EDIT THE SOURCE NOTEBOOKS INSTEAD
"""Small module with helper functions to analyze and manipulate Python abstract syntax tree"""
import k1lib, math, numpy as np, random, base64, json, time, ast; from typing import List, Iterator
from collections import defaultdict, deque; import k1lib.cli as cli
__all__ = ["walk", "pretty", "py2js", "asyncGuard"]
class JSTranspileError(Exception): pass                                          # JSTranspileError
def _walk(m, parent, accessor="", depth=0): # yields (elem, parent, parent accessor, depth). This function don't need to recurse leaf nodes, which is a pretty good time saver # _walk
    yield m, parent, accessor, depth                                             # _walk
    if isinstance(m, (ast.Module, ast.ClassDef, ast.FunctionDef)):               # _walk
        for expr in m.body: yield from _walk(expr, m, f".body[{len(m.body)}]", depth+1) # _walk
    elif isinstance(m, ast.Expr):                                                # _walk
        yield from _walk(m.value, m, ".value", depth+1)                          # _walk
    elif isinstance(m, ast.Lambda):                                              # _walk
        yield from _walk(m.body, m, ".body", depth+1)                            # _walk
    elif isinstance(m, ast.BinOp):                                               # _walk
        yield from _walk(m.left, m, ".left", depth+1)                            # _walk
        yield from _walk(m.op, m, ".op", depth+1)                                # _walk
        yield from _walk(m.right, m, ".right", depth+1)                          # _walk
    elif isinstance(m, ast.Assign):                                              # _walk
        yield from _walk(m.value, m, ".value", depth+1)                          # _walk
        for e in m.targets: yield from _walk(e, m, f".targets[{len(m.targets)}]", depth+1) # _walk
    elif isinstance(m, ast.If):                                                  # _walk
        yield from _walk(m.test, m, ".test", depth+1)                            # _walk
        for e in m.body: yield from _walk(e, m, f".body[{len(m.body)}]", depth+1) # _walk
        for e in m.orelse: yield from _walk(e, m, f".orelse[{len(m.orelse)}]", depth+1) # _walk
    elif isinstance(m, ast.Call):                                                # _walk
        yield from _walk(m.func, m, ".func", depth+1)                            # _walk
        for e in m.args: yield from _walk(e, m, f".args[{len(m.args)}]", depth+1) # _walk
        for e in m.keywords: yield from _walk(e, m, f".keywords[{len(m.keywords)}]", depth+1) # _walk
    elif isinstance(m, ast.Attribute):                                           # _walk
        yield from _walk(m.value, m, ".value", depth+1)                          # _walk
    elif isinstance(m, ast.UnaryOp):                                             # _walk
        yield from _walk(m.op, m, ".op", depth+1)                                # _walk
        yield from _walk(m.operand, m, ".operand", depth+1)                      # _walk
    elif isinstance(m, ast.Subscript):                                           # _walk
        yield from _walk(m.value, m, ".value", depth+1)                          # _walk
        yield from _walk(m.slice, m, ".slice", depth+1)                          # _walk
        yield from _walk(m.ctx, m, ".ctx", depth+1)                              # _walk
    # elif isinstance(m, ast.Store):                                             # _walk
    #     yield from _walk(m.parent, m, ".parent", depth+1)                      # _walk
    elif isinstance(m, ast.Compare):                                             # _walk
        yield from _walk(m.left, m, ".left", depth+1)                            # _walk
        for e in m.ops: yield from _walk(e, m, f".ops[{len(m.ops)}]", depth+1)   # _walk
        for e in m.comparators: yield from _walk(e, m, f".comparators[{len(m.comparators)}]", depth+1) # _walk
    elif isinstance(m, ast.List):                                                # _walk
        for e in m.elts: yield from _walk(e, m, f".elts[{len(m.elts)}]", depth+1) # _walk
    elif isinstance(m, ast.Tuple):                                               # _walk
        for e in m.elts: yield from _walk(e, m, f".elts[{len(m.elts)}]", depth+1) # _walk
    elif isinstance(m, ast.Set):                                                 # _walk
        for e in m.elts: yield from _walk(e, m, f".elts[{len(m.elts)}]", depth+1) # _walk
    elif isinstance(m, ast.comprehension):                                       # _walk
        yield from _walk(m.is_async, m, ".is_async", depth+1)                    # _walk
        yield from _walk(m.iter, m, ".iter", depth+1)                            # _walk
        yield from _walk(m.target, m, ".target", depth+1)                        # _walk
        for e in m.ifs: yield from _walk(e, m, f".ifs[{len(m.ifs)}]", depth+1)   # _walk
    elif isinstance(m, (ast.ListComp, ast.GeneratorExp, ast.SetComp)):           # _walk
        yield from _walk(m.elt, m, ".elt", depth+1)                              # _walk
        for e in m.generators: yield from _walk(e, m, f".generators[{len(m.generators)}]", depth+1) # _walk
    elif isinstance(m, ast.DictComp):                                            # _walk
        yield from _walk(m.key, m, ".key", depth+1)                              # _walk
        yield from _walk(m.value, m, ".value", depth+1)                          # _walk
        for e in m.generators: yield from _walk(e, m, f".generators[{len(m.generators)}]", depth+1) # _walk
    elif isinstance(m, ast.Starred):                                             # _walk
        yield from _walk(m.ctx, m, ".ctx", depth+1)                              # _walk
        yield from _walk(m.value, m, ".value", depth+1)                          # _walk
    elif isinstance(m, ast.keyword):                                             # _walk
        yield from _walk(m.value, m, ".value", depth+1)                          # _walk
    elif isinstance(m, ast.Dict):                                                # _walk
        for e in m.keys: yield from _walk(e, m, f".keys[{len(m.keys)}]", depth+1) # _walk
        for e in m.values: yield from _walk(e, m, f".values[{len(m.values)}]", depth+1) # _walk
    elif isinstance(m, ast.IfExp):                                               # _walk
        yield from _walk(m.body, m, ".body", depth+1)                            # _walk
        yield from _walk(m.test, m, ".test", depth+1)                            # _walk
        yield from _walk(m.orelse, m, ".orelse", depth+1)                        # _walk
    elif isinstance(m, ast.Slice):                                               # _walk
        yield from _walk(m.lower, m, ".lower", depth+1)                          # _walk
        yield from _walk(m.upper, m, ".upper", depth+1)                          # _walk
        yield from _walk(m.step, m, ".step", depth+1)                            # _walk
    elif isinstance(m, ast.FormattedValue):                                      # _walk
        yield from _walk(m.value, m, ".value", depth+1)                          # _walk
    elif isinstance(m, ast.JoinedStr):                                           # _walk
        for e in m.values: yield from _walk(e, m, f".values[{len(m.values)}]", depth+1) # _walk
[docs]class walk(cli.BaseCli): # walk
[docs] def __init__(self): # walk """Walks the AST tree, returning (elem, parent, parent accessor, depth) tuple. Example:: "abc.py" | cat() | join("\\n") | kast.walk() # returns generator of tuples """ # walk pass # walk
[docs] def __ror__(self, m:str): # walk if isinstance(m, str): m = ast.parse(m) # walk return _walk(m, None, "", 0) # walk
[docs]class pretty(cli.BaseCli): # pretty
[docs] def __init__(self): # pretty """Returns list[str] containing the prettified tree structure of the AST. Mainly for asthetic, not for parsing by other tools downstream. Example:: "x+3" | kast.pretty() "lambda x: x+3" | kast.pretty() """ # pretty pass # pretty
[docs] def __ror__(self, it:str): # pretty m = it; res = [] # pretty if isinstance(m, str): m = ast.parse(m) # pretty for m, parent, accessor, depth in _walk(m, None, "", 0): # pretty pad = " "*depth; res.append(f"{pad}{accessor} {m}") # pretty if isinstance(m, ast.arguments): res[-1] += f" {[a.arg for a in m.args]}" # pretty elif isinstance(m, ast.Import): res[-1] += f" .names[{len(m.names)}].name={', '.join([e.name for e in m.names])}" # pretty elif isinstance(m, ast.ImportFrom): res[-1] += f" .module={repr(m.module)} .names[{len(m.names)}].name={', '.join([e.name for e in m.names])}" # pretty elif isinstance(m, ast.BinOp): res[-1] += f" .op={m.op}" # pretty elif isinstance(m, ast.Lambda): res[-1] += f" .args={[a.arg for a in m.args.args]}" # pretty elif isinstance(m, (ast.ClassDef, ast.FunctionDef)): res[-1] += f" .name={repr(m.name)}" # pretty elif isinstance(m, ast.Name): res[-1] += f" .id={repr(m.id)}" # pretty elif isinstance(m, ast.Constant): res[-1] += f" .value={repr(m.value)}" # pretty elif isinstance(m, ast.Attribute): res[-1] += f" .attr={repr(m.attr)}" # pretty elif isinstance(m, ast.keyword): res[-1] += f" .arg={repr(m.arg)}" # pretty return res # pretty
jstCallDict = {} # pretty def _jstGuardNArgs(name, ns, f): # common guard across all short and simple functions # _jstGuardNArgs ns = set(ns) # _jstGuardNArgs if len(ns) == 0: jstCallDict[name] = f # _jstGuardNArgs else: # _jstGuardNArgs def inner(m, lcs): # _jstGuardNArgs if len(m.args) not in ns: raise JSTranspileError(f"Function `{name}` only support these number of arguments: {ns}. You have {len(m.args)} arguments total!") # _jstGuardNArgs return f(m, lcs) # _jstGuardNArgs jstCallDict[name] = inner # _jstGuardNArgs _jstGuardNArgs("abs", [1], lambda m, lcs: f"Math.abs({_py2js(m.args[0], lcs)})") # _jstGuardNArgs _jstGuardNArgs("all", [1], lambda m, lcs: f"{_py2js(m.args[0], lcs)}.every((x) => x)") # _jstGuardNArgs _jstGuardNArgs("any", [1], lambda m, lcs: f"{_py2js(m.args[0], lcs)}.some((x) => x)") # _jstGuardNArgs def _jst_divmod(m, lcs): # _jst_divmod if len(m.args) != 2: raise JSTranspileError(f"Only support divmod() with 2 arguments!") # _jst_divmod a = _py2js(m.args[0], lcs); b = _py2js(m.args[1], lcs) # _jst_divmod return f"[Math.floor(a/b), a-b*Math.floor(a/b)]" # _jst_divmod jstCallDict["divmod"] = _jst_divmod # _jst_divmod def _jst_enumerate(m, lcs): # _jst_enumerate if len(m.args) != 1: raise JSTranspileError("Currently don't support enumerate() with the start parameter") # _jst_enumerate return f"{_py2js(m.args[0], lcs)}.map((x, i) => [x, i])" # _jst_enumerate jstCallDict["enumerate"] = _jst_enumerate # _jst_enumerate _jstGuardNArgs("float", [1], lambda m, lcs: f"parseFloat({_py2js(m.args[0], lcs)})") # _jst_enumerate def _jst_getattr(m, lcs): # _jst_getattr a = _py2js(m.args[0], lcs); b = _py2js(m.args[1], lcs) # _jst_getattr if len(m.args) == 2: return f"{a}[{b}]" # _jst_getattr else: return f"{a}[{b}] ?? {_py2js(m.args[2], lcs)}" # _jst_getattr jstCallDict["getattr"] = _jst_getattr # _jst_getattr _jstGuardNArgs("hasattr", [2], lambda m, lcs: f"{_py2js(m.args[0], lcs)}[{_py2js(m.args[1], lcs)}] !== undefined") # _jst_getattr _jstGuardNArgs("int", [1], lambda m, lcs: f"parseInt({_py2js(m.args[0], lcs)})") # _jst_getattr _jstGuardNArgs("iter", [1], lambda m, lcs: f"Array.from({_py2js(m.args[0], lcs)})") # _jst_getattr _jstGuardNArgs("len", [1], lambda m, lcs: f"{_py2js(m.args[0], lcs)}.length") # _jst_getattr _jstGuardNArgs("list", [1], lambda m, lcs: f"Array.from({_py2js(m.args[0], lcs)})") # _jst_getattr def _jst_max(m, lcs): # _jst_max if len(m.args) == 0: raise JSTranspileError("Only support max() with 1 or more parameters") # _jst_max if len(m.args) == 1: return f"Math.max(...({_py2js(m.args[0], lcs)}))" # _jst_max return f"Math.max({', '.join([_py2js(e, lcs) for e in m.args])})" # _jst_max jstCallDict["max"] = _jst_max # _jst_max def _jst_min(m, lcs): # _jst_min if len(m.args) == 0: raise JSTranspileError("Only support min() with 1 or more parameters") # _jst_min if len(m.args) == 1: return f"Math.min(...({_py2js(m.args[0], lcs)}))" # _jst_min return f"Math.min({', '.join([_py2js(e, lcs) for e in m.args])})" # _jst_min jstCallDict["min"] = _jst_min # _jst_min _jstGuardNArgs("pow", [2], lambda m, lcs: f"Math.pow({_py2js(m.args[0], lcs)}, {_py2js(m.args[1], lcs)})") # _jst_min _jstGuardNArgs("print", [], lambda m, lcs: f"console.log({', '.join([_py2js(e, lcs) for e in m.args])})") # _jst_min def _jst_range(m, lcs): # _jst_range n = len(m.args) # _jst_range if n == 1: start = None; stop = m.args[0] # _jst_range elif n == 2: start, stop = m.args # _jst_range else: raise JSTranspileError("Only support range() without the step variable") # _jst_range ans = f"[...Array({_py2js(stop, lcs)}).keys()]" # _jst_range if start is not None: ans = f"{ans}.slice({_py2js(start, lcs)})" # _jst_range return ans # _jst_range jstCallDict["range"] = _jst_range # _jst_range _jstGuardNArgs("reversed", [1], lambda m, lcs: f"({_py2js(m.args[0], lcs)}).toReversed()") # _jst_range def _jst_round(m, lcs): # _jst_round if len(m.args) > 2 or len(m.args) <= 0: raise JSTranspileError("Only support round() with 1 or 2 arguments!") # _jst_round if len(m.args) == 1: return f"Math.round({_py2js(m.args[0], lcs)})" # _jst_round else: b = _py2js(m.args[1], lcs); return f"Math.round(({_py2js(m.args[0], lcs)})*Math.pow(10, {b})+Number.EPSILON)/Math.pow(10, {b})" # _jst_round jstCallDict["round"] = _jst_round # _jst_round _jstGuardNArgs("set", [1], lambda m, lcs: f"new Set({_py2js(m.args[0], lcs)})") # _jst_round _jstGuardNArgs("setattr", [3], lambda m, lcs: f"({_py2js(m.args[0], lcs)}[{_py2js(m.args[1], lcs)}] = {_py2js(m.args[2], lcs)})") # _jst_round # sorted, ignored cause user can just use sort() cli instead! Python's sort() has weird args # _jst_round _jstGuardNArgs("str", [1], lambda m, lcs: f"({_py2js(m.args[0], lcs)}).toString()") # _jst_round _jstGuardNArgs("sum", [1], lambda m, lcs: f"({_py2js(m.args[0], lcs)}).reduce((partialSum, a) => partialSum + a, 0)") # _jst_round _jstGuardNArgs("tuple", [1], lambda m, lcs: f"Array.from({_py2js(m.args[0], lcs)})") # _jst_round _jstGuardNArgs("type", [1], lambda m, lcs: f"typeof({_py2js(m.args[0], lcs)})") # _jst_round _jstGuardNArgs("zip", [], lambda m, lcs: f"[{', '.join([_py2js(e, lcs) for e in m.args])}].transpose()") # _jst_round def flattenAttribute(x) -> str: # flattenAttribute if isinstance(x, ast.Name): return x.id # flattenAttribute elif isinstance(x, ast.Attribute): return f"{flattenAttribute(x.value)}.{x.attr}" # flattenAttribute else: raise JSTranspileError("kast.flattenAttribute() only supports attributes that only contains ast.Attribute and ast.Name") # flattenAttribute def pyLambParse(s:str) -> "ast.AST": # parses code, get rid of "lambda: " in front if exists # pyLambParse # s might contain stringified # pyLambParse m = ast.parse(s) # pyLambParse if len(m.body) == 0: raise JSTranspileError(f"No expression inside passed in code `{s}`") # pyLambParse if len(m.body) > 1: raise JSTranspileError(f"py2js() function can only work with relatively simple operations. The code passed in is too complicated: `{s}`") # pyLambParse expr = m.body[0].value # pyLambParse if isinstance(expr, ast.Lambda): expr = expr.body # pyLambParse return expr # pyLambParse
[docs]def py2js(m): # py2js """Converts simple python expression into js code. Example:: kast.py2js("x+3") # returns ('(x+3)', {'x'}) kast.py2js("lambda x: x+3") # returns ('(x+3)', {'x'}) kast.py2js("lambda y: y+3") # returns ('(y+3)', {'y'}) kast.py2js("lambda y: y+3*x") # returns ('(y+(3*x))', {'x', 'y'}) kast.py2js("x<3 and 45>10") # returns ('((x<3)&&(45>10))', {'x'}) kast.py2js("abs((x//5.1) * 2.3)") # returns ('Math.abs((Math.floor(x/5.1)*2.3))', {'x'}) kast.py2js("not x > 3") # returns ('!(x>3)', {'x'}) This will return a tuple of 2 objects. First is the JS code, and second is the set of unknown variables in the JS code. If the code is a lambda function, then it only considers what's inside the function. More complex examples:: # returns ('[...Array(10).keys()].map((x, i) => [x, i]).filter(([x, y]) => ((x%3)===1)).map(([x, y]) => (Math.pow(x, y)+z))', {'z'}) kast.py2js("[x**y + z for x, y in enumerate(range(10)) if x % 3 == 1]") Both JS and Python code when run should return [1, 256, 823543] for z = 0. A lot of builtin Python functions are transpiled automatically, including:: abs, all, any, divmod, enumerate, float, getattr, hasattr, int, iter len, list, max, min, pow, print, range, reversed, round, set, setattr str, sum, tuple, type, zip Other builtin operations don't quite make sense though. But the neat thing is, any functions beyond these builtin functions are not transpiled, and instead just passes along to the JS code. This allows you to kinda mix and match code styles from both Python and JS. For example:: # returns ("(document.querySelector(('#' + someIdx + '_tail')).style['color'] = (((Math.round(Math.pow(x, 2))===Math.abs(-12))) ? ('red') : ('green')))", {'someIdx', 'x'}) kast.py2js("setattr(document.querySelector(f'#{someIdx}_tail').style, 'color', 'red' if round(x**2) == Math.abs(-12) else 'green')") See how we're both using the Python transpiled ``**`` (to ``Math.pow(...)``), yet uses ``Math.abs(...)`` directly, which is not defined in Python, but is defined in JS, without any trouble. Also notice that ``document.querySelector`` and f-strings work too. So in short, overall syntax is Python's, but you can call random JS functions! This can do lots of transformations, but don't expect it to be airtight. I've done all I can to make the thing airtight, but I'm sure you can come up with some complex scenario where it would fail. This is meant to be applied on short pieces of code only.""" # py2js lcs = set(); ans = _py2js(m, lcs); return ans, lcs # py2js
def _listCompTarget(m, lcs): # extracts target of list comprehensions, like [... for x, [y, z] in ...] # _listCompTarget if isinstance(m, ast.Name): return _py2js(m, lcs) # _listCompTarget if isinstance(m, (ast.Tuple, ast.List)): s = ", ".join([_listCompTarget(e, lcs) for e in m.elts]); return f"[{s}]" # _listCompTarget raise JSTranspileError(f"While trying to expand target assignment in list/set/dict comprehensions, encountered unknown type: {type(m)}") # _listCompTarget def _py2js(m:"ast.AST|str", lcs:"list[str]") -> str: # lcs for list of variable names, only used in ast.Name # _py2js if isinstance(m, str): m = pyLambParse(m) # _py2js if isinstance(m, ast.BinOp): # _py2js left = _py2js(m.left, lcs); right = _py2js(m.right, lcs) # _py2js if isinstance(m.op, ast.FloorDiv): return f"Math.floor({left}/{right})" # TODO: might be weird dealing with negative floors # _py2js if isinstance(m.op, ast.Pow): return f"Math.pow({left}, {right})" # _py2js return f"({left}{_py2js(m.op, lcs)}{right})" # _py2js if isinstance(m, ast.Compare): # only supports a<b or a<b<c, dont support more elems, like a<b<c<d # _py2js nops = len(m.ops); ncomps = len(m.comparators) # _py2js if nops != ncomps: raise JSTranspileError(f"Number of operations {nops} differed from number of comparators {ncomps}") # _py2js if nops == 0: raise JSTranspileError("Number of operations can't be zero") # _py2js if nops > 2: raise JSTranspileError("Don't support Compares with more than 2 comparisons. Change `a<b<c<d` into `(a<b<c) and (c<d)`") # _py2js left = _py2js(m.left, lcs); comps = [_py2js(e, lcs) for e in m.comparators]; ops = [_py2js(o, lcs) for o in m.ops] # _py2js if nops == 1: return f"({left}{ops[0]}{comps[0]})" # _py2js if nops == 2: return f"({left}{ops[0]}{comps[0]}) && ({comps[0]}{ops[1]}{comps[1]})" # _py2js return m # _py2js if isinstance(m, ast.BoolOp): return f"({_py2js(m.op, lcs).join([_py2js(v, lcs) for v in m.values])})" # _py2js if isinstance(m, ast.UnaryOp): # _py2js if isinstance(m.op, ast.Not): return f"!{_py2js(m.operand, lcs)}" # _py2js if isinstance(m.op, ast.Invert): raise JSTranspileError(f"No notion of inversion '~' exists in JS") # _py2js if isinstance(m.op, ast.USub): return f"-{_py2js(m.operand, lcs)}" # _py2js if isinstance(m.op, ast.UAdd): return f"{_py2js(m.operand, lcs)}" # _py2js raise JSTranspileError(f"kast.py2js() doesn't know how to parse UnaryOp `{m.op}`") # _py2js if isinstance(m, ast.Name): lcs.add(m.id); return f"{m.id}" # _py2js if isinstance(m, ast.Constant): # _py2js if m.value is None: return f"null" # _py2js if isinstance(m.value, str): s = m.value.replace("'", "\\'"); return f"'{s}'" # _py2js return f"{m.value}" # _py2js if isinstance(m, ast.Add): return f"+" # _py2js if isinstance(m, ast.Sub): return f"-" # _py2js if isinstance(m, ast.Mult): return f"*" # _py2js if isinstance(m, ast.MatMult): raise JSTranspileError("No notion of matrix multiplication '@' exists in JS") # _py2js if isinstance(m, ast.Div): return f"/" # _py2js if isinstance(m, ast.Mod): return f"%" # _py2js if isinstance(m, ast.Pow): return f"^" # _py2js if isinstance(m, ast.Eq): return f"===" # _py2js if isinstance(m, ast.NotEq): return f"!==" # _py2js if isinstance(m, ast.Gt): return f">" # _py2js if isinstance(m, ast.GtE): return f">=" # _py2js if isinstance(m, ast.Lt): return f"<" # _py2js if isinstance(m, ast.LtE): return f"<=" # _py2js if isinstance(m, ast.LShift): return "<<" # _py2js if isinstance(m, ast.RShift): return ">>" # _py2js if isinstance(m, ast.And): return "&&" # _py2js if isinstance(m, ast.Or): return "||" # _py2js if isinstance(m, ast.Attribute): return f"{_py2js(m.value, lcs)}.{m.attr}" # _py2js if isinstance(m, ast.Call): # _py2js if len(m.keywords) > 0: raise JSTranspileError(f"keyword arguments don't exist in JS, please use normal arguments in your function calls only, at `{m}`") # _py2js funcName = flattenAttribute(m.func) # _py2js if funcName in jstCallDict: return jstCallDict[funcName](m, lcs) # _py2js return f"{funcName}({', '.join([_py2js(a, lcs) for a in m.args])})" # _py2js if isinstance(m, (ast.List, ast.Tuple)): return f"[{', '.join([_py2js(e, lcs) for e in m.elts])}]" # _py2js if isinstance(m, ast.Set): return f"new Set([{', '.join([_py2js(e, lcs) for e in m.elts])}])" # _py2js if isinstance(m, ast.Dict): # _py2js keys = [f"{_py2js(k, lcs)}" if isinstance(k, ast.Constant) else (f"[{_py2js(k, lcs)}]" if k is not None else None) for k in m.keys] # _py2js values = [f"{_py2js(v, lcs)}" for v in m.values] # _py2js a = [(f"...{v}" if k is None else f"{k}: {v}") for k, v in zip(keys, values)]; return "{" + ", ".join(a) + "}" # _py2js if isinstance(m, ast.Starred): return f"...{_py2js(m.value, lcs)}" # _py2js if isinstance(m, ast.IfExp): return f"(({_py2js(m.test, lcs)}) ? ({_py2js(m.body, lcs)}) : ({_py2js(m.orelse, lcs)}))" # _py2js if isinstance(m, ast.Subscript): # _py2js tail = ""; slices = m.slice.elts if isinstance(m.slice, ast.Tuple) else [m.slice] # _py2js for s in reversed(slices): # _py2js if isinstance(s, ast.Slice): # _py2js if s.step is not None: pr = '\n'.join(m | pretty()); raise JSTranspileError(f"JS does not support stepped slices! This can be augmented on my side later on though. kast.pretty() on the element:\n\n{pr}") # _py2js l = 'undefined' if s.lower is None else _py2js(s.lower, lcs) # _py2js u = 'undefined' if s.upper is None else _py2js(s.upper, lcs) # _py2js if tail: dataIdx = k1lib.cli.init._jsDAuto(); tail = f".slice({l}, {u}).map(({dataIdx}) => {dataIdx}{tail})" # _py2js else: tail = f".slice({l}, {u})" # short path # _py2js else: tail = f".at({_py2js(s, lcs)}){tail}" # _py2js return f"{_py2js(m.value, lcs)}{tail}" # _py2js if isinstance(m, (ast.ListComp, ast.GeneratorExp, ast.SetComp)): # _py2js if len(m.generators) != 1: raise JSTranspileError("Currently only supports 1 generator in list/set/generator comprehensions") # _py2js g = m.generators[0]; targetLcs = set(); oldLcs = set(lcs) # _py2js if g.is_async: raise JSTranspileError("Does not support async functions yet") # _py2js ans = _py2js(g.iter, lcs); target = _listCompTarget(g.target, targetLcs) # _py2js for if_ in g.ifs: ans = f"{ans}.filter(({target}) => {_py2js(if_, lcs)})" # _py2js ans = f"{ans}.map(({target}) => {_py2js(m.elt, lcs)})" # _py2js for x in list(lcs): # try removing unknown variables in the .elt expression that appears in the target, cause those are not unknown variables! # _py2js if x not in oldLcs and x in targetLcs: lcs.remove(x) # _py2js return f"new Set({ans})" if isinstance(m, ast.SetComp) else ans # _py2js if isinstance(m, ast.DictComp): # _py2js if len(m.generators) != 1: raise JSTranspileError("Currently only supports 1 generator in dict comprehensions") # _py2js g = m.generators[0]; targetLcs = set(); oldLcs = set(lcs) # _py2js if g.is_async: raise JSTranspileError("Does not support async functions yet") # _py2js ans = _py2js(g.iter, lcs); target = _listCompTarget(g.target, targetLcs) # _py2js for if_ in g.ifs: ans = f"{ans}.filter(({target}) => {_py2js(if_, lcs)})" # _py2js ans = f"{ans}.map(({target}) => [{_py2js(m.key, lcs)}, {_py2js(m.value, lcs)}]).toDict()" # _py2js for x in list(lcs): # try removing unknown variables in the .elt expression that appears in the target, cause those are not unknown variables! # _py2js if x not in oldLcs and x in targetLcs: lcs.remove(x) # _py2js return ans # _py2js if isinstance(m, ast.FormattedValue): return _py2js(m.value, lcs) # _py2js if isinstance(m, ast.JoinedStr): return f"({' + '.join([_py2js(e, lcs) for e in m.values])})" # _py2js raise JSTranspileError(f"kast.py2js() doesn't know how to parse {type(m)}") # _py2js def prepareFunc(fn:"str|cli.op", extraVars:"list[str]"=None, checks=True): # prepareFunc """for toJsFunc() transpilation subsystem. Returns (transpiled function, variable declarations, argument variables). If can't understand the object passed in, return None :param fn: assumes this is a cli.op(), a lambda function, or an expression inside a lambda function :param extraVars: available variables that can be resolved from js function. Can come from arguments, or from js function declaration, or other places. Specify this to shut up the variable resolver checks""" # prepareFunc g = cli.init._k1_global_frame(); avaiVals = dict(); reqVals = set(); extraVars = extraVars or [] # prepareFunc if isinstance(fn, cli.op): # prepareFunc fn, vs = fn.ab_fastFS() # prepareFunc for v, vv in vs.items(): reqVals.add(v); avaiVals[v] = vv # prepareFunc elif not isinstance(fn, str): return None; raise Exception(f"Function inside apply() has to either be a cli.op(), or a string containg a Python expression. Instead, it's {type(fn)}") # prepareFunc # adds args from lambda func to available vars # prepareFunc argVars = kast_lambda(fn); extraVars.extend(argVars) # prepareFunc # transpiles to js # prepareFunc fn, vs = py2js(fn) # prepareFunc # try to resolve variables # prepareFunc for v in vs: # prepareFunc reqVals.add(v) # prepareFunc if v == "x": continue # just ignore "x" variable, cause that's the default # prepareFunc try: avaiVals[v] = g[v] # prepareFunc except: pass # prepareFunc # removing available variables from required variables # prepareFunc for v in avaiVals: reqVals.remove(v) # prepareFunc extraVars = set(extraVars) # prepareFunc if checks: # prepareFunc for v in reqVals: # prepareFunc if v not in extraVars: # prepareFunc raise Exception(f"Expression depends on the variables {v}, but they couldn't be resolved") # prepareFunc # forming declaration statements # prepareFunc return fn, "\n".join([f"{k} = {json.dumps(v)};" for k, v in avaiVals.items()]), argVars # prepareFunc def kast_lambda(fn:"str|cli.op") -> "list[str]": # kast_lambda """Grab args of the input lambda func. If it's just a simple expression then return ['x']""" # kast_lambda l = ast.parse(fn).body[0].value # kast_lambda if isinstance(l, ast.Lambda): return [a.arg for a in l.args.args] # kast_lambda return ["x"] # kast_lambda def prepareFunc2(fn, checks=True) -> "(header, fIdx) | None": # prepareFunc2 res = prepareFunc(fn, [], checks) # prepareFunc2 if res is None: return None # prepareFunc2 expr, avaiVars, args = res # prepareFunc2 fIdx = k1lib.cli.init._jsFAuto() # prepareFunc2 dataIdx = k1lib.cli.init._jsDAuto() # prepareFunc2 return f"""{avaiVars} {fIdx} = ({', '.join(args)}) => {{ return {expr}; }}""", fIdx # prepareFunc2 def prepareFunc3(fn, meta, kwargs=None, args=None) -> "(header, fIdx)": # prepareFunc3 """Kinda similar to prepareFunc2(), but this time uses 3 different strategies to transpile: - Using prepareFunc2(), works for string with lambda functions and cli.op() - Using the object's builtin ._jsF() function - Using k1lib.settings.cli.kjs.jsF dictionary of different custom _jsF() functions Throws error if can't transpile the function :param kwargs: keyword arguments and arguments provided by aS(), apply(), filt(), and so on. """ # prepareFunc3 res = prepareFunc2(fn, False) # tries to compile str/cli.op # prepareFunc3 if res: # prepareFunc3 if not(kwargs is None or len(kwargs) == 0): raise Exception(f"Keyword arguments doesn't exist in JS! Please convert the function `{fn}` into positional arguments") # prepareFunc3 if args is None or len(args) == 0: return res # prepareFunc3 # prepare extra code here if extra args are available # prepareFunc3 fIdx = cli.init._jsFAuto(); dataIdx = cli.init._jsDAuto(); argIdx = cli.init._jsDAuto() # prepareFunc3 _header, _fIdx = res; return f"""{_header}\n{argIdx} = {json.dumps(args)};\n{fIdx} = ({dataIdx}) => {_fIdx}({dataIdx}, ...{argIdx})""", fIdx # prepareFunc3 elif hasattr(fn, "_jsF"): return fn._jsF(meta, *(args or ()), **(kwargs or {})) # if not str/cli.op, then lookup the funcs, then pass through the kwargs and whatnot # prepareFunc3 elif fn in k1lib.settings.cli.kjs.jsF: # prepareFunc3 try: return k1lib.settings.cli.kjs.jsF[fn](meta, *(args or ()), **(kwargs or {})) # prepareFunc3 except Exception as e: print(f"Function `{fn}` exist in system, but throws the below error when executed"); raise e # prepareFunc3 else: raise Exception(f"*._jsF() right now doesn't know how to transpile to JS this func: {fn}\n\nIt could be you're using regular lambda functions. Convert it to use cli.op(), or just enter the lambda function as a string, so `lambda x: x**2` turns into the string `\"lambda x: x**2\"`\n\nOr, if it's a function you use often, add to `settings.cli.kjs.jsF`") # prepareFunc3
[docs]def asyncGuard(data:"(header, fIdx)") -> "(header, fIdx, async?)": # asyncGuard """Converts ._jsF() function signatures from (header, fIdx) into (header, fIdx, is fIdx async?). Example:: # returns tuple (header, fIdx, async?) kast.asyncGuard(apply("x**2")._jsF(None)) """ # asyncGuard if data is NotImplemented: return NotImplemented # asyncGuard header, fIdx = data # asyncGuard g1 = cli.grep(f"{fIdx}[ ]*=[ ]*(?P<g>(async)|((?!\(*)))[ ]*\(", extract="g") # asyncGuard g2 = cli.grep(f"(?P<g>async)[ ]*function[ ]*{fIdx}\(", extract="g") # asyncGuard x = header.split("\n"); return [header, fIdx, len(list(g1(x))) + len(list(g2(x))) > 0] # asyncGuard