summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorMartin Panter <vadmium+py@gmail.com>2016-05-28 00:41:57 (GMT)
committerMartin Panter <vadmium+py@gmail.com>2016-05-28 00:41:57 (GMT)
commit6bb91f3b6e51352f91bcf785d3f6fe160ed2cd85 (patch)
tree0f97c3f8b3fac65680e880ec870796138af78faf /Lib
parentfc36e66af46957a65b12ab0087027727f8555aed (diff)
downloadcpython-6bb91f3b6e51352f91bcf785d3f6fe160ed2cd85.zip
cpython-6bb91f3b6e51352f91bcf785d3f6fe160ed2cd85.tar.gz
cpython-6bb91f3b6e51352f91bcf785d3f6fe160ed2cd85.tar.bz2
Issue #20699: Document that “io” methods accept bytes-like objects
This matches the usage of ZipFile and BufferedWriter. This still requires return values to be bytes() objects. Also document and test that the write() methods should only access their argument before they return.
Diffstat (limited to 'Lib')
-rw-r--r--Lib/_pyio.py26
-rw-r--r--Lib/test/test_io.py78
-rw-r--r--Lib/test/test_memoryio.py23
3 files changed, 88 insertions, 39 deletions
diff --git a/Lib/_pyio.py b/Lib/_pyio.py
index 313fadf..10b87d3 100644
--- a/Lib/_pyio.py
+++ b/Lib/_pyio.py
@@ -296,8 +296,9 @@ class IOBase(metaclass=abc.ABCMeta):
called.
The basic type used for binary data read from or written to a file is
- bytes. bytearrays are accepted too, and in some cases (such as
- readinto) needed. Text I/O classes work with str data.
+ bytes. Other bytes-like objects are accepted as method arguments too. In
+ some cases (such as readinto), a writable object is required. Text I/O
+ classes work with str data.
Note that calling any method (even inquiries) on a closed stream is
undefined. Implementations may raise OSError in this case.
@@ -596,7 +597,7 @@ class RawIOBase(IOBase):
return data
def readinto(self, b):
- """Read up to len(b) bytes into bytearray b.
+ """Read bytes into a pre-allocated bytes-like object b.
Returns an int representing the number of bytes read (0 for EOF), or
None if the object is set not to block and has no data to read.
@@ -606,7 +607,8 @@ class RawIOBase(IOBase):
def write(self, b):
"""Write the given buffer to the IO stream.
- Returns the number of bytes written, which may be less than len(b).
+ Returns the number of bytes written, which may be less than the
+ length of b in bytes.
"""
self._unsupported("write")
@@ -659,7 +661,7 @@ class BufferedIOBase(IOBase):
self._unsupported("read1")
def readinto(self, b):
- """Read up to len(b) bytes into bytearray b.
+ """Read bytes into a pre-allocated bytes-like object b.
Like read(), this may issue multiple reads to the underlying raw
stream, unless the latter is 'interactive'.
@@ -673,7 +675,7 @@ class BufferedIOBase(IOBase):
return self._readinto(b, read1=False)
def readinto1(self, b):
- """Read up to len(b) bytes into *b*, using at most one system call
+ """Read bytes into buffer *b*, using at most one system call
Returns an int representing the number of bytes read (0 for EOF).
@@ -701,8 +703,8 @@ class BufferedIOBase(IOBase):
def write(self, b):
"""Write the given bytes buffer to the IO stream.
- Return the number of bytes written, which is never less than
- len(b).
+ Return the number of bytes written, which is always the length of b
+ in bytes.
Raises BlockingIOError if the buffer is full and the
underlying raw stream cannot accept more data at the moment.
@@ -884,7 +886,8 @@ class BytesIO(BufferedIOBase):
raise ValueError("write to closed file")
if isinstance(b, str):
raise TypeError("can't write str to binary stream")
- n = len(b)
+ with memoryview(b) as view:
+ n = view.nbytes # Size of any bytes-like object
if n == 0:
return 0
pos = self._pos
@@ -1090,14 +1093,13 @@ class BufferedReader(_BufferedIOMixin):
def _readinto(self, buf, read1):
"""Read data into *buf* with at most one system call."""
- if len(buf) == 0:
- return 0
-
# Need to create a memoryview object of type 'b', otherwise
# we may not be able to assign bytes to it, and slicing it
# would create a new object.
if not isinstance(buf, memoryview):
buf = memoryview(buf)
+ if buf.nbytes == 0:
+ return 0
buf = buf.cast('B')
written = 0
diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py
index a7bb95b..7323198 100644
--- a/Lib/test/test_io.py
+++ b/Lib/test/test_io.py
@@ -45,6 +45,22 @@ try:
except ImportError:
threading = None
+try:
+ import ctypes
+except ImportError:
+ def byteslike(*pos, **kw):
+ return array.array("b", bytes(*pos, **kw))
+else:
+ def byteslike(*pos, **kw):
+ """Create a bytes-like object having no string or sequence methods"""
+ data = bytes(*pos, **kw)
+ obj = EmptyStruct()
+ ctypes.resize(obj, len(data))
+ memoryview(obj).cast("B")[:] = data
+ return obj
+ class EmptyStruct(ctypes.Structure):
+ pass
+
def _default_chunk_size():
"""Get the default TextIOWrapper chunk size"""
with open(__file__, "r", encoding="latin-1") as f:
@@ -284,7 +300,9 @@ class IOTest(unittest.TestCase):
self.assertEqual(f.tell(), 6)
self.assertEqual(f.seek(-1, 1), 5)
self.assertEqual(f.tell(), 5)
- self.assertEqual(f.write(bytearray(b" world\n\n\n")), 9)
+ buffer = bytearray(b" world\n\n\n")
+ self.assertEqual(f.write(buffer), 9)
+ buffer[:] = b"*" * 9 # Overwrite our copy of the data
self.assertEqual(f.seek(0), 0)
self.assertEqual(f.write(b"h"), 1)
self.assertEqual(f.seek(-1, 2), 13)
@@ -297,20 +315,21 @@ class IOTest(unittest.TestCase):
def read_ops(self, f, buffered=False):
data = f.read(5)
self.assertEqual(data, b"hello")
- data = bytearray(data)
+ data = byteslike(data)
self.assertEqual(f.readinto(data), 5)
- self.assertEqual(data, b" worl")
+ self.assertEqual(bytes(data), b" worl")
+ data = bytearray(5)
self.assertEqual(f.readinto(data), 2)
self.assertEqual(len(data), 5)
self.assertEqual(data[:2], b"d\n")
self.assertEqual(f.seek(0), 0)
self.assertEqual(f.read(20), b"hello world\n")
self.assertEqual(f.read(1), b"")
- self.assertEqual(f.readinto(bytearray(b"x")), 0)
+ self.assertEqual(f.readinto(byteslike(b"x")), 0)
self.assertEqual(f.seek(-6, 2), 6)
self.assertEqual(f.read(5), b"world")
self.assertEqual(f.read(0), b"")
- self.assertEqual(f.readinto(bytearray()), 0)
+ self.assertEqual(f.readinto(byteslike()), 0)
self.assertEqual(f.seek(-6, 1), 5)
self.assertEqual(f.read(5), b" worl")
self.assertEqual(f.tell(), 10)
@@ -321,6 +340,10 @@ class IOTest(unittest.TestCase):
f.seek(6)
self.assertEqual(f.read(), b"world\n")
self.assertEqual(f.read(), b"")
+ f.seek(0)
+ data = byteslike(5)
+ self.assertEqual(f.readinto1(data), 5)
+ self.assertEqual(bytes(data), b"hello")
LARGE = 2**31
@@ -637,10 +660,15 @@ class IOTest(unittest.TestCase):
def test_array_writes(self):
a = array.array('i', range(10))
n = len(a.tobytes())
- with self.open(support.TESTFN, "wb", 0) as f:
- self.assertEqual(f.write(a), n)
- with self.open(support.TESTFN, "wb") as f:
- self.assertEqual(f.write(a), n)
+ def check(f):
+ with f:
+ self.assertEqual(f.write(a), n)
+ f.writelines((a,))
+ check(self.BytesIO())
+ check(self.FileIO(support.TESTFN, "w"))
+ check(self.BufferedWriter(self.MockRawIO()))
+ check(self.BufferedRandom(self.MockRawIO()))
+ check(self.BufferedRWPair(self.MockRawIO(), self.MockRawIO()))
def test_closefd(self):
self.assertRaises(ValueError, self.open, support.TESTFN, 'w',
@@ -799,6 +827,19 @@ class IOTest(unittest.TestCase):
with self.assertRaises(ValueError):
self.open(support.TESTFN, 'w', newline='invalid')
+ def test_buffered_readinto_mixin(self):
+ # Test the implementation provided by BufferedIOBase
+ class Stream(self.BufferedIOBase):
+ def read(self, size):
+ return b"12345"
+ read1 = read
+ stream = Stream()
+ for method in ("readinto", "readinto1"):
+ with self.subTest(method):
+ buffer = byteslike(5)
+ self.assertEqual(getattr(stream, method)(buffer), 5)
+ self.assertEqual(bytes(buffer), b"12345")
+
class CIOTest(IOTest):
@@ -1390,6 +1431,11 @@ class BufferedWriterTest(unittest.TestCase, CommonBufferedTests):
bufio = self.tp(writer, 8)
bufio.write(b"abc")
self.assertFalse(writer._write_stack)
+ buffer = bytearray(b"def")
+ bufio.write(buffer)
+ buffer[:] = b"***" # Overwrite our copy of the data
+ bufio.flush()
+ self.assertEqual(b"".join(writer._write_stack), b"abcdef")
def test_write_overflow(self):
writer = self.MockRawIO()
@@ -1716,11 +1762,13 @@ class BufferedRWPairTest(unittest.TestCase):
self.assertEqual(pair.read1(3), b"abc")
def test_readinto(self):
- pair = self.tp(self.BytesIO(b"abcdef"), self.MockRawIO())
+ for method in ("readinto", "readinto1"):
+ with self.subTest(method):
+ pair = self.tp(self.BytesIO(b"abcdef"), self.MockRawIO())
- data = bytearray(5)
- self.assertEqual(pair.readinto(data), 5)
- self.assertEqual(data, b"abcde")
+ data = byteslike(5)
+ self.assertEqual(getattr(pair, method)(data), 5)
+ self.assertEqual(bytes(data), b"abcde")
def test_write(self):
w = self.MockRawIO()
@@ -1728,7 +1776,9 @@ class BufferedRWPairTest(unittest.TestCase):
pair.write(b"abc")
pair.flush()
- pair.write(b"def")
+ buffer = bytearray(b"def")
+ pair.write(buffer)
+ buffer[:] = b"***" # Overwrite our copy of the data
pair.flush()
self.assertEqual(w._write_stack, [b"abc", b"def"])
diff --git a/Lib/test/test_memoryio.py b/Lib/test/test_memoryio.py
index 812db4c..55b693e 100644
--- a/Lib/test/test_memoryio.py
+++ b/Lib/test/test_memoryio.py
@@ -399,7 +399,16 @@ class MemoryTestMixin:
del __main__.PickleTestMemIO
-class BytesIOMixin:
+class PyBytesIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase):
+ # Test _pyio.BytesIO; class also inherited for testing C implementation
+
+ UnsupportedOperation = pyio.UnsupportedOperation
+
+ @staticmethod
+ def buftype(s):
+ return s.encode("ascii")
+ ioclass = pyio.BytesIO
+ EOF = b""
def test_getbuffer(self):
memio = self.ioclass(b"1234567890")
@@ -426,18 +435,6 @@ class BytesIOMixin:
memio.close()
self.assertRaises(ValueError, memio.getbuffer)
-
-class PyBytesIOTest(MemoryTestMixin, MemorySeekTestMixin,
- BytesIOMixin, unittest.TestCase):
-
- UnsupportedOperation = pyio.UnsupportedOperation
-
- @staticmethod
- def buftype(s):
- return s.encode("ascii")
- ioclass = pyio.BytesIO
- EOF = b""
-
def test_read1(self):
buf = self.buftype("1234567890")
memio = self.ioclass(buf)