Coverage for python/lsst/utils/wrappers.py: 11%

Shortcuts 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

142 statements  

1# This file is part of utils. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

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

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

7# for details of code ownership. 

8# 

9# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12import sys 

13import types 

14 

15import numpy as np 

16 

17__all__ = ("continueClass", "inClass", "TemplateMeta") 

18 

19 

20INTRINSIC_SPECIAL_ATTRIBUTES = frozenset(( 

21 "__qualname__", 

22 "__module__", 

23 "__metaclass__", 

24 "__dict__", 

25 "__weakref__", 

26 "__class__", 

27 "__subclasshook__", 

28 "__name__", 

29 "__doc__", 

30)) 

31 

32 

33def isAttributeSafeToTransfer(name, value): 

34 """Return True if an attribute is safe to monkeypatch-transfer to another 

35 class. 

36 

37 This rejects special methods that are defined automatically for all 

38 classes, leaving only those explicitly defined in a class decorated by 

39 `continueClass` or registered with an instance of `TemplateMeta`. 

40 """ 

41 if name.startswith("__") and (value is getattr(object, name, None) 

42 or name in INTRINSIC_SPECIAL_ATTRIBUTES): 

43 return False 

44 return True 

45 

46 

47def continueClass(cls): 

48 """Re-open the decorated class, adding any new definitions into the 

49 original. 

50 

51 For example: 

52 

53 .. code-block:: python 

54 

55 class Foo: 

56 pass 

57 

58 @continueClass 

59 class Foo: 

60 def run(self): 

61 return None 

62 

63 is equivalent to: 

64 

65 .. code-block:: python 

66 

67 class Foo: 

68 def run(self): 

69 return None 

70 

71 .. warning:: 

72 

73 Python's built-in `super` function does not behave properly in classes 

74 decorated with `continueClass`. Base class methods must be invoked 

75 directly using their explicit types instead. 

76 

77 """ 

78 orig = getattr(sys.modules[cls.__module__], cls.__name__) 

79 for name in dir(cls): 

80 # Common descriptors like classmethod and staticmethod can only be 

81 # accessed without invoking their magic if we use __dict__; if we use 

82 # getattr on those we'll get e.g. a bound method instance on the dummy 

83 # class rather than a classmethod instance we can put on the target 

84 # class. 

85 attr = cls.__dict__.get(name, None) or getattr(cls, name) 

86 if isAttributeSafeToTransfer(name, attr): 

87 setattr(orig, name, attr) 

88 return orig 

89 

90 

91def inClass(cls, name=None): 

92 """Add the decorated function to the given class as a method. 

93 

94 For example: 

95 

96 .. code-block:: python 

97 

98 class Foo: 

99 pass 

100 

101 @inClass(Foo) 

102 def run(self): 

103 return None 

104 

105 is equivalent to: 

106 

107 .. code-block:: python 

108 

109 class Foo: 

110 def run(self): 

111 return None 

112 

113 Standard decorators like ``classmethod``, ``staticmethod``, and 

114 ``property`` may be used *after* this decorator. Custom decorators 

115 may only be used if they return an object with a ``__name__`` attribute 

116 or the ``name`` optional argument is provided. 

117 """ 

118 def decorate(func): 

119 # Using 'name' instead of 'name1' breaks the closure because 

120 # assignment signals a strictly local variable. 

121 name1 = name 

122 if name1 is None: 

123 if hasattr(func, "__name__"): 

124 name1 = func.__name__ 

125 else: 

126 if hasattr(func, "__func__"): 

127 # classmethod and staticmethod have __func__ but 

128 # no __name__ 

129 name1 = func.__func__.__name__ 

130 elif hasattr(func, "fget"): 

131 # property has fget but no __name__ 

132 name1 = func.fget.__name__ 

133 else: 

134 raise ValueError( 

135 "Could not guess attribute name for '{}'.".format(func) 

136 ) 

137 setattr(cls, name1, func) 

138 return func 

139 return decorate 

140 

141 

142class TemplateMeta(type): 

