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# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21from __future__ import annotations 

22 

23__all__ = ( 

24 "allSlots", 

25 "getClassOf", 

26 "getFullTypeName", 

27 "getInstanceOf", 

28 "immutable", 

29 "iterable", 

30 "safeMakeDir", 

31 "Singleton", 

32 "stripIfNotNone", 

33 "transactional", 

34) 

35 

36import errno 

37import os 

38import builtins 

39import functools 

40import re 

41from typing import ( 

42 Any, 

43 Callable, 

44 Dict, 

45 Iterable, 

46 Iterator, 

47 List, 

48 Mapping, 

49 Optional, 

50 Type, 

51 TypeVar, 

52 Union, 

53) 

54 

55from lsst.utils import doImport 

56 

57 

58def safeMakeDir(directory: str) -> None: 

59 """Make a directory in a manner avoiding race conditions""" 

60 if directory != "" and not os.path.exists(directory): 

61 try: 

62 os.makedirs(directory) 

63 except OSError as e: 

64 # Don't fail if directory exists due to race 

65 if e.errno != errno.EEXIST: 

66 raise e 

67 

68 

69def iterable(a: Any) -> Iterable[Any]: 

70 """Make input iterable. 

71 

72 There are three cases, when the input is: 

73 

74 - iterable, but not a `str` or Mapping -> iterate over elements 

75 (e.g. ``[i for i in a]``) 

76 - a `str` -> return single element iterable (e.g. ``[a]``) 

77 - a Mapping -> return single element iterable 

78 - not iterable -> return single elment iterable (e.g. ``[a]``). 

79 

80 Parameters 

81 ---------- 

82 a : iterable or `str` or not iterable 

83 Argument to be converted to an iterable. 

84 

85 Returns 

86 ------- 

87 i : `generator` 

88 Iterable version of the input value. 

89 """ 

90 if isinstance(a, str): 

91 yield a 

92 return 

93 if isinstance(a, Mapping): 

94 yield a 

95 return 

96 try: 

97 yield from a 

98 except Exception: 

99 yield a 

100 

101 

102def allSlots(self: Any) -> Iterator[str]: 

103 """ 

104 Return combined ``__slots__`` for all classes in objects mro. 

105 

106 Parameters 

107 ---------- 

108 self : `object` 

109 Instance to be inspected. 

110 

111 Returns 

112 ------- 

113 slots : `itertools.chain` 

114 All the slots as an iterable. 

115 """ 

116 from itertools import chain 

117 return chain.from_iterable(getattr(cls, "__slots__", []) for cls in self.__class__.__mro__) 

118 

119 

120def getFullTypeName(cls: Any) -> str: 

121 """Return full type name of the supplied entity. 

122 

123 Parameters 

124 ---------- 

125 cls : `type` or `object` 

126 Entity from which to obtain the full name. Can be an instance 

127 or a `type`. 

128 

129 Returns 

130 ------- 

131 name : `str` 

132 Full name of type. 

133 

134 Notes 

135 ----- 

136 Builtins are returned without the ``builtins`` specifier included. This 

137 allows `str` to be returned as "str" rather than "builtins.str". Any 

138 parts of the path that start with a leading underscore are removed 

139 on the assumption that they are an implementation detail and the 

140 entity will be hoisted into the parent namespace. 

141 """ 

142 # If we have an instance we need to convert to a type 

143 if not hasattr(cls, "__qualname__"): 143 ↛ 144line 143 didn't jump to line 144, because the condition on line 143 was never true

144 cls = type(cls) 

145 if hasattr(builtins, cls.__qualname__): 

146 # Special case builtins such as str and dict 

147 return cls.__qualname__ 

148 

149 real_name = cls.__module__ + "." + cls.__qualname__ 

150 

151 # Remove components with leading underscores 

152 cleaned_name = ".".join(c for c in real_name.split(".") if not c.startswith("_")) 

153 

154 # Consistency check 

155 if real_name != cleaned_name: 155 ↛ 156line 155 didn't jump to line 156, because the condition on line 155 was never true

156 try: 

157 test = doImport(cleaned_name) 

158 except Exception: 

159 # Could not import anything so return the real name 

160 return real_name 

161 

162 # The thing we imported should match the class we started with 

163 # despite the clean up. If it does not we return the real name 

164 if test is not cls: 

165 return real_name 

166 

167 return cleaned_name 

168 

169 

170def getClassOf(typeOrName: Union[Type, str]) -> Type: 

171 """Given the type name or a type, return the python type. 

172 

173 If a type name is given, an attempt will be made to import the type. 

174 

175 Parameters 

176 ---------- 

177 typeOrName : `str` or Python class 

178 A string describing the Python class to load or a Python type. 

179 

180 Returns 

181 ------- 

182 type_ : `type` 

183 Directly returns the Python type if a type was provided, else 

184 tries to import the given string and returns the resulting type. 

185 

186 Notes 

187 ----- 

188 This is a thin wrapper around `~lsst.utils.doImport`. 

189 """ 

190 if isinstance(typeOrName, str): 

191 cls = doImport(typeOrName) 

192 else: 

193 cls = typeOrName 

194 return cls 

195 

196 

197def getInstanceOf(typeOrName: Union[Type, str], *args: Any, **kwargs: Any) -> Any: 

