Skip to content

Commit 87faec2

Browse files
gh-115809: Improve TimedRotatingFileHandler.getFilesToDelete() (GH-115812)
Improve algorithm for computing which rolled-over log files to delete in logging.TimedRotatingFileHandler. It is now reliable for handlers without namer and with arbitrary deterministic namer that leaves the datetime part in the file name unmodified.
1 parent 002a594 commit 87faec2

File tree

3 files changed

+56
-36
lines changed

3 files changed

+56
-36
lines changed

Lib/logging/handlers.py

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -232,19 +232,19 @@ def __init__(self, filename, when='h', interval=1, backupCount=0,
232232
if self.when == 'S':
233233
self.interval = 1 # one second
234234
self.suffix = "%Y-%m-%d_%H-%M-%S"
235-
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$"
235+
extMatch = r"(?<!\d)\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(?!\d)"
236236
elif self.when == 'M':
237237
self.interval = 60 # one minute
238238
self.suffix = "%Y-%m-%d_%H-%M"
239-
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}(\.\w+)?$"
239+
extMatch = r"(?<!\d)\d{4}-\d{2}-\d{2}_\d{2}-\d{2}(?!\d)"
240240
elif self.when == 'H':
241241
self.interval = 60 * 60 # one hour
242242
self.suffix = "%Y-%m-%d_%H"
243-
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}(\.\w+)?$"
243+
extMatch = r"(?<!\d)\d{4}-\d{2}-\d{2}_\d{2}(?!\d)"
244244
elif self.when == 'D' or self.when == 'MIDNIGHT':
245245
self.interval = 60 * 60 * 24 # one day
246246
self.suffix = "%Y-%m-%d"
247-
self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$"
247+
extMatch = r"(?<!\d)\d{4}-\d{2}-\d{2}(?!\d)"
248248
elif self.when.startswith('W'):
249249
self.interval = 60 * 60 * 24 * 7 # one week
250250
if len(self.when) != 2:
@@ -253,11 +253,17 @@ def __init__(self, filename, when='h', interval=1, backupCount=0,
253253
raise ValueError("Invalid day specified for weekly rollover: %s" % self.when)
254254
self.dayOfWeek = int(self.when[1])
255255
self.suffix = "%Y-%m-%d"
256-
self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$"
256+
extMatch = r"(?<!\d)\d{4}-\d{2}-\d{2}(?!\d)"
257257
else:
258258
raise ValueError("Invalid rollover interval specified: %s" % self.when)
259259

260-
self.extMatch = re.compile(self.extMatch, re.ASCII)
260+
# extMatch is a pattern for matching a datetime suffix in a file name.
261+
# After custom naming, it is no longer guaranteed to be separated by
262+
# periods from other parts of the filename. The lookup statements
263+
# (?<!\d) and (?!\d) ensure that the datetime suffix (which itself
264+
# starts and ends with digits) is not preceded or followed by digits.
265+
# This reduces the number of false matches and improves performance.
266+
self.extMatch = re.compile(extMatch, re.ASCII)
261267
self.interval = self.interval * interval # multiply by units requested
262268
# The following line added because the filename passed in could be a
263269
# path object (see Issue #27493), but self.baseFilename will be a string
@@ -376,31 +382,22 @@ def getFilesToDelete(self):
376382
for fileName in fileNames:
377383
if fileName[:plen] == prefix:
378384
suffix = fileName[plen:]
379-
if self.extMatch.match(suffix):
385+
if self.extMatch.fullmatch(suffix):
380386
result.append(os.path.join(dirName, fileName))
381387
else:
382-
# See bpo-44753: Don't use the extension when computing the prefix.
383-
n, e = os.path.splitext(baseName)
384-
prefix = n + '.'
385-
plen = len(prefix)
386388
for fileName in fileNames:
387-
# Our files could be just about anything after custom naming, but
388-
# likely candidates are of the form
389-
# foo.log.DATETIME_SUFFIX or foo.DATETIME_SUFFIX.log
390-
if (not fileName.startswith(baseName) and fileName.endswith(e) and
391-
len(fileName) > (plen + 1) and not fileName[plen+1].isdigit()):
392-
continue
389+
# Our files could be just about anything after custom naming,
390+
# but they should contain the datetime suffix.
391+
# Try to find the datetime suffix in the file name and verify
392+
# that the file name can be generated by this handler.
393+
m = self.extMatch.search(fileName)
394+
while m:
395+
dfn = self.namer(self.baseFilename + "." + m[0])
396+
if os.path.basename(dfn) == fileName:
397+
result.append(os.path.join(dirName, fileName))
398+
break
399+
m = self.extMatch.search(fileName, m.start() + 1)
393400

394-
if fileName[:plen] == prefix:
395-
suffix = fileName[plen:]
396-
# See bpo-45628: The date/time suffix could be anywhere in the
397-
# filename
398-
399-
parts = suffix.split('.')
400-
for part in parts:
401-
if self.extMatch.match(part):
402-
result.append(os.path.join(dirName, fileName))
403-
break
404401
if len(result) < self.backupCount:
405402
result = []
406403
else:

Lib/test/test_logging.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6260,7 +6260,7 @@ def test_compute_files_to_delete(self):
62606260
for i in range(10):
62616261
times.append(dt.strftime('%Y-%m-%d_%H-%M-%S'))
62626262
dt += datetime.timedelta(seconds=5)
6263-
prefixes = ('a.b', 'a.b.c', 'd.e', 'd.e.f')
6263+
prefixes = ('a.b', 'a.b.c', 'd.e', 'd.e.f', 'g')
62646264
files = []
62656265
rotators = []
62666266
for prefix in prefixes:
@@ -6273,10 +6273,22 @@ def test_compute_files_to_delete(self):
62736273
if prefix.startswith('a.b'):
62746274
for t in times:
62756275
files.append('%s.log.%s' % (prefix, t))
6276-
else:
6277-
rotator.namer = lambda name: name.replace('.log', '') + '.log'
6276+
elif prefix.startswith('d.e'):
6277+
def namer(filename):
6278+
dirname, basename = os.path.split(filename)
6279+
basename = basename.replace('.log', '') + '.log'
6280+
return os.path.join(dirname, basename)
6281+
rotator.namer = namer
62786282
for t in times:
62796283
files.append('%s.%s.log' % (prefix, t))
6284+
elif prefix == 'g':
6285+
def namer(filename):
6286+
dirname, basename = os.path.split(filename)
6287+
basename = 'g' + basename[6:] + '.oldlog'
6288+
return os.path.join(dirname, basename)
6289+
rotator.namer = namer
6290+
for t in times:
6291+
files.append('g%s.oldlog' % t)
62806292
# Create empty files
62816293
for fn in files:
62826294
p = os.path.join(wd, fn)
@@ -6286,18 +6298,23 @@ def test_compute_files_to_delete(self):
62866298
for i, prefix in enumerate(prefixes):
62876299
rotator = rotators[i]
62886300
candidates = rotator.getFilesToDelete()
6289-
self.assertEqual(len(candidates), 3)
6301+
self.assertEqual(len(candidates), 3, candidates)
62906302
if prefix.startswith('a.b'):
62916303
p = '%s.log.' % prefix
62926304
for c in candidates:
62936305
d, fn = os.path.split(c)
62946306
self.assertTrue(fn.startswith(p))
6295-
else:
6307+
elif prefix.startswith('d.e'):
62966308
for c in candidates:
62976309
d, fn = os.path.split(c)
6298-
self.assertTrue(fn.endswith('.log'))
6310+
self.assertTrue(fn.endswith('.log'), fn)
62996311
self.assertTrue(fn.startswith(prefix + '.') and
63006312
fn[len(prefix) + 2].isdigit())
6313+
elif prefix == 'g':
6314+
for c in candidates:
6315+
d, fn = os.path.split(c)
6316+
self.assertTrue(fn.endswith('.oldlog'))
6317+
self.assertTrue(fn.startswith('g') and fn[1].isdigit())
63016318

63026319
def test_compute_files_to_delete_same_filename_different_extensions(self):
63036320
# See GH-93205 for background
@@ -6321,6 +6338,8 @@ def test_compute_files_to_delete_same_filename_different_extensions(self):
63216338
rotators.append(rotator)
63226339
for t in times:
63236340
files.append('%s.%s' % (prefix, t))
6341+
for t in times:
6342+
files.append('a.log.%s.c' % t)
63246343
# Create empty files
63256344
for f in files:
63266345
(wd / f).touch()
@@ -6329,11 +6348,11 @@ def test_compute_files_to_delete_same_filename_different_extensions(self):
63296348
backupCount = i+1
63306349
rotator = rotators[i]
63316350
candidates = rotator.getFilesToDelete()
6332-
self.assertEqual(len(candidates), n_files - backupCount)
6333-
matcher = re.compile(r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$")
6351+
self.assertEqual(len(candidates), n_files - backupCount, candidates)
6352+
matcher = re.compile(r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\Z")
63346353
for c in candidates:
63356354
d, fn = os.path.split(c)
6336-
self.assertTrue(fn.startswith(prefix))
6355+
self.assertTrue(fn.startswith(prefix+'.'))
63376356
suffix = fn[(len(prefix)+1):]
63386357
self.assertRegex(suffix, matcher)
63396358

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Improve algorithm for computing which rolled-over log files to delete in
2+
:class:`logging.TimedRotatingFileHandler`. It is now reliable for handlers
3+
without ``namer`` and with arbitrary deterministic ``namer`` that leaves the
4+
datetime part in the file name unmodified.

0 commit comments

Comments
 (0)