Coverage for python/lsst/utils/wrappers.py : 11%

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#
24import sys
25import types
27import numpy as np
29__all__ = ("continueClass", "inClass", "TemplateMeta")
32INTRINSIC_SPECIAL_ATTRIBUTES = frozenset((
33 "__qualname__",
34 "__module__",
35 "__metaclass__",
36 "__dict__",
37 "__weakref__",
38 "__class__",
39 "__subclasshook__",
40 "__name__",
41 "__doc__",
42))
45def isAttributeSafeToTransfer(name, value):
46 """Return True if an attribute is safe to monkeypatch-transfer to another
47 class.
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
59def continueClass(cls):
60 """Re-open the decorated class, adding any new definitions into the original.
62 For example:
64 .. code-block:: python
66 class Foo:
67 pass
69 @continueClass
70 class Foo:
71 def run(self):
72 return None
74 is equivalent to:
76 .. code-block:: python
78 class Foo:
79 def run(self):
80 return None
82 .. warning::
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.
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
102def inClass(cls, name=None):
103 """Add the decorated function to the given class as a method.
105 For example:
107 .. code-block:: python
109 class Foo:
110 pass
112 @inClass(Foo)
113 def run(self):
114 return None
116 is equivalent to:
118 .. code-block:: python
120 class Foo:
121 def run(self):
122 return None
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
152class TemplateMeta(type):
153 """A metaclass for abstract base classes that tie together wrapped C++
154 template types.
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.
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::
172 .. code-block:: python
174 import numpy as np
175 from ._image import ImageF, ImageD
177 class Image(metaclass=TemplateMeta):
178 pass
180 Image.register(np.float32, ImageF)
181 Image.register(np.float64, ImageD)
182 Image.alias("F", ImageF)
183 Image.alias("D", ImageD)
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).
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::
194 .. code-block:: python
196 img = Image(52, 64, dtype=np.float32)
198 This simply forwards additional positional and keyword arguments to the
199 wrapped template class's constructor.
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.
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::
217 .. code-block:: python
219 class Image(metaclass=TemplateMeta):
221 def sum(self):
222 return np.sum(self.getArray())
224 Image.register(np.float32, ImageF)
225 Image.register(np.float64, ImageD)
227 .. note::
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.
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::
243 .. code-block:: python
245 Image[np.float32] -> ImageF
246 Image["D"] -> ImageD
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.
253 .. warning::
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)..
259 """
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)
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
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)
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)
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
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
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()))
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).
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))
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)
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 )
426 for name, attr in cls._inherited.items():
427 setattr(subclass, name, attr)
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
446 # Immutable mapping interface defined below. We don't use collections
447 # mixins because we don't want their comparison operators.
449 def __getitem__(cls, key):
450 return cls._registry[key]
452 def __iter__(cls):
453 return iter(cls._registry)
455 def __len__(cls):
456 return len(cls._registry)
458 def __contains__(cls, key):
459 return key in cls._registry
461 def keys(cls):
462 """Return an iterable containing all keys (including aliases).
463 """
464 return cls._registry.keys()
466 def values(cls):
467 """Return an iterable of registered subclasses, with duplicates
468 corresponding to any aliases.
469 """
470 return cls._registry.values()
472 def items(cls):
473 """Return an iterable of (key, subclass) pairs.
474 """
475 return cls._registry.items()
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)