3

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?

jd.
  • 10,678
  • 3
  • 46
  • 55
  • I've never used a global parameter this way in pyv8. I'd just write this as `ctx = PyV8.JSContext() ctx.enter() ctx.locals.data = [{'a':1}] print ctx.eval('data.length')` Could you try this as a hello world before doing something more complex? – Michel Müller Feb 21 '15 at 14:26
  • Thanks, but it's not any better. `ctx.locals.x = [{'a':1}]; ctx.eval('x.length'); ctx.eval('x[0].a')` prints None and 1. – jd. Feb 21 '15 at 17:02

1 Answers1

4

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
Jthorpe
  • 9,756
  • 2
  • 49
  • 64
Michel Müller
  • 5,535
  • 3
  • 31
  • 49
  • Hi @Jthorpe. Thank for the edit. I think the reasoning behind get_js_obj using a python dict in the original was that those get correctly converted to a JS object when assigning the resulting object to the context locals. Therefore I didn't see the need to do any manual conversion there. I'm also wondering a bit in your modified version - does it work afterwards when doing assignments like `js_obj[key] = some_obj` on python side? – Michel Müller May 05 '17 at 00:05
  • I haven't used this for anything other than the usual base types (bool, string, int, float, tuples, lists, and dicts), but it's been working great for me. The issue I had is that I wanted to make a [thin wrapper](https://github.com/jdthorpe/ajvpy) the [AJV](https://github.com/epoberezkin/ajv) library, and previously using `ctx.locals.x = get_js_obj({'hi':'world'})` in python would result in a JS object such that `x.hasOwnProperty('items')` was `true` (referring to the python `dict.items` method). – Jthorpe May 05 '17 at 22:46
  • I see, interesting - I wasn't aware that PyV8 would also assign these properties. Well if you've tested it and it works I'll trust that I can let the edit stand. – Michel Müller May 06 '17 at 02:36