diff pydershell/pydershell.py @ 19:057260102960

Made wrapping of python objects much nicer.
author Atul Varma <varmaa@toolness.com>
date Mon, 07 Sep 2009 12:57:20 -0700
parents 69e5523ebdc6
children 8dd18f864351
line wrap: on
line diff
--- a/pydershell/pydershell.py	Mon Sep 07 04:38:44 2009 -0700
+++ b/pydershell/pydershell.py	Mon Sep 07 12:57:20 2009 -0700
@@ -71,32 +71,25 @@
     Securely wraps a JS object to behave like any normal Python object.
     """
 
-    __slots__ = ['_jsobject', '_cx', '_this']
+    __slots__ = ['_jsobject', '_sandbox', '_this']
 
-    def __init__(self, cx, jsobject, this):
+    def __init__(self, sandbox, jsobject, this):
         if not isinstance(jsobject, pydermonkey.Object):
             raise TypeError("Cannot wrap '%s' object" %
                             type(jsobject).__name__)
+        object.__setattr__(self, '_sandbox', sandbox)
         object.__setattr__(self, '_jsobject', jsobject)
-        object.__setattr__(self, '_cx', cx)
         object.__setattr__(self, '_this', this)
 
-    @staticmethod
-    def wrap(cx, jsvalue, this):
-        if isinstance(jsvalue, pydermonkey.Function):
-            return SafeJsFunctionWrapper(cx, jsvalue, this)
-        elif isinstance(jsvalue, pydermonkey.Object):
-            return SafeJsObjectWrapper(cx, jsvalue, this)
-        else:
-            # It's a primitive value.
-            return jsvalue
+    @property
+    def wrapped_jsobject(self):
+        return self._jsobject
 
     def _wrap_to_python(self, jsvalue):
-        return self.wrap(self._cx, jsvalue, self._jsobject)
+        return self._sandbox.wrap_jsobject(jsvalue, self._jsobject)
 
     def _wrap_to_js(self, value):
-        # TODO: Add support for wrapping non-primitive python objects.
-        return value
+        return self._sandbox.wrap_pyobject(value)
 
     def __eq__(self, other):
         if isinstance(other, SafeJsObjectWrapper):
@@ -114,7 +107,7 @@
         self.__setattr__(item, value)
 
     def __setattr__(self, name, value):
-        cx = self._cx
+        cx = self._sandbox.cx
         jsobject = self._jsobject
 
         cx.define_property(jsobject, name,
@@ -124,19 +117,19 @@
         return self.__getattr__(item)
 
     def __getattr__(self, name):
-        cx = self._cx
+        cx = self._sandbox.cx
         jsobject = self._jsobject
 
         return self._wrap_to_python(cx.get_property(jsobject, name))
 
     def __contains__(self, item):
-        cx = self._cx
+        cx = self._sandbox.cx
         jsobject = self._jsobject
 
         return cx.has_property(jsobject, item)
 
     def __iter__(self):
-        cx = self._cx
+        cx = self._sandbox.cx
         jsobject = self._jsobject
 
         properties = cx.enumerate(jsobject)
@@ -148,48 +141,23 @@
     Securely wraps a JS function to behave like any normal Python object.
     """
 
-    def __init__(self, cx, jsfunction, this):
+    def __init__(self, sandbox, jsfunction, this):
         if not isinstance(jsfunction, pydermonkey.Function):
             raise TypeError("Cannot wrap '%s' object" %
                             type(jsobject).__name__)
-        SafeJsObjectWrapper.__init__(self, cx, jsfunction, this)
+        SafeJsObjectWrapper.__init__(self, sandbox, jsfunction, this)
 
     def __call__(self, *args):
-        cx = self._cx
+        cx = self._sandbox.cx
         jsobject = self._jsobject
         this = self._this
 
-        obj = cx.call_function(this, jsobject,
-                               self._wrap_to_js(args))
-        return self._wrap_to_python(obj)
-
-def safejsfunc(cx, on_obj, name=None):
-    """
-    Exposes the decorated Python function on the given JS object.
-
-    Any unexpected exceptions raised by the function will be
-    re-raised as InternalError exceptions.
-    """
+        arglist = []
+        for arg in args:
+            arglist.append(self._wrap_to_js(arg))
 
-    def make_wrapper(func):
-        if name is None:
-            func_name = func.__name__
-        else:
-            func_name = name
-        def wrapper(func_cx, this, args):
-            try:
-                return func(func_cx, this, args)
-            except pydermonkey.error:
-                raise
-            except Exception:
-                raise InternalError()
-        cx.define_property(
-            on_obj,
-            func_name, 
-            cx.new_function(wrapper, func_name)
-            )
-        return func
-    return make_wrapper
+        obj = cx.call_function(this, jsobject, tuple(arglist))
+        return self._wrap_to_python(obj)
 
 def format_stack(js_stack):
     """
@@ -225,30 +193,17 @@
     lines.insert(0, "Traceback (most recent call last):")
     return '\n'.join(lines)
 
-class JSRuntime(object):
+class JSSandbox(object):
     """
