Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from pathlib import Path
2import json
3from collections.abc import MutableMapping, Mapping
4from contextlib import contextmanager
5from ase.io.jsonio import read_json, write_json, encode
6from ase.utils import opencew
9def missing(key):
10 raise KeyError(key)
13class Locked(Exception):
14 pass
17class CacheLock:
18 def __init__(self, fd, key):
19 self.fd = fd
20 self.key = key
22 def save(self, value):
23 json_utf8 = encode(value).encode('utf-8')
24 try:
25 self.fd.write(json_utf8)
26 except Exception as ex:
27 raise RuntimeError(f'Failed to save {value} to cache') from ex
28 finally:
29 self.fd.close()
32class MultiFileJSONCache(MutableMapping):
33 writable = True
35 def __init__(self, directory):
36 self.directory = Path(directory)
38 def _filename(self, key):
39 return self.directory / f'cache.{key}.json'
41 def _glob(self):
42 return self.directory.glob('cache.*.json')
44 def __iter__(self):
45 for path in self._glob():
46 cache, key = path.stem.split('.', 1)
47 if cache != 'cache':
48 continue
49 yield key
51 def __len__(self):
52 # Very inefficient this, but not a big usecase.
53 return len(list(self._glob()))
55 @contextmanager
56 def lock(self, key):
57 self.directory.mkdir(exist_ok=True, parents=True)
58 path = self._filename(key)
59 fd = opencew(path)
60 try:
61 if fd is None:
62 yield None
63 else:
64 yield CacheLock(fd, key)
65 finally:
66 if fd is not None:
67 fd.close()
69 def __setitem__(self, key, value):
70 with self.lock(key) as handle:
71 if handle is None:
72 raise Locked(key)
73 handle.save(value)
75 def __getitem__(self, key):
76 path = self._filename(key)
77 try:
78 return read_json(path, always_array=False)
79 except FileNotFoundError:
80 missing(key)
81 except json.decoder.JSONDecodeError:
82 # May be partially written, which typically means empty
83 # because the file was locked with exclusive-write-open.
84 #
85 # Since we decide what keys we have based on which files exist,
86 # we are obligated to return a value for this case too.
87 # So we return None.
88 return None
90 def __delitem__(self, key):
91 try:
92 self._filename(key).unlink()
93 except FileNotFoundError:
94 missing(key)
96 def combine(self):
97 cache = CombinedJSONCache.dump_cache(self.directory, dict(self))
98 assert set(cache) == set(self)
99 self.clear()
100 assert len(self) == 0
101 return cache
103 def split(self):
104 return self
106 def filecount(self):
107 return len(self)
109 def strip_empties(self):
110 empties = [key for key, value in self.items() if value is None]
111 for key in empties:
112 del self[key]
113 return len(empties)
116class CombinedJSONCache(Mapping):
117 writable = False
119 def __init__(self, directory, dct):
120 self.directory = Path(directory)
121 self._dct = dict(dct)
123 def filecount(self):
124 return int(self._filename.is_file())
126 @property
127 def _filename(self):
128 return self.directory / 'combined.json'
130 def _dump_json(self):
131 target = self._filename
132 if target.exists():
133 raise RuntimeError(f'Already exists: {target}')
134 self.directory.mkdir(exist_ok=True, parents=True)
135 write_json(target, self._dct)
137 def __len__(self):
138 return len(self._dct)
140 def __iter__(self):
141 return iter(self._dct)
143 def __getitem__(self, index):
144 return self._dct[index]
146 @classmethod
147 def dump_cache(cls, path, dct):
148 cache = cls(path, dct)
149 cache._dump_json()
150 return cache
152 @classmethod
153 def load(cls, path):
154 # XXX Very hacky this one
155 cache = cls(path, {})
156 dct = read_json(cache._filename, always_array=False)
157 cache._dct.update(dct)
158 return cache
160 def clear(self):
161 self._filename.unlink()
162 self._dct.clear()
164 def combine(self):
165 return self
167 def split(self):
168 cache = MultiFileJSONCache(self.directory)
169 assert len(cache) == 0
170 cache.update(self)
171 assert set(cache) == set(self)
172 self.clear()
173 return cache
176def get_json_cache(directory):
177 try:
178 return CombinedJSONCache.load(directory)
179 except FileNotFoundError:
180 return MultiFileJSONCache(directory)