Skip to content

Repository files navigation

PyPI status indicatorGithub workflow status indicatorISC License

Minimal, modern embedded V8 for Python.

MiniRacer logo: a V8 with a very snakey 8

Full documentation.

In brief

  • Latest ECMAScript support
  • Web Assembly support
  • Unicode support
  • Thread safe
  • Re-usable contexts

MiniRacer can be easily used by Django or Flask projects to minify assets, run babel or WASM modules.

PyMiniRacer was created by Sqreen, and originally lived at https://github.com/sqreen/PyMiniRacer with the PyPI package py-mini-racer. After dicussion with the original Sqreen team, I have created a new official home for at https://github.com/bpcreech/PyMiniRacer with a new PyPI package mini-racer (note: no py-). See the full history for more.

Examples

MiniRacer is straightforward to use:

 $ pip install mini-racer

and then:

 $ python3>>>frompy_mini_racerimportMiniRacer>>>ctx=MiniRacer()
>>>ctx.eval("1+1")
2>>>ctx.eval("var x = {company: 'Sqreen'}; x.company")
'Sqreen'>>>print(ctx.eval("'❤'"))
❤
>>>ctx.eval("var fun = () => ({ foo: 1 });")

Variables are kept inside of a context:

>>>ctx.eval("x.company")
'Sqreen'

You can evaluate whole scripts within JavaScript, or define and return JavaScript function objects and call them from Python (new in v0.11.0):

>>>square=ctx.eval("a => a*a")
>>>square(4)
16

JavaScript Objects and Arrays are modeled in Python as dictionaries and lists (or, more precisely, MutableMapping and MutableSequence instances), respectively (new in v0.11.0):

>>>obj=ctx.eval("var obj = {'foo': 'bar'}; obj")
>>>obj["foo"]
'bar'>>>list(obj.keys())
['foo']
>>>arr=ctx.eval("var arr = ['a', 'b']; arr")
>>>arr[1]
'b'>>>'a'inarrTrue>>>arr.append(obj)
>>>ctx.eval("JSON.stringify(arr)")
'["a","b",{"foo":"bar"}]'

Meanwhile, call uses JSON to transfer data between JavaScript and Python, and converts data in bulk:

>>>ctx.call("fun")
{'foo': 1}

Composite values are serialized using JSON. Use a custom JSON encoder when sending non-JSON encodable parameters:

importjsonfromdatetimeimportdatetimeclassCustomEncoder(json.JSONEncoder):
defdefault(self, obj):
ifisinstance(obj, datetime):
returnobj.isoformat()
returnjson.JSONEncoder.default(self, obj)
>>>ctx.eval("var f = function(args) { return args; }")
>>>ctx.call("f", datetime.now(), encoder=CustomEncoder)
'2017-03-31T16:51:02.474118'

MiniRacer is ES6 capable:

>>>ctx.execute("[1,2,3].includes(5)")
False

JavaScript null and undefined are modeled in Python as None and JSUndefined, respectively:

>>>list(ctx.eval("[null, undefined]"))
[None, JSUndefined]

You can prevent runaway execution in synchronous code using the timeout_sec parameter:

>>>ctx.eval('while (true) {}', timeout_sec=2)
# Spins for 2 seconds and then emits a traceback ending with...raiseJSTimeoutExceptionfromepy_mini_racer._exc.JSTimeoutException: JavaScriptwasterminatedbytimeout>>>func=ctx.eval('() => {while (true) {}}')
>>>func(timeout_sec=2)
# Spins for 2 seconds and then emits a traceback ending with...raiseJSTimeoutExceptionfromepy_mini_racer._exc.JSTimeoutException: JavaScriptwasterminatedbytimeout

MiniRacer supports asynchronous execution using JS Promise instances (new in v0.10.0):

>>>promise=ctx.eval(
... "new Promise((res, rej) => setTimeout(() => res(42), 10000))")
>>>promise.get() # blocks for 10 seconds, and then:42

For more deterministic cleanup behavior, we strongly recommend allocating a MiniRacer from a context manager (new in v0.14.0):

>>>frompy_mini_racerimportmini_racer>>>withmini_racer() asctx:
... print(ctx.eval("Array.from('foobar').reverse().join('')"))
raboof

MiniRacer uses asyncio internally to manage V8. Both MiniRacer() and the mini_racer() context manager will capture the currently-running event loop, or you can specify a loop explicitly, and in non-async contexts, MiniRacer will launch its own event loop with its own background thread to service it. (new in v0.14.0)

