changeset 29:296aba0e0a17

made pydertron into a package, created setup.py.
author Atul Varma <avarma@mozilla.com>
date Mon, 10 May 2010 01:18:22 -0700
parents d28100e071a7
children 83104982c815
files pydertron.py pydertron/__init__.py setup.py
diffstat 3 files changed, 670 insertions(+), 648 deletions(-) [+]
line wrap: on
line diff
--- a/pydertron.py	Mon May 10 00:59:19 2010 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,648 +0,0 @@
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Pydertron.
-#
-# The Initial Developer of the Original Code is Mozilla.
-# Portions created by the Initial Developer are Copyright (C) 2007
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#   Atul Varma <atul@mozilla.com>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
-
-"""
-    Pydertron is a high-level wrapper for Pydermonkey that
-    provides convenient, secure object wrapping between JS and Python
-    space.
-"""
-
-import sys
-import threading
-import traceback
-import weakref
-import types
-import atexit
-
-import pydermonkey
-
-class ContextWatchdogThread(threading.Thread):
-    """
-    Watches active JS contexts and triggers their operation callbacks
-    at a regular interval.
-    """
-
-    # Default interval, in seconds, that the operation callbacks are
-    # triggered at.
-    DEFAULT_INTERVAL = 0.25
-
-    def __init__(self, interval=DEFAULT_INTERVAL):
-        threading.Thread.__init__(self)
-        self._lock = threading.Lock()
-        self._stop = threading.Event()
-        self._contexts = []
-        self.interval = interval
-
-    def add_context(self, cx):
-        self._lock.acquire()
-        try:
-            self._contexts.append(weakref.ref(cx))
-        finally:
-            self._lock.release()
-
-    def join(self):
-        self._stop.set()
-        threading.Thread.join(self)
-
-    def run(self):
-        while not self._stop.isSet():
-            new_list = []
-            self._lock.acquire()
-            try:
-                for weakcx in self._contexts:
-                    cx = weakcx()
-                    if cx:
-                        new_list.append(weakcx)
-                        cx.trigger_operation_callback()
-                        del cx
-                self._contexts = new_list
-            finally:
-                self._lock.release()
-            self._stop.wait(self.interval)
-
-# Create a global watchdog.
-watchdog = ContextWatchdogThread()
-watchdog.start()
-atexit.register(watchdog.join)
-
-class InternalError(BaseException):
-    """
-    Represents an error in a JS-wrapped Python function that wasn't
-    expected to happen; because it's derived from BaseException, it
-    unrolls the whole JS/Python stack so that the error can be
-    reported to the outermost calling code.
-    """
-
-    def __init__(self):
-        BaseException.__init__(self)
-        self.exc_info = sys.exc_info()
-
-class SafeJsObjectWrapper(object):
-    """
-    Securely wraps a JS object to behave like any normal Python
-    object. Like JS objects, though, accessing undefined object
-    results merely in pydermonkey.undefined.
-
-    Object properties may be accessed either via attribute or
-    item-based lookup.
-    """
-
-    __slots__ = ['_jsobject', '_sandbox', '_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, '_this', this)
-
-    @property
-    def wrapped_jsobject(self):
-        return self._jsobject
-
-    def _wrap_to_python(self, jsvalue):
-        return self._sandbox.wrap_jsobject(jsvalue, self._jsobject)
-
-    def _wrap_to_js(self, value):
-        return self._sandbox.wrap_pyobject(value)
-
-    def __eq__(self, other):
-        if isinstance(other, SafeJsObjectWrapper):
-            return self._jsobject == other._jsobject
-        else:
-            return False
-
-    def __str__(self):
-        return self.toString()
-
-    def __unicode__(self):
-        return self.toString()
-
-    def __setitem__(self, item, value):
-        self.__setattr__(item, value)
-
-    def __setattr__(self, name, value):
-        cx = self._sandbox.cx
-        jsobject = self._jsobject
-
-        cx.define_property(jsobject, name,
-                           self._wrap_to_js(value))
-
-    def __getitem__(self, item):
-        return self.__getattr__(item)
-
-    def __getattr__(self, name):
-        cx = self._sandbox.cx
-        jsobject = self._jsobject
-
-        return self._wrap_to_python(cx.get_property(jsobject, name))
-
-    def __contains__(self, item):
-        cx = self._sandbox.cx
-        jsobject = self._jsobject
-
-        return cx.has_property(jsobject, item)
-
-    def __iter__(self):
-        cx = self._sandbox.cx
-        jsobject = self._jsobject
-
-        properties = cx.enumerate(jsobject)
-        for property in properties:
-            yield property
-
-class SafeJsFunctionWrapper(SafeJsObjectWrapper):
-    """
-    Securely wraps a JS function to behave like any normal Python object.
-    """
-
-    def __init__(self, sandbox, jsfunction, this):
-        if not isinstance(jsfunction, pydermonkey.Function):
-            raise TypeError("Cannot wrap '%s' object" %
-                            type(jsobject).__name__)
-        SafeJsObjectWrapper.__init__(self, sandbox, jsfunction, this)
-
-    def __call__(self, *args):
-        cx = self._sandbox.cx
-        jsobject = self._jsobject
-        this = self._this
-
-        arglist = []
-        for arg in args:
-            arglist.append(self._wrap_to_js(arg))
-
-        obj = cx.call_function(this, jsobject, tuple(arglist))
-        return self._wrap_to_python(obj)
-
-def format_stack(js_stack, open=open):
-    """
-    Returns a formatted Python-esque stack traceback of the given
-    JS stack.
-    """
-
-    STACK_LINE  ="  File \"%(filename)s\", line %(lineno)d, in %(name)s"
-
-    lines = []
-    while js_stack:
-        script = js_stack['script']
-        function = js_stack['function']
-        if script:
-            frameinfo = dict(filename = script.filename,
-                             lineno = js_stack['lineno'],
-                             name = '<module>')
-        elif function and not function.is_python:
-            frameinfo = dict(filename = function.filename,
-                             lineno = js_stack['lineno'],
-                             name = function.name)
-        else:
-            frameinfo = None
-        if frameinfo:
-            lines.insert(0, STACK_LINE % frameinfo)
-            try:
-                filelines = open(frameinfo['filename']).readlines()
-                line = filelines[frameinfo['lineno'] - 1].strip()
-                lines.insert(1, "    %s" % line)
-            except Exception:
-                pass
-        js_stack = js_stack['caller']
-    lines.insert(0, "Traceback (most recent call last):")
-    return '\n'.join(lines)
-
-def jsexposed(name=None, on=None):
-    """
-    Decorator used to expose the decorated function or method to
-    untrusted JS.
-
-    'name' is an optional alternative name for the function.
-
-    'on' is an optional SafeJsObjectWrapper that the function can be
-    automatically attached as a property to.
-    """
-
-    if callable(name):
-        func = name
-        func.__jsexposed__ = True
-        return func
-
-    def make_exposed(func):
-        if name:
-            func.__name__ = name
-        func.__jsexposed__ = True
-        if on:
-            on[func.__name__] = func
-        return func
-    return make_exposed
-
-class JsExposedObject(object):
-    """
-    Trivial base/mixin class for any Python classes that choose to
-    expose themselves to JS code.
-    """
-
-    pass
-
-class JsSandbox(object):
-    """
-    A JS runtime and associated functionality capable of securely
-    loading and executing scripts.
-    """
-
-    def __init__(self, fs, watchdog=watchdog, opcb=None):
-        rt = pydermonkey.Runtime()
-        cx = rt.new_context()
-        root_proto = cx.new_object()
-        cx.init_standard_classes(root_proto)
-        root = cx.new_object(None, root_proto)
-
-        cx.set_operation_callback(self._opcb)
-        cx.set_throw_hook(self._throwhook)
-        watchdog.add_context(cx)
-
-        self.fs = fs
-        self.opcb = opcb
-        self.rt = rt
-        self.cx = cx
-        self.curr_exc = None
-        self.py_stack = None
-        self.js_stack = None
-        self.__modules = {}
-        self.__py_to_js = {}
-        self.__type_protos = {}
-        self.__globals = {}
-        self.__root_proto = root_proto
-        self.root = self.wrap_jsobject(root, root)
-
-    def set_globals(self, **globals):
-        """
-        Sets the global properties for the root object and all global
-        scopes (e.g., SecurableModules).  This should be called before
-        any scripts are executed.
-        """
-
-        self.__globals.update(globals)
-        self._install_globals(self.root)
-
-    def finish(self):
-        """
-        Cleans up all resources used by the sandbox, breaking any reference
-        cycles created due to issue #2 in pydermonkey:
-
-        http://code.google.com/p/pydermonkey/issues/detail?id=2
-        """
-
-        for jsobj in self.__py_to_js.values():
-            self.cx.clear_object_private(jsobj)
-        del self.__py_to_js
-        del self.__type_protos
-        del self.curr_exc
-        del self.py_stack
-        del self.js_stack
-        del self.cx
-        del self.rt
-
-    def _opcb(self, cx):
-        # Note that if a keyboard interrupt was triggered,
-        # it'll get raised here automatically.
-        if self.opcb:
-            self.opcb()
-
-    def _throwhook(self, cx):
-        curr_exc = cx.get_pending_exception()
-        if self.curr_exc != curr_exc:
-            self.curr_exc = curr_exc
-            self.py_stack = traceback.extract_stack()
-            self.js_stack = cx.get_stack()
-
-    def __wrap_pycallable(self, func, pyproto=None):
-        if func in self.__py_to_js:
-            return self.__py_to_js[func]
-
-        if hasattr(func, '__name__'):
-            name = func.__name__
-        else:
-            name = ""
-
-        if pyproto:
-            def wrapper(func_cx, this, args):
-                try:
-                    arglist = []
-                    for arg in args:
-                        arglist.append(self.wrap_jsobject(arg))
-                    instance = func_cx.get_object_private(this)
-                    if instance is None or not isinstance(instance, pyproto):
-                        raise pydermonkey.ScriptError("Method type mismatch")
-
-                    # TODO: Fill in extra required params with
-                    # pymonkey.undefined?  or automatically throw an
-                    # exception to calling js code?
-                    return self.wrap_pyobject(func(instance, *arglist))
-                except pydermonkey.ScriptError:
-                    raise
-                except Exception:
-                    raise InternalError()
-        else:
-            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 self.wrap_pyobject(func(*arglist))
-                except pydermonkey.ScriptError:
-                    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_pyinstance(self, value):
-        pyproto = type(value)
-        if pyproto not in self.__type_protos:
-            jsproto = self.cx.new_object()
-            if hasattr(pyproto, '__jsprops__'):
-                define_getter = self.cx.get_property(jsproto,
-                                                     '__defineGetter__')
-                define_setter = self.cx.get_property(jsproto,
-                                                     '__defineSetter__')
-                for name in pyproto.__jsprops__:
-                    prop = getattr(pyproto, name)
-                    if not type(prop) == property:
-                        raise TypeError("Expected attribute '%s' to "
-                                        "be a property" % name)
-                    getter = None
-                    setter = None
-                    if prop.fget:
-                        getter = self.__wrap_pycallable(prop.fget,
-                                                        pyproto)
-                    if prop.fset:
-                        setter = self.__wrap_pycallable(prop.fset,
-                                                        pyproto)
-                    if getter:
-                        self.cx.call_function(jsproto,
-                                              define_getter,
-                                              (name, getter))
-                    if setter:
-                        self.cx.call_function(jsproto,
-                                              define_setter,
-                                              (name, setter,))
-            for name in dir(pyproto):
-                attr = getattr(pyproto, name)
-                if (isinstance(attr, types.UnboundMethodType) and
-                    hasattr(attr, '__jsexposed__') and
-                    attr.__jsexposed__):
-                    jsmethod = self.__wrap_pycallable(attr, pyproto)
-                    self.cx.define_property(jsproto, name, jsmethod)
-            self.__type_protos[pyproto] = jsproto
-        return self.cx.new_object(value, self.__type_protos[pyproto])
-
-    def wrap_pyobject(self, value):
-        """
-        Wraps the given Python object for export to untrusted JS.
-
-        If the Python object isn't of a type that can be exposed to JS,
-        a TypeError is raised.
-        """
-
-        if (isinstance(value, (int, basestring, float, bool,
-                               pydermonkey.Object)) or
-            value is pydermonkey.undefined or
-            value is None):
-            return value
-        if isinstance(value, SafeJsObjectWrapper):
-            # It's already wrapped, just unwrap it.
-            return value.wrapped_jsobject
-        elif callable(value):
-            if not (hasattr(value, '__jsexposed__') and
-                    value.__jsexposed__):
-                raise ValueError("Callable isn't configured for exposure "
-                                 "to untrusted JS code")
-            return self.__wrap_pycallable(value)
-        elif isinstance(value, JsExposedObject):
-            return self.__wrap_pyinstance(value)
-        else:
-            raise TypeError("Can't expose objects of type '%s' to JS." %
-                            type(value).__name__)
-
-    def wrap_jsobject(self, jsvalue, this=None):
-        """
-        Wraps the given pydermonkey.Object for import to trusted
-        Python code. If the type is just a primitive, it's simply
-        returned, since no wrapping is needed.
-        """
-
-        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):
-            # It's a wrapped Python object instance, just unwrap it.
-            instance = self.cx.get_object_private(jsvalue)
-            if instance:
-                if not isinstance(instance, JsExposedObject):
-                    raise AssertionError("Object private is not of type "
-                                         "JsExposedObject")
-                return instance
-            else:
-                return SafeJsObjectWrapper(self, jsvalue, this)
-        else:
-            # It's a primitive value.
-            return jsvalue
-
-    def new_array(self, *contents):
-        """
-        Creates a new JavaScript array with the given contents and
-        returns a wrapper for it.
-        """
-
-        array = self.wrap_jsobject(self.cx.new_array_object())
-        for item in contents:
-            array.push(item)
-        return array
-
-    def new_object(self, **contents):
-        """
-        Creates a new JavaScript object with the given properties and
-        returns a wrapper for it.
-        """
-
-        obj = self.wrap_jsobject(self.cx.new_object())
-        for name in contents:
-            obj[name] = contents[name]
-        return obj
-
-    def get_calling_script(self):
-        """
-        Returns the filename of the current stack's most recent
-        JavaScript caller.
-        """
-
-        frame = self.cx.get_stack()['caller']
-        curr_script = None
-        while frame and curr_script is None:
-            if frame['function'] and frame['function'].filename:
-                curr_script = frame['function'].filename
-            elif frame['script']:
-                curr_script = frame['script'].filename
-            frame = frame['caller']
-
-        if curr_script is None:
-            raise RuntimeError("Can't find calling script")
-        return curr_script
-
-    def _install_globals(self, object):
-        for name in self.__globals:
-            object[name] = self.__globals[name]
-        object['require'] = self._require
-
-    @jsexposed(name='require')
-    def _require(self, path):
-        """
-        Implementation for the global require() function, implemented
-        as per the CommonJS SecurableModule specification:
-
-        http://wiki.commonjs.org/wiki/CommonJS/Modules/SecurableModules
-        """
-
-        filename = self.fs.find_module(self.get_calling_script(), path)
-        if not filename:
-            raise pydermonkey.ScriptError('Module not found: %s' % path)
-        if not filename in self.__modules:
-            cx = self.cx
-            module = cx.new_object(None, self.__root_proto)
-            exports = cx.new_object()
-            cx.define_property(module, 'exports', exports)
-            self._install_globals(self.wrap_jsobject(module))
-            self.__modules[filename] = self.wrap_jsobject(exports)
-            contents = self.fs.open(filename).read()
-            cx.evaluate_script(module, contents, filename, 1)
-        return self.__modules[filename]
-
-    def run_script(self, contents, filename='<string>', lineno=1,
-                   callback=None, stderr=None):
-        """
-        Runs the given JS script, returning 0 on success, -1 on failure.
-        """
-
-        if stderr is None:
-            stderr = sys.stderr
-
-        retval = -1
-        cx = self.cx
-        try:
-            result = cx.evaluate_script(self.root.wrapped_jsobject,
-                                        contents, filename, lineno)
-            if callback:
-                callback(self.wrap_jsobject(result))
-            retval = 0
-        except pydermonkey.ScriptError, e:
-            params = dict(
-                stack_trace = format_stack(self.js_stack, self.fs.open),
-                error = e.args[1]
-                )
-            stderr.write("%(stack_trace)s\n%(error)s\n" % params)
-        except InternalError, e:
-            stderr.write("An internal error occurred.\n")
-            traceback.print_exception(e.exc_info[0], e.exc_info[1],
-                                      e.exc_info[2], None, stderr)
-        return retval
-
-class HttpFileSystem(object):
-    """
-    File system through which all resources are loaded over HTTP.
-    """
-
-    def __init__(self, base_url):
-        self.base_url = base_url
-
-    def find_module(self, curr_url, path):
-        import urlparse
-
-        if path.startswith("."):
-            base_url = curr_url
-        else:
-            base_url = self.base_url
-
-        url = "%s.js" % urlparse.urljoin(base_url, path)
-        if not url.startswith(self.base_url):
-            return None
-        return url
-
-    def open(self, url):
-        import urllib
-
-        return urllib.urlopen(url)
-
-class LocalFileSystem(object):
-    """
-    File system through which all resources are loaded over the local
-    filesystem.
-    """
-
-    def __init__(self, root_dir):
-        self.root_dir = root_dir
-
-    def find_module(self, curr_script, path):
-        import os
-
-        if path.startswith("."):
-            base_dir = os.path.dirname(curr_script)
-        else:
-            base_dir = self.root_dir
-
-        ospath = path.replace('/', os.path.sep)
-        filename = os.path.join(base_dir, "%s.js" % ospath)
-        filename = os.path.normpath(filename)
-        if (filename.startswith(self.root_dir) and
-            (os.path.exists(filename) and
-             not os.path.isdir(filename))):
-            return filename
-        else:
-            return None
-
-    def open(self, filename):
-        return open(filename, 'r')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pydertron/__init__.py	Mon May 10 01:18:22 2010 -0700
@@ -0,0 +1,648 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is Pydertron.
+#
+# The Initial Developer of the Original Code is Mozilla.
+# Portions created by the Initial Developer are Copyright (C) 2007
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Atul Varma <atul@mozilla.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+"""
+    Pydertron is a high-level wrapper for Pydermonkey that
+    provides convenient, secure object wrapping between JS and Python
+    space.
+"""
+
+import sys
+import threading
+import traceback
+import weakref
+import types
+import atexit
+
+import pydermonkey
+
+class ContextWatchdogThread(threading.Thread):
+    """
+    Watches active JS contexts and triggers their operation callbacks
+    at a regular interval.
+    """
+
+    # Default interval, in seconds, that the operation callbacks are
+    # triggered at.
+    DEFAULT_INTERVAL = 0.25
+
+    def __init__(self, interval=DEFAULT_INTERVAL):
+        threading.Thread.__init__(self)
+        self._lock = threading.Lock()
+        self._stop = threading.Event()
+        self._contexts = []
+        self.interval = interval
+
+    def add_context(self, cx):
+        self._lock.acquire()
+        try:
+            self._contexts.append(weakref.ref(cx))
+        finally:
+            self._lock.release()
+
+    def join(self):
+        self._stop.set()
+        threading.Thread.join(self)
+
+    def run(self):
+        while not self._stop.isSet():
+            new_list = []
+            self._lock.acquire()
+            try:
+                for weakcx in self._contexts:
+                    cx = weakcx()
+                    if cx:
+                        new_list.append(weakcx)
+                        cx.trigger_operation_callback()
+                        del cx
+                self._contexts = new_list
+            finally:
+                self._lock.release()
+            self._stop.wait(self.interval)
+
+# Create a global watchdog.
+watchdog = ContextWatchdogThread()
+watchdog.start()
+atexit.register(watchdog.join)
+
+class InternalError(BaseException):
+    """
+    Represents an error in a JS-wrapped Python function that wasn't
+    expected to happen; because it's derived from BaseException, it
+    unrolls the whole JS/Python stack so that the error can be
+    reported to the outermost calling code.
+    """
+
+    def __init__(self):
+        BaseException.__init__(self)
+        self.exc_info = sys.exc_info()
+
+class SafeJsObjectWrapper(object):
+    """
+    Securely wraps a JS object to behave like any normal Python
+    object. Like JS objects, though, accessing undefined object
+    results merely in pydermonkey.undefined.
+
+    Object properties may be accessed either via attribute or
+    item-based lookup.
+    """
+
+    __slots__ = ['_jsobject', '_sandbox', '_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, '_this', this)
+
+    @property
+    def wrapped_jsobject(self):
+        return self._jsobject
+
+    def _wrap_to_python(self, jsvalue):
+        return self._sandbox.wrap_jsobject(jsvalue, self._jsobject)
+
+    def _wrap_to_js(self, value):
+        return self._sandbox.wrap_pyobject(value)
+
+    def __eq__(self, other):
+        if isinstance(other, SafeJsObjectWrapper):
+            return self._jsobject == other._jsobject
+        else:
+            return False
+
+    def __str__(self):
+        return self.toString()
+
+    def __unicode__(self):
+        return self.toString()
+
+    def __setitem__(self, item, value):
+        self.__setattr__(item, value)
+
+    def __setattr__(self, name, value):
+        cx = self._sandbox.cx
+        jsobject = self._jsobject
+
+        cx.define_property(jsobject, name,
+                           self._wrap_to_js(value))
+
+    def __getitem__(self, item):
+        return self.__getattr__(item)
+
+    def __getattr__(self, name):
+        cx = self._sandbox.cx
+        jsobject = self._jsobject
+
+        return self._wrap_to_python(cx.get_property(jsobject, name))
+
+    def __contains__(self, item):
+        cx = self._sandbox.cx
+        jsobject = self._jsobject
+
+        return cx.has_property(jsobject, item)
+
+    def __iter__(self):
+        cx = self._sandbox.cx
+        jsobject = self._jsobject
+
+        properties = cx.enumerate(jsobject)
+        for property in properties:
+            yield property
+
+class SafeJsFunctionWrapper(SafeJsObjectWrapper):
+    """
+    Securely wraps a JS function to behave like any normal Python object.
+    """
+
+    def __init__(self, sandbox, jsfunction, this):
+        if not isinstance(jsfunction, pydermonkey.Function):
+            raise TypeError("Cannot wrap '%s' object" %
+                            type(jsobject).__name__)
+        SafeJsObjectWrapper.__init__(self, sandbox, jsfunction, this)
+
+    def __call__(self, *args):
+        cx = self._sandbox.cx
+        jsobject = self._jsobject
+        this = self._this
+
+        arglist = []
+        for arg in args:
+            arglist.append(self._wrap_to_js(arg))
+
+        obj = cx.call_function(this, jsobject, tuple(arglist))
+        return self._wrap_to_python(obj)
+
+def format_stack(js_stack, open=open):
+    """
+    Returns a formatted Python-esque stack traceback of the given
+    JS stack.
+    """
+
+    STACK_LINE  ="  File \"%(filename)s\", line %(lineno)d, in %(name)s"
+
+    lines = []
+    while js_stack:
+        script = js_stack['script']
+        function = js_stack['function']
+        if script:
+            frameinfo = dict(filename = script.filename,
+                             lineno = js_stack['lineno'],
+                             name = '<module>')
+        elif function and not function.is_python:
+            frameinfo = dict(filename = function.filename,
+                             lineno = js_stack['lineno'],
+                             name = function.name)
+        else:
+            frameinfo = None
+        if frameinfo:
+            lines.insert(0, STACK_LINE % frameinfo)
+            try:
+                filelines = open(frameinfo['filename']).readlines()
+                line = filelines[frameinfo['lineno'] - 1].strip()
+                lines.insert(1, "    %s" % line)
+            except Exception:
+                pass
+        js_stack = js_stack['caller']
+    lines.insert(0, "Traceback (most recent call last):")
+    return '\n'.join(lines)
+
+def jsexposed(name=None, on=None):
+    """
+    Decorator used to expose the decorated function or method to
+    untrusted JS.
+
+    'name' is an optional alternative name for the function.
+
+    'on' is an optional SafeJsObjectWrapper that the function can be
+    automatically attached as a property to.
+    """
+
+    if callable(name):
+        func = name
+        func.__jsexposed__ = True
+        return func
+
+    def make_exposed(func):
+        if name:
+            func.__name__ = name
+        func.__jsexposed__ = True
+        if on:
+            on[func.__name__] = func
+        return func
+    return make_exposed
+
+class JsExposedObject(object):
+    """
+    Trivial base/mixin class for any Python classes that choose to
+    expose themselves to JS code.
+    """
+
+    pass
+
+class JsSandbox(object):
+    """
+    A JS runtime and associated functionality capable of securely
+    loading and executing scripts.
+    """
+
+    def __init__(self, fs, watchdog=watchdog, opcb=None):
+        rt = pydermonkey.Runtime()
+        cx = rt.new_context()
+        root_proto = cx.new_object()
+        cx.init_standard_classes(root_proto)
+        root = cx.new_object(None, root_proto)
+
+        cx.set_operation_callback(self._opcb)
+        cx.set_throw_hook(self._throwhook)
+        watchdog.add_context(cx)
+
+        self.fs = fs
+        self.opcb = opcb
+        self.rt = rt
+        self.cx = cx
+        self.curr_exc = None
+        self.py_stack = None
+        self.js_stack = None
+        self.__modules = {}
+        self.__py_to_js = {}
+        self.__type_protos = {}
+        self.__globals = {}
+        self.__root_proto = root_proto
+        self.root = self.wrap_jsobject(root, root)
+
+    def set_globals(self, **globals):
+        """
+        Sets the global properties for the root object and all global
+        scopes (e.g., SecurableModules).  This should be called before
+        any scripts are executed.
+        """
+
+        self.__globals.update(globals)
+        self._install_globals(self.root)
+
+    def finish(self):
+        """
+        Cleans up all resources used by the sandbox, breaking any reference
+        cycles created due to issue #2 in pydermonkey:
+
+        http://code.google.com/p/pydermonkey/issues/detail?id=2
+        """
+
+        for jsobj in self.__py_to_js.values():
+            self.cx.clear_object_private(jsobj)
+        del self.__py_to_js
+        del self.__type_protos
+        del self.curr_exc
+        del self.py_stack
+        del self.js_stack
+        del self.cx
+        del self.rt
+
+    def _opcb(self, cx):
+        # Note that if a keyboard interrupt was triggered,
+        # it'll get raised here automatically.
+        if self.opcb:
+            self.opcb()
+
+    def _throwhook(self, cx):
+        curr_exc = cx.get_pending_exception()
+        if self.curr_exc != curr_exc:
+            self.curr_exc = curr_exc
+            self.py_stack = traceback.extract_stack()
+            self.js_stack = cx.get_stack()
+
+    def __wrap_pycallable(self, func, pyproto=None):
+        if func in self.__py_to_js:
+            return self.__py_to_js[func]
+
+        if hasattr(func, '__name__'):
+            name = func.__name__
+        else:
+            name = ""
+
+        if pyproto:
+            def wrapper(func_cx, this, args):
+                try:
+                    arglist = []
+                    for arg in args:
+                        arglist.append(self.wrap_jsobject(arg))
+                    instance = func_cx.get_object_private(this)
+                    if instance is None or not isinstance(instance, pyproto):
+                        raise pydermonkey.ScriptError("Method type mismatch")
+
+                    # TODO: Fill in extra required params with
+                    # pymonkey.undefined?  or automatically throw an
+                    # exception to calling js code?
+                    return self.wrap_pyobject(func(instance, *arglist))
+                except pydermonkey.ScriptError:
+                    raise
+                except Exception:
+                    raise InternalError()
+        else:
+            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 self.wrap_pyobject(func(*arglist))
+                except pydermonkey.ScriptError:
+                    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_pyinstance(self, value):
+        pyproto = type(value)
+        if pyproto not in self.__type_protos:
+            jsproto = self.cx.new_object()
+            if hasattr(pyproto, '__jsprops__'):
+                define_getter = self.cx.get_property(jsproto,
+                                                     '__defineGetter__')
+                define_setter = self.cx.get_property(jsproto,
+                                                     '__defineSetter__')
+                for name in pyproto.__jsprops__:
+                    prop = getattr(pyproto, name)
+                    if not type(prop) == property:
+                        raise TypeError("Expected attribute '%s' to "
+                                        "be a property" % name)
+                    getter = None
+                    setter = None
+                    if prop.fget:
+                        getter = self.__wrap_pycallable(prop.fget,
+                                                        pyproto)
+                    if prop.fset:
+                        setter = self.__wrap_pycallable(prop.fset,
+                                                        pyproto)
+                    if getter:
+                        self.cx.call_function(jsproto,
+                                              define_getter,
+                                              (name, getter))
+                    if setter:
+                        self.cx.call_function(jsproto,
+                                              define_setter,
+                                              (name, setter,))
+            for name in dir(pyproto):
+                attr = getattr(pyproto, name)
+                if (isinstance(attr, types.UnboundMethodType) and
+                    hasattr(attr, '__jsexposed__') and
+                    attr.__jsexposed__):
+                    jsmethod = self.__wrap_pycallable(attr, pyproto)
+                    self.cx.define_property(jsproto, name, jsmethod)
+            self.__type_protos[pyproto] = jsproto
+        return self.cx.new_object(value, self.__type_protos[pyproto])
+
+    def wrap_pyobject(self, value):
+        """
+        Wraps the given Python object for export to untrusted JS.
+
+        If the Python object isn't of a type that can be exposed to JS,
+        a TypeError is raised.
+        """
+
+        if (isinstance(value, (int, basestring, float, bool,
+                               pydermonkey.Object)) or
+            value is pydermonkey.undefined or
+            value is None):
+            return value
+        if isinstance(value, SafeJsObjectWrapper):
+            # It's already wrapped, just unwrap it.
+            return value.wrapped_jsobject
+        elif callable(value):
+            if not (hasattr(value, '__jsexposed__') and
+                    value.__jsexposed__):
+                raise ValueError("Callable isn't configured for exposure "
+                                 "to untrusted JS code")
+            return self.__wrap_pycallable(value)
+        elif isinstance(value, JsExposedObject):
+            return self.__wrap_pyinstance(value)
+        else:
+            raise TypeError("Can't expose objects of type '%s' to JS." %
+                            type(value).__name__)
+
+    def wrap_jsobject(self, jsvalue, this=None):
+        """
+        Wraps the given pydermonkey.Object for import to trusted
+        Python code. If the type is just a primitive, it's simply
+        returned, since no wrapping is needed.
+        """
+
+        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):
+            # It's a wrapped Python object instance, just unwrap it.
+            instance = self.cx.get_object_private(jsvalue)
+            if instance:
+                if not isinstance(instance, JsExposedObject):
+                    raise AssertionError("Object private is not of type "
+                                         "JsExposedObject")
+                return instance
+            else:
+                return SafeJsObjectWrapper(self, jsvalue, this)
+        else:
+            # It's a primitive value.
+            return jsvalue
+
+    def new_array(self, *contents):
+        """
+        Creates a new JavaScript array with the given contents and
+        returns a wrapper for it.
+        """
+
+        array = self.wrap_jsobject(self.cx.new_array_object())
+        for item in contents:
+            array.push(item)
+        return array
+
+    def new_object(self, **contents):
+        """
+        Creates a new JavaScript object with the given properties and
+        returns a wrapper for it.
+        """
+
+        obj = self.wrap_jsobject(self.cx.new_object())
+        for name in contents:
+            obj[name] = contents[name]
+        return obj
+
+    def get_calling_script(self):
+        """
+        Returns the filename of the current stack's most recent
+        JavaScript caller.
+        """
+
+        frame = self.cx.get_stack()['caller']
+        curr_script = None
+        while frame and curr_script is None:
+            if frame['function'] and frame['function'].filename:
+                curr_script = frame['function'].filename
+            elif frame['script']:
+                curr_script = frame['script'].filename
+            frame = frame['caller']
+
+        if curr_script is None:
+            raise RuntimeError("Can't find calling script")
+        return curr_script
+
+    def _install_globals(self, object):
+        for name in self.__globals:
+            object[name] = self.__globals[name]
+        object['require'] = self._require
+
+    @jsexposed(name='require')
+    def _require(self, path):
+        """
+        Implementation for the global require() function, implemented
+        as per the CommonJS SecurableModule specification:
+
+        http://wiki.commonjs.org/wiki/CommonJS/Modules/SecurableModules
+        """
+
+        filename = self.fs.find_module(self.get_calling_script(), path)
+        if not filename:
+            raise pydermonkey.ScriptError('Module not found: %s' % path)
+        if not filename in self.__modules:
+            cx = self.cx
+            module = cx.new_object(None, self.__root_proto)
+            exports = cx.new_object()
+            cx.define_property(module, 'exports', exports)
+            self._install_globals(self.wrap_jsobject(module))
+            self.__modules[filename] = self.wrap_jsobject(exports)
+            contents = self.fs.open(filename).read()
+            cx.evaluate_script(module, contents, filename, 1)
+        return self.__modules[filename]
+
+    def run_script(self, contents, filename='<string>', lineno=1,
+                   callback=None, stderr=None):
+        """
+        Runs the given JS script, returning 0 on success, -1 on failure.
+        """
+
+        if stderr is None:
+            stderr = sys.stderr
+
+        retval = -1
+        cx = self.cx
+        try:
+            result = cx.evaluate_script(self.root.wrapped_jsobject,
+                                        contents, filename, lineno)
+            if callback:
+                callback(self.wrap_jsobject(result))
+            retval = 0
+        except pydermonkey.ScriptError, e:
+            params = dict(
+                stack_trace = format_stack(self.js_stack, self.fs.open),
+                error = e.args[1]
+                )
+            stderr.write("%(stack_trace)s\n%(error)s\n" % params)
+        except InternalError, e:
+            stderr.write("An internal error occurred.\n")
+            traceback.print_exception(e.exc_info[0], e.exc_info[1],
+                                      e.exc_info[2], None, stderr)
+        return retval
+
+class HttpFileSystem(object):
+    """
+    File system through which all resources are loaded over HTTP.
+    """
+
+    def __init__(self, base_url):
+        self.base_url = base_url
+
+    def find_module(self, curr_url, path):
+        import urlparse
+
+        if path.startswith("."):
+            base_url = curr_url
+        else:
+            base_url = self.base_url
+
+        url = "%s.js" % urlparse.urljoin(base_url, path)
+        if not url.startswith(self.base_url):
+            return None
+        return url
+
+    def open(self, url):
+        import urllib
+
+        return urllib.urlopen(url)
+
+class LocalFileSystem(object):
+    """
+    File system through which all resources are loaded over the local
+    filesystem.
+    """
+
+    def __init__(self, root_dir):
+        self.root_dir = root_dir
+
+    def find_module(self, curr_script, path):
+        import os
+
+        if path.startswith("."):
+            base_dir = os.path.dirname(curr_script)
+        else:
+            base_dir = self.root_dir
+
+        ospath = path.replace('/', os.path.sep)
+        filename = os.path.join(base_dir, "%s.js" % ospath)
+        filename = os.path.normpath(filename)
+        if (filename.startswith(self.root_dir) and
+            (os.path.exists(filename) and
+             not os.path.isdir(filename))):
+            return filename
+        else:
+            return None
+
+    def open(self, filename):
+        return open(filename, 'r')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Mon May 10 01:18:22 2010 -0700
@@ -0,0 +1,22 @@
+from setuptools import setup
+
+setup(
+    name = "pydertron",
+    version = "0.0.1",
+    author = "Atul Varma",
+    author_email = "atul@mozilla.com",
+    description = ("An experimental high-level wrapper for Pydermonkey "
+                   "that provides convenient, secure object wrapping "
+                   "between JS and Python space."),
+    license = "MPL 1.1/GPL 2.0/LGPL 2.1",
+    url = "http://hg.toolness.com/pydertron",
+    packages = ("pydertron",),
+    classifiers = (
+        'Development Status :: 3 - Alpha',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)',
+        'Programming Language :: Python',
+        'Programming Language :: JavaScript',
+        'Topic :: Software Development :: Interpreters',
+        )
+    )