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 

40from typing import ( 

41 Any, 

42 Callable, 

43 Dict, 

44 Iterable, 

45 Iterator, 

46 Mapping, 

47 Optional, 

48 Type, 

49 TypeVar, 

50 Union, 

51) 

52 

53from lsst.utils import doImport 

54 

55 

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

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

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

59 try: 

60 os.makedirs(directory) 

61 except OSError as e: 

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

63 if e.errno != errno.EEXIST: 

64 raise e 

65 

66 

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

68 """Make input iterable. 

69 

70 There are three cases, when the input is: 

71 

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

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

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

75 - a Mapping -> return single element iterable 

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

77 

78 Parameters 

79 ---------- 

80 a : iterable or `str` or not iterable 

81 Argument to be converted to an iterable. 

82 

83 Returns 

84 ------- 

85 i : `generator` 

86 Iterable version of the input value. 

87 """ 

88 if isinstance(a, str): 

89 yield a 

90 return 

91 if isinstance(a, Mapping): 

92 yield a 

93 return 

94 try: 

95 yield from a 

96 except Exception: 

97 yield a 

98 

99 

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

101 """ 

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

103 

104 Parameters 

105 ---------- 

106 self : `object` 

107 Instance to be inspected. 

108 

109 Returns 

110 ------- 

111 slots : `itertools.chain` 

112 All the slots as an iterable. 

113 """ 

114 from itertools import chain 

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

116 

117 

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

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

120 

121 Parameters 

122 ---------- 

123 cls : `type` or `object` 

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

125 or a `type`. 

126 

127 Returns 

128 ------- 

129 name : `str` 

130 Full name of type. 

131 

132 Notes 

133 ----- 

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

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

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

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

138 entity will be hoisted into the parent namespace. 

139 """ 

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

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

142 cls = type(cls) 

143 if hasattr(builtins, cls.__qualname__): 

144 # Special case builtins such as str and dict 

145 return cls.__qualname__ 

146 

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

148 

149 # Remove components with leading underscores 

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

151 

152 # Consistency check 

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

154 try: 

155 test = doImport(cleaned_name) 

156 except Exception: 

157 # Could not import anything so return the real name 

158 return real_name 

159 

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

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

162 if test is not cls: 

163 return real_name 

164 

165 return cleaned_name 

166 

167 

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

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

170 

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

172 

173 Parameters 

174 ---------- 

175 typeOrName : `str` or Python class 

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

177 

178 Returns 

179 ------- 

180 type_ : `type` 

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

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

183 

184 Notes 

185 ----- 

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

187 """ 

188 if isinstance(typeOrName, str): 

189 cls = doImport(typeOrName) 

190 else: 

191 cls = typeOrName 

192 return cls 

193 

194 

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

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

197 

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

199 

200 Parameters 

201 ---------- 

202 typeOrName : `str` or Python class 

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

204 args : `tuple` 

205 Positional arguments to use pass to the object constructor. 

206 kwargs : `dict` 

207 Keyword arguments to pass to object constructor. 

208 

209 Returns 

210 ------- 

211 instance : `object` 

212 Instance of the requested type, instantiated with the provided 

213 parameters. 

214 """ 

215 cls = getClassOf(typeOrName) 

216 return cls(*args, **kwargs) 

217 

218 

219class Singleton(type): 

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

221 

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

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

224 the arguments the first time an instance is instantiated. 

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

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

227 adjust state of the singleton. 

228 """ 

229 

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

231 

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

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

234 # constructor arguments. 

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

236 if cls not in cls._instances: 

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

238 return cls._instances[cls] 

239 

240 

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

242 

243 

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

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

246 

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

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

249 """ 

250 @functools.wraps(func) 

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

252 with self.transaction(): 

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

254 return inner # type: ignore 

255 

256 

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

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

259 

260 Parameters 

261 ---------- 

262 s : `str`, optional 

263 Input string. 

264 

265 Returns 

266 ------- 

267 r : `str` or `None` 

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

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

270 """ 

271 if s is not None: 

272 s = s.strip() 

273 return s 

274 

275 

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

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

278 the decorated class. 

279 

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

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

282 attribute will raise `AttributeError`. 

283 

284 Because this behavior interferes with the default implementation for 

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

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

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

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

289 also be used). 

290 """ 

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

292 if hasattr(self, name): 

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

294 object.__setattr__(self, name, value) 

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

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

297 cls.__setattr__ = __setattr__ # type: ignore 

298 

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

300 # Disable default state-setting when unpickled. 

301 return {} 

302 cls.__getstate__ = __getstate__ 

303 

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

305 # Disable default state-setting when copied. 

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

307 assert not state 

308 cls.__setstate__ = __setstate__ 

309 return cls