Commit aebcdba8 authored by Nadeem Vawda's avatar Nadeem Vawda

Make BZ2File's fileobj support easier to use.

The fileobj argument was added during the 3.3 development cycle, so this change
does not break backward compatibility with 3.2.
parent 68721019
...@@ -26,17 +26,18 @@ All of the classes in this module may safely be accessed from multiple threads. ...@@ -26,17 +26,18 @@ All of the classes in this module may safely be accessed from multiple threads.
(De)compression of files (De)compression of files
------------------------ ------------------------
.. class:: BZ2File(filename=None, mode='r', buffering=None, compresslevel=9, \*, fileobj=None) .. class:: BZ2File(filename, mode='r', buffering=None, compresslevel=9)
Open a bzip2-compressed file. Open a bzip2-compressed file.
The :class:`BZ2File` can wrap an existing :term:`file object` (given by If *filename* is a :class:`str` or :class:`bytes` object, open the named file
*fileobj*), or operate directly on a named file (named by *filename*). directly. Otherwise, *filename* should be a :term:`file object`, which will
Exactly one of these two parameters should be provided. be used to read or write the compressed data.
The *mode* argument can be either ``'r'`` for reading (default), ``'w'`` for The *mode* argument can be either ``'r'`` for reading (default), ``'w'`` for
overwriting, or ``'a'`` for appending. If *fileobj* is provided, a mode of overwriting, or ``'a'`` for appending. If *filename* is a file object (rather
``'w'`` does not truncate the file, and is instead equivalent to ``'a'``. than an actual file name), a mode of ``'w'`` does not truncate the file, and
is instead equivalent to ``'a'``.
The *buffering* argument is ignored. Its use is deprecated. The *buffering* argument is ignored. Its use is deprecated.
...@@ -69,7 +70,8 @@ All of the classes in this module may safely be accessed from multiple threads. ...@@ -69,7 +70,8 @@ All of the classes in this module may safely be accessed from multiple threads.
:meth:`read1` and :meth:`readinto` methods were added. :meth:`read1` and :meth:`readinto` methods were added.
.. versionchanged:: 3.3 .. versionchanged:: 3.3
The *fileobj* argument to the constructor was added. Support was added for *filename* being a :term:`file object` instead of an
actual filename.
.. versionchanged:: 3.3 .. versionchanged:: 3.3
The ``'a'`` (append) mode was added, along with support for reading The ``'a'`` (append) mode was added, along with support for reading
......
...@@ -39,13 +39,12 @@ class BZ2File(io.BufferedIOBase): ...@@ -39,13 +39,12 @@ class BZ2File(io.BufferedIOBase):
returned as bytes, and data to be written should be given as bytes. returned as bytes, and data to be written should be given as bytes.
""" """
def __init__(self, filename=None, mode="r", buffering=None, def __init__(self, filename, mode="r", buffering=None, compresslevel=9):
compresslevel=9, *, fileobj=None):
"""Open a bzip2-compressed file. """Open a bzip2-compressed file.
If filename is given, open the named file. Otherwise, operate on If filename is a str or bytes object, is gives the name of the file to
the file object given by fileobj. Exactly one of these two be opened. Otherwise, it should be a file object, which will be used to
parameters should be provided. read or write the compressed data.
mode can be 'r' for reading (default), 'w' for (over)writing, or mode can be 'r' for reading (default), 'w' for (over)writing, or
'a' for appending. 'a' for appending.
...@@ -91,15 +90,15 @@ class BZ2File(io.BufferedIOBase): ...@@ -91,15 +90,15 @@ class BZ2File(io.BufferedIOBase):
else: else:
raise ValueError("Invalid mode: {!r}".format(mode)) raise ValueError("Invalid mode: {!r}".format(mode))
if filename is not None and fileobj is None: if isinstance(filename, (str, bytes)):
self._fp = open(filename, mode) self._fp = open(filename, mode)
self._closefp = True self._closefp = True
self._mode = mode_code self._mode = mode_code
elif fileobj is not None and filename is None: elif hasattr(filename, "read") or hasattr(filename, "write"):
self._fp = fileobj self._fp = filename
self._mode = mode_code self._mode = mode_code
else: else:
raise ValueError("Must give exactly one of filename and fileobj") raise TypeError("filename must be a str or bytes object, or a file")
def close(self): def close(self):
"""Flush and close the file. """Flush and close the file.
......
...@@ -1657,8 +1657,8 @@ class TarFile(object): ...@@ -1657,8 +1657,8 @@ class TarFile(object):
except ImportError: except ImportError:
raise CompressionError("bz2 module is not available") raise CompressionError("bz2 module is not available")
fileobj = bz2.BZ2File(filename=name if fileobj is None else None, fileobj = bz2.BZ2File(fileobj or name, mode,
mode=mode, fileobj=fileobj, compresslevel=compresslevel) compresslevel=compresslevel)
try: try:
t = cls.taropen(name, mode, fileobj, **kwargs) t = cls.taropen(name, mode, fileobj, **kwargs)
......
...@@ -81,6 +81,20 @@ class BZ2FileTest(BaseTest): ...@@ -81,6 +81,20 @@ class BZ2FileTest(BaseTest):
with open(self.filename, "wb") as f: with open(self.filename, "wb") as f:
f.write(self.DATA * streams) f.write(self.DATA * streams)
def testBadArgs(self):
with self.assertRaises(TypeError):
BZ2File(123.456)
with self.assertRaises(ValueError):
BZ2File("/dev/null", "z")
with self.assertRaises(ValueError):
BZ2File("/dev/null", "rx")
with self.assertRaises(ValueError):
BZ2File("/dev/null", "rbt")
with self.assertRaises(ValueError):
BZ2File("/dev/null", compresslevel=0)
with self.assertRaises(ValueError):
BZ2File("/dev/null", compresslevel=10)
def testRead(self): def testRead(self):
self.createTempFile() self.createTempFile()
with BZ2File(self.filename) as bz2f: with BZ2File(self.filename) as bz2f:
...@@ -348,7 +362,7 @@ class BZ2FileTest(BaseTest): ...@@ -348,7 +362,7 @@ class BZ2FileTest(BaseTest):
def testFileno(self): def testFileno(self):
self.createTempFile() self.createTempFile()
with open(self.filename, 'rb') as rawf: with open(self.filename, 'rb') as rawf:
bz2f = BZ2File(fileobj=rawf) bz2f = BZ2File(rawf)
try: try:
self.assertEqual(bz2f.fileno(), rawf.fileno()) self.assertEqual(bz2f.fileno(), rawf.fileno())
finally: finally:
...@@ -356,7 +370,7 @@ class BZ2FileTest(BaseTest): ...@@ -356,7 +370,7 @@ class BZ2FileTest(BaseTest):
self.assertRaises(ValueError, bz2f.fileno) self.assertRaises(ValueError, bz2f.fileno)
def testSeekable(self): def testSeekable(self):
bz2f = BZ2File(fileobj=BytesIO(self.DATA)) bz2f = BZ2File(BytesIO(self.DATA))
try: try:
self.assertTrue(bz2f.seekable()) self.assertTrue(bz2f.seekable())
bz2f.read() bz2f.read()
...@@ -365,7 +379,7 @@ class BZ2FileTest(BaseTest): ...@@ -365,7 +379,7 @@ class BZ2FileTest(BaseTest):
bz2f.close() bz2f.close()
self.assertRaises(ValueError, bz2f.seekable) self.assertRaises(ValueError, bz2f.seekable)
bz2f = BZ2File(fileobj=BytesIO(), mode="w") bz2f = BZ2File(BytesIO(), mode="w")
try: try:
self.assertFalse(bz2f.seekable()) self.assertFalse(bz2f.seekable())
finally: finally:
...@@ -374,7 +388,7 @@ class BZ2FileTest(BaseTest): ...@@ -374,7 +388,7 @@ class BZ2FileTest(BaseTest):
src = BytesIO(self.DATA) src = BytesIO(self.DATA)
src.seekable = lambda: False src.seekable = lambda: False
bz2f = BZ2File(fileobj=src) bz2f = BZ2File(src)
try: try:
self.assertFalse(bz2f.seekable()) self.assertFalse(bz2f.seekable())
finally: finally:
...@@ -382,7 +396,7 @@ class BZ2FileTest(BaseTest): ...@@ -382,7 +396,7 @@ class BZ2FileTest(BaseTest):
self.assertRaises(ValueError, bz2f.seekable) self.assertRaises(ValueError, bz2f.seekable)
def testReadable(self): def testReadable(self):
bz2f = BZ2File(fileobj=BytesIO(self.DATA)) bz2f = BZ2File(BytesIO(self.DATA))
try: try:
self.assertTrue(bz2f.readable()) self.assertTrue(bz2f.readable())
bz2f.read() bz2f.read()
...@@ -391,7 +405,7 @@ class BZ2FileTest(BaseTest): ...@@ -391,7 +405,7 @@ class BZ2FileTest(BaseTest):
bz2f.close() bz2f.close()
self.assertRaises(ValueError, bz2f.readable) self.assertRaises(ValueError, bz2f.readable)
bz2f = BZ2File(fileobj=BytesIO(), mode="w") bz2f = BZ2File(BytesIO(), mode="w")
try: try:
self.assertFalse(bz2f.readable()) self.assertFalse(bz2f.readable())
finally: finally:
...@@ -399,7 +413,7 @@ class BZ2FileTest(BaseTest): ...@@ -399,7 +413,7 @@ class BZ2FileTest(BaseTest):
self.assertRaises(ValueError, bz2f.readable) self.assertRaises(ValueError, bz2f.readable)
def testWritable(self): def testWritable(self):
bz2f = BZ2File(fileobj=BytesIO(self.DATA)) bz2f = BZ2File(BytesIO(self.DATA))
try: try:
self.assertFalse(bz2f.writable()) self.assertFalse(bz2f.writable())
bz2f.read() bz2f.read()
...@@ -408,7 +422,7 @@ class BZ2FileTest(BaseTest): ...@@ -408,7 +422,7 @@ class BZ2FileTest(BaseTest):
bz2f.close() bz2f.close()
self.assertRaises(ValueError, bz2f.writable) self.assertRaises(ValueError, bz2f.writable)
bz2f = BZ2File(fileobj=BytesIO(), mode="w") bz2f = BZ2File(BytesIO(), mode="w")
try: try:
self.assertTrue(bz2f.writable()) self.assertTrue(bz2f.writable())
finally: finally:
...@@ -512,14 +526,14 @@ class BZ2FileTest(BaseTest): ...@@ -512,14 +526,14 @@ class BZ2FileTest(BaseTest):
def testReadBytesIO(self): def testReadBytesIO(self):
with BytesIO(self.DATA) as bio: with BytesIO(self.DATA) as bio:
with BZ2File(fileobj=bio) as bz2f: with BZ2File(bio) as bz2f:
self.assertRaises(TypeError, bz2f.read, None) self.assertRaises(TypeError, bz2f.read, None)
self.assertEqual(bz2f.read(), self.TEXT) self.assertEqual(bz2f.read(), self.TEXT)
self.assertFalse(bio.closed) self.assertFalse(bio.closed)
def testPeekBytesIO(self): def testPeekBytesIO(self):
with BytesIO(self.DATA) as bio: with BytesIO(self.DATA) as bio:
with BZ2File(fileobj=bio) as bz2f: with BZ2File(bio) as bz2f:
pdata = bz2f.peek() pdata = bz2f.peek()
self.assertNotEqual(len(pdata), 0) self.assertNotEqual(len(pdata), 0)
self.assertTrue(self.TEXT.startswith(pdata)) self.assertTrue(self.TEXT.startswith(pdata))
...@@ -527,7 +541,7 @@ class BZ2FileTest(BaseTest): ...@@ -527,7 +541,7 @@ class BZ2FileTest(BaseTest):
def testWriteBytesIO(self): def testWriteBytesIO(self):
with BytesIO() as bio: with BytesIO() as bio:
with BZ2File(fileobj=bio, mode="w") as bz2f: with BZ2File(bio, "w") as bz2f:
self.assertRaises(TypeError, bz2f.write) self.assertRaises(TypeError, bz2f.write)
bz2f.write(self.TEXT) bz2f.write(self.TEXT)
self.assertEqual(self.decompress(bio.getvalue()), self.TEXT) self.assertEqual(self.decompress(bio.getvalue()), self.TEXT)
...@@ -535,14 +549,14 @@ class BZ2FileTest(BaseTest): ...@@ -535,14 +549,14 @@ class BZ2FileTest(BaseTest):
def testSeekForwardBytesIO(self): def testSeekForwardBytesIO(self):
with BytesIO(self.DATA) as bio: with BytesIO(self.DATA) as bio:
with BZ2File(fileobj=bio) as bz2f: with BZ2File(bio) as bz2f:
self.assertRaises(TypeError, bz2f.seek) self.assertRaises(TypeError, bz2f.seek)
bz2f.seek(150) bz2f.seek(150)
self.assertEqual(bz2f.read(), self.TEXT[150:]) self.assertEqual(bz2f.read(), self.TEXT[150:])
def testSeekBackwardsBytesIO(self): def testSeekBackwardsBytesIO(self):
with BytesIO(self.DATA) as bio: with BytesIO(self.DATA) as bio:
with BZ2File(fileobj=bio) as bz2f: with BZ2File(bio) as bz2f:
bz2f.read(500) bz2f.read(500)
bz2f.seek(-150, 1) bz2f.seek(-150, 1)
self.assertEqual(bz2f.read(), self.TEXT[500-150:]) self.assertEqual(bz2f.read(), self.TEXT[500-150:])
......
...@@ -15,6 +15,9 @@ Core and Builtins ...@@ -15,6 +15,9 @@ Core and Builtins
Library Library
------- -------
- BZ2File.__init__() now accepts a file object as its first argument, rather
than requiring a separate "fileobj" argument.
- gzip.open() now accepts file objects as well as filenames. - gzip.open() now accepts file objects as well as filenames.
- Issue #14992: os.makedirs(path, exist_ok=True) would raise an OSError - Issue #14992: os.makedirs(path, exist_ok=True) would raise an OSError
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment