Source code for lsst.pex.config.registry

#
# LSST Data Management System
# Copyright 2008, 2009, 2010 LSST Corporation.
#
# 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 <http://www.lsstcorp.org/LegalNotices/>.
#
from builtins import object

import collections
import copy

from .config import Config, FieldValidationError, _typeStr
from .configChoiceField import ConfigInstanceDict, ConfigChoiceField

__all__ = ("Registry", "makeRegistry", "RegistryField", "registerConfig", "registerConfigurable")


class ConfigurableWrapper(object):
    """A wrapper for configurables

    Used for configurables that don't contain a ConfigClass attribute,
    or contain one that is being overridden.
    """
    def __init__(self, target, ConfigClass):
        self.ConfigClass = ConfigClass
        self._target = target

    def __call__(self, *args, **kwargs):
        return self._target(*args, **kwargs)


class Registry(collections.Mapping):
    """A base class for global registries, mapping names to configurables.

    There are no hard requirements on configurable, but they typically create an algorithm
    or are themselves the algorithm, and typical usage is as follows:
    - configurable is a callable whose call signature is (config, ...extra arguments...)
    - All configurables added to a particular registry will have the same call signature
    - All configurables in a registry will typically share something important in common.
      For example all configurables in psfMatchingRegistry return a psf matching
      class that has a psfMatch method with a particular call signature.

    A registry acts like a read-only dictionary with an additional register method to add items.
    The dict contains configurables and each configurable has an instance ConfigClass.

    Example:
    registry = Registry()
    class FooConfig(Config):
        val = Field(dtype=int, default=3, doc="parameter for Foo")
    class Foo(object):
        ConfigClass = FooConfig
        def __init__(self, config):
            self.config = config
        def addVal(self, num):
            return self.config.val + num
    registry.register("foo", Foo)
    names = registry.keys() # returns ("foo",)
    fooConfigurable = registry["foo"]
    fooConfig = fooItem.ConfigClass()
    foo = fooConfigurable(fooConfig)
    foo.addVal(5) # returns config.val + 5
    """

    def __init__(self, configBaseType=Config):
        """Construct a registry of name: configurables

        @param configBaseType: base class for config classes in registry
        """
        if not issubclass(configBaseType, Config):
            raise TypeError("configBaseType=%s must be a subclass of Config" % _typeStr(configBaseType,))
        self._configBaseType = configBaseType
        self._dict = {}

    def register(self, name, target, ConfigClass=None):
        """Add a new item to the registry.

        @param target       A callable 'object that takes a Config instance as its first argument.
                            This may be a Python type, but is not required to be.
        @param ConfigClass  A subclass of pex_config Config used to configure the configurable;
                            if None then configurable.ConfigClass is used.

        @note: If ConfigClass is provided then then 'target' is wrapped in a new object that forwards
               function calls to it.  Otherwise the original 'target' is stored.

        @raise AttributeError if ConfigClass is None and target does not have attribute ConfigClass
        """
        if name in self._dict:
            raise RuntimeError("An item with name %r already exists" % name)
        if ConfigClass is None:
            wrapper = target
        else:
            wrapper = ConfigurableWrapper(target, ConfigClass)
        if not issubclass(wrapper.ConfigClass, self._configBaseType):
            raise TypeError("ConfigClass=%s is not a subclass of %r" %
                            (_typeStr(wrapper.ConfigClass), _typeStr(self._configBaseType)))
        self._dict[name] = wrapper

    def __getitem__(self, key):
        return self._dict[key]

    def __len__(self):
        return len(self._dict)

    def __iter__(self):
        return iter(self._dict)

    def __contains__(self, key):
        return key in self._dict

    def makeField(self, doc, default=None, optional=False, multi=False):
        return RegistryField(doc, self, default, optional, multi)


class RegistryAdaptor(collections.Mapping):
    """Private class that makes a Registry behave like the thing a ConfigChoiceField expects."""

    def __init__(self, registry):
        self.registry = registry

    def __getitem__(self, k):
        return self.registry[k].ConfigClass

    def __iter__(self):
        return iter(self.registry)

    def __len__(self):
        return len(self.registry)

    def __contains__(self, k):
        return k in self.registry


class RegistryInstanceDict(ConfigInstanceDict):
    def __init__(self, config, field):
        ConfigInstanceDict.__init__(self, config, field)
        self.registry = field.registry

    def _getTarget(self):
        if self._field.multi:
            raise FieldValidationError(self._field, self._config,
                                       "Multi-selection field has no attribute 'target'")
        return self._field.typemap.registry[self._selection]
    target = property(_getTarget)

    def _getTargets(self):
        if not self._field.multi:
            raise FieldValidationError(self._field, self._config,
                                       "Single-selection field has no attribute 'targets'")
        return [self._field.typemap.registry[c] for c in self._selection]
    targets = property(_getTargets)

    def apply(self, *args, **kw):
        """Call the active target(s) with the active config as a keyword arg

        If this is a multi-selection field, return a list obtained by calling
        each active target with its corresponding active config.

        Additional arguments will be passed on to the configurable target(s)
        """
        if self.active is None:
            msg = "No selection has been made.  Options: %s" % \
                (" ".join(list(self._field.typemap.registry.keys())))
            raise FieldValidationError(self._field, self._config, msg)
        if self._field.multi:
            retvals = []
            for c in self._selection:
                retvals.append(self._field.typemap.registry[c](*args, config=self[c], **kw))
            return retvals
        else:
            return self._field.typemap.registry[self.name](*args, config=self[self.name], **kw)

    def __setattr__(self, attr, value):
        if attr == "registry":
            object.__setattr__(self, attr, value)
        else:
            ConfigInstanceDict.__setattr__(self, attr, value)


class RegistryField(ConfigChoiceField):
    instanceDictClass = RegistryInstanceDict

    def __init__(self, doc, registry, default=None, optional=False, multi=False):
        types = RegistryAdaptor(registry)
        self.registry = registry
        ConfigChoiceField.__init__(self, doc, types, default, optional, multi)

    def __deepcopy__(self, memo):
        """Customize deep-copying, want a reference to the original registry.
        WARNING: this must be overridden by subclasses if they change the
            constructor signature!
        """
        other = type(self)(doc=self.doc, registry=self.registry,
                           default=copy.deepcopy(self.default),
                           optional=self.optional, multi=self.multi)
        other.source = self.source
        return other


def makeRegistry(doc, configBaseType=Config):
    """A convenience function to create a new registry.

    The returned value is an instance of a trivial subclass of Registry whose only purpose is to
    customize its doc string and set attrList.
    """
    cls = type("Registry", (Registry,), {"__doc__": doc})
    return cls(configBaseType=configBaseType)


def registerConfigurable(name, registry, ConfigClass=None):
    """A decorator that adds a class as a configurable in a Registry.

    If the 'ConfigClass' argument is None, the class's ConfigClass attribute will be used.
    """
    def decorate(cls):
        registry.register(name, target=cls, ConfigClass=ConfigClass)
        return cls
    return decorate


def registerConfig(name, registry, target):
    """A decorator that adds a class as a ConfigClass in a Registry, and associates it with the given
    configurable.
    """
    def decorate(cls):
        registry.register(name, target=target, ConfigClass=cls)
        return cls
    return decorate