Skip to content

Commit d3756ed

Browse files
[3.11] gh-115809: Improve TimedRotatingFileHandler.getFilesToDelete() (GH-115812) (GH-116262)
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. (cherry picked from commit 87faec2) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 82e7692 commit d3756ed

File tree

3 files changed

+56
-36
lines changed

3 files changed

+56
-36
lines changed

Lib/logging/handlers.py

+24-27
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

+28-9
Original file line numberDiff line numberDiff line change
@@ -5834,7 +5834,7 @@ def test_compute_files_to_delete(self):
58345834
for i in range(10):
58355835
times.append(dt.strftime('%Y-%m-%d_%H-%M-%S'))
58365836
dt += datetime.timedelta(seconds=5)
5837-
prefixes = ('a.b', 'a.b.c', 'd.e', 'd.e.f')
5837+
prefixes = ('a.b', 'a.b.c', 'd.e', 'd.e.f', 'g')
58385838
files = []
58395839
rotators = []
58405840
for prefix in prefixes:
@@ -5847,10 +5847,22 @@ def test_compute_files_to_delete(self):
58475847
if prefix.startswith('a.b'):
58485848
for t in times:
58495849
files.append('%s.log.%s' % (prefix, t))
5850-
else:
5851-
rotator.namer = lambda name: name.replace('.log', '') + '.log'
5850+
elif prefix.startswith('d.e'):
5851+
def namer(filename):
5852+
dirname, basename = os.path.split(filename)
5853+
basename = basename.replace('.log', '') + '.log'
5854+
return os.path.join(dirname, basename)
5855+
rotator.namer = namer
58525856
for t in times:
58535857
files.append('%s.%s.log' % (prefix, t))
5858+
elif prefix == 'g':
5859+
def namer(filename):
5860+
dirname, basename = os.path.split(filename)
5861+
basename = 'g' + basename[6:] + '.oldlog'
5862+
return os.path.join(dirname, basename)
5863+
rotator.namer = namer
5864+
for t in times:
5865+
files.append('g%s.oldlog' % t)
58545866
# Create empty files
58555867
for fn in files:
58565868
p = os.path.join(wd, fn)
@@ -5860,18 +5872,23 @@ def test_compute_files_to_delete(self):
58605872
for i, prefix in enumerate(prefixes):
58615873
rotator = rotators[i]
58625874
candidates = rotator.getFilesToDelete()
5863-
self.assertEqual(len(candidates), 3)
5875+
self.assertEqual(len(candidates), 3, candidates)
58645876
if prefix.startswith('a.b'):
58655877
p = '%s.log.' % prefix
58665878
for c in candidates:
58675879
d, fn = os.path.split(c)
58685880
self.assertTrue(fn.startswith(p))
5869-
else:
5881+
elif prefix.startswith('d.e'):
58705882
for c in candidates:
58715883
d, fn = os.path.split(c)
5872-
self.assertTrue(fn.endswith('.log'))
5884+
self.assertTrue(fn.endswith('.log'), fn)
58735885
self.assertTrue(fn.startswith(prefix + '.') and
58745886
fn[len(prefix) + 2].isdigit())
5887+
elif prefix == 'g':
5888+
for c in candidates:
5889+
d, fn = os.path.split(c)
5890+
self.assertTrue(fn.endswith('.oldlog'))
5891+
self.assertTrue(fn.startswith('g') and fn[1].isdigit())
58755892

58765893
def test_compute_files_to_delete_same_filename_different_extensions(self):
58775894
# See GH-93205 for background
@@ -5895,6 +5912,8 @@ def test_compute_files_to_delete_same_filename_different_extensions(self):
58955912
rotators.append(rotator)
58965913
for t in times:
58975914
files.append('%s.%s' % (prefix, t))
5915+
for t in times:
5916+
files.append('a.log.%s.c' % t)
58985917
# Create empty files
58995918
for f in files:
59005919
(wd / f).touch()
@@ -5903,11 +5922,11 @@ def test_compute_files_to_delete_same_filename_different_extensions(self):
59035922
backupCount = i+1
59045923
rotator = rotators[i]
59055924
candidates = rotator.getFilesToDelete()
5906-
self.assertEqual(len(candidates), n_files - backupCount)
5907-
matcher = re.compile(r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$")
5925+
self.assertEqual(len(candidates), n_files - backupCount, candidates)
5926+
matcher = re.compile(r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\Z")
59085927
for c in candidates:
59095928
d, fn = os.path.split(c)
5910-
self.assertTrue(fn.startswith(prefix))
5929+
self.assertTrue(fn.startswith(prefix+'.'))
59115930
suffix = fn[(len(prefix)+1):]
59125931
self.assertRegex(suffix, matcher)
59135932

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)