Last active
January 18, 2016 03:19
-
-
Save NickBeeuwsaert/23b4e6c3d7790742e57f to your computer and use it in GitHub Desktop.
Attempt at converting Python's AST into ES2015
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ NOTE: This is incomplete! | |
| There are a lot of things that don't work, | |
| like classes, generator functions and async functions! | |
| """ | |
| import ast | |
| import itertools | |
| import json | |
| from io import StringIO | |
| import argparse | |
| BINOPS = { | |
| ast.Add: '({} + {})', | |
| ast.Mult: '({} * {})', | |
| ast.Div: '({} / {})', | |
| ast.Sub: '({} - {})', | |
| ast.Pow: '({} ** {})', | |
| # NOTE: I think pythons notion of the mod operator | |
| # Is different than JS, double check this later... | |
| ast.Mod: '({} % {})', | |
| ast.LShift: '({} << {})', | |
| ast.RShift: '({} >> {})', | |
| ast.BitOr: '({}|{})', | |
| ast.BitAnd: '({}&{})', | |
| ast.BitXor: '({}^{})', | |
| ast.FloorDiv: 'Math.floor({} / {})', | |
| } | |
| BOOLOPS = { | |
| ast.And: ' && ', | |
| ast.Or: ' || ' | |
| } | |
| UNARYOPS = { | |
| ast.Not: '!', | |
| ast.UAdd: '+', | |
| ast.USub: '-', | |
| ast.Invert: '~' | |
| } | |
| CMPOPS = { | |
| ast.Eq: '({} == {})', | |
| ast.NotEq: '({} !== {})', | |
| ast.Lt: '({} < {})', | |
| ast.LtE: '({} <= {})', | |
| ast.GtE: '({} >= {})', | |
| ast.Gt: '({} > {})', | |
| ast.Is: '({} === {})', | |
| ast.IsNot: '({} !== {})', | |
| # TODO: | |
| # Python is awesome and lets you compare if objects are equal using `==` | |
| # and if they are the same object using `is` | |
| # But in JS, [] != [], so I need to find a clean way to replace | |
| # pythons in in JS (same for the comparision operators above) | |
| ast.In: 'Array.prototype.indexOf.call({1}, {0}) !== -1', | |
| ast.NotIn: 'Array.prototype.indexOf.call({1}, {0}) === -1' | |
| } | |
| class Visitor(ast.NodeVisitor): | |
| def indent(self, s, amount=4): | |
| return '\n'.join(' '*amount + line for line in s.split('\n')) | |
| def visit_Module(self, node): | |
| return '\n'.join(map(self.visit, node.body)) | |
| def visit_Import(self, node): | |
| return '\n'.join( | |
| 'var {} = require({!r});'.format( | |
| name.asname or name.name, | |
| name.name | |
| ) for name in node.names | |
| ) | |
| result = [] | |
| for name in node.names: | |
| result.append('var {} = require({!r});'.format( | |
| name.asname or name.name, | |
| name.name | |
| )) | |
| return '\n'.join(result) | |
| def visit_ClassDef(self, node): | |
| # TODO: replace this with a better alternative to python classes | |
| # ES2015 classes even close to Python classes | |
| # Need to figure out a way to do multiple inheritance | |
| # and methods need to get the self argument | |
| return '\n'.join([ | |
| 'class ' + node.name + (' extends '+self.visit(node.bases[-1]) if node.bases else '') + ' {' | |
| ] + [ | |
| self.indent(self.visit(stmt)) for stmt in node.body | |
| ] + [ | |
| '}' | |
| ]) + '\n' | |
| def visit_ImportFrom(self, node): | |
| aliases = [] | |
| for name in node.names: | |
| if name.asname: | |
| aliases.append('{}: {}'.format(name.name, name.asname)) | |
| else: | |
| aliases.append(name.name) | |
| return 'var {{{}}} = require({!r});'.format( | |
| ', '.join(aliases), | |
| '../'*node.level + node.module | |
| ) | |
| def visit_Nonlocal(self, node): | |
| # TODO: Mark these variables as nonlocal | |
| # which would basically mean omitting declaring them with var | |
| return 'var ' + ', '.join(map(self.visit, node.names)) + ';' | |
| def visit_Global(self, node): | |
| # TODO: declare these as global | |
| # which, for a browner might mean | |
| # declaring it as var name = window.name; | |
| # or something... | |
| return 'var ' + ', '.join(map(self.visit, node.names)) + '' | |
| def generic_visit(self, node): | |
| print('Unused: ', type(node).__name__) | |
| super().generic_visit(node) | |
| return '' | |
| def visit_FunctionDef(self, node): | |
| # TODO: Track variable declarations in the function, | |
| # So they can be declared at the top | |
| # Also, if yield is called in the function | |
| # so the function can be declared as a generator (function* func_name) | |
| result = [ | |
| 'function {}({}){{'.format(node.name, self.visit(node.args)), | |
| self.indent('"use strict";'), | |
| self.indent('\n'.join(self.visit(stmt) for stmt in node.body)), | |
| '}' | |
| ] | |
| # Loop through the decorators and apply them | |
| for decorator in node.decorator_list: | |
| result.append('{0} = {1}({0});'.format(node.name, self.visit(decorator))) | |
| return '\n'.join(result) + '\n' | |
| # TODO: then pop the 'stack' | |
| def visit_Lambda(self, node): | |
| # Those phat arrows tho | |
| return '({}) => {}'.format( | |
| self.visit(node.args), | |
| self.visit(node.body) | |
| ) | |
| def visit_arguments(self, node): | |
| # TODO: Work on how we can pass javascript arguments in a pythonic way | |
| # Currently the function is being built up like this: | |
| # function(a, b, c, {d=1, e=2}, args=[], kwargs={}) | |
| # which works for very simple functions, | |
| # But won't work when you do things like | |
| # pass positional arguments as keywords | |
| num_pos_args = len(node.args) - len(node.defaults) | |
| args = [] | |
| positional_args, keyword_args = node.args[:num_pos_args], node.args[num_pos_args:] | |
| args += map(self.visit, positional_args) | |
| kwargs = [ | |
| '{}={}'.format(self.visit(k), self.visit(v)) | |
| for k, v in zip(keyword_args, node.defaults) | |
| ] | |
| kwargs += list(map(self.visit, node.kwonlyargs)) | |
| if kwargs: | |
| args.append('{' + ', '.join(kwargs) + '}') | |
| if node.vararg: | |
| args.append(self.visit(node.vararg) + '=[]') | |
| if node.kwarg: | |
| args.append(self.visit(node.kwarg) + '={}') | |
| return ', '.join(args) | |
| def visit_Pass(self, node): | |
| # No-op | |
| return '' | |
| def visit_Break(self, node): | |
| return 'break;' | |
| def visit_Continue(self, node): | |
| return 'continue;' | |
| def visit_For(self, node): | |
| return '\n'.join([ | |
| 'for(var {} of {})'.format( | |
| self.visit(node.target), | |
| self.visit(node.iter) | |
| ) + '{' | |
| ] + [self.indent(self.visit(stmt)) for stmt in node.body] + ['}']) | |
| # TODO: Handle the else condition | |
| def visit_While(self, node): | |
| return '\n'.join([ | |
| 'while({})'.format( | |
| self.visit(node.test) | |
| ) + '{' | |
| ] + [self.indent(self.visit(stmt)) for stmt in node.body] + ['}']) | |
| # TODO: Handle the else condition | |
| def visit_Assign(self, node): | |
| # TODO: Check current scope and omit 'var' if the variable | |
| # has already been declared | |
| return 'var {} = {};'.format( | |
| ', '.join(map(self.visit, node.targets)), | |
| self.visit(node.value) | |
| ) | |
| def visit_AugAssign(self, node): | |
| # we could use Javascripts shorthand | |
| # assignments here but I'm lazy | |
| return 'var {} = {};'.format( | |
| self.visit(node.target), | |
| BINOPS[type(node.op)].format( | |
| self.visit(node.target), | |
| self.visit(node.value) | |
| ) | |
| ) | |
| def convert_array(self, node): | |
| return '[' + ', '.join(map(self.visit, node.elts)) + ']' | |
| visit_Tuple = convert_array | |
| visit_List = convert_array | |
| def visit_Return(self, node): | |
| if not node.value: | |
| return 'return;' | |
| return 'return {};'.format(self.visit(node.value)) | |
| def visit_If(self, node): | |
| result = ['if({}) {{\n{}\n}}'.format( | |
| self.visit(node.test), | |
| self.indent('\n'.join(map(self.visit, node.body))) | |
| )] | |
| result += [ | |
| ' else '+self.visit(n) | |
| if isinstance(n, ast.If) | |
| else ' else {{\n{}\n}}'.format(self.indent(self.visit(n))) | |
| for n in node.orelse | |
| ] | |
| return ''.join(result) | |
| def visit_Num(self, node): | |
| return str(node.n) | |
| def visit_BoolOp(self, node): | |
| return '({})'.format( | |
| BOOLOPS[type(node.op)].join(self.visit(v) for v in node.values) | |
| ) | |
| def visit_NameConstant(self, node): | |
| return json.dumps(node.value) | |
| def visit_BinOp(self, node): | |
| return BINOPS[type(node.op)].format( | |
| self.visit(node.left), | |
| self.visit(node.right) | |
| ) | |
| def visit_Compare(self, node): | |
| output = [] | |
| for l, o, p in zip([node.left] + node.comparators, node.ops, node.comparators): | |
| op_type = type(o) | |
| output.append( | |
| CMPOPS[op_type].format( | |
| self.visit(l), | |
| self.visit(p) | |
| ) | |
| ) | |
| return ' && '.join(output) | |
| def visit_arg(self, node): | |
| return node.arg | |
| def visit_Name(self, node): | |
| return node.id | |
| def visit_Attribute(self, node): | |
| return '{}.{}'.format( | |
| self.visit(node.value), | |
| node.attr | |
| ) | |
| def visit_UnaryOp(self, node): | |
| return UNARYOPS[type(node.op)] + self.visit(node.operand) | |
| def visit_Call(self, node): | |
| return '{}({})'.format( | |
| self.visit(node.func), | |
| ', '.join(map(self.visit, node.args)) | |
| ) | |
| def visit_Str(self, node): | |
| return json.dumps(node.s) | |
| def visit_Expr(self, node): | |
| return self.visit(node.value) + ';' | |
| if __name__ == '__main__': | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument('input', metavar='INPUT', help='Python file to convert to JS') | |
| args = parser.parse_args() | |
| v = Visitor() | |
| with open(args.input, 'r') as tree_fp: | |
| tree = ast.parse(tree_fp.read()) | |
| print(v.visit(tree)) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import functools as ft | |
| from argparse import ArgumentParser as AP | |
| class TestClass(object): | |
| def __init__(self, x, y, z): | |
| self._x, self._y, self._z = x, y, z | |
| @property | |
| def x(self): | |
| return self._x | |
| def test_func(a, b, c, d = 1, e = 2, f = 3, *args, **kwargs): | |
| print("Hello, world") | |
| return 5 | |
| def test_func_kwargs(**kwargs): | |
| print("{} {}".format(":D", "D:")) | |
| return | |
| def kwonly_args(b = 1, *args, x = 1): | |
| from ..x import x | |
| if 1 < 2 < 3: | |
| print("Guess what! 1 is less than 2 (which is also less than 3)") | |
| elif 1 > 2 > 3: | |
| print("Something is wrong with the universe") | |
| else: | |
| print("Save yourself") | |
| while True: | |
| x -= 1 | |
| if not x: break | |
| else: | |
| print("This wont get converted yet") | |
| for i in range(1, 10): | |
| print("#" + i) | |
| else: | |
| print("this either") | |
| x = lambda a, b, c: a+b+c | |
| if __name__ == "__main__": | |
| x = 1, 2, 3 | |
| y = 5 ** 6 | |
| y **= 2 | |
| y //= 3 | |
| print("Hello, world!") | |
| for x in ft.chain([x, y], [1, 2, 3], [4, 5, 6]): | |
| print(x**2) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment