gh-133982: Test _pyio.BytesIO in free-threaded tests (gh-136218) · python/cpython@48cb9b6 · GitHub | Latest TMZ Celebrity News & Gossip | Watch TMZ Live
Skip to content

Commit 48cb9b6

Browse files
authored
gh-133982: Test _pyio.BytesIO in free-threaded tests (gh-136218)
1 parent b499105 commit 48cb9b6

File tree

5 files changed

+80
-47
lines changed

5 files changed

+80
-47
lines changed

Doc/library/io.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,9 @@ than raw I/O does.
719719
The optional argument *initial_bytes* is a :term:`bytes-like object` that
720720
contains initial data.
721721

722+
Methods may be used from multiple threads without external locking in
723+
:term:`free threading` builds.
724+
722725
:class:`BytesIO` provides or overrides these methods in addition to those
723726
from :class:`BufferedIOBase` and :class:`IOBase`:
724727

Lib/_pyio.py

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -876,16 +876,28 @@ class BytesIO(BufferedIOBase):
876876
_buffer = None
877877

878878
def __init__(self, initial_bytes=None):
879+
# Use to keep self._buffer and self._pos consistent.
880+
self._lock = Lock()
881+
879882
buf = bytearray()
880883
if initial_bytes is not None:
881884
buf += initial_bytes
882-
self._buffer = buf
883-
self._pos = 0
885+
886+
with self._lock:
887+
self._buffer = buf
888+
self._pos = 0
884889

885890
def __getstate__(self):
886891
if self.closed:
887892
raise ValueError("__getstate__ on closed file")
888-
return self.__dict__.copy()
893+
with self._lock:
894+
state = self.__dict__.copy()
895+
del state['_lock']
896+
return state
897+
898+
def __setstate__(self, state):
899+
self.__dict__.update(state)
900+
self._lock = Lock()
889901

890902
def getvalue(self):
891903
"""Return the bytes value (contents) of the buffer
@@ -918,14 +930,16 @@ def read(self, size=-1):
918930
raise TypeError(f"{size!r} is not an integer")
919931
else:
920932
size = size_index()
921-
if size < 0:
922-
size = len(self._buffer)
923-
if len(self._buffer) <= self._pos:
924-
return b""
925-
newpos = min(len(self._buffer), self._pos + size)
926-
b = self._buffer[self._pos : newpos]
927-
self._pos = newpos
928-
return bytes(b)
933+
934+
with self._lock:
935+
if size < 0:
936+
size = len(self._buffer)
937+
if len(self._buffer) <= self._pos:
938+
return b""
939+
newpos = min(len(self._buffer), self._pos + size)
940+
b = self._buffer[self._pos : newpos]
941+
self._pos = newpos
942+
return bytes(b)
929943

930944
def read1(self, size=-1):
931945
"""This is the same as read.
@@ -941,12 +955,14 @@ def write(self, b):
941955
n = view.nbytes # Size of any bytes-like object
942956
if n == 0:
943957
return 0
944-
pos = self._pos
945-
if pos > len(self._buffer):
946-
# Pad buffer to pos with null bytes.
947-
self._buffer.resize(pos)
948-
self._buffer[pos:pos + n] = b
949-
self._pos += n
958+
959+
with self._lock:
960+
pos = self._pos
961+
if pos > len(self._buffer):
962+
# Pad buffer to pos with null bytes.
963+
self._buffer.resize(pos)
964+
self._buffer[pos:pos + n] = b
965+
self._pos += n
950966
return n
951967

952968
def seek(self, pos, whence=0):
@@ -963,9 +979,11 @@ def seek(self, pos, whence=0):
963979
raise ValueError("negative seek position %r" % (pos,))
964980
self._pos = pos
965981
elif whence == 1:
966-
self._pos = max(0, self._pos + pos)
982+
with self._lock:
983+
self._pos = max(0, self._pos + pos)
967984
elif whence == 2:
968-
self._pos = max(0, len(self._buffer) + pos)
985+
with self._lock:
986+
self._pos = max(0, len(self._buffer) + pos)
969987
else:
970988
raise ValueError("unsupported whence value")
971989
return self._pos
@@ -978,18 +996,20 @@ def tell(self):
978996
def truncate(self, pos=None):
979997
if self.closed:
980998
raise ValueError("truncate on closed file")
981-
if pos is None:
982-
pos = self._pos
983-
else:
984-
try:
985-
pos_index = pos.__index__
986-
except AttributeError:
987-
raise TypeError(f"{pos!r} is not an integer")
999+
1000+
with self._lock:
1001+
if pos is None:
1002+
pos = self._pos
9881003
else:
989-
pos = pos_index()
990-
if pos < 0:
991-
raise ValueError("negative truncate position %r" % (pos,))
992-
del self._buffer[pos:]
1004+
try:
1005+
pos_index = pos.__index__
1006+
except AttributeError:
1007+
raise TypeError(f"{pos!r} is not an integer")
1008+
else:
1009+
pos = pos_index()
1010+
if pos < 0:
1011+
raise ValueError("negative truncate position %r" % (pos,))
1012+
del self._buffer[pos:]
9931013
return pos
9941014

