Add atomic write utility and tests

This commit is contained in:
thePR0M3TH3AN committed 2025-08-03 08:57:04 -04:00
1 parent 2294656f36
commit 032caed3d0
8 files changed
+123 -24

No files matched your search

+2
View File
@@ -35,6 +35,7 @@ try:
clear_and_print_fingerprint,
clear_header_with_notification,
)
from .atomic_write import atomic_write
if logger.isEnabledFor(logging.DEBUG):
logger.info("Modules imported successfully.")
@@ -68,4 +69,5 @@ __all__ = [
"clear_and_print_fingerprint",
"clear_header_with_notification",
"pause",
"atomic_write",
]
+62
View File
@@ -0,0 +1,62 @@
"""Utility helpers for performing atomic file writes.
This module provides a small helper function :func:`atomic_write` which
implements a simple pattern for writing files atomically. Data is written to a
temporary file in the same directory, flushed and synced to disk, and then
``os.replace`` is used to atomically move the temporary file into place.
The function accepts a callable ``write_func`` that receives the temporary file
object. This keeps the helper flexible enough to support both text and binary
writes and allows callers to perform complex serialisation steps (e.g. JSON
dumping) without exposing a partially written file to other processes.
"""
from __future__ import annotations
import os
import tempfile
from pathlib import Path
from typing import Callable, Any, IO
def atomic_write(
path: str | Path,
write_func: Callable[[IO[Any]], None],
*,
mode: str = "w",
**open_kwargs: Any,
) -> None:
"""Write to ``path`` atomically using ``write_func``.
Parameters
----------
path:
Destination file path.
write_func:
Callable that receives an open file object and performs the actual
write. The callable should not close the file.
mode:
File mode used when opening the temporary file. Defaults to ``"w"``.
**open_kwargs:
Additional keyword arguments passed to :func:`os.fdopen`.
"""
dest = Path(path)
dest.parent.mkdir(parents=True, exist_ok=True)
fd, tmp_path = tempfile.mkstemp(dir=str(dest.parent))
try:
with os.fdopen(fd, mode, **open_kwargs) as tmp_file:
write_func(tmp_file)
tmp_file.flush()
os.fsync(tmp_file.fileno())
os.replace(tmp_path, dest)
except Exception:
try:
os.unlink(tmp_path)
except FileNotFoundError:
pass
raise
__all__ = ["atomic_write"]
+4 -6
View File
@@ -21,6 +21,7 @@ from typing import Optional, Any
from termcolor import colored
from constants import APP_DIR, SCRIPT_CHECKSUM_FILE
from utils.atomic_write import atomic_write
# Instantiate the logger
logger = logging.getLogger(__name__)
@@ -121,8 +122,7 @@ def update_checksum(content: str, checksum_file_path: str) -> bool:
hasher = hashlib.sha256()
hasher.update(content.encode("utf-8"))
new_checksum = hasher.hexdigest()
with open(checksum_file_path, "w") as f:
f.write(new_checksum)
atomic_write(checksum_file_path, lambda f: f.write(new_checksum))
logging.debug(f"Updated checksum for '{checksum_file_path}' to: {new_checksum}")
return True
except Exception as e:
@@ -179,8 +179,7 @@ def initialize_checksum(file_path: str, checksum_file_path: str) -> bool:
return False
try:
with open(checksum_file_path, "w") as f:
f.write(checksum)
atomic_write(checksum_file_path, lambda f: f.write(checksum))
logging.debug(
f"Initialized checksum file '{checksum_file_path}' with checksum: {checksum}"
)
@@ -206,8 +205,7 @@ def update_checksum_file(file_path: str, checksum_file_path: str) -> bool:
if checksum is None:
return False
try:
with open(checksum_file_path, "w") as f:
f.write(checksum)
atomic_write(checksum_file_path, lambda f: f.write(checksum))
logging.debug(
f"Updated checksum for '{file_path}' to '{checksum}' at '{checksum_file_path}'."
)
+10 -10
View File
@@ -9,6 +9,7 @@ from typing import List, Optional
import shutil # Ensure shutil is imported if used within the class
from utils.atomic_write import atomic_write
from utils.fingerprint import generate_fingerprint
# Instantiate the logger
@@ -92,16 +93,15 @@ class FingerprintManager:
Saves the current list of fingerprints to the fingerprints.json file.
"""
try:
with open(self.fingerprints_file, "w") as f:
json.dump(
{
"fingerprints": self.fingerprints,
"last_used": self.current_fingerprint,
"names": self.names,
},
f,
indent=4,
)
data = {
"fingerprints": self.fingerprints,
"last_used": self.current_fingerprint,
"names": self.names,
}
atomic_write(
self.fingerprints_file,
lambda f: json.dump(data, f, indent=4),
)
logger.debug(
f"Fingerprints saved: {self.fingerprints} (last used: {self.current_fingerprint})"
)