mirror of
https://github.com/python/cpython.git
synced 2025-07-07 19:35:27 +00:00
gh-133982: Test _pyio.BytesIO in free-threaded tests (gh-136218)
This commit is contained in:
parent
b4991056f4
commit
48cb9b6112
5 changed files with 80 additions and 47 deletions
|
@ -719,6 +719,9 @@ than raw I/O does.
|
||||||
The optional argument *initial_bytes* is a :term:`bytes-like object` that
|
The optional argument *initial_bytes* is a :term:`bytes-like object` that
|
||||||
contains initial data.
|
contains initial data.
|
||||||
|
|
||||||
|
Methods may be used from multiple threads without external locking in
|
||||||
|
:term:`free threading` builds.
|
||||||
|
|
||||||
:class:`BytesIO` provides or overrides these methods in addition to those
|
:class:`BytesIO` provides or overrides these methods in addition to those
|
||||||
from :class:`BufferedIOBase` and :class:`IOBase`:
|
from :class:`BufferedIOBase` and :class:`IOBase`:
|
||||||
|
|
||||||
|
|
80
Lib/_pyio.py
80
Lib/_pyio.py
|
@ -876,16 +876,28 @@ class BytesIO(BufferedIOBase):
|
||||||
_buffer = None
|
_buffer = None
|
||||||
|
|
||||||
def __init__(self, initial_bytes=None):
|
def __init__(self, initial_bytes=None):
|
||||||
|
# Use to keep self._buffer and self._pos consistent.
|
||||||
|
self._lock = Lock()
|
||||||
|
|
||||||
buf = bytearray()
|
buf = bytearray()
|
||||||
if initial_bytes is not None:
|
if initial_bytes is not None:
|
||||||
buf += initial_bytes
|
buf += initial_bytes
|
||||||
self._buffer = buf
|
|
||||||
self._pos = 0
|
with self._lock:
|
||||||
|
self._buffer = buf
|
||||||
|
self._pos = 0
|
||||||
|
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
if self.closed:
|
if self.closed:
|
||||||
raise ValueError("__getstate__ on closed file")
|
raise ValueError("__getstate__ on closed file")
|
||||||
return self.__dict__.copy()
|
with self._lock:
|
||||||
|
state = self.__dict__.copy()
|
||||||
|
del state['_lock']
|
||||||
|
return state
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
self.__dict__.update(state)
|
||||||
|
self._lock = Lock()
|
||||||
|
|
||||||
def getvalue(self):
|
def getvalue(self):
|
||||||
"""Return the bytes value (contents) of the buffer
|
"""Return the bytes value (contents) of the buffer
|
||||||
|
@ -918,14 +930,16 @@ class BytesIO(BufferedIOBase):
|
||||||
raise TypeError(f"{size!r} is not an integer")
|
raise TypeError(f"{size!r} is not an integer")
|
||||||
else:
|
else:
|
||||||
size = size_index()
|
size = size_index()
|
||||||
if size < 0:
|
|
||||||
size = len(self._buffer)
|
with self._lock:
|
||||||
if len(self._buffer) <= self._pos:
|
if size < 0:
|
||||||
return b""
|
size = len(self._buffer)
|
||||||
newpos = min(len(self._buffer), self._pos + size)
|
if len(self._buffer) <= self._pos:
|
||||||
b = self._buffer[self._pos : newpos]
|
return b""
|
||||||
self._pos = newpos
|
newpos = min(len(self._buffer), self._pos + size)
|
||||||
return bytes(b)
|
b = self._buffer[self._pos : newpos]
|
||||||
|
self._pos = newpos
|
||||||
|
return bytes(b)
|
||||||
|
|
||||||
def read1(self, size=-1):
|
def read1(self, size=-1):
|
||||||
"""This is the same as read.
|
"""This is the same as read.
|
||||||
|
@ -941,12 +955,14 @@ class BytesIO(BufferedIOBase):
|
||||||
n = view.nbytes # Size of any bytes-like object
|
n = view.nbytes # Size of any bytes-like object
|
||||||
if n == 0:
|
if n == 0:
|
||||||
return 0
|
return 0
|
||||||
pos = self._pos
|
|
||||||
if pos > len(self._buffer):
|
with self._lock:
|
||||||
# Pad buffer to pos with null bytes.
|
pos = self._pos
|
||||||
self._buffer.resize(pos)
|
if pos > len(self._buffer):
|
||||||
self._buffer[pos:pos + n] = b
|
# Pad buffer to pos with null bytes.
|
||||||
self._pos += n
|
self._buffer.resize(pos)
|
||||||
|
self._buffer[pos:pos + n] = b
|
||||||
|
self._pos += n
|
||||||
return n
|
return n
|
||||||
|
|
||||||
def seek(self, pos, whence=0):
|
def seek(self, pos, whence=0):
|
||||||
|
@ -963,9 +979,11 @@ class BytesIO(BufferedIOBase):
|
||||||
raise ValueError("negative seek position %r" % (pos,))
|
raise ValueError("negative seek position %r" % (pos,))
|
||||||
self._pos = pos
|
self._pos = pos
|
||||||
elif whence == 1:
|
elif whence == 1:
|
||||||
self._pos = max(0, self._pos + pos)
|
with self._lock:
|
||||||
|
self._pos = max(0, self._pos + pos)
|
||||||
elif whence == 2:
|
elif whence == 2:
|
||||||
self._pos = max(0, len(self._buffer) + pos)
|
with self._lock:
|
||||||
|
self._pos = max(0, len(self._buffer) + pos)
|
||||||
else:
|
else:
|
||||||
raise ValueError("unsupported whence value")
|
raise ValueError("unsupported whence value")
|
||||||
return self._pos
|
return self._pos
|
||||||
|
@ -978,18 +996,20 @@ class BytesIO(BufferedIOBase):
|
||||||
def truncate(self, pos=None):
|
def truncate(self, pos=None):
|
||||||
if self.closed:
|
if self.closed:
|
||||||
raise ValueError("truncate on closed file")
|
raise ValueError("truncate on closed file")
|
||||||
if pos is None:
|
|
||||||
pos = self._pos
|
with self._lock:
|
||||||
else:
|
if pos is None:
|
||||||
try:
|
pos = self._pos
|
||||||
pos_index = pos.__index__
|
|
||||||
except AttributeError:
|
|
||||||
raise TypeError(f"{pos!r} is not an integer")
|
|
||||||
else:
|
else:
|
||||||
pos = pos_index()
|
try:
|
||||||
if pos < 0:
|
pos_index = pos.__index__
|
||||||
raise ValueError("negative truncate position %r" % (pos,))
|
except AttributeError:
|
||||||
del self._buffer[pos:]
|
raise TypeError(f"{pos!r} is not an integer")
|
||||||
|
else:
|
||||||
|
pos = pos_index()
|
||||||
|
if pos < 0:
|
||||||
|
raise ValueError("negative truncate position %r" % (pos,))
|
||||||
|
del self._buffer[pos:]
|
||||||
return pos
|
return pos
|
||||||
|
|
||||||
def readable(self):
|
def readable(self):
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
import io
|
||||||
|
import _pyio as pyio
|
||||||
import threading
|
import threading
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from test.support import threading_helper
|
from test.support import threading_helper
|
||||||
from random import randint
|
from random import randint
|
||||||
from io import BytesIO
|
|
||||||
from sys import getsizeof
|
from sys import getsizeof
|
||||||
|
|
||||||
|
|
||||||
class TestBytesIO(TestCase):
|
class ThreadSafetyMixin:
|
||||||
# Test pretty much everything that can break under free-threading.
|
# Test pretty much everything that can break under free-threading.
|
||||||
# Non-deterministic, but at least one of these things will fail if
|
# Non-deterministic, but at least one of these things will fail if
|
||||||
# BytesIO object is not free-thread safe.
|
# BytesIO object is not free-thread safe.
|
||||||
|
@ -90,20 +91,27 @@ class TestBytesIO(TestCase):
|
||||||
barrier.wait()
|
barrier.wait()
|
||||||
getsizeof(b)
|
getsizeof(b)
|
||||||
|
|
||||||
self.check([write] * 10, BytesIO())
|
self.check([write] * 10, self.ioclass())
|
||||||
self.check([writelines] * 10, BytesIO())
|
self.check([writelines] * 10, self.ioclass())
|
||||||
self.check([write] * 10 + [truncate] * 10, BytesIO())
|
self.check([write] * 10 + [truncate] * 10, self.ioclass())
|
||||||
self.check([truncate] + [read] * 10, BytesIO(b'0\n'*204800))
|
self.check([truncate] + [read] * 10, self.ioclass(b'0\n'*204800))
|
||||||
self.check([truncate] + [read1] * 10, BytesIO(b'0\n'*204800))
|
self.check([truncate] + [read1] * 10, self.ioclass(b'0\n'*204800))
|
||||||
self.check([truncate] + [readline] * 10, BytesIO(b'0\n'*20480))
|
self.check([truncate] + [readline] * 10, self.ioclass(b'0\n'*20480))
|
||||||
self.check([truncate] + [readlines] * 10, BytesIO(b'0\n'*20480))
|
self.check([truncate] + [readlines] * 10, self.ioclass(b'0\n'*20480))
|
||||||
self.check([truncate] + [readinto] * 10, BytesIO(b'0\n'*204800), bytearray(b'0\n'*204800))
|
self.check([truncate] + [readinto] * 10, self.ioclass(b'0\n'*204800), bytearray(b'0\n'*204800))
|
||||||
self.check([close] + [write] * 10, BytesIO())
|
self.check([close] + [write] * 10, self.ioclass())
|
||||||
self.check([truncate] + [getvalue] * 10, BytesIO(b'0\n'*204800))
|
self.check([truncate] + [getvalue] * 10, self.ioclass(b'0\n'*204800))
|
||||||
self.check([truncate] + [getbuffer] * 10, BytesIO(b'0\n'*204800))
|
self.check([truncate] + [getbuffer] * 10, self.ioclass(b'0\n'*204800))
|
||||||
self.check([truncate] + [iter] * 10, BytesIO(b'0\n'*20480))
|
self.check([truncate] + [iter] * 10, self.ioclass(b'0\n'*20480))
|
||||||
self.check([truncate] + [getstate] * 10, BytesIO(b'0\n'*204800))
|
self.check([truncate] + [getstate] * 10, self.ioclass(b'0\n'*204800))
|
||||||
self.check([truncate] + [setstate] * 10, BytesIO(b'0\n'*204800), (b'123', 0, None))
|
state = self.ioclass(b'123').__getstate__()
|
||||||
self.check([truncate] + [sizeof] * 10, BytesIO(b'0\n'*204800))
|
self.check([truncate] + [setstate] * 10, self.ioclass(b'0\n'*204800), state)
|
||||||
|
self.check([truncate] + [sizeof] * 10, self.ioclass(b'0\n'*204800))
|
||||||
|
|
||||||
# no tests for seek or tell because they don't break anything
|
# no tests for seek or tell because they don't break anything
|
||||||
|
|
||||||
|
class CBytesIOTest(ThreadSafetyMixin, TestCase):
|
||||||
|
ioclass = io.BytesIO
|
||||||
|
|
||||||
|
class PyBytesIOTest(ThreadSafetyMixin, TestCase):
|
||||||
|
ioclass = pyio.BytesIO
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
# * test_univnewlines - tests universal newline support
|
# * test_univnewlines - tests universal newline support
|
||||||
# * test_largefile - tests operations on a file greater than 2**32 bytes
|
# * test_largefile - tests operations on a file greater than 2**32 bytes
|
||||||
# (only enabled with -ulargefile)
|
# (only enabled with -ulargefile)
|
||||||
|
# * test_free_threading/test_io - tests thread safety of io objects
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# ATTENTION TEST WRITERS!!!
|
# ATTENTION TEST WRITERS!!!
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Update Python implementation of :class:`io.BytesIO` to be thread safe.
|
Loading…
Add table
Add a link
Reference in a new issue