# -*- 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 ` 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 ('', ''): 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"))