From bbd3cf8f1ef1e91a8d6dac6411e18b4b9084abf5 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 28 Mar 2017 00:56:28 +0200 Subject: 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. --- Lib/unittest/case.py | 52 ++++++++++++++++++++++++------------------ Lib/unittest/test/test_case.py | 13 +++++++++++ Misc/NEWS | 3 +++ 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index b523f73..f4dbc52 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -153,28 +153,32 @@ class _AssertRaisesBaseContext(_BaseTestCaseContext): If args is not empty, call a callable passing positional and keyword arguments. """ - if not _is_subtype(self.expected, self._base_type): - raise TypeError('%s() arg 1 must be %s' % - (name, self._base_type_str)) - if args and args[0] is None: - warnings.warn("callable is None", - DeprecationWarning, 3) - args = () - if not args: - self.msg = kwargs.pop('msg', None) - if kwargs: - warnings.warn('%r is an invalid keyword argument for ' - 'this function' % next(iter(kwargs)), - DeprecationWarning, 3) - return self - - callable_obj, *args = args try: - self.obj_name = callable_obj.__name__ - except AttributeError: - self.obj_name = str(callable_obj) - with self: - callable_obj(*args, **kwargs) + if not _is_subtype(self.expected, self._base_type): + raise TypeError('%s() arg 1 must be %s' % + (name, self._base_type_str)) + if args and args[0] is None: + warnings.warn("callable is None", + DeprecationWarning, 3) + args = () + if not args: + self.msg = kwargs.pop('msg', None) + if kwargs: + warnings.warn('%r is an invalid keyword argument for ' + 'this function' % next(iter(kwargs)), + DeprecationWarning, 3) + return self + + callable_obj, *args = args + try: + self.obj_name = callable_obj.__name__ + except AttributeError: + self.obj_name = str(callable_obj) + with self: + callable_obj(*args, **kwargs) + finally: + # bpo-23890: manually break a reference cycle + self = None class _AssertRaisesContext(_AssertRaisesBaseContext): @@ -725,7 +729,11 @@ class TestCase(object): self.assertEqual(the_exception.error_code, 3) """ context = _AssertRaisesContext(expected_exception, self) - return context.handle('assertRaises', args, kwargs) + try: + return context.handle('assertRaises', args, kwargs) + finally: + # bpo-23890: manually break a reference cycle + context = None def assertWarns(self, expected_warning, *args, **kwargs): """Fail unless a warning of class warnClass is triggered diff --git a/Lib/unittest/test/test_case.py b/Lib/unittest/test/test_case.py index 8f752b8..b849591 100644 --- a/Lib/unittest/test/test_case.py +++ b/Lib/unittest/test/test_case.py @@ -1273,6 +1273,19 @@ test case with self.assertRaises(TypeError): self.assertRaises((ValueError, object)) + def testAssertRaisesRefcount(self): + # bpo-23890: assertRaises() must not keep objects alive longer + # than expected + def func() : + try: + raise ValueError + except ValueError: + raise ValueError + + refcount = sys.getrefcount(func) + self.assertRaises(ValueError, func) + self.assertEqual(refcount, sys.getrefcount(func)) + def testAssertRaisesRegex(self): class ExceptionMock(Exception): pass diff --git a/Misc/NEWS b/Misc/NEWS index c8471b7..260538b 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -291,6 +291,9 @@ Extension Modules Library ------- +- bpo-23890: unittest.TestCase.assertRaises() now manually breaks a reference + cycle to not keep objects alive longer than expected. + - bpo-29901: The zipapp module now supports general path-like objects, not just pathlib.Path. -- cgit v0.12