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