Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
PYTHON ?= python3
# CURDIR is reliable with spaces; lastword(MAKEFILE_LIST) breaks on "Web Browser/..."
ROOT := $(CURDIR)

export PYTHONPATH := $(ROOT)

.PHONY: help test test-simple test-es6 test-es7 test-language test-all

help:
@echo "Js2Py test targets:"
@echo " make test Run quick integration tests (default)"
@echo " make test-simple Run simple_test.py (ES5 + ES6 smoke tests)"
@echo " make test-es6 Run tests/test_es6.py"
@echo " make test-es7 Run tests/test_es7.py"
@echo " make test-language Run ES5.1 language suite (tests/run.py, slow)"
@echo " make test-all Run quick tests and the language suite"

test: test-simple test-es6 test-es7
@:

test-simple:
PYTHONPATH="$(ROOT)" $(PYTHON) "$(ROOT)/simple_test.py"

test-es6:
PYTHONPATH="$(ROOT)" $(PYTHON) "$(ROOT)/tests/test_es6.py"

test-es7:
PYTHONPATH="$(ROOT)" $(PYTHON) "$(ROOT)/tests/test_es7.py"

test-language:
@test -f "$(ROOT)/tests/node_failed.txt" || touch "$(ROOT)/tests/node_failed.txt"
cd "$(ROOT)/tests" && PYTHONPATH="$(ROOT)" $(PYTHON) run.py

test-all: test test-language
2 changes: 1 addition & 1 deletion js2py/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
__all__ = [
'EvalJs', 'translate_js', 'import_js', 'eval_js', 'parse_js',
'translate_file', 'run_file', 'disable_pyimport', 'eval_js6',
'translate_js6', 'PyJsException', 'get_file_contents',
'translate_js6', 'eval_js7', 'translate_js7', 'PyJsException', 'get_file_contents',
'write_file_contents', 'require'
]

Expand Down
44 changes: 41 additions & 3 deletions js2py/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,10 @@ def get(self, prop, throw=True):
# fast local scope
cand = self.own.get(prop)
if cand is None:
return self.prototype.get(prop, throw)
parent = self.prototype
if isinstance(parent, Scope):
return parent.get(prop, throw)
return parent.get(prop)
return cand
# slow, global scope
if prop not in self.own:
Expand Down Expand Up @@ -1369,15 +1372,35 @@ def __repr__(self):
ObjectPrototype = PyJsObject()


def _js_argcount(fcode):
"""Number of JavaScript parameters (excluding this, arguments, and var)."""
names = fcode.co_varnames[:fcode.co_argcount]
if (len(names) >= 3 and names[-1] == 'var' and names[-3] == 'this' and
names[-2] == 'arguments'):
return len(names) - 3
if len(names) >= 2 and names[-2] == 'this' and names[-1] == 'arguments':
return len(names) - 2
return fcode.co_argcount - 2


#Function
class PyJsFunction(PyJs):
Class = 'Function'

