Skip to content
This repository was archived by the owner on Apr 17, 2026. It is now read-only.

Commit a2c6578

Browse files
committed
fix: avoid Windows permission error during atomic save
1 parent 9c5dff4 commit a2c6578

2 files changed

Lines changed: 28 additions & 3 deletions

File tree

dbase/__init__.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,21 +208,31 @@ def _save_data(self) -> None:
208208

209209
data = self._collect_public_data()
210210

211+
tmp_path = f"{self._file_path}.tmp"
211212
try:
212213
payload = self._encode_payload(data)
213-
tmp_path = f"{self._file_path}.tmp"
214214
with open(tmp_path, 'w', encoding='utf-8') as tmp_file:
215215
json.dump(payload, tmp_file, indent=2, ensure_ascii=False)
216216
tmp_file.flush()
217217
os.fsync(tmp_file.fileno())
218218

219-
os.replace(tmp_path, self._file_path)
220-
221219
if self._file and not self._file.closed:
222220
self._file.close()
221+
222+
os.replace(tmp_path, self._file_path)
223223
object.__setattr__(self, '_file', open(self._file_path, 'r+', encoding='utf-8'))
224224
except Exception as error:
225225
self._log(f"{get_message('save_data_error')}: {str(error)}", 'ERROR')
226+
if os.path.exists(tmp_path):
227+
try:
228+
os.remove(tmp_path)
229+
except Exception:
230+
pass
231+
if self._file is None or self._file.closed:
232+
try:
233+
object.__setattr__(self, '_file', open(self._file_path, 'r+', encoding='utf-8'))
234+
except Exception:
235+
pass
226236

227237
def close(self) -> None:
228238
self._save_data()

tests/test_database_core.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,18 @@ def test_temp_file_cleanup():
4747
db_key_path = db.get_file_path()
4848
assert os.path.exists(db_key_path)
4949
assert not os.path.exists(db_key_path)
50+
51+
52+
def test_save_closes_file_before_replace(tmp_path, monkeypatch):
53+
path = tmp_path / 'win-safe.json'
54+
db = DataBase(str(path), show_logs=False)
55+
56+
original_replace = os.replace
57+
58+
def checked_replace(src, dst):
59+
assert db.get_file().closed is True
60+
return original_replace(src, dst)
61+
62+
monkeypatch.setattr(os, 'replace', checked_replace)
63+
db.key = 'value'
64+
assert db.key == 'value'

0 commit comments

Comments
 (0)