https://github.com/python/cpython/commit/82905dd165f198f1b18625ae0f0ede9367df75a4
commit: 82905dd165f198f1b18625ae0f0ede9367df75a4
branch: main
author: Stan Ulbrych <[email protected]>
committer: StanFromIreland <[email protected]>
date: 2026-08-07T16:21:04+02:00
summary:
gh-154948: Fix `test_zipfile` failure when `SOURCE_DATE_EPOCH` is set (#155001)
files:
M Doc/library/test.rst
M Lib/test/support/os_helper.py
M Lib/test/test_compileall.py
M Lib/test/test_importlib/source/test_file_loader.py
M Lib/test/test_py_compile.py
M Lib/test/test_regrtest.py
M Lib/test/test_zipfile/test_core.py
diff --git a/Doc/library/test.rst b/Doc/library/test.rst
index 893154246ae4d81..f3b5383658b5ac4 100644
--- a/Doc/library/test.rst
+++ b/Doc/library/test.rst
@@ -1656,6 +1656,32 @@ The :mod:`!test.support.os_helper` module provides
support for os tests.
wrapped with a wait loop that checks for the existence of the file.
+.. decorator:: with_source_date_epoch(*, epoch=123456789)
+
+ A decorator for running tests with the :envvar:`SOURCE_DATE_EPOCH`
+ environment variable set to *epoch*.
+
+
+.. decorator:: without_source_date_epoch
+
+ A decorator for running tests with the :envvar:`SOURCE_DATE_EPOCH`
+ environment variable unset.
+
+
+.. class:: SourceDateEpochTestMeta
+
+ Metaclass wrapping all test methods of the class with
+ :func:`with_source_date_epoch` if the *source_date_epoch* keyword class
+ argument is true, or with :func:`without_source_date_epoch` otherwise.
+ For example::
+
+ class TestsWithSourceEpoch(Tests,
+ metaclass=SourceDateEpochTestMeta,
+ source_date_epoch=True):
+ pass
+
+
+
:mod:`!test.support.import_helper` --- Utilities for import tests
=================================================================
diff --git a/Lib/test/support/os_helper.py b/Lib/test/support/os_helper.py
index daf6060940e97f0..e1e2e69cb3d8334 100644
--- a/Lib/test/support/os_helper.py
+++ b/Lib/test/support/os_helper.py
@@ -1,6 +1,7 @@
import collections.abc
import contextlib
import errno
+import functools
import logging
import os
import re
@@ -806,6 +807,48 @@ def __exit__(self, *ignore_exc):
os.environ = self._environ
+def without_source_date_epoch(fxn):
+ """Runs function with SOURCE_DATE_EPOCH unset."""
+ @functools.wraps(fxn)
+ def wrapper(*args, **kwargs):
+ with EnvironmentVarGuard() as env:
+ env.unset('SOURCE_DATE_EPOCH')
+ return fxn(*args, **kwargs)
+ return wrapper
+
+
+_MISSING = sentinel("MISSING")
+
+def with_source_date_epoch(fxn=_MISSING, *, epoch=123456789):
+ """Runs function with SOURCE_DATE_EPOCH set to *epoch*."""
+ if fxn is _MISSING:
+ return functools.partial(with_source_date_epoch, epoch=epoch)
+
+ @functools.wraps(fxn)
+ def wrapper(*args, **kwargs):
+ with EnvironmentVarGuard() as env:
+ env['SOURCE_DATE_EPOCH'] = str(epoch)
+ return fxn(*args, **kwargs)
+ return wrapper
+
+
+# Run tests with SOURCE_DATE_EPOCH set or unset explicitly.
+class SourceDateEpochTestMeta(type(unittest.TestCase)):
+ def __new__(mcls, name, bases, dct, *, source_date_epoch):
+ cls = super().__new__(mcls, name, bases, dct)
+
+ for attr in dir(cls):
+ if attr.startswith('test_'):
+ meth = getattr(cls, attr)
+ if source_date_epoch:
+ wrapper = with_source_date_epoch(meth)
+ else:
+ wrapper = without_source_date_epoch(meth)
+ setattr(cls, attr, wrapper)
+
+ return cls
+
+
try:
if support.MS_WINDOWS:
import ctypes
diff --git a/Lib/test/test_compileall.py b/Lib/test/test_compileall.py
index 95dcb4ef9fdc202..9a6ca2be1624c6c 100644
--- a/Lib/test/test_compileall.py
+++ b/Lib/test/test_compileall.py
@@ -28,8 +28,8 @@
from test import support
from test.support import os_helper
from test.support import script_helper
-from test.test_py_compile import without_source_date_epoch
-from test.test_py_compile import SourceDateEpochTestMeta
+from test.support.os_helper import without_source_date_epoch
+from test.support.os_helper import SourceDateEpochTestMeta
from test.support.os_helper import FakePath
diff --git a/Lib/test/test_importlib/source/test_file_loader.py
b/Lib/test/test_importlib/source/test_file_loader.py
index e4bd850f3514ff1..ca2cc045fa674e1 100644
--- a/Lib/test/test_importlib/source/test_file_loader.py
+++ b/Lib/test/test_importlib/source/test_file_loader.py
@@ -5,21 +5,18 @@
machinery = util.import_importlib('importlib.machinery')
importlib_util = util.import_importlib('importlib.util')
-import errno
import marshal
import os
import py_compile
-import shutil
import stat
import sys
import types
import unittest
-import warnings
-from test.support.import_helper import make_legacy_pyc, unload
+from test.support.import_helper import make_legacy_pyc
-from test.test_py_compile import without_source_date_epoch
-from test.test_py_compile import SourceDateEpochTestMeta
+from test.support.os_helper import without_source_date_epoch
+from test.support.os_helper import SourceDateEpochTestMeta
class SimpleTest:
diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py
index b4265e6a0b458db..616e5d0c7cf971c 100644
--- a/Lib/test/test_py_compile.py
+++ b/Lib/test/test_py_compile.py
@@ -1,4 +1,3 @@
-import functools
import importlib.util
import os
import py_compile
@@ -11,43 +10,7 @@
from test import support
from test.support import os_helper, script_helper
-
-
-def without_source_date_epoch(fxn):
- """Runs function with SOURCE_DATE_EPOCH unset."""
- @functools.wraps(fxn)
- def wrapper(*args, **kwargs):
- with os_helper.EnvironmentVarGuard() as env:
- env.unset('SOURCE_DATE_EPOCH')
- return fxn(*args, **kwargs)
- return wrapper
-
-
-def with_source_date_epoch(fxn):
- """Runs function with SOURCE_DATE_EPOCH set."""
- @functools.wraps(fxn)
- def wrapper(*args, **kwargs):
- with os_helper.EnvironmentVarGuard() as env:
- env['SOURCE_DATE_EPOCH'] = '123456789'
- return fxn(*args, **kwargs)
- return wrapper
-
-
-# Run tests with SOURCE_DATE_EPOCH set or unset explicitly.
-class SourceDateEpochTestMeta(type(unittest.TestCase)):
- def __new__(mcls, name, bases, dct, *, source_date_epoch):
- cls = super().__new__(mcls, name, bases, dct)
-
- for attr in dir(cls):
- if attr.startswith('test_'):
- meth = getattr(cls, attr)
- if source_date_epoch:
- wrapper = with_source_date_epoch(meth)
- else:
- wrapper = without_source_date_epoch(meth)
- setattr(cls, attr, wrapper)
-
- return cls
+from test.support.os_helper import SourceDateEpochTestMeta
class PyCompileTestsBase:
diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py
index 6d30d267cd5ad29..6ba440053089161 100644
--- a/Lib/test/test_regrtest.py
+++ b/Lib/test/test_regrtest.py
@@ -170,21 +170,20 @@ def test_randomize(self):
ns = self.parse_args([opt])
self.assertTrue(ns.randomize)
- with os_helper.EnvironmentVarGuard() as env:
- # with SOURCE_DATE_EPOCH
- env['SOURCE_DATE_EPOCH'] = '1697839080'
- ns = self.parse_args(['--randomize'])
- regrtest = main.Regrtest(ns)
- self.assertFalse(regrtest.randomize)
- self.assertIsInstance(regrtest.random_seed, str)
- self.assertEqual(regrtest.random_seed, '1697839080')
-
- # without SOURCE_DATE_EPOCH
- del env['SOURCE_DATE_EPOCH']
- ns = self.parse_args(['--randomize'])
- regrtest = main.Regrtest(ns)
- self.assertTrue(regrtest.randomize)
- self.assertIsInstance(regrtest.random_seed, int)
+ @os_helper.with_source_date_epoch(epoch=1697839080)
+ def test_randomize_with_source_date_epoch(self):
+ ns = self.parse_args(['--randomize'])
+ regrtest = main.Regrtest(ns)
+ self.assertFalse(regrtest.randomize)
+ self.assertIsInstance(regrtest.random_seed, str)
+ self.assertEqual(regrtest.random_seed, '1697839080')
+
+ @os_helper.without_source_date_epoch
+ def test_randomize_without_source_date_epoch(self):
+ ns = self.parse_args(['--randomize'])
+ regrtest = main.Regrtest(ns)
+ self.assertTrue(regrtest.randomize)
+ self.assertIsInstance(regrtest.random_seed, int)
def test_no_randomize(self):
ns = self.parse_args([])
diff --git a/Lib/test/test_zipfile/test_core.py
b/Lib/test/test_zipfile/test_core.py
index 83f2eef6b6f8ab6..e9974d6c05648bb 100644
--- a/Lib/test/test_zipfile/test_core.py
+++ b/Lib/test/test_zipfile/test_core.py
@@ -22,14 +22,15 @@
from random import randint, random, randbytes
from test import archiver_tests
-from test.support import script_helper, os_helper
+from test.support import script_helper
from test.support import (
findfile, requires_zlib, requires_bz2, requires_lzma,
requires_zstd, captured_stdout, captured_stderr, requires_subprocess,
cpython_only, gc_collect
)
from test.support.os_helper import (
- TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath
+ TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath,
+ with_source_date_epoch, without_source_date_epoch,
)
from test.support.import_helper import ensure_lazy_imports
from test.support.warnings_helper import check_no_resource_warning
@@ -1961,6 +1962,7 @@ def test_repack_file_entry_before_first_file(self):
with zipfile.ZipFile(TESTFN) as zh:
self.assertIsNone(zh.testzip())
+ @without_source_date_epoch # SOURCE_DATE_EPOCH would bypass the time mock
below
@mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for
ZipFile.writestr()
def test_repack_bytes_before_removed_files(self):
"""Should preserve if there are bytes before stale local file
entries."""
@@ -2005,6 +2007,7 @@ def test_repack_bytes_before_removed_files(self):
with zipfile.ZipFile(TESTFN) as zh:
self.assertIsNone(zh.testzip())
+ @without_source_date_epoch # SOURCE_DATE_EPOCH would bypass the time mock
below
@mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for
ZipFile.writestr()
def test_repack_bytes_after_removed_files(self):
"""Should keep extra bytes if there are bytes after stale local file
entries."""
@@ -2048,6 +2051,7 @@ def test_repack_bytes_after_removed_files(self):
with zipfile.ZipFile(TESTFN) as zh:
self.assertIsNone(zh.testzip())
+ @without_source_date_epoch # SOURCE_DATE_EPOCH would bypass the time mock
below
@mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for
ZipFile.writestr()
def test_repack_bytes_between_removed_files(self):
"""Should strip only local file entries before random bytes."""
@@ -2252,6 +2256,7 @@ def test_repack_removed_partial(self):
with zipfile.ZipFile(TESTFN) as zh:
self.assertIsNone(zh.testzip())
+ @without_source_date_epoch # SOURCE_DATE_EPOCH would bypass the time mock
below
@mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for
ZipFile.writestr()
def test_repack_removed_bytes_between_files(self):
"""Should not remove bytes between local file entries."""
@@ -4004,29 +4009,24 @@ def test_writestr_extended_local_header_issue1202(self):
zinfo.flag_bits |= zipfile._MASK_USE_DATA_DESCRIPTOR #
Include an extended local header.
orig_zip.writestr(zinfo, data)
+ @with_source_date_epoch(epoch=1735715999)
def test_write_with_source_date_epoch(self):
- with os_helper.EnvironmentVarGuard() as env:
- # Set the SOURCE_DATE_EPOCH environment variable to a specific
timestamp
- env['SOURCE_DATE_EPOCH'] = "1735715999"
-
- with zipfile.ZipFile(TESTFN, "w") as zf:
- zf.writestr("test_source_date_epoch.txt", "Testing
SOURCE_DATE_EPOCH")
+ with zipfile.ZipFile(TESTFN, "w") as zf:
+ zf.writestr("test_source_date_epoch.txt", "Testing
SOURCE_DATE_EPOCH")
- with zipfile.ZipFile(TESTFN, "r") as zf:
- zip_info = zf.getinfo("test_source_date_epoch.txt")
- expected_utc = (2025, 1, 1, 7, 19, 58)
- self.assertEqual(zip_info.date_time, expected_utc)
+ with zipfile.ZipFile(TESTFN, "r") as zf:
+ zip_info = zf.getinfo("test_source_date_epoch.txt")
+ expected_utc = (2025, 1, 1, 7, 19, 58)
+ self.assertEqual(zip_info.date_time, expected_utc)
+ @without_source_date_epoch
def test_write_without_source_date_epoch(self):
- with os_helper.EnvironmentVarGuard() as env:
- del env['SOURCE_DATE_EPOCH']
-
- with zipfile.ZipFile(TESTFN, "w") as zf:
- zf.writestr("test_no_source_date_epoch.txt", "Testing without
SOURCE_DATE_EPOCH")
+ with zipfile.ZipFile(TESTFN, "w") as zf:
+ zf.writestr("test_no_source_date_epoch.txt", "Testing without
SOURCE_DATE_EPOCH")
- with zipfile.ZipFile(TESTFN, "r") as zf:
- zip_info = zf.getinfo("test_no_source_date_epoch.txt")
- self.assertTimestampAlmostEqual(time.localtime(),
zip_info.date_time, tolerance=2)
+ with zipfile.ZipFile(TESTFN, "r") as zf:
+ zip_info = zf.getinfo("test_no_source_date_epoch.txt")
+ self.assertTimestampAlmostEqual(time.localtime(),
zip_info.date_time, tolerance=2)
def assertTimestampAlmostEqual(self, time1, time2, tolerance):
import datetime
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]