]> granicus.if.org Git - python/commitdiff
Issue #14159: Fix the len() of weak sets to return a better approximation when some...
authorAntoine Pitrou <solipsis@pitrou.net>
Thu, 1 Mar 2012 15:26:35 +0000 (16:26 +0100)
committerAntoine Pitrou <solipsis@pitrou.net>
Thu, 1 Mar 2012 15:26:35 +0000 (16:26 +0100)
Moreover, the implementation is now O(1) rather than O(n).
Thanks to Yury Selivanov for reporting.

Lib/_weakrefset.py
Lib/test/test_weakref.py
Lib/test/test_weakset.py
Misc/NEWS

index ffa5e315786dcf77670f920e68914c3c64e7fd12..91077c5b95092b00ab9ea460c344436eb2341ac3 100644 (file)
@@ -63,7 +63,7 @@ class WeakSet(object):
                     yield item
 
     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):
         try:
index bc2982fadbcdbe89be8f4a96767a307fc3d9be8c..f2e328b9eb960f523a1f1c0d22775294eb77eb78 100644 (file)
@@ -815,11 +815,71 @@ class Object:
     def __repr__(self):
         return "<Object %r>" % self.arg
 
+class RefCycle:
+    def __init__(self):
+        self.cycle = self
+
 
 class MappingTestCase(TestBase):
 
     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.iteritems()
+        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.iteritems()
+            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):
         #
         #  This exercises d.copy(), d.items(), d[], del d[], len(d).
index 89c2822b6ee2b5801e3300eb642ca539ac37bed3..f981bddb19337bfc15100be1b93ea57d8d10c493 100644 (file)
@@ -30,6 +30,10 @@ class SomeClass(object):
     def __hash__(self):
         return hash((SomeClass, self.value))
 
+class RefCycle(object):
+    def __init__(self):
+        self.cycle = self
+
 class TestWeakSet(unittest.TestCase):
 
     def setUp(self):
@@ -369,6 +373,49 @@ class TestWeakSet(unittest.TestCase):
             s.clear()
         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):
     test_support.run_unittest(TestWeakSet)
index 8460cdda718f63c99e342108622239f83f62b5d8..6b3fbc2ab4568131309e9eeeb6d5dbe0296d05d4 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -101,6 +101,10 @@ Core and Builtins
 Library
 -------
 
+- Issue #14159: Fix the len() of weak sets to return a better approximation
+  when some objects are dead or dying.  Moreover, the implementation is now
+  O(1) rather than O(n).
+
 - Issue #2945: Make the distutils upload command aware of bdist_rpm products.
 
 - Issue #13447: Add a test file to host regression tests for bugs in the