0
# Utilities
1
2
Additional utilities for atomic file operations and helper functions that complement the core locking functionality.
3
4
## Capabilities
5
6
### Atomic File Operations
7
8
Utility for atomic file writing that ensures files are written completely or not at all, preventing corruption from interrupted writes.
9
10
```python { .api }
11
def open_atomic(filename: Filename, binary: bool = True) -> typing.Iterator[typing.IO]:
12
"""
13
Context manager for atomic file writing using temporary files.
14
15
Instead of locking, this method allows you to write the entire file
16
and atomically move it to the final location using os.rename().
17
18
Parameters:
19
- filename: Target filename (str or pathlib.Path)
20
- binary: If True, open in binary mode ('wb'), else text mode ('w')
21
22
Yields:
23
- File handle for writing to temporary file
24
25
Raises:
26
- AssertionError: If target file already exists
27
28
Note:
29
- Creates parent directories if they don't exist
30
- Uses os.rename() which is atomic on most platforms
31
- Temporary file is created in same directory as target
32
- Automatic cleanup if operation fails
33
"""
34
```
35
36
### Usage Examples
37
38
Basic atomic file writing:
39
40
```python
41
import portalocker
42
43
# Write file atomically - either complete file is written or nothing
44
with portalocker.open_atomic('config.json') as fh:
45
import json
46
config_data = {'key': 'value', 'settings': {'debug': True}}
47
json.dump(config_data, fh)
48
# File only becomes visible at config.json when context exits successfully
49
50
# If anything goes wrong during writing, config.json won't be created/modified
51
```
52
53
Text mode atomic writing:
54
55
```python
56
import portalocker
57
58
# Write text file atomically
59
with portalocker.open_atomic('output.txt', binary=False) as fh:
60
fh.write('Line 1\n')
61
fh.write('Line 2\n')
62
fh.write('Final line\n')
63
# File is complete when context exits
64
```
65
66
Atomic file updates:
67
68
```python
69
import portalocker
70
import json
71
import os
72
73
def update_config_file(filename, new_settings):
74
"""Atomically update a JSON config file"""
75
76
# Read existing config
77
existing_config = {}
78
if os.path.exists(filename):
79
with open(filename, 'r') as fh:
80
existing_config = json.load(fh)
81
82
# Merge with new settings
83
existing_config.update(new_settings)
84
85
# Write atomically - if this fails, original file is unchanged
86
with portalocker.open_atomic(filename, binary=False) as fh:
87
json.dump(existing_config, fh, indent=2)
88
fh.write('\n')
89
90
# Usage
91
update_config_file('app_config.json', {'debug': False, 'version': '2.0'})
92
```
93
94
Binary file atomic operations:
95
96
```python
97
import portalocker
98
import pickle
99
100
def save_data_atomically(filename, data):
101
"""Save Python object to file atomically"""
102
with portalocker.open_atomic(filename, binary=True) as fh:
103
pickle.dump(data, fh)
104
105
# Save large data structure atomically
106
large_dataset = {'users': [...], 'transactions': [...]}
107
save_data_atomically('dataset.pkl', large_dataset)
108
```
109
110
Working with pathlib.Path:
111
112
```python
113
import portalocker
114
import pathlib
115
116
# Works with pathlib.Path objects
117
data_dir = pathlib.Path('/data/exports')
118
output_file = data_dir / 'report.csv'
119
120
with portalocker.open_atomic(output_file, binary=False) as fh:
121
fh.write('column1,column2,column3\n')
122
fh.write('value1,value2,value3\n')
123
# Parent directories created automatically if needed
124
```
125
126
Error handling and cleanup:
127
128
```python
129
import portalocker
130
import json
131
132
try:
133
with portalocker.open_atomic('critical_data.json') as fh:
134
# If this raises an exception, the target file remains untouched
135
data = generate_critical_data()
136
json.dump(data, fh)
137
138
# Simulate an error during writing
139
if should_fail():
140
raise ValueError("Processing failed")
141
142
except ValueError as e:
143
print(f"Writing failed: {e}")
144
# critical_data.json was not created/modified
145
# Temporary file was automatically cleaned up
146
```
147
148
Atomic replacement of existing files:
149
150
```python
151
import portalocker
152
import shutil
153
154
def atomic_file_replacement(source_file, target_file):
155
"""Atomically replace target file with processed version of source"""
156
157
# Process source and write to target atomically
158
with open(source_file, 'r') as src:
159
with portalocker.open_atomic(target_file, binary=False) as dst:
160
# Process and write line by line
161
for line in src:
162
processed_line = process_line(line)
163
dst.write(processed_line)
164
165
# If we get here, target_file now contains the processed version
166
print(f"Successfully replaced {target_file}")
167
168
# Usage
169
atomic_file_replacement('input.log', 'processed.log')
170
```
171
172
## Comparison with File Locking
173
174
Atomic file operations vs file locking serve different purposes:
175
176
```python
177
import portalocker
178
179
# File locking: Multiple processes coordinate access to same file
180
def append_with_locking(filename, data):
181
"""Multiple processes can safely append to the same file"""
182
with portalocker.Lock(filename, 'a') as fh:
183
fh.write(data + '\n')
184
fh.flush()
185
186
# Atomic writing: Ensure file is completely written or not at all
187
def replace_with_atomic(filename, data):
188
"""Ensure file contains complete data or doesn't exist"""
189
with portalocker.open_atomic(filename, binary=False) as fh:
190
fh.write(data)
191
192
# Use locking for shared access
193
append_with_locking('shared.log', 'Process 1 data')
194
append_with_locking('shared.log', 'Process 2 data')
195
196
# Use atomic for complete replacement
197
replace_with_atomic('config.json', '{"version": "1.0"}')
198
```
199
200
## Platform Considerations
201
202
The atomic nature depends on the underlying filesystem:
203
204
```python
205
import portalocker
206
import os
207
208
# os.rename() is atomic on most POSIX systems and Windows
209
# but behavior may vary on network filesystems
210
211
def safe_atomic_write(filename, data):
212
"""Write with awareness of platform limitations"""
213
try:
214
with portalocker.open_atomic(filename, binary=False) as fh:
215
fh.write(data)
216
return True
217
except Exception as e:
218
print(f"Atomic write failed: {e}")
219
return False
220
221
# For critical applications, consider combining with locking
222
def ultra_safe_write(filename, data):
223
"""Combine atomic writing with file locking"""
224
lock_file = filename + '.lock'
225
226
with portalocker.Lock(lock_file, 'w'):
227
# Only one process can write at a time
228
with portalocker.open_atomic(filename, binary=False) as fh:
229
fh.write(data)
230
```
231
232
## Type Definitions
233
234
```python { .api }
235
from typing import Union, Iterator
236
import pathlib
237
import typing
238
239
# Filename type (same as used by Lock classes)
240
Filename = Union[str, pathlib.Path]
241
242
# Generic IO type for file handles
243
IO = Union[typing.IO[str], typing.IO[bytes]]
244
```