-    A JS runtime capable of loading and executing scripts.
+    A JS runtime and associated functionality capable of securely
+    loading and executing scripts.
     """
 
     def __init__(self, watchdog=watchdog):
         rt = pydermonkey.Runtime()
         cx = rt.new_context()
-        globalobj = cx.new_object()
-        cx.init_standard_classes(globalobj)
-
-        @safejsfunc(cx, globalobj)
-        def bar(cx, this, args):
-            obj = SafeJsObjectWrapper.wrap(cx, args[0], this)
-            print obj.bar()
-
-        @safejsfunc(cx, globalobj)
-        def foo(cx, this, args):
-            return cx.call_function(this, args[0], ())
-
-        @safejsfunc(cx, globalobj, 'print')
-        def jsprint(cx, this, args):
-            if len(args) > 0:
-                print args[0]
+        root = cx.new_object()
+        cx.init_standard_classes(root)
 
         cx.set_operation_callback(self._opcb)
         cx.set_throw_hook(self._throwhook)
@@ -256,10 +211,21 @@
 
         self.rt = rt
         self.cx = cx
-        self.globalobj = globalobj
         self.curr_exc = None
         self.py_stack = None
         self.js_stack = None
+        self.__py_to_js = {}
+        self.root = self.wrap_jsobject(root, root)
+
+    def finish(self):
+        for jsobj in self.__py_to_js.values():
+            self.cx.clear_object_private(jsobj)
+        del self.__py_to_js
+        del self.curr_exc
+        del self.py_stack
+        del self.js_stack
+        del self.cx
+        del self.rt
 
     def _opcb(self, cx):
         # Don't do anything; if a keyboard interrupt was triggered,
@@ -273,11 +239,66 @@
             self.py_stack = traceback.extract_stack()
             self.js_stack = cx.get_stack()
 
+    def __wrap_pycallable(self, func):
+        if func in self.__py_to_js:
+            return self.__py_to_js[func]
+
+        if hasattr(func, '__name__'):
+            name = func.__name__
+        else:
+            name = ""
+
+        def wrapper(func_cx, this, args):
+            try:
+                arglist = []
+                for arg in args:
+                    arglist.append(self.wrap_jsobject(arg))
+                # TODO: Fill in extra required params with pymonkey.undefined?
+                # or automatically throw an exception to calling js code?
+                return func(*arglist)
+            except pydermonkey.error:
+                raise
+            except Exception:
+                raise InternalError()
+        wrapper.wrapped_pyobject = func
+        wrapper.__name__ = name
+
+        jsfunc = self.cx.new_function(wrapper, name)
+        self.__py_to_js[func] = jsfunc
+
+        return jsfunc
+
+    def wrap_pyobject(self, value):
+        if isinstance(value, SafeJsObjectWrapper):
+            # It's already wrapped, just unwrap it.
+            return value.wrapped_jsobject
+        elif callable(value):
+            return self.__wrap_pycallable(value)
+        elif isinstance(value, object):
+            raise NotImplementedError("TODO")
+        else:
+            # Here's hoping it's a primitive value.
+            return value
+
+    def wrap_jsobject(self, jsvalue, this=None):
+        if this is None:
+            this = self.root.wrapped_jsobject
+        if isinstance(jsvalue, pydermonkey.Function):
+            if jsvalue.is_python:
+                # It's a Python function, just unwrap it.
+                return self.cx.get_object_private(jsvalue).wrapped_pyobject
+            return SafeJsFunctionWrapper(self, jsvalue, this)
+        elif isinstance(jsvalue, pydermonkey.Object):
+            return SafeJsObjectWrapper(self, jsvalue, this)
+        else:
+            # It's a primitive value.
+            return jsvalue
+
     def run_script(self, filename):
         contents = open(filename).read()
         cx = self.cx
         try:
-            cx.evaluate_script(self.globalobj, contents,
+            cx.evaluate_script(self.root.wrapped_jsobject, contents,
                                filename, 1)
         except pydermonkey.error, e:
             print format_stack(self.js_stack)
@@ -288,9 +309,24 @@
             print e.exc_info[1]
 
 if __name__ == '__main__':
-    runtime = JSRuntime()
-    runtime.run_script('test.js')
-    del runtime
+    sandbox = JSSandbox()
+
+    def bar(obj):
+        print obj.bar()
+    sandbox.root.bar = bar
+
+    def foo(callback):
+        return callback()
+    sandbox.root.foo = foo
+
+    def jsprint(string):
+        print string
+    sandbox.root['print'] = jsprint
+
+    sandbox.run_script('test.js')
+
+    sandbox.finish()
+    del sandbox
 
     import gc
     gc.collect()