Source code for lsst.utils.wrappers
#
# LSST Data Management System
#
# Copyright 2008-2017 AURA/LSST.
#
# This product includes software developed by the
# LSST Project (http://www.lsst.org/).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the LSST License Statement and
# the GNU General Public License along with this program. If not,
# see <https://www.lsstcorp.org/LegalNotices/>.
#
from __future__ import absolute_import, division, print_function
import sys
import types
import numpy as np
__all__ = ("continueClass", "inClass", "TemplateMeta")
INTRINSIC_SPECIAL_ATTRIBUTES = frozenset((
"__qualname__",
"__module__",
"__metaclass__",
"__dict__",
"__weakref__",
"__class__",
"__subclasshook__",
"__name__",
"__doc__",
))
def isAttributeSafeToTransfer(name, value):
"""Return True if an attribute is safe to monkeypatch-transfer to another
class.
This rejects special methods that are defined automatically for all
classes, leaving only those explicitly defined in a class decorated by
`continueClass` or registered with an instance of `TemplateMeta`.
"""
if name.startswith("__") and (value is getattr(object, name, None) or
name in INTRINSIC_SPECIAL_ATTRIBUTES):
return False
return True
[docs]def continueClass(cls):
"""Re-open the decorated class, adding any new definitions into the original.
For example::
class Foo:
pass
@continueClass
class Foo:
def run(self):
return None
is equivalent to::
class Foo:
def run(self):
return None
"""
orig = getattr(sys.modules[cls.__module__], cls.__name__)
for name in dir(cls):
# Common descriptors like classmethod and staticmethod can only be
# accessed without invoking their magic if we use __dict__; if we use
# getattr on those we'll get e.g. a bound method instance on the dummy
# class rather than a classmethod instance we can put on the target
# class.
attr = cls.__dict__.get(name, None) or getattr(cls, name)
if isAttributeSafeToTransfer(name, attr):
setattr(orig, name, attr)
return orig
[docs]def inClass(cls, name=None):
"""Add the decorated function to the given class as a method.
For example::
class Foo:
pass
@inClass(Foo)
def run(self):
return None
is equivalent to::
class Foo:
def run(self):
return None
Standard decorators like ``classmethod``, ``staticmethod``, and
``property`` may be used *after* this decorator. Custom decorators
may only be used if they return an object with a ``__name__`` attribute
or the ``name`` optional argument is provided.
"""
def decorate(func):
# Using 'name' instead of 'name1' breaks the closure because
# assignment signals a strictly local variable.
name1 = name
if name1 is None:
if hasattr(func, "__name__"):
name1 = func.__name__
else:
if hasattr(func, "__func__"):
# classmethod and staticmethod have __func__ but no __name__
name1 = func.__func__.__name__
elif hasattr(func, "fget"):
# property has fget but no __name__
name1 = func.fget.__name__
else:
raise ValueError(
"Could not guess attribute name for '{}'.".format(func)
)
setattr(cls, name1, func)
return func
return decorate
[docs]class TemplateMeta(type):
"""A metaclass for abstract base classes that tie together wrapped C++
template types.
C++ template classes are most easily wrapped with a separate Python class
for each template type, which results in an unnatural Python interface.
TemplateMeta provides a thin layer that connects these Python classes by
giving them a common base class and acting as a factory to construct them
in a consistent way.
To use, simply create a new class with the name of the template class, and
use ``TemplateMeta`` as its metaclass, and then call ``register`` on each
of its subclasses. This registers the class with a "type key" - usually a
Python representation of the C++ template types. The type key must be a
hashable object - strings, type objects, and tuples of these (for C++
classes with multiple template parameters) are good choices. Alternate
type keys for existing classes can be added by calling ``alias``, but only
after a subclass already been registered with a "primary" type key. For
example (using Python 3 metaclass syntax)::
import numpy as np
from ._image import ImageF, ImageD
class Image(metaclass=TemplateMeta):
pass
Image.register(np.float32, ImageF)
Image.register(np.float64, ImageD)
Image.alias("F", ImageF)
Image.alias("D", ImageD)
We have intentionally used ``numpy`` types as the primary keys for these
objects in this example, with strings as secondary aliases simply because
the primary key is added as a ``dtype`` attribute on the the registered
classes (so ``ImageF.dtype == numpy.float32`` in the above example).
This allows user code to construct objects directly using ``Image``, as
long as an extra ``dtype`` keyword argument is passed that matches one of
the type keys::
img = Image(52, 64, dtype=np.float32)
This simply forwards additional positional and keyword arguments to the
wrapped template class's constructor.
The choice of "dtype" as the name of the template parameter is also
configurable, and in fact multiple template parameters are also supported,
by setting a ``TEMPLATE_PARAMS`` class attribute on the ABC to a tuple
containing the names of the template parameters. A ``TEMPLATE_DEFAULTS``
attribute can also be defined to a tuple of the same length containing
default values for the template parameters, allowing them to be omitted in
constructor calls. When the length of these attributes is more than one,
the type keys passed to ``register`` and ``alias`` should be tuple of the
same length; when the length of these attributes is one, type keys should
generally not be tuples.
As an aid for those writing the Python wrappers for C++ classes,
``TemplateMeta`` also provides a way to add pure-Python methods and other
attributes to the wrapped template classes. To add a ``sum`` method to
all registered types, for example, we can just do::
class Image(metaclass=TemplateMeta):
def sum(self):
return np.sum(self.getArray())
Image.register(np.float32, ImageF)
Image.register(np.float64, ImageD)
.. note::
``TemplateMeta`` works by overriding the ``__instancecheck__`` and
``__subclasscheck__`` special methods, and hence does not appear in
its registered subclasses' method resolution order or ``__bases__``
attributes. That means its attributes are not inherited by registered
subclasses. Instead, attributes added to an instance of
``TemplateMeta`` are *copied* into the types registered with it. These
attributes will thus *replace* existing attributes in those classes
with the same name, and subclasses cannot delegate to base class
implementations of these methods.
Finally, abstract base classes that use ``TemplateMeta`` define a dict-
like interface for accessing their registered subclasses, providing
something like the C++ syntax for templates::
Image[np.float32] -> ImageF
Image["D"] -> ImageD
Both primary dtypes and aliases can be used as keys in this interface,
which means types with aliases will be present multiple times in the dict.
To obtain the sequence of unique subclasses, use the ``__subclasses__``
method.
"""
def __new__(cls, name, bases, attrs):
# __new__ is invoked when the abstract base class is defined (via a
# class statement). We save a dict of class attributes (including
# methods) that were defined in the class body so we can copy them
# to registered subclasses later.
# We also initialize an empty dict to store the registered subclasses.
attrs["_inherited"] = {k: v for k, v in attrs.items()
if isAttributeSafeToTransfer(k, v)}
# The special "TEMPLATE_PARAMS" class attribute, if defined, contains
# names of the template parameters, which we use to set those
# attributes on registered subclasses and intercept arguments to the
# constructor. This line removes it from the dict of things that
# should be inherited while setting a default of 'dtype' if it's not
# defined.
attrs["TEMPLATE_PARAMS"] = \
attrs["_inherited"].pop("TEMPLATE_PARAMS", ("dtype",))
attrs["TEMPLATE_DEFAULTS"] = \
attrs["_inherited"].pop("TEMPLATE_DEFAULTS",
(None,)*len(attrs["TEMPLATE_PARAMS"]))
attrs["_registry"] = dict()
self = type.__new__(cls, name, bases, attrs)
if len(self.TEMPLATE_PARAMS) == 0:
raise ValueError(
"TEMPLATE_PARAMS must be a tuple with at least one element."
)
if len(self.TEMPLATE_DEFAULTS) != len(self.TEMPLATE_PARAMS):
raise ValueError(
"TEMPLATE_PARAMS and TEMPLATE_DEFAULTS must have same length."
)
return self
[docs] def __call__(self, *args, **kwds):
# __call__ is invoked when someone tries to construct an instance of
# the abstract base class.
# If the ABC defines a "TEMPLATE_PARAMS" attribute, we use those strings
# as the kwargs we should intercept to find the right type.
# Generate a type mapping key from input keywords. If the type returned
# from the keyword lookup is a numpy dtype object, fetch the underlying
# type of the dtype
key = []
for p, d in zip(self.TEMPLATE_PARAMS, self.TEMPLATE_DEFAULTS):
tempKey = kwds.pop(p, d)
if isinstance(tempKey, np.dtype):
tempKey = tempKey.type
key.append(tempKey)
key = tuple(key)
# indices are only tuples if there are multiple elements
cls = self._registry.get(key[0] if len(key) == 1 else key, None)
if cls is None:
d = {k: v for k, v in zip(self.TEMPLATE_PARAMS, key)}
raise TypeError("No registered subclass for {}.".format(d))
return cls(*args, **kwds)
def __subclasscheck__(self, subclass):
# Special method hook for the issubclass built-in: we return true for
# any registered type or true subclass thereof.
if subclass in self._registry:
return True
for v in self._registry.values():
if issubclass(subclass, v):
return True
return False
def __instancecheck__(self, instance):
# Special method hook for the isinstance built-in: we return true for
# an instance of any registered type or true subclass thereof.
if type(instance) in self._registry:
return True
for v in self._registry.values():
if isinstance(instance, v):
return True
return False
def __subclasses__(self):
"""Return a tuple of all classes that inherit from this class.
"""
# This special method isn't defined as part of the Python data model,
# but it exists on builtins (including ABCMeta), and it provides useful
# functionality.
return tuple(set(self._registry.values()))
[docs] def register(self, key, subclass):
"""Register a subclass of this ABC with the given key (a string,
number, type, or other hashable).
Register may only be called once for a given key or a given subclass.
"""
if key is None:
raise ValueError("None may not be used as a key.")
if subclass in self._registry.values():
raise ValueError(
"This subclass has already registered with another key; "
"use alias() instead."
)
if self._registry.setdefault(key, subclass) != subclass:
if len(self.TEMPLATE_PARAMS) == 1:
d = {self.TEMPLATE_PARAMS[0]: key}
else:
d = {k: v for k, v in zip(self.TEMPLATE_PARAMS, key)}
raise KeyError(
"Another subclass is already registered with {}".format(d)
)
# If the key used to register a class matches the default key,
# make the static methods available through the ABC
if self.TEMPLATE_DEFAULTS:
defaults = (self.TEMPLATE_DEFAULTS[0] if
len(self.TEMPLATE_DEFAULTS) == 1 else
self.TEMPLATE_DEFAULTS)
if key == defaults:
conflictStr = ("Base Class has an attribute with the same"
"name as a {} method in the default subclass"
". Cannot link {} method to base class")
# In the following if statements, the explicit lookup in
# __dict__ must be done, as a call to getattr returns the
# bound method, which no longer reports as a static or class
# method. The static methods must be transfered to the ABC
# in this unbound state, so that python will still see them
# as static methods and not attempt to pass self. The class
# methods must be transfered to the ABC as a bound method
# so that the correct cls be called with the class method
for name in subclass.__dict__:
if name == "__new__":
continue
obj = subclass.__dict__[name]
# copy over the static methods
isBuiltin = isinstance(obj, types.BuiltinFunctionType)
isStatic = isinstance(obj, staticmethod)
if isBuiltin or isStatic:
if hasattr(self, name):
raise AttributeError(conflictStr.format("static"))
setattr(self, name, obj)
# copy over the class methods
elif isinstance(obj, classmethod):
if hasattr(self, name):
raise AttributeError(conflictStr.format("class"))
setattr(self, name, getattr(subclass, name))
def setattrSafe(name, value):
try:
currentValue = getattr(subclass, name)
if currentValue != value:
msg = ("subclass already has a '{}' attribute with "
"value {} != {}.")
raise ValueError(
msg.format(name, currentValue, value)
)
except AttributeError:
setattr(subclass, name, value)
if len(self.TEMPLATE_PARAMS) == 1:
setattrSafe(self.TEMPLATE_PARAMS[0], key)
elif len(self.TEMPLATE_PARAMS) == len(key):
for p, k in zip(self.TEMPLATE_PARAMS, key):
setattrSafe(p, k)
else:
raise ValueError(
"key must have {} elements (one for each of {})".format(
len(self.TEMPLATE_PARAMS), self.TEMPLATE_PARAMS
)
)
for name, attr in self._inherited.items():
setattr(subclass, name, attr)
[docs] def alias(self, key, subclass):
"""Add an alias that allows an existing subclass to be accessed with a
different key.
"""
if key is None:
raise ValueError("None may not be used as a key.")
if key in self._registry:
raise KeyError("Cannot multiply-register key {}".format(key))
primaryKey = tuple(getattr(subclass, p, None)
for p in self.TEMPLATE_PARAMS)
if len(primaryKey) == 1:
# indices are only tuples if there are multiple elements
primaryKey = primaryKey[0]
if self._registry.get(primaryKey, None) != subclass:
raise ValueError("Subclass is not registered with this base class.")
self._registry[key] = subclass
# Immutable mapping interface defined below. We don't use collections
# mixins because we don't want their comparison operators.
def __getitem__(self, key):
return self._registry[key]
def __iter__(self):
return iter(self._registry)
def __len__(self):
return len(self._registry)
def __contains__(self, key):
return key in self._registry
[docs] def keys(self):
"""Return an iterable containing all keys (including aliases).
"""
return self._registry.keys()
[docs] def values(self):
"""Return an iterable of registered subclasses, with duplicates
corresponding to any aliases.
"""
return self._registry.values()
[docs] def items(self):
"""Return an iterable of (key, subclass) pairs.
"""
return self._registry.items()
[docs] def get(self, key, default=None):
"""Return the subclass associated with the given key (including
aliases), or ``default`` if the key is not recognized.
"""
return self._registry.get(key, default)