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
1# (c) Stefan Countryman 2019
3"""
4A mechanism for marking a file as locked, i.e. not eligible for automatic
5regeneration, or as obsolete, i.e. eligible for automatic regeneration. It can
6still manually be changed in either case.
7"""
9import os
10import logging
11import json
12from glob import glob
13import functools
14from collections import namedtuple
15LOGGER = logging.getLogger(__name__)
18class LockHandler(namedtuple('LockHandlerTuple', ('eventdir', 'manifest'))):
19 """
20 A class for marking a set of paths as locked, i.e. not eligible for
21 automated obsolescence and regeneration. At time of writing, this is
22 accomplished by creating an empty lockfile that
23 ``llama.filehandler.FileHandler.is_obsolete`` will check for. The
24 ``manifest`` should filenames (as returned by ``basename`` or
25 ``llama.filehandler.FileHandler.filename``). Is also used for precomputing
26 obsolescence values and storing the results in ``obsolescence_files``.
27 """
29 @property
30 def lockfiles(self):
31 """A list of lockfiles associated with this manifest. If any of these
32 exists, all files in the manifest are considered locked. Create these
33 files to mark them as locked."""
34 return ('.{}.lock'.format(fname) for fname in self.manifest)
36 @property
37 def obsolescence_files(self):
38 """A list of obsolescence file names associated with this manifest. If
39 any of these exists, the obsolescence status of the manifest will be
40 read from it. Create these files to store precomputed or manual
41 obsolescence values."""
42 return ('.{}.obsolescence'.format(fname) for fname in self.manifest)
44 @property
45 def lockpaths(self):
46 """Full paths to lockfiles for this manifest."""
47 return (os.path.join(self.eventdir, f) for f in self.lockfiles)
49 @property
50 def obsolescence_paths(self):
51 """Full paths to obsolescence files for this manifest."""
52 return (os.path.join(self.eventdir, f) for f in
53 self.obsolescence_files)
55 @property
56 def is_locked(self):
57 """Check whether the files in this manifest are locked."""
58 return any(os.path.isfile(f) for f in self.lockpaths)
60 def lock(self):
61 """Mark the files in this manifest as locked. Since locking prevents
62 obsolescence, marking a file as locked will also write cache the
63 obsolescence value as ``False``."""
64 LOGGER.debug("Locking files: %s", self.manifest)
65 for lockfile in self.lockpaths:
66 with open(lockfile, 'w') as _lock:
67 pass
68 self.record_obsolescence(False)
70 def unlock(self):
71 """Mark the files in this manifest as unlocked. Will remove any cached
72 obsolescence values (since obsolescence must now be recalculated)."""
73 LOGGER.debug("Unlocking files: %s", self.manifest)
74 self.remove_obsolescence()
75 for lockfile in self.lockpaths:
76 if os.path.isfile(lockfile):
77 os.remove(lockfile)
79 def record_obsolescence(self, obsolete):
80 """Store the file's obsolescence values. Will ignore any locks, which
81 separately veto obsolescence."""
82 LOGGER.debug("Obsoleting files: %s", self.manifest)
83 if not isinstance(obsolete, bool):
84 raise ValueError("Expected bool, got: {}".format(obsolete))
85 for obsfile in self.obsolescence_paths:
86 with open(obsfile, 'w') as obs:
87 json.dump(obsolete, obs)
89 def remove_obsolescence(self):
90 """Delete obsolescence files recording obsolescence state."""
91 for obsfile in self.obsolescence_paths:
92 if os.path.isfile(obsfile):
93 os.remove(obsfile)
95 def remove_all_obsolescence(self):
96 """Delete all obsolescence cache files for this event."""
97 for obsfile in glob(os.path.join(self.eventdir, '.*.obsolescence')):
98 os.remove(obsfile)
100 @property
101 def obsolescence(self):
102 """Get the precomputed obsolescence value for this manifest (if it
103 exists); raise an FileNotFoundError if none is available and a
104 ValueError if a valid value could not be read from the obsolescence
105 file."""
106 for obsfile in self.obsolescence_paths:
107 if os.path.isfile(obsfile):
108 with open(obsfile) as obs:
109 obsolete = json.load(obs)
110 if not isinstance(obsolete, bool):
111 raise ValueError(("Expected bool from file {}, instead "
112 "got: {}").format(obsfile, obsolete))
113 return obsolete
114 raise FileNotFoundError(("Could not find any obsolescence files for "
115 "this manifest: {}").format(self.manifest))
118class LockMixin(object):
119 """
120 A mixin that lets ``FileHandler``-like objects (producing a manifest and
121 eventdir) mark files as locked, i.e. inelligible for automatic obsolescence
122 checks and subsequent regeneration. This functionality is added via a
123 ``lock`` property on the ``FileHandler``-like object with sub-methods
124 implementing its functionality.
125 """
127 @property
128 def lock(self):
129 """Return a ``LockHandler`` instance corresponding to this
130 ``FileHandler`` and all other file paths in its ``manifest``."""
131 return LockHandler(self.eventdir, self.manifest)
134def cache_obsolescence(func):
135 """Decorator for a ``LockMixin`` instance's obsolescence check method that
136 marks all files in the manifest with the returned obsolescence value of the
137 decorated method. Immediately returns a cached obsolescence value if
138 stored."""
139 @functools.wraps(func)
140 def wrapper(self):
141 try:
142 return self.lock.obsolescence
143 except FileNotFoundError:
144 obsolete = func(self)
145 LOGGER.debug("Caching %s obsolescence state as: %s", self, obsolete)
146 self.lock.record_obsolescence(obsolete)
147 return obsolete
148 return wrapper
151def trash_obsolescence(func):
152 """Decorator for a ``LockMixin`` instance's generation method that removes
153 all obsolescence files for the instance's manifest. Used for cache
154 invalidation."""
155 @functools.wraps(func)
156 def wrapper(self, *args, **kwargs):
157 res = func(self, *args, **kwargs)
158 LOGGER.debug("Removing %s cached obsolescence files: %s", self,
159 self.lock.obsolescence_files)
160 self.lock.remove_obsolescence()
161 return res
162 return wrapper