Skip to content

Commit 222b284

Browse files
committed
Issue #7105: weak dict iterators are fragile because of unpredictable GC runs
Backport the fix from pyton 3.x for this issue.
1 parent c289fa7 commit 222b284

File tree

4 files changed

+235
-35
lines changed

4 files changed

+235
-35
lines changed

Doc/library/weakref.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ than needed.
163163

164164
.. method:: WeakKeyDictionary.iterkeyrefs()
165165

166-
Return an :term:`iterator` that yields the weak references to the keys.
166+
Return an iterable of the weak references to the keys.
167167

168168
.. versionadded:: 2.5
169169

@@ -195,7 +195,7 @@ methods of :class:`WeakKeyDictionary` objects.
195195

196196
.. method:: WeakValueDictionary.itervaluerefs()
197197

198-
Return an :term:`iterator` that yields the weak references to the values.
198+
Return an iterable of the weak references to the values.
199199

200200
.. versionadded:: 2.5
201201

Lib/test/test_weakref.py

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import UserList
55
import weakref
66
import operator
7+
import contextlib
8+
import copy
79

810
from test import test_support
911

@@ -903,7 +905,7 @@ class MappingTestCase(TestBase):
903905
def check_len_cycles(self, dict_type, cons):
904906
N = 20
905907
items = [RefCycle() for i in range(N)]
906-
dct = dict_type(cons(o) for o in items)
908+
dct = dict_type(cons(i, o) for i, o in enumerate(items))
907909
# Keep an iterator alive
908910
it = dct.iteritems()
909911
try:
@@ -913,18 +915,23 @@ def check_len_cycles(self, dict_type, cons):
913915
del items
914916
gc.collect()
915917
n1 = len(dct)
918+
list(it)
916919
del it
917920
gc.collect()
918921
n2 = len(dct)
919-
# one item may be kept alive inside the iterator
920-
self.assertIn(n1, (0, 1))
922+
# iteration should prevent garbage collection here
923+
# Note that this is a test on an implementation detail. The requirement
924+
# is only to provide stable iteration, not that the size of the container
925+
# stay fixed.
926+
self.assertEqual(n1, 20)
927+
#self.assertIn(n1, (0, 1))
921928
self.assertEqual(n2, 0)
922929

923930
def test_weak_keyed_len_cycles(self):
924-
self.check_len_cycles(weakref.WeakKeyDictionary, lambda k: (k, 1))
931+
self.check_len_cycles(weakref.WeakKeyDictionary, lambda n, k: (k, n))
925932

926933
def test_weak_valued_len_cycles(self):
927-
self.check_len_cycles(weakref.WeakValueDictionary, lambda k: (1, k))
934+
self.check_len_cycles(weakref.WeakValueDictionary, lambda n, k: (n, k))
928935

929936
def check_len_race(self, dict_type, cons):
930937
# Extended sanity checks for len() in the face of cyclic collection
@@ -1090,6 +1097,86 @@ def check_iters(self, dict):
10901097
self.assertEqual(len(values), 0,
10911098
"itervalues() did not touch all values")
10921099

