Commit 3dd4180d authored by Antoine Pitrou's avatar Antoine Pitrou

Issue #14159: Fix the len() of weak containers (WeakSet, WeakKeyDictionary,...

Issue #14159: Fix the len() of weak containers (WeakSet, WeakKeyDictionary, WeakValueDictionary) to return a better approximation when some objects are dead or dying.
Moreover, the implementation is now O(1) rather than O(n).
Thanks to Yury Selivanov for reporting.
parents f45830a0 82c5a2ef
...@@ -63,7 +63,7 @@ class WeakSet: ...@@ -63,7 +63,7 @@ class WeakSet:
yield item yield item
def __len__(self): def __len__(self):
return sum(x() is not None for x in self.data) return len(self.data) - len(self._pending_removals)
def __contains__(self, item): def __contains__(self, item):
try: try:
......
...@@ -812,11 +812,71 @@ class Object: ...@@ -812,11 +812,71 @@ class Object:
def __hash__(self): def __hash__(self):
return hash(self.arg) return hash(self.arg)
class RefCycle:
def __init__(self):
self.cycle = self
class MappingTestCase(TestBase): class MappingTestCase(TestBase):
COUNT = 10 COUNT = 10
def check_len_cycles(self, dict_type, cons):
N = 20
items = [RefCycle() for i in range(N)]
dct = dict_type(cons(o) for o in items)
# Keep an iterator alive
it = dct.items()
try:
next(it)
except StopIteration:
pass
del items
gc.collect()
n1 = len(dct)
del it
gc.collect()
n2 = len(dct)
# one item may be kept alive inside the iterator
self.assertIn(n1, (0, 1))
self.assertEqual(n2, 0)
def test_weak_keyed_len_cycles(self):
self.check_len_cycles(weakref.WeakKeyDictionary, lambda k: (k, 1))
def test_weak_valued_len_cycles(self):
self.check_len_cycles(weakref.WeakValueDictionary, lambda k: (1, k))
def check_len_race(self, dict_type, cons):
# Extended sanity checks for len() in the face of cyclic collection
self.addCleanup(gc.set_threshold, *gc.get_threshold())
for th in range(1, 100):
N = 20
gc.collect(0)
gc.set_threshold(th, th, th)
items = [RefCycle() for i in range(N)]
dct = dict_type(cons(o) for o in items)
del items
# All items will be collected at next garbage collection pass
it = dct.items()
try:
next(it)
except StopIteration:
pass
n1 = len(dct)
del it
n2 = len(dct)
self.assertGreaterEqual(n1, 0)
self.assertLessEqual(n1, N)
self.assertGreaterEqual(n2, 0)
self.assertLessEqual(n2, n1)
def test_weak_keyed_len_race(self):
self.check_len_race(weakref.WeakKeyDictionary, lambda k: (k, 1))
def test_weak_valued_len_race(self):
self.check_len_race(weakref.WeakValueDictionary, lambda k: (1, k))
def test_weak_values(self): def test_weak_values(self):
# #
# This exercises d.copy(), d.items(), d[], del d[], len(d). # This exercises d.copy(), d.items(), d[], del d[], len(d).
......
...@@ -17,6 +17,10 @@ import contextlib ...@@ -17,6 +17,10 @@ import contextlib
class Foo: class Foo:
pass pass
class RefCycle:
def __init__(self):
self.cycle = self
class TestWeakSet(unittest.TestCase): class TestWeakSet(unittest.TestCase):
...@@ -359,6 +363,49 @@ class TestWeakSet(unittest.TestCase): ...@@ -359,6 +363,49 @@ class TestWeakSet(unittest.TestCase):
s.clear() s.clear()
self.assertEqual(len(s), 0) self.assertEqual(len(s), 0)
def test_len_cycles(self):
N = 20
items = [RefCycle() for i in range(N)]
s = WeakSet(items)
del items
it = iter(s)
try:
next(it)
except StopIteration:
pass
gc.collect()
n1 = len(s)
del it
gc.collect()
n2 = len(s)
# one item may be kept alive inside the iterator
self.assertIn(n1, (0, 1))
self.assertEqual(n2, 0)
def test_len_race(self):
# Extended sanity checks for len() in the face of cyclic collection
self.addCleanup(gc.set_threshold, *gc.get_threshold())
for th in range(1, 100):
N = 20
gc.collect(0)
gc.set_threshold(th, th, th)
items = [RefCycle() for i in range(N)]
s = WeakSet(items)
del items
# All items will be collected at next garbage collection pass
it = iter(s)
try:
next(it)
except StopIteration:
pass
n1 = len(s)
del it
n2 = len(s)
self.assertGreaterEqual(n1, 0)
self.assertLessEqual(n1, N)
self.assertGreaterEqual(n2, 0)
self.assertLessEqual(n2, n1)
def test_main(verbose=None): def test_main(verbose=None):
support.run_unittest(TestWeakSet) support.run_unittest(TestWeakSet)
......
...@@ -78,7 +78,7 @@ class WeakValueDictionary(collections.MutableMapping): ...@@ -78,7 +78,7 @@ class WeakValueDictionary(collections.MutableMapping):
del self.data[key] del self.data[key]
def __len__(self): def __len__(self):
return sum(wr() is not None for wr in self.data.values()) return len(self.data) - len(self._pending_removals)
def __contains__(self, key): def __contains__(self, key):
try: try:
...@@ -290,7 +290,7 @@ class WeakKeyDictionary(collections.MutableMapping): ...@@ -290,7 +290,7 @@ class WeakKeyDictionary(collections.MutableMapping):
return self.data[ref(key)] return self.data[ref(key)]
def __len__(self): def __len__(self):
return len(self.data) return len(self.data) - len(self._pending_removals)
def __repr__(self): def __repr__(self):
return "<WeakKeyDictionary at %s>" % id(self) return "<WeakKeyDictionary at %s>" % id(self)
......
...@@ -508,6 +508,11 @@ Core and Builtins ...@@ -508,6 +508,11 @@ Core and Builtins
Library Library
------- -------
- Issue #14159: Fix the len() of weak containers (WeakSet, WeakKeyDictionary,
WeakValueDictionary) to return a better approximation when some objects
are dead or dying. Moreover, the implementation is now O(1) rather than
O(n).
- Issue #13125: Silence spurious test_lib2to3 output when in non-verbose mode. - Issue #13125: Silence spurious test_lib2to3 output when in non-verbose mode.
Patch by Mikhail Novikov. Patch by Mikhail Novikov.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment