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