diff options
Diffstat (limited to 'Lib/zipfile.py')
-rw-r--r-- | Lib/zipfile.py | 395 |
1 files changed, 232 insertions, 163 deletions
diff --git a/Lib/zipfile.py b/Lib/zipfile.py index 6e776882539..5b3f6f9603e 100644 --- a/Lib/zipfile.py +++ b/Lib/zipfile.py @@ -1,10 +1,19 @@ """ Read and write ZIP files. + +XXX references to utf-8 need further investigation. """ -import struct, os, time, sys, shutil -import binascii, cStringIO, stat import io +import os import re +import imp +import sys +import time +import stat +import shutil +import struct +import binascii + try: import zlib # We may need its compression method @@ -13,10 +22,10 @@ except ImportError: zlib = None crc32 = binascii.crc32 -__all__ = ["BadZipfile", "error", "ZIP_STORED", "ZIP_DEFLATED", "is_zipfile", - "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile" ] +__all__ = ["BadZipFile", "BadZipfile", "error", "ZIP_STORED", "ZIP_DEFLATED", + "is_zipfile", "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile"] -class BadZipfile(Exception): +class BadZipFile(Exception): pass @@ -26,7 +35,8 @@ class LargeZipFile(Exception): and those extensions are disabled. """ -error = BadZipfile # The exception raised by this module +error = BadZipfile = BadZipFile # Pre-3.2 compatibility names + ZIP64_LIMIT = (1 << 31) - 1 ZIP_FILECOUNT_LIMIT = 1 << 16 @@ -45,8 +55,8 @@ ZIP_DEFLATED = 8 # The "end of central directory" structure, magic number, size, and indices # (section V.I in the format document) -structEndArchive = "<4s4H2LH" -stringEndArchive = "PK\005\006" +structEndArchive = b"<4s4H2LH" +stringEndArchive = b"PK\005\006" sizeEndCentDir = struct.calcsize(structEndArchive) _ECD_SIGNATURE = 0 @@ -65,7 +75,7 @@ _ECD_LOCATION = 9 # The "central directory" structure, magic number, size, and indices # of entries in the structure (section V.F in the format document) structCentralDir = "<4s4B4HL2L5H2L" -stringCentralDir = "PK\001\002" +stringCentralDir = b"PK\001\002" sizeCentralDir = struct.calcsize(structCentralDir) # indexes of entries in the central directory structure @@ -92,7 +102,7 @@ _CD_LOCAL_HEADER_OFFSET = 18 # The "local file header" structure, magic number, size, and indices # (section V.A in the format document) structFileHeader = "<4s2B4HL2L2H" -stringFileHeader = "PK\003\004" +stringFileHeader = b"PK\003\004" sizeFileHeader = struct.calcsize(structFileHeader) _FH_SIGNATURE = 0 @@ -110,13 +120,13 @@ _FH_EXTRA_FIELD_LENGTH = 11 # The "Zip64 end of central directory locator" structure, magic number, and size structEndArchive64Locator = "<4sLQL" -stringEndArchive64Locator = "PK\x06\x07" +stringEndArchive64Locator = b"PK\x06\x07" sizeEndCentDir64Locator = struct.calcsize(structEndArchive64Locator) # The "Zip64 end of central directory" record, magic number, size, and indices # (section V.G in the format document) structEndArchive64 = "<4sQ2H2L4Q" -stringEndArchive64 = "PK\x06\x06" +stringEndArchive64 = b"PK\x06\x06" sizeEndCentDir64 = struct.calcsize(structEndArchive64) _CD64_SIGNATURE = 0 @@ -171,7 +181,7 @@ def _EndRecData64(fpin, offset, endrec): return endrec if diskno != 0 or disks != 1: - raise BadZipfile("zipfiles that span multiple disks are not supported") + raise BadZipFile("zipfiles that span multiple disks are not supported") # Assume no 'zip64 extensible data' fpin.seek(offset - sizeEndCentDir64Locator - sizeEndCentDir64, 2) @@ -211,13 +221,13 @@ def _EndRecData(fpin): except IOError: return None data = fpin.read() - if data[0:4] == stringEndArchive and data[-2:] == "\000\000": + if data[0:4] == stringEndArchive and data[-2:] == b"\000\000": # the signature is correct and there's no comment, unpack structure endrec = struct.unpack(structEndArchive, data) endrec=list(endrec) # Append a blank comment and record start offset - endrec.append("") + endrec.append(b"") endrec.append(filesize - sizeEndCentDir) # Try to read the "Zip64 end of central directory" structure @@ -296,8 +306,8 @@ class ZipInfo (object): # Standard values: self.compress_type = ZIP_STORED # Type of compression for the file - self.comment = "" # Comment for each file - self.extra = "" # ZIP extra data + self.comment = b"" # Comment for each file + self.extra = b"" # ZIP extra data if sys.platform == 'win32': self.create_system = 0 # System which created ZIP archive else: @@ -351,19 +361,10 @@ class ZipInfo (object): return header + filename + extra def _encodeFilenameFlags(self): - if isinstance(self.filename, unicode): - try: - return self.filename.encode('ascii'), self.flag_bits - except UnicodeEncodeError: - return self.filename.encode('utf-8'), self.flag_bits | 0x800 - else: - return self.filename, self.flag_bits - - def _decodeFilename(self): - if self.flag_bits & 0x800: - return self.filename.decode('utf-8') - else: - return self.filename + try: + return self.filename.encode('ascii'), self.flag_bits + except UnicodeEncodeError: + return self.filename.encode('utf-8'), self.flag_bits | 0x800 def _decodeExtra(self): # Try to decode the extra field. @@ -381,20 +382,20 @@ class ZipInfo (object): elif ln == 0: counts = () else: - raise RuntimeError, "Corrupt extra field %s"%(ln,) + raise RuntimeError("Corrupt extra field %s"%(ln,)) idx = 0 # ZIP64 extension (large files and/or large archives) - if self.file_size in (0xffffffffffffffffL, 0xffffffffL): + if self.file_size in (0xffffffffffffffff, 0xffffffff): self.file_size = counts[idx] idx += 1 - if self.compress_size == 0xFFFFFFFFL: + if self.compress_size == 0xFFFFFFFF: self.compress_size = counts[idx] idx += 1 - if self.header_offset == 0xffffffffL: + if self.header_offset == 0xffffffff: old = self.header_offset self.header_offset = counts[idx] idx+=1 @@ -437,7 +438,7 @@ class _ZipDecrypter: def _crc32(self, ch, crc): """Compute the CRC32 primitive on one byte.""" - return ((crc >> 8) & 0xffffff) ^ self.crctable[(crc ^ ord(ch)) & 0xff] + return ((crc >> 8) & 0xffffff) ^ self.crctable[(crc ^ ch) & 0xff] def __init__(self, pwd): self.key0 = 305419896 @@ -450,14 +451,13 @@ class _ZipDecrypter: self.key0 = self._crc32(c, self.key0) self.key1 = (self.key1 + (self.key0 & 255)) & 4294967295 self.key1 = (self.key1 * 134775813 + 1) & 4294967295 - self.key2 = self._crc32(chr((self.key1 >> 24) & 255), self.key2) + self.key2 = self._crc32((self.key1 >> 24) & 255, self.key2) def __call__(self, c): """Decrypt a single character.""" - c = ord(c) + assert isinstance(c, int) k = self.key2 | 2 c = c ^ (((k * (k^1)) >> 8) & 255) - c = chr(c) self._UpdateKeys(c) return c @@ -495,10 +495,10 @@ class ZipExtFile(io.BufferedIOBase): MIN_READ_SIZE = 4096 # Search for universal newlines or line chunks. - PATTERN = re.compile(r'^(?P<chunk>[^\r\n]+)|(?P<newline>\n|\r\n?)') + PATTERN = re.compile(br'^(?P<chunk>[^\r\n]+)|(?P<newline>\n|\r\n?)') def __init__(self, fileobj, mode, zipinfo, decrypter=None, - close_fileobj=False): + close_fileobj=False): self._fileobj = fileobj self._decrypter = decrypter self._close_fileobj = close_fileobj @@ -515,9 +515,9 @@ class ZipExtFile(io.BufferedIOBase): raise NotImplementedError("compression type %d (%s)" % (self._compress_type, descr)) else: raise NotImplementedError("compression type %d" % (self._compress_type,)) - self._unconsumed = '' + self._unconsumed = b'' - self._readbuffer = '' + self._readbuffer = b'' self._offset = 0 self._universal = 'U' in mode @@ -545,7 +545,7 @@ class ZipExtFile(io.BufferedIOBase): if not self._universal and limit < 0: # Shortcut common case - newline found in buffer. - i = self._readbuffer.find('\n', self._offset) + 1 + i = self._readbuffer.find(b'\n', self._offset) + 1 if i > 0: line = self._readbuffer[self._offset: i] self._offset = i @@ -554,10 +554,10 @@ class ZipExtFile(io.BufferedIOBase): if not self._universal: return io.BufferedIOBase.readline(self, limit) - line = '' + line = b'' while limit < 0 or len(line) < limit: readahead = self.peek(2) - if readahead == '': + if readahead == b'': return line # @@ -576,7 +576,7 @@ class ZipExtFile(io.BufferedIOBase): if newline not in self.newlines: self.newlines.append(newline) self._offset += len(newline) - return line + '\n' + return line + b'\n' chunk = match.group('chunk') if limit >= 0: @@ -603,7 +603,7 @@ class ZipExtFile(io.BufferedIOBase): """Read and return up to n bytes. If the argument is omitted, None, or negative, data is read and returned until EOF is reached.. """ - buf = '' + buf = b'' if n is None: n = -1 while True: @@ -625,7 +625,7 @@ class ZipExtFile(io.BufferedIOBase): self._running_crc = crc32(newdata, self._running_crc) & 0xffffffff # Check the CRC if we're at the end of the file if eof and self._running_crc != self._expected_crc: - raise BadZipfile("Bad CRC-32 for file %r" % self.name) + raise BadZipFile("Bad CRC-32 for file %r" % self.name) def read1(self, n): """Read up to n bytes with at most one read() system call.""" @@ -647,7 +647,7 @@ class ZipExtFile(io.BufferedIOBase): self._compress_left -= len(data) if data and self._decrypter is not None: - data = ''.join(map(self._decrypter, data)) + data = bytes(map(self._decrypter, data)) if self._compress_type == ZIP_STORED: self._update_crc(data, eof=(self._compress_left==0)) @@ -680,14 +680,14 @@ class ZipExtFile(io.BufferedIOBase): return data def close(self): - try : + try: if self._close_fileobj: self._fileobj.close() finally: - super(ZipExtFile, self).close() + super().close() -class ZipFile(object): +class ZipFile: """ Class with methods to open, read, write, close, list zip files. z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=False) @@ -713,10 +713,10 @@ class ZipFile(object): pass elif compression == ZIP_DEFLATED: if not zlib: - raise RuntimeError,\ - "Compression requires the (missing) zlib module" + raise RuntimeError( + "Compression requires the (missing) zlib module") else: - raise RuntimeError, "That compression method is not supported" + raise RuntimeError("That compression method is not supported") self._allowZip64 = allowZip64 self._didModify = False @@ -726,19 +726,20 @@ class ZipFile(object): self.compression = compression # Method of compression self.mode = key = mode.replace('b', '')[0] self.pwd = None - self._comment = '' + self._comment = b'' # Check if we were passed a file-like object - if isinstance(file, basestring): + if isinstance(file, str): + # No, it's a filename self._filePassed = 0 self.filename = file modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'} try: - self.fp = open(file, modeDict[mode]) + self.fp = io.open(file, modeDict[mode]) except IOError: if mode == 'a': mode = key = 'w' - self.fp = open(file, modeDict[mode]) + self.fp = io.open(file, modeDict[mode]) else: raise else: @@ -759,7 +760,7 @@ class ZipFile(object): self._RealGetContents() # seek to start of directory and overwrite self.fp.seek(self.start_dir, 0) - except BadZipfile: + except BadZipFile: # file is not a zip file, just append self.fp.seek(0, 2) @@ -787,11 +788,11 @@ class ZipFile(object): try: endrec = _EndRecData(fp) except IOError: - raise BadZipfile("File is not a zip file") + raise BadZipFile("File is not a zip file") if not endrec: - raise BadZipfile, "File is not a zip file" + raise BadZipFile("File is not a zip file") if self.debug > 1: - print endrec + print(endrec) size_cd = endrec[_ECD_SIZE] # bytes in central directory offset_cd = endrec[_ECD_OFFSET] # offset of central directory self._comment = endrec[_ECD_COMMENT] # archive comment @@ -804,21 +805,28 @@ class ZipFile(object): if self.debug > 2: inferred = concat + offset_cd - print "given, inferred, offset", offset_cd, inferred, concat + print("given, inferred, offset", offset_cd, inferred, concat) # self.start_dir: Position of start of central directory self.start_dir = offset_cd + concat fp.seek(self.start_dir, 0) data = fp.read(size_cd) - fp = cStringIO.StringIO(data) + fp = io.BytesIO(data) total = 0 while total < size_cd: centdir = fp.read(sizeCentralDir) if centdir[0:4] != stringCentralDir: - raise BadZipfile, "Bad magic number for central directory" + raise BadZipFile("Bad magic number for central directory") centdir = struct.unpack(structCentralDir, centdir) if self.debug > 2: - print centdir + print(centdir) filename = fp.read(centdir[_CD_FILENAME_LENGTH]) + flags = centdir[5] + if flags & 0x800: + # UTF-8 file names extension + filename = filename.decode('utf-8') + else: + # Historical ZIP filename encoding + filename = filename.decode('cp437') # Create ZipInfo instance to store file information x = ZipInfo(filename) x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH]) @@ -835,7 +843,6 @@ class ZipFile(object): x._decodeExtra() x.header_offset = x.header_offset + concat - x.filename = x._decodeFilename() self.filelist.append(x) self.NameToInfo[x.filename] = x @@ -845,7 +852,7 @@ class ZipFile(object): + centdir[_CD_COMMENT_LENGTH]) if self.debug > 2: - print "total", total + print("total", total) def namelist(self): @@ -860,12 +867,14 @@ class ZipFile(object): archive.""" return self.filelist - def printdir(self): + def printdir(self, file=None): """Print a table of contents for the zip file.""" - print "%-46s %19s %12s" % ("File Name", "Modified ", "Size") + print("%-46s %19s %12s" % ("File Name", "Modified ", "Size"), + file=file) for zinfo in self.filelist: date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time[:6] - print "%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size) + print("%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size), + file=file) def testzip(self): """Read all the files and check the CRC.""" @@ -877,7 +886,7 @@ class ZipFile(object): with self.open(zinfo.filename, "r") as f: while f.read(chunk_size): # Check CRC-32 pass - except BadZipfile: + except BadZipFile: return zinfo.filename def getinfo(self, name): @@ -891,7 +900,12 @@ class ZipFile(object): def setpassword(self, pwd): """Set default password for encrypted files.""" - self.pwd = pwd + if pwd and not isinstance(pwd, bytes): + raise TypeError("pwd: expected bytes, got %s" % type(pwd)) + if pwd: + self.pwd = pwd + else: + self.pwd = None @property def comment(self): @@ -900,6 +914,8 @@ class ZipFile(object): @comment.setter def comment(self, comment): + if not isinstance(comment, bytes): + raise TypeError("comment: expected bytes, got %s" % type(comment)) # check for valid comment length if len(comment) >= ZIP_MAX_COMMENT: if self.debug: @@ -911,24 +927,25 @@ class ZipFile(object): def read(self, name, pwd=None): """Return file bytes (as a string) for name.""" - return self.open(name, "r", pwd).read() + with self.open(name, "r", pwd) as fp: + return fp.read() def open(self, name, mode="r", pwd=None): """Return file-like object for 'name'.""" if mode not in ("r", "U", "rU"): - raise RuntimeError, 'open() requires mode "r", "U", or "rU"' + raise RuntimeError('open() requires mode "r", "U", or "rU"') + if pwd and not isinstance(pwd, bytes): + raise TypeError("pwd: expected bytes, got %s" % type(pwd)) if not self.fp: - raise RuntimeError, \ - "Attempt to read ZIP archive that was already closed" + raise RuntimeError( + "Attempt to read ZIP archive that was already closed") # Only open a new file for instances where we were not # given a file object in the constructor if self._filePassed: zef_file = self.fp - should_close = False else: - zef_file = open(self.filename, 'rb') - should_close = True + zef_file = io.open(self.filename, 'rb') try: # Make sure we have an info object @@ -938,23 +955,28 @@ class ZipFile(object): else: # Get info object for name zinfo = self.getinfo(name) - zef_file.seek(zinfo.header_offset, 0) # Skip the file header: fheader = zef_file.read(sizeFileHeader) if fheader[0:4] != stringFileHeader: - raise BadZipfile, "Bad magic number for file header" + raise BadZipFile("Bad magic number for file header") fheader = struct.unpack(structFileHeader, fheader) fname = zef_file.read(fheader[_FH_FILENAME_LENGTH]) if fheader[_FH_EXTRA_FIELD_LENGTH]: zef_file.read(fheader[_FH_EXTRA_FIELD_LENGTH]) - if fname != zinfo.orig_filename: - raise BadZipfile, \ - 'File name in directory "%s" and header "%s" differ.' % ( - zinfo.orig_filename, fname) + if zinfo.flag_bits & 0x800: + # UTF-8 filename + fname_str = fname.decode("utf-8") + else: + fname_str = fname.decode("cp437") + + if fname_str != zinfo.orig_filename: + raise BadZipFile( + 'File name in directory %r and header %r differ.' + % (zinfo.orig_filename, fname)) # check for encrypted flag & handle password is_encrypted = zinfo.flag_bits & 0x1 @@ -963,8 +985,8 @@ class ZipFile(object): if not pwd: pwd = self.pwd if not pwd: - raise RuntimeError, "File %s is encrypted, " \ - "password required for extraction" % name + raise RuntimeError("File %s is encrypted, password " + "required for extraction" % name) zd = _ZipDecrypter(pwd) # The first 12 bytes in the cypher stream is an encryption header @@ -972,21 +994,21 @@ class ZipFile(object): # completely random, while the 12th contains the MSB of the CRC, # or the MSB of the file time depending on the header type # and is used to check the correctness of the password. - bytes = zef_file.read(12) - h = map(zd, bytes[0:12]) + header = zef_file.read(12) + h = list(map(zd, header[0:12])) if zinfo.flag_bits & 0x8: # compare against the file type from extended local headers check_byte = (zinfo._raw_time >> 8) & 0xff else: # compare against the CRC otherwise check_byte = (zinfo.CRC >> 24) & 0xff - if ord(h[11]) != check_byte: + if h[11] != check_byte: raise RuntimeError("Bad password for file", name) return ZipExtFile(zef_file, mode, zinfo, zd, - close_fileobj=should_close) + close_fileobj=not self._filePassed) except: - if should_close: + if not self._filePassed: zef_file.close() raise @@ -1046,7 +1068,7 @@ class ZipFile(object): return targetpath with self.open(member, pwd=pwd) as source, \ - file(targetpath, "wb") as target: + open(targetpath, "wb") as target: shutil.copyfileobj(source, target) return targetpath @@ -1055,24 +1077,24 @@ class ZipFile(object): """Check for errors before writing a file to the archive.""" if zinfo.filename in self.NameToInfo: if self.debug: # Warning for duplicate names - print "Duplicate name:", zinfo.filename + print("Duplicate name:", zinfo.filename) if self.mode not in ("w", "a"): - raise RuntimeError, 'write() requires mode "w" or "a"' + raise RuntimeError('write() requires mode "w" or "a"') if not self.fp: - raise RuntimeError, \ - "Attempt to write ZIP archive that was already closed" + raise RuntimeError( + "Attempt to write ZIP archive that was already closed") if zinfo.compress_type == ZIP_DEFLATED and not zlib: - raise RuntimeError, \ - "Compression requires the (missing) zlib module" + raise RuntimeError( + "Compression requires the (missing) zlib module") if zinfo.compress_type not in (ZIP_STORED, ZIP_DEFLATED): - raise RuntimeError, \ - "That compression method is not supported" + raise RuntimeError("That compression method is not supported") if zinfo.file_size > ZIP64_LIMIT: if not self._allowZip64: raise LargeZipFile("Filesize would require ZIP64 extensions") if zinfo.header_offset > ZIP64_LIMIT: if not self._allowZip64: - raise LargeZipFile("Zipfile size would require ZIP64 extensions") + raise LargeZipFile( + "Zipfile size would require ZIP64 extensions") def write(self, filename, arcname=None, compress_type=None): """Put the bytes from filename into the archive under the name @@ -1094,7 +1116,7 @@ class ZipFile(object): if isdir: arcname += '/' zinfo = ZipInfo(arcname, date_time) - zinfo.external_attr = (st[0] & 0xFFFF) << 16L # Unix attributes + zinfo.external_attr = (st[0] & 0xFFFF) << 16 # Unix attributes if compress_type is None: zinfo.compress_type = self.compression else: @@ -1155,16 +1177,19 @@ class ZipFile(object): self.filelist.append(zinfo) self.NameToInfo[zinfo.filename] = zinfo - def writestr(self, zinfo_or_arcname, bytes, compress_type=None): - """Write a file into the archive. The contents is the string - 'bytes'. 'zinfo_or_arcname' is either a ZipInfo instance or + def writestr(self, zinfo_or_arcname, data, compress_type=None): + """Write a file into the archive. The contents is 'data', which + may be either a 'str' or a 'bytes' instance; if it is a 'str', + it is encoded as UTF-8 first. + 'zinfo_or_arcname' is either a ZipInfo instance or the name of the file in the archive.""" + if isinstance(data, str): + data = data.encode("utf-8") if not isinstance(zinfo_or_arcname, ZipInfo): zinfo = ZipInfo(filename=zinfo_or_arcname, date_time=time.localtime(time.time())[:6]) - zinfo.compress_type = self.compression - zinfo.external_attr = 0600 << 16 + zinfo.external_attr = 0o600 << 16 else: zinfo = zinfo_or_arcname @@ -1172,24 +1197,24 @@ class ZipFile(object): raise RuntimeError( "Attempt to write to ZIP archive that was already closed") + zinfo.file_size = len(data) # Uncompressed size + zinfo.header_offset = self.fp.tell() # Start of header data if compress_type is not None: zinfo.compress_type = compress_type - zinfo.file_size = len(bytes) # Uncompressed size - zinfo.header_offset = self.fp.tell() # Start of header bytes self._writecheck(zinfo) self._didModify = True - zinfo.CRC = crc32(bytes) & 0xffffffff # CRC-32 checksum + zinfo.CRC = crc32(data) & 0xffffffff # CRC-32 checksum if zinfo.compress_type == ZIP_DEFLATED: co = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15) - bytes = co.compress(bytes) + co.flush() - zinfo.compress_size = len(bytes) # Compressed size + data = co.compress(data) + co.flush() + zinfo.compress_size = len(data) # Compressed size else: zinfo.compress_size = zinfo.file_size - zinfo.header_offset = self.fp.tell() # Start of header bytes + zinfo.header_offset = self.fp.tell() # Start of header data self.fp.write(zinfo.FileHeader()) - self.fp.write(bytes) + self.fp.write(data) self.fp.flush() if zinfo.flag_bits & 0x08: # Write CRC and file sizes after the file data @@ -1230,7 +1255,7 @@ class ZipFile(object): if zinfo.header_offset > ZIP64_LIMIT: extra.append(zinfo.header_offset) - header_offset = 0xffffffffL + header_offset = 0xffffffff else: header_offset = zinfo.header_offset @@ -1250,22 +1275,21 @@ class ZipFile(object): try: filename, flag_bits = zinfo._encodeFilenameFlags() centdir = struct.pack(structCentralDir, - stringCentralDir, create_version, - zinfo.create_system, extract_version, zinfo.reserved, - flag_bits, zinfo.compress_type, dostime, dosdate, - zinfo.CRC, compress_size, file_size, - len(filename), len(extra_data), len(zinfo.comment), - 0, zinfo.internal_attr, zinfo.external_attr, - header_offset) + stringCentralDir, create_version, + zinfo.create_system, extract_version, zinfo.reserved, + flag_bits, zinfo.compress_type, dostime, dosdate, + zinfo.CRC, compress_size, file_size, + len(filename), len(extra_data), len(zinfo.comment), + 0, zinfo.internal_attr, zinfo.external_attr, + header_offset) except DeprecationWarning: - print >>sys.stderr, (structCentralDir, - stringCentralDir, create_version, - zinfo.create_system, extract_version, zinfo.reserved, - zinfo.flag_bits, zinfo.compress_type, dostime, dosdate, - zinfo.CRC, compress_size, file_size, - len(zinfo.filename), len(extra_data), len(zinfo.comment), - 0, zinfo.internal_attr, zinfo.external_attr, - header_offset) + print((structCentralDir, stringCentralDir, create_version, + zinfo.create_system, extract_version, zinfo.reserved, + zinfo.flag_bits, zinfo.compress_type, dostime, dosdate, + zinfo.CRC, compress_size, file_size, + len(zinfo.filename), len(extra_data), len(zinfo.comment), + 0, zinfo.internal_attr, zinfo.external_attr, + header_offset), file=sys.stderr) raise self.fp.write(centdir) self.fp.write(filename) @@ -1311,7 +1335,13 @@ class ZipFile(object): class PyZipFile(ZipFile): """Class to create ZIP archives with Python library files and packages.""" - def writepy(self, pathname, basename = ""): + def __init__(self, file, mode="r", compression=ZIP_STORED, + allowZip64=False, optimize=-1): + ZipFile.__init__(self, file, mode=mode, compression=compression, + allowZip64=allowZip64) + self._optimize = optimize + + def writepy(self, pathname, basename=""): """Add all files from "pathname" to the ZIP archive. If pathname is a package directory, search the directory and @@ -1333,10 +1363,10 @@ class PyZipFile(ZipFile): else: basename = name if self.debug: - print "Adding package in", pathname, "as", basename + print("Adding package in", pathname, "as", basename) fname, arcname = self._get_codename(initname[0:-3], basename) if self.debug: - print "Adding", arcname + print("Adding", arcname) self.write(fname, arcname) dirlist = os.listdir(pathname) dirlist.remove("__init__.py") @@ -1352,12 +1382,12 @@ class PyZipFile(ZipFile): fname, arcname = self._get_codename(path[0:-3], basename) if self.debug: - print "Adding", arcname + print("Adding", arcname) self.write(fname, arcname) else: # This is NOT a package directory, add its files at top level if self.debug: - print "Adding files from directory", pathname + print("Adding files from directory", pathname) for filename in os.listdir(pathname): path = os.path.join(pathname, filename) root, ext = os.path.splitext(filename) @@ -1365,15 +1395,15 @@ class PyZipFile(ZipFile): fname, arcname = self._get_codename(path[0:-3], basename) if self.debug: - print "Adding", arcname + print("Adding", arcname) self.write(fname, arcname) else: if pathname[-3:] != ".py": - raise RuntimeError, \ - 'Files added with writepy() must end with ".py"' + raise RuntimeError( + 'Files added with writepy() must end with ".py"') fname, arcname = self._get_codename(pathname[0:-3], basename) if self.debug: - print "Adding file", arcname + print("Adding file", arcname) self.write(fname, arcname) def _get_codename(self, pathname, basename): @@ -1383,25 +1413,64 @@ class PyZipFile(ZipFile): archive name, compiling if necessary. For example, given /python/lib/string, return (/python/lib/string.pyc, string). """ - file_py = pathname + ".py" - file_pyc = pathname + ".pyc" - file_pyo = pathname + ".pyo" - if os.path.isfile(file_pyo) and \ - os.stat(file_pyo).st_mtime >= os.stat(file_py).st_mtime: - fname = file_pyo # Use .pyo file - elif not os.path.isfile(file_pyc) or \ - os.stat(file_pyc).st_mtime < os.stat(file_py).st_mtime: + def _compile(file, optimize=-1): import py_compile if self.debug: - print "Compiling", file_py + print("Compiling", file) try: - py_compile.compile(file_py, file_pyc, None, True) - except py_compile.PyCompileError,err: - print err.msg - fname = file_pyc + py_compile.compile(file, doraise=True, optimize=optimize) + except py_compile.PyCompileError as error: + print(err.msg) + return False + return True + + file_py = pathname + ".py" + file_pyc = pathname + ".pyc" + file_pyo = pathname + ".pyo" + pycache_pyc = imp.cache_from_source(file_py, True) + pycache_pyo = imp.cache_from_source(file_py, False) + if self._optimize == -1: + # legacy mode: use whatever file is present + if (os.path.isfile(file_pyo) and + os.stat(file_pyo).st_mtime >= os.stat(file_py).st_mtime): + # Use .pyo file. + arcname = fname = file_pyo + elif (os.path.isfile(file_pyc) and + os.stat(file_pyc).st_mtime >= os.stat(file_py).st_mtime): + # Use .pyc file. + arcname = fname = file_pyc + elif (os.path.isfile(pycache_pyc) and + os.stat(pycache_pyc).st_mtime >= os.stat(file_py).st_mtime): + # Use the __pycache__/*.pyc file, but write it to the legacy pyc + # file name in the archive. + fname = pycache_pyc + arcname = file_pyc + elif (os.path.isfile(pycache_pyo) and + os.stat(pycache_pyo).st_mtime >= os.stat(file_py).st_mtime): + # Use the __pycache__/*.pyo file, but write it to the legacy pyo + # file name in the archive. + fname = pycache_pyo + arcname = file_pyo + else: + # Compile py into PEP 3147 pyc file. + if _compile(file_py): + fname = (pycache_pyc if __debug__ else pycache_pyo) + arcname = (file_pyc if __debug__ else file_pyo) + else: + fname = arcname = file_py else: - fname = file_pyc - archivename = os.path.split(fname)[1] + # new mode: use given optimization level + if self._optimize == 0: + fname = pycache_pyc + arcname = file_pyc + else: + fname = pycache_pyo + arcname = file_pyo + if not (os.path.isfile(fname) and + os.stat(fname).st_mtime >= os.stat(file_py).st_mtime): + if not _compile(file_py, optimize=self._optimize): + fname = arcname = file_py + archivename = os.path.split(arcname)[1] if basename: archivename = "%s/%s" % (basename, archivename) return (fname, archivename) @@ -1420,29 +1489,29 @@ def main(args = None): args = sys.argv[1:] if not args or args[0] not in ('-l', '-c', '-e', '-t'): - print USAGE + print(USAGE) sys.exit(1) if args[0] == '-l': if len(args) != 2: - print USAGE + print(USAGE) sys.exit(1) with ZipFile(args[1], 'r') as zf: zf.printdir() elif args[0] == '-t': if len(args) != 2: - print USAGE + print(USAGE) sys.exit(1) with ZipFile(args[1], 'r') as zf: badfile = zf.testzip() if badfile: print("The following enclosed file is corrupted: {!r}".format(badfile)) - print "Done testing" + print("Done testing") elif args[0] == '-e': if len(args) != 3: - print USAGE + print(USAGE) sys.exit(1) with ZipFile(args[1], 'r') as zf: @@ -1461,7 +1530,7 @@ def main(args = None): elif args[0] == '-c': if len(args) < 3: - print USAGE + print(USAGE) sys.exit(1) def addToZip(zf, path, zippath): |