1100+
def check_weak_destroy_while_iterating(self, dict, objects, iter_name):
1101+
n = len(dict)
1102+
it = iter(getattr(dict, iter_name)())
1103+
next(it) # Trigger internal iteration
1104+
# Destroy an object
1105+
del objects[-1]
1106+
gc.collect() # just in case
1107+
# We have removed either the first consumed object, or another one
1108+
self.assertIn(len(list(it)), [len(objects), len(objects) - 1])
1109+
del it
1110+
# The removal has been committed
1111+
self.assertEqual(len(dict), n - 1)
1112+
1113+
def check_weak_destroy_and_mutate_while_iterating(self, dict, testcontext):
1114+
# Check that we can explicitly mutate the weak dict without
1115+
# interfering with delayed removal.
1116+
# `testcontext` should create an iterator, destroy one of the
1117+
# weakref'ed objects and then return a new key/value pair corresponding
1118+
# to the destroyed object.
1119+
with testcontext() as (k, v):
1120+
self.assertFalse(k in dict)
1121+
with testcontext() as (k, v):
1122+
self.assertRaises(KeyError, dict.__delitem__, k)
1123+
self.assertFalse(k in dict)
1124+
with testcontext() as (k, v):
1125+
self.assertRaises(KeyError, dict.pop, k)
1126+
self.assertFalse(k in dict)
1127+
with testcontext() as (k, v):
1128+
dict[k] = v
1129+
self.assertEqual(dict[k], v)
1130+
ddict = copy.copy(dict)
1131+
with testcontext() as (k, v):
1132+
dict.update(ddict)
1133+
self.assertEqual(dict, ddict)
1134+
with testcontext() as (k, v):
1135+
dict.clear()
1136+
self.assertEqual(len(dict), 0)
1137+
1138+
def test_weak_keys_destroy_while_iterating(self):
1139+
# Issue #7105: iterators shouldn't crash when a key is implicitly removed
1140+
dict, objects = self.make_weak_keyed_dict()
1141+
self.check_weak_destroy_while_iterating(dict, objects, 'iterkeys')
1142+
self.check_weak_destroy_while_iterating(dict, objects, 'iteritems')
1143+
self.check_weak_destroy_while_iterating(dict, objects, 'itervalues')
1144+
self.check_weak_destroy_while_iterating(dict, objects, 'iterkeyrefs')
1145+
dict, objects = self.make_weak_keyed_dict()
1146+
@contextlib.contextmanager
1147+
def testcontext():
1148+
try:
1149+
it = iter(dict.iteritems())
1150+
next(it)
1151+
# Schedule a key/value for removal and recreate it
1152+
v = objects.pop().arg
1153+
gc.collect() # just in case
1154+
yield Object(v), v
1155+
finally:
1156+
it = None # should commit all removals
1157+
self.check_weak_destroy_and_mutate_while_iterating(dict, testcontext)
1158+
1159+
def test_weak_values_destroy_while_iterating(self):
1160+
# Issue #7105: iterators shouldn't crash when a key is implicitly removed
1161+
dict, objects = self.make_weak_valued_dict()
1162+
self.check_weak_destroy_while_iterating(dict, objects, 'iterkeys')
1163+
self.check_weak_destroy_while_iterating(dict, objects, 'iteritems')
1164+
self.check_weak_destroy_while_iterating(dict, objects, 'itervalues')
1165+
self.check_weak_destroy_while_iterating(dict, objects, 'itervaluerefs')
1166+
dict, objects = self.make_weak_valued_dict()
1167+
@contextlib.contextmanager
1168+
def testcontext():
1169+
try:
1170+
it = iter(dict.iteritems())
1171+
next(it)
1172+
# Schedule a key/value for removal and recreate it
1173+
k = objects.pop().arg
1174+
gc.collect() # just in case
1175+
yield k, Object(k)
1176+
finally:
1177+
it = None # should commit all removals
1178+
self.check_weak_destroy_and_mutate_while_iterating(dict, testcontext)
1179+
10931180
def test_make_weak_keyed_dict_from_dict(self):
10941181
o = Object(3)
10951182
dict = weakref.WeakKeyDictionary({o:364})

Lib/test/test_weakset.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import collections
1212
import gc
1313
import contextlib
14+
from UserString import UserString as ustr
1415

1516

1617
class Foo:
@@ -448,6 +449,54 @@ def test_len_race(self):
448449
self.assertGreaterEqual(n2, 0)
449450
self.assertLessEqual(n2, n1)
450451

452+
def test_weak_destroy_while_iterating(self):
453+
# Issue #7105: iterators shouldn't crash when a key is implicitly removed
454+
# Create new items to be sure no-one else holds a reference
455+
items = [ustr(c) for c in ('a', 'b', 'c')]
456+
s = WeakSet(items)
457+
it = iter(s)
458+
next(it) # Trigger internal iteration
459+
# Destroy an item
460+
del items[-1]
461+
gc.collect() # just in case
462+
# We have removed either the first consumed items, or another one
463+
self.assertIn(len(list(it)), [len(items), len(items) - 1])
464+
del it
465+
# The removal has been committed
466+
self.assertEqual(len(s), len(items))
467+
468+
def test_weak_destroy_and_mutate_while_iterating(self):
469+
# Issue #7105: iterators shouldn't crash when a key is implicitly removed
470+
items = [ustr(c) for c in string.ascii_letters]
471+
s = WeakSet(items)
472+
@contextlib.contextmanager
473+
def testcontext():
474+
try:
475+
it = iter(s)
476+
next(it)
477+
# Schedule an item for removal and recreate it
478+
u = ustr(str(items.pop()))
479+
gc.collect() # just in case
480+
yield u
481+
finally:
482+
it = None # should commit all removals
483+
484+
with testcontext() as u:
485+
self.assertFalse(u in s)
486+
with testcontext() as u:
487+
self.assertRaises(KeyError, s.remove, u)
488+
self.assertFalse(u in s)
489+
with testcontext() as u:
490+
s.add(u)
491+
self.assertTrue(u in s)
492+
t = s.copy()
493+
with testcontext() as u:
494+
s.update(t)
495+
self.assertEqual(len(s), len(t))
496+
with testcontext() as u:
497+
s.clear()
498+
self.assertEqual(len(s), 0)
499+
451500

452501
def test_main(verbose=None):
453502
test_support.run_unittest(TestWeakSet)

0 commit comments

Comments
 (0)