gh-160433: Correct timed log backup names across DST changes (GH-160564)

This commit is contained in:
Liang Hu authored and GitHub committed 2026-10-24 07:59:45 +01:00
1 parent 7dd8b4736a
commit 457ba33e96
3 files changed
+84 -8

No files matched your search

+11 -8
View File
@@ -459,14 +459,17 @@ class TimedRotatingFileHandler(BaseRotatingHandler):
timeTuple = time.gmtime(t)
else:
timeTuple = time.localtime(t)
dstNow = time.localtime(currentTime)[-1]
dstThen = timeTuple[-1]
if dstNow != dstThen:
if dstNow:
addend = 3600
else:
addend = -3600
timeTuple = time.localtime(t + addend)
# DST affects calendar-based intervals, not fixed-duration ones.
if self.when == 'MIDNIGHT' or self.when.startswith('W'):
# A record can arrive well after the scheduled rollover.
dstAtRollover = time.localtime(self.rolloverAt)[-1]
dstThen = timeTuple[-1]
if dstAtRollover != dstThen:
if dstAtRollover:
addend = 3600
else:
addend = -3600
timeTuple = time.localtime(t + addend)
dfn = self.rotation_filename(self.baseFilename + "." +
time.strftime(self.suffix, timeTuple))
self.rolloverAt = self.computeRollover(currentTime)
+71
View File
@@ -6726,6 +6726,77 @@ class RotatingFileHandlerTest(BaseFileTest):
rh.close()
class TimedRotatingFileHandlerTest(BaseFileTest):
def assert_rollover_contents(self, when, timestamps, expected, **kwargs):
with tempfile.TemporaryDirectory() as dirname:
filename = os.path.join(dirname, 'app.log')
now = [timestamps[0]]
with patch('time.time', lambda: now[0]):
handler = logging.handlers.TimedRotatingFileHandler(
filename, when=when, delay=True, encoding='utf-8', **kwargs)
try:
for i, timestamp in enumerate(timestamps):
now[0] = timestamp
handler.emit(logging.makeLogRecord({'msg': str(i)}))
finally:
handler.close()
actual = {}
for name in os.listdir(dirname):
with open(os.path.join(dirname, name), encoding='utf-8') as f:
actual[name.removeprefix('app.log')] = f.read().splitlines()
self.assertEqual(actual, expected)
@support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
def test_rollover_fixed_interval_dst(self):
DT = datetime.datetime
cases = (
('H', DT(2026, 3, 8, 1, 30), 3),
('H', DT(2026, 11, 1, 1, 30), 2),
('D', DT(2026, 3, 7, 23, 30), 3),
('D', DT(2026, 11, 1, 0, 30), 2),
)
for when, start, count in cases:
interval = 3600 if when == 'H' else 86400
timestamps = [start.timestamp() + i * interval for i in range(count)]
suffix = '.%Y-%m-%d_%H' if when == 'H' else '.%Y-%m-%d'
for utc in (False, True):
with self.subTest(when=when, start=start, utc=utc):
convert = time.gmtime if utc else time.localtime
expected = {
time.strftime(suffix, convert(timestamp)): [str(i)]
for i, timestamp in enumerate(timestamps[:-1])
}
expected[''] = [str(count - 1)]
self.assert_rollover_contents(
when, timestamps, expected, utc=utc)
@support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
def test_rollover_delayed_dst(self):
DT = datetime.datetime
cases = (
('MIDNIGHT', None,
[DT(2026, 10, day, hour).timestamp()
for day in (30, 31) for hour in (9, 18)] +
[DT(2026, 11, day, hour).timestamp()
for day in (1, 2, 3) for hour in (9, 18)],
{'.2026-10-30': ['0', '1'], '.2026-10-31': ['2', '3'],
'.2026-11-01': ['4', '5'], '.2026-11-02': ['6', '7'],
'': ['8', '9']}),
('W6', None,
[DT(2026, 10, 23, 9).timestamp(),
DT(2026, 11, 1, 9).timestamp(),
DT(2026, 11, 2, 9).timestamp()],
{'.2026-10-19': ['0'], '.2026-10-26': ['1'], '': ['2']}),
('MIDNIGHT', datetime.time(23, 30),
[DT(2026, month, 1, 12).timestamp() for month in (3, 4, 10, 12)],
{'.2026-02-28': ['0'], '.2026-03-31': ['1'],
'.2026-09-30': ['2'], '': ['3']}),
)
for when, at_time, timestamps, expected in cases:
for utc in (False, True):
with self.subTest(when=when, atTime=at_time, utc=utc):
self.assert_rollover_contents(
when, timestamps, expected, atTime=at_time, utc=utc)
@unittest.skipIf(support.is_wasi, "WASI does not have /dev/null.")
def test_should_not_rollover(self):
# See bpo-45401. Should only ever rollover regular files
@@ -0,0 +1,2 @@
Fix incorrect backup names around daylight saving time changes in
:class:`logging.handlers.TimedRotatingFileHandler`, including delayed rollovers.