Skip to content

UlionTse/exejs

Repository files navigation

ExeJS

Run JavaScript from Python — the modern successor to PyExecJS.

Zero dependencies · Multi-runtime auto-detection · Async-ready · No temp files · Timeout guard

PyPI - Version Conda - Version PyPI - Python PyPI - License PyPI - Status PyPI - Wheel PyPI - Downloads


Table of Contents

Features

  • Multi-runtime — auto-detects among Node, JavaScriptCore, SpiderMonkey, JScript, PhantomJS, SlimerJS, and Nashorn.
  • Zero dependencies — built on the Python standard library; pip install exejs and you're done.
  • Async-readyevaluate_async / execute_async / call_async built on asyncio.
  • No temp files — compiles and runs via stdin (except JScript), so it won't trigger antivirus alerts.
  • Timeout guardtimeout parameter kills runaway scripts instead of hanging forever.
  • Structured argscall() auto JSON-serializes objects and supports property paths like calc.mul.
  • Typed — ships py.typed, fully annotated, Python 3.9+.

Why ExeJS

PyExecJS has been EOL since 2018. ExeJS exists to fix three concrete pain points:

  1. PyExecJS is unmaintained, and downstream builds that depend on it fail or get cancelled. (Issue#1, Issue#2)
  2. PyExecJS writes compiled code to a temp file by default, which triggers antivirus alerts and blocks execution. (Issue#3)
  3. No async API, no timeout control, still carries Python 2 baggage.

Comparison

PyExecJS (EOL 2018) ExeJS
Maintained stopped 2018 active
Python 2 yes no (3.9+)
Async API yes
Temp files yes (antivirus alerts) no (stdin, except JScript)
Timeout yes
Context reuse (compile/call) limited yes
Structured args (auto JSON, property paths) yes
Dependencies some (six) zero
Typed (py.typed) yes

Requirements

  • Python >= 3.9
  • A JavaScript runtime installed and on PATH — Node.js is the recommended default. See Supported Runtimes for the full list and install hints.

Installation

# PyPI
pip install --upgrade exejs

# Conda
conda install conda-forge::exejs

# From source
git clone https://github.com/UlionTse/exejs.git
cd exejs
pip install .

Quick Start

import exejs

exejs.evaluate("[1, 2, 3].map(x => x * 2)")  # [2, 4, 6]

# evaluate (one-shot expression)
exejs.evaluate("'red yellow blue'.split(' ')")  # ['red', 'yellow', 'blue']

# execute (one-shot statements; use `return` to send a value back)
exejs.execute('var x = 40 + 2; return x;')  # 42

For context reuse, structured args, async, and timeout, see Advanced.

Supported Runtimes

Runtime Command Engine Install / Notes
Node node Chrome (V8) recommended, all platforms — nodejs.org/download
NodeJS nodejs Chrome (V8) Debian/Ubuntu alias of Node
JavaScriptCore jsc Safari (WebKit) macOS only (bundled)
SpiderMonkey js Firefox (Gecko) js package on most Linux distros
Phantomjs phantomjs WebKit upstream abandoned 2018
SlimerJS slimerjs Gecko upstream abandoned 2021
Nashorn jjs Java (JVM) Oracle JDK; removed since JDK 15
JScript cscript //E:jscript //Nologo IE (Trident) Windows only (built-in); uses temp file

ExeJS auto-detects the first available runtime in the order above. Use reset_runtime to pick one explicitly.

API Reference

Top-level functions

exejs.evaluate(source: str, timeout: float | None = None) -> Any
exejs.execute(source: str, timeout: float | None = None) -> Any
exejs.compile(source: str = '', cwd: str | None = None) -> RuntimeCompileContext

async exejs.evaluate_async(source: str, timeout: float | None = None) -> Any
async exejs.execute_async(source: str, timeout: float | None = None) -> Any

Compile context

ctx = exejs.compile(source='', cwd=None)

ctx.call(key: str, *args, timeout: float | None = None) -> Any        # key supports paths like 'calc.mul'
ctx.evaluate(source: str, timeout: float | None = None) -> Any
ctx.execute(source: str, timeout: float | None = None) -> Any

async ctx.call_async(key: str, *args, timeout: float | None = None) -> Any
async ctx.evaluate_async(source: str, timeout: float | None = None) -> Any
async ctx.execute_async(source: str, timeout: float | None = None) -> Any

Runtime management

exejs.reset_runtime(name: str) -> None                       # switch runtime by name
exejs.get_current_runtime() -> Runtime
exejs.get_current_runtime_name() -> str
exejs.find_available_runtime() -> Runtime                    # first available
exejs.find_all_runtime_name_list(is_available: bool = True) -> list[str]

Exception hierarchy

ExejsError
├── ExejsRuntimeNameError          # unknown runtime name passed to reset_runtime
├── ExejsRuntimeUnavailableError   # no runtime found / runtime not on PATH
├── ExejsProcessExitError          # subprocess failed / non-zero exit
├── ExejsProgramError              # JS crashed or returned invalid JSON
└── ExejsTimeoutError              # timeout exceeded

Advanced

Compile & call (context reuse)

Reuse a single JS context across multiple calls instead of re-launching a runtime each time:

import exejs

ctx = exejs.compile('function add(x, y) { return x + y; }')
ctx.call('add', 1, 2)  # 3
ctx.call('add', 10, 20)  # 30

Structured arguments

call() auto JSON-serializes arguments and supports property paths as the key:

# call an object method (key may be a property path)
ctx = exejs.compile('var calc = { mul: function(a, b) { return a * b; } };')
ctx.call('calc.mul', 3, 4)  # 12

# call with a dict argument (auto JSON-serialized)
ctx = exejs.compile('function greet(u) { return "hi " + u.name + ", age " + u.age; }')
ctx.call('greet', {'name': 'Tom', 'age': 18})  # 'hi Tom, age 18'

Async API

Async is useful inside async web frameworks (FastAPI, aiohttp) or when you want to avoid blocking the event loop:

import asyncio
import exejs

asyncio.run(exejs.evaluate_async("'red yellow blue'.split(' ')"))

Timeout

timeout (seconds) applies to both sync and async variants. On expiry the subprocess is killed and ExejsTimeoutError is raised:

try:
    exejs.execute('while (true) {}', timeout=2.0)
except exejs.ExejsTimeoutError as e:
    print('killed:', e)

Choosing a runtime

import exejs

# see what's available on this machine
print(exejs.find_all_runtime_name_list())  # e.g. ['Node', 'JScript']

# force a specific one
exejs.reset_runtime('Node')
print(exejs.get_current_runtime_name())  # 'Node'

Working directory

compile(source, cwd=...) sets the subprocess working directory, useful when your JS reads relative files:

ctx = exejs.compile('return require("./config.json")', cwd='/path/to/project')
ctx.evaluate('')

FAQ

  • ExejsRuntimeUnavailableError: Unable to find available javascript runtime — No JS runtime is installed or none is on PATH. Install Node.js from nodejs.org and reopen your terminal, or call exejs.reset_runtime('JScript') on Windows where cscript is built in.
  • Antivirus blocks execution — If you are using the JScript runtime (Windows), it still writes a temp file which may trigger antivirus. Switch to Node (exejs.reset_runtime('Node')) to avoid this. All other runtimes use stdin and should not be affected.
  • cscript not recognized on Windows — JScript is provided by Windows Script Host. Ensure cscript.exe exists (usually at C:\Windows\System32\) and is on PATH; on stripped-down Windows images you may need to enable the Windows Script Host feature.
  • Blocks the event loop in asyncio code — The sync evaluate / execute block the calling thread. Inside an async framework use evaluate_async / execute_async instead so the event loop stays responsive.

Changelog

See change_log.md.

Contributing

Pull requests are welcome. To set up a dev environment:

git clone https://github.com/UlionTse/exejs.git
cd exejs
pip install -e .
pip install pytest
pytest exejs/tests

Run pytest exejs/tests before submitting a PR. Code style follows PEP 8. Please open an issue first to discuss any non-trivial change.

Acknowledgements

Notable Usage

ExeJS is a core dependency of Translators, where it handles the JavaScript execution layer that translation engines rely on.

License

Apache-2.0 © UlionTse

About

Run JavaScript code from Python.「髯祭司」是一个旨在使用python运行javascript的库。

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages