Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1bf974d

Browse files
committed
Closes #21173: Fix len() on a WeakKeyDictionary when .clear() was called with an iterator alive.
1 parent bed04a7 commit 1bf974d

3 files changed

Lines changed: 52 additions & 0 deletions

File tree

Lib/test/test_weakref.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,36 @@ def check_weak_destroy_and_mutate_while_iterating(self, dict, testcontext):
12981298
dict.clear()
12991299
self.assertEqual(len(dict), 0)
13001300

1301+
def check_weak_del_and_len_while_iterating(self, dict, testcontext):
1302+
# Check that len() works when both iterating and removing keys
1303+
# explicitly through various means (.pop(), .clear()...), while
1304+
# implicit mutation is deferred because an iterator is alive.
1305+
# (each call to testcontext() should schedule one item for removal
1306+
# for this test to work properly)
1307+
o = Object(123456)
1308+
with testcontext():
1309+
n = len(dict)
1310+
dict.popitem()
1311+
self.assertEqual(len(dict), n - 1)
1312+
dict[o] = o
1313+
self.assertEqual(len(dict), n)
1314+
with testcontext():
1315+
self.assertEqual(len(dict), n - 1)
1316+
dict.pop(next(dict.keys()))
1317+
self.assertEqual(len(dict), n - 2)
1318+
with testcontext():
1319+
self.assertEqual(len(dict), n - 3)
1320+
del dict[next(dict.keys())]
1321+
self.assertEqual(len(dict), n - 4)
1322+
with testcontext():
1323+
self.assertEqual(len(dict), n - 5)
1324+
dict.popitem()
1325+
self.assertEqual(len(dict), n - 6)
1326+
with testcontext():
1327+
dict.clear()
1328+
self.assertEqual(len(dict), 0)
1329+
self.assertEqual(len(dict), 0)
1330+
13011331
def test_weak_keys_destroy_while_iterating(self):
13021332
# Issue #7105: iterators shouldn't crash when a key is implicitly removed
13031333
dict, objects = self.make_weak_keyed_dict()
@@ -1319,6 +1349,10 @@ def testcontext():
13191349
it = None # should commit all removals
13201350
gc.collect()
13211351
self.check_weak_destroy_and_mutate_while_iterating(dict, testcontext)
1352+
# Issue #21173: len() fragile when keys are both implicitly and
1353+
# explicitly removed.
1354+
dict, objects = self.make_weak_keyed_dict()
1355+
self.check_weak_del_and_len_while_iterating(dict, testcontext)
13221356

13231357
def test_weak_values_destroy_while_iterating(self):
13241358
# Issue #7105: iterators shouldn't crash when a key is implicitly removed
@@ -1342,6 +1376,8 @@ def testcontext():
13421376
it = None # should commit all removals
13431377
gc.collect()
13441378
self.check_weak_destroy_and_mutate_while_iterating(dict, testcontext)
1379+
dict, objects = self.make_weak_valued_dict()
1380+
self.check_weak_del_and_len_while_iterating(dict, testcontext)
13451381

13461382
def test_make_weak_keyed_dict_from_dict(self):
13471383
o = Object(3)

Lib/weakref.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ def remove(k, selfref=ref(self)):
322322
# A list of dead weakrefs (keys to be removed)
323323
self._pending_removals = []
324324
self._iterating = set()
325+
self._dirty_len = False
325326
if dict is not None:
326327
self.update(dict)
327328

@@ -338,13 +339,23 @@ def _commit_removals(self):
338339
except KeyError:
339340
pass
340341

342+
def _scrub_removals(self):
343+
d = self.data
344+
self._pending_removals = [k for k in self._pending_removals if k in d]
345+
self._dirty_len = False
346+
341347
def __delitem__(self, key):
348+
self._dirty_len = True
342349
del self.data[ref(key)]
343350

344351
def __getitem__(self, key):
345352
return self.data[ref(key)]
346353

347354
def __len__(self):
355+
if self._dirty_len and self._pending_removals:
356+
# self._pending_removals may still contain keys which were
357+
# explicitly removed, we have to scrub them (see issue #21173).
358+
self._scrub_removals()
348359
return len(self.data) - len(self._pending_removals)
349360

350361
def __repr__(self):
@@ -417,13 +428,15 @@ def keyrefs(self):
417428
return list(self.data)
418429

419430
def popitem(self):
431+
self._dirty_len = True
420432
while True:
421433
key, value = self.data.popitem()
422434
o = key()
423435
if o is not None:
424436
return o, value
425437

426438
def pop(self, key, *args):
439+
self._dirty_len = True
427440
return self.data.pop(ref(key), *args)
428441

429442
def setdefault(self, key, default=None):

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ Core and Builtins
2222
Library
2323
-------
2424

25+
- Issue #21173: Fix len() on a WeakKeyDictionary when .clear() was called
26+
with an iterator alive.
27+
2528
- Issue #11866: Eliminated race condition in the computation of names
2629
for new threads.
2730

0 commit comments

Comments
 (0)