Coverage for python/lsst/pex/config/registry.py: 31%
Shortcuts 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
Shortcuts 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# This file is part of pex_config.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
28__all__ = ("Registry", "makeRegistry", "RegistryField", "registerConfig", "registerConfigurable")
30import collections.abc
31import copy
33from .config import Config, FieldValidationError, _typeStr
34from .configChoiceField import ConfigInstanceDict, ConfigChoiceField
37class ConfigurableWrapper:
38 """A wrapper for configurables.
40 Used for configurables that don't contain a ``ConfigClass`` attribute,
41 or contain one that is being overridden.
42 """
44 def __init__(self, target, ConfigClass):
45 self.ConfigClass = ConfigClass
46 self._target = target
48 def __call__(self, *args, **kwargs):
49 return self._target(*args, **kwargs)
52class Registry(collections.abc.Mapping):
53 """A base class for global registries, which map names to configurables.
55 A registry acts like a read-only dictionary with an additional `register`
56 method to add targets. Targets in the registry are configurables (see
57 *Notes*).
59 Parameters
60 ----------
61 configBaseType : `lsst.pex.config.Config`-type
62 The base class for config classes in the registry.
64 Notes
65 -----
66 A configurable is a callable with call signature ``(config, *args)``
67 Configurables typically create an algorithm or are themselves the
68 algorithm. Often configurables are `lsst.pipe.base.Task` subclasses, but
69 this is not required.
71 A ``Registry`` has these requirements:
73 - All configurables added to a particular registry have the same call
74 signature.
75 - All configurables in a registry typically share something important
76 in common. For example, all configurables in ``psfMatchingRegistry``
77 return a PSF matching class that has a ``psfMatch`` method with a
78 particular call signature.
80 Examples
81 --------
82 This examples creates a configurable class ``Foo`` and adds it to a
83 registry. First, creating the configurable:
85 >>> from lsst.pex.config import Registry, Config
86 >>> class FooConfig(Config):
87 ... val = Field(dtype=int, default=3, doc="parameter for Foo")
88 ...
89 >>> class Foo:
90 ... ConfigClass = FooConfig
91 ... def __init__(self, config):
92 ... self.config = config
93 ... def addVal(self, num):
94 ... return self.config.val + num
95 ...
97 Next, create a ``Registry`` instance called ``registry`` and register the
98 ``Foo`` configurable under the ``"foo"`` key:
100 >>> registry = Registry()
101 >>> registry.register("foo", Foo)
102 >>> print(list(registry.keys()))
103 ["foo"]
105 Now ``Foo`` is conveniently accessible from the registry itself.
107 Finally, use the registry to get the configurable class and create an
108 instance of it:
110 >>> FooConfigurable = registry["foo"]
111 >>> foo = FooConfigurable(FooConfigurable.ConfigClass())
112 >>> foo.addVal(5)
113 8
114 """
116 def __init__(self, configBaseType=Config):
117 if not issubclass(configBaseType, Config):
118 raise TypeError("configBaseType=%s must be a subclass of Config" % _typeStr(configBaseType,))
119 self._configBaseType = configBaseType
120 self._dict = {}
122 def register(self, name, target, ConfigClass=None):
123 """Add a new configurable target to the registry.
125 Parameters
126 ----------
127 name : `str`
128 Name that the ``target`` is registered under. The target can
129 be accessed later with `dict`-like patterns using ``name`` as
130 the key.
131 target : obj
132 A configurable type, usually a subclass of `lsst.pipe.base.Task`.
133 ConfigClass : `lsst.pex.config.Config`-type, optional
134 A subclass of `lsst.pex.config.Config` used to configure the
135 configurable. If `None` then the configurable's ``ConfigClass``
136 attribute is used.
138 Raises
139 ------
140 RuntimeError
141 Raised if an item with ``name`` is already in the registry.
142 AttributeError
143 Raised if ``ConfigClass`` is `None` and ``target`` does not have
144 a ``ConfigClass`` attribute.
146 Notes
147 -----
148 If ``ConfigClass`` is provided then the ``target`` configurable is
149 wrapped in a new object that forwards function calls to it. Otherwise
150 the original ``target`` is stored.
151 """
152 if name in self._dict:
153 raise RuntimeError("An item with name %r already exists" % name)
154 if ConfigClass is None:
155 wrapper = target
156 else:
157 wrapper = ConfigurableWrapper(target, ConfigClass)
158 if not issubclass(wrapper.ConfigClass, self._configBaseType):
159 raise TypeError("ConfigClass=%s is not a subclass of %r" %
160 (_typeStr(wrapper.ConfigClass), _typeStr(self._configBaseType)))
161 self._dict[name] = wrapper
163 def __getitem__(self, key):
164 return self._dict[key]
166 def __len__(self):
167 return len(self._dict)
169 def __iter__(self):
170 return iter(self._dict)
172 def __contains__(self, key):
173 return key in self._dict
175 def makeField(self, doc, default=None, optional=False, multi=False):
176 """Create a `RegistryField` configuration field from this registry.
178 Parameters
179 ----------
180 doc : `str`
181 A description of the field.
182 default : object, optional
183 The default target for the field.
184 optional : `bool`, optional
185 When `False`, `lsst.pex.config.Config.validate` fails if the
186 field's value is `None`.
187 multi : `bool`, optional
188 A flag to allow multiple selections in the `RegistryField` if
189 `True`.
191 Returns
192 -------
193 field : `lsst.pex.config.RegistryField`
194 `~lsst.pex.config.RegistryField` Configuration field.
195 """
196 return RegistryField(doc, self, default, optional, multi)
199class RegistryAdaptor(collections.abc.Mapping):
200 """Private class that makes a `Registry` behave like the thing a
201 `~lsst.pex.config.ConfigChoiceField` expects.
203 Parameters
204 ----------
205 registry : `Registry`
206 `Registry` instance.
207 """
209 def __init__(self, registry):
210 self.registry = registry
212 def __getitem__(self, k):
213 return self.registry[k].ConfigClass
215 def __iter__(self):
216 return iter(self.registry)
218 def __len__(self):
219 return len(self.registry)
221 def __contains__(self, k):
222 return k in self.registry
225class RegistryInstanceDict(ConfigInstanceDict):
226 """Dictionary of instantiated configs, used to populate a `RegistryField`.
228 Parameters
229 ----------
230 config : `lsst.pex.config.Config`
231 Configuration instance.
232 field : `RegistryField`
233 Configuration field.
234 """
236 def __init__(self, config, field):
237 ConfigInstanceDict.__init__(self, config, field)
238 self.registry = field.registry
240 def _getTarget(self):
241 if self._field.multi:
242 raise FieldValidationError(self._field, self._config,
243 "Multi-selection field has no attribute 'target'")
244 return self.types.registry[self._selection]
246 target = property(_getTarget)
248 def _getTargets(self):
249 if not self._field.multi:
250 raise FieldValidationError(self._field, self._config,
251 "Single-selection field has no attribute 'targets'")
252 return [self.types.registry[c] for c in self._selection]
254 targets = property(_getTargets)
256 def apply(self, *args, **kw):
257 """Call the active target(s) with the active config as a keyword arg
259 If this is a multi-selection field, return a list obtained by calling
260 each active target with its corresponding active config.
262 Additional arguments will be passed on to the configurable target(s)
263 """
264 if self.active is None:
265 msg = "No selection has been made. Options: %s" % \
266 " ".join(self.types.registry.keys())
267 raise FieldValidationError(self._field, self._config, msg)
268 if self._field.multi:
269 retvals = []
270 for c in self._selection:
271 retvals.append(self.types.registry[c](*args, config=self[c], **kw))
272 return retvals
273 else:
274 return self.types.registry[self.name](*args, config=self[self.name], **kw)
276 def __setattr__(self, attr, value):
277 if attr == "registry":
278 object.__setattr__(self, attr, value)
279 else:
280 ConfigInstanceDict.__setattr__(self, attr, value)
283class RegistryField(ConfigChoiceField):
284 """A configuration field whose options are defined in a `Registry`.
286 Parameters
287 ----------
288 doc : `str`
289 A description of the field.
290 registry : `Registry`
291 The registry that contains this field.
292 default : `str`, optional
293 The default target key.
294 optional : `bool`, optional
295 When `False`, `lsst.pex.config.Config.validate` fails if the field's
296 value is `None`.
297 multi : `bool`, optional
298 If `True`, the field allows multiple selections. The default is
299 `False`.
301 See also
302 --------
303 ChoiceField
304 ConfigChoiceField
305 ConfigDictField
306 ConfigField
307 ConfigurableField
308 DictField
309 Field
310 ListField
311 RangeField
312 """
314 instanceDictClass = RegistryInstanceDict
315 """Class used to hold configurable instances in the field.
316 """
318 def __init__(self, doc, registry, default=None, optional=False, multi=False):
319 types = RegistryAdaptor(registry)
320 self.registry = registry
321 ConfigChoiceField.__init__(self, doc, types, default, optional, multi)
323 def __deepcopy__(self, memo):
324 """Customize deep-copying, want a reference to the original registry.
326 WARNING: this must be overridden by subclasses if they change the
327 constructor signature!
328 """
329 other = type(self)(doc=self.doc, registry=self.registry,
330 default=copy.deepcopy(self.default),
331 optional=self.optional, multi=self.multi)
332 other.source = self.source
333 return other
336def makeRegistry(doc, configBaseType=Config):
337 """Create a `Registry`.
339 Parameters
340 ----------
341 doc : `str`
342 Docstring for the created `Registry` (this is set as the ``__doc__``
343 attribute of the `Registry` instance.
344 configBaseType : `lsst.pex.config.Config`-type
345 Base type of config classes in the `Registry`
346 (`lsst.pex.config.Registry.configBaseType`).
348 Returns
349 -------
350 registry : `Registry`
351 Registry with ``__doc__`` and `~Registry.configBaseType` attributes
352 set.
353 """
354 cls = type("Registry", (Registry,), {"__doc__": doc})
355 return cls(configBaseType=configBaseType)
358def registerConfigurable(name, registry, ConfigClass=None):
359 """A decorator that adds a class as a configurable in a `Registry`
360 instance.
362 Parameters
363 ----------
364 name : `str`
365 Name of the target (the decorated class) in the ``registry``.
366 registry : `Registry`
367 The `Registry` instance that the decorated class is added to.
368 ConfigClass : `lsst.pex.config.Config`-type, optional
369 Config class associated with the configurable. If `None`, the class's
370 ``ConfigClass`` attribute is used instead.
372 See also
373 --------
374 registerConfig
376 Notes
377 -----
378 Internally, this decorator runs `Registry.register`.
379 """
380 def decorate(cls):
381 registry.register(name, target=cls, ConfigClass=ConfigClass)
382 return cls
383 return decorate
386def registerConfig(name, registry, target):
387 """Decorator that adds a class as a ``ConfigClass`` in a `Registry` and
388 associates it with the given configurable.
390 Parameters
391 ----------
392 name : `str`
393 Name of the ``target`` in the ``registry``.
394 registry : `Registry`
395 The registry containing the ``target``.
396 target : obj
397 A configurable type, such as a subclass of `lsst.pipe.base.Task`.
399 See also
400 --------
401 registerConfigurable
403 Notes
404 -----
405 Internally, this decorator runs `Registry.register`.
406 """
407 def decorate(cls):
408 registry.register(name, target=target, ConfigClass=cls)
409 return cls
410 return decorate