Skip to content

Commit 3bfcfdd

Browse files
committed
quick file rotation util
1 parent 60ecb20 commit 3bfcfdd

File tree

2 files changed

+93
-2
lines changed

2 files changed

+93
-2
lines changed

boltons/fileutils.py

+36-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def __set__(self, fp_obj, value):
138138
' or one or more of %r'
139139
% (invalid_chars, value, self._perm_chars))
140140

141-
sort_key = lambda c: self._perm_val[c]
141+
def sort_key(c): return self._perm_val[c]
142142
new_value = ''.join(sorted(set(value),
143143
key=sort_key, reverse=True))
144144
setattr(fp_obj, self.attribute, new_value)
@@ -687,3 +687,38 @@ def __enter__(self):
687687
def __exit__(self, exc_type, exc_val, exc_tb):
688688
return
689689

690+
691+
def rotate_file(filename, *, keep: int = 5):
692+
"""
693+
If *filename.ext* exists, it will be moved to *filename.1.ext*,
694+
with all conflicting filenames being moved up by one, dropping any files beyond *keep*.
695+
696+
After rotation, *filename* will be available for creation as a new file.
697+
698+
Fails if *filename* is not a file or if *keep* is not > 0.
699+
"""
700+
if keep < 1:
701+
raise ValueError(f'expected "keep" to be >=1, not {keep}')
702+
if not os.path.exists(filename):
703+
return
704+
if not os.path.isfile(filename):
705+
raise ValueError(f'expected {filename} to be a file')
706+
707+
fn_root, fn_ext = os.path.splitext(filename)
708+
kept_names = []
709+
for i in range(1, keep + 1):
710+
if fn_ext:
711+
kept_names.append(f'{fn_root}.{i}{fn_ext}')
712+
else:
713+
kept_names.append(f'{fn_root}.{i}')
714+
715+
fns = [filename] + kept_names
716+
for orig_name, kept_name in reversed(list(zip(fns, fns[1:]))):
717+
if not os.path.exists(orig_name):
718+
continue
719+
os.rename(orig_name, kept_name)
720+
721+
if os.path.exists(kept_names[-1]):
722+
os.remove(kept_names[-1])
723+
724+
return

tests/test_fileutils.py

+57-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import os.path
22

3+
4+
5+
6+
37
from boltons import fileutils
48
from boltons.fileutils import FilePerms, iter_find_files
59

@@ -33,4 +37,56 @@ def _to_baseless_list(paths):
3337

3438
boltons_parent = os.path.dirname(BOLTONS_PATH)
3539
assert 'fileutils.py' in _to_baseless_list(iter_find_files(boltons_parent, patterns=['*.py']))
36-
assert 'fileutils.py' not in _to_baseless_list(iter_find_files(boltons_parent, patterns=['*.py'], max_depth=0))
40+
assert 'fileutils.py' not in _to_baseless_list(iter_find_files(boltons_parent, patterns=['*.py'], max_depth=0))
41+
42+
43+
def test_rotate_file_no_rotation(tmp_path):
44+
file_path = tmp_path / 'test_file.txt'
45+
fileutils.rotate_file(file_path)
46+
assert not file_path.exists()
47+
48+
49+
def test_rotate_file_one_rotation(tmp_path):
50+
file_path = tmp_path / 'test_file.txt'
51+
file_path.write_text('test content')
52+
assert file_path.exists()
53+
54+
fileutils.rotate_file(file_path)
55+
assert not file_path.exists()
56+
assert (tmp_path / 'test_file.1.txt').exists()
57+
58+
59+
def test_rotate_file_full_rotation(tmp_path):
60+
file_path = tmp_path / 'test_file.txt'
61+
file_path.write_text('test content 0')
62+
for i in range(1, 5):
63+
cur_path = tmp_path / f'test_file.{i}.txt'
64+
cur_path.write_text(f'test content {i}')
65+
assert cur_path.exists()
66+
67+
fileutils.rotate_file(file_path, keep=5)
68+
assert not file_path.exists()
69+
70+
for i in range(1, 5):
71+
cur_path = tmp_path / f'test_file.{i}.txt'
72+
assert cur_path.read_text() == f'test content {i-1}'
73+
74+
assert not (tmp_path / 'test_file.5.txt').exists()
75+
76+
def test_rotate_file_full_rotation_no_ext(tmp_path):
77+
file_path = tmp_path / 'test_file'
78+
file_path.write_text('test content 0')
79+
for i in range(1, 5):
80+
cur_path = tmp_path / f'test_file.{i}'
81+
cur_path.write_text(f'test content {i}')
82+
assert cur_path.exists()
83+
84+
fileutils.rotate_file(file_path, keep=5)
85+
assert not file_path.exists()
86+
87+
for i in range(1, 5):
88+
cur_path = tmp_path / f'test_file.{i}'
89+
assert cur_path.read_text() == f'test content {i-1}'
90+
91+
assert not (tmp_path / 'test_file.5').exists()
92+

0 commit comments

Comments
 (0)