@@ -86,6 +86,7 @@ def test_bool(self):
8686 self .assertIs (bool ({}), False )
8787 self .assertIs (bool ({1 : 2 }), True )
8888
89+
8990 def test_keys (self ):
9091 d = {}
9192 self .assertEqual (set (d .keys ()), set ())
@@ -465,18 +466,120 @@ class A:
465466 self .assertEqual (gc .is_tracked (d ), gc .is_tracked (d2 ))
466467
467468 def test_copy_noncompact (self ):
468- # Dicts don't compact themselves on del/pop operations.
469- # Copy will use a slow merging strategy that produces
470- # a compacted copy when roughly 33% of dict is a non-used
471- # keys-space (to optimize memory footprint).
472- # In this test we want to hit the slow/compacting
473- # branch of dict.copy() and make sure it works OK.
469+ # Deletions leave holes in the entry table. Copying a sparse
470+ # dictionary must preserve its contents and compact the table.
474471 d = {k : k for k in range (1000 )}
475472 for k in range (950 ):
476473 del d [k ]
477474 d2 = d .copy ()
478475 self .assertEqual (d2 , d )
479476
477+ def test_sparse_dict_copy (self ):
478+ for keys in ([f'key{ i } ' for i in range (120 )], list (range (120 ))):
479+ for remaining in (0 , 1 , 10 , 40 , 80 , 119 , 120 ):
480+ with self .subTest (key_type = type (keys [0 ]), remaining = remaining ):
481+ source = {key : object () for key in keys }
482+ for key in keys [:120 - remaining ]:
483+ del source [key ]
484+
485+ expected = list (source .items ())
486+ for result in (source .copy (), dict (source ), {** source }):
487+ self .assertIsNot (result , source )
488+ self .assertEqual (list (result .items ()), expected )
489+ for (_ , value ), (_ , copied ) in zip (
490+ expected , result .items ()):
491+ self .assertIs (copied , value )
492+ result .clear ()
493+ self .assertEqual (list (source .items ()), expected )
494+
495+ def test_sparse_dict_copy_reinserted_keys (self ):
496+ for keys in ([f'key{ i } ' for i in range (120 )], list (range (120 ))):
497+ source = {key : object () for key in keys }
498+ for key in keys [:100 ]:
499+ del source [key ]
500+ for key in keys [:5 ]:
501+ source [key ] = object ()
502+
503+ expected = list (source .items ())
504+ for result in (source .copy (), dict (source ), {** source }):
505+ self .assertEqual (list (result .items ()), expected )
506+
507+ def test_sparse_dict_copy_does_not_rehash (self ):
508+ class Key :
509+ def __init__ (self , value ):
510+ self .value = value
511+ self .hash_calls = 0
512+
513+ def __hash__ (self ):
514+ self .hash_calls += 1
515+ return self .value % 5
516+
517+ def __eq__ (self , other ):
518+ return self is other
519+
520+ keys = [Key (i ) for i in range (90 )]
521+ source = {key : key .value for key in keys }
522+ for key in keys [:70 ]:
523+ del source [key ]
524+ calls = [key .hash_calls for key in keys ]
525+ expected = list (source .items ())
526+ for result in (source .copy (), dict (source ), {** source }):
527+ self .assertEqual ([key .hash_calls for key in keys ], calls )
528+ for (key , value ), (copy_key , copy_value ) in zip (
529+ expected , result .items ()):
530+ self .assertIs (copy_key , key )
531+ self .assertEqual (copy_value , value )
532+
533+ def test_sparse_dict_copy_releases_references (self ):
534+ class Key :
535+ pass
536+
537+ class Value :
538+ pass
539+
540+ key = Key ()
541+ value = Value ()
542+ source = {i : None for i in range (150 )}
543+ source [key ] = value
544+ for i in range (150 ):
545+ del source [i ]
546+
547+ key_ref = weakref .ref (key )
548+ value_ref = weakref .ref (value )
549+ copies = [source .copy (), dict (source ), {** source }]
550+ del source , key , value
551+
552+ self .assertIsNotNone (key_ref ())
553+ self .assertIsNotNone (value_ref ())
554+ copies .clear ()
555+ gc .collect ()
556+ self .assertIsNone (key_ref ())
557+ self .assertIsNone (value_ref ())
558+
559+ def test_sparse_dict_copy_threshold (self ):
560+ for key_type in (str , int ):
561+ for remaining in (1 , 10 , 14 , 15 , 16 , 17 , 30 , 31 , 32 , 33 , 500 ):
562+ with self .subTest (key_type = key_type , remaining = remaining ):
563+ d = {key_type (i ): i for i in range (1000 )}
564+ for i in range (1000 - remaining ):
565+ del d [key_type (i )]
566+ expected = list (d .items ())
567+ for copy in (d .copy (), dict (d ), {** d }):
568+ self .assertEqual (list (copy .items ()), expected )
569+
570+ @support .nomemtest
571+ def test_sparse_dict_copy_no_memory (self ):
572+ import _testcapi
573+
574+ d = dict .fromkeys (range (128 ))
575+ for i in range (96 ):
576+ del d [i ]
577+
578+ for copy in (d .copy , lambda : dict (d ), lambda : {** d }):
579+ with self .subTest (copy = copy ):
580+ with self .assertRaises (MemoryError ):
581+ _testcapi .call_with_nomemory (0 , 0 , copy )
582+
480583 @support .nomemtest
481584 def test_copy_empty_no_memory (self ):
482585 import _testcapi
@@ -1958,6 +2061,19 @@ def test_copy(self):
19582061 self .assertEqual (d2 , frozendict (x = 1 , y = 2 ))
19592062 self .assertEqual (type (d2 ), frozendict )
19602063
2064+ def test_copy_from_sparse_dict (self ):
2065+ for keys in ([f'key{ i } ' for i in range (1000 )], list (range (1000 ))):
2066+ source = {key : key for key in keys }
2067+ for key in keys [:700 ]:
2068+ del source [key ]
2069+
2070+ expected = list (source .items ())
2071+ d = frozendict (source )
2072+ self .assertEqual (list (d .items ()), expected )
2073+ self .assertEqual (list (d .copy ().items ()), expected )
2074+ self .assertEqual (list (dict (d ).items ()), expected )
2075+ self .assertEqual (list (FrozenDict (d ).copy ().items ()), expected )
2076+
19612077 def test_merge (self ):
19622078 # test "a | b" operator
19632079 self .assertEqual (frozendict (x = 1 ) | frozendict (y = 2 ),
0 commit comments