|
4 | 4 | import collections |
5 | 5 | import weakref |
6 | 6 | import operator |
| 7 | +import contextlib |
| 8 | +import copy |
7 | 9 |
|
8 | 10 | from test import support |
9 | 11 |
|
@@ -788,6 +790,10 @@ def __init__(self, arg): |
788 | 790 | self.arg = arg |
789 | 791 | def __repr__(self): |
790 | 792 | return "<Object %r>" % self.arg |
| 793 | + def __eq__(self, other): |
| 794 | + if isinstance(other, Object): |
| 795 | + return self.arg == other.arg |
| 796 | + return NotImplemented |
791 | 797 | def __lt__(self, other): |
792 | 798 | if isinstance(other, Object): |
793 | 799 | return self.arg < other.arg |
@@ -935,6 +941,87 @@ def check_iters(self, dict): |
935 | 941 | self.assertFalse(values, |
936 | 942 | "itervalues() did not touch all values") |
937 | 943 |
|
| 944 | + def check_weak_destroy_while_iterating(self, dict, objects, iter_name): |
| 945 | + n = len(dict) |
| 946 | + it = iter(getattr(dict, iter_name)()) |
| 947 | + next(it) # Trigger internal iteration |
| 948 | + # Destroy an object |
| 949 | + del objects[-1] |
| 950 | + gc.collect() # just in case |
| 951 | + # We have removed either the first consumed object, or another one |
| 952 | + self.assertIn(len(list(it)), [len(objects), len(objects) - 1]) |
| 953 | + del it |
| 954 | + # The removal has been committed |
| 955 | + self.assertEqual(len(dict), n - 1) |
| 956 | + |
| 957 | + def check_weak_destroy_and_mutate_while_iterating(self, dict, testcontext): |
| 958 | + # Check that we can explicitly mutate the weak dict without |
| 959 | + # interfering with delayed removal. |
| 960 | + # `testcontext` should create an iterator, destroy one of the |
| 961 | + # weakref'ed objects and then return a new key/value pair corresponding |
| 962 | + # to the destroyed object. |
| 963 | + with testcontext() as (k, v): |
| 964 | + self.assertFalse(k in dict) |
| 965 | + with testcontext() as (k, v): |
| 966 | + self.assertRaises(KeyError, dict.__delitem__, k) |
| 967 | + self.assertFalse(k in dict) |
| 968 | + with testcontext() as (k, v): |
| 969 | + self.assertRaises(KeyError, dict.pop, k) |
| 970 | + self.assertFalse(k in dict) |
| 971 | + with testcontext() as (k, v): |
| 972 | + dict[k] = v |
| 973 | + self.assertEqual(dict[k], v) |
| 974 | + ddict = copy.copy(dict) |
| 975 | + with testcontext() as (k, v): |
| 976 | + dict.update(ddict) |
| 977 | + self.assertEqual(dict, ddict) |
| 978 | + with testcontext() as (k, v): |
| 979 | + dict.clear() |
| 980 | + self.assertEqual(len(dict), 0) |
| 981 | + |
| 982 | + def test_weak_keys_destroy_while_iterating(self): |
| 983 | + # Issue #7105: iterators shouldn't crash when a key is implicitly removed |
| 984 | + dict, objects = self.make_weak_keyed_dict() |
| 985 | + self.check_weak_destroy_while_iterating(dict, objects, 'keys') |
| 986 | + self.check_weak_destroy_while_iterating(dict, objects, 'items') |
| 987 | + self.check_weak_destroy_while_iterating(dict, objects, 'values') |
| 988 | + self.check_weak_destroy_while_iterating(dict, objects, 'keyrefs') |
| 989 | + dict, objects = self.make_weak_keyed_dict() |
| 990 | + @contextlib.contextmanager |
| 991 | + def testcontext(): |
| 992 | + try: |
| 993 | + it = iter(dict.items()) |
| 994 | + next(it) |
| 995 | + # Schedule a key/value for removal and recreate it |
| 996 | + v = objects.pop().arg |
| 997 | + gc.collect() # just in case |
| 998 | + yield Object(v), v |
| 999 | + finally: |
| 1000 | + it = None # should commit all removals |
| 1001 | + self.check_weak_destroy_and_mutate_while_iterating(dict, testcontext) |
| 1002 | + |
| 1003 | + def test_weak_values_destroy_while_iterating(self): |
| 1004 | + # Issue #7105: iterators shouldn't crash when a key is implicitly removed |
| 1005 | + dict, objects = self.make_weak_valued_dict() |
| 1006 | + self.check_weak_destroy_while_iterating(dict, objects, 'keys') |
| 1007 | + self.check_weak_destroy_while_iterating(dict, objects, 'items') |
| 1008 | + self.check_weak_destroy_while_iterating(dict, objects, 'values') |
| 1009 | + self.check_weak_destroy_while_iterating(dict, objects, 'itervaluerefs') |
| 1010 | + self.check_weak_destroy_while_iterating(dict, objects, 'valuerefs') |
| 1011 | + dict, objects = self.make_weak_valued_dict() |
| 1012 | + @contextlib.contextmanager |
| 1013 | + def testcontext(): |
| 1014 | + try: |
| 1015 | + it = iter(dict.items()) |
| 1016 | + next(it) |
| 1017 | + # Schedule a key/value for removal and recreate it |
| 1018 | + k = objects.pop().arg |
| 1019 | + gc.collect() # just in case |
| 1020 | + yield k, Object(k) |
| 1021 | + finally: |
| 1022 | + it = None # should commit all removals |
| 1023 | + self.check_weak_destroy_and_mutate_while_iterating(dict, testcontext) |
| 1024 | + |
938 | 1025 | def test_make_weak_keyed_dict_from_dict(self): |
939 | 1026 | o = Object(3) |
940 | 1027 | dict = weakref.WeakKeyDictionary({o:364}) |
|
0 commit comments