LSSTApplications  16.0+22,16.0-1-g14407aa+7,16.0-1-g16dcc35+1,16.0-1-g928b041+1,16.0-1-g9884240+1,16.0-1-gce273f5+1,16.0-10-g0b41441+3,16.0-10-g549e159,16.0-11-ge4eb5c1,16.0-11-gf74ee13d,16.0-11-gf9130ea+4,16.0-2-g0febb12,16.0-2-g636dfb1,16.0-2-g839ba83+11,16.0-2-g9d5294e+8,16.0-25-g88abbd732,16.0-3-g0e3a094+1,16.0-3-g19f2ef8,16.0-3-g3806c63+1,16.0-3-g5dc86b7+11,16.0-3-g6923fb6+1,16.0-3-g9515088+5,16.0-3-gcfd6c53+6,16.0-4-g347813e+5,16.0-4-g50d071e+1,16.0-4-g5f3a788,16.0-4-g6c14c47+7,16.0-4-g7690030+6,16.0-4-gd6aeece+1,16.0-4-ge3254b7,16.0-5-gb3f8a4b+5,16.0-6-g9134ce5,16.0-6-gbe2f956+3,16.0-6-gf0acd13,16.0-7-g3dac777+5,16.0-8-g4aca173+6,16.0-9-g377a976,16.0-9-gc30114dc+3,16.0-9-gf78d8dd,w.2018.32
LSSTDataManagementBasePackage
wrappers.py
Go to the documentation of this file.
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 from __future__ import absolute_import, division, print_function
24 
25 import sys
26 import types
27 
28 import numpy as np
29 
30 __all__ = ("continueClass", "inClass", "TemplateMeta")
31 
32 
33 INTRINSIC_SPECIAL_ATTRIBUTES = frozenset((
34  "__qualname__",
35  "__module__",
36  "__metaclass__",
37  "__dict__",
38  "__weakref__",
39  "__class__",
40  "__subclasshook__",
41  "__name__",
42  "__doc__",
43 ))
44 
45 
46 def isAttributeSafeToTransfer(name, value):
47  """Return True if an attribute is safe to monkeypatch-transfer to another
48  class.
49 
50  This rejects special methods that are defined automatically for all
51  classes, leaving only those explicitly defined in a class decorated by
52  `continueClass` or registered with an instance of `TemplateMeta`.
53  """
54  if name.startswith("__") and (value is getattr(object, name, None) or
55  name in INTRINSIC_SPECIAL_ATTRIBUTES):
56  return False
57  return True
58 
59 
60 def continueClass(cls):
61  """Re-open the decorated class, adding any new definitions into the original.
62 
63  For example::
64 
65  class Foo:
66  pass
67 
68  @continueClass
69  class Foo:
70  def run(self):
71  return None
72 
73  is equivalent to::
74 
75  class Foo:
76  def run(self):
77  return None
78 
79  """
80  orig = getattr(sys.modules[cls.__module__], cls.__name__)
81  for name in dir(cls):
82  # Common descriptors like classmethod and staticmethod can only be
83  # accessed without invoking their magic if we use __dict__; if we use
84  # getattr on those we'll get e.g. a bound method instance on the dummy
85  # class rather than a classmethod instance we can put on the target
86  # class.
87  attr = cls.__dict__.get(name, None) or getattr(cls, name)
88  if isAttributeSafeToTransfer(name, attr):
89  setattr(orig, name, attr)
90  return orig
91 
92 
93 def inClass(cls, name=None):
94  """Add the decorated function to the given class as a method.
95 
96  For example::
97 
98  class Foo:
99  pass
100 
101  @inClass(Foo)
102  def run(self):
103  return None
104 
105  is equivalent to::
106 
107  class Foo:
108  def run(self):
109  return None
110 
111  Standard decorators like ``classmethod``, ``staticmethod``, and
112  ``property`` may be used *after* this decorator. Custom decorators
113  may only be used if they return an object with a ``__name__`` attribute
114  or the ``name`` optional argument is provided.
115  """
116  def decorate(func):
117  # Using 'name' instead of 'name1' breaks the closure because
118  # assignment signals a strictly local variable.
119  name1 = name
120  if name1 is None:
121  if hasattr(func, "__name__"):
122  name1 = func.__name__
123  else:
124  if hasattr(func, "__func__"):
125  # classmethod and staticmethod have __func__ but no __name__
126  name1 = func.__func__.__name__
127  elif hasattr(func, "fget"):
128  # property has fget but no __name__
129  name1 = func.fget.__name__
130  else:
131  raise ValueError(
132  "Could not guess attribute name for '{}'.".format(func)
133  )
134  setattr(cls, name1, func)
135  return func
136  return decorate
137 
138 
140  """A metaclass for abstract base classes that tie together wrapped C++
141  template types.
142 
143  C++ template classes are most easily wrapped with a separate Python class
144  for each template type, which results in an unnatural Python interface.
145  TemplateMeta provides a thin layer that connects these Python classes by
146  giving them a common base class and acting as a factory to construct them
147  in a consistent way.
148 
149  To use, simply create a new class with the name of the template class, and
150  use ``TemplateMeta`` as its metaclass, and then call ``register`` on each
151  of its subclasses. This registers the class with a "type key" - usually a
152  Python representation of the C++ template types. The type key must be a
153  hashable object - strings, type objects, and tuples of these (for C++
154  classes with multiple template parameters) are good choices. Alternate
155  type keys for existing classes can be added by calling ``alias``, but only
156  after a subclass already been registered with a "primary" type key. For
157  example (using Python 3 metaclass syntax)::
158 
159  import numpy as np
160  from ._image import ImageF, ImageD
161 
162  class Image(metaclass=TemplateMeta):
163  pass
164 
165  Image.register(np.float32, ImageF)
166  Image.register(np.float64, ImageD)
167  Image.alias("F", ImageF)
168  Image.alias("D", ImageD)
169 
170  We have intentionally used ``numpy`` types as the primary keys for these
171  objects in this example, with strings as secondary aliases simply because
172  the primary key is added as a ``dtype`` attribute on the the registered
173  classes (so ``ImageF.dtype == numpy.float32`` in the above example).
174 
175  This allows user code to construct objects directly using ``Image``, as
176  long as an extra ``dtype`` keyword argument is passed that matches one of
177  the type keys::
178 
179  img = Image(52, 64, dtype=np.float32)
180 
181  This simply forwards additional positional and keyword arguments to the
182  wrapped template class's constructor.
183 
184  The choice of "dtype" as the name of the template parameter is also
185  configurable, and in fact multiple template parameters are also supported,
186  by setting a ``TEMPLATE_PARAMS`` class attribute on the ABC to a tuple
187  containing the names of the template parameters. A ``TEMPLATE_DEFAULTS``
188  attribute can also be defined to a tuple of the same length containing
189  default values for the template parameters, allowing them to be omitted in
190  constructor calls. When the length of these attributes is more than one,
191  the type keys passed to ``register`` and ``alias`` should be tuple of the
192  same length; when the length of these attributes is one, type keys should
193  generally not be tuples.
194 
195  As an aid for those writing the Python wrappers for C++ classes,
196  ``TemplateMeta`` also provides a way to add pure-Python methods and other
197  attributes to the wrapped template classes. To add a ``sum`` method to
198  all registered types, for example, we can just do::
199 
200  class Image(metaclass=TemplateMeta):
201 
202  def sum(self):
203  return np.sum(self.getArray())
204 
205  Image.register(np.float32, ImageF)
206  Image.register(np.float64, ImageD)
207 
208  .. note::
209 
210  ``TemplateMeta`` works by overriding the ``__instancecheck__`` and
211  ``__subclasscheck__`` special methods, and hence does not appear in
212  its registered subclasses' method resolution order or ``__bases__``
213  attributes. That means its attributes are not inherited by registered
214  subclasses. Instead, attributes added to an instance of
215  ``TemplateMeta`` are *copied* into the types registered with it. These
216  attributes will thus *replace* existing attributes in those classes
217  with the same name, and subclasses cannot delegate to base class
218  implementations of these methods.
219 
220  Finally, abstract base classes that use ``TemplateMeta`` define a dict-
221  like interface for accessing their registered subclasses, providing
222  something like the C++ syntax for templates::
223 
224  Image[np.float32] -> ImageF
225  Image["D"] -> ImageD
226 
227  Both primary dtypes and aliases can be used as keys in this interface,
228  which means types with aliases will be present multiple times in the dict.
229  To obtain the sequence of unique subclasses, use the ``__subclasses__``
230  method.
231  """
232 
233  def __new__(cls, name, bases, attrs):
234  # __new__ is invoked when the abstract base class is defined (via a
235  # class statement). We save a dict of class attributes (including
236  # methods) that were defined in the class body so we can copy them
237  # to registered subclasses later.
238  # We also initialize an empty dict to store the registered subclasses.
239  attrs["_inherited"] = {k: v for k, v in attrs.items()
240  if isAttributeSafeToTransfer(k, v)}
241  # The special "TEMPLATE_PARAMS" class attribute, if defined, contains
242  # names of the template parameters, which we use to set those
243  # attributes on registered subclasses and intercept arguments to the
244  # constructor. This line removes it from the dict of things that
245  # should be inherited while setting a default of 'dtype' if it's not
246  # defined.
247  attrs["TEMPLATE_PARAMS"] = \
248  attrs["_inherited"].pop("TEMPLATE_PARAMS", ("dtype",))
249  attrs["TEMPLATE_DEFAULTS"] = \
250  attrs["_inherited"].pop("TEMPLATE_DEFAULTS",
251  (None,)*len(attrs["TEMPLATE_PARAMS"]))
252  attrs["_registry"] = dict()
253  self = type.__new__(cls, name, bases, attrs)
254 
255  if len(self.TEMPLATE_PARAMS) == 0:
256  raise ValueError(
257  "TEMPLATE_PARAMS must be a tuple with at least one element."
258  )
259  if len(self.TEMPLATE_DEFAULTS) != len(self.TEMPLATE_PARAMS):
260  raise ValueError(
261  "TEMPLATE_PARAMS and TEMPLATE_DEFAULTS must have same length."
262  )
263  return self
264 
265  def __call__(self, *args, **kwds):
266  # __call__ is invoked when someone tries to construct an instance of
267  # the abstract base class.
268  # If the ABC defines a "TEMPLATE_PARAMS" attribute, we use those strings
269  # as the kwargs we should intercept to find the right type.
270 
271  # Generate a type mapping key from input keywords. If the type returned
272  # from the keyword lookup is a numpy dtype object, fetch the underlying
273  # type of the dtype
274  key = []
275  for p, d in zip(self.TEMPLATE_PARAMS, self.TEMPLATE_DEFAULTS):
276  tempKey = kwds.pop(p, d)
277  if isinstance(tempKey, np.dtype):
278  tempKey = tempKey.type
279  key.append(tempKey)
280  key = tuple(key)
281 
282  # indices are only tuples if there are multiple elements
283  cls = self._registry.get(key[0] if len(key) == 1 else key, None)
284  if cls is None:
285  d = {k: v for k, v in zip(self.TEMPLATE_PARAMS, key)}
286  raise TypeError("No registered subclass for {}.".format(d))
287  return cls(*args, **kwds)
288 
289  def __subclasscheck__(self, subclass):
290  # Special method hook for the issubclass built-in: we return true for
291  # any registered type or true subclass thereof.
292  if subclass in self._registry:
293  return True
294  for v in self._registry.values():
295  if issubclass(subclass, v):
296  return True
297  return False
298 
299  def __instancecheck__(self, instance):
300  # Special method hook for the isinstance built-in: we return true for
301  # an instance of any registered type or true subclass thereof.
302  if type(instance) in self._registry:
303  return True
304  for v in self._registry.values():
305  if isinstance(instance, v):
306  return True
307  return False
308 
309  def __subclasses__(self):
310  """Return a tuple of all classes that inherit from this class.
311  """
312  # This special method isn't defined as part of the Python data model,
313  # but it exists on builtins (including ABCMeta), and it provides useful
314  # functionality.
315  return tuple(set(self._registry.values()))
316 
317  def register(self, key, subclass):
318  """Register a subclass of this ABC with the given key (a string,
319  number, type, or other hashable).
320 
321  Register may only be called once for a given key or a given subclass.
322  """
323  if key is None:
324  raise ValueError("None may not be used as a key.")
325  if subclass in self._registry.values():
326  raise ValueError(
327  "This subclass has already registered with another key; "
328  "use alias() instead."
329  )
330  if self._registry.setdefault(key, subclass) != subclass:
331  if len(self.TEMPLATE_PARAMS) == 1:
332  d = {self.TEMPLATE_PARAMS[0]: key}
333  else:
334  d = {k: v for k, v in zip(self.TEMPLATE_PARAMS, key)}
335  raise KeyError(
336  "Another subclass is already registered with {}".format(d)
337  )
338  # If the key used to register a class matches the default key,
339  # make the static methods available through the ABC
340  if self.TEMPLATE_DEFAULTS:
341  defaults = (self.TEMPLATE_DEFAULTS[0] if
342  len(self.TEMPLATE_DEFAULTS) == 1 else
343  self.TEMPLATE_DEFAULTS)
344  if key == defaults:
345  conflictStr = ("Base Class has an attribute with the same"
346  "name as a {} method in the default subclass"
347  ". Cannot link {} method to base class")
348  # In the following if statements, the explicit lookup in
349  # __dict__ must be done, as a call to getattr returns the
350  # bound method, which no longer reports as a static or class
351  # method. The static methods must be transfered to the ABC
352  # in this unbound state, so that python will still see them
353  # as static methods and not attempt to pass self. The class
354  # methods must be transfered to the ABC as a bound method
355  # so that the correct cls be called with the class method
356  for name in subclass.__dict__:
357  if name == "__new__":
358  continue
359  obj = subclass.__dict__[name]
360  # copy over the static methods
361  isBuiltin = isinstance(obj, types.BuiltinFunctionType)
362  isStatic = isinstance(obj, staticmethod)
363  if isBuiltin or isStatic:
364  if hasattr(self, name):
365  raise AttributeError(conflictStr.format("static"))
366  setattr(self, name, obj)
367  # copy over the class methods
368  elif isinstance(obj, classmethod):
369  if hasattr(self, name):
370  raise AttributeError(conflictStr.format("class"))
371  setattr(self, name, getattr(subclass, name))
372 
373  def setattrSafe(name, value):
374  try:
375  currentValue = getattr(subclass, name)
376  if currentValue != value:
377  msg = ("subclass already has a '{}' attribute with "
378  "value {} != {}.")
379  raise ValueError(
380  msg.format(name, currentValue, value)
381  )
382  except AttributeError:
383  setattr(subclass, name, value)
384 
385  if len(self.TEMPLATE_PARAMS) == 1:
386  setattrSafe(self.TEMPLATE_PARAMS[0], key)
387  elif len(self.TEMPLATE_PARAMS) == len(key):
388  for p, k in zip(self.TEMPLATE_PARAMS, key):
389  setattrSafe(p, k)
390  else:
391  raise ValueError(
392  "key must have {} elements (one for each of {})".format(
393  len(self.TEMPLATE_PARAMS), self.TEMPLATE_PARAMS
394  )
395  )
396 
397  for name, attr in self._inherited.items():
398  setattr(subclass, name, attr)
399 
400  def alias(self, key, subclass):
401  """Add an alias that allows an existing subclass to be accessed with a
402  different key.
403  """
404  if key is None:
405  raise ValueError("None may not be used as a key.")
406  if key in self._registry:
407  raise KeyError("Cannot multiply-register key {}".format(key))
408  primaryKey = tuple(getattr(subclass, p, None)
409  for p in self.TEMPLATE_PARAMS)
410  if len(primaryKey) == 1:
411  # indices are only tuples if there are multiple elements
412  primaryKey = primaryKey[0]
413  if self._registry.get(primaryKey, None) != subclass:
414  raise ValueError("Subclass is not registered with this base class.")
415  self._registry[key] = subclass
416 
417  # Immutable mapping interface defined below. We don't use collections
418  # mixins because we don't want their comparison operators.
419 
420  def __getitem__(self, key):
421  return self._registry[key]
422 
423  def __iter__(self):
424  return iter(self._registry)
425 
426  def __len__(self):
427  return len(self._registry)
428 
429  def __contains__(self, key):
430  return key in self._registry
431 
432  def keys(self):
433  """Return an iterable containing all keys (including aliases).
434  """
435  return self._registry.keys()
436 
437  def values(self):
438  """Return an iterable of registered subclasses, with duplicates
439  corresponding to any aliases.
440  """
441  return self._registry.values()
442 
443  def items(self):
444  """Return an iterable of (key, subclass) pairs.
445  """
446  return self._registry.items()
447 
448  def get(self, key, default=None):
449  """Return the subclass associated with the given key (including
450  aliases), or ``default`` if the key is not recognized.
451  """
452  return self._registry.get(key, default)
def isAttributeSafeToTransfer(name, value)
Definition: wrappers.py:46
def __call__(self, args, kwds)
Definition: wrappers.py:265
daf::base::PropertySet * set
Definition: fits.cc:818
def continueClass(cls)
Definition: wrappers.py:60
def alias(self, key, subclass)
Definition: wrappers.py:400
def __instancecheck__(self, instance)
Definition: wrappers.py:299
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:134
def __subclasscheck__(self, subclass)
Definition: wrappers.py:289
def __new__(cls, name, bases, attrs)
Definition: wrappers.py:233
def get(self, key, default=None)
Definition: wrappers.py:448
def inClass(cls, name=None)
Definition: wrappers.py:93
def register(self, key, subclass)
Definition: wrappers.py:317