[pypy-commit] pypy py3.5: CPython's ``sys.settrace()`` sometimes reports an ``exception`` at the

arigo pypy.commits at gmail.com
Tue Dec 13 09:49:19 EST 2016


Author: Armin Rigo <arigo at tunes.org>
Branch: py3.5
Changeset: r89045:d86691a0c3ca
Date: 2016-12-13 15:48 +0100
http://bitbucket.org/pypy/pypy/changeset/d86691a0c3ca/

Log:	CPython's ``sys.settrace()`` sometimes reports an ``exception`` at
	the end of ``for`` or ``yield from`` lines for the
	``StopIteration``, and sometimes not. The problem is that it occurs
	in an ill-defined subset of cases. PyPy attempts to emulate that but
	the precise set of cases is not exactly the same.

diff --git a/pypy/doc/cpython_differences.rst b/pypy/doc/cpython_differences.rst
--- a/pypy/doc/cpython_differences.rst
+++ b/pypy/doc/cpython_differences.rst
@@ -478,6 +478,12 @@
   from the Makefile used to build the interpreter. PyPy should bake the values
   in during compilation, but does not do that yet.
 
+* CPython's ``sys.settrace()`` sometimes reports an ``exception`` at the
+  end of ``for`` or ``yield from`` lines for the ``StopIteration``, and
+  sometimes not.  The problem is that it occurs in an ill-defined subset
+  of cases.  PyPy attempts to emulate that but the precise set of cases
+  is not exactly the same.
+
 .. _`is ignored in PyPy`: http://bugs.python.org/issue14621
 .. _`little point`: http://events.ccc.de/congress/2012/Fahrplan/events/5152.en.html
 .. _`#2072`: https://bitbucket.org/pypy/pypy/issue/2072/
diff --git a/pypy/interpreter/error.py b/pypy/interpreter/error.py
--- a/pypy/interpreter/error.py
+++ b/pypy/interpreter/error.py
@@ -309,6 +309,9 @@
             tb.frame.mark_as_escaped()
         return tb
 
+    def has_any_traceback(self):
+        return self._application_traceback is not None
+
     def set_cause(self, space, w_cause):
         if w_cause is None:
             return
diff --git a/pypy/interpreter/generator.py b/pypy/interpreter/generator.py
--- a/pypy/interpreter/generator.py
+++ b/pypy/interpreter/generator.py
@@ -185,7 +185,7 @@
             if not e.match(space, space.w_StopIteration):
                 raise
             e.normalize_exception(space)
-            space.getexecutioncontext().exception_trace(frame, e)
+            frame._report_stopiteration_sometimes(w_yf, e)
             try:
                 w_stop_value = space.getattr(e.get_w_value(space),
                                              space.wrap("value"))
diff --git a/pypy/interpreter/pyopcode.py b/pypy/interpreter/pyopcode.py
--- a/pypy/interpreter/pyopcode.py
+++ b/pypy/interpreter/pyopcode.py
@@ -1154,13 +1154,31 @@
             if not e.match(self.space, self.space.w_StopIteration):
                 raise
             # iterator exhausted
-            self.space.getexecutioncontext().exception_trace(self, e)
+            self._report_stopiteration_sometimes(w_iterator, e)
             self.popvalue()
             next_instr += jumpby
         else:
             self.pushvalue(w_nextitem)
         return next_instr
 
+    def _report_stopiteration_sometimes(self, w_iterator, operr):
+        # CPython 3.5 calls the exception trace in an ill-defined subset
+        # of cases: only if tp_iternext returned NULL and set a
+        # StopIteration exception, but not if tp_iternext returned NULL
+        # *without* setting an exception.  We can't easily emulate that
+        # behavior at this point.  For example, the generator's
+        # tp_iternext uses one or other case depending on whether the
+        # generator is already exhausted or just exhausted now.  We'll
+        # classify that as a CPython incompatibility and use an
+        # approximative rule: if w_iterator is a generator-iterator,
+        # we always report it; if operr has already a stack trace
+        # attached (likely from a custom __iter__() method), we also
+        # report it; in other cases, we don't.
+        from pypy.interpreter.generator import GeneratorOrCoroutine
+        if (isinstance(w_iterator, GeneratorOrCoroutine) or
+                operr.has_any_traceback()):
+            self.space.getexecutioncontext().exception_trace(self, operr)
+
     def FOR_LOOP(self, oparg, next_instr):
         raise BytecodeCorruption("old opcode, no longer in use")
 
diff --git a/pypy/interpreter/test/test_pyframe.py b/pypy/interpreter/test/test_pyframe.py
--- a/pypy/interpreter/test/test_pyframe.py
+++ b/pypy/interpreter/test/test_pyframe.py
@@ -666,10 +666,33 @@
         sys.settrace(None)
         print('seen:', seen)
         # on Python 3 we get an extra 'exception' when 'for' catches
-        # StopIteration
+        # StopIteration (but not always! mess)
         assert seen == ['call', 'line', 'call', 'return', 'exception', 'return']
         assert frames[-2].f_code.co_name == 'g'
 
+    def test_nongenerator_trace_stopiteration(self):
+        import sys
+        gen = iter([5])
+        assert next(gen) == 5
+        seen = []
+        frames = []
+        def trace_func(frame, event, *args):
+            print('TRACE:', frame, event, args)
+            seen.append(event)
+            frames.append(frame)
+            return trace_func
+        def g():
+            for x in gen:
+                never_entered
+        sys.settrace(trace_func)
+        g()
+        sys.settrace(None)
+        print('seen:', seen)
+        # hack: don't report the StopIteration for some "simple"
+        # iterators.
+        assert seen == ['call', 'line', 'return']
+        assert frames[-2].f_code.co_name == 'g'
+
     def test_yieldfrom_trace_stopiteration(self): """
         import sys
         def f2():
@@ -726,6 +749,30 @@
         assert frames[-2].f_code.co_name == 'g'
         """
 
+    def test_yieldfrom_trace_stopiteration_3(self): """
+        import sys
+        def f():
+            yield from []
+        gen = f()
+        seen = []
+        frames = []
+        def trace_func(frame, event, *args):
+            print('TRACE:', frame, event, args)
+            seen.append(event)
+            frames.append(frame)
+            return trace_func
+        def g():
+            for x in gen:
+                never_entered
+        sys.settrace(trace_func)
+        g()      # invokes next_yield_from() from YIELD_FROM()
+        sys.settrace(None)
+        print('seen:', seen)
+        assert seen == ['call', 'line', 'call', 'line',
+                        'return', 'exception', 'return']
+        assert frames[-4].f_code.co_name == 'f'
+        """
+
     def test_clear_locals(self):
         def make_frames():
             def outer():


More information about the pypy-commit mailing list