Skip to content

Commit c46e921

Browse files
authored
Add files via upload
1 parent 7b4abd9 commit c46e921

1 file changed

Lines changed: 319 additions & 0 deletions

File tree

pycatfile.py

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import time
2626
import stat
2727
import zlib
28+
import mmap
2829
import base64
2930
import shutil
3031
import socket
@@ -97,6 +98,14 @@
9798
except NameError: # Py3
9899
unicode = str
99100

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+
100109
def to_text(s, encoding="utf-8", errors="ignore"):
101110
if s is None:
102111
return u""
@@ -163,6 +172,12 @@ def _wrap(stream):
163172
except NameError:
164173
FileNotFoundError = IOError
165174

175+
try:
176+
UnsupportedOperation = io.UnsupportedOperation # Py3
177+
except AttributeError:
178+
class UnsupportedOperation(IOError): # Py2 fallback
179+
pass
180+
166181
# RAR file support
167182
rarfile_support = False
168183
try:
@@ -5800,6 +5815,310 @@ def UncompressBytesAltFP(fp, formatspecs=__file_format_multi_dict__, filestart=0
58005815
filefp.seek(0, 0)
58015816
return filefp
58025817

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

58046123
def CompressOpenFileAlt(fp, compression="auto", compressionlevel=None, compressionuselist=compressionlistalt, formatspecs=__file_format_dict__):
58056124
if(not hasattr(fp, "read")):

0 commit comments

Comments
 (0)