Hide keyboard shortcuts

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 

2 

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""" 

8 

9import os 

10import logging 

11import json 

12from glob import glob 

13import functools 

14from collections import namedtuple 

15LOGGER = logging.getLogger(__name__) 

16 

17 

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 """ 

28 

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) 

35 

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) 

43 

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) 

48 

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) 

54 

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) 

59 

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) 

69 

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) 

78 

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) 

88 

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) 

94 

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) 

99 

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)) 

116 

117 

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 """ 

126 

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) 

132 

133 

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 

149 

150 

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