9951015
def readable(self):

Lib/test/test_free_threading/test_io.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
import io
2+
import _pyio as pyio
13
import threading
24
from unittest import TestCase
35
from test.support import threading_helper
46
from random import randint
5-
from io import BytesIO
67
from sys import getsizeof
78

89

9-
class TestBytesIO(TestCase):
10+
class ThreadSafetyMixin:
1011
# Test pretty much everything that can break under free-threading.
1112
# Non-deterministic, but at least one of these things will fail if
1213
# BytesIO object is not free-thread safe.
@@ -90,20 +91,27 @@ def sizeof(barrier, b, *ignore):
9091
barrier.wait()
9192
getsizeof(b)
9293

93-
self.check([write] * 10, BytesIO())
94-
self.check([writelines] * 10, BytesIO())
95-
self.check([write] * 10 + [truncate] * 10, BytesIO())
96-
self.check([truncate] + [read] * 10, BytesIO(b'0\n'*204800))
97-
self.check([truncate] + [read1] * 10, BytesIO(b'0\n'*204800))
98-
self.check([truncate] + [readline] * 10, BytesIO(b'0\n'*20480))
99-
self.check([truncate] + [readlines] * 10, BytesIO(b'0\n'*20480))
100-
self.check([truncate] + [readinto] * 10, BytesIO(b'0\n'*204800), bytearray(b'0\n'*204800))
101-
self.check([close] + [write] * 10, BytesIO())
102-
self.check([truncate] + [getvalue] * 10, BytesIO(b'0\n'*204800))
103-
self.check([truncate] + [getbuffer] * 10, BytesIO(b'0\n'*204800))
104-
self.check([truncate] + [iter] * 10, BytesIO(b'0\n'*20480))
105-
self.check([truncate] + [getstate] * 10, BytesIO(b'0\n'*204800))
106-
self.check([truncate] + [setstate] * 10, BytesIO(b'0\n'*204800), (b'123', 0, None))
107-
self.check([truncate] + [sizeof] * 10, BytesIO(b'0\n'*204800))
94+
self.check([write] * 10, self.ioclass())
95+
self.check([writelines] * 10, self.ioclass())
96+
self.check([write] * 10 + [truncate] * 10, self.ioclass())
97+
self.check([truncate] + [read] * 10, self.ioclass(b'0\n'*204800))
98+
self.check([truncate] + [read1] * 10, self.ioclass(b'0\n'*204800))
99+
self.check([truncate] + [readline] * 10, self.ioclass(b'0\n'*20480))
100+
self.check([truncate] + [readlines] * 10, self.ioclass(b'0\n'*20480))
101+
self.check([truncate] + [readinto] * 10, self.ioclass(b'0\n'*204800), bytearray(b'0\n'*204800))
102+
self.check([close] + [write] * 10, self.ioclass())
103+
self.check([truncate] + [getvalue] * 10, self.ioclass(b'0\n'*204800))
104+
self.check([truncate] + [getbuffer] * 10, self.ioclass(b'0\n'*204800))
105+
self.check([truncate] + [iter] * 10, self.ioclass(b'0\n'*20480))
106+
self.check([truncate] + [getstate] * 10, self.ioclass(b'0\n'*204800))
107+
state = self.ioclass(b'123').__getstate__()
108+
self.check([truncate] + [setstate] * 10, self.ioclass(b'0\n'*204800), state)
109+
self.check([truncate] + [sizeof] * 10, self.ioclass(b'0\n'*204800))
108110

109111
# no tests for seek or tell because they don't break anything
112+
113+
class CBytesIOTest(ThreadSafetyMixin, TestCase):
114+
ioclass = io.BytesIO
115+
116+
class PyBytesIOTest(ThreadSafetyMixin, TestCase):
117+
ioclass = pyio.BytesIO

Lib/test/test_io.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# * test_univnewlines - tests universal newline support
1010
# * test_largefile - tests operations on a file greater than 2**32 bytes
1111
# (only enabled with -ulargefile)
12+
# * test_free_threading/test_io - tests thread safety of io objects
1213

1314
################################################################################
1415
# ATTENTION TEST WRITERS!!!
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update Python implementation of :class:`io.BytesIO` to be thread safe.

0 commit comments

Comments
 (0)

TMZ Celebrity News – Breaking Stories, Videos & Gossip

Looking for the latest TMZ celebrity news? You've come to the right place. From shocking Hollywood scandals to exclusive videos, TMZ delivers it all in real time.

Whether it’s a red carpet slip-up, a viral paparazzi moment, or a legal drama involving your favorite stars, TMZ news is always first to break the story. Stay in the loop with daily updates, insider tips, and jaw-dropping photos.

🎥 Watch TMZ Live

TMZ Live brings you daily celebrity news and interviews straight from the TMZ newsroom. Don’t miss a beat—watch now and see what’s trending in Hollywood.