diff --git a/.travis.yml b/.travis.yml index 2205848..0949de8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,26 +1,32 @@ +dist: trusty language: python +addons: + firefox: "latest" + python: - - "2.6" - "2.7" - - "3.2" - "3.3" - "3.4" + - "3.5" + - "3.6" env: - - "SLIMERJSLAUNCHER=$(which firefox) DISPLAY=:99.0 PATH=$TRAVIS_BUILD_DIR/slimerjs:$PATH" + global: + - "JDK=oracle8" -before_script: - - "sh -e /etc/init.d/xvfb start" - - "echo 'Installing Slimer'" - - "wget http://download.slimerjs.org/releases/0.9.3/slimerjs-0.9.3-linux-x86_64.tar.bz2" - - "tar xvf slimerjs-*.tar.bz2" - - "rm slimerjs-*.tar.bz2" - - "mv slimerjs-* slimerjs" +jdk: + - oraclejdk8 install: + - "sudo apt-get install -y oracle-java8-installer phantomjs libmozjs-24-bin" + - "sudo ln -s /usr/bin/js24 /usr/bin/js" - "./install_development.sh" - "python setup.py install" script: + - "jjs -v < /dev/null" + - "js --help" + - "node --version && node --help" + - "python -m execjs --print-available-runtimes" - "python setup.py test" diff --git a/README.md b/README.md index 3b7a601..e89124c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,20 @@ -PyExecJS -======== +PyExecJS (EOL) +============== [![Build Status](https://travis-ci.org/doloopwhile/PyExecJS.svg?branch=travis-ci)](https://travis-ci.org/doloopwhile/PyExecJS) +# End of life + +This library is no longer maintananced ([Reason](https://gist.github.com/doloopwhile/8c6ec7dd4703e8a44e559411cb2ea221)). +Bugs are not be fixed (even if they are trivial or essential). + +We suggest to use other library or to make a fork. + +--- + Run JavaScript code from Python. PyExecJS is a porting of ExecJS from Ruby. -PyExecJS automatically picks the best runtime available to evaluate your JavaScript program, -then returns the result to you as a Python object. +PyExecJS **automatically** picks the best runtime available to evaluate your JavaScript program. A short example: @@ -21,34 +29,21 @@ A short example: >>> ctx.call("add", 1, 2) 3 -Of course, you can pick particular JavaScript runtime by get() function: - - >>> default = execjs.get() # the automatically picked runtime - >>> default.eval("1 + 2") - 3 - >>> jscript = execjs.get("JScript") - >>> jscript.eval("1 + 2") - 3 - >>> node = execjs.get("Node") - >>> node.eval("1 + 2") - 3 - -If EXECJS_RUNTIME environment variable is specified, PyExecJS pick the JavaScript runtime as a default: - - >>> #execjs.get().name # this value is depends on your environment. - >>> os.environ["EXECJS_RUNTIME"] = "Node" - >>> execjs.get().name - 'Node.js (V8)' +# Supported runtimes -PyExecJS supports these runtimes: +## First-class support (runtime class is provided and tested) * [PyV8](http://code.google.com/p/pyv8/) - A python wrapper for Google V8 engine, * [Node.js](http://nodejs.org/) +* [PhantomJS](http://phantomjs.org/) +* [Nashorn](http://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/intro.html#sthref16) - Included with Oracle Java 8 + +## Second-class support (runtime class is privided but not tested) + * Apple JavaScriptCore - Included with Mac OS X -* [Mozilla SpiderMonkey](http://www.mozilla.org/js/spidermonkey/) * [Microsoft Windows Script Host](http://msdn.microsoft.com/en-us/library/9bbdkx3k.aspx) (JScript) * [SlimerJS](http://slimerjs.org/) -* [PhantomJS](http://phantomjs.org/) +* [Mozilla SpiderMonkey](http://www.mozilla.org/js/spidermonkey/) # Installation @@ -58,15 +53,70 @@ or $ easy_install PyExecJS +# Details + +If `EXECJS_RUNTIME` environment variable is specified, PyExecJS pick the JavaScript runtime as a default: + + >>> execjs.get().name # this value is depends on your environment. + >>> os.environ["EXECJS_RUNTIME"] = "Node" + >>> execjs.get().name + 'Node.js (V8)' + +You can choose JavaScript runtime by `execjs.get()`: + + >>> default = execjs.get() # the automatically picked runtime + >>> default.eval("1 + 2") + 3 + >>> import execjs.runtime_names + >>> jscript = execjs.get(execjs.runtime_names.JScript) + >>> jscript.eval("1 + 2") + 3 + >>> import execjs.runtime_names + >>> node = execjs.get(execjs.runtime_names.Node) + >>> node.eval("1 + 2") + 3 + +The pros of PyExecJS is that you do not need take care of JavaScript environment. +Especially, it works in Windows environment without installing extra libraries. + +One of cons of PyExecJS is performance. PyExecJS communicate JavaScript runtime by text and it is slow. +The other cons is that it does not fully support runtime specific features. + +[PyV8](https://code.google.com/p/pyv8/) might be better choice for some use case. # License -Copyright (c) 2012 Omoto Kenji. -Copyright (c) 2011 Sam Stephenson and Josh Peek. +Copyright (c) 2016 Omoto Kenji. +Copyright (c) 2011 Sam Stephenson and Josh Peek. (As a author of ExecJS) Released under the MIT license. See `LICENSE` for details. -# Changes +# Changelog + +## 1.5.0 +- Eased version requirement for six. + +## 1.4.1 +- Fixed arguments of module-level functions. +- Fixed bug of execution with pipe. +- Fixed wrong excption is raised. + +## 1.4.0 +- Fixed required libraries. +- Fixed order of output of `--print-available-runtimes`. +- Execute some JavaScript runtime with pipe/stdin (without temporary file). + +## 1.3.1 +- Fixed `--print-available-runtimes` fails in Python 2.7. + +## 1.3.0 +- Added `cwd` argument. + +## 1.2.0 +- Supported Python 3.5 +- Supported Nashorn(Java 8 JavaScript engine) as runtime +- Dropped support for Python 2.6 and 3.2 + ## 1.1.0 - Supported Python 3.4 - Supported SlimerJS as runtime diff --git a/README.rst b/README.rst index bd39ef9..e3a5e83 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,20 @@ -PyExecJS -======== +PyExecJS (EOL) +============== + +End of life +=========== + +This library is no longer maintananced. Bugs are not be fixed (even if +they are trivial or essential). + +We suggest to use other library or to make a fork. + +-------------- Run JavaScript code from Python. -PyExecJS is a porting of ExecJS from Ruby. PyExecJS automatically picks -the best runtime available to evaluate your JavaScript program, then -returns the result to you as a Python object. +PyExecJS is a porting of ExecJS from Ruby. PyExecJS **automatically** +picks the best runtime available to evaluate your JavaScript program. A short example: @@ -22,42 +31,28 @@ A short example: >>> ctx.call("add", 1, 2) 3 -Of course, you can pick particular JavaScript runtime by get() function: - -:: - - >>> default = execjs.get() # the automatically picked runtime - >>> default.eval("1 + 2") - 3 - >>> jscript = execjs.get("JScript") - >>> jscript.eval("1 + 2") - 3 - >>> node = execjs.get("Node") - >>> node.eval("1 + 2") - 3 - -If EXECJS\_RUNTIME environment variable is specified, PyExecJS pick the -JavaScript runtime as a default: +Supported runtimes +================== -:: - - >>> #execjs.get().name # this value is depends on your environment. - >>> os.environ["EXECJS_RUNTIME"] = "Node" - >>> execjs.get().name - 'Node.js (V8)' - -PyExecJS supports these runtimes: +First-class support (runtime class is provided and tested) +---------------------------------------------------------- - `PyV8 `__ - A python wrapper for Google V8 engine, - `Node.js `__ +- `PhantomJS `__ +- `Nashorn `__ + - Included with Oracle Java 8 + +Second-class support (runtime class is privided but not tested) +--------------------------------------------------------------- + - Apple JavaScriptCore - Included with Mac OS X -- `Mozilla SpiderMonkey `__ - `Microsoft Windows Script Host `__ (JScript) - `SlimerJS `__ -- `PhantomJS `__ +- `Mozilla SpiderMonkey `__ Installation ============ @@ -72,16 +67,93 @@ or $ easy_install PyExecJS +Details +======= + +If ``EXECJS_RUNTIME`` environment variable is specified, PyExecJS pick +the JavaScript runtime as a default: + +:: + + >>> execjs.get().name # this value is depends on your environment. + >>> os.environ["EXECJS_RUNTIME"] = "Node" + >>> execjs.get().name + 'Node.js (V8)' + +You can choose JavaScript runtime by ``execjs.get()``: + +:: + + >>> default = execjs.get() # the automatically picked runtime + >>> default.eval("1 + 2") + 3 + >>> import execjs.runtime_names + >>> jscript = execjs.get(execjs.runtime_names.JScript) + >>> jscript.eval("1 + 2") + 3 + >>> import execjs.runtime_names + >>> node = execjs.get(execjs.runtime_names.Node) + >>> node.eval("1 + 2") + 3 + +The pros of PyExecJS is that you do not need take care of JavaScript +environment. Especially, it works in Windows environment without +installing extra libraries. + +One of cons of PyExecJS is performance. PyExecJS communicate JavaScript +runtime by text and it is slow. The other cons is that it does not fully +support runtime specific features. + +`PyV8 `__ might be better choice for +some use case. + License ======= -Copyright (c) 2012 Omoto Kenji. Copyright (c) 2011 Sam Stephenson and -Josh Peek. +Copyright (c) 2016 Omoto Kenji. Copyright (c) 2011 Sam Stephenson and +Josh Peek. (As a author of ExecJS) Released under the MIT license. See ``LICENSE`` for details. -Changes -======= +Changelog +========= + +1.5.0 +----- + +- Eased version requirement for six. + +1.4.1 +----- + +- Fixed arguments of module-level functions. +- Fixed bug of execution with pipe. +- Fixed wrong excption is raised. + +1.4.0 +----- + +- Fixed required libraries. +- Fixed order of output of ``--print-available-runtimes``. +- Execute some JavaScript runtime with pipe/stdin (without temporary + file). + +1.3.1 +----- + +- Fixed ``--print-available-runtimes`` fails in Python 2.7. + +1.3.0 +----- + +- Added ``cwd`` argument. + +1.2.0 +----- + +- Supported Python 3.5 +- Supported Nashorn(Java 8 JavaScript engine) as runtime +- Dropped support for Python 2.6 and 3.2 1.1.0 ----- @@ -124,4 +196,3 @@ Changes ----- - First release. - diff --git a/execjs/__init__.py b/execjs/__init__.py index 3c3a07f..98a4bea 100755 --- a/execjs/__init__.py +++ b/execjs/__init__.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 # -*- coding: ascii -*- -from __future__ import unicode_literals, division, with_statement -''' - Run JavaScript code from Python. +'''r +Run JavaScript code from Python. - PyExecJS is a porting of ExecJS from Ruby. - PyExecJS automatically picks the best runtime available to evaluate your JavaScript program, - then returns the result to you as a Python object. +PyExecJS is a porting of ExecJS from Ruby. +PyExecJS automatically picks the best runtime available to evaluate your JavaScript program, +then returns the result to you as a Python object. - A short example: +A short example: >>> import execjs >>> execjs.eval("'red yellow blue'.split(' ')") @@ -21,554 +20,43 @@ >>> ctx.call("add", 1, 2) 3 ''' +from __future__ import unicode_literals, division, with_statement -import os -import os.path -import re -import stat -import io -import platform -import tempfile -from subprocess import Popen, PIPE, STDOUT -import json +from execjs._exceptions import ( + Error, + RuntimeError, + ProgramError, + RuntimeUnavailableError, +) + +import execjs._runtimes +from execjs._external_runtime import ExternalRuntime +from execjs._abstract_runtime import AbstractRuntime -try: - from collections import OrderedDict -except ImportError: - from ordereddict import OrderedDict __all__ = """ get register runtimes get_from_environment exec_ eval compile - ExternalRuntime Context - Error RuntimeError ProgramError RuntimeUnavailable + ExternalRuntime + Error RuntimeError ProgramError RuntimeUnavailableError """.split() -class Error(Exception): - pass - - -class RuntimeError(Error): - pass - - -class ProgramError(Error): - pass - - -class RuntimeUnavailable(RuntimeError): - pass - - -def register(name, runtime): - '''Register a JavaScript runtime.''' - _runtimes[name] = runtime - - -def get(name=None): - """ - Return a appropriate JavaScript runtime. - If name is specified, return the runtime. - """ - if name is None: - return _auto_detect() - - try: - runtime = runtimes()[name] - except KeyError: - raise RuntimeUnavailable("{name} runtime is not defined".format(name=name)) - else: - if not runtime.is_available(): - raise RuntimeUnavailable( - "{name} runtime is not available on this system".format(name=runtime.name)) - return runtime - - -def runtimes(): - """return a dictionary of all supported JavaScript runtimes.""" - return dict(_runtimes) - - -def available_runtimes(): - """return a dictionary of all supported JavaScript runtimes which is usable""" - return dict((name, runtime) for name, runtime in _runtimes.items() if runtime.is_available()) - - -def _auto_detect(): - runtime = get_from_environment() - if runtime is not None: - return runtime - - for runtime in _runtimes.values(): - if runtime.is_available(): - return runtime - - raise RuntimeUnavailable("Could not find a JavaScript runtime.") - - -def get_from_environment(): - ''' - Return the JavaScript runtime that is specified in EXECJS_RUNTIME environment variable. - If EXECJS_RUNTIME environment variable is empty or invalid, return None. - ''' - try: - name = os.environ["EXECJS_RUNTIME"] - except KeyError: - return None - - if not name: - #name is None or empty str - return None - return get(name) - - -def eval(source): - return get().eval(source) - - -def exec_(source): - return get().exec_(source) - - -def compile(source): - return get().compile(source) - - -def _root(): - return os.path.abspath(os.path.dirname(__file__)) - - -def _is_windows(): - return platform.system() == 'Windows' - - -def _json2_source(): - # The folowing code is json2.js(https://github.com/douglascrockford/JSON-js). - # It is compressed by YUI Compressor Online(http://yui.2clics.net/). - - return 'var JSON;if(!JSON){JSON={}}(function(){function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(key){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf()}}var cx=/[\\u0000\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g,escapable=/[\\\\\\"\\x00-\\x1f\\x7f-\\x9f\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g,gap,indent,meta={"\\b":"\\\\b","\\t":"\\\\t","\\n":"\\\\n","\\f":"\\\\f","\\r":"\\\\r",\'"\':\'\\\\"\',"\\\\":"\\\\\\\\"},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?\'"\'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+\'"\':\'"\'+string+\'"\'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i>> encode_unicode_codepoints("a") == 'a' - True - >>> ascii = ''.join(chr(i) for i in range(0x80)) - >>> encode_unicode_codepoints(ascii) == ascii - True - >>> encode_unicode_codepoints('\u4e16\u754c') == '\\u4e16\\u754c' - True - """ - codepoint_format = '\\u{0:04x}'.format - - def codepoint(m): - return codepoint_format(ord(m.group(0))) - - return re.sub('[^\x00-\x7f]', codepoint, str) - - -class PyV8Runtime: - def __init__(self): - try: - import PyV8 - except ImportError: - self._is_available = False - else: - self._is_available = True - - @property - def name(self): - return "PyV8" - - def exec_(self, source): - return self.Context().exec_(source) - - def eval(self, source): - return self.Context().eval(source) - - def compile(self, source): - return self.Context(source) - - def is_available(self): - return self._is_available - - class Context: - def __init__(self, source=""): - self._source = source - - def exec_(self, source): - source = '''\ - (function() {{ - {0}; - {1}; - }})()'''.format( - encode_unicode_codepoints(self._source), - encode_unicode_codepoints(source) - ) - source = str(source) - - import PyV8 - import contextlib - #backward compatibility - with contextlib.nested(PyV8.JSContext(), PyV8.JSEngine()) as (ctxt, engine): - js_errors = (PyV8.JSError, IndexError, ReferenceError, SyntaxError, TypeError) - try: - script = engine.compile(source) - except js_errors as e: - raise RuntimeError(e) - try: - value = script.run() - except js_errors as e: - raise ProgramError(e) - return self.convert(value) - - def eval(self, source): - return self.exec_('return ' + encode_unicode_codepoints(source)) - - def call(self, identifier, *args): - args = json.dumps(args) - return self.eval("{identifier}.apply(this, {args})".format(identifier=identifier, args=args)) - - @classmethod - def convert(cls, obj): - from PyV8 import _PyV8 - if isinstance(obj, bytes): - return obj.decode('utf8') - if isinstance(obj, _PyV8.JSArray): - return [cls.convert(v) for v in obj] - elif isinstance(obj, _PyV8.JSFunction): - return None - elif isinstance(obj, _PyV8.JSObject): - ret = {} - for k in obj.keys(): - v = cls.convert(obj[k]) - if v is not None: - ret[cls.convert(k)] = v - return ret - else: - return obj - - -_runtimes = OrderedDict() -_runtimes['PyV8'] = PyV8Runtime() - -for command in ["nodejs", "node"]: - _runtimes["Node"] = runtime = ExternalRuntime( - name="Node.js (V8)", - command=[command], - encoding='UTF-8', - runner_source=r"""(function(program, execJS) { execJS(program) })(function() { #{source} -}, function(program) { - var output; - var print = function(string) { - process.stdout.write('' + string + '\n'); - }; - try { - result = program(); - print('') - if (typeof result == 'undefined' && result !== null) { - print('["ok"]'); - } else { - try { - print(JSON.stringify(['ok', result])); - } catch (err) { - print('["err"]'); - } - } - } catch (err) { - print(JSON.stringify(['err', '' + err])); - } -});""", - ) - if runtime.is_available(): - break - - -_runtimes['JavaScriptCore'] = ExternalRuntime( - name="JavaScriptCore", - command=["/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc"], - runner_source=r"""(function(program, execJS) { execJS(program) })(function() { - return eval(#{encoded_source}); -}, function(program) { - var output; - try { - result = program(); - print(""); - if (typeof result == 'undefined' && result !== null) { - print('["ok"]'); - } else { - try { - print(JSON.stringify(['ok', result])); - } catch (err) { - print('["err"]'); - } - } - } catch (err) { - print(JSON.stringify(['err', '' + err])); - } -}); -""" -) - - -_runtimes['SpiderMonkey'] = _runtimes['Spidermonkey'] = ExternalRuntime( - name="SpiderMonkey", - command=["js"], - runner_source=r"""(function(program, execJS) { execJS(program) })(function() { #{source} -}, function(program) { - #{json2_source} - var output; - try { - result = program(); - print(""); - if (typeof result == 'undefined' && result !== null) { - print('["ok"]'); - } else { - try { - print(JSON.stringify(['ok', result])); - } catch (err) { - print('["err"]'); - } - } - } catch (err) { - print(JSON.stringify(['err', '' + err])); - } -}); -""") - - -_runtimes['JScript'] = ExternalRuntime( - name="JScript", - command=["cscript", "//E:jscript", "//Nologo"], - encoding="ascii", - runner_source=r"""(function(program, execJS) { execJS(program) })(function() { - return eval(#{encoded_source}); -}, function(program) { - #{json2_source} - var output, print = function(string) { - string = string.replace(/[^\x00-\x7f]/g, function(ch){ - return '\\u' + ('0000' + ch.charCodeAt(0).toString(16)).slice(-4); - }); - WScript.Echo(string); - }; - try { - result = program(); - print("") - if (typeof result == 'undefined' && result !== null) { - print('["ok"]'); - } else { - try { - print(JSON.stringify(['ok', result])); - } catch (err) { - print('["err"]'); - } - } - } catch (err) { - print(JSON.stringify(['err', err.name + ': ' + err.message])); - } -}); -""" -) +def exec_(source, cwd=None): + return get().exec_(source, cwd) +exec_.__doc__ = AbstractRuntime.exec_.__doc__ -for _name, _command in [ - ['PhantomJS', 'phantomjs'], - ['SlimerJS', 'slimerjs'], -]: - _runtimes[_name] = ExternalRuntime( - name=_name, - command=[_command], - runner_source=r""" -(function(program, execJS) { execJS(program) })(function() { - return eval(#{encoded_source}); -}, function(program) { - var output; - var print = function(string) { - console.log('' + string); - }; - try { - result = program(); - print('') - if (typeof result == 'undefined' && result !== null) { - print('["ok"]'); - } else { - try { - print(JSON.stringify(['ok', result])); - } catch (err) { - print('["err"]'); - } - } - } catch (err) { - print(JSON.stringify(['err', '' + err])); - } -}); -phantom.exit(); -""") +def compile(source, cwd=None): + return get().compile(source, cwd) +compile.__doc__ = AbstractRuntime.compile.__doc__ diff --git a/execjs/__main__.py b/execjs/__main__.py index 07eac07..28cf92f 100755 --- a/execjs/__main__.py +++ b/execjs/__main__.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 # -*- coding: ascii -*- +from __future__ import unicode_literals import sys import io from argparse import ArgumentParser, Action, SUPPRESS @@ -18,11 +19,8 @@ def __init__(self, option_strings, dest=SUPPRESS, default=SUPPRESS, help=None): ) def __call__(self, parser, namespace, values, option_string=None): - buffer = io.StringIO() - for name, runtime in sorted(execjs.runtimes().items()): - if runtime.is_available(): - buffer.write(name + "\n") - parser.exit(message=buffer.getvalue()) + message = "".join(name + "\n" for name, runtime in execjs.runtimes().items() if runtime.is_available()) + parser.exit(message=message) def main(): diff --git a/execjs/_abstract_runtime.py b/execjs/_abstract_runtime.py new file mode 100644 index 0000000..3096b4b --- /dev/null +++ b/execjs/_abstract_runtime.py @@ -0,0 +1,43 @@ +from abc import ABCMeta, abstractmethod +import six +import execjs._exceptions as exceptions + + +@six.add_metaclass(ABCMeta) +class AbstractRuntime(object): + ''' + Abstract base class for runtime class. + ''' + def exec_(self, source, cwd=None): + '''Execute source by JavaScript runtime and return all output to stdout as a string. + + source -- JavaScript code to execute. + cwd -- Directory where call JavaScript runtime. It may be ignored in some derived class. + ''' + return self.compile('', cwd=cwd).exec_(source) + + def eval(self, source, cwd=None): + '''Evaluate source in JavaScript runtime. + + source -- JavaScript code to evaluate. + cwd -- Directory where call JavaScript runtime. It may be ignored in some derived class. + ''' + return self.compile('', cwd=cwd).eval(source) + + def compile(self, source, cwd=None): + '''Bulk source as a context object. The source can be used to execute another code. + + source -- JavaScript code to bulk. + cwd -- Directory where call JavaScript runtime. It may be ignored in some derived class. + ''' + if not self.is_available(): + raise exceptions.RuntimeUnavailableError + return self._compile(source, cwd=cwd) + + @abstractmethod + def is_available(self): + raise NotImplementedError + + @abstractmethod + def _compile(self, source, cwd=None): + raise NotImplementedError diff --git a/execjs/_abstract_runtime_context.py b/execjs/_abstract_runtime_context.py new file mode 100644 index 0000000..f645cf5 --- /dev/null +++ b/execjs/_abstract_runtime_context.py @@ -0,0 +1,53 @@ +import execjs +from abc import ABCMeta, abstractmethod +import six + + +@six.add_metaclass(ABCMeta) +class AbstractRuntimeContext(object): + ''' + Abstract base class for runtime context class. + ''' + def exec_(self, source): + '''Execute source by JavaScript runtime and return all output to stdout as a string. + + source -- JavaScript code to execute. + ''' + if not self.is_available(): + raise execjs.RuntimeUnavailableError + return self._exec_(source) + + def eval(self, source): + '''Evaluate source in JavaScript runtime. + + source -- JavaScript code to evaluate. + ''' + if not self.is_available(): + raise execjs.RuntimeUnavailableError + return self._eval(source) + + def call(self, name, *args): + '''Call a JavaScript function in context. + + name -- Name of funtion object to call + args -- Arguments for the funtion object + ''' + if not self.is_available(): + raise execjs.RuntimeUnavailableError + return self._call(name, *args) + + @abstractmethod + def is_available(self): + raise NotImplementedError + + @abstractmethod + def _exec_(self, source): + raise NotImplementedError + + @abstractmethod + def _eval(self, source): + raise NotImplementedError + + @abstractmethod + def _call(self, name, *args): + raise NotImplementedError diff --git a/execjs/_exceptions.py b/execjs/_exceptions.py new file mode 100644 index 0000000..0723550 --- /dev/null +++ b/execjs/_exceptions.py @@ -0,0 +1,24 @@ +# Abstract base error classes +class Error(Exception): + pass + +# Abstract class that represents errors of runtime engine. +# ex. Specified runtime engine is not installed, runtime engine aborted (by its bugs). +# By the way "RuntimeError" is bad name because it is confusing with the standard exception. +class RuntimeError(Error): + pass + +# Concrete runtime error classes +class RuntimeUnavailableError(RuntimeError): pass + +class ProcessExitedWithNonZeroStatus(RuntimeError): + def __init__(self, status, stdout, stderr): + RuntimeError.__init__(self, status, stdout, stderr) + self.status = status + self.stdout = stdout + self.stderr = stderr + +# Errors due to JS script. +# ex. Script has syntax error, executed and raised exception. +class ProgramError(Error): + pass diff --git a/execjs/_external_runtime.py b/execjs/_external_runtime.py new file mode 100644 index 0000000..2b05c16 --- /dev/null +++ b/execjs/_external_runtime.py @@ -0,0 +1,294 @@ +from subprocess import Popen, PIPE +import io +import json +import os +import os.path +import platform +import re +import stat +import sys +import tempfile +import six +import execjs._json2 as _json2 +import execjs._runner_sources as _runner_sources + +from execjs._exceptions import ( + ProcessExitedWithNonZeroStatus, + ProgramError +) + +from execjs._abstract_runtime import AbstractRuntime +from execjs._abstract_runtime_context import AbstractRuntimeContext +from execjs._misc import encode_unicode_codepoints + + +class ExternalRuntime(AbstractRuntime): + '''Runtime to execute codes with external command.''' + def __init__(self, name, command, runner_source, encoding='utf8', tempfile=False): + self._name = name + if isinstance(command, str): + command = [command] + self._command = command + self._runner_source = runner_source + self._encoding = encoding + self._tempfile = tempfile + + self._available = self._binary() is not None + + def __str__(self): + return "{class_name}({runtime_name})".format( + class_name=type(self).__name__, + runtime_name=self._name, + ) + + @property + def name(self): + return self._name + + def is_available(self): + return self._available + + def _compile(self, source, cwd=None): + return self.Context(self, source, cwd=cwd, tempfile=self._tempfile) + + def _binary(self): + if not hasattr(self, "_binary_cache"): + self._binary_cache = _which(self._command) + return self._binary_cache + + class Context(AbstractRuntimeContext): + # protected + + def __init__(self, runtime, source='', cwd=None, tempfile=False): + self._runtime = runtime + self._source = source + self._cwd = cwd + self._tempfile = tempfile + + def is_available(self): + return self._runtime.is_available() + + def _eval(self, source): + if not source.strip(): + data = "''" + else: + data = "'('+" + json.dumps(source, ensure_ascii=True) + "+')'" + + code = 'return eval({data})'.format(data=data) + return self.exec_(code) + + def _exec_(self, source): + if self._source: + source = self._source + '\n' + source + + if self._tempfile: + output = self._exec_with_tempfile(source) + else: + output = self._exec_with_pipe(source) + return self._extract_result(output) + + def _call(self, identifier, *args): + args = json.dumps(args) + return self._eval("{identifier}.apply(this, {args})".format(identifier=identifier, args=args)) + + def _exec_with_pipe(self, source): + cmd = self._runtime._binary() + + p = None + try: + p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=self._cwd, universal_newlines=True) + input = self._compile(source) + if six.PY2: + input = input.encode(sys.getfilesystemencoding()) + stdoutdata, stderrdata = p.communicate(input=input) + ret = p.wait() + finally: + del p + + self._fail_on_non_zero_status(ret, stdoutdata, stderrdata) + return stdoutdata + + def _exec_with_tempfile(self, source): + (fd, filename) = tempfile.mkstemp(prefix='execjs', suffix='.js') + os.close(fd) + try: + with io.open(filename, "w+", encoding=self._runtime._encoding) as fp: + fp.write(self._compile(source)) + cmd = self._runtime._binary() + [filename] + + p = None + try: + p = Popen(cmd, stdout=PIPE, stderr=PIPE, cwd=self._cwd, universal_newlines=True) + stdoutdata, stderrdata = p.communicate() + ret = p.wait() + finally: + del p + + self._fail_on_non_zero_status(ret, stdoutdata, stderrdata) + return stdoutdata + finally: + os.remove(filename) + + def _fail_on_non_zero_status(self, status, stdoutdata, stderrdata): + if status != 0: + raise ProcessExitedWithNonZeroStatus(status=status, stdout=stdoutdata, stderr=stderrdata) + + def _compile(self, source): + runner_source = self._runtime._runner_source + + replacements = { + '#{source}': lambda: source, + '#{encoded_source}': lambda: json.dumps( + "(function(){ " + + encode_unicode_codepoints(source) + + " })()" + ), + '#{json2_source}': _json2._json2_source, + } + + pattern = "|".join(re.escape(k) for k in replacements) + + runner_source = re.sub(pattern, lambda m: replacements[m.group(0)](), runner_source) + + return runner_source + + def _extract_result(self, output): + output = output.replace("\r\n", "\n").replace("\r", "\n") + output_last_line = output.split("\n")[-2] + + ret = json.loads(output_last_line) + if len(ret) == 1: + ret = [ret[0], None] + status, value = ret + + if status == "ok": + return value + else: + raise ProgramError(value) + + +def _is_windows(): + """protected""" + return platform.system() == 'Windows' + + +def _decode_if_not_text(s): + """protected""" + if isinstance(s, six.text_type): + return s + return s.decode(sys.getfilesystemencoding()) + + +def _find_executable(prog, pathext=("",)): + """protected""" + pathlist = _decode_if_not_text(os.environ.get('PATH', '')).split(os.pathsep) + + for dir in pathlist: + for ext in pathext: + filename = os.path.join(dir, prog + ext) + try: + st = os.stat(filename) + except os.error: + continue + if stat.S_ISREG(st.st_mode) and (stat.S_IMODE(st.st_mode) & 0o111): + return filename + return None + + +def _which(command): + """protected""" + if isinstance(command, str): + command = [command] + command = list(command) + name = command[0] + args = command[1:] + + if _is_windows(): + pathext = _decode_if_not_text(os.environ.get("PATHEXT", "")) + path = _find_executable(name, pathext.split(os.pathsep)) + else: + path = _find_executable(name) + + if not path: + return None + return [path] + args + + +def node(): + r = node_node() + if r.is_available(): + return r + return node_nodejs() + + +def node_node(): + return ExternalRuntime( + name="Node.js (V8)", + command=['node'], + encoding='UTF-8', + runner_source=_runner_sources.Node + ) + + +def node_nodejs(): + return ExternalRuntime( + name="Node.js (V8)", + command=['nodejs'], + encoding='UTF-8', + runner_source=_runner_sources.Node + ) + + +def jsc(): + return ExternalRuntime( + name="JavaScriptCore", + command=["/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc"], + runner_source=_runner_sources.JavaScriptCore, + tempfile=True + ) + + +def spidermonkey(): + return ExternalRuntime( + name="SpiderMonkey", + command=["js"], + runner_source=_runner_sources.SpiderMonkey, + tempfile=True + ) + + +def jscript(): + return ExternalRuntime( + name="JScript", + command=["cscript", "//E:jscript", "//Nologo"], + encoding="ascii", + runner_source=_runner_sources.JScript, + tempfile=True + ) + + +def phantomjs(): + return ExternalRuntime( + name="PhantomJS", + command=["phantomjs"], + runner_source=_runner_sources.PhantomJS, + tempfile=True + ) + + +def slimerjs(): + return ExternalRuntime( + name="SlimerJS", + command=["slimerjs"], + runner_source=_runner_sources.SlimerJS, + tempfile=True + ) + + +def nashorn(): + return ExternalRuntime( + name="Nashorn", + command=["jjs"], + runner_source=_runner_sources.Nashorn, + tempfile=True + ) diff --git a/execjs/_json2.py b/execjs/_json2.py new file mode 100644 index 0000000..a6e4f81 --- /dev/null +++ b/execjs/_json2.py @@ -0,0 +1,5 @@ +def _json2_source(): + # The folowing code is json2.js(https://github.com/douglascrockford/JSON-js). + # It is compressed by YUI Compressor Online(http://yui.2clics.net/). + + return 'var JSON;if(!JSON){JSON={}}(function(){function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(key){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf()}}var cx=/[\\u0000\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g,escapable=/[\\\\\\"\\x00-\\x1f\\x7f-\\x9f\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g,gap,indent,meta={"\\b":"\\\\b","\\t":"\\\\t","\\n":"\\\\n","\\f":"\\\\f","\\r":"\\\\r",\'"\':\'\\\\"\',"\\\\":"\\\\\\\\"},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?\'"\'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+\'"\':\'"\'+string+\'"\'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i>> encode_unicode_codepoints("a") == 'a' + True + >>> ascii = ''.join(chr(i) for i in range(0x80)) + >>> encode_unicode_codepoints(ascii) == ascii + True + >>> encode_unicode_codepoints('\u4e16\u754c') == '\\u4e16\\u754c' + True + """ + codepoint_format = '\\u{0:04x}'.format + + def codepoint(m): + return codepoint_format(ord(m.group(0))) + + return re.sub('[^\x00-\x7f]', codepoint, str) diff --git a/execjs/_pyv8runtime.py b/execjs/_pyv8runtime.py new file mode 100644 index 0000000..06ddcb7 --- /dev/null +++ b/execjs/_pyv8runtime.py @@ -0,0 +1,86 @@ +import json + +import execjs._exceptions as exceptions +from execjs._abstract_runtime import AbstractRuntime +from execjs._abstract_runtime_context import AbstractRuntimeContext +from execjs._misc import encode_unicode_codepoints + +try: + import PyV8 +except ImportError: + _pyv8_available = False +else: + _pyv8_available = True + + +class PyV8Runtime(AbstractRuntime): + '''Runtime to execute codes with PyV8.''' + def __init__(self): + pass + + @property + def name(self): + return "PyV8" + + def _compile(self, source, cwd=None): + return self.Context(source) + + def is_available(self): + return _pyv8_available + + class Context(AbstractRuntimeContext): + def __init__(self, source=""): + self._source = source + + def is_available(self): + return _pyv8_available + + def _exec_(self, source): + source = '''\ + (function() {{ + {0}; + {1}; + }})()'''.format( + encode_unicode_codepoints(self._source), + encode_unicode_codepoints(source) + ) + source = str(source) + + # backward compatibility + with PyV8.JSContext() as ctxt, PyV8.JSEngine() as engine: + js_errors = (PyV8.JSError, IndexError, ReferenceError, SyntaxError, TypeError) + try: + script = engine.compile(source) + except js_errors as e: + raise exceptions.ProgramError(e) + try: + value = script.run() + except js_errors as e: + raise exceptions.ProgramError(e) + return self.convert(value) + + def _eval(self, source): + return self.exec_('return ' + encode_unicode_codepoints(source)) + + def _call(self, identifier, *args): + args = json.dumps(args) + return self.eval("{identifier}.apply(this, {args})".format(identifier=identifier, args=args)) + + @classmethod + def convert(cls, obj): + from PyV8 import _PyV8 + if isinstance(obj, bytes): + return obj.decode('utf8') + if isinstance(obj, _PyV8.JSArray): + return [cls.convert(v) for v in obj] + elif isinstance(obj, _PyV8.JSFunction): + return None + elif isinstance(obj, _PyV8.JSObject): + ret = {} + for k in obj.keys(): + v = cls.convert(obj[k]) + if v is not None: + ret[cls.convert(k)] = v + return ret + else: + return obj diff --git a/execjs/_runner_sources.py b/execjs/_runner_sources.py new file mode 100644 index 0000000..a90a745 --- /dev/null +++ b/execjs/_runner_sources.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# -*- coding: ascii -*- +from __future__ import unicode_literals, division, with_statement + + +Node = r"""(function(program, execJS) { execJS(program) })(function() { #{source} +}, function(program) { + var output; + var print = function(string) { + process.stdout.write('' + string + '\n'); + }; + try { + result = program(); + print('') + if (typeof result == 'undefined' && result !== null) { + print('["ok"]'); + } else { + try { + print(JSON.stringify(['ok', result])); + } catch (err) { + print('["err"]'); + } + } + } catch (err) { + print(JSON.stringify(['err', '' + err])); + } +});""" + + +JavaScriptCore = r"""(function(program, execJS) { execJS(program) })(function() { + return eval(#{encoded_source}); +}, function(program) { + var output; + try { + result = program(); + print(""); + if (typeof result == 'undefined' && result !== null) { + print('["ok"]'); + } else { + try { + print(JSON.stringify(['ok', result])); + } catch (err) { + print('["err"]'); + } + } + } catch (err) { + print(JSON.stringify(['err', '' + err])); + } +}); +""" + +SpiderMonkey = r"""(function(program, execJS) { execJS(program) })(function() { #{source} +}, function(program) { + #{json2_source} + var output; + try { + result = program(); + print(""); + if (typeof result == 'undefined' && result !== null) { + print('["ok"]'); + } else { + try { + print(JSON.stringify(['ok', result])); + } catch (err) { + print('["err"]'); + } + } + } catch (err) { + print(JSON.stringify(['err', '' + err])); + } +}); +""" +Nashorn = SpiderMonkey + +JScript = r"""(function(program, execJS) { execJS(program) })(function() { + return eval(#{encoded_source}); +}, function(program) { + #{json2_source} + var output, print = function(string) { + string = string.replace(/[^\x00-\x7f]/g, function(ch){ + return '\\u' + ('0000' + ch.charCodeAt(0).toString(16)).slice(-4); + }); + WScript.Echo(string); + }; + try { + result = program(); + print("") + if (typeof result == 'undefined' && result !== null) { + print('["ok"]'); + } else { + try { + print(JSON.stringify(['ok', result])); + } catch (err) { + print('["err"]'); + } + } + } catch (err) { + print(JSON.stringify(['err', err.name + ': ' + err.message])); + } +}); +""" + +PhantomJS = r""" +(function(program, execJS) { execJS(program) })(function() { + return eval(#{encoded_source}); +}, function(program) { + var output; + var print = function(string) { + console.log('' + string); + }; + try { + result = program(); + print('') + if (typeof result == 'undefined' && result !== null) { + print('["ok"]'); + } else { + try { + print(JSON.stringify(['ok', result])); + } catch (err) { + print('["err"]'); + } + } + } catch (err) { + print(JSON.stringify(['err', '' + err])); + } +}); +phantom.exit(); +""" + +SlimerJS = PhantomJS diff --git a/execjs/_runtimes.py b/execjs/_runtimes.py new file mode 100644 index 0000000..a3ccd85 --- /dev/null +++ b/execjs/_runtimes.py @@ -0,0 +1,74 @@ +import os.path +from collections import OrderedDict + +import execjs.runtime_names as runtime_names +import execjs._external_runtime as external_runtime +import execjs._pyv8runtime as pyv8runtime +import execjs._exceptions as exceptions + + +def register(name, runtime): + '''Register a JavaScript runtime.''' + _runtimes.append((name, runtime)) + + +def get(name=None): + """ + Return a appropriate JavaScript runtime. + If name is specified, return the runtime. + """ + if name is None: + return get_from_environment() or _find_available_runtime() + return _find_runtime_by_name(name) + + +def runtimes(): + """return a dictionary of all supported JavaScript runtimes.""" + return OrderedDict(_runtimes) + + +def get_from_environment(): + ''' + Return the JavaScript runtime that is specified in EXECJS_RUNTIME environment variable. + If EXECJS_RUNTIME environment variable is empty or invalid, return None. + ''' + name = os.environ.get("EXECJS_RUNTIME", "") + if not name: + return None + + try: + return _find_runtime_by_name(name) + except exceptions.RuntimeUnavailableError: + return None + + +def _find_available_runtime(): + for _, runtime in _runtimes: + if runtime.is_available(): + return runtime + raise exceptions.RuntimeUnavailableError("Could not find an available JavaScript runtime.") + + +def _find_runtime_by_name(name): + for runtime_name, runtime in _runtimes: + if runtime_name.lower() == name.lower(): + break + else: + raise exceptions.RuntimeUnavailableError("{name} runtime is not defined".format(name=name)) + + if not runtime.is_available(): + raise exceptions.RuntimeUnavailableError( + "{name} runtime is not available on this system".format(name=runtime.name)) + return runtime + + +_runtimes = [] + +register(runtime_names.PyV8, pyv8runtime.PyV8Runtime()) +register(runtime_names.Node, external_runtime.node()) +register(runtime_names.JavaScriptCore, external_runtime.jsc()) +register(runtime_names.SpiderMonkey, external_runtime.spidermonkey()) +register(runtime_names.JScript, external_runtime.jscript()) +register(runtime_names.PhantomJS, external_runtime.phantomjs()) +register(runtime_names.SlimerJS, external_runtime.slimerjs()) +register(runtime_names.Nashorn, external_runtime.nashorn()) diff --git a/execjs/runtime_names.py b/execjs/runtime_names.py new file mode 100644 index 0000000..cb85226 --- /dev/null +++ b/execjs/runtime_names.py @@ -0,0 +1,8 @@ +PyV8 = "PyV8" +Node = "Node" +JavaScriptCore = "JavaScriptCore" +SpiderMonkey = "SpiderMonkey" +JScript = "JScript" +PhantomJS = "PhantomJS" +SlimerJS = "SlimerJS" +Nashorn = "Nashorn" diff --git a/setup.py b/setup.py index 574782c..68689ea 100755 --- a/setup.py +++ b/setup.py @@ -1,23 +1,19 @@ #!/usr/bin/env python3 # -*- coding: ascii -*- from __future__ import division, with_statement -version = '1.1.0' +from setuptools import setup, find_packages +import sys +import io + +version = '1.5.1' author = "Omoto Kenji" license = "MIT License" author_email = 'doloopwhile@gmail.com' -from setuptools import setup, find_packages -import sys -import io with io.open('README.rst', encoding='ascii') as fp: long_description = fp.read() -if sys.version_info < (2, 7): - install_requires = "argparse ordereddict".split() -else: - install_requires = [] - setup( packages=find_packages(), include_package_data=True, @@ -36,14 +32,13 @@ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Programming Language :: JavaScript', ], - install_requires=install_requires, + install_requires=["six >= 1.10.0"], test_suite="test_execjs", ) diff --git a/test_execjs.py b/test_execjs.py index dce8d00..146865a 100755 --- a/test_execjs.py +++ b/test_execjs.py @@ -3,15 +3,16 @@ from __future__ import unicode_literals import sys import os +import doctest +import six + +import execjs if sys.version_info < (2, 7): import unittest2 as unittest else: import unittest -import doctest -import execjs - class RuntimeTestBase: def test_context_call(self): @@ -24,7 +25,7 @@ def test_nested_context_call(self): def test_context_call_missing_function(self): context = self.runtime.compile("") - with self.assertRaises(execjs.ProgramError): + with self.assertRaises(execjs.Error): context.call("missing") def test_exec(self): @@ -74,11 +75,11 @@ def test_compile_large_scripts(self): self.assertTrue(self.runtime.exec_(code)) def test_syntax_error(self): - with self.assertRaises(execjs.RuntimeError): + with self.assertRaises(execjs.Error): self.runtime.exec_(")") def test_thrown_exception(self): - with self.assertRaises(execjs.ProgramError): + with self.assertRaises(execjs.Error): self.runtime.exec_("throw 'hello'") def test_broken_substitutions(self): @@ -90,18 +91,17 @@ class DefaultRuntimeTest(unittest.TestCase, RuntimeTestBase): def setUp(self): self.runtime = execjs +class NodeRuntimeTest(unittest.TestCase, RuntimeTestBase): + def setUp(self): + self.runtime = execjs.get('Node') -for name, runtime in execjs.available_runtimes().items(): - class_name = name.capitalize() + "RuntimeTest" - - def f(runtime=runtime): - class RuntimeTest(unittest.TestCase, RuntimeTestBase): - def setUp(self): - self.runtime = runtime - RuntimeTest.__name__ = str(class_name) # 2.x compatibility - return RuntimeTest - exec("{class_name} = f()".format(class_name=class_name)) +class NashornRuntimeTest(unittest.TestCase, RuntimeTestBase): + def setUp(self): + self.runtime = execjs.get('Nashorn') +class PhantomJSRuntimeTest(unittest.TestCase, RuntimeTestBase): + def setUp(self): + self.runtime = execjs.get('PhantomJS') class CommonTest(unittest.TestCase): def test_empty_path_environ(self): @@ -126,9 +126,14 @@ def test_runtime_availability(self): r = execjs.ExternalRuntime("success", ["python"], "") self.assertTrue(r.is_available()) + def test_attributes_export(self): + for name in execjs.__all__: + self.assertTrue(hasattr(execjs, name), "{} is not defined".format(name)) + def load_tests(loader, tests, ignore): - tests.addTests(doctest.DocTestSuite(execjs)) + if six.PY3: + tests.addTests(doctest.DocTestSuite(execjs)) return tests