|
14 | 14 | import threading |
15 | 15 | import time |
16 | 16 |
|
| 17 | +from compression._common import _streams |
| 18 | + |
17 | 19 | try: |
18 | 20 | import zlib # We may need its compression method |
19 | 21 | crc32 = zlib.crc32 |
@@ -881,6 +883,205 @@ def _get_decompressor(compress_type): |
881 | 883 | raise NotImplementedError("compression type %d" % (compress_type,)) |
882 | 884 |
|
883 | 885 |
|
| 886 | +# Value 0 no longer used |
| 887 | +_MODE_READ = 1 |
| 888 | +# Value 2 no longer used |
| 889 | +_MODE_WRITE = 3 |
| 890 | + |
| 891 | +class _DeflateFile(_streams.BaseStream): |
| 892 | + |
| 893 | + """A file object providing transparent Deflate (de)compression. |
| 894 | +
|
| 895 | + A _DeflateFile can act as a wrapper for an existing file object, or |
| 896 | + refer directly to a named file on disk. |
| 897 | +
|
| 898 | + Note that _DeflateFile provides a *binary* file interface - data read |
| 899 | + is returned as bytes, and data to be written must be given as bytes. |
| 900 | + """ |
| 901 | + |
| 902 | + def __init__(self, filename=None, mode="r", *, |
| 903 | + level=zlib.Z_DEFAULT_COMPRESSION, method=zlib.DEFLATED, |
| 904 | + wbits=zlib.MAX_WBITS, memLevel=zlib.DEF_MEM_LEVEL, |
| 905 | + strategy=zlib.Z_DEFAULT_STRATEGY, zdict=None): |
| 906 | + """Open an Deflate-compressed file in binary mode. |
| 907 | +
|
| 908 | + TODO(emmatyping): docstring |
| 909 | + """ |
| 910 | + self._fp = None |
| 911 | + self._closefp = False |
| 912 | + self._mode = None |
| 913 | + |
| 914 | + if mode in ("r", "rb"): |
| 915 | + mode_code = _MODE_READ |
| 916 | + elif mode in ("w", "wb", "a", "ab", "x", "xb"): |
| 917 | + mode_code = _MODE_WRITE |
| 918 | + self._compressor = zlib.compressobj(level, method, wbits, memLevel, |
| 919 | + strategy, zdict) |
| 920 | + self._pos = 0 |
| 921 | + else: |
| 922 | + raise ValueError("Invalid mode: {!r}".format(mode)) |
| 923 | + |
| 924 | + if isinstance(filename, (str, bytes, os.PathLike)): |
| 925 | + if "b" not in mode: |
| 926 | + mode += "b" |
| 927 | + self._fp = io.open(filename, mode) |
| 928 | + self._closefp = True |
| 929 | + self._mode = mode_code |
| 930 | + elif hasattr(filename, "read") or hasattr(filename, "write"): |
| 931 | + self._fp = filename |
| 932 | + self._mode = mode_code |
| 933 | + else: |
| 934 | + raise TypeError("filename must be a str, bytes, file or PathLike object") |
| 935 | + |
| 936 | + if self._mode == _MODE_READ: |
| 937 | + raw = _streams.DecompressReader(self._fp, zlib.decompressobj, |
| 938 | + trailing_error=zlib.error, wbits=wbits, zdict=zdict) |
| 939 | + self._buffer = io.BufferedReader(raw) |
| 940 | + |
| 941 | + def close(self): |
| 942 | + """Flush and close the file. |
| 943 | +
|
| 944 | + May be called more than once without error. Once the file is |
| 945 | + closed, any other operation on it will raise a ValueError. |
| 946 | + """ |
| 947 | + if self.closed: |
| 948 | + return |
| 949 | + try: |
| 950 | + if self._mode == _MODE_READ: |
| 951 | + self._buffer.close() |
| 952 | + self._buffer = None |
| 953 | + elif self._mode == _MODE_WRITE: |
| 954 | + self._fp.write(self._compressor.flush()) |
| 955 | + self._compressor = None |
| 956 | + finally: |
| 957 | + try: |
| 958 | + if self._closefp: |
| 959 | + self._fp.close() |
| 960 | + finally: |
| 961 | + self._fp = None |
| 962 | + self._closefp = False |
| 963 | + |
| 964 | + @property |
| 965 | + def closed(self): |
| 966 | + """True if this file is closed.""" |
| 967 | + return self._fp is None |
| 968 | + |
| 969 | + @property |
| 970 | + def name(self): |
| 971 | + self._check_not_closed() |
| 972 | + return self._fp.name |
| 973 | + |
| 974 | + @property |
| 975 | + def mode(self): |
| 976 | + return 'wb' if self._mode == _MODE_WRITE else 'rb' |
| 977 | + |
| 978 | + def fileno(self): |
| 979 | + """Return the file descriptor for the underlying file.""" |
| 980 | + self._check_not_closed() |
| 981 | + return self._fp.fileno() |
| 982 | + |
| 983 | + def seekable(self): |
| 984 | + """Return whether the file supports seeking.""" |
| 985 | + return self.readable() and self._buffer.seekable() |
| 986 | + |
| 987 | + def readable(self): |
| 988 | + """Return whether the file was opened for reading.""" |
| 989 | + self._check_not_closed() |
| 990 | + return self._mode == _MODE_READ |
| 991 | + |
| 992 | + def writable(self): |
| 993 | + """Return whether the file was opened for writing.""" |
| 994 | + self._check_not_closed() |
| 995 | + return self._mode == _MODE_WRITE |
| 996 | + |
| 997 | + def peek(self, size=-1): |
| 998 | + """Return buffered data without advancing the file position. |
| 999 | +
|
| 1000 | + Always returns at least one byte of data, unless at EOF. |
| 1001 | + The exact number of bytes returned is unspecified. |
| 1002 | + """ |
| 1003 | + self._check_can_read() |
| 1004 | + # Relies on the undocumented fact that BufferedReader.peek() always |
| 1005 | + # returns at least one byte (except at EOF) |
| 1006 | + return self._buffer.peek(size) |
| 1007 | + |
| 1008 | + def read(self, size=-1): |
| 1009 | + """Read up to size uncompressed bytes from the file. |
| 1010 | +
|
| 1011 | + If size is negative or omitted, read until EOF is reached. |
| 1012 | + Returns b"" if the file is already at EOF. |
| 1013 | + """ |
| 1014 | + self._check_can_read() |
| 1015 | + return self._buffer.read(size) |
| 1016 | + |
| 1017 | + def read1(self, size=-1): |
| 1018 | + """Read up to size uncompressed bytes, while trying to avoid |
| 1019 | + making multiple reads from the underlying stream. Reads up to a |
| 1020 | + buffer's worth of data if size is negative. |
| 1021 | +
|
| 1022 | + Returns b"" if the file is at EOF. |
| 1023 | + """ |
| 1024 | + self._check_can_read() |
| 1025 | + if size < 0: |
| 1026 | + size = io.DEFAULT_BUFFER_SIZE |
| 1027 | + return self._buffer.read1(size) |
| 1028 | + |
| 1029 | + def readline(self, size=-1): |
| 1030 | + """Read a line of uncompressed bytes from the file. |
| 1031 | +
|
| 1032 | + The terminating newline (if present) is retained. If size is |
| 1033 | + non-negative, no more than size bytes will be read (in which |
| 1034 | + case the line may be incomplete). Returns b'' if already at EOF. |
| 1035 | + """ |
| 1036 | + self._check_can_read() |
| 1037 | + return self._buffer.readline(size) |
| 1038 | + |
| 1039 | + def write(self, data): |
| 1040 | + """Write a bytes object to the file. |
| 1041 | +
|
| 1042 | + Returns the number of uncompressed bytes written, which is |
| 1043 | + always the length of data in bytes. Note that due to buffering, |
| 1044 | + the file on disk may not reflect the data written until close() |
| 1045 | + is called. |
| 1046 | + """ |
| 1047 | + self._check_can_write() |
| 1048 | + if isinstance(data, (bytes, bytearray)): |
| 1049 | + length = len(data) |
| 1050 | + else: |
| 1051 | + # accept any data that supports the buffer protocol |
| 1052 | + data = memoryview(data) |
| 1053 | + length = data.nbytes |
| 1054 | + |
| 1055 | + compressed = self._compressor.compress(data) |
| 1056 | + self._fp.write(compressed) |
| 1057 | + self._pos += length |
| 1058 | + return length |
| 1059 | + |
| 1060 | + def seek(self, offset, whence=io.SEEK_SET): |
| 1061 | + """Change the file position. |
| 1062 | +
|
| 1063 | + The new position is specified by offset, relative to the |
| 1064 | + position indicated by whence. Possible values for whence are: |
| 1065 | +
|
| 1066 | + 0: start of stream (default): offset must not be negative |
| 1067 | + 1: current stream position |
| 1068 | + 2: end of stream; offset must not be positive |
| 1069 | +
|
| 1070 | + Returns the new file position. |
| 1071 | +
|
| 1072 | + Note that seeking is emulated, so depending on the parameters, |
| 1073 | + this operation may be extremely slow. |
| 1074 | + """ |
| 1075 | + self._check_can_seek() |
| 1076 | + return self._buffer.seek(offset, whence) |
| 1077 | + |
| 1078 | + def tell(self): |
| 1079 | + """Return the current file position.""" |
| 1080 | + self._check_not_closed() |
| 1081 | + if self._mode == _MODE_READ: |
| 1082 | + return self._buffer.tell() |
| 1083 | + return self._pos |
| 1084 | + |
884 | 1085 | class _SharedFile: |
885 | 1086 | def __init__(self, file, pos, close, lock, writing): |
886 | 1087 | self._file = file |
|
0 commit comments