# 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