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 Union, 

50) 

51 

52from lsst.utils import doImport 

53 

54 

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

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

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

58 try: 

59 os.makedirs(directory) 

60 except OSError as e: 

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

62 if e.errno != errno.EEXIST: 

63 raise e 

64 

65 

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

67 """Make input iterable. 

68 

69 There are three cases, when the input is: 

70 

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

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

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

74 - a Mapping -> return single element iterable 

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

76 

77 Parameters 

78 ---------- 

79 a : iterable or `str` or not iterable 

80 Argument to be converted to an iterable. 

81 

82 Returns 

83 ------- 

84 i : `generator` 

85 Iterable version of the input value. 

86 """ 

87 if isinstance(a, str): 

88 yield a 

89 return 

90 if isinstance(a, Mapping): 

91 yield a 

92 return 

93 try: 

94 yield from a 

95 except Exception: 

96 yield a 

97 

98 

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

100 """ 

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

102 

103 Parameters 

104 ---------- 

105 self : `object` 

106 Instance to be inspected. 

107 

108 Returns 

109 ------- 

110 slots : `itertools.chain` 

111 All the slots as an iterable. 

112 """ 

113 from itertools import chain 

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

115 

116 

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

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

119 

120 Parameters 

121 ---------- 

122 cls : `type` or `object` 

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

124 or a `type`. 

125 

126 Returns 

127 ------- 

128 name : `str` 

129 Full name of type. 

130 

131 Notes 

132 ----- 

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

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

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

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

137 entity will be hoisted into the parent namespace. 

138 """ 

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

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

141 cls = type(cls) 

142 if hasattr(builtins, cls.__qualname__): 142 ↛ 144line 142 didn't jump to line 144, because the condition on line 142 was never true

143 # Special case builtins such as str and dict 

144 return cls.__qualname__ 

145 

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

147 

148 # Remove components with leading underscores 

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

150 

151 # Consistency check 

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

153 try: 

154 test = doImport(cleaned_name) 

155 except Exception: 

156 # Could not import anything so return the real name 

157 return real_name 

158 

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

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

161 if test is not cls: 

162 return real_name 

163 

164 return cleaned_name 

165 

166 

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

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

169 

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

171 

172 Parameters 

173 ---------- 

174 typeOrName : `str` or Python class 

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

176 

177 Returns 

178 ------- 

179 type_ : `type` 

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

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

182 

183 Notes 

184 ----- 

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

186 """ 

187 if isinstance(typeOrName, str): 

188 cls = doImport(typeOrName) 

189 else: 

190 cls = typeOrName 

191 return cls 

192 

193 

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

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

196 

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

198 

199 Parameters 

200 ---------- 

201 typeOrName : `str` or Python class 

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

203 args : `tuple` 

204 Positional arguments to use pass to the object constructor. 

205 kwargs : `dict` 

206 Keyword arguments to pass to object constructor. 

207 

208 Returns 

209 ------- 

210 instance : `object` 

211 Instance of the requested type, instantiated with the provided 

212 parameters. 

213 """ 

214 cls = getClassOf(typeOrName) 

215 return cls(*args, **kwargs) 

216 

217 

218class Singleton(type): 

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

220 

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

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

223 the arguments the first time an instance is instantiated. 

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

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

226 adjust state of the singleton. 

227 """ 

228 

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

230 

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

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

233 # constructor arguments. 

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

235 if cls not in cls._instances: 

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

237 return cls._instances[cls] 

238 

239 

240def transactional(func: Callable) -> Callable: 

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

242 

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

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

245 """ 

246 @functools.wraps(func) 

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

248 with self.transaction(): 

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

250 return inner 

251 

252 

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

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

255 

256 Parameters 

257 ---------- 

258 s : `str`, optional 

259 Input string. 

260 

261 Returns 

262 ------- 

263 r : `str` or `None` 

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

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

266 """ 

267 if s is not None: 

268 s = s.strip() 

269 return s 

270 

271 

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

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

274 the decorated class. 

275 

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

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

278 attribute will raise `AttributeError`. 

279 

280 Because this behavior interferes with the default implementation for 

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

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

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

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

285 also be used). 

286 """ 

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

288 if hasattr(self, name): 

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

290 object.__setattr__(self, name, value) 

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

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

293 cls.__setattr__ = __setattr__ # type: ignore 

294 

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

296 # Disable default state-setting when unpickled. 

297 return {} 

298 cls.__getstate__ = __getstate__ 

299 

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

301 # Disable default state-setting when copied. 

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

303 assert not state 

304 cls.__setstate__ = __setstate__ 

305 return cls