Skip to content

Commit ed213e4

Browse files
committed
Cleaner tracebacks for captured exceptions
1 parent bf6cd53 commit ed213e4

File tree

6 files changed

+53
-2
lines changed

6 files changed

+53
-2
lines changed

newsfragments/21.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
On Python 3, the exception frame generated within :func:`capture` and
2+
:func:`acapture` has been removed from the traceback.

src/outcome/_async.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from ._sync import Error as ErrorBase
44
from ._sync import Outcome as OutcomeBase
55
from ._sync import Value as ValueBase
6+
from ._util import remove_tb_frames
67

78
__all__ = ['Error', 'Outcome', 'Value', 'acapture', 'capture']
89

@@ -18,6 +19,7 @@ def capture(sync_fn, *args, **kwargs):
1819
try:
1920
return Value(sync_fn(*args, **kwargs))
2021
except BaseException as exc:
22+
exc = remove_tb_frames(exc, 1)
2123
return Error(exc)
2224

2325

@@ -31,6 +33,7 @@ async def acapture(async_fn, *args, **kwargs):
3133
try:
3234
return Value(await async_fn(*args, **kwargs))
3335
except BaseException as exc:
36+
exc = remove_tb_frames(exc, 1)
3437
return Error(exc)
3538

3639

src/outcome/_sync.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import attr
77

8-
from ._util import ABC, AlreadyUsedError
8+
from ._util import ABC, AlreadyUsedError, remove_tb_frames
99

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

@@ -20,6 +20,7 @@ def capture(sync_fn, *args, **kwargs):
2020
try:
2121
return Value(sync_fn(*args, **kwargs))
2222
except BaseException as exc:
23+
exc = remove_tb_frames(exc, 1)
2324
return Error(exc)
2425

2526

@@ -104,7 +105,10 @@ def __repr__(self):
104105

105106
def unwrap(self):
106107
self._set_unwrapped()
107-
raise self.error
108+
# Tracebacks show the 'raise' line below out of context, so let's give
109+
# this variable a name that makes sense out of context.
110+
captured_error = self.error
111+
raise captured_error
108112

109113
def send(self, it):
110114
self._set_unwrapped()

src/outcome/_util.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ def fix_one(obj):
2424
fix_one(obj)
2525

2626

27+
def remove_tb_frames(exc, n):
28+
if sys.version_info < (3,):
29+
return exc
30+
tb = exc.__traceback__
31+
for _ in range(n):
32+
tb = tb.tb_next
33+
return exc.with_traceback(tb)
34+
35+
2736
if sys.version_info < (3,):
2837

2938
class ABC(object):

tests/test_async.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
import traceback
23

34
import pytest
45
import trio
@@ -50,3 +51,18 @@ async def my_agen_func():
5051
await e.asend(my_agen)
5152
with pytest.raises(StopAsyncIteration):
5253
await my_agen.asend(None)
54+
55+
56+
async def test_traceback_frame_removal():
57+
async def raise_ValueError(x):
58+
raise ValueError(x)
59+
60+
e = await outcome.acapture(raise_ValueError, 'abc')
61+
try:
62+
e.unwrap()
63+
except Exception as exc:
64+
frames = traceback.extract_tb(exc.__traceback__)
65+
functions = [function for _, _, function, _ in frames]
66+
assert functions[-2:] == ['unwrap', 'raise_ValueError']
67+
else:
68+
pytest.fail('Did not raise')

tests/test_sync.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import absolute_import, division, print_function
33

44
import sys
5+
import traceback
56

67
import pytest
78

@@ -109,3 +110,19 @@ def raise_ValueError(x):
109110
def test_inheritance():
110111
assert issubclass(Value, outcome.Outcome)
111112
assert issubclass(Error, outcome.Outcome)
113+
114+
115+
@pytest.mark.skipif(sys.version_info < (3,), reason="requires python 3")
116+
def test_traceback_frame_removal():
117+
def raise_ValueError(x):
118+
raise ValueError(x)
119+
120+
e = outcome.capture(raise_ValueError, 'abc')
121+
try:
122+
e.unwrap()
123+
except Exception as exc:
124+
frames = traceback.extract_tb(exc.__traceback__)
125+
functions = [function for _, _, function, _ in frames]
126+
assert functions[-2:] == ['unwrap', 'raise_ValueError']
127+
else:
128+
pytest.fail('Did not raise')

0 commit comments

Comments
 (0)