Skip to content

Commit bb7e107

Browse files
committed
add .peek()
1 parent c73c831 commit bb7e107

3 files changed

Lines changed: 51 additions & 1 deletion

File tree

src/outcome/_impl.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,23 @@ class Outcome(abc.ABC, Generic[ValueT]):
122122
hashable.
123123
124124
"""
125+
@abc.abstractmethod
126+
def peek(self) -> ValueT:
127+
"""Return or raise the contained value or exception, without
128+
invalidating the outcome.
129+
130+
These two lines of code are equivalent::
131+
132+
x = fn(*args)
133+
x = outcome.capture(fn, *args).peek()
134+
135+
"""
136+
125137

126138
@abc.abstractmethod
127139
def unwrap(self) -> ValueT:
128-
"""Return or raise the contained value or exception.
140+
"""Return or raise the contained value or exception, and invalidate
141+
the outcome.
129142
130143
These two lines of code are equivalent::
131144
@@ -173,6 +186,9 @@ def __repr__(self) -> str:
173186
except AttributeError:
174187
return 'Value(<AlreadyUsed>)'
175188

189+
def peek(self) -> ValueT:
190+
return self.value
191+
176192
def unwrap(self) -> ValueT:
177193
try:
178194
v = self._value
@@ -226,6 +242,27 @@ def _unwrap_error(self) -> BaseException:
226242
return v
227243
raise AlreadyUsedError
228244

245+
def peek(self) -> NoReturn:
246+
# Tracebacks show the 'raise' line below out of context, so let's give
247+
# this variable a name that makes sense out of context.
248+
captured_error = self.error
249+
try:
250+
raise captured_error
251+
finally:
252+
# We want to avoid creating a reference cycle here. Python does
253+
# collect cycles just fine, so it wouldn't be the end of the world
254+
# if we did create a cycle, but the cyclic garbage collector adds
255+
# latency to Python programs, and the more cycles you create, the
256+
# more often it runs, so it's nicer to avoid creating them in the
257+
# first place. For more details see:
258+
#
259+
# https://github.com/python-trio/trio/issues/1770
260+
#
261+
# In particuar, by deleting this local variables from the 'unwrap'
262+
# methods frame, we avoid the 'captured_error' object's
263+
# __traceback__ from indirectly referencing 'captured_error'.
264+
del captured_error, self
265+
229266
def unwrap(self) -> NoReturn:
230267
# Tracebacks show the 'raise' line below out of context, so let's give
231268
# this variable a name that makes sense out of context.

tests/test_async.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ async def add(x, y):
1616

1717
v = await outcome.acapture(add, 3, y=4)
1818
assert v == Value(7)
19+
assert v.peek() == 7
1920

2021
async def raise_ValueError(x):
2122
await asyncio.sleep(0)
@@ -24,6 +25,10 @@ async def raise_ValueError(x):
2425
e = await outcome.acapture(raise_ValueError, 9)
2526
assert type(e.error) is ValueError
2627
assert e.error.args == (9,)
28+
with pytest.raises(ValueError):
29+
e.peek()
30+
with pytest.raises(ValueError):
31+
e.unwrap()
2732

2833

2934
async def test_asend():

tests/test_sync.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def test_Outcome():
1111
v = Value(1)
1212
assert v.value == 1
1313
assert repr(v) == "Value(1)"
14+
assert v.peek() == 1
1415
assert v.unwrap() == 1
1516
assert repr(v) == "Value(<AlreadyUsed>)"
1617

@@ -23,6 +24,8 @@ def test_Outcome():
2324
e = Error(exc)
2425
assert e.error is exc
2526
assert repr(e) == f"Error({exc!r})"
27+
with pytest.raises(RuntimeError):
28+
e.peek()
2629
with pytest.raises(RuntimeError):
2730
e.unwrap()
2831
with pytest.raises(AlreadyUsedError):
@@ -94,6 +97,7 @@ def add(x, y):
9497

9598
v = outcome.capture(add, 2, y=3)
9699
assert type(v) == Value
100+
assert v.peek() == 5
97101
assert v.unwrap() == 5
98102

99103
def raise_ValueError(x):
@@ -103,6 +107,10 @@ def raise_ValueError(x):
103107
assert type(e) == Error
104108
assert type(e.error) is ValueError
105109
assert e.error.args == ("two",)
110+
with pytest.raises(ValueError):
111+
e.peek()
112+
with pytest.raises(ValueError):
113+
e.unwrap()
106114

107115

108116
def test_inheritance():

0 commit comments

Comments
 (0)