Neurotic Pink Mongooses

    pythonia
    TypeScript icon, indicating that this package has built-in type declarations

    1.0.0 • Public • Published

    JSPyBridge

    NPM version PyPI Build Status Gitpod ready-to-code

    Interoperate Node.js and Python. You can run Python from Node.js, or run Node.js from Python. Work in progress.

    Requires Node.js 14 and Python 3.8 or newer.

    Key Features

    • Ability to call async and sync functions and get object properties with a native feel
    • Built-in garbage collection
    • Bidirectional callbacks with arbitrary arguments
    • Iteration and exception handling support
    • Object inspection allows you to easily console.log or print() any foreign objects
    • (Bridge to call Python from JS) Python class extension and inheritance. See pytorch and tensorflow examples.
    • (Bridge to call JS from Python) Native decorator-based event emitter support
    • (Bridge to call JS from Python) First-class Jupyter Notebook/Google Colab support. See some Google Colab uses below.

    Basic usage example

    See some examples here. See documentation below and in here.

    Access JavaScript from Python

    pip3 install javascript
    from javascript import require, globalThis
    
    chalk, fs = require("chalk"), require("fs")
    
    print("Hello", chalk.red("world!"), "it's", globalThis.Date().toLocaleString())
    fs.writeFileSync("HelloWorld.txt", "hi!")

    Access Python from JavaScript

    Make sure to have the dependencies installed before hand!

    npm i pythonia
    import { python } from 'pythonia'
    // Import tkinter
    const tk = await python('tkinter')
    // All Python API access must be prefixed with await
    const root = await tk.Tk()
    // A function call with a $ suffix will treat the last argument as a kwarg dict
    const a = await tk.Label$(root, { text: 'Hello World' })
    await a.pack()
    await root.mainloop()
    python.exit() // Make sure to exit Python in the end to allow node to exit. You can also use process.exit.

    Examples

    Gitpod ready-to-code

    Check out some cool examples below! Try them on Gitpod! Click the Open in Gitpod link above, and then open the examples folder.

    PyTorch numpy tensorflow mineflayer

    Bridge feature comparison

    Unlike other bridges, you may notice you're not just writing Python code in JavaScript, or vice-versa. You can operate on objects on the other side of the bridge as if the objects existed on your side. This is achieved through real interop support: you can call callbacks, and do loss-less function calls with any arguments you like (with the exception of floating points percision of course).

    python(ia) bridge javascript bridge npm:python-bridge
    Garbage collection
    Class extension support Not built-in (rare use case), can be manually done with custom proxy
    Passthrough stdin (Standard input is not piped to bridge processes. Instead, listen to standard input then expose an API on the other side of the bridge recieve the data.)
    Passthrough stdout, stderr
    Long-running sync calls
    Long-running async calls (need to manually create new thread) (AsyncTask) (need to manually create new thread)
    Callbacks
    Call classes
    Iterators
    Inline eval
    Dependency Management
    Local File Imports
    Error Management
    Object inspection

    Who's using it

    Documentation

    From Python

    You can import the bridge module with

    from javascript import require

    This will import the require function which you can use just like in Node.js. This is a slightly modified require function which does dependency management for you. The first paramater is the name or location of the file to import. Internally, this calls the ES6 dynamic import() function. Which supports both CommonJS and ES6 modules.

    If you are passing a module name (does not start with / or include a .) such as 'chalk', it will search for the dependency in the internal node_module folder and if not found, install it automatically. This install will only happen once, it won't impact startup afterwards.

    The second paramater to the built-in require function is the version of the package you want, for example require('chalk', '^3') to get a version greater than major version 3. Just like you would if you were using npm install. It's reccomended to only use the major version as the name and version will be internally treated as a unique package, for example 'chalk--^3'. If you leave this empty, we will install latest version instead, or use the version that may already be installed globally.

    Usage

    • All function calls to JavaScript are thread synchronous
    • ES6 classes can be constructed without new
    • ES5 classes can be constructed with the .new psuedo method
    • Use @On decorator when binding event listeners. Use off() to disable it.
    • All callbacks run on a dedicated callback thread. DO NOT BLOCK in a callback or all other events will be blocked. Instead:
    • Use the @AsyncTask decorator when you need to spawn a new thread for an async JS task.

    For more, see docs/python.md.

    Usage

    👉 Click here to see some code usage examples 👈

    Basic import

    Let's say we have a file in JS like this called time.js ...

    function whatTimeIsIt() {
        return (new Date()).toLocaleString()
    }
    module.exports = { whatTimeIsIt }

    Then we can call it from Python !

    from javascript import require
    time = require('./time.js')
    print(time.whatTimeIsIt())

    Event emitter

    You must use the provided On, Once, decorator and off function over the normal dot methods.

    emitter.js

    const { EventEmitter } = require('events')
    class MyEmitter extends EventEmitter {
        counter = 0
        inc() {
            this.emit('increment', ++this.counter)
        }
    }
    module.exports = { MyEmitter }

    listener.py

    from javascript import require, On, off
    MyEmitter = require('./emitter.js')
    # New class instance
    myEmitter = MyEmitter()
    # Decorator usage
    @On(myEmitter, 'increment')
    def handleIncrement(this, counter):
        print("Incremented", counter)
        # Stop listening. `this` is the this variable in JS.
        off(myEmitter, 'increment', handleIncrement)
    # Trigger the event handler
    myEmitter.inc()

    ES5 class

    es5.js

    function MyClass(num) {
        this.getNum = () => num
    }
    module.exports = { MyClass }

    es5.py

    MyEmitter = require('./es5.js')
    myClass = MyClass.new(3)
    print(myClass.getNum())

    Iteration

    items.js

    module.exports = { items: [5, 6, 7, 8] }

    items.py

    items = require('./items.js')
    for item in items:
        print(item)

    Callback

    callback.js

    export function method(cb, salt) {
        cb(42 + salt)
    }

    callback.py

    method = require('./callback').method
    # Example with a lambda, but you can also pass a function ref
    method(lambda v: print(v), 2) # Prints 44

    From JavaScript

    • All the Python APIs are async. You must await them all.
    • Use python.exit() or process.exit() at the end to quit the Python process.
    • This library doesn't manage the packaging.
      • Right now you need to install all the deps from pip globally, but later on we may allow loading from pip-envs.
    • When you do a normal Python function call, you can supply "positional" arguments, which must be in the correct order to what the Python function expects.
    • Some Python objects accept arbitrary keyword arguments. You can call these functions by using the special $ function syntax.
      • When you do a function call with a $ before the parenthesis, such as await some.pythonCall$(), the final argument is evaluated as a kwarg dictionary. You can supply named arguments this way.
    • Property access with a $ at the end acts as a error suppression operator.
      • Any errors will be ignored and instead undefined will be returned
    • See docs/javascript.md for more docs, and the examples for more info

    Usage

    👉 Click here to see some code usage examples 👈

    Basic import

    Let's say we have a file in Python like this called time.py ...

    import datetime
    def what_time_is_it():
      return str(datetime.datetime.now())

    Then we can call it from JavaScript !

    import { python } from 'pythonia'
    const time = await python('./time.py')
    console.log("It's", await time.what_time_is_it())
    python.exit()

    Iterating

    • When iterating a Python object, you must use a for await loop instead of a normal for-of loop.

    iter.py

    import os
    def get_files():
      for f in os.listdir():
        yield f

    iter.js

    const iter = await python('./iter.py')
    const files = await iter.get_files()
    for await (const file of files) {
      console.log(file)
    }

    Details

    • When doing a function call, any foreign objects will be sent to you as a reference. For example, if you're in JavaScript and do a function call to Python that returns an array, you won't get a JS array back, but you will get a reference to the Python array. You can still access the array normally with the [] notation, as long as you use await. If you would like the bridge to turn the foreign refrence to something native, you can request a primitive value by calling .valueOf() on the Python array. This would give you a JS array. It works the same the other way around.
    • The above behavior makes it very fast to pipe data from one function onto another, avoiding costly conversions.
    • This above behavior is not present for callbacks and function parameters. The bridge will try to serialize what it can, and will give you a foreign reference if it's unable to serialize something. So if you pass a JS object, you'll get a Python dict, but if the dict contains something like a class, you'll get a reference in its place.

    Notable details

    • The ffid keyword is reserved. You cannot use it in variable names, object keys or values as this is used to internlly track objects.

    • On the bridge to call JavaScript from Python, due to the limiatations of Python and cross-platform IPC, we currently communicate over standard error which means that JSON output in JS standard error can interfere with the bridge. The same issue exists on Windows with python. You are however very unlikely to have issues with this.

    • You can set the Node.js/Python binary paths by setting the NODE_BIN or PYTHON_BIN enviornment variables before importing the library. Otherwise, the node and python3 or python binaries will be called relative to your PATH enviornment variable.

    Install

    npm i pythonia

    DownloadsWeekly Downloads

    94

    Version

    1.0.0

    License

    none

    Unpacked Size

    524 kB

    Total Files

    18

    Last publish

    Collaborators

    • extremeheat