changeset: 105741:bcb1f0698401 branch: 2.7 parent: 105721:34b9fe09a2b7 user: Antoine Pitrou date: Mon Dec 19 11:12:58 2016 +0100 files: Lib/test/test_support.py Lib/test/test_weakref.py Lib/weakref.py Misc/NEWS description: Issue #19542: Fix bugs in WeakValueDictionary.setdefault() and WeakValueDictionary.pop() when a GC collection happens in another thread. Original patch and report by Armin Rigo. diff -r 34b9fe09a2b7 -r bcb1f0698401 Lib/test/test_support.py --- a/Lib/test/test_support.py Sun Dec 18 05:27:49 2016 +0000 +++ b/Lib/test/test_support.py Mon Dec 19 11:12:58 2016 +0100 @@ -1710,3 +1710,13 @@ # The sequence should be deallocated just after the end of iterating gc_collect() test.assertTrue(done[0]) + +@contextlib.contextmanager +def disable_gc(): + have_gc = gc.isenabled() + gc.disable() + try: + yield + finally: + if have_gc: + gc.enable() diff -r 34b9fe09a2b7 -r bcb1f0698401 Lib/test/test_weakref.py --- a/Lib/test/test_weakref.py Sun Dec 18 05:27:49 2016 +0000 +++ b/Lib/test/test_weakref.py Mon Dec 19 11:12:58 2016 +0100 @@ -6,6 +6,7 @@ import operator import contextlib import copy +import time from test import test_support @@ -56,6 +57,32 @@ self.cycle = self +@contextlib.contextmanager +def collect_in_thread(period=0.0001): + """ + Ensure GC collections happen in a different thread, at a high frequency. + """ + threading = test_support.import_module('threading') + please_stop = False + + def collect(): + while not please_stop: + time.sleep(period) + gc.collect() + + with test_support.disable_gc(): + old_interval = sys.getcheckinterval() + sys.setcheckinterval(20) + t = threading.Thread(target=collect) + t.start() + try: + yield + finally: + please_stop = True + t.join() + sys.setcheckinterval(old_interval) + + class TestBase(unittest.TestCase): def setUp(self): @@ -1394,6 +1421,23 @@ self.assertEqual(len(d), 0) self.assertEqual(count, 2) + def test_threaded_weak_valued_setdefault(self): + d = weakref.WeakValueDictionary() + with collect_in_thread(): + for i in range(50000): + x = d.setdefault(10, RefCycle()) + self.assertIsNot(x, None) # we never put None in there! + del x + + def test_threaded_weak_valued_pop(self): + d = weakref.WeakValueDictionary() + with collect_in_thread(): + for i in range(50000): + d[10] = RefCycle() + x = d.pop(10, 10) + self.assertIsNot(x, None) # we never put None in there! + + from test import mapping_tests class WeakValueDictionaryTestCase(mapping_tests.BasicTestMappingProtocol): diff -r 34b9fe09a2b7 -r bcb1f0698401 Lib/weakref.py --- a/Lib/weakref.py Sun Dec 18 05:27:49 2016 +0000 +++ b/Lib/weakref.py Mon Dec 19 11:12:58 2016 +0100 @@ -202,24 +202,27 @@ try: o = self.data.pop(key)() except KeyError: + o = None + if o is None: if args: return args[0] - raise - if o is None: - raise KeyError, key + else: + raise KeyError, key else: return o def setdefault(self, key, default=None): try: - wr = self.data[key] + o = self.data[key]() except KeyError: + o = None + if o is None: if self._pending_removals: self._commit_removals() self.data[key] = KeyedRef(default, self._remove, key) return default else: - return wr() + return o def update(*args, **kwargs): if not args: diff -r 34b9fe09a2b7 -r bcb1f0698401 Misc/NEWS --- a/Misc/NEWS Sun Dec 18 05:27:49 2016 +0000 +++ b/Misc/NEWS Mon Dec 19 11:12:58 2016 +0100 @@ -13,6 +13,10 @@ Library ------- +- Issue #19542: Fix bugs in WeakValueDictionary.setdefault() and + WeakValueDictionary.pop() when a GC collection happens in another + thread. + - Issue #28925: cPickle now correctly propagates errors when unpickle instances of old-style classes.