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

Commit 5f188c6

Browse files
committed
fix: make save robust against transient Windows file locks
1 parent a2c6578 commit 5f188c6

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

dbase/__init__.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import base64
44
import json
55
import os
6+
import time
67
from tempfile import NamedTemporaryFile
78
from typing import Any
89

@@ -202,6 +203,19 @@ def check_file_exists(file_path: str) -> bool:
202203
except Exception:
203204
return False
204205

206+
207+
def _replace_with_retry(self, src: str, dst: str, retries: int = 8, delay: float = 0.05) -> None:
208+
last_error = None
209+
for _ in range(retries):
210+
try:
211+
os.replace(src, dst)
212+
return
213+
except PermissionError as error:
214+
last_error = error
215+
time.sleep(delay)
216+
if last_error is not None:
217+
raise last_error
218+
205219
def _save_data(self) -> None:
206220
if self._file is None or self._file_path is None:
207221
return
@@ -219,7 +233,7 @@ def _save_data(self) -> None:
219233
if self._file and not self._file.closed:
220234
self._file.close()
221235

222-
os.replace(tmp_path, self._file_path)
236+
self._replace_with_retry(tmp_path, self._file_path)
223237
object.__setattr__(self, '_file', open(self._file_path, 'r+', encoding='utf-8'))
224238
except Exception as error:
225239
self._log(f"{get_message('save_data_error')}: {str(error)}", 'ERROR')
@@ -235,9 +249,11 @@ def _save_data(self) -> None:
235249
pass
236250

237251
def close(self) -> None:
238-
self._save_data()
252+
if self._file is not None:
253+
self._save_data()
239254
if self._file and not self._file.closed:
240255
self._file.close()
256+
object.__setattr__(self, '_file', None)
241257
if self._is_temp and self._auto_cleanup_temp and self._file_path:
242258
try:
243259
if os.path.exists(self._file_path):

tests/test_database_core.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,31 @@ def checked_replace(src, dst):
6262
monkeypatch.setattr(os, 'replace', checked_replace)
6363
db.key = 'value'
6464
assert db.key == 'value'
65+
66+
67+
def test_save_retries_on_permission_error(tmp_path, monkeypatch):
68+
path = tmp_path / 'retry.json'
69+
db = DataBase(str(path), show_logs=False)
70+
71+
calls = {'count': 0}
72+
original_replace = os.replace
73+
74+
def flaky_replace(src, dst):
75+
calls['count'] += 1
76+
if calls['count'] < 3:
77+
raise PermissionError('locked')
78+
return original_replace(src, dst)
79+
80+
monkeypatch.setattr(os, 'replace', flaky_replace)
81+
db.value = 1
82+
assert db.value == 1
83+
assert calls['count'] == 3
84+
85+
86+
def test_close_is_idempotent(tmp_path):
87+
path = tmp_path / 'close.json'
88+
db = DataBase(str(path), show_logs=False)
89+
db.value = 'ok'
90+
db.close()
91+
db.close()
92+
assert path.exists()

0 commit comments

Comments
 (0)