Skip to content

Commit e61b5f3

Browse files
committed
Merge pull request #56 from python-effect/local-only-stopiteration
re-raise StopIteration unless it comes directly from the @do-wrapped generator
2 parents db1f3f0 + de84ea6 commit e61b5f3

File tree

2 files changed

+36
-8
lines changed

2 files changed

+36
-8
lines changed

effect/do.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from __future__ import print_function
99

10+
import sys
1011
import types
1112

1213
from . import Effect, Func
@@ -26,11 +27,12 @@ def foo():
2627
eff = foo()
2728
return eff.on(...)
2829
29-
``@do`` must decorate a generator function. Any yielded values must either
30-
be Effects or the result of a :func:`do_return` call. The result of a
31-
yielded Effect will be passed back into the generator as the result of the
32-
``yield`` expression. Yielded :func:`do_return` values will provide the
33-
ultimate result of the Effect that is returned by the decorated function.
30+
``@do`` must decorate a generator function (not any other type of
31+
iterator). Any yielded values must either be Effects or the result of a
32+
:func:`do_return` call. The result of a yielded Effect will be passed back
33+
into the generator as the result of the ``yield`` expression. Yielded
34+
:func:`do_return` values will provide the ultimate result of the Effect
35+
that is returned by the decorated function.
3436
3537
It's important to note that any generator function decorated by ``@do``
3638
will no longer return a generator, but instead it will return an Effect,
@@ -88,7 +90,16 @@ def _do(result, generator, is_error):
8890
else:
8991
val = generator.send(result)
9092
except StopIteration:
91-
return None
93+
# If the generator we're spinning directly raises StopIteration, we'll
94+
# treat it like returning None from the function. But there may be a
95+
# case where some other code is raising StopIteration up through this
96+
# generator, in which case we shouldn't really treat it like a function
97+
# return -- it could quite easily hide bugs.
98+
tb = sys.exc_info()[2]
99+
if tb.tb_next:
100+
raise
101+
else:
102+
return None
92103
if type(val) is _ReturnSentinel:
93104
return val.result
94105
elif type(val) is Effect:

effect/test_do.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import sys
22
from functools import partial
33

4+
from py.test import raises as raises
5+
46
from testtools import TestCase
5-
from testtools.matchers import raises, MatchesException
7+
from testtools.matchers import raises as match_raises, MatchesException
68

79
from . import (
810
ComposedDispatcher, Constant, Effect, Error, TypeDispatcher,
@@ -21,7 +23,7 @@ def test_do_non_gf(self):
2123
f = lambda: None
2224
self.assertThat(
2325
lambda: perf(do(f)()),
24-
raises(TypeError(
26+
match_raises(TypeError(
2527
"%r is not a generator function. It returned None." % (f,)
2628
)))
2729

@@ -133,3 +135,18 @@ def f():
133135
eff = f()
134136
self.assertEqual(perf(eff), 'foo')
135137
self.assertEqual(perf(eff), 'foo')
138+
139+
140+
def test_stop_iteration_only_local():
141+
"""
142+
Arbitrary :obj:`StopIteration` exceptions are not treated the same way as
143+
falling off the end of the generator -- they are raised through.
144+
"""
145+
@do
146+
def f():
147+
raise StopIteration()
148+
yield Effect(Constant('foo'))
149+
150+
eff = f()
151+
with raises(StopIteration):
152+
perf(eff)

0 commit comments

Comments
 (0)