|
25 | 25 | import time |
26 | 26 | import stat |
27 | 27 | import zlib |
| 28 | +import mmap |
28 | 29 | import base64 |
29 | 30 | import shutil |
30 | 31 | import socket |
|
97 | 98 | except NameError: # Py3 |
98 | 99 | unicode = str |
99 | 100 |
|
| 101 | +if PY2: |
| 102 | + # In Py2, 'str' is bytes; define a 'bytes' alias for clarity |
| 103 | + bytes_type = str |
| 104 | + text_type = unicode # noqa: F821 (Py2-only) |
| 105 | +else: |
| 106 | + bytes_type = bytes |
| 107 | + text_type = str |
| 108 | + |
100 | 109 | def to_text(s, encoding="utf-8", errors="ignore"): |
101 | 110 | if s is None: |
102 | 111 | return u"" |
@@ -163,6 +172,12 @@ def _wrap(stream): |
163 | 172 | except NameError: |
164 | 173 | FileNotFoundError = IOError |
165 | 174 |
|
| 175 | +try: |
| 176 | + UnsupportedOperation = io.UnsupportedOperation # Py3 |
| 177 | +except AttributeError: |
| 178 | + class UnsupportedOperation(IOError): # Py2 fallback |
| 179 | + pass |
| 180 | + |
166 | 181 | # RAR file support |
167 | 182 | rarfile_support = False |
168 | 183 | try: |
@@ -5786,6 +5801,310 @@ def UncompressBytesAltFP(fp, formatspecs=__file_format_multi_dict__, filestart=0 |
5786 | 5801 | filefp.seek(0, 0) |
5787 | 5802 | return filefp |
5788 | 5803 |
|
| 5804 | +def _extract_base_fp(obj): |
| 5805 | + """Return deepest file-like with a working fileno(), or None.""" |
| 5806 | + seen = set() |
| 5807 | + cur = obj |
| 5808 | + while cur and id(cur) not in seen: |
| 5809 | + seen.add(id(cur)) |
| 5810 | + fileno = getattr(cur, "fileno", None) |
| 5811 | + if callable(fileno): |
| 5812 | + try: |
| 5813 | + fileno() # probe |
| 5814 | + return cur |
| 5815 | + except Exception: |
| 5816 | + pass |
| 5817 | + # Walk common wrappers (gzip/bz2/lzma/TextIOWrapper/Buffered* etc.) |
| 5818 | + for attr in ("fileobj", "fp", "_fp", "buffer", "raw"): |
| 5819 | + nxt = getattr(cur, attr, None) |
| 5820 | + if nxt is not None and id(nxt) not in seen: |
| 5821 | + cur = nxt |
| 5822 | + break |
| 5823 | + else: |
| 5824 | + cur = None |
| 5825 | + return None |
| 5826 | + |
| 5827 | + |
| 5828 | +class FileLikeAdapter(object): |
| 5829 | + """ |
| 5830 | + Py2/3-compatible file-like adapter that can wrap: |
| 5831 | + - BytesIO / memory buffers |
| 5832 | + - real files |
| 5833 | + - compressed streams (gzip/bz2/lzma/...) |
| 5834 | + - an (fp, mmap) pair for uncompressed random-access speed |
| 5835 | + |
| 5836 | + Bytes-only API. Honors mode ("rb", "wb", "r+b", etc.). |
| 5837 | + """ |
| 5838 | + |
| 5839 | + def __init__(self, fp_like, mode="rb", mm=None, name=None): |
| 5840 | + self._fp = fp_like # underlying stream (BytesIO/file/gzip/...) |
| 5841 | + self._mm = mm # optional memory map for uncompressed files |
| 5842 | + self._pos = 0 # mapping position when using _mm |
| 5843 | + self._mode = mode |
| 5844 | + self.name = name if name is not None else getattr(fp_like, "name", None) |
| 5845 | + self._closed = False |
| 5846 | + |
| 5847 | + # permissions (simple flags from mode) |
| 5848 | + self._readable = ("r" in mode) or ("+" in mode) |
| 5849 | + self._writable = ("w" in mode) or ("a" in mode) or ("x" in mode) or ("+" in mode) |
| 5850 | + |
| 5851 | + # Accept write_through attr; ignore (compat shim) |
| 5852 | + self.write_through = False |
| 5853 | + |
| 5854 | + # ---- capability flags ---- |
| 5855 | + def readable(self): |
| 5856 | + return bool(self._readable) |
| 5857 | + |
| 5858 | + def writable(self): |
| 5859 | + return bool(self._writable) |
| 5860 | + |
| 5861 | + def seekable(self): |
| 5862 | + if self._mm is not None: |
| 5863 | + return True |
| 5864 | + s = getattr(self._fp, "seekable", None) |
| 5865 | + if callable(s): |
| 5866 | + try: |
| 5867 | + return bool(s()) |
| 5868 | + except Exception: |
| 5869 | + return hasattr(self._fp, "seek") |
| 5870 | + return hasattr(self._fp, "seek") |
| 5871 | + |
| 5872 | + @property |
| 5873 | + def closed(self): |
| 5874 | + base_closed = getattr(self._fp, "closed", None) |
| 5875 | + return bool(base_closed) or self._closed |
| 5876 | + |
| 5877 | + # ---- position ---- |
| 5878 | + def tell(self): |
| 5879 | + if self._mm is not None: |
| 5880 | + return self._pos |
| 5881 | + return self._fp.tell() |
| 5882 | + |
| 5883 | + def seek(self, offset, whence=io.SEEK_SET): |
| 5884 | + if self._mm is None: |
| 5885 | + return self._fp.seek(offset, whence) |
| 5886 | + |
| 5887 | + if whence == io.SEEK_SET: |
| 5888 | + new = offset |
| 5889 | + elif whence == io.SEEK_CUR: |
| 5890 | + new = self._pos + offset |
| 5891 | + elif whence == io.SEEK_END: |
| 5892 | + new = len(self._mm) + offset |
| 5893 | + else: |
| 5894 | + raise ValueError("bad whence") |
| 5895 | + |
| 5896 | + if not (0 <= new <= len(self._mm)): |
| 5897 | + raise ValueError("seek out of range") |
| 5898 | + self._pos = new |
| 5899 | + return self._pos |
| 5900 | + |
| 5901 | + # ---- reads ---- |
| 5902 | + def read(self, n=-1): |
| 5903 | + if not self._readable: |
| 5904 | + raise UnsupportedOperation("not readable") |
| 5905 | + |
| 5906 | + if self._mm is None: |
| 5907 | + return self._fp.read(n) |
| 5908 | + |
| 5909 | + if n is None or n < 0: |
| 5910 | + n = len(self._mm) - self._pos |
| 5911 | + end = min(self._pos + n, len(self._mm)) |
| 5912 | + if end <= self._pos: |
| 5913 | + return b"" if not PY2 else bytes_type() |
| 5914 | + # In Py2, bytes(self._mm[slice]) returns str (bytes); fine. |
| 5915 | + out = bytes(self._mm[self._pos:end]) |
| 5916 | + self._pos = end |
| 5917 | + return out |
| 5918 | + |
| 5919 | + def readinto(self, b): |
| 5920 | + if not self._readable: |
| 5921 | + raise UnsupportedOperation("not readable") |
| 5922 | + |
| 5923 | + if self._mm is None: |
| 5924 | + readinto = getattr(self._fp, "readinto", None) |
| 5925 | + if callable(readinto): |
| 5926 | + return readinto(b) |
| 5927 | + # Emulate readinto if missing (common on Py2 wrappers) |
| 5928 | + data = self._fp.read(len(b)) |
| 5929 | + if not data: |
| 5930 | + return 0 |
| 5931 | + mv = memoryview(b) |
| 5932 | + n = min(len(mv), len(data)) |
| 5933 | + mv[:n] = data[:n] |
| 5934 | + return n |
| 5935 | + |
| 5936 | + mv = memoryview(b) |
| 5937 | + remaining = len(self._mm) - self._pos |
| 5938 | + n = min(len(mv), remaining) |
| 5939 | + if n <= 0: |
| 5940 | + return 0 |
| 5941 | + mv[:n] = self._mm[self._pos:self._pos + n] |
| 5942 | + self._pos += n |
| 5943 | + return n |
| 5944 | + |
| 5945 | + def readline(self, limit=-1): |
| 5946 | + if not self._readable: |
| 5947 | + raise UnsupportedOperation("not readable") |
| 5948 | + |
| 5949 | + if self._mm is None: |
| 5950 | + return self._fp.readline(limit) |
| 5951 | + |
| 5952 | + if limit is not None and limit >= 0: |
| 5953 | + end_limit = min(self._pos + limit, len(self._mm)) |
| 5954 | + else: |
| 5955 | + end_limit = len(self._mm) |
| 5956 | + |
| 5957 | + nl = self._mm.find(b"\n", self._pos, end_limit) |
| 5958 | + if nl == -1: |
| 5959 | + end = end_limit |
| 5960 | + else: |
| 5961 | + end = nl + 1 |
| 5962 | + out = bytes(self._mm[self._pos:end]) |
| 5963 | + self._pos = end |
| 5964 | + return out |
| 5965 | + |
| 5966 | + def readlines(self, hint=-1): |
| 5967 | + lines, total = [], 0 |
| 5968 | + while True: |
| 5969 | + line = self.readline() |
| 5970 | + if not line: |
| 5971 | + break |
| 5972 | + lines.append(line) |
| 5973 | + total += len(line) |
| 5974 | + if hint >= 0 and total >= hint: |
| 5975 | + break |
| 5976 | + return lines |
| 5977 | + |
| 5978 | + # Iteration (Py2/3) |
| 5979 | + def __iter__(self): |
| 5980 | + return self |
| 5981 | + |
| 5982 | + def __next__(self): |
| 5983 | + line = self.readline() |
| 5984 | + if not line: |
| 5985 | + raise StopIteration |
| 5986 | + return line |
| 5987 | + |
| 5988 | + # Py2 alias |
| 5989 | + if PY2: |
| 5990 | + next = __next__ |
| 5991 | + |
| 5992 | + # ---- writes ---- |
| 5993 | + def write(self, b): |
| 5994 | + if not self._writable: |
| 5995 | + raise UnsupportedOperation("not writable") |
| 5996 | + |
| 5997 | + if not isinstance(b, bytes_type): |
| 5998 | + # for safety, only bytes; caller handles text encoding externally |
| 5999 | + raise TypeError("write() requires bytes") |
| 6000 | + |
| 6001 | + if self._mm is None: |
| 6002 | + return self._fp.write(b) |
| 6003 | + |
| 6004 | + mv = memoryview(b) |
| 6005 | + end = self._pos + len(mv) |
| 6006 | + if end > len(self._mm): |
| 6007 | + raise IOError("write past mapped size; pre-size or resize()") |
| 6008 | + self._mm[self._pos:end] = mv |
| 6009 | + self._pos = end |
| 6010 | + return len(mv) |
| 6011 | + |
| 6012 | + def writelines(self, lines): |
| 6013 | + for line in lines: |
| 6014 | + self.write(line) |
| 6015 | + |
| 6016 | + # ---- durability & size ---- |
| 6017 | + def flush(self): |
| 6018 | + # 1) flush mapping first |
| 6019 | + if self._mm is not None: |
| 6020 | + try: |
| 6021 | + self._mm.flush() |
| 6022 | + except Exception: |
| 6023 | + pass |
| 6024 | + # 2) flush Python/stdio buffers |
| 6025 | + try: |
| 6026 | + self._fp.flush() |
| 6027 | + except Exception: |
| 6028 | + pass |
| 6029 | + # 3) fsync real file if any (skips BytesIO and many compressed) |
| 6030 | + base = _extract_base_fp(self._fp) |
| 6031 | + if base is not None: |
| 6032 | + try: |
| 6033 | + os.fsync(base.fileno()) |
| 6034 | + except Exception: |
| 6035 | + pass |
| 6036 | + |
| 6037 | + def truncate(self, size=None): |
| 6038 | + if self._mm is not None: |
| 6039 | + base = _extract_base_fp(self._fp) |
| 6040 | + if base is None: |
| 6041 | + raise UnsupportedOperation("truncate unsupported for mmapped non-file") |
| 6042 | + if size is None: |
| 6043 | + size = self.tell() |
| 6044 | + # Safe approach across OSes: close map, truncate file, re-map |
| 6045 | + was_pos = self._pos |
| 6046 | + try: |
| 6047 | + self._mm.close() |
| 6048 | + except Exception: |
| 6049 | + pass |
| 6050 | + base.truncate(size) |
| 6051 | + access = mmap.ACCESS_WRITE if self._writable else mmap.ACCESS_READ |
| 6052 | + self._mm = mmap.mmap(base.fileno(), size, access=access) |
| 6053 | + self._pos = min(was_pos, size) |
| 6054 | + return size |
| 6055 | + |
| 6056 | + trunc = getattr(self._fp, "truncate", None) |
| 6057 | + if not callable(trunc): |
| 6058 | + raise UnsupportedOperation("truncate unsupported by underlying object") |
| 6059 | + return trunc(size) |
| 6060 | + |
| 6061 | + # ---- fd/tty ---- |
| 6062 | + def fileno(self): |
| 6063 | + f = getattr(self._fp, "fileno", None) |
| 6064 | + if callable(f): |
| 6065 | + return f() |
| 6066 | + raise UnsupportedOperation("no fileno()") |
| 6067 | + |
| 6068 | + def isatty(self): |
| 6069 | + f = getattr(self._fp, "isatty", None) |
| 6070 | + try: |
| 6071 | + return bool(f()) if callable(f) else False |
| 6072 | + except Exception: |
| 6073 | + return False |
| 6074 | + |
| 6075 | + # ---- close & ctx mgr ---- |
| 6076 | + def close(self): |
| 6077 | + if self._closed: |
| 6078 | + return |
| 6079 | + try: |
| 6080 | + if self._writable: |
| 6081 | + self.flush() |
| 6082 | + finally: |
| 6083 | + if self._mm is not None: |
| 6084 | + try: |
| 6085 | + self._mm.close() |
| 6086 | + except Exception: |
| 6087 | + pass |
| 6088 | + self._mm = None |
| 6089 | + try: |
| 6090 | + self._fp.close() |
| 6091 | + except Exception: |
| 6092 | + pass |
| 6093 | + self._closed = True |
| 6094 | + |
| 6095 | + def __enter__(self): |
| 6096 | + return self |
| 6097 | + |
| 6098 | + def __exit__(self, exc_type, exc, tb): |
| 6099 | + self.close() |
| 6100 | + |
| 6101 | + # Accept write_through sets (compat with your current code) |
| 6102 | + def __setattr__(self, name, value): |
| 6103 | + if name == "write_through": |
| 6104 | + object.__setattr__(self, name, value) |
| 6105 | + return |
| 6106 | + object.__setattr__(self, name, value) |
| 6107 | + |
5789 | 6108 |
|
5790 | 6109 | def CompressOpenFileAlt(fp, compression="auto", compressionlevel=None, compressionuselist=compressionlistalt, formatspecs=__file_format_dict__): |
5791 | 6110 | if(not hasattr(fp, "read")): |
|
0 commit comments