def __init__(self, func, prototype=None, extensible=True, source=None):
cand = fix_js_args(func)
has_scope = cand is func
func = cand
self.argcount = six.get_function_code(func).co_argcount - 2 - has_scope
fcode = six.get_function_code(func)
fargs = fcode.co_varnames[fcode.co_argcount - 2:fcode.co_argcount]
if fargs == ('this', 'arguments') or fargs == ('arguments', 'var'):
self._js_global_this = False
self.argcount = _js_argcount(fcode)
elif cand is func:
self._js_global_this = True
self.argcount = fcode.co_argcount
else:
self._js_global_this = False
self.argcount = fcode.co_argcount - 2
self.code = func
self.source = source if source else '{ [python code] }'
self.func_name = func.__name__ if not func.__name__.startswith(
Expand Down Expand Up @@ -1459,6 +1482,21 @@ def call(self, this, args=()):
args = args[0:arglen]
elif len(args) < arglen:
args += (undefined, ) * (arglen - len(args))
if self._js_global_this:
g = self.code.__globals__
saved = {}
for name, val in (('this', this), ('arguments', arguments)):
if name in g:
saved[name] = g[name]
g[name] = val
try:
return Js(self.code(*args))
finally:
for name, val in six.iteritems(saved):
g[name] = val
for name in ('this', 'arguments'):
if name not in saved:
g.pop(name, None)
args += this, arguments #append extra params to the arg list
try:
return Js(self.code(*args))
Expand Down
21 changes: 21 additions & 0 deletions js2py/es6/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@
babel = None
babelPresetEs2015 = None

import re

# Patterns for ES6+ syntax that pyjsparser rejects or that needs Babel downleveling.
_ES6_SYNTAX_RE = re.compile(
r'(?:'
r'=>|' # arrow functions
r'\bclass\b|' # classes
r'`|' # template literals
r'\.\.\.|' # spread/rest
r'\bfor\s*\([^)]*\bof\b|' # for...of
r'\bimport\b|\bexport\b|' # modules
r'\basync\b|\bawait\b|' # async/await
r'\bfunction\s*\*' # generators
r')',
re.MULTILINE)


def looks_like_es6(code):
"""Return True if source likely contains ES6+ syntax needing transpilation."""
return bool(_ES6_SYNTAX_RE.search(code))


def js6_to_js5(code):
global INITIALISED, babel, babelPresetEs2015
Expand Down
113 changes: 113 additions & 0 deletions js2py/es7/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""ES7 (ES2016) support: exponentiation operator and related syntax detection."""

import re

_ES7_SYNTAX_RE = re.compile(
r'(?:'
r'(?<![*/])\*\*(?!\*)|' # ** but not */ or ***
r'\.includes\s*\(' # Array.prototype.includes
r')',
re.MULTILINE)

_PATCHED = False


def looks_like_es7(code):
"""Return True if source likely contains ES7 syntax."""
return bool(_ES7_SYNTAX_RE.search(code))


def ensure_pyjsparser_es7():
"""Patch pyjsparser once to parse ** with correct right-associativity."""
global _PATCHED
if _PATCHED:
return
import pyjsparser.parser as parser
import pyjsparser.pyjsparserdata as data
from pyjsparser.pyjsparserdata import Token

data.PRECEDENCE['**'] = 12

_orig_scan = parser.PyJsParser.scanPunctuator

def scan_punctuator(self):
if (self.index < self.length - 1
and self.source[self.index:self.index + 2] == '**'):
start = self.index
self.index += 2
return {
'type': Token.Punctuator,
'value': '**',
'lineNumber': self.lineNumber,
'lineStart': self.lineStart,
'start': start,
'end': self.index,
}
return _orig_scan(self)

def _should_reduce(new_prec, stack_prec, stack_op, new_op):
if new_prec < stack_prec:
return True
if new_prec > stack_prec:
return False
if stack_op == '**' or new_op == '**':
return False
return True

_orig_parse_binary = parser.PyJsParser.parseBinaryExpression

def parse_binary_expression(self):
marker = self.lookahead
left = self.inheritCoverGrammar(self.parseUnaryExpression)

token = self.lookahead
prec = self.binaryPrecedence(token, self.state['allowIn'])
if prec == 0:
return left
self.isAssignmentTarget = self.isBindingElement = parser.false
token['prec'] = prec
self.lex()

markers = [marker, self.lookahead]
right = self.isolateCoverGrammar(self.parseUnaryExpression)

stack = [left, token, right]

while True:
prec = self.binaryPrecedence(self.lookahead, self.state['allowIn'])
if not prec > 0:
break
new_op = self.lookahead['value']
while len(stack) > 2:
stack_prec = stack[len(stack) - 2]['prec']
stack_op = stack[len(stack) - 2]['value']
if not _should_reduce(prec, stack_prec, stack_op, new_op):
break
right = stack.pop()
operator = stack.pop()['value']
left = stack.pop()
markers.pop()
expr = parser.WrappingNode(
markers[len(markers) - 1]).finishBinaryExpression(
operator, left, right)
stack.append(expr)

token = self.lex()
token['prec'] = prec
stack.append(token)
markers.append(self.lookahead)
expr = self.isolateCoverGrammar(self.parseUnaryExpression)
stack.append(expr)

i = len(stack) - 1
expr = stack[i]
markers.pop()
while i > 1:
expr = parser.WrappingNode(markers.pop()).finishBinaryExpression(
stack[i - 1]['value'], stack[i - 2], expr)
i -= 2
return expr

parser.PyJsParser.scanPunctuator = scan_punctuator
parser.PyJsParser.parseBinaryExpression = parse_binary_expression
_PATCHED = True
51 changes: 36 additions & 15 deletions js2py/evaljs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# coding=utf-8
from .translators import translate_js, DEFAULT_HEADER
from .es6 import js6_to_js5
from .translators.translator import _prepare_js_source
import sys
import time
import json
Expand All @@ -11,8 +11,8 @@

__all__ = [
'EvalJs', 'translate_js', 'import_js', 'eval_js', 'translate_file',
'eval_js6', 'translate_js6', 'run_file', 'disable_pyimport',
'get_file_contents', 'write_file_contents'
'eval_js6', 'translate_js6', 'eval_js7', 'translate_js7', 'run_file',
'disable_pyimport', 'get_file_contents', 'write_file_contents'
]
DEBUG = False

Expand Down Expand Up @@ -57,11 +57,14 @@ def write_file_contents(path_or_file, contents):
f.write(contents)


def translate_file(input_path, output_path):
def translate_file(input_path, output_path, es6=False, es7=False):
'''
Translates input JS file to python and saves the it to the output path.
It appends some convenience code at the end so that it is easy to import JS objects.

es6: False, True, or 'auto' — transpile ES6 via Babel before translation.
es7: False, True, or 'auto' — enable ES2016 features (e.g. **).

For example we have a file 'example.js' with: var a = function(x) {return x}
translate_file('example.js', 'example.py')

Expand All @@ -72,7 +75,7 @@ def translate_file(input_path, output_path):
'''
js = get_file_contents(input_path)

py_code = translate_js(js)
py_code = translate_js(js, es6=es6, es7=es7)
lib_name = os.path.basename(output_path).split('.')[0]
head = '__all__ = [%s]\n\n# Don\'t look below, you will not understand this Python code :) I don\'t.\n\n' % repr(
lib_name)
Expand All @@ -92,11 +95,14 @@ def run_file(path_or_file, context=None):
return eval_value, context


def eval_js(js):
def eval_js(js, es6=False, es7=False):
"""Just like javascript eval. Translates javascript to python,
executes and returns python object.
js is javascript source code

es6: False, True, or 'auto' — see translate_js.
es7: False, True, or 'auto' — enable ES2016 features (e.g. **).

EXAMPLE:
>>> import js2py
>>> add = js2py.eval_js('function add(a, b) {return a + b}')
Expand All @@ -112,17 +118,27 @@ def eval_js(js):
If you really want to convert object to python dict you can use to_dict method.
"""
e = EvalJs()
return e.eval(js)
return e.eval(js, es6=es6, es7=es7)


def eval_js6(js):
"""Just like eval_js but with experimental support for js6 via babel."""
return eval_js(js6_to_js5(js))
return eval_js(_prepare_js_source(js, es6=True))


def translate_js6(js):
"""Just like translate_js but with experimental support for js6 via babel."""
return translate_js(js6_to_js5(js))
return translate_js(_prepare_js_source(js, es6=True))


def eval_js7(js):
"""Like eval_js with ES7 (ES2016) support enabled."""
return eval_js(js, es7=True)


def translate_js7(js):
"""Like translate_js with ES7 (ES2016) support enabled."""
return translate_js(js, es7=True)


class EvalJs(object):
Expand Down Expand Up @@ -171,9 +187,12 @@ def _js_require_impl(npm_module_name):
for k, v in six.iteritems(context):
setattr(self._var, k, v)

def execute(self, js=None, use_compilation_plan=False):
def execute(self, js=None, use_compilation_plan=False, es6=False, es7=False):
"""executes javascript js in current context

es6: False, True, or 'auto' — transpile ES6 via Babel before translation.
es7: False, True, or 'auto' — enable ES2016 features (e.g. **).

During initial execute() the converted js is cached for re-use. That means next time you
run the same javascript snippet you save many instructions needed to parse and convert the
js code to python code.
Expand All @@ -188,18 +207,20 @@ def execute(self, js=None, use_compilation_plan=False):
cache = self.__dict__['cache']
except KeyError:
cache = self.__dict__['cache'] = {}
hashkey = hashlib.md5(js.encode('utf-8')).digest()
cache_key = (hashlib.md5(js.encode('utf-8')).digest(), es6, es7)
try:
compiled = cache[hashkey]
compiled = cache[cache_key]
except KeyError:
code = translate_js(
js, '', use_compilation_plan=use_compilation_plan)
compiled = cache[hashkey] = compile(code, '<EvalJS snippet>',
js, '', use_compilation_plan=use_compilation_plan, es6=es6,
es7=es7)
compiled = cache[cache_key] = compile(code, '<EvalJS snippet>',
'exec')
exec (compiled, self._context)

def eval(self, expression, use_compilation_plan=False):
def eval(self, expression, use_compilation_plan=False, es6=False, es7=False):
"""evaluates expression in current context and returns its value"""
expression = _prepare_js_source(expression, es6, es7)
code = 'PyJsEvalResult = eval(%s)' % json.dumps(expression)
self.execute(code, use_compilation_plan=use_compilation_plan)
return self['PyJsEvalResult']
Expand Down
Loading