@@ -553,10 +553,10 @@ def test_hex_separator_basics(self):
553553 self .assertEqual (three_bytes .hex ('*' , - 2 ), 'b901*ef' )
554554 self .assertEqual (three_bytes .hex (sep = ':' , bytes_per_sep = 2 ), 'b9:01ef' )
555555 self .assertEqual (three_bytes .hex (sep = '*' , bytes_per_sep = - 2 ), 'b901*ef' )
556- for bytes_per_sep in 3 , - 3 , 2 ** 31 - 1 , - ( 2 ** 31 - 1 ) :
556+ for bytes_per_sep in 3 , - 3 , sys . maxsize , - sys . maxsize :
557557 with self .subTest (bytes_per_sep = bytes_per_sep ):
558558 self .assertEqual (three_bytes .hex (':' , bytes_per_sep ), 'b901ef' )
559- for bytes_per_sep in 2 ** 31 , - 2 ** 31 , 2 ** 1000 , - 2 ** 1000 :
559+ for bytes_per_sep in sys . maxsize + 1 , - sys . maxsize - 1 , 2 ** 1000 , - 2 ** 1000 :
560560 with self .subTest (bytes_per_sep = bytes_per_sep ):
561561 try :
562562 self .assertEqual (three_bytes .hex (':' , bytes_per_sep ), 'b901ef' )
@@ -586,6 +586,37 @@ def test_hex_separator_six_bytes(self):
586586 self .assertEqual (six_bytes .hex (':' , - 6 ), '0306090c0f12' )
587587 self .assertEqual (six_bytes .hex (' ' , - 95 ), '0306090c0f12' )
588588
589+ def test_hex_simd_boundaries (self ):
590+ # Test lengths around the SIMD threshold (16 bytes).
591+ # SIMD processes 16 bytes at a time; smaller inputs use scalar code.
592+ for length in (14 , 15 , 16 , 17 , 31 , 32 , 33 , 64 , 65 ):
593+ data = self .type2test (bytes (range (length )))
594+ expected = '' .join (f'{ b :02x} ' for b in range (length ))
595+ with self .subTest (length = length ):
596+ self .assertEqual (data .hex (), expected )
597+
598+ def test_hex_nibble_boundaries (self ):
599+ # Test the nibble value boundary at 9/10 (where '9' becomes 'a').
600+ # SIMD uses signed comparison for efficiency; verify correctness
601+ # at this boundary for various nibble combinations.
602+ boundary_bytes = self .type2test (bytes ([
603+ 0x09 , # both nibbles: 0, 9
604+ 0x0a , # both nibbles: 0, 10
605+ 0x90 , # both nibbles: 9, 0
606+ 0x99 , # both nibbles: 9, 9 (max all-digit)
607+ 0x9a , # both nibbles: 9, 10
608+ 0xa0 , # both nibbles: 10, 0
609+ 0xa9 , # both nibbles: 10, 9
610+ 0xaa , # both nibbles: 10, 10 (min all-letter)
611+ 0x00 , # min value
612+ 0xff , # max value
613+ ]))
614+ self .assertEqual (boundary_bytes .hex (), '090a90999aa0a9aa00ff' )
615+
616+ # Repeat with 16+ bytes to exercise SIMD path
617+ simd_boundary = self .type2test (boundary_bytes * 2 )
618+ self .assertEqual (simd_boundary .hex (), '090a90999aa0a9aa00ff' * 2 )
619+
589620 def test_join (self ):
590621 self .assertEqual (self .type2test (b"" ).join ([]), b"" )
591622 self .assertEqual (self .type2test (b"" ).join ([b"" ]), b"" )
@@ -616,6 +647,32 @@ def test_join(self):
616647 with self .assertRaises (TypeError ):
617648 dot_join ([memoryview (b"ab" ), "cd" , b"ef" ])
618649
650+ def test_join_concurrent_buffer_mutation (self ):
651+ # __buffer__() can release the GIL, letting another thread concurrently
652+ # mutate the joined sequence (simulated here by mutating in __buffer__).
653+ # See: https://github.com/python/cpython/issues/151295
654+ def make_seq (mutate ):
655+ # Item is only referenced from the list slot, so mutate() frees it.
656+ class Item :
657+ def __buffer__ (self , flags ):
658+ mutate (seq )
659+ return memoryview (b'x' )
660+ seq = [b'a' , Item (), b'c' ]
661+ return seq
662+
663+ for sep in (self .type2test (b'' ), self .type2test (b'::' )):
664+ with self .subTest (sep = sep ):
665+ # Changing the list length is reported as a RuntimeError.
666+ seq = make_seq (lambda seq : seq .clear ())
667+ self .assertRaises (RuntimeError , sep .join , seq )
668+
669+ # The list length is unchanged, so the size-change recheck
670+ # cannot fire: only keeping the item alive avoids the crash.
671+ def replace (seq ):
672+ seq [1 ] = b'z'
673+ seq = make_seq (replace )
674+ self .assertEqual (sep .join (seq ), sep .join ([b'a' , b'x' , b'c' ]))
675+
619676 def test_count (self ):
620677 b = self .type2test (b'mississippi' )
621678 i = 105
@@ -794,16 +851,16 @@ def __int__(self):
794851 pi = PseudoFloat (3.1415 )
795852
796853 exceptions_params = [
797- ('%x format: an integer is required , not float' , b'%x' , 3.14 ),
798- ('%X format: an integer is required , not float' , b'%X' , 2.11 ),
799- ('%o format: an integer is required , not float' , b'%o' , 1.79 ),
800- ('%x format: an integer is required , not PseudoFloat' , b'%x' , pi ),
801- ('%x format: an integer is required , not complex' , b'%x' , 3j ),
802- ('%X format: an integer is required , not complex' , b'%X' , 2j ),
803- ('%o format: an integer is required , not complex' , b'%o' , 1j ),
804- ('%u format: a real number is required , not complex' , b'%u' , 3j ),
805- ('%i format: a real number is required , not complex' , b'%i' , 2j ),
806- ('%d format: a real number is required , not complex' , b'%d' , 2j ),
854+ ('%x requires an integer, not float' , b'%x' , 3.14 ),
855+ ('%X requires an integer, not float' , b'%X' , 2.11 ),
856+ ('%o requires an integer, not float' , b'%o' , 1.79 ),
857+ (r '%x requires an integer, not .*\. PseudoFloat' , b'%x' , pi ),
858+ ('%x requires an integer, not complex' , b'%x' , 3j ),
859+ ('%X requires an integer, not complex' , b'%X' , 2j ),
860+ ('%o requires an integer, not complex' , b'%o' , 1j ),
861+ ('%u requires a real number, not complex' , b'%u' , 3j ),
862+ ('%i requires a real number, not complex' , b'%i' , 2j ),
863+ ('%d requires a real number, not complex' , b'%d' , 2j ),
807864 (
808865 r'%c requires an integer in range\(256\)'
809866 r' or a single byte, not .*\.PseudoFloat' ,
@@ -812,7 +869,7 @@ def __int__(self):
812869 ]
813870
814871 for msg , format_bytes , value in exceptions_params :
815- with self .assertRaisesRegex (TypeError , msg ):
872+ with self .assertRaisesRegex (TypeError , 'format argument: ' + msg ):
816873 operator .mod (format_bytes , value )
817874
818875 def test_memory_leak_gh_140939 (self ):
@@ -849,6 +906,13 @@ def test_replace(self):
849906 self .assertEqual (b .replace (b'i' , b'a' ), b'massassappa' )
850907 self .assertEqual (b .replace (b'ss' , b'x' ), b'mixixippi' )
851908
909+ def test_replace_count_keyword (self ):
910+ b = self .type2test (b'aa' )
911+ self .assertEqual (b .replace (b'a' , b'b' , count = 0 ), b'aa' )
912+ self .assertEqual (b .replace (b'a' , b'b' , count = 1 ), b'ba' )
913+ self .assertEqual (b .replace (b'a' , b'b' , count = 2 ), b'bb' )
914+ self .assertEqual (b .replace (b'a' , b'b' , count = 3 ), b'bb' )
915+
852916 def test_replace_int_error (self ):
853917 self .assertRaises (TypeError , self .type2test (b'a b' ).replace , 32 , b'' )
854918
@@ -1337,10 +1401,6 @@ class SubBytes(bytes):
13371401 self .assertNotEqual (id (s ), id (1 * s ))
13381402 self .assertNotEqual (id (s ), id (s * 2 ))
13391403
1340- @unittest .expectedFailure # TODO: RUSTPYTHON
1341- def test_mod (self ):
1342- return super ().test_mod ()
1343-
13441404
13451405class ByteArrayTest (BaseBytesTest , unittest .TestCase ):
13461406 type2test = bytearray
@@ -1431,6 +1491,16 @@ def test_clear(self):
14311491 b .append (ord ('p' ))
14321492 self .assertEqual (b , b'p' )
14331493
1494+ # Cleared object should be empty.
1495+ b = bytearray (b'abc' )
1496+ b .clear ()
1497+ self .assertEqual (b .__alloc__ (), 0 )
1498+ base_size = sys .getsizeof (bytearray ())
1499+ self .assertEqual (sys .getsizeof (b ), base_size )
1500+ c = b .copy ()
1501+ self .assertEqual (c .__alloc__ (), 0 )
1502+ self .assertEqual (sys .getsizeof (c ), base_size )
1503+
14341504 def test_copy (self ):
14351505 b = bytearray (b'abc' )
14361506 bb = b .copy ()
@@ -1493,6 +1563,124 @@ def test_resize(self):
14931563 self .assertRaises (MemoryError , bytearray ().resize , sys .maxsize )
14941564 self .assertRaises (MemoryError , bytearray (1000 ).resize , sys .maxsize )
14951565
1566+ def test_take_bytes (self ):
1567+ ba = bytearray (b'ab' )
1568+ self .assertEqual (ba .take_bytes (), b'ab' )
1569+ self .assertEqual (len (ba ), 0 )
1570+ self .assertEqual (ba , bytearray (b'' ))
1571+ self .assertEqual (ba .__alloc__ (), 0 )
1572+ base_size = sys .getsizeof (bytearray ())
1573+ self .assertEqual (sys .getsizeof (ba ), base_size )
1574+
1575+ # Positive and negative slicing.
1576+ ba = bytearray (b'abcdef' )
1577+ self .assertEqual (ba .take_bytes (1 ), b'a' )
1578+ self .assertEqual (ba , bytearray (b'bcdef' ))
1579+ self .assertEqual (len (ba ), 5 )
1580+ self .assertEqual (ba .take_bytes (- 5 ), b'' )
1581+ self .assertEqual (ba , bytearray (b'bcdef' ))
1582+ self .assertEqual (len (ba ), 5 )
1583+ self .assertEqual (ba .take_bytes (- 3 ), b'bc' )
1584+ self .assertEqual (ba , bytearray (b'def' ))
1585+ self .assertEqual (len (ba ), 3 )
1586+ self .assertEqual (ba .take_bytes (3 ), b'def' )
1587+ self .assertEqual (ba , bytearray (b'' ))
1588+ self .assertEqual (len (ba ), 0 )
1589+
1590+ # Take nothing from emptiness.
1591+ self .assertEqual (ba .take_bytes (0 ), b'' )
1592+ self .assertEqual (ba .take_bytes (), b'' )
1593+ self .assertEqual (ba .take_bytes (None ), b'' )
1594+
1595+ # Out of bounds, bad take value.
1596+ self .assertRaises (IndexError , ba .take_bytes , - 1 )
1597+ self .assertRaises (TypeError , ba .take_bytes , 3.14 )
1598+ ba = bytearray (b'abcdef' )
1599+ self .assertRaises (IndexError , ba .take_bytes , 7 )
1600+
1601+ # Offset between physical and logical start (ob_bytes != ob_start).
1602+ ba = bytearray (b'abcde' )
1603+ del ba [:2 ]
1604+ self .assertEqual (ba , bytearray (b'cde' ))
1605+ self .assertEqual (ba .take_bytes (), b'cde' )
1606+
1607+ # Overallocation at end.
1608+ ba = bytearray (b'abcde' )
1609+ del ba [- 2 :]
1610+ self .assertEqual (ba , bytearray (b'abc' ))
1611+ self .assertEqual (ba .take_bytes (), b'abc' )
1612+ ba = bytearray (b'abcde' )
1613+ ba .resize (4 )
1614+ self .assertEqual (ba .take_bytes (), b'abcd' )
1615+
1616+ # Take of a bytearray with references should fail.
1617+ ba = bytearray (b'abc' )
1618+ with memoryview (ba ) as mv :
1619+ self .assertRaises (BufferError , ba .take_bytes )
1620+ self .assertEqual (ba .take_bytes (), b'abc' )
1621+
1622+ # Leaving one byte must not adopt the shared single-byte bytes object
1623+ # as the buffer.
1624+ ba = bytearray (b'abc' )
1625+ self .assertEqual (ba .take_bytes (2 ), b'ab' )
1626+ ba [0 ] = ord ('A' )
1627+ self .assertEqual (ba , bytearray (b'A' ))
1628+ self .assertEqual (ord (b'c' ), ord ('c' ))
1629+
1630+ @support .cpython_only # tests an implementation detail
1631+ def test_take_bytes_optimization (self ):
1632+ # Validate optimization around taking lots of little chunks out of a
1633+ # much bigger buffer. Save work by only copying a little rather than
1634+ # moving a lot.
1635+ ba = bytearray (b'abcdef' + b'0' * 1000 )
1636+ start_alloc = ba .__alloc__ ()
1637+
1638+ # Take two bytes at a time, checking alloc doesn't change.
1639+ self .assertEqual (ba .take_bytes (2 ), b'ab' )
1640+ self .assertEqual (ba .__alloc__ (), start_alloc )
1641+ self .assertEqual (len (ba ), 4 + 1000 )
1642+ self .assertEqual (ba .take_bytes (2 ), b'cd' )
1643+ self .assertEqual (ba .__alloc__ (), start_alloc )
1644+ self .assertEqual (len (ba ), 2 + 1000 )
1645+ self .assertEqual (ba .take_bytes (2 ), b'ef' )
1646+ self .assertEqual (ba .__alloc__ (), start_alloc )
1647+ self .assertEqual (len (ba ), 0 + 1000 )
1648+ self .assertEqual (ba .__alloc__ (), start_alloc )
1649+
1650+ # Take over half, alloc shrinks to exact size.
1651+ self .assertEqual (ba .take_bytes (501 ), b'0' * 501 )
1652+ self .assertEqual (len (ba ), 499 )
1653+ bytes_header_size = sys .getsizeof (b'' )
1654+ self .assertEqual (ba .__alloc__ (), 499 + bytes_header_size )
1655+
1656+ def test_take_bytes_reentrant_resize (self ):
1657+ # gh-153570: n.__index__() can resize the bytearray, so take_bytes()
1658+ # must re-read the size afterwards. It cached the size before the
1659+ # call and used it for the bounds check and the buffer reads, so a
1660+ # reentrant clear() returned freed memory (a use-after-free read).
1661+ def take (target , resize , n ):
1662+ class Evil :
1663+ def __index__ (self ):
1664+ resize (target )
1665+ return n
1666+ return target .take_bytes (Evil ())
1667+
1668+ # clear() during __index__: nothing is left to take.
1669+ ba = bytearray (b'abcdefgh' )
1670+ with self .assertRaises (IndexError ):
1671+ take (ba , lambda b : b .clear (), 8 )
1672+ self .assertEqual (ba , b'' )
1673+
1674+ # shrink during __index__: n past the new size is out of range.
1675+ ba = bytearray (b'abcdefgh' )
1676+ with self .assertRaises (IndexError ):
1677+ take (ba , lambda b : b .__delitem__ (slice (4 , None )), 8 )
1678+ self .assertEqual (ba , b'abcd' )
1679+
1680+ # grow during __index__: the take runs against the new, larger size.
1681+ ba = bytearray (b'abcd' )
1682+ self .assertEqual (take (ba , lambda b : b .extend (b'efgh' ), 8 ), b'abcdefgh' )
1683+ self .assertEqual (ba , b'' )
14961684
14971685 @unittest .expectedFailure # TODO: RUSTPYTHON
14981686 def test_setitem (self ):
@@ -2073,9 +2261,45 @@ def __len__(self):
20732261
20742262 self .assertRaises (BufferError , ba .hex , S (b':' ))
20752263
2076- @unittest .expectedFailure # TODO: RUSTPYTHON
2077- def test_mod (self ):
2078- return super ().test_mod ()
2264+ def test_no_init_called (self ):
2265+ # A bytearray created without calling bytearray.__init__
2266+ # should not crash the interpreter (see gh-153419).
2267+ def bytearray_new ():
2268+ return bytearray .__new__ (bytearray )
2269+
2270+ bytearray_new ().insert (0 , 1 )
2271+ bytearray_new ().extend (b"x" )
2272+ bytearray_new ().extend ([1 , 2 , 3 ])
2273+ bytearray_new ().resize (4 )
2274+ bytearray_new ().__init__ (5 )
2275+ bytearray_new ().__init__ (b"xyz" )
2276+ bytearray_new ().take_bytes ()
2277+ bytearray_new ().take_bytes (0 )
2278+
2279+ a = bytearray_new ()
2280+ a .append (1 )
2281+
2282+ a = bytearray_new ()
2283+ a += b"x"
2284+
2285+ a = bytearray_new ()
2286+ a [:] = b"xyz"
2287+
2288+ def test_reinit_length (self ):
2289+ # There is a shortcut taken when resizing, where alloc/2 < newsize.
2290+ # In this case, the existing buffer is reused, rather than reset.
2291+ # If this happens when newsize == 0 and alloc == 1, then various
2292+ # code assumptions can be violated. This test should catch those
2293+ # in debug builds. (see gh-153419)
2294+ a = bytearray (1 )
2295+ a .__init__ ()
2296+ self .assertEqual (a , b"" )
2297+
2298+ def test_reinit_with_view (self ):
2299+ a = bytearray ()
2300+ with memoryview (a ):
2301+ self .assertRaises (BufferError , a .__init__ , "x" , "ascii" )
2302+ self .assertEqual (a , b"" )
20792303
20802304
20812305class AssortedBytesTest (unittest .TestCase ):
@@ -2105,7 +2329,7 @@ def test_bytearray_repr(self, f=repr):
21052329 self .assertEqual (f (bytearray ([7 , 8 , 9 , 10 , 11 , 12 , 13 ])),
21062330 r"bytearray(b'\x07\x08\t\n\x0b\x0c\r')" )
21072331 self .assertEqual (f (bytearray (b'"' )), """bytearray(b'"')""" ) # '"'
2108- self .assertEqual (f (bytearray (b"'" )), r '''bytearray(b"\ '")''' ) # "\ '"
2332+ self .assertEqual (f (bytearray (b"'" )), '''bytearray(b"'")''' ) # "'"
21092333 self .assertEqual (f (bytearray (b"'\" " )), r"""bytearray(b'\'"')""" ) # '\'"'
21102334 self .assertEqual (f (bytearray (b"\" '\" " )), r"""bytearray(b'"\'"')""" ) # '"\'"'
21112335 self .assertEqual (f (bytearray (b'\' "\' ' )), r"""bytearray(b'\'"\'')""" ) # '\'"\''
@@ -2698,6 +2922,18 @@ def resize(b, a): # MODIFIES!
26982922 b .wait ()
26992923 a .resize (10 )
27002924
2925+ def take_bytes (b , a ): # MODIFIES!
2926+ b .wait ()
2927+ c = a .take_bytes ()
2928+ assert not c or c [0 ] == 48 # '0'
2929+
2930+ def take_bytes_n (b , a ): # MODIFIES!
2931+ b .wait ()
2932+ try :
2933+ c = a .take_bytes (10 )
2934+ assert c == b'0123456789'
2935+ except IndexError : pass
2936+
27012937 def check (funcs , a = None , * args ):
27022938 if a is None :
27032939 a = bytearray (b'0' * 0x400000 )
@@ -2762,6 +2998,10 @@ def check(funcs, a=None, *args):
27622998
27632999 check ([clear ] + [resize ] * 10 )
27643000
3001+ check ([clear ] + [take_bytes ] * 10 )
3002+ check ([take_bytes_n ] * 10 , bytearray (b'0123456789' * 0x400 ))
3003+ check ([take_bytes_n ] * 10 , bytearray (b'0123456789' * 5 ))
3004+
27653005 check ([clear ] + [contains ] * 10 )
27663006 check ([clear ] + [subscript ] * 10 )
27673007 check ([clear2 ] + [ass_subscript2 ] * 10 , None , bytearray (b'0' * 0x400000 ))
@@ -2834,5 +3074,18 @@ def resize_stress(ba):
28343074 with threading_helper .start_threads (threads ):
28353075 pass
28363076
3077+ @threading_helper .reap_threads
3078+ @threading_helper .requires_working_threading ()
3079+ def test_free_threading_bytearray_resize_other_thread (self ):
3080+ # Shrinking a bytearray whose buffer another thread owns must not
3081+ # adopt the immortal single-byte bytes object a the buffer.
3082+ ba = bytearray (b'abc' )
3083+ thread = threading .Thread (target = ba .resize , args = (1 ,))
3084+ with threading_helper .start_threads ([thread ]):
3085+ pass
3086+ ba [0 ] = ord ('X' )
3087+ self .assertEqual (ba , bytearray (b'X' ))
3088+ self .assertEqual (ord (b'a' ), ord ('a' ))
3089+
28373090if __name__ == "__main__" :
28383091 unittest .main ()
0 commit comments