diff -r 8c3e7d519b8b Doc/library/weakref.rst --- a/Doc/library/weakref.rst Sun Oct 11 16:50:57 2009 +0200 +++ b/Doc/library/weakref.rst Tue Oct 13 01:53:00 2009 +0200 @@ -155,7 +155,7 @@ than needed. .. method:: WeakKeyDictionary.keyrefs() - Return an :term:`iterator` that yields the weak references to the keys. + Return an iterable of the weak references to the keys. .. class:: WeakValueDictionary([dict]) @@ -178,7 +178,7 @@ These method have the same issues as the .. method:: WeakValueDictionary.valuerefs() - Return an :term:`iterator` that yields the weak references to the values. + Return an iterable of the weak references to the values. .. class:: WeakSet([elements]) diff -r 8c3e7d519b8b Lib/test/test_weakref.py --- a/Lib/test/test_weakref.py Sun Oct 11 16:50:57 2009 +0200 +++ b/Lib/test/test_weakref.py Tue Oct 13 01:53:00 2009 +0200 @@ -4,6 +4,8 @@ import unittest import collections import weakref import operator +import contextlib +import copy from test import support @@ -777,6 +779,10 @@ class Object: self.arg = arg def __repr__(self): return "" % self.arg + def __eq__(self, other): + if isinstance(other, Object): + return self.arg == other.arg + return NotImplemented def __lt__(self, other): if isinstance(other, Object): return self.arg < other.arg @@ -924,6 +930,87 @@ class MappingTestCase(TestBase): self.assertFalse(values, "itervalues() did not touch all values") + def check_weak_destroy_while_iterating(self, dict, objects, iter_name): + n = len(dict) + it = iter(getattr(dict, iter_name)()) + next(it) # Trigger internal iteration + # Destroy an object + del objects[-1] + gc.collect() # just in case + # We have removed either the first consumed object, or another one + self.assertIn(len(list(it)), [len(objects), len(objects) - 1]) + del it + # The removal has been committed + self.assertEqual(len(dict), n - 1) + + def check_weak_destroy_and_mutate_while_iterating(self, dict, testcontext): + # Check that we can explicitly mutate the weak dict without + # interfering with delayed removal. + # `testcontext` should create an iterator, destroy one of the + # weakref'ed objects and then return a new key/value pair corresponding + # to the destroyed object. + with testcontext() as (k, v): + self.assertFalse(k in dict) + with testcontext() as (k, v): + self.assertRaises(KeyError, dict.__delitem__, k) + self.assertFalse(k in dict) + with testcontext() as (k, v): + self.assertRaises(KeyError, dict.pop, k) + self.assertFalse(k in dict) + with testcontext() as (k, v): + dict[k] = v + self.assertEqual(dict[k], v) + ddict = copy.copy(dict) + with testcontext() as (k, v): + dict.update(ddict) + self.assertEqual(dict, ddict) + with testcontext() as (k, v): + dict.clear() + self.assertEqual(len(dict), 0) + + def test_weak_keys_destroy_while_iterating(self): + # Issue #7105: iterators shouldn't crash when a key is implicitly removed + dict, objects = self.make_weak_keyed_dict() + self.check_weak_destroy_while_iterating(dict, objects, 'keys') + self.check_weak_destroy_while_iterating(dict, objects, 'items') + self.check_weak_destroy_while_iterating(dict, objects, 'values') + self.check_weak_destroy_while_iterating(dict, objects, 'keyrefs') + dict, objects = self.make_weak_keyed_dict() + @contextlib.contextmanager + def testcontext(): + try: + it = iter(dict.items()) + next(it) + # Schedule a key/value for removal and recreate it + v = objects.pop().arg + gc.collect() # just in case + yield Object(v), v + finally: + it = None # should commit all removals + self.check_weak_destroy_and_mutate_while_iterating(dict, testcontext) + + def test_weak_values_destroy_while_iterating(self): + # Issue #7105: iterators shouldn't crash when a key is implicitly removed + dict, objects = self.make_weak_valued_dict() + self.check_weak_destroy_while_iterating(dict, objects, 'keys') + self.check_weak_destroy_while_iterating(dict, objects, 'items') + self.check_weak_destroy_while_iterating(dict, objects, 'values') + self.check_weak_destroy_while_iterating(dict, objects, 'itervaluerefs') + self.check_weak_destroy_while_iterating(dict, objects, 'valuerefs') + dict, objects = self.make_weak_valued_dict() + @contextlib.contextmanager + def testcontext(): + try: + it = iter(dict.items()) + next(it) + # Schedule a key/value for removal and recreate it + k = objects.pop().arg + gc.collect() # just in case + yield k, Object(k) + finally: + it = None # should commit all removals + self.check_weak_destroy_and_mutate_while_iterating(dict, testcontext) + def test_make_weak_keyed_dict_from_dict(self): o = Object(3) dict = weakref.WeakKeyDictionary({o:364}) diff -r 8c3e7d519b8b Lib/weakref.py --- a/Lib/weakref.py Sun Oct 11 16:50:57 2009 +0200 +++ b/Lib/weakref.py Tue Oct 13 01:53:00 2009 +0200 @@ -30,6 +30,31 @@ __all__ = ["ref", "proxy", "getweakrefco "WeakSet"] +class _IterationGuard: + # This context manager registers itself in the current iterators of the + # weak dictionary, such as to delay all removals until the context manager + # exits. + # This technique should be relatively thread-safe (since sets are). + + def __init__(self, weakdict): + # Don't create cycles + self.weakdict = ref(weakdict) + + def __enter__(self): + wd = self.weakdict() + if wd is not None: + wd._iterating.add(self) + return self + + def __exit__(self, e, t, b): + wd = self.weakdict() + if wd is not None: + s = wd._iterating + s.remove(self) + if not s: + wd._commit_removals() + + class WeakValueDictionary(collections.MutableMapping): """Mapping class that references values weakly. @@ -46,11 +71,25 @@ class WeakValueDictionary(collections.Mu def remove(wr, selfref=ref(self)): self = selfref() if self is not None: - del self.data[wr.key] + if self._iterating: + self._pending_removals.append(wr.key) + else: + del self.data[wr.key] self._remove = remove + # A list of keys to be removed + self._pending_removals = [] + self._iterating = set() self.data = d = {} self.update(*args, **kw) + def _commit_removals(self): + l = self._pending_removals + d = self.data + # We shouldn't encounter any KeyError, because this method should + # always be called *before* mutating the dict. + while l: + del d[l.pop()] + def __getitem__(self, key): o = self.data[key]() if o is None: @@ -59,6 +98,8 @@ class WeakValueDictionary(collections.Mu return o def __delitem__(self, key): + if self._pending_removals: + self._commit_removals() del self.data[key] def __len__(self): @@ -75,6 +116,8 @@ class WeakValueDictionary(collections.Mu return "" % id(self) def __setitem__(self, key, value): + if self._pending_removals: + self._commit_removals() self.data[key] = KeyedRef(value, self._remove, key) def copy(self): @@ -110,24 +153,19 @@ class WeakValueDictionary(collections.Mu return o def items(self): - L = [] - for key, wr in self.data.items(): - o = wr() - if o is not None: - L.append((key, o)) - return L - - def items(self): - for wr in self.data.values(): - value = wr() - if value is not None: - yield wr.key, value + with _IterationGuard(self): + for k, wr in self.data.items(): + v = wr() + if v is not None: + yield k, v def keys(self): - return iter(self.data.keys()) + with _IterationGuard(self): + for k, wr in self.data.items(): + if wr() is not None: + yield k - def __iter__(self): - return iter(self.data.keys()) + __iter__ = keys def itervaluerefs(self): """Return an iterator that yields the weak references to the values. @@ -139,15 +177,20 @@ class WeakValueDictionary(collections.Mu keep the values around longer than needed. """ - return self.data.values() + with _IterationGuard(self): + for wr in self.data.values(): + yield wr def values(self): - for wr in self.data.values(): - obj = wr() - if obj is not None: - yield obj + with _IterationGuard(self): + for wr in self.data.values(): + obj = wr() + if obj is not None: + yield obj def popitem(self): + if self._pending_removals: + self._commit_removals() while 1: key, wr = self.data.popitem() o = wr() @@ -155,6 +198,8 @@ class WeakValueDictionary(collections.Mu return key, o def pop(self, key, *args): + if self._pending_removals: + self._commit_removals() try: o = self.data.pop(key)() except KeyError: @@ -170,12 +215,16 @@ class WeakValueDictionary(collections.Mu try: wr = self.data[key] except KeyError: + if self._pending_removals: + self._commit_removals() self.data[key] = KeyedRef(default, self._remove, key) return default else: return wr() def update(self, dict=None, **kwargs): + if self._pending_removals: + self._commit_removals() d = self.data if dict is not None: if not hasattr(dict, "items"): @@ -195,7 +244,7 @@ class WeakValueDictionary(collections.Mu keep the values around longer than needed. """ - return self.data.values() + return list(self.data.values()) class KeyedRef(ref): @@ -235,9 +284,29 @@ class WeakKeyDictionary(collections.Muta def remove(k, selfref=ref(self)): self = selfref() if self is not None: - del self.data[k] + if self._iterating: + self._pending_removals.append(k) + else: + del self.data[k] self._remove = remove - if dict is not None: self.update(dict) + # A list of dead weakrefs (keys to be removed) + self._pending_removals = [] + self._iterating = set() + if dict is not None: + self.update(dict) + + def _commit_removals(self): + # NOTE: We don't need to call this method before mutating the dict, + # because a dead weakref never compares equal to a live weakref, + # even if they happened to refer to equal objects. + # However, it means keys may already have been removed. + l = self._pending_removals + d = self.data + while l: + try: + del d[l.pop()] + except KeyError: + pass def __delitem__(self, key): del self.data[ref(key)] @@ -284,34 +353,26 @@ class WeakKeyDictionary(collections.Muta return wr in self.data def items(self): - for wr, value in self.data.items(): - key = wr() - if key is not None: - yield key, value - - def keyrefs(self): - """Return an iterator that yields the weak references to the keys. - - The references are not guaranteed to be 'live' at the time - they are used, so the result of calling the references needs - to be checked before being used. This can be used to avoid - creating references that will cause the garbage collector to - keep the keys around longer than needed. - - """ - return self.data.keys() + with _IterationGuard(self): + for wr, value in self.data.items(): + key = wr() + if key is not None: + yield key, value def keys(self): - for wr in self.data.keys(): - obj = wr() - if obj is not None: - yield obj + with _IterationGuard(self): + for wr in self.data: + obj = wr() + if obj is not None: + yield obj - def __iter__(self): - return iter(self.keys()) + __iter__ = keys def values(self): - return iter(self.data.values()) + with _IterationGuard(self): + for wr, value in self.data.items(): + if wr() is not None: + yield value def keyrefs(self): """Return a list of weak references to the keys. @@ -323,7 +384,7 @@ class WeakKeyDictionary(collections.Muta keep the keys around longer than needed. """ - return self.data.keys() + return list(self.data) def popitem(self): while 1:

AltStyle によって変換されたページ (->オリジナル) /