Skip to content

Commit

Permalink
Merge pull request #29 from njsmith/no-refcycle-in-unwrap
Browse files Browse the repository at this point in the history
Avoid creating a reference cycle when calling Error.unwrap
  • Loading branch information
njsmith committed Nov 16, 2020
2 parents 0d0cfb0 + 5a3b4e9 commit 4b917c4
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 1 deletion.
17 changes: 16 additions & 1 deletion src/outcome/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,22 @@ def unwrap(self):
# Tracebacks show the 'raise' line below out of context, so let's give
# this variable a name that makes sense out of context.
captured_error = self.error
raise captured_error
try:
raise captured_error
finally:
# We want to avoid creating a reference cycle here. Python does
# collect cycles just fine, so it wouldn't be the end of the world
# if we did create a cycle, but the cyclic garbage collector adds
# latency to Python programs, and the more cycles you create, the
# more often it runs, so it's nicer to avoid creating them in the
# first place. For more details see:
#
# https://github.com/python-trio/trio/issues/1770
#
# In particuar, by deleting this local variables from the 'unwrap'
# methods frame, we avoid the 'captured_error' object's
# __traceback__ from indirectly referencing 'captured_error'.
del captured_error, self

def send(self, it):
self._set_unwrapped()
Expand Down
20 changes: 20 additions & 0 deletions tests/test_sync.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
import traceback

import pytest
Expand Down Expand Up @@ -117,3 +118,22 @@ def raise_ValueError(x):
frames = traceback.extract_tb(exc_info.value.__traceback__)
functions = [function for _, _, function, _ in frames]
assert functions[-2:] == ['unwrap', 'raise_ValueError']


def test_Error_unwrap_does_not_create_reference_cycles():
# See comment in Error.unwrap for why reference cycles are tricky
exc = ValueError()
err = Error(exc)
try:
err.unwrap()
except ValueError:
pass
# Top frame in the traceback is the current test function; we don't care
# about its references
assert exc.__traceback__.tb_frame is sys._getframe()
# The next frame down is the 'unwrap' frame; we want to make sure it
# doesn't reference the exception (or anything else for that matter, just
# to be thorough)
unwrap_frame = exc.__traceback__.tb_next.tb_frame
assert unwrap_frame.f_code.co_name == "unwrap"
assert unwrap_frame.f_locals == {}

0 comments on commit 4b917c4

Please sign in to comment.