Commits

Philip Jenvey committed f2d3714

support __context__ and __traceback__, be stricter about __cause__

Comments (0)

Files changed (5)

pypy/interpreter/error.py

 
     def __init__(self, w_type, w_value, tb=None, w_cause=None):
         assert w_type is not None
-        self.setup(w_type)
-        self._w_value = w_value
+        self.setup(w_type, w_value)
         self._application_traceback = tb
         self.w_cause = w_cause
 
-    def setup(self, w_type):
+    def setup(self, w_type, w_value=None):
+        from pypy.objspace.std.typeobject import W_TypeObject
         self.w_type = w_type
+        self._w_value = w_value
+        if isinstance(w_type, W_TypeObject):
+            self.setup_context(w_type.space)
         if not we_are_translated():
             self.debug_excs = []
 
+    def setup_context(self, space):
+        # Implicit exception chaining
+        last_operror = space.getexecutioncontext().sys_exc_info()
+        if last_operror is None:
+            return
+
+        # We must normalize the value right now to check for cycles
+        self.normalize_exception(space)
+        w_value = self.get_w_value(space)
+        w_last_value = last_operror.get_w_value(space)
+        if not space.is_w(w_value, w_last_value):
+            # Avoid reference cycles through the context chain. This is
+            # O(chain length) but context chains are usually very short.
+            w_obj = w_last_value
+            while True:
+                w_context = space.getattr(w_obj, space.wrap('__context__'))
+                if space.is_w(w_context, space.w_None):
+                    break
+                if space.is_w(w_context, w_value):
+                    space.setattr(w_obj, space.wrap('__context__'), space.w_None)
+                    break
+                w_obj = w_context
+            space.setattr(w_value, space.wrap('__context__'), w_last_value)
+
     def clear(self, space):
         self.w_type = space.w_None
         self._w_value = space.w_None
                         w_value = space.call_function(w_type, w_value)
                     w_type = self._exception_getclass(space, w_value)
             if self.w_cause:
+                # ensure w_cause is of a valid type
+                self._exception_getclass(space, self.w_cause, "exception causes")
                 space.setattr(w_value, space.wrap("__cause__"), self.w_cause)
-
+            if self._application_traceback:
+                from pypy.interpreter.pytraceback import PyTraceback
+                from pypy.module.exceptions.interp_exceptions import W_BaseException
+                tb = self._application_traceback
+                if (isinstance(w_value, W_BaseException) and
+                    isinstance(tb, PyTraceback)):
+                    # traceback hasn't escaped yet
+                    w_value.w_traceback = tb
+                else:
+                    # traceback has escaped
+                    space.setattr(w_value, space.wrap("__traceback__"),
+                                  space.wrap(self.get_traceback()))
         else:
             # the only case left here is (inst, None), from a 'raise inst'.
             w_inst = w_type
         self.w_type   = w_type
         self._w_value = w_value
 
-    def _exception_getclass(self, space, w_inst):
+    def _exception_getclass(self, space, w_inst, what="exceptions"):
         w_type = space.exception_getclass(w_inst)
         if not space.exception_is_valid_class_w(w_type):
             typename = w_type.getname(space)
-            msg = ("exceptions must be classes or instances deriving from "
-                   "BaseException, not %s")
-            raise operationerrfmt(space.w_TypeError, msg, typename)
+            msg = "%s must derive from BaseException, not %s"
+            raise operationerrfmt(space.w_TypeError, msg, what, typename)
         return w_type
 
     def write_unraisable(self, space, where, w_object=None,
         #
         class OpErrFmt(OperationError):
             def __init__(self, w_type, strings, *args):
-                self.setup(w_type)
                 assert len(args) == len(strings) - 1
                 self.xstrings = strings
                 for i, fmt, attr in entries:
                     setattr(self, attr, args[i])
                 assert w_type is not None
+                self.setup(w_type)
             def _compute_value(self):
                 lst = [None] * (len(formats) + len(formats) + 1)
                 for i, fmt, attr in entries:

pypy/interpreter/pyopcode.py

             # re-raise, no new traceback obj will be attached
             self.last_exception = operror
             raise RaiseWithExplicitTraceback(operror)
-        w_value = w_cause = space.w_None
         if nbargs == 2:
             w_cause = self.popvalue()
             if space.exception_is_valid_obj_as_class_w(w_cause):
                 w_cause = space.call_function(w_cause)
+        else:
+            w_cause = None
         w_value = self.popvalue()
         if space.exception_is_valid_obj_as_class_w(w_value):
             w_type = w_value

pypy/interpreter/test/test_interpreter.py

                 raise 1
             ''', 'f', [])
         assert "TypeError:" in x
-        assert ("exceptions must be classes or instances deriving from "
-                "BaseException, not ") in x
+        assert "exceptions must derive from BaseException" in x
 
     def test_except2(self):
         x = self.codetest('''

pypy/interpreter/test/test_raise.py

             assert sys.exc_info()[0] is ValueError
         assert sys.exc_info() == (None, None, None)
 
-    def test_raise_with___traceback__(self):
-        import sys
-        try:
-            raise ValueError
-        except:
-            exc_type,exc_val,exc_tb = sys.exc_info()
-        try:
-            exc_val.__traceback__ = exc_tb
-            raise exc_val
-        except:
-            exc_type2,exc_val2,exc_tb2 = sys.exc_info()
-        assert exc_type is exc_type2
-        assert exc_val is exc_val2
-        assert exc_tb is exc_tb2.tb_next
-
     def test_reraise_1(self):
         raises(IndexError, """
             import sys
                 break
         assert sys.exc_info() == (None, None, None)
 
+class AppTestRaiseContext:
+
+    def test_instance_context(self):
+        context = IndexError()
+        try:
+            try:
+                raise context
+            except:
+                raise OSError()
+        except OSError as e:
+            assert e.__context__ is context
+        else:
+            fail('No exception raised')
+
+    def test_class_context(self):
+        context = IndexError
+        try:
+            try:
+                raise context
+            except:
+                raise OSError()
+        except OSError as e:
+            assert e.__context__ != context
+            assert isinstance(e.__context__, context)
+        else:
+            fail('No exception raised')
+
+    def test_internal_exception(self):
+        try:
+            try:
+                1/0
+            except:
+                xyzzy
+        except NameError as e:
+            assert isinstance(e.__context__, ZeroDivisionError)
+        else:
+            fail("No exception raised")
+
+    def test_cycle_broken(self):
+        try:
+            try:
+                1/0
+            except ZeroDivisionError as e:
+                raise e
+        except ZeroDivisionError as e:
+            assert e.__context__ is None
+        else:
+            fail("No exception raised")
+
+    def test_reraise_cycle_broken(self):
+        try:
+            try:
+                xyzzy
+            except NameError as a:
+                try:
+                    1/0
+                except ZeroDivisionError:
+                    raise a
+        except NameError as e:
+            assert e.__context__.__context__ is None
+        else:
+            fail("No exception raised")
+
+class AppTestTraceback:
+
+    def test_raise_with___traceback__(self):
+        import sys
+        try:
+            raise ValueError
+        except:
+            exc_type,exc_val,exc_tb = sys.exc_info()
+        try:
+            exc_val.__traceback__ = exc_tb
+            raise exc_val
+        except:
+            exc_type2,exc_val2,exc_tb2 = sys.exc_info()
+        assert exc_type is exc_type2
+        assert exc_val is exc_val2
+        assert exc_tb is exc_tb2.tb_next
+
+    def test_sets_traceback(self):
+        import types
+        try:
+            raise IndexError()
+        except IndexError as e:
+            assert isinstance(e.__traceback__, types.TracebackType)
+        else:
+            fail("No exception raised")
+
+    def test_accepts_traceback(self):
+        import sys
+        def get_tb():
+            try:
+                raise OSError()
+            except:
+                return sys.exc_info()[2]
+        tb = get_tb()
+        try:
+            raise IndexError().with_traceback(tb)
+        except IndexError as e:
+            assert e.__traceback__ != tb
+            assert e.__traceback__.tb_next is tb
+        else:
+            fail("No exception raised")
+
     def test_invalid_reraise(self):
         try:
             raise
             assert "No active exception" in str(e)
         else:
             fail("Expected RuntimeError")
+
+    def test_invalid_cause(self):
+        """
+        try:
+            raise IndexError from 5
+        except TypeError as e:
+            assert "exception cause" in str(e)
+        else:
+            fail("Expected TypeError")
+            """
+
+    def test_invalid_cause_setter(self):
+        """
+        class Setter(BaseException):
+            def set_cause(self, cause):
+                self.cause = cause
+            __cause__ = property(fset=set_cause)
+        try:
+            raise Setter from 5
+        except TypeError as e:
+            assert "exception cause" in str(e)
+        else:
+            fail("Expected TypeError")
+            """

pypy/module/exceptions/interp_exceptions.py

         self.w_context = w_newcontext
 
     def descr_gettraceback(self, space):
-        return self.w_traceback
+        from pypy.interpreter.pytraceback import PyTraceback
+        tb = self.w_traceback
+        if tb is not None and isinstance(tb, PyTraceback):
+            # tb escapes to app level (see OperationError.get_traceback)
+            tb.frame.mark_as_escaped()
+        return tb
 
     def descr_settraceback(self, space, w_newtraceback):
         msg = '__traceback__ must be a traceback or None'