>>>frompy_mini_racerimportMiniRacer, mini_racer>>>ctx=MiniRacer() # launches a new event loop in a new thread>>>withmini_racer() asctx: # same: launches a new event loop in a new thread
... pass
...
>>>asyncdefdemo():
... withmini_racer() asctx: # reuses the running event loop
... pass
...
>>>importasyncio>>>asyncio.run(demo())
>>>my_loop=asyncio.new_event_loop()
>>>withmini_racer(my_loop) asctx: # uses the specified event loop
... pass

When calling into MiniRacer from async code, you must await promises using await (instead of promise.get()):

%python-masyncio>>>frompy_mini_racerimportmini_racer>>>withmini_racer() asctx:
... promise=ctx.eval(
... "new Promise((res, rej) => setTimeout(() => res(42), 10000))")
... print(awaitpromise) # yields for 10 seconds, and then:
...
42

MiniRacer does not support the timeout_sec parameter in async evaluation. Instead request a cancelable evaluation and use a construct like asyncio.wait_for:

%python-masyncio>>>frompy_mini_racerimportmini_racer>>>withmini_racer() asctx:
... # Use eval_cancelable(...), which has async semantics:
... awaitasyncio.wait_for(ctx.eval_cancelable('while (true) {}'), timeout=2)
# Spins for 2 seconds and then emits a traceback ending with...raiseTimeoutErrorfromexc_valTimeoutError>>>withmini_racer() asctx:
... func=ctx.eval('() => {while (true) {}}')
... # Upgrade func using .cancelable(), which introduces async semantics:
... cancelable_func=func.cancelable()
... awaitasyncio.wait_for(cancelable_func(), timeout=2)
# Spins for 2 seconds and then emits a traceback ending with...raiseTimeoutErrorfromexc_valTimeoutError

You can install callbacks from JavaScript to Python (new in v0.12.0). Only async callbacks are supported:

%python-masyncio>>>frompy_mini_racerimportmini_racer>>>asyncdefread_file(fn):
... withopen(fn) asf: # (or aiofiles would be even better here)
... returnf.read()
...
>>>withmini_racer() asctx:
... asyncwithctx.wrap_py_function(read_file) asjsfunc:
... # "Install" our (async) JS function on the global "this" object:
... ctx.eval('this')['read_file'] =jsfunc
... d=awaitctx.eval('read_file("/usr/share/dict/words")')
... print(d.split()[0:10])
['A', 'AA', 'AAA', "AA's", 'AB', 'ABC', "ABC's", 'ABCs', 'ABM', "ABM's"]

Note that adding Python callbacks may degrade the security properties of PyMiniRacer! See PyMiniRacer's security goals.

MiniRacer supports the ECMA Intl API:

# Indonesian dates!>>>ctx.eval('Intl.DateTimeFormat(["ban", "id"]).format(new Date())')
'16/3/2024'

V8 heap information can be retrieved:

>>>ctx.heap_stats()
{'total_physical_size': 1613896,
'used_heap_size': 1512520,
'total_heap_size': 3997696,
'total_heap_size_executable': 3145728,
'heap_size_limit': 1501560832}

A WASM example is available in the tests.

Compatibility

PyMiniRacer is compatible with Python 3.10-3.14 and is based on ctypes.

PyMiniRacer is distributed using wheels on PyPI. The wheels are intended to provide compatibility with:

OSx86_64aarch64
macOS ≥ 10.9
Windows ≥ 10
Ubuntu ≥ 20.04
Debian ≥ 11
RHEL ≥ 9
other Linuxes with glibc ≥ 2.27
Alpine ≥ 3.19
other Linux with musl ≥ 1.2

In order to run on Alpine you must install gcompat and run with LD_PRELOAD="/lib/libgcompat.so.0".

If you have a up-to-date pip and it doesn't use a wheel, you might have an environment for which no wheel is built. Please open an issue.

Developing and releasing PyMiniRacer

See the contribution guide.

Credits

Built with love by Sqreen.

PyMiniRacer launch was described in this blog post.

PyMiniRacer is inspired by mini_racer, built for the Ruby world by Sam Saffron.

In 2024, PyMiniRacer was revived, and adopted by Ben Creech. Upon discussion with the original Sqreen authors, we decided to re-launch PyMiniRacer as a fork under https://github.com/bpcreech/PyMiniRacer and https://pypi.org/project/mini-racer/.

About

PyMiniRacer is a V8 bridge in Python.

Resources

Contributing

Stars

217 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages