Skip to content

Commit 3a12172

Browse files
authored
Add files via upload
1 parent 62a7999 commit 3a12172

1 file changed

Lines changed: 319 additions & 0 deletions

File tree

pyfoxfile.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:
@@ -5838,6 +5853,310 @@ def UncompressBytesAltFP(fp, formatspecs=__file_format_multi_dict__, filestart=0
58385853
filefp.seek(0, 0)
58395854
return filefp
58405855

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

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

0 commit comments

Comments
 (0)