You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

361 lines
14 KiB

# -*- coding: utf-8 -*-
from __future__ import print_function, absolute_import, with_statement, unicode_literals, division
import re
import os
import sys
import types
import inspect
import hashlib
import tempfile
import subprocess
from . import Parser
from .stdlib import get_full_std_lib # noqa
from .modules import create_js_module
class JSString(unicode):
""" A subclass of string, so we can add attributes to JS string objects.
"""
pass
def py2js(ob=None, new_name=None, **parser_options):
""" Convert Python to JavaScript.
Parameters:
ob (str, module, function, class): The code, function or class
to transpile.
new_name (str, optional): If given, renames the function or class. This
can be used to simply change the name and/or add a prefix. It can
also be used to turn functions into methods using
"MyClass.prototype.foo". Double-underscore name mangling is taken
into account in the process.
parser_options: Additional options, see
:class:`Parser class <pscript.Parser>` for details.
Returns:
str: The JavaScript code as a str object that
has a ``meta`` attribute with the following fields:
* filename (str): the name of the file that defines the object.
* linenr (int): the starting linenr for the object definition.
* pycode (str): the Python code used to generate the JS.
* pyhash (str): a hash of the Python code.
* vars_defined (set): names defined in the toplevel namespace.
* vars_unknown (set): names used in the code but not defined in it.
This includes namespaces, e.g. "foo.some_function".
* vars_global (set): names explicitly declared global.
* std_functions (set): stdlib functions used in this code.
* std_method (set): stdlib methods used in this code.
Notes:
The Python source code for a class is acquired by name.
Therefore one should avoid decorating classes in modules where
multiple classes with the same name are defined. This is a
consequence of classes not having a corresponding code object (in
contrast to functions).
"""
def py2js_(ob):
if isinstance(ob, basestring):
thetype = 'str'
pycode = ob
filename = None
linenr = 0
elif isinstance(ob, types.ModuleType) and hasattr(ob, '__file__'):
thetype = 'str'
filename = inspect.getsourcefile(ob)
linenr = 0
pycode = open(filename, 'rb').read().decode("utf-8")
if pycode.startswith('# -*- coding:'):
pycode = '\n' + pycode.split('\n', 1)[-1]
elif isinstance(ob, (type, types.FunctionType, types.MethodType)):
thetype = 'class' if isinstance(ob, type) else 'def'
# Get code
try:
filename = inspect.getsourcefile(ob)
lines, linenr = inspect.getsourcelines(ob)
except Exception as err:
raise ValueError('Could not get source code for object %r: %s' %
(ob, err))
if getattr(ob, '__name__', '') in ('', '<lambda>'):
raise ValueError('py2js() got anonymous function from '
'"%s", line %i, %r.' % (filename, linenr, ob))
# Normalize indentation, based on first line
indent = len(lines[0]) - len(lines[0].lstrip())
for i in xrange(len(lines)):
line = lines[i]
line_indent = len(line) - len(line.lstrip())
if line_indent < indent and line.strip():
assert line.lstrip().startswith('#') # only possible for comments
lines[i] = indent * ' ' + line.lstrip()
else:
lines[i] = line[indent:]
# Skip any decorators
while not lines[0].lstrip().startswith((thetype, 'async ' + thetype)):
lines.pop(0)
# join lines and rename
pycode = ''.join(lines)
else:
raise ValueError('py2js() only accepts non-builtin modules, '
'classes and functions.')
# Get hash, in case we ever want to cache JS accross sessions
h = hashlib.sha256('pscript version 1'.encode("utf-8"))
h.update(pycode.encode("utf-8"))
hash = h.digest()
# Get JS code
if filename:
p = Parser(pycode, (filename, linenr), **parser_options)
else:
p = Parser(pycode, **parser_options)
jscode = p.dump()
if new_name:
if thetype not in ('class', 'def'):
raise TypeError('py2js() can only rename functions and classes.')
jscode = js_rename(jscode, ob.__name__, new_name, thetype)
# Collect undefined variables
# vars_unknown = [name for name, s in p.vars.get_undefined()]
vars_unknown = set()
for name, usages in p.vars.get_undefined():
for usage in usages:
vars_unknown.add(usage)
# todo: now that we have so much info in the meta, maybe we should
# use use py2js everywhere where we now use Parser and move its docs here.
# Wrap in JSString
jscode = JSString(jscode)
jscode.meta = {}
jscode.meta['filename'] = filename
jscode.meta['linenr'] = linenr
jscode.meta['pycode'] = pycode
jscode.meta['pyhash'] = hash
jscode.meta['std_functions'] = p._std_functions
jscode.meta['std_methods'] = p._std_methods
jscode.meta['vars_defined'] = p.vars.get_defined()
jscode.meta['vars_global'] = p.vars.get_globals()
jscode.meta['vars_unknown'] = vars_unknown
return jscode
if ob is None:
return py2js_ # uses as a decorator with some options set
return py2js_(ob)
re_sub1 = re.compile(r'this\.__(\w*?[a-zA-Z0-9](?!__)\W)', re.UNICODE)
def js_rename(jscode, cur_name, new_name, type=None):
""" Rename a function or class in a JavaScript code string.
The new name can be prefixed (i.e. have dots in it). Functions can be
converted to methods by prefixing with a name that starts with a capital
letter (and probably ".prototype"). Double-underscore-mangling
is taken into account.
Parameters:
jscode (str): the JavaScript source code
cur_name (str): the current name (must be an identifier, e.g. no dots).
new_name (str): the name to replace the current name with
type (str): the Python object type, can be 'class' or 'def'. If None,
the type is inferred from the object name based on PEP8
Returns:
str: the modified JavaScript source code
"""
assert cur_name and '.' not in cur_name
if type:
isclass = type == 'class'
else:
isclass = cur_name[0].lower() != cur_name[0] # For backward compat.
if isclass:
# cur_cls_name = cur_name
new_cls_name = new_name.split('.')[-1]
else:
new_cls_name = ''
parts = new_name.split('.')
prefix = '.'.join(parts[:-1])
if prefix:
maybe_cls = parts[-3] if parts[-2] == 'prototype' else parts[-2]
maybe_cls = maybe_cls.strip('$') # allow special names
if maybe_cls[0].lower() != maybe_cls[0]:
new_cls_name = maybe_cls
cur_name_short = cur_name.split('.')[-1]
new_name_short = new_name.split('.')[-1]
if isclass:
# If this is about a class ...
jscode = jscode.replace('.__name__ = "%s"' % cur_name_short,
'.__name__ = "%s"' % new_name_short)
jscode = jscode.replace('._%s__' % cur_name_short,
'._%s__' % new_name_short)
jscode = jscode.replace('%s.prototype' % cur_name,
'%s.prototype' % new_name)
else:
# If this is about a function / method
jscode = jscode.replace('function flx_%s' % cur_name_short,
'function flx_%s' % new_name_short, 1)
if new_cls_name: # use regexp to match double-underscore but no magics!
jscode = re_sub1.sub('this._%s__\\1' % new_cls_name, jscode)
#jscode = jscode.replace('this.__', 'this._%s__' % new_cls_name)
# Always do this
jscode = jscode.replace('%s = function' % cur_name,
'%s = function' % new_name, 1)
jscode = jscode.replace('%s = async function' % cur_name,
'%s = async function' % new_name, 1)
if '.' in new_name:
jscode = jscode.replace('var %s;\n' % cur_name, '', 1)
else:
jscode = jscode.replace('var %s;\n' % cur_name,
'var %s;\n' % new_name, 1)
if 'this._Component__properties__' in jscode:
1/0
return jscode
NODE_EXE = None
def get_node_exe():
""" Small utility that provides the node exe. The first time this
is called both 'nodejs' and 'node' are tried. To override the
executable path, set the ``PSCRIPT_NODE_EXE`` environment variable.
"""
# This makes things work on Ubuntu's nodejs as well as other node
# implementations, and allows users to set the node exe if necessary
global NODE_EXE
NODE_EXE = os.getenv('PSCRIPT_NODE_EXE', os.getenv('FLEXX_NODE_EXE')) or NODE_EXE
if NODE_EXE is None:
NODE_EXE = 'nodejs'
try:
subprocess.check_output([NODE_EXE, '-v'])
except Exception: # pragma: no cover
NODE_EXE = 'node'
return NODE_EXE
_eval_count = 0
def evaljs(jscode, whitespace=True, print_result=True, extra_nodejs_args=None):
""" Evaluate JavaScript code in Node.js.
Parameters:
jscode (str): the JavaScript code to evaluate.
whitespace (bool): if whitespace is False, the whitespace
is removed from the result. Default True.
print_result (bool): whether to print the result of the evaluation.
Default True. If False, larger pieces of code can be evaluated
because we can use file-mode.
extra_nodejs_args (list): Extra command line args to pass to nodejs.
Returns:
str: the last result as a string.
"""
global _eval_count
# Init command
cmd = [get_node_exe()]
if extra_nodejs_args:
cmd.extend(extra_nodejs_args)
# Prepare command
if len(jscode) > 2**14:
if print_result:
# Strictly speaking, this is a limitation of Windows, but come-on!
raise RuntimeError('evaljs() wont send more than 16 kB of code '
'over the command line, but cannot use a file '
'unless print_result is False.')
_eval_count += 1
fname = 'pscript_%i_%i.js' % (os.getpid(), _eval_count)
filename = os.path.join(tempfile.gettempdir(), fname)
with open(filename, 'wb') as f:
f.write(jscode.encode("utf-8"))
cmd += ['--use_strict', filename]
else:
filename = None
p_or_e = ['-p', '-e'] if print_result else ['-e']
cmd += ['--use_strict'] + p_or_e + [jscode]
if sys.version_info[0] < 3:
cmd = [c.encode('raw_unicode_escape') for c in cmd]
# Call node
try:
res = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except Exception as err:
if hasattr(err, 'output'):
err = err.output.decode("utf-8")
else:
err = unicode(err)
err = err[:400] + '...' if len(err) > 400 else err
raise RuntimeError(err)
finally:
if filename is not None:
try:
os.remove(filename)
except Exception:
pass
# Process result
res = res.decode("utf-8").rstrip()
if print_result and res.endswith('undefined'):
res = res[:-9].rstrip()
if not whitespace:
res = res.replace('\n', '').replace('\t', '').replace(' ', '')
return res
def evalpy(pycode, whitespace=True):
""" Evaluate PScript code in Node.js (after translating to JS).
Parameters:
pycode (str): the PScript code to evaluate.
whitespace (bool): if whitespace is False, the whitespace is
removed from the result. Default True.
Returns:
str: the last result as a string.
"""
# delibirate numpy doc style to see if napoleon handles it the same
return evaljs(py2js(pycode), whitespace)
def script2js(filename, namespace=None, target=None, module_type='umd',
**parser_options):
""" Export a .py file to a .js file.
Parameters:
filename (str): the filename of the .py file to transpile.
namespace (str): the namespace for this module. (optional)
target (str): the filename of the resulting .js file. If not given
or None, will use the ``filename``, but with a ``.js`` extension.
module_type (str): the type of module to produce (if namespace is given),
can be 'hidden', 'simple', 'amd', 'umd', default 'umd'.
parser_options: additional options for the parser. See Parser class
for details.
"""
# Import
assert filename.endswith('.py')
pycode = open(filename, 'rb').read().decode("utf-8")
# Convert
parser = Parser(pycode, filename, **parser_options)
jscode = '/* Do not edit, autogenerated by pscript */\n\n' + parser.dump()
# Wrap in module
if namespace:
exports = [name for name in parser.vars.get_defined()
if not name.startswith('_')]
jscode = create_js_module(namespace, jscode, [], exports, module_type)
# Export
if target is None:
dirname, fname = os.path.split(filename)
filename2 = os.path.join(dirname, fname[:-3] + '.js')
else:
filename2 = target
with open(filename2, 'wb') as f:
f.write(jscode.encode("utf-8"))