forked from cool-RR/python_toolbox
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfile_tools.py
More file actions
185 lines (145 loc) · 5.97 KB
/
file_tools.py
File metadata and controls
185 lines (145 loc) · 5.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# Copyright 2009-2017 Ram Rachum.
# This program is distributed under the MIT license.
from __future__ import generator_stop
import pathlib
import os
import re
from python_toolbox import cute_iter_tools
from python_toolbox import context_management
N_MAX_ATTEMPTS = 100
numbered_name_pattern = re.compile(
r'''(?P<raw_name>.*) \((?P<number>[0-9]+)\)'''
)
def _get_next_path(path):
r'''
Get the name that `path` should be renamed to if taken.
For example, "c:\example.ogg" would become "c:\example (1).ogg", while
"c:\example (1).ogg" would become "c:\example (2).ogg".
(Uses `Path` objects rather than strings.)
'''
assert isinstance(path, pathlib.Path)
suffix = path.suffix
stem = path.stem
parent_with_separator = str(path)[:-len(path.name)]
assert pathlib.Path('{}{}{}'.format(parent_with_separator,
stem, suffix)) == path
match = numbered_name_pattern.match(stem)
if match:
fixed_stem = '{} ({})'.format(
match.group('raw_name'),
int(match.group('number'))+1,
)
else:
fixed_stem = '{} (1)'.format(stem,)
return pathlib.Path(
'{}{}{}'.format(parent_with_separator, fixed_stem, suffix)
)
def iterate_file_paths(path):
r'''
Iterate over file paths, hoping to find one that's available.
For example, when given "c:\example.ogg", would first yield
"c:\example.ogg", then "c:\example (1).ogg", then "c:\example (2).ogg", and
so on.
(Uses `Path` objects rather than strings.)
'''
while True:
yield path
path = _get_next_path(path)
def create_folder_renaming_if_taken(path):
'''
Create a new folder with name `path`, renaming it if name taken.
If the name given is "example", the new name would be "example (1)", and if
that's taken "example (2)", and so on.
Returns a path object to the newly-created folder.
'''
for path in cute_iter_tools.shorten(iterate_file_paths(pathlib.Path(path)),
N_MAX_ATTEMPTS):
try:
os.makedirs(str(path), exist_ok=False)
except OSError:
pass
else:
return path
else:
raise Exception("Exceeded {} tries, can't create folder {}".format(
N_MAX_ATTEMPTS,
path
))
def create_file_renaming_if_taken(path, mode='x',
buffering=-1, encoding=None,
errors=None, newline=None):
'''
Create a new file with name `path` for writing, renaming it if name taken.
If the name given is "example.zip", the new name would be "example
(1).zip", and if that's taken "example (2).zip", and so on.
Returns the file open and ready for writing. It's best to use this as a
context manager similarly to `open` so the file would be closed.
'''
assert 'x' in mode
for path in cute_iter_tools.shorten(iterate_file_paths(pathlib.Path(path)),
N_MAX_ATTEMPTS):
try:
return path.open(mode, buffering=buffering,
encoding=encoding, errors=errors,
newline=newline)
except FileExistsError:
pass
else:
raise Exception("Exceeded {} tries, can't create file {}".format(
N_MAX_ATTEMPTS,
path
))
def write_to_file_renaming_if_taken(path, data, mode='x',
buffering=-1, encoding=None,
errors=None, newline=None):
'''
Write `data` to a new file with name `path`, renaming it if name taken.
If the name given is "example.zip", the new name would be "example
(1).zip", and if that's taken "example (2).zip", and so on.
'''
with create_file_renaming_if_taken(
path, mode=mode, buffering=buffering, encoding=encoding, errors=errors,
newline=newline) as file:
return file.write(data)
def atomic_create_and_write(path, data=None, binary=False):
'''
Write data to file, but use a temporary file as a buffer.
The data you write to this file is actuall written to a temporary file in
the same folder, and only after you close it, without having an exception
raised, it renames the temporary file to your original file name. If an
exception was raised during writing it deletes the temporary file.
This way you're sure you're not getting a half-baked file.
'''
with atomic_create(path, binary=binary) as file:
return file.write(data)
@context_management.ContextManagerType
def atomic_create(path, binary=False):
'''
Create a file for writing, but use a temporary file as a buffer.
Use as a context manager:
with atomic_create(path) as my_file:
my_file.write('Whatever')
When you write to this file it actually writes to a temporary file in the
same folder, and only after you close it, without having an exception
raised, it renames the temporary file to your original file name. If an
exception was raised during writing it deletes the temporary file.
This way you're sure you're not getting a half-baked file.
'''
path = pathlib.Path(path)
if path.exists():
raise FileExistsError(f"There's already a file called {path}")
desired_temp_file_path = path.parent / f'._{path.stem}.tmp'
try:
with create_file_renaming_if_taken(desired_temp_file_path,
'xb' if binary else 'x') as temp_file:
actual_temp_file_path = pathlib.Path(temp_file.name)
yield temp_file
# This part runs only if there was no exception when writing to the
# file:
if path.exists():
raise FileExistsError(f"There's already a file called {path}")
actual_temp_file_path.rename(path)
assert path.exists()
finally:
if actual_temp_file_path.exists():
actual_temp_file_path.unlink()