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