Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions Doc/library/gzip.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ 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:: next
The *mtime* parameter can now be a :class:`~datetime.datetime` object as well
as a :class:`float`. If not :ref:`timezone aware <datetime-naive-aware>` then a
:class:`ValueError` will be raised.

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 +214,11 @@ The module defines the following items:
For the previous behaviour of using the current time,
pass ``None`` to *mtime*.

.. versionchanged:: next
The *mtime* parameter can now be a :class:`~datetime.datetime` object as well
as a :class:`float`. If not :ref:`timezone aware <datetime-naive-aware>` then a
:class:`ValueError` will be raised.

.. function:: decompress(data)

Decompress the *data*, returning a :class:`bytes` object containing the
Expand Down
14 changes: 12 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 @@ -224,6 +225,8 @@ def __init__(self, filename=None, mode=None,
-zlib.MAX_WBITS,
zlib.DEF_MEM_LEVEL,
0)
if isinstance(mtime, datetime) and mtime.tzinfo is None:
raise ValueError("Refusing to write naive datetime to Gzip header")
self._write_mtime = mtime
self._buffer_size = _WRITE_BUFFER_SIZE
self._buffer = io.BufferedWriter(_WriteBufferStream(self),
Expand All @@ -239,7 +242,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 +282,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 +485,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 +597,10 @@ 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):
if mtime.tzinfo is None:
raise ValueError("Refusing to write naive datetime to Gzip header")
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
39 changes: 39 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,24 @@ 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_mtime_as_datetime_no_timezone(self):
mtime = datetime(1973, 11, 29, 21, 33, 9)
self.assertIsNone(mtime.tzinfo)
with self.assertRaises(ValueError):
with gzip.GzipFile(self.filename, 'w', mtime = mtime) as fWrite:
fWrite.write(data1)

def test_metadata(self):
mtime = 123456789

Expand Down Expand Up @@ -713,6 +732,26 @@ 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_as_datetime_no_timezone(self):
mtime = datetime(1973, 11, 29, 21, 33, 9)
self.assertIsNone(mtime.tzinfo)
for data in [data1, data2]:
for args in [(), (1,), (6,), (9,)]:
with self.subTest(data=data, args=args):
with self.assertRaises(ValueError):
gzip.compress(data, *args, mtime=mtime)

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 @@
Allow the *mtime* parameters in :func:`gzip.compress` and :class:`gzip.GzipFile` to be :class:`datetime.datetime` objects.
Loading