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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy