Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Doc/library/gzip.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ The module defines the following items:
If *mtime* is omitted or ``None``, the current time is used. Use *mtime* = 0
to generate a compressed stream that does not depend on creation time.

.. versionchanged:: 3.14
The ``mtime`` parameter can now be a :class:`~datetime.datetime` object as well
as a :class:`float`.

See below for the :attr:`mtime` attribute that is set when decompressing.

Calling a :class:`GzipFile` object's :meth:`!close` method does not close
Expand Down Expand Up @@ -209,6 +213,10 @@ The module defines the following items:
For the previous behaviour of using the current time,
pass ``None`` to *mtime*.

.. versionchanged:: 3.14
The ``mtime`` parameter can now be a :class:`~datetime.datetime` object as well
as a :class:`float`.

.. function:: decompress(data)

Decompress the *data*, returning a :class:`bytes` object containing the
Expand Down
10 changes: 8 additions & 2 deletions Lib/gzip.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

# based on Andrew Kuchling's minigzip.py distributed with the zlib module

from datetime import datetime, timezone
import struct, sys, time, os
import zlib
import builtins
Expand Down Expand Up @@ -239,7 +240,8 @@ def __init__(self, filename=None, mode=None,
@property
def mtime(self):
"""Last modification time read from stream, or None"""
return self._buffer.raw._last_mtime
mtime = self._buffer.raw._last_mtime
return int(mtime.timestamp()) if mtime is not None else None

def __repr__(self):
s = repr(self.fileobj)
Expand Down Expand Up @@ -278,6 +280,8 @@ def _write_gzip_header(self, compresslevel):
mtime = self._write_mtime
if mtime is None:
mtime = time.time()
elif isinstance(mtime, datetime):
mtime = mtime.timestamp()
write32u(self.fileobj, int(mtime))
if compresslevel == _COMPRESS_LEVEL_BEST:
xfl = b'\002'
Expand Down Expand Up @@ -479,7 +483,7 @@ def _read_gzip_header(fp):
break
if flag & FHCRC:
_read_exact(fp, 2) # Read & discard the 16-bit header CRC
return last_mtime
return datetime.fromtimestamp(last_mtime, tz=timezone.utc)


class _GzipReader(_compression.DecompressReader):
Expand Down Expand Up @@ -591,6 +595,8 @@ def compress(data, compresslevel=_COMPRESS_LEVEL_BEST, *, mtime=0):
gzip_data = zlib.compress(data, level=compresslevel, wbits=31)
if mtime is None:
mtime = time.time()
elif isinstance(mtime, datetime):
mtime = mtime.timestamp()
# Reuse gzip header created by zlib, replace mtime and OS byte for
# consistency.
header = struct.pack("<4sLBB", gzip_data, int(mtime), gzip_data[8], 255)
Expand Down
23 changes: 23 additions & 0 deletions Lib/test/test_gzip.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import struct
import sys
import unittest
from datetime import datetime, timezone
from subprocess import PIPE, Popen
from test.support import import_helper
from test.support import os_helper
Expand Down Expand Up @@ -316,6 +317,17 @@ def test_mtime(self):
self.assertEqual(dataRead, data1)
self.assertEqual(fRead.mtime, mtime)

def test_mtime_as_datetime(self):
mtime = datetime(1973, 11, 29, 21, 33, 9, tzinfo=timezone.utc)
with gzip.GzipFile(self.filename, 'w', mtime = mtime) as fWrite:
fWrite.write(data1)
with gzip.GzipFile(self.filename) as fRead:
self.assertTrue(hasattr(fRead, 'mtime'))
self.assertIsNone(fRead.mtime)
dataRead = fRead.read()
self.assertEqual(dataRead, data1)
self.assertEqual(fRead.mtime, int(mtime.timestamp()))

def test_metadata(self):
mtime = 123456789

Expand Down Expand Up @@ -713,6 +725,17 @@ def test_compress_mtime(self):
f.read(1) # to set mtime attribute
self.assertEqual(f.mtime, mtime)

def test_compress_mtime_as_datetime(self):
mtime = datetime(1973, 11, 29, 21, 33, 9, tzinfo=timezone.utc)
for data in [data1, data2]:
for args in [(), (1,), (6,), (9,)]:
with self.subTest(data=data, args=args):
datac = gzip.compress(data, *args, mtime=mtime)
self.assertEqual(type(datac), bytes)
with gzip.GzipFile(fileobj=io.BytesIO(datac), mode="rb") as f:
f.read(1) # to set mtime attribute
self.assertEqual(f.mtime, int(mtime.timestamp()))

def test_compress_mtime_default(self):
# test for gh-125260
datac = gzip.compress(data1, mtime=0)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Allow the mtime parameters in gzip.compress and gzip.GzipFile to be datetime
objects.
Loading