143 """A metaclass for abstract base classes that tie together wrapped C++ 

144 template types. 

145 

146 C++ template classes are most easily wrapped with a separate Python class 

147 for each template type, which results in an unnatural Python interface. 

148 TemplateMeta provides a thin layer that connects these Python classes by 

149 giving them a common base class and acting as a factory to construct them 

150 in a consistent way. 

151 

152 To use, simply create a new class with the name of the template class, and 

153 use ``TemplateMeta`` as its metaclass, and then call ``register`` on each 

154 of its subclasses. This registers the class with a "type key" - usually a 

155 Python representation of the C++ template types. The type key must be a 

156 hashable object - strings, type objects, and tuples of these (for C++ 

157 classes with multiple template parameters) are good choices. Alternate 

158 type keys for existing classes can be added by calling ``alias``, but only 

159 after a subclass already been registered with a "primary" type key. For 

160 example:: 

161 

162 .. code-block:: python 

163 

164 import numpy as np 

165 from ._image import ImageF, ImageD 

166 

167 class Image(metaclass=TemplateMeta): 

168 pass 

169 

170 Image.register(np.float32, ImageF) 

171 Image.register(np.float64, ImageD) 

172 Image.alias("F", ImageF) 

173 Image.alias("D", ImageD) 

174 

175 We have intentionally used ``numpy`` types as the primary keys for these 

176 objects in this example, with strings as secondary aliases simply because 

177 the primary key is added as a ``dtype`` attribute on the the registered 

178 classes (so ``ImageF.dtype == numpy.float32`` in the above example). 

179 

180 This allows user code to construct objects directly using ``Image``, as 

181 long as an extra ``dtype`` keyword argument is passed that matches one of 

182 the type keys:: 

183 

184 .. code-block:: python 

185 

186 img = Image(52, 64, dtype=np.float32) 

187 

188 This simply forwards additional positional and keyword arguments to the 

189 wrapped template class's constructor. 

190 

191 The choice of "dtype" as the name of the template parameter is also 

192 configurable, and in fact multiple template parameters are also supported, 

193 by setting a ``TEMPLATE_PARAMS`` class attribute on the ABC to a tuple 

194 containing the names of the template parameters. A ``TEMPLATE_DEFAULTS`` 

195 attribute can also be defined to a tuple of the same length containing 

196 default values for the template parameters, allowing them to be omitted in 

197 constructor calls. When the length of these attributes is more than one, 

198 the type keys passed to ``register`` and ``alias`` should be tuple of the 

199 same length; when the length of these attributes is one, type keys should 

200 generally not be tuples. 

201 

202 As an aid for those writing the Python wrappers for C++ classes, 

203 ``TemplateMeta`` also provides a way to add pure-Python methods and other 

204 attributes to the wrapped template classes. To add a ``sum`` method to 

205 all registered types, for example, we can just do:: 

206 

207 .. code-block:: python 

208 

209 class Image(metaclass=TemplateMeta): 

210 

211 def sum(self): 

212 return np.sum(self.getArray()) 

213 

214 Image.register(np.float32, ImageF) 

215 Image.register(np.float64, ImageD) 

216 

217 .. note:: 

218 

219 ``TemplateMeta`` works by overriding the ``__instancecheck__`` and 

220 ``__subclasscheck__`` special methods, and hence does not appear in 

221 its registered subclasses' method resolution order or ``__bases__`` 

222 attributes. That means its attributes are not inherited by registered 

223 subclasses. Instead, attributes added to an instance of 

224 ``TemplateMeta`` are *copied* into the types registered with it. These 

225 attributes will thus *replace* existing attributes in those classes 

226 with the same name, and subclasses cannot delegate to base class 

227 implementations of these methods. 

228 

229 Finally, abstract base classes that use ``TemplateMeta`` define a dict- 

230 like interface for accessing their registered subclasses, providing 

231 something like the C++ syntax for templates:: 

232 

233 .. code-block:: python 

234 

235 Image[np.float32] -> ImageF 

236 Image["D"] -> ImageD 

237 

238 Both primary dtypes and aliases can be used as keys in this interface, 

239 which means types with aliases will be present multiple times in the dict. 

240 To obtain the sequence of unique subclasses, use the ``__subclasses__`` 

241 method. 

242 

243 .. warning:: 

244 

245 Python's built-in `super` function does not behave properly in classes 

246 that have `TemplateMeta` as their metaclass (which should be rare, as 

247 TemplateMeta ABCs will have base classes of their own).. 

248 

249 """ 

250 

251 def __new__(cls, name, bases, attrs): 

252 # __new__ is invoked when the abstract base class is defined (via a 

253 # class statement). We save a dict of class attributes (including 

254 # methods) that were defined in the class body so we can copy them 

255 # to registered subclasses later. 

256 # We also initialize an empty dict to store the registered subclasses. 

257 attrs["_inherited"] = {k: v for k, v in attrs.items() 

258 if isAttributeSafeToTransfer(k, v)} 

259 # The special "TEMPLATE_PARAMS" class attribute, if defined, contains 

260 # names of the template parameters, which we use to set those 

261 # attributes on registered subclasses and intercept arguments to the 

262 # constructor. This line removes it from the dict of things that 

263 # should be inherited while setting a default of 'dtype' if it's not 

264 # defined. 

265 attrs["TEMPLATE_PARAMS"] = \ 

266 attrs["_inherited"].pop("TEMPLATE_PARAMS", ("dtype",)) 

267 attrs["TEMPLATE_DEFAULTS"] = \ 

268 attrs["_inherited"].pop("TEMPLATE_DEFAULTS", 

269 (None,)*len(attrs["TEMPLATE_PARAMS"])) 

270 attrs["_registry"] = dict() 

271 self = type.__new__(cls, name, bases, attrs) 

272 

273 if len(self.TEMPLATE_PARAMS) == 0: 

274 raise ValueError( 

275 "TEMPLATE_PARAMS must be a tuple with at least one element." 

276 ) 

277 if len(self.TEMPLATE_DEFAULTS) != len(self.TEMPLATE_PARAMS): 

278 raise ValueError( 

279 "TEMPLATE_PARAMS and TEMPLATE_DEFAULTS must have same length." 

280 ) 

281 return self 

282 

283 def __call__(cls, *args, **kwds): 

284 # __call__ is invoked when someone tries to construct an instance of 

285 # the abstract base class. 

286 # If the ABC defines a "TEMPLATE_PARAMS" attribute, we use those 

287 # strings as the kwargs we should intercept to find the right type. 

288 # Generate a type mapping key from input keywords. If the type returned 

289 # from the keyword lookup is a numpy dtype object, fetch the underlying 

290 # type of the dtype 

291 key = [] 

292 for p, d in zip(cls.TEMPLATE_PARAMS, cls.TEMPLATE_DEFAULTS): 

293 tempKey = kwds.pop(p, d) 

294 if isinstance(tempKey, np.dtype): 

295 tempKey = tempKey.type 

296 key.append(tempKey) 

297 key = tuple(key) 

298 

299 # indices are only tuples if there are multiple elements 

300 clz = cls._registry.get(key[0] if len(key) == 1 else key, None) 

301 if clz is None: 

302 d = {k: v for k, v in zip(cls.TEMPLATE_PARAMS, key)} 

303 raise TypeError("No registered subclass for {}.".format(d)) 

304 return clz(*args, **kwds) 

305 

306 def __subclasscheck__(cls, subclass): 

307 # Special method hook for the issubclass built-in: we return true for 

308 # any registered type or true subclass thereof. 

309 if subclass in cls._registry: 

310 return True 

311 for v in cls._registry.values(): 

312 if issubclass(subclass, v): 

313 return True 

314 return False 

315 

316 def __instancecheck__(cls, instance): 

317 # Special method hook for the isinstance built-in: we return true for 

318 # an instance of any registered type or true subclass thereof. 

319 if type(instance) in cls._registry: 

320 return True 

321 for v in cls._registry.values(): 

322 if isinstance(instance, v): 

323 return True 

324 return False 

325 

326 def __subclasses__(cls): 

327 """Return a tuple of all classes that inherit from this class. 

328 """ 

329 # This special method isn't defined as part of the Python data model, 

330 # but it exists on builtins (including ABCMeta), and it provides useful 

331 # functionality. 

332 return tuple(set(cls._registry.values())) 

333 

334 def register(cls, key, subclass): 

335 """Register a subclass of this ABC with the given key (a string, 

336 number, type, or other hashable). 

337 

338 Register may only be called once for a given key or a given subclass. 

339 """ 

340 if key is None: 

341 raise ValueError("None may not be used as a key.") 

342 if subclass in cls._registry.values(): 

343 raise ValueError( 

344 "This subclass has already registered with another key; " 

345 "use alias() instead." 

346 ) 

347 if cls._registry.setdefault(key, subclass) != subclass: 

348 if len(cls.TEMPLATE_PARAMS) == 1: 

349 d = {cls.TEMPLATE_PARAMS[0]: key} 

350 else: 

351 d = {k: v for k, v in zip(cls.TEMPLATE_PARAMS, key)} 

352 raise KeyError( 

353 "Another subclass is already registered with {}".format(d) 

354 ) 

355 # If the key used to register a class matches the default key, 

356 # make the static methods available through the ABC 

357 if cls.TEMPLATE_DEFAULTS: 

358 defaults = (cls.TEMPLATE_DEFAULTS[0] if 

359 len(cls.TEMPLATE_DEFAULTS) == 1 else 

360 cls.TEMPLATE_DEFAULTS) 

361 if key == defaults: 

362 conflictStr = ("Base class has attribute {}" 

363 " which is a {} method of {}." 

364 " Cannot link method to base class.") 

365 # In the following if statements, the explicit lookup in 

366 # __dict__ must be done, as a call to getattr returns the 

367 # bound method, which no longer reports as a static or class 

368 # method. The static methods must be transfered to the ABC 

369 # in this unbound state, so that python will still see them 

370 # as static methods and not attempt to pass self. The class 

371 # methods must be transfered to the ABC as a bound method 

372 # so that the correct cls be called with the class method 

373 for name in subclass.__dict__: 

374 if name in ("__new__", "__init_subclass__"): 

375 continue 

376 obj = subclass.__dict__[name] 

377 # copy over the static methods 

378 isBuiltin = isinstance(obj, types.BuiltinFunctionType) 

379 isStatic = isinstance(obj, staticmethod) 

380 if isBuiltin or isStatic: 

381 if hasattr(cls, name): 

382 raise AttributeError( 

383 conflictStr.format(name, "static", subclass)) 

384 setattr(cls, name, obj) 

385 # copy over the class methods 

386 elif isinstance(obj, classmethod): 

387 if hasattr(cls, name): 

388 raise AttributeError( 

389 conflictStr.format(name, "class", subclass)) 

390 setattr(cls, name, getattr(subclass, name)) 

391 

392 def setattrSafe(name, value): 

393 try: 

394 currentValue = getattr(subclass, name) 

395 if currentValue != value: 

396 msg = ("subclass already has a '{}' attribute with " 

397 "value {} != {}.") 

398 raise ValueError( 

399 msg.format(name, currentValue, value) 

400 ) 

401 except AttributeError: 

402 setattr(subclass, name, value) 

403 

404 if len(cls.TEMPLATE_PARAMS) == 1: 

405 setattrSafe(cls.TEMPLATE_PARAMS[0], key) 

406 elif len(cls.TEMPLATE_PARAMS) == len(key): 

407 for p, k in zip(cls.TEMPLATE_PARAMS, key): 

408 setattrSafe(p, k) 

409 else: 

410 raise ValueError( 

411 "key must have {} elements (one for each of {})".format( 

412 len(cls.TEMPLATE_PARAMS), cls.TEMPLATE_PARAMS 

413 ) 

414 ) 

415 

416 for name, attr in cls._inherited.items(): 

417 setattr(subclass, name, attr) 

418 

419 def alias(cls, key, subclass): 

420 """Add an alias that allows an existing subclass to be accessed with a 

421 different key. 

422 """ 

423 if key is None: 

424 raise ValueError("None may not be used as a key.") 

425 if key in cls._registry: 

426 raise KeyError("Cannot multiply-register key {}".format(key)) 

427 primaryKey = tuple(getattr(subclass, p, None) 

428 for p in cls.TEMPLATE_PARAMS) 

429 if len(primaryKey) == 1: 

430 # indices are only tuples if there are multiple elements 

431 primaryKey = primaryKey[0] 

432 if cls._registry.get(primaryKey, None) != subclass: 

433 raise ValueError("Subclass is not registered with this base class.") 

434 cls._registry[key] = subclass 

435 

436 # Immutable mapping interface defined below. We don't use collections 

437 # mixins because we don't want their comparison operators. 

438 

439 def __getitem__(cls, key): 

440 return cls._registry[key] 

441 

442 def __iter__(cls): 

443 return iter(cls._registry) 

444 

445 def __len__(cls): 

446 return len(cls._registry) 

447 

448 def __contains__(cls, key): 

449 return key in cls._registry 

450 

451 def keys(cls): 

452 """Return an iterable containing all keys (including aliases). 

453 """ 

454 return cls._registry.keys() 

455 

456 def values(cls): 

457 """Return an iterable of registered subclasses, with duplicates 

458 corresponding to any aliases. 

459 """ 

460 return cls._registry.values() 

461 

462 def items(cls): 

463 """Return an iterable of (key, subclass) pairs. 

464 """ 

465 return cls._registry.items() 

466 

467 def get(cls, key, default=None): 

468 """Return the subclass associated with the given key (including 

469 aliases), or ``default`` if the key is not recognized. 

470 """ 

471 return cls._registry.get(key, default)