@@ -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 ())
@@ -466,17 +467,123 @@ class A:
466467
467468 def test_copy_noncompact (self ):
468469 # 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 ).
470+ # Copy uses a slow merging strategy that produces a
471+ # compacted copy when fewer than about 1/64 of the original
472+ # entries remain (to avoid scanning mostly deleted entries ).
472473 # In this test we want to hit the slow/compacting
473474 # branch of dict.copy() and make sure it works OK.
474475 d = {k : k for k in range (1000 )}
475- for k in range (950 ):
476+ for k in range (999 ):
476477 del d [k ]
477478 d2 = d .copy ()
478479 self .assertEqual (d2 , d )
479480
481+ def test_sparse_dict_copy (self ):
482+ for keys in ([f'key{ i } ' for i in range (120 )], list (range (120 ))):
483+ for remaining in (0 , 1 , 10 , 40 , 80 , 119 , 120 ):
484+ with self .subTest (key_type = type (keys [0 ]), remaining = remaining ):
485+ source = {key : object () for key in keys }
486+ for key in keys [:120 - remaining ]:
487+ del source [key ]
488+
489+ expected = list (source .items ())
490+ for result in (source .copy (), dict (source ), {** source }):
491+ self .assertIsNot (result , source )
492+ self .assertEqual (list (result .items ()), expected )
493+ for (_ , value ), (_ , copied ) in zip (
494+ expected , result .items ()):
495+ self .assertIs (copied , value )
496+ result .clear ()
497+ self .assertEqual (list (source .items ()), expected )
498+
499+ def test_sparse_dict_copy_reinserted_keys (self ):
500+ for keys in ([f'key{ i } ' for i in range (120 )], list (range (120 ))):
501+ source = {key : object () for key in keys }
502+ for key in keys [:100 ]:
503+ del source [key ]
504+ for key in keys [:5 ]:
505+ source [key ] = object ()
506+
507+ expected = list (source .items ())
508+ for result in (source .copy (), dict (source ), {** source }):
509+ self .assertEqual (list (result .items ()), expected )
510+
511+ def test_sparse_dict_copy_does_not_rehash (self ):
512+ class Key :
513+ def __init__ (self , value ):
514+ self .value = value
515+ self .hash_calls = 0
516+
517+ def __hash__ (self ):
518+ self .hash_calls += 1
519+ return self .value % 5
520+
521+ def __eq__ (self , other ):
522+ return self is other
523+
524+ keys = [Key (i ) for i in range (90 )]
525+ source = {key : key .value for key in keys }
526+ for key in keys [:70 ]:
527+ del source [key ]
528+ calls = [key .hash_calls for key in keys ]
529+ expected = list (source .items ())
530+ for result in (source .copy (), dict (source ), {** source }):
531+ self .assertEqual ([key .hash_calls for key in keys ], calls )
532+ for (key , value ), (copy_key , copy_value ) in zip (
533+ expected , result .items ()):
534+ self .assertIs (copy_key , key )
535+ self .assertEqual (copy_value , value )
536+
537+ def test_sparse_dict_copy_releases_references (self ):
538+ class Key :
539+ pass
540+
541+ class Value :
542+ pass
543+
544+ key = Key ()
545+ value = Value ()
546+ source = {i : None for i in range (150 )}
547+ source [key ] = value
548+ for i in range (150 ):
549+ del source [i ]
550+
551+ key_ref = weakref .ref (key )
552+ value_ref = weakref .ref (value )
553+ copies = [source .copy (), dict (source ), {** source }]
554+ del source , key , value
555+
556+ self .assertIsNotNone (key_ref ())
557+ self .assertIsNotNone (value_ref ())
558+ copies .clear ()
559+ gc .collect ()
560+ self .assertIsNone (key_ref ())
561+ self .assertIsNone (value_ref ())
562+
563+ def test_sparse_dict_copy_threshold (self ):
564+ for key_type in (str , int ):
565+ for remaining in (1 , 10 , 14 , 15 , 16 , 17 , 30 , 31 , 32 , 33 , 500 ):
566+ with self .subTest (key_type = key_type , remaining = remaining ):
567+ d = {key_type (i ): i for i in range (1000 )}
568+ for i in range (1000 - remaining ):
569+ del d [key_type (i )]
570+ expected = list (d .items ())
571+ for copy in (d .copy (), dict (d ), {** d }):
572+ self .assertEqual (list (copy .items ()), expected )
573+
574+ @support .nomemtest
575+ def test_sparse_dict_copy_no_memory (self ):
576+ import _testcapi
577+
578+ d = dict .fromkeys (range (128 ))
579+ for i in range (96 ):
580+ del d [i ]
581+
582+ for copy in (d .copy , lambda : dict (d ), lambda : {** d }):
583+ with self .subTest (copy = copy ):
584+ with self .assertRaises (MemoryError ):
585+ _testcapi .call_with_nomemory (0 , 0 , copy )
586+
480587 @support .nomemtest
481588 def test_copy_empty_no_memory (self ):
482589 import _testcapi
@@ -1972,6 +2079,19 @@ def keys(self):
19722079 self .assertEqual (type (d2 ), frozendict )
19732080 self .assertEqual (list (d2 .items ()), pairs [::- 1 ])
19742081
2082+ def test_copy_from_sparse_dict (self ):
2083+ for keys in ([f'key{ i } ' for i in range (1000 )], list (range (1000 ))):
2084+ source = {key : key for key in keys }
2085+ for key in keys [:700 ]:
2086+ del source [key ]
2087+
2088+ expected = list (source .items ())
2089+ d = frozendict (source )
2090+ self .assertEqual (list (d .items ()), expected )
2091+ self .assertEqual (list (d .copy ().items ()), expected )
2092+ self .assertEqual (list (dict (d ).items ()), expected )
2093+ self .assertEqual (list (FrozenDict (d ).copy ().items ()), expected )
2094+
19752095 def test_merge (self ):
19762096 # test "a | b" operator
19772097 self .assertEqual (frozendict (x = 1 ) | frozendict (y = 2 ),
0 commit comments