Search code examples
pythonv8pyv8

Converting Python objects to JavaScript for PyV8


I'm trying to pass Python data (lists, dicts, strings..., arbitrarily nested) to PyV8.

class Global(object):
    def __init__(self, data):
        self.data = data
ctx = PyV8.JSContext(Global([{'a':1}]))
ctx.enter()
res = ctx.eval('data.length')
js_len = PyV8.convert(res)
print js_len

The code above prints None, presumably because the data object is not transformed to a JSArray and thus data.length evaluates to undefined. Is there a reliable way to do the necessary conversion in PyV8 other than using JSON?


Solution

  • Apparently PyV8 doesn't correctly convert python lists to Javascript arrays, which leads my_list.length to return undefined, which is getting converted to None.

    ctx = PyV8.JSContext()
    ctx.enter()
    ctx.locals.a = [{'a':1}]
    print ctx.locals.a
    #> [{'a': 1}]
    print ctx.eval("a.length")
    #> None
    print ctx.eval("a[0].a")
    #> 1
    ctx.locals.blub = {'a':1}
    print ctx.eval("blub.a")
    #> 1
    print ctx.eval("Object.keys(blub)")
    #> a
    ctx.locals.blub = {'a':[1,2,3]}
    print ctx.eval("Object.keys(blub)")
    #> a
    print ctx.eval("blub.a")
    #> [1, 2, 3]
    ctx.locals.blub2 = [{'a':[1,2,3]}]
    print ctx.eval("blub2")
    #> [{'a': [1, 2, 3]}]
    print ctx.eval("blub2.length")
    #> None
    print ctx.eval("Array.isArray(blub2)")
    #> False
    print ctx.eval("typeof(blub2)")
    #> object
    print ctx.eval("blub2[0].a")
    #> [1, 2, 3]
    print ctx.eval("typeof(blub.a)")
    #> object
    print ctx.eval("Array.isArray(blub.a)")
    #> False
    

    The answer is to use PyV8.JSArray(my_list). I've written the following helper functions for my project that deal with various little problems and make it easy to convert back and forth between python and js objects. These are targeted at a specific version of PyV8 however (which is the only version I can recommend, see discussion in the linked issues), so your results may vary if you use them as-is. Example usage:

    ctx.locals.blub3 = get_js_obj({'a':[1,2,3]})
    ctx.locals.blub4 = get_js_obj([1,2,3])
    ctx.eval("blub3.a.length")
    #> 3
    ctx.eval("blub4.length")
    #> 3
    

    And here are the functions.

    def access_with_js(ctx, route):
        if len(route) == 0:
            raise Exception("route must have at least one element")
        accessor_string = route[0]
        for elem in route[1:]:
            if type(elem) in [str, unicode]:
                accessor_string += "['" + elem + "']"
            elif type(elem) == int:
                accessor_string += "[" + str(elem) + "]"
            else:
                raise Exception("invalid element in route, must be text or number")
        return ctx.eval(accessor_string)
    
    def get_py_obj(ctx, obj, route=[]):
        def dict_is_empty(dict):
            for key in dict:
                return False
            return True
    
        def access(obj, key):
            if key in obj:
                return obj[key]
            return None
    
        cloned = None
        if isinstance(obj, list) or isinstance(obj, PyV8.JSArray):
            cloned = []
            temp = str(access_with_js(ctx, route)) #working around a problem with PyV8 r429
            num_elements = len(obj)
            for index in range(num_elements):
                elem = obj[index]
                cloned.append(get_py_obj(ctx, elem, route + [index]))
        elif isinstance(obj, dict) or isinstance(obj, PyV8.JSObject):
            cloned = {}
            for key in obj.keys():
                cloned_val = None
                if type(key) == int:
                    #workaround for a problem with PyV8 where it won't let me access
                    #objects with integer accessors
                    val = None
                    try:
                        val = access(obj, str(key))
                    except KeyError:
                        pass
                    if val == None:
                        val = access(obj, key)
                    cloned_val = get_py_obj(ctx, val, route + [key])
                else:
                    cloned_val = get_py_obj(ctx, access(obj, key), route + [key])
                cloned[key] = cloned_val
        elif type(obj) == str:
            cloned = obj.decode('utf-8')
        else:
            cloned = obj
        return cloned
    
    def get_js_obj(ctx,obj):
        #workaround for a problem with PyV8 where it will implicitely convert python lists to js objects
        #-> we need to explicitely do the conversion. see also the wrapper classes for JSContext above.
        if isinstance(obj, list):
            js_list = []
            for entry in obj:
                js_list.append(get_js_obj(ctx,entry))
            return PyV8.JSArray(js_list)
        elif isinstance(obj, dict):
            js_obj = ctx.eval("new Object();") # PyV8.JSObject cannot be instantiated from Python
            for key in obj.keys():
    
                try:
                    js_obj[key] = get_js_obj(ctx,obj[key])
                except Exception, e:
                    # unicode keys raise a Boost.Python.ArgumentError 
                    # which can't be caught directly:
                    # https://mail.python.org/pipermail/cplusplus-sig/2010-April/015470.html
                    if (not str(e).startswith("Python argument types in")):
                        raise
                    import unicodedata
                    js_obj[unicodedata.normalize('NFKD', key).encode('ascii','ignore')] = get_js_obj(ctx,obj[key])
            return js_obj
        else:
            return obj