198 """Given the type name or a type, instantiate an object of that type. 

199 

200 If a type name is given, an attempt will be made to import the type. 

201 

202 Parameters 

203 ---------- 

204 typeOrName : `str` or Python class 

205 A string describing the Python class to load or a Python type. 

206 args : `tuple` 

207 Positional arguments to use pass to the object constructor. 

208 kwargs : `dict` 

209 Keyword arguments to pass to object constructor. 

210 

211 Returns 

212 ------- 

213 instance : `object` 

214 Instance of the requested type, instantiated with the provided 

215 parameters. 

216 """ 

217 cls = getClassOf(typeOrName) 

218 return cls(*args, **kwargs) 

219 

220 

221class Singleton(type): 

222 """Metaclass to convert a class to a Singleton. 

223 

224 If this metaclass is used the constructor for the singleton class must 

225 take no arguments. This is because a singleton class will only accept 

226 the arguments the first time an instance is instantiated. 

227 Therefore since you do not know if the constructor has been called yet it 

228 is safer to always call it with no arguments and then call a method to 

229 adjust state of the singleton. 

230 """ 

231 

232 _instances: Dict[Type, Any] = {} 

233 

234 # Signature is intentionally not substitutable for type.__call__ (no *args, 

235 # **kwargs) to require classes that use this metaclass to have no 

236 # constructor arguments. 

237 def __call__(cls) -> Any: # type: ignore 

238 if cls not in cls._instances: 

239 cls._instances[cls] = super(Singleton, cls).__call__() 

240 return cls._instances[cls] 

241 

242 

243F = TypeVar("F", bound=Callable) 

244 

245 

246def transactional(func: F) -> F: 

247 """Decorator that wraps a method and makes it transactional. 

248 

249 This depends on the class also defining a `transaction` method 

250 that takes no arguments and acts as a context manager. 

251 """ 

252 @functools.wraps(func) 

253 def inner(self: Any, *args: Any, **kwargs: Any) -> Any: 

254 with self.transaction(): 

255 return func(self, *args, **kwargs) 

256 return inner # type: ignore 

257 

258 

259def stripIfNotNone(s: Optional[str]) -> Optional[str]: 

260 """Strip leading and trailing whitespace if the given object is not None. 

261 

262 Parameters 

263 ---------- 

264 s : `str`, optional 

265 Input string. 

266 

267 Returns 

268 ------- 

269 r : `str` or `None` 

270 A string with leading and trailing whitespace stripped if `s` is not 

271 `None`, or `None` if `s` is `None`. 

272 """ 

273 if s is not None: 

274 s = s.strip() 

275 return s 

276 

277 

278def immutable(cls: Type) -> Type: 

279 """A class decorator that simulates a simple form of immutability for 

280 the decorated class. 

281 

282 A class decorated as `immutable` may only set each of its attributes once 

283 (by convention, in ``__new__``); any attempts to set an already-set 

284 attribute will raise `AttributeError`. 

285 

286 Because this behavior interferes with the default implementation for 

287 the ``pickle`` and ``copy`` modules, `immutable` provides implementations 

288 of ``__getstate__`` and ``__setstate__`` that override this behavior. 

289 Immutable classes can them implement pickle/copy via ``__getnewargs__`` 

290 only (other approaches such as ``__reduce__`` and ``__deepcopy__`` may 

291 also be used). 

292 """ 

293 def __setattr__(self: Any, name: str, value: Any) -> None: # noqa: N807 

294 if hasattr(self, name): 

295 raise AttributeError(f"{cls.__name__} instances are immutable.") 

296 object.__setattr__(self, name, value) 

297 # mypy says the variable here has signature (str, Any) i.e. no "self"; 

298 # I think it's just confused by descriptor stuff. 

299 cls.__setattr__ = __setattr__ # type: ignore 

300 

301 def __getstate__(self: Any) -> dict: # noqa: N807 

302 # Disable default state-setting when unpickled. 

303 return {} 

304 cls.__getstate__ = __getstate__ 

305 

306 def __setstate__(self: Any, state: Any) -> None: # noqa: N807 

307 # Disable default state-setting when copied. 

308 # Sadly what works for pickle doesn't work for copy. 

309 assert not state 

310 cls.__setstate__ = __setstate__ 

311 return cls 

312 

313 

314def findFileResources(values: Iterable[str], regex: Optional[str] = None) -> List[str]: 

315 """Get the files from a list of values. If a value is a file it is added to 

316 the list of returned files. If a value is a directory, all the files in 

317 the directory (recursively) that match the regex will be returned. 

318 

319 Parameters 

320 ---------- 

321 values : iterable [`str`] 

322 The files to return and directories in which to look for files to 

323 return. 

324 regex : `str` 

325 The regex to use when searching for files within directories. Optional, 

326 by default returns all the found files. 

327 

328 Returns 

329 ------- 

330 resources: `list` [`str`] 

331 The passed-in files and files found in passed-in directories. 

332 """ 

333 fileRegex = None if regex is None else re.compile(regex) 

334 resources = [] 

335 

336 # Find all the files of interest 

337 for location in values: 

338 if os.path.isdir(location): 

339 for root, dirs, files in os.walk(location): 

340 for name in files: 

341 path = os.path.join(root, name) 

342 if os.path.isfile(path) and (fileRegex is None or fileRegex.search(name)): 

343 resources.append(path) 

344 else: 

345 resources.append(location) 

346 return resources