summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/spack/spack/test/util/file_cache.py10
-rw-r--r--lib/spack/spack/util/file_cache.py88
2 files changed, 58 insertions, 40 deletions
diff --git a/lib/spack/spack/test/util/file_cache.py b/lib/spack/spack/test/util/file_cache.py
index cd9ce1c5b8..4af8a1ed03 100644
--- a/lib/spack/spack/test/util/file_cache.py
+++ b/lib/spack/spack/test/util/file_cache.py
@@ -31,6 +31,11 @@ def test_write_and_read_cache_file(file_cache):
assert text == "foobar\n"
+def test_read_before_init(file_cache):
+ with file_cache.read_transaction("test.yaml") as stream:
+ assert stream is None
+
+
@pytest.mark.not_on_windows("Locks not supported on Windows")
def test_failed_write_and_read_cache_file(file_cache):
"""Test failing to write then attempting to read a cached file."""
@@ -46,11 +51,6 @@ def test_failed_write_and_read_cache_file(file_cache):
# File does not exist
assert not file_cache.init_entry("test.yaml")
- # Attempting to read will cause a FileNotFoundError
- with pytest.raises(FileNotFoundError, match=r"test\.yaml"):
- with file_cache.read_transaction("test.yaml"):
- pass
-
def test_write_and_remove_cache_file(file_cache):
"""Test two write transactions on a cached file. Then try to remove an
diff --git a/lib/spack/spack/util/file_cache.py b/lib/spack/spack/util/file_cache.py
index bc75704d75..e72d431f97 100644
--- a/lib/spack/spack/util/file_cache.py
+++ b/lib/spack/spack/util/file_cache.py
@@ -7,6 +7,7 @@ import errno
import math
import os
import shutil
+from typing import IO, Optional, Tuple
from llnl.util.filesystem import mkdirp, rename
@@ -14,6 +15,51 @@ from spack.error import SpackError
from spack.util.lock import Lock, ReadTransaction, WriteTransaction
+def _maybe_open(path: str) -> Optional[IO[str]]:
+ try:
+ return open(path, "r")
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+ return None
+
+
+class ReadContextManager:
+ def __init__(self, path: str) -> None:
+ self.path = path
+
+ def __enter__(self) -> Optional[IO[str]]:
+ """Return a file object for the cache if it exists."""
+ self.cache_file = _maybe_open(self.path)
+ return self.cache_file
+
+ def __exit__(self, type, value, traceback):
+ if self.cache_file:
+ self.cache_file.close()
+
+
+class WriteContextManager:
+ def __init__(self, path: str) -> None:
+ self.path = path
+ self.tmp_path = f"{self.path}.tmp"
+
+ def __enter__(self) -> Tuple[Optional[IO[str]], IO[str]]:
+ """Return (old_file, new_file) file objects, where old_file is optional."""
+ self.old_file = _maybe_open(self.path)
+ self.new_file = open(self.tmp_path, "w")
+ return self.old_file, self.new_file
+
+ def __exit__(self, type, value, traceback):
+ if self.old_file:
+ self.old_file.close()
+ self.new_file.close()
+
+ if value:
+ os.remove(self.tmp_path)
+ else:
+ rename(self.tmp_path, self.path)
+
+
class FileCache:
"""This class manages cached data in the filesystem.
@@ -107,7 +153,8 @@ class FileCache:
cache_file.read()
"""
- return ReadTransaction(self._get_lock(key), acquire=lambda: open(self.cache_path(key)))
+ path = self.cache_path(key)
+ return ReadTransaction(self._get_lock(key), acquire=lambda: ReadContextManager(path))
def write_transaction(self, key):
"""Get a write transaction on a file cache item.
@@ -117,40 +164,11 @@ class FileCache:
moves the file into place on top of the old file atomically.
"""
- filename = self.cache_path(key)
- if os.path.exists(filename) and not os.access(filename, os.W_OK):
- raise CacheError(
- "Insufficient permissions to write to file cache at {0}".format(filename)
- )
-
- # TODO: this nested context manager adds a lot of complexity and
- # TODO: is pretty hard to reason about in llnl.util.lock. At some
- # TODO: point we should just replace it with functions and simplify
- # TODO: the locking code.
- class WriteContextManager:
- def __enter__(cm):
- cm.orig_filename = self.cache_path(key)
- cm.orig_file = None
- if os.path.exists(cm.orig_filename):
- cm.orig_file = open(cm.orig_filename, "r")
-
- cm.tmp_filename = self.cache_path(key) + ".tmp"
- cm.tmp_file = open(cm.tmp_filename, "w")
-
- return cm.orig_file, cm.tmp_file
-
- def __exit__(cm, type, value, traceback):
- if cm.orig_file:
- cm.orig_file.close()
- cm.tmp_file.close()
-
- if value:
- os.remove(cm.tmp_filename)
-
- else:
- rename(cm.tmp_filename, cm.orig_filename)
-
- return WriteTransaction(self._get_lock(key), acquire=WriteContextManager)
+ path = self.cache_path(key)
+ if os.path.exists(path) and not os.access(path, os.W_OK):
+ raise CacheError(f"Insufficient permissions to write to file cache at {path}")
+
+ return WriteTransaction(self._get_lock(key), acquire=lambda: WriteContextManager(path))
def mtime(self, key) -> float:
"""Return modification time of cache file, or -inf if it does not exist.