44import UserList
55import weakref
66import operator
7+ import contextlib
8+ import copy
79
810from 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 })
0 commit comments