Skip to content

Commit bbd3cf8

Browse files
authored
Fix ref cycles in TestCase.assertRaises() (#193)
bpo-23890: unittest.TestCase.assertRaises() now manually breaks a reference cycle to not keep objects alive longer than expected.
1 parent 6003db7 commit bbd3cf8

File tree

3 files changed

+46
-22
lines changed

3 files changed

+46
-22
lines changed

Lib/unittest/case.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -153,28 +153,32 @@ def handle(self, name, args, kwargs):
153153
If args is not empty, call a callable passing positional and keyword
154154
arguments.
155155
"""
156-
if not _is_subtype(self.expected, self._base_type):
157-
raise TypeError('%s() arg 1 must be %s' %
158-
(name, self._base_type_str))
159-
if args and args[0] is None:
160-
warnings.warn("callable is None",
161-
DeprecationWarning, 3)
162-
args = ()
163-
if not args:
164-
self.msg = kwargs.pop('msg', None)
165-
if kwargs:
166-
warnings.warn('%r is an invalid keyword argument for '
167-
'this function' % next(iter(kwargs)),
168-
DeprecationWarning, 3)
169-
return self
170-
171-
callable_obj, *args = args
172156
try:
173-
self.obj_name = callable_obj.__name__
174-
except AttributeError:
175-
self.obj_name = str(callable_obj)
176-
with self:
177-
callable_obj(*args, **kwargs)
157+
if not _is_subtype(self.expected, self._base_type):
158+
raise TypeError('%s() arg 1 must be %s' %
159+
(name, self._base_type_str))
160+
if args and args[0] is None:
161+
warnings.warn("callable is None",
162+
DeprecationWarning, 3)
163+
args = ()
164+
if not args:
165+
self.msg = kwargs.pop('msg', None)
166+
if kwargs:
167+
warnings.warn('%r is an invalid keyword argument for '
168+
'this function' % next(iter(kwargs)),
169+
DeprecationWarning, 3)
170+
return self
171+
172+
callable_obj, *args = args
173+
try:
174+
self.obj_name = callable_obj.__name__
175+
except AttributeError:
176+
self.obj_name = str(callable_obj)
177+
with self:
178+
callable_obj(*args, **kwargs)
179+
finally:
180+
# bpo-23890: manually break a reference cycle
181+
self = None
178182

179183

180184
class _AssertRaisesContext(_AssertRaisesBaseContext):
@@ -725,7 +729,11 @@ def assertRaises(self, expected_exception, *args, **kwargs):
725729
self.assertEqual(the_exception.error_code, 3)
726730
"""
727731
context = _AssertRaisesContext(expected_exception, self)
728-
return context.handle('assertRaises', args, kwargs)
732+
try:
733+
return context.handle('assertRaises', args, kwargs)
734+
finally:
735+
# bpo-23890: manually break a reference cycle
736+
context = None
729737

730738
def assertWarns(self, expected_warning, *args, **kwargs):
731739
"""Fail unless a warning of class warnClass is triggered

Lib/unittest/test/test_case.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,19 @@ def testAssertRaisesNoExceptionType(self):
12731273
with self.assertRaises(TypeError):
12741274
self.assertRaises((ValueError, object))
12751275

1276+
def testAssertRaisesRefcount(self):
1277+
# bpo-23890: assertRaises() must not keep objects alive longer
1278+
# than expected
1279+
def func() :
1280+
try:
1281+
raise ValueError
1282+
except ValueError:
1283+
raise ValueError
1284+
1285+
refcount = sys.getrefcount(func)
1286+
self.assertRaises(ValueError, func)
1287+
self.assertEqual(refcount, sys.getrefcount(func))
1288+
12761289
def testAssertRaisesRegex(self):
12771290
class ExceptionMock(Exception):
12781291
pass

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,9 @@ Extension Modules
291291
Library
292292
-------
293293

294+
- bpo-23890: unittest.TestCase.assertRaises() now manually breaks a reference
295+
cycle to not keep objects alive longer than expected.
296+
294297
- bpo-29901: The zipapp module now supports general path-like objects, not
295298
just pathlib.Path.
296299

0 commit comments

Comments
 (0)