FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Closes #21173: Fix len() on a WeakKeyDictionary when .clear() was cal… · python/cpython@414e15a · GitHub

Repository navigation

Commit 414e15a

Browse files
committed
Closes #21173: Fix len() on a WeakKeyDictionary when .clear() was called with an iterator alive.
2 parents 861470c + 1bf974d commit 414e15a

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
@@ -1310,6 +1310,36 @@ def check_weak_destroy_and_mutate_while_iterating(self, dict, testcontext):
13101310
dict.clear()
13111311
self.assertEqual(len(dict), 0)
13121312

1313+
def check_weak_del_and_len_while_iterating(self, dict, testcontext):
1314+
# Check that len() works when both iterating and removing keys
1315+
# explicitly through various means (.pop(), .clear()...), while
1316+
# implicit mutation is deferred because an iterator is alive.
1317+
# (each call to testcontext() should schedule one item for removal
1318+
# for this test to work properly)
1319+
o = Object(123456)
1320+
with testcontext():
1321+
n = len(dict)
1322+
dict.popitem()
1323+
self.assertEqual(len(dict), n - 1)
1324+
dict[o] = o
1325+
self.assertEqual(len(dict), n)
1326+
with testcontext():
1327+
self.assertEqual(len(dict), n - 1)
1328+
dict.pop(next(dict.keys()))
1329+
self.assertEqual(len(dict), n - 2)
1330+
with testcontext():
1331+
self.assertEqual(len(dict), n - 3)
1332+
del dict[next(dict.keys())]
1333+
self.assertEqual(len(dict), n - 4)
1334+
with testcontext():
1335+
self.assertEqual(len(dict), n - 5)
1336+
dict.popitem()
1337+
self.assertEqual(len(dict), n - 6)
1338+
with testcontext():
1339+
dict.clear()
1340+
self.assertEqual(len(dict), 0)
1341+
self.assertEqual(len(dict), 0)
1342+
13131343
def test_weak_keys_destroy_while_iterating(self):
13141344
# Issue #7105: iterators shouldn't crash when a key is implicitly removed
13151345
dict, objects = self.make_weak_keyed_dict()
@@ -1331,6 +1361,10 @@ def testcontext():
13311361
it = None # should commit all removals
13321362
gc.collect()
13331363
self.check_weak_destroy_and_mutate_while_iterating(dict, testcontext)
1364+
# Issue #21173: len() fragile when keys are both implicitly and
1365+
# explicitly removed.
1366+
dict, objects = self.make_weak_keyed_dict()
1367+
self.check_weak_del_and_len_while_iterating(dict, testcontext)
13341368

13351369
def test_weak_values_destroy_while_iterating(self):
13361370
# Issue #7105: iterators shouldn't crash when a key is implicitly removed
@@ -1354,6 +1388,8 @@ def testcontext():
13541388
it = None # should commit all removals
13551389
gc.collect()
13561390
self.check_weak_destroy_and_mutate_while_iterating(dict, testcontext)
1391+
dict, objects = self.make_weak_valued_dict()
1392+
self.check_weak_del_and_len_while_iterating(dict, testcontext)
13571393

13581394
def test_make_weak_keyed_dict_from_dict(self):
13591395
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
@@ -166,6 +166,9 @@ Core and Builtins
166166
Library
167167
-------
168168

169+
- Issue #21173: Fix len() on a WeakKeyDictionary when .clear() was called
170+
with an iterator alive.
171+
169172
- Issue #11866: Eliminated race condition in the computation of names
170173
for new threads.
171174

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL