Coverage for python/lsst/pex/config/registry.py: 31%
99 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-05 10:36 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-05 10:36 +0000
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 ConfigChoiceField, ConfigInstanceDict
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(
119 "configBaseType=%s must be a subclass of Config"
120 % _typeStr(
121 configBaseType,
122 )
123 )
124 self._configBaseType = configBaseType
125 self._dict = {}
127 def register(self, name, target, ConfigClass=None):
128 """Add a new configurable target to the registry.
130 Parameters
131 ----------
132 name : `str`
133 Name that the ``target`` is registered under. The target can
134 be accessed later with `dict`-like patterns using ``name`` as
135 the key.
136 target : obj
137 A configurable type, usually a subclass of `lsst.pipe.base.Task`.
138 ConfigClass : `lsst.pex.config.Config`-type, optional
139 A subclass of `lsst.pex.config.Config` used to configure the
140 configurable. If `None` then the configurable's ``ConfigClass``
141 attribute is used.
143 Raises
144 ------
145 RuntimeError
146 Raised if an item with ``name`` is already in the registry.
147 AttributeError
148 Raised if ``ConfigClass`` is `None` and ``target`` does not have
149 a ``ConfigClass`` attribute.
151 Notes
152 -----
153 If ``ConfigClass`` is provided then the ``target`` configurable is
154 wrapped in a new object that forwards function calls to it. Otherwise
155 the original ``target`` is stored.
156 """
157 if name in self._dict:
158 raise RuntimeError("An item with name %r already exists" % name)
159 if ConfigClass is None:
160 wrapper = target
161 else:
162 wrapper = ConfigurableWrapper(target, ConfigClass)
163 if not issubclass(wrapper.ConfigClass, self._configBaseType):
164 raise TypeError(
165 "ConfigClass=%s is not a subclass of %r"
166 % (_typeStr(wrapper.ConfigClass), _typeStr(self._configBaseType))
167 )
168 self._dict[name] = wrapper
170 def __getitem__(self, key):
171 return self._dict[key]
173 def __len__(self):
174 return len(self._dict)
176 def __iter__(self):
177 return iter(self._dict)
179 def __contains__(self, key):
180 return key in self._dict
182 def makeField(self, doc, default=None, optional=False, multi=False):
183 """Create a `RegistryField` configuration field from this registry.
185 Parameters
186 ----------
187 doc : `str`
188 A description of the field.
189 default : object, optional
190 The default target for the field.
191 optional : `bool`, optional
192 When `False`, `lsst.pex.config.Config.validate` fails if the
193 field's value is `None`.
194 multi : `bool`, optional
195 A flag to allow multiple selections in the `RegistryField` if
196 `True`.
198 Returns
199 -------
200 field : `lsst.pex.config.RegistryField`
201 `~lsst.pex.config.RegistryField` Configuration field.
202 """
203 return RegistryField(doc, self, default, optional, multi)
206class RegistryAdaptor(collections.abc.Mapping):
207 """Private class that makes a `Registry` behave like the thing a
208 `~lsst.pex.config.ConfigChoiceField` expects.
210 Parameters
211 ----------
212 registry : `Registry`
213 `Registry` instance.
214 """
216 def __init__(self, registry):
217 self.registry = registry
219 def __getitem__(self, k):
220 return self.registry[k].ConfigClass
222 def __iter__(self):
223 return iter(self.registry)
225 def __len__(self):
226 return len(self.registry)
228 def __contains__(self, k):
229 return k in self.registry
232class RegistryInstanceDict(ConfigInstanceDict):
233 """Dictionary of instantiated configs, used to populate a `RegistryField`.
235 Parameters
236 ----------
237 config : `lsst.pex.config.Config`
238 Configuration instance.
239 field : `RegistryField`
240 Configuration field.
241 """
243 def __init__(self, config, field):
244 ConfigInstanceDict.__init__(self, config, field)
245 self.registry = field.registry
247 def _getTarget(self):
248 if self._field.multi:
249 raise FieldValidationError(
250 self._field, self._config, "Multi-selection field has no attribute 'target'"
251 )
252 return self.types.registry[self._selection]
254 target = property(_getTarget)
256 def _getTargets(self):
257 if not self._field.multi:
258 raise FieldValidationError(
259 self._field, self._config, "Single-selection field has no attribute 'targets'"
260 )
261 return [self.types.registry[c] for c in self._selection]
263 targets = property(_getTargets)
265 def apply(self, *args, **kw):
266 """Call the active target(s) with the active config as a keyword arg
268 If this is a multi-selection field, return a list obtained by calling
269 each active target with its corresponding active config.
271 Additional arguments will be passed on to the configurable target(s)
272 """
273 if self.active is None:
274 msg = "No selection has been made. Options: %s" % " ".join(self.types.registry.keys())
275 raise FieldValidationError(self._field, self._config, msg)
276 if self._field.multi:
277 retvals = []
278 for c in self._selection:
279 retvals.append(self.types.registry[c](*args, config=self[c], **kw))
280 return retvals
281 else:
282 return self.types.registry[self.name](*args, config=self[self.name], **kw)
284 def __setattr__(self, attr, value):
285 if attr == "registry":
286 object.__setattr__(self, attr, value)
287 else:
288 ConfigInstanceDict.__setattr__(self, attr, value)
291class RegistryField(ConfigChoiceField):
292 """A configuration field whose options are defined in a `Registry`.
294 Parameters
295 ----------
296 doc : `str`
297 A description of the field.
298 registry : `Registry`
299 The registry that contains this field.
300 default : `str`, optional
301 The default target key.
302 optional : `bool`, optional
303 When `False`, `lsst.pex.config.Config.validate` fails if the field's
304 value is `None`.
305 multi : `bool`, optional
306 If `True`, the field allows multiple selections. The default is
307 `False`.
309 See also
310 --------
311 ChoiceField
312 ConfigChoiceField
313 ConfigDictField
314 ConfigField
315 ConfigurableField
316 DictField
317 Field
318 ListField
319 RangeField
320 """
322 instanceDictClass = RegistryInstanceDict
323 """Class used to hold configurable instances in the field.
324 """
326 def __init__(self, doc, registry, default=None, optional=False, multi=False):
327 types = RegistryAdaptor(registry)
328 self.registry = registry
329 ConfigChoiceField.__init__(self, doc, types, default, optional, multi)
331 def __deepcopy__(self, memo):
332 """Customize deep-copying, want a reference to the original registry.
334 WARNING: this must be overridden by subclasses if they change the
335 constructor signature!
336 """
337 other = type(self)(
338 doc=self.doc,
339 registry=self.registry,
340 default=copy.deepcopy(self.default),
341 optional=self.optional,
342 multi=self.multi,
343 )
344 other.source = self.source
345 return other
348def makeRegistry(doc, configBaseType=Config):
349 """Create a `Registry`.
351 Parameters
352 ----------
353 doc : `str`
354 Docstring for the created `Registry` (this is set as the ``__doc__``
355 attribute of the `Registry` instance.
356 configBaseType : `lsst.pex.config.Config`-type
357 Base type of config classes in the `Registry`
358 (`lsst.pex.config.Registry.configBaseType`).
360 Returns
361 -------
362 registry : `Registry`
363 Registry with ``__doc__`` and `~Registry.configBaseType` attributes
364 set.
365 """
366 cls = type("Registry", (Registry,), {"__doc__": doc})
367 return cls(configBaseType=configBaseType)
370def registerConfigurable(name, registry, ConfigClass=None):
371 """A decorator that adds a class as a configurable in a `Registry`
372 instance.
374 Parameters
375 ----------
376 name : `str`
377 Name of the target (the decorated class) in the ``registry``.
378 registry : `Registry`
379 The `Registry` instance that the decorated class is added to.
380 ConfigClass : `lsst.pex.config.Config`-type, optional
381 Config class associated with the configurable. If `None`, the class's
382 ``ConfigClass`` attribute is used instead.
384 See also
385 --------
386 registerConfig
388 Notes
389 -----
390 Internally, this decorator runs `Registry.register`.
391 """
393 def decorate(cls):
394 registry.register(name, target=cls, ConfigClass=ConfigClass)
395 return cls
397 return decorate
400def registerConfig(name, registry, target):
401 """Decorator that adds a class as a ``ConfigClass`` in a `Registry` and
402 associates it with the given configurable.
404 Parameters
405 ----------
406 name : `str`
407 Name of the ``target`` in the ``registry``.
408 registry : `Registry`
409 The registry containing the ``target``.
410 target : obj
411 A configurable type, such as a subclass of `lsst.pipe.base.Task`.
413 See also
414 --------
415 registerConfigurable
417 Notes
418 -----
419 Internally, this decorator runs `Registry.register`.
420 """
422 def decorate(cls):
423 registry.register(name, target=target, ConfigClass=cls)
424 return cls
426 return decorate