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