diff --git a/Doc/library/lzma.rst b/Doc/library/lzma.rst --- a/Doc/library/lzma.rst +++ b/Doc/library/lzma.rst @@ -34,18 +34,19 @@ Reading and writing compressed files ------------------------------------ .. function:: open(filename, mode="rb", \*, format=None, check=-1, preset=None, filters=None, encoding=None, errors=None, newline=None) Open an LZMA-compressed file in binary or text mode, returning a :term:`file object`. The *filename* argument can be either an actual file name (given as a - :class:`str` or :class:`bytes` object), in which case the named file is - opened, or it can be an existing file object to read from or write to. + :class:`str`, :class:`bytes` or :term:`path-like object` object), in + which case the named file is opened, or it can be an existing file object + to read from or write to. The *mode* argument can be any of ``"r"``, ``"rb"``, ``"w"``, ``"wb"``, ``"x"``, ``"xb"``, ``"a"`` or ``"ab"`` for binary mode, or ``"rt"``, ``"wt"``, ``"xt"``, or ``"at"`` for text mode. The default is ``"rb"``. When opening a file for reading, the *format* and *filters* arguments have the same meanings as for :class:`LZMADecompressor`. In this case, the *check* and *preset* arguments should not be used. @@ -59,26 +60,30 @@ Reading and writing compressed files For text mode, a :class:`LZMAFile` object is created, and wrapped in an :class:`io.TextIOWrapper` instance with the specified encoding, error handling behavior, and line ending(s). .. versionchanged:: 3.4 Added support for the ``"x"``, ``"xb"`` and ``"xt"`` modes. + .. versionchanged:: 3.6 + Accepts a :term:`path-like object`. + .. class:: LZMAFile(filename=None, mode="r", \*, format=None, check=-1, preset=None, filters=None) Open an LZMA-compressed file in binary mode. An :class:`LZMAFile` can wrap an already-open :term:`file object`, or operate directly on a named file. The *filename* argument specifies either the file - object to wrap, or the name of the file to open (as a :class:`str` or - :class:`bytes` object). When wrapping an existing file object, the wrapped - file will not be closed when the :class:`LZMAFile` is closed. + object to wrap, or the name of the file to open (as a :class:`str`, + :class:`bytes` or :term:`path-like object` object). When wrapping an + existing file object, the wrapped file will not be closed when the + :class:`LZMAFile` is closed. The *mode* argument can be either ``"r"`` for reading (default), ``"w"`` for overwriting, ``"x"`` for exclusive creation, or ``"a"`` for appending. These can equivalently be given as ``"rb"``, ``"wb"``, ``"xb"`` and ``"ab"`` respectively. If *filename* is a file object (rather than an actual file name), a mode of ``"w"`` does not truncate the file, and is instead equivalent to ``"a"``. @@ -113,16 +118,19 @@ Reading and writing compressed files .. versionchanged:: 3.4 Added support for the ``"x"`` and ``"xb"`` modes. .. versionchanged:: 3.5 The :meth:`~io.BufferedIOBase.read` method now accepts an argument of ``None``. + .. versionchanged:: 3.6 + Accepts a :term:`path-like object`. + Compressing and decompressing data in memory -------------------------------------------- .. class:: LZMACompressor(format=FORMAT_XZ, check=-1, preset=None, filters=None) Create a compressor object, which can be used to compress data incrementally. diff --git a/Lib/lzma.py b/Lib/lzma.py --- a/Lib/lzma.py +++ b/Lib/lzma.py @@ -18,16 +18,17 @@ container formats, as well as raw compre "MODE_FAST", "MODE_NORMAL", "PRESET_DEFAULT", "PRESET_EXTREME", "LZMACompressor", "LZMADecompressor", "LZMAFile", "LZMAError", "open", "compress", "decompress", "is_check_supported", ] import builtins import io +import os from _lzma import * from _lzma import _encode_filter_properties, _decode_filter_properties import _compression _MODE_CLOSED = 0 _MODE_READ = 1 # Value 2 no longer used @@ -44,19 +45,20 @@ class LZMAFile(_compression.BaseStream): Note that LZMAFile provides a *binary* file interface - data read is returned as bytes, and data to be written must be given as bytes. """ def __init__(self, filename=None, mode="r", *, format=None, check=-1, preset=None, filters=None): """Open an LZMA-compressed file in binary mode. - filename can be either an actual file name (given as a str or - bytes object), in which case the named file is opened, or it can - be an existing file object to read from or write to. + filename can be either an actual file name (given as a str, + bytes, or PathLike object), in which case the named file is + opened, or it can be an existing file object to read from or + write to. mode can be "r" for reading (default), "w" for (over)writing, "x" for creating exclusively, or "a" for appending. These can equivalently be given as "rb", "wb", "xb" and "ab" respectively. format specifies the container format to use for the file. If mode is "r", this defaults to FORMAT_AUTO. Otherwise, the default is FORMAT_XZ. @@ -107,27 +109,27 @@ class LZMAFile(_compression.BaseStream): format = FORMAT_XZ mode_code = _MODE_WRITE self._compressor = LZMACompressor(format=format, check=check, preset=preset, filters=filters) self._pos = 0 else: raise ValueError("Invalid mode: {!r}".format(mode)) - if isinstance(filename, (str, bytes)): + if isinstance(filename, (str, bytes, os.PathLike)): if "b" not in mode: mode += "b" self._fp = builtins.open(filename, mode) self._closefp = True self._mode = mode_code elif hasattr(filename, "read") or hasattr(filename, "write"): self._fp = filename self._mode = mode_code else: - raise TypeError("filename must be a str or bytes object, or a file") + raise TypeError("filename must be a str, bytes, file or PathLike object") if self._mode == _MODE_READ: raw = _compression.DecompressReader(self._fp, LZMADecompressor, trailing_error=LZMAError, format=format, filters=filters) self._buffer = io.BufferedReader(raw) def close(self): """Flush and close the file. @@ -258,19 +260,19 @@ class LZMAFile(_compression.BaseStream): return self._pos def open(filename, mode="rb", *, format=None, check=-1, preset=None, filters=None, encoding=None, errors=None, newline=None): """Open an LZMA-compressed file in binary or text mode. - filename can be either an actual file name (given as a str or bytes - object), in which case the named file is opened, or it can be an - existing file object to read from or write to. + filename can be either an actual file name (given as a str, bytes, + or PathLike object), in which case the named file is opened, or it + can be an existing file object to read from or write to. The mode argument can be "r", "rb" (default), "w", "wb", "x", "xb", "a", or "ab" for binary mode, or "rt", "wt", "xt", or "at" for text mode. The format, check, preset and filters arguments specify the compression settings, as for LZMACompressor, LZMADecompressor and LZMAFile. diff --git a/Lib/test/test_lzma.py b/Lib/test/test_lzma.py --- a/Lib/test/test_lzma.py +++ b/Lib/test/test_lzma.py @@ -1,11 +1,12 @@ import _compression from io import BytesIO, UnsupportedOperation, DEFAULT_BUFFER_SIZE import os +import pathlib import pickle import random import unittest from test.support import ( _4G, TESTFN, import_module, bigmemtest, run_unittest, unlink ) @@ -483,16 +484,26 @@ class FileTestCase(unittest.TestCase): pass with LZMAFile(BytesIO(), "w") as f: pass with LZMAFile(BytesIO(), "x") as f: pass with LZMAFile(BytesIO(), "a") as f: pass + def test_init_with_PathLike_filename(self): + filename = pathlib.Path(TESTFN) + with TempFile(filename, COMPRESSED_XZ): + with LZMAFile(filename) as f: + self.assertEqual(f.read(), INPUT) + with LZMAFile(filename, "a") as f: + f.write(INPUT) + with LZMAFile(filename) as f: + self.assertEqual(f.read(), INPUT * 2) + def test_init_with_filename(self): with TempFile(TESTFN, COMPRESSED_XZ): with LZMAFile(TESTFN) as f: pass with LZMAFile(TESTFN, "w") as f: pass with LZMAFile(TESTFN, "a") as f: pass @@ -1175,16 +1186,27 @@ class OpenTestCase(unittest.TestCase): self.assertEqual(file_data, INPUT) with lzma.open(TESTFN, "rb") as f: self.assertEqual(f.read(), INPUT) with lzma.open(TESTFN, "ab") as f: f.write(INPUT) with lzma.open(TESTFN, "rb") as f: self.assertEqual(f.read(), INPUT * 2) + def test_with_pathlike_filename(self): + filename = pathlib.Path(TESTFN) + with TempFile(filename): + with lzma.open(filename, "wb") as f: + f.write(INPUT) + with open(filename, "rb") as f: + file_data = lzma.decompress(f.read()) + self.assertEqual(file_data, INPUT) + with lzma.open(filename, "rb") as f: + self.assertEqual(f.read(), INPUT) + def test_bad_params(self): # Test invalid parameter combinations. with self.assertRaises(ValueError): lzma.open(TESTFN, "") with self.assertRaises(ValueError): lzma.open(TESTFN, "rbt") with self.assertRaises(ValueError): lzma.open(TESTFN, "rb", encoding="utf-8")