Skip to content

Commit 3e4e94e

Browse files
committed
Prevent an Outcome to be unwrapped twice.
1 parent 9df5122 commit 3e4e94e

File tree

9 files changed

+58
-12
lines changed

9 files changed

+58
-12
lines changed

docs/source/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ API Reference
2121
.. autoclass:: Error
2222
:members:
2323
:inherited-members:
24+
25+
.. autoclass:: AlreadyUsedError

docs/source/tutorial.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ which, like before, is the same as::
2525

2626
x = await f(*args, **kwargs)
2727

28+
An Outcome object may not be unwrapped twice. Attempting to do so will
29+
raise an :class:`AlreadyUsedError`.
30+
2831
See the :ref:`api-reference` for the types involved.

newsfragments/7.feature.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
An Outcome may only be unwrapped or sent once.
2+
3+
Attempting to do so a second time will raise an :class:`AlreadyUsedError`.

src/outcome/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99
if sys.version_info >= (3, 5):
1010
from ._async import Error, Outcome, Value, acapture, capture
11-
__all__ = ('Error', 'Outcome', 'Value', 'acapture', 'capture')
11+
__all__ = ('Error', 'Outcome', 'Value', 'acapture', 'capture', 'AlreadyUsedError')
1212
else:
1313
from ._sync import Error, Outcome, Value, capture
14-
__all__ = ('Error', 'Outcome', 'Value', 'capture')
14+
__all__ = ('Error', 'Outcome', 'Value', 'capture', 'AlreadyUsedError')
1515

1616

17-
from ._util import fixup_module_metadata
17+
from ._util import fixup_module_metadata, AlreadyUsedError
1818
fixup_module_metadata(__name__, globals())
1919
del fixup_module_metadata

src/outcome/_async.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Error as ErrorBase, Outcome as OutcomeBase, Value as ValueBase
55
)
66

7+
from ._util import AlreadyUsedError
78
__all__ = ['Error', 'Outcome', 'Value', 'acapture', 'capture']
89

910

@@ -49,11 +50,13 @@ async def asend(self, agen):
4950

5051
class Value(ValueBase):
5152
async def asend(self, agen):
53+
self._set_unwrapped()
5254
return await agen.asend(self.value)
5355

5456

5557
class Error(ErrorBase):
5658
async def asend(self, agen):
59+
self._set_unwrapped()
5760
return await agen.athrow(self.error)
5861

5962

src/outcome/_sync.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import abc
55
import attr
66

7-
from ._util import ABC
7+
from ._util import ABC, AlreadyUsedError
88

99
__all__ = ['Error', 'Outcome', 'Value', 'capture']
1010

@@ -21,7 +21,7 @@ def capture(sync_fn, *args, **kwargs):
2121
except BaseException as exc:
2222
return Error(exc)
2323

24-
24+
@attr.s(repr=False, init=False, slots=True)
2525
class Outcome(ABC):
2626
"""An abstract class representing the result of a Python computation.
2727
@@ -37,7 +37,13 @@ class Outcome(ABC):
3737
hashable.
3838
3939
"""
40-
__slots__ = ()
40+
_unwrapped = attr.ib(default=False, cmp=False, init=False)
41+
42+
def _set_unwrapped(self):
43+
if self._unwrapped:
44+
raise AlreadyUsedError
45+
object.__setattr__(self, '_unwrapped', True)
46+
4147

4248
@abc.abstractmethod
4349
def unwrap(self):
@@ -62,7 +68,7 @@ def send(self, gen):
6268
"""
6369

6470

65-
@attr.s(frozen=True, repr=False)
71+
@attr.s(frozen=True, repr=False, slots=True)
6672
class Value(Outcome):
6773
"""Concrete :class:`Outcome` subclass representing a regular value.
6874
@@ -75,13 +81,15 @@ def __repr__(self):
7581
return 'Value({!r})'.format(self.value)
7682

7783
def unwrap(self):
84+
self._set_unwrapped()
7885
return self.value
7986

8087
def send(self, gen):
88+
self._set_unwrapped()
8189
return gen.send(self.value)
8290

8391

84-
@attr.s(frozen=True, repr=False)
92+
@attr.s(frozen=True, repr=False, slots=True)
8593
class Error(Outcome):
8694
"""Concrete :class:`Outcome` subclass representing a raised exception.
8795
@@ -94,7 +102,10 @@ def __repr__(self):
94102
return 'Error({!r})'.format(self.error)
95103

96104
def unwrap(self):
105+
self._set_unwrapped()
97106
raise self.error
98107

99108
def send(self, it):
109+
self._set_unwrapped()
100110
return it.throw(self.error)
111+

src/outcome/_util.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
import sys
77

88

9+
class AlreadyUsedError(RuntimeError):
10+
"""An Outcome may not be unwrapped twice."""
11+
pass
12+
13+
914
def fixup_module_metadata(module_name, namespace):
1015
def fix_one(obj):
1116
mod = getattr(obj, "__module__", None)

tests/test_async.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from async_generator import async_generator, yield_
66

77
import outcome
8-
from outcome import Error, Value
8+
from outcome import Error, Value, AlreadyUsedError
99

1010
pytestmark = pytest.mark.trio
1111

@@ -38,8 +38,15 @@ async def my_agen_func():
3838
my_agen = my_agen_func().__aiter__()
3939
if sys.version_info < (3, 5, 2):
4040
my_agen = await my_agen
41+
v = Value("value")
42+
e = Error(KeyError())
4143
assert (await my_agen.asend(None)) == 1
42-
assert (await Value("value").asend(my_agen)) == 2
43-
assert (await Error(KeyError()).asend(my_agen)) == 3
44+
assert (await v.asend(my_agen)) == 2
45+
with pytest.raises(AlreadyUsedError):
46+
await v.asend(my_agen)
47+
48+
assert (await e.asend(my_agen)) == 3
49+
with pytest.raises(AlreadyUsedError):
50+
await e.asend(my_agen)
4451
with pytest.raises(StopAsyncIteration):
4552
await my_agen.asend(None)

tests/test_sync.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66

77
import outcome
8-
from outcome import Error, Value
8+
from outcome import Error, Value, AlreadyUsedError
99

1010

1111
def test_Outcome():
@@ -14,13 +14,21 @@ def test_Outcome():
1414
assert v.unwrap() == 1
1515
assert repr(v) == "Value(1)"
1616

17+
with pytest.raises(AlreadyUsedError):
18+
v.unwrap()
19+
20+
v = Value(1)
21+
1722
exc = RuntimeError("oops")
1823
e = Error(exc)
1924
assert e.error is exc
2025
with pytest.raises(RuntimeError):
2126
e.unwrap()
27+
with pytest.raises(AlreadyUsedError):
28+
e.unwrap()
2229
assert repr(e) == "Error({!r})".format(exc)
2330

31+
e = Error(exc)
2432
with pytest.raises(TypeError):
2533
Error("hello")
2634
with pytest.raises(TypeError):
@@ -33,6 +41,8 @@ def expect_1():
3341
it = iter(expect_1())
3442
next(it)
3543
assert v.send(it) == "ok"
44+
with pytest.raises(AlreadyUsedError):
45+
v.send(it)
3646

3747
def expect_RuntimeError():
3848
with pytest.raises(RuntimeError):
@@ -42,6 +52,8 @@ def expect_RuntimeError():
4252
it = iter(expect_RuntimeError())
4353
next(it)
4454
assert e.send(it) == "ok"
55+
with pytest.raises(AlreadyUsedError):
56+
e.send(it)
4557

4658

4759
def test_Outcome_eq_hash():

0 commit comments

Comments
 (0)