22import contextlib
33import copy
44import gc
5+ import operator
56import pickle
7+ import re
68from random import randrange , shuffle
79import struct
810import sys
@@ -739,11 +741,44 @@ def test_ordered_dict_items_result_gc(self):
739741 # when it's mutated and returned from __next__:
740742 self .assertTrue (gc .is_tracked (next (it )))
741743
744+
745+ class _TriggerSideEffectOnEqual :
746+ count = 0 # number of calls to __eq__
747+ trigger = 1 # count value when to trigger side effect
748+
749+ def __eq__ (self , other ):
750+ if self .__class__ .count == self .__class__ .trigger :
751+ self .side_effect ()
752+ self .__class__ .count += 1
753+ return True
754+
755+ def __hash__ (self ):
756+ # all instances represent the same key
757+ return - 1
758+
759+ def side_effect (self ):
760+ raise NotImplementedError
761+
742762class PurePythonOrderedDictTests (OrderedDictTests , unittest .TestCase ):
743763
744764 module = py_coll
745765 OrderedDict = py_coll .OrderedDict
746766
767+ def test_issue119004_attribute_error (self ):
768+ class Key (_TriggerSideEffectOnEqual ):
769+ def side_effect (self ):
770+ del dict1 [TODEL ]
771+
772+ TODEL = Key ()
773+ dict1 = self .OrderedDict (dict .fromkeys ((0 , TODEL , 4.2 )))
774+ dict2 = self .OrderedDict (dict .fromkeys ((0 , Key (), 4.2 )))
775+ # This causes an AttributeError due to the linked list being changed
776+ msg = re .escape ("'NoneType' object has no attribute 'key'" )
777+ self .assertRaisesRegex (AttributeError , msg , operator .eq , dict1 , dict2 )
778+ self .assertEqual (Key .count , 2 )
779+ self .assertDictEqual (dict1 , dict .fromkeys ((0 , 4.2 )))
780+ self .assertDictEqual (dict2 , dict .fromkeys ((0 , Key (), 4.2 )))
781+
747782
748783class CPythonBuiltinDictTests (unittest .TestCase ):
749784 """Builtin dict preserves insertion order.
@@ -764,8 +799,85 @@ class CPythonBuiltinDictTests(unittest.TestCase):
764799del method
765800
766801
802+ class CPythonOrderedDictSideEffects :
803+
804+ def check_runtime_error_issue119004 (self , dict1 , dict2 ):
805+ msg = re .escape ("OrderedDict mutated during iteration" )
806+ self .assertRaisesRegex (RuntimeError , msg , operator .eq , dict1 , dict2 )
807+
808+ def test_issue119004_change_size_by_clear (self ):
809+ class Key (_TriggerSideEffectOnEqual ):
810+ def side_effect (self ):
811+ dict1 .clear ()
812+
813+ dict1 = self .OrderedDict (dict .fromkeys ((0 , Key (), 4.2 )))
814+ dict2 = self .OrderedDict (dict .fromkeys ((0 , Key (), 4.2 )))
815+ self .check_runtime_error_issue119004 (dict1 , dict2 )
816+ self .assertEqual (Key .count , 2 )
817+ self .assertDictEqual (dict1 , {})
818+ self .assertDictEqual (dict2 , dict .fromkeys ((0 , Key (), 4.2 )))
819+
820+ def test_issue119004_change_size_by_delete_key (self ):
821+ class Key (_TriggerSideEffectOnEqual ):
822+ def side_effect (self ):
823+ del dict1 [TODEL ]
824+
825+ TODEL = Key ()
826+ dict1 = self .OrderedDict (dict .fromkeys ((0 , TODEL , 4.2 )))
827+ dict2 = self .OrderedDict (dict .fromkeys ((0 , Key (), 4.2 )))
828+ self .check_runtime_error_issue119004 (dict1 , dict2 )
829+ self .assertEqual (Key .count , 2 )
830+ self .assertDictEqual (dict1 , dict .fromkeys ((0 , 4.2 )))
831+ self .assertDictEqual (dict2 , dict .fromkeys ((0 , Key (), 4.2 )))
832+
833+ def test_issue119004_change_linked_list_by_clear (self ):
834+ class Key (_TriggerSideEffectOnEqual ):
835+ def side_effect (self ):
836+ dict1 .clear ()
837+ dict1 ['a' ] = dict1 ['b' ] = 'c'
838+
839+ dict1 = self .OrderedDict (dict .fromkeys ((0 , Key (), 4.2 )))
840+ dict2 = self .OrderedDict (dict .fromkeys ((0 , Key (), 4.2 )))
841+ self .check_runtime_error_issue119004 (dict1 , dict2 )
842+ self .assertEqual (Key .count , 2 )
843+ self .assertDictEqual (dict1 , dict .fromkeys (('a' , 'b' ), 'c' ))
844+ self .assertDictEqual (dict2 , dict .fromkeys ((0 , Key (), 4.2 )))
845+
846+ def test_issue119004_change_linked_list_by_delete_key (self ):
847+ class Key (_TriggerSideEffectOnEqual ):
848+ def side_effect (self ):
849+ del dict1 [TODEL ]
850+ dict1 ['a' ] = 'c'
851+
852+ TODEL = Key ()
853+ dict1 = self .OrderedDict (dict .fromkeys ((0 , TODEL , 4.2 )))
854+ dict2 = self .OrderedDict (dict .fromkeys ((0 , Key (), 4.2 )))
855+ self .check_runtime_error_issue119004 (dict1 , dict2 )
856+ self .assertEqual (Key .count , 2 )
857+ self .assertDictEqual (dict1 , {0 : None , 'a' : 'c' , 4.2 : None })
858+ self .assertDictEqual (dict2 , dict .fromkeys ((0 , Key (), 4.2 )))
859+
860+ def test_issue119004_change_size_by_delete_key_in_dict_eq (self ):
861+ class Key (_TriggerSideEffectOnEqual ):
862+ trigger = 0
863+ def side_effect (self ):
864+ del dict1 [TODEL ]
865+
866+ TODEL = Key ()
867+ dict1 = self .OrderedDict (dict .fromkeys ((0 , TODEL , 4.2 )))
868+ dict2 = self .OrderedDict (dict .fromkeys ((0 , Key (), 4.2 )))
869+ self .assertEqual (Key .count , 0 )
870+ # the side effect is in dict.__eq__ and modifies the length
871+ self .assertNotEqual (dict1 , dict2 )
872+ self .assertEqual (Key .count , 2 )
873+ self .assertDictEqual (dict1 , dict .fromkeys ((0 , 4.2 )))
874+ self .assertDictEqual (dict2 , dict .fromkeys ((0 , Key (), 4.2 )))
875+
876+
767877@unittest .skipUnless (c_coll , 'requires the C version of the collections module' )
768- class CPythonOrderedDictTests (OrderedDictTests , unittest .TestCase ):
878+ class CPythonOrderedDictTests (OrderedDictTests ,
879+ CPythonOrderedDictSideEffects ,
880+ unittest .TestCase ):
769881
770882 module = c_coll
771883 OrderedDict = c_coll .OrderedDict
0 commit comments