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# 

2# LSST Data Management System 

3# 

4# Copyright 2008-2017 AURA/LSST. 

5# 

6# This product includes software developed by the 

7# LSST Project (http://www.lsst.org/). 

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 LSST License Statement and 

20# the GNU General Public License along with this program. If not, 

21# see <https://www.lsstcorp.org/LegalNotices/>. 

22# 

23 

24import sys 

25import types 

26 

27import numpy as np 

28 

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

30 

31 

32INTRINSIC_SPECIAL_ATTRIBUTES = frozenset(( 

33 "__qualname__", 

34 "__module__", 

35 "__metaclass__", 

36 "__dict__", 

37 "__weakref__", 

38 "__class__", 

39 "__subclasshook__", 

40 "__name__", 

41 "__doc__", 

42)) 

43 

44 

45def isAttributeSafeToTransfer(name, value): 

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

47 class. 

48 

49 This rejects special methods that are defined automatically for all 

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

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

52 """ 

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

54 or name in INTRINSIC_SPECIAL_ATTRIBUTES): 

55 return False 

56 return True 

57 

58 

59def continueClass(cls): 

60 """Re-open the decorated class, adding any new definitions into the original. 

61 

62 For example: 

63 

64 .. code-block:: python 

65 

66 class Foo: 

67 pass 

68 

69 @continueClass 

70 class Foo: 

71 def run(self): 

72 return None 

73 

74 is equivalent to: 

75 

76 .. code-block:: python 

77 

78 class Foo: 

79 def run(self): 

80 return None 

81 

82 .. warning:: 

83 

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

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

86 directly using their explicit types instead. 

87 

88 """ 

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

90 for name in dir(cls): 

91 # Common descriptors like classmethod and staticmethod can only be 

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

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

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

95 # class. 

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

97 if isAttributeSafeToTransfer(name, attr): 

98 setattr(orig, name, attr) 

99 return orig 

100 

101 

102def inClass(cls, name=None): 

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

104 

105 For example: 

106 

107 .. code-block:: python 

108 

109 class Foo: 

110 pass 

111 

112 @inClass(Foo) 

113 def run(self): 

114 return None 

115 

116 is equivalent to: 

117 

118 .. code-block:: python 

119 

120 class Foo: 

121 def run(self): 

122 return None 

123 

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

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

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

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

128 """ 

129 def decorate(func): 

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

131 # assignment signals a strictly local variable. 

132 name1 = name 

133 if name1 is None: 

134 if hasattr(func, "__name__"): 

135 name1 = func.__name__ 

136 else: 

137 if hasattr(func, "__func__"): 

138 # classmethod and staticmethod have __func__ but no __name__ 

139 name1 = func.__func__.__name__ 

140 elif hasattr(func, "fget"): 

141 # property has fget but no __name__ 

142 name1 = func.fget.__name__ 

143 else: 

144 raise ValueError( 

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

146 ) 

147 setattr(cls, name1, func) 

148 return func 

149 return decorate 

150 

151 

152class TemplateMeta(type): 

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

154 template types. 

155 

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

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

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

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

160 in a consistent way. 

161 

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

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

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

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

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

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

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

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

170 example:: 

171 

172 .. code-block:: python 

173 

174 import numpy as np 

175 from ._image import ImageF, ImageD 

176 

177 class Image(metaclass=TemplateMeta): 

178 pass 

179 

180 Image.register(np.float32, ImageF) 

181 Image.register(np.float64, ImageD) 

182 Image.alias("F", ImageF) 

183 Image.alias("D", ImageD) 

184 

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

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

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

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

189 

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

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

192 the type keys:: 

193 

194 .. code-block:: python 

195 

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

197 

198 This simply forwards additional positional and keyword arguments to the 

199 wrapped template class's constructor. 

200 

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

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

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

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

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

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

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

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

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

210 generally not be tuples. 

211 

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

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

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

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

216 

217 .. code-block:: python 

218 

219 class Image(metaclass=TemplateMeta): 

220 

221 def sum(self): 

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

223 

224 Image.register(np.float32, ImageF) 

225 Image.register(np.float64, ImageD) 

226 

227 .. note:: 

228 

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

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

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

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

233 subclasses. Instead, attributes added to an instance of 

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

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

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

237 implementations of these methods. 

238 

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

240 like interface for accessing their registered subclasses, providing 

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

242 

243 .. code-block:: python 

244 

245 Image[np.float32] -> ImageF 

246 Image["D"] -> ImageD 

247 

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

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

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

251 method. 

252 

253 .. warning:: 

254 

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

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

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

258 

259 """ 

260 

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

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

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

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

265 # to registered subclasses later. 

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

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

268 if isAttributeSafeToTransfer(k, v)} 

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

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

271 # attributes on registered subclasses and intercept arguments to the 

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

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

274 # defined. 

275 attrs["TEMPLATE_PARAMS"] = \ 

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

277 attrs["TEMPLATE_DEFAULTS"] = \ 

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

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

280 attrs["_registry"] = dict() 

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

282 

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

284 raise ValueError( 

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

286 ) 

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

288 raise ValueError( 

289 "TEMPLATE_PARAMS and TEMPLATE_DEFAULTS must have same length." 

290 ) 

291 return self 

292 

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

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

295 # the abstract base class. 

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

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

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

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

300 # type of the dtype 

301 key = [] 

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

303 tempKey = kwds.pop(p, d) 

304 if isinstance(tempKey, np.dtype): 

305 tempKey = tempKey.type 

306 key.append(tempKey) 

307 key = tuple(key) 

308 

309 # indices are only tuples if there are multiple elements 

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

311 if clz is None: 

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

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

314 return clz(*args, **kwds) 

315 

316 def __subclasscheck__(cls, subclass): 

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

318 # any registered type or true subclass thereof. 

319 if subclass in cls._registry: 

320 return True 

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

322 if issubclass(subclass, v): 

323 return True 

324 return False 

325 

326 def __instancecheck__(cls, instance): 

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

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

329 if type(instance) in cls._registry: 

330 return True 

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

332 if isinstance(instance, v): 

333 return True 

334 return False 

335 

336 def __subclasses__(cls): 

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

338 """ 

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

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

341 # functionality. 

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

343 

344 def register(cls, key, subclass): 

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

346 number, type, or other hashable). 

347 

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

349 """ 

350 if key is None: 

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

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

353 raise ValueError( 

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

355 "use alias() instead." 

356 ) 

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

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

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

360 else: 

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

362 raise KeyError( 

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

364 ) 

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

366 # make the static methods available through the ABC 

367 if cls.TEMPLATE_DEFAULTS: 

368 defaults = (cls.TEMPLATE_DEFAULTS[0] if 

369 len(cls.TEMPLATE_DEFAULTS) == 1 else 

370 cls.TEMPLATE_DEFAULTS) 

371 if key == defaults: 

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

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

374 " Cannot link method to base class.") 

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

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

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

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

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

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

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

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

383 for name in subclass.__dict__: 

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

385 continue 

386 obj = subclass.__dict__[name] 

387 # copy over the static methods 

388 isBuiltin = isinstance(obj, types.BuiltinFunctionType) 

389 isStatic = isinstance(obj, staticmethod) 

390 if isBuiltin or isStatic: 

391 if hasattr(cls, name): 

392 raise AttributeError( 

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

394 setattr(cls, name, obj) 

395 # copy over the class methods 

396 elif isinstance(obj, classmethod): 

397 if hasattr(cls, name): 

398 raise AttributeError( 

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

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

401 

402 def setattrSafe(name, value): 

403 try: 

404 currentValue = getattr(subclass, name) 

405 if currentValue != value: 

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

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

408 raise ValueError( 

409 msg.format(name, currentValue, value) 

410 ) 

411 except AttributeError: 

412 setattr(subclass, name, value) 

413 

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

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

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

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

418 setattrSafe(p, k) 

419 else: 

420 raise ValueError( 

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

422 len(cls.TEMPLATE_PARAMS), cls.TEMPLATE_PARAMS 

423 ) 

424 ) 

425 

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

427 setattr(subclass, name, attr) 

428 

429 def alias(cls, key, subclass): 

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

431 different key. 

432 """ 

433 if key is None: 

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

435 if key in cls._registry: 

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

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

438 for p in cls.TEMPLATE_PARAMS) 

439 if len(primaryKey) == 1: 

440 # indices are only tuples if there are multiple elements 

441 primaryKey = primaryKey[0] 

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

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

444 cls._registry[key] = subclass 

445 

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

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

448 

449 def __getitem__(cls, key): 

450 return cls._registry[key] 

451 

452 def __iter__(cls): 

453 return iter(cls._registry) 

454 

455 def __len__(cls): 

456 return len(cls._registry) 

457 

458 def __contains__(cls, key): 

459 return key in cls._registry 

460 

461 def keys(cls): 

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

463 """ 

464 return cls._registry.keys() 

465 

466 def values(cls): 

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

468 corresponding to any aliases. 

469 """ 

470 return cls._registry.values() 

471 

472 def items(cls): 

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

474 """ 

475 return cls._registry.items() 

476 

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

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

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

480 """ 

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