Skip to content

Commit 85502c5

Browse files
committed
Avoid creating a reference cycle when calling Error.unwrap
This avoids invoking the cycle collector as often; see python-trio/trio#1770 for more details.
1 parent 0d0cfb0 commit 85502c5

File tree

2 files changed

+36
-1
lines changed

2 files changed

+36
-1
lines changed

src/outcome/_impl.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,22 @@ def unwrap(self):
134134
# Tracebacks show the 'raise' line below out of context, so let's give
135135
# this variable a name that makes sense out of context.
136136
captured_error = self.error
137-
raise captured_error
137+
try:
138+
raise captured_error
139+
finally:
140+
# We want to avoid creating a reference cycle here. Python does
141+
# collect cycles just fine, so it wouldn't be the end of the world
142+
# if we did create a cycle, but the cyclic garbage collector adds
143+
# latency to Python programs, and the more cycles you create, the
144+
# more often it runs, so it's nicer to avoid creating them in the
145+
# first place. For more details see:
146+
#
147+
# https://github.com/python-trio/trio/issues/1770
148+
#
149+
# In particuar, by deleting this local variables from the 'unwrap'
150+
# methods frame, we avoid the 'captured_error' object's
151+
# __traceback__ from indirectly referencing 'captured_error'.
152+
del captured_error, self
138153

139154
def send(self, it):
140155
self._set_unwrapped()

tests/test_sync.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
import traceback
23

34
import pytest
@@ -117,3 +118,22 @@ def raise_ValueError(x):
117118
frames = traceback.extract_tb(exc_info.value.__traceback__)
118119
functions = [function for _, _, function, _ in frames]
119120
assert functions[-2:] == ['unwrap', 'raise_ValueError']
121+
122+
123+
def test_Error_unwrap_does_not_create_reference_cycles():
124+
# See comment in Error.unwrap for why reference cycles are tricky
125+
exc = ValueError()
126+
err = Error(exc)
127+
try:
128+
err.unwrap()
129+
except ValueError:
130+
pass
131+
# Top frame in the traceback is the current test function; we don't care
132+
# about it's references
133+
assert exc.__traceback__.tb_frame is sys._getframe()
134+
# The next frame down is the 'unwrap' frame; we want to make sure it
135+
# doesn't reference the exception (or anything else for that matter, just
136+
# to be thorough)
137+
unwrap_frame = exc.__traceback__.tb_next.tb_frame
138+
assert unwrap_frame.f_code.co_name == "unwrap"
139+
assert unwrap_frame.f_locals == {}

0 commit comments

Comments
 (0)