Coverage for python/lsst/pex/config/registry.py: 30%
104 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-06 02:38 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-06 02:38 -0700
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, on_none=None):
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`.
197 on_none: `Callable`, optional
198 A callable that should be invoked when ``apply`` is called but the
199 selected name or names is `None`. Will be passed the field
200 attribute proxy (`RegistryInstanceDict`) and then all positional
201 and keyword arguments passed to ``apply``.
203 Returns
204 -------
205 field : `lsst.pex.config.RegistryField`
206 `~lsst.pex.config.RegistryField` Configuration field.
207 """
208 return RegistryField(doc, self, default, optional, multi, on_none=on_none)
211class RegistryAdaptor(collections.abc.Mapping):
212 """Private class that makes a `Registry` behave like the thing a
213 `~lsst.pex.config.ConfigChoiceField` expects.
215 Parameters
216 ----------
217 registry : `Registry`
218 `Registry` instance.
219 """
221 def __init__(self, registry):
222 self.registry = registry
224 def __getitem__(self, k):
225 return self.registry[k].ConfigClass
227 def __iter__(self):
228 return iter(self.registry)
230 def __len__(self):
231 return len(self.registry)
233 def __contains__(self, k):
234 return k in self.registry
237class RegistryInstanceDict(ConfigInstanceDict):
238 """Dictionary of instantiated configs, used to populate a `RegistryField`.
240 Parameters
241 ----------
242 config : `lsst.pex.config.Config`
243 Configuration instance.
244 field : `RegistryField`
245 Configuration field.
246 """
248 def __init__(self, config, field):
249 ConfigInstanceDict.__init__(self, config, field)
250 self.registry = field.registry
252 def _getTarget(self):
253 if self._field.multi:
254 raise FieldValidationError(
255 self._field, self._config, "Multi-selection field has no attribute 'target'"
256 )
257 return self.types.registry[self._selection]
259 target = property(_getTarget)
261 def _getTargets(self):
262 if not self._field.multi:
263 raise FieldValidationError(
264 self._field, self._config, "Single-selection field has no attribute 'targets'"
265 )
266 return [self.types.registry[c] for c in self._selection]
268 targets = property(_getTargets)
270 def apply(self, *args, **kwargs):
271 """Call the active target(s) with the active config as a keyword arg.
273 Parameters
274 ----------
275 selection : `str` or `~collections.abc.Iterable` [ `str` ]
276 Name or names of targets, depending on whether ``multi=True``.
277 *args, **kwargs
278 Additional arguments will be passed on to the configurable
279 target(s).
281 Returns
282 -------
283 result
284 If this is a single-selection field, the return value from calling
285 the target. If this is a multi-selection field, a list thereof.
286 """
287 if self.active is None:
288 if self._field._on_none is not None:
289 return self._field._on_none(self, *args, **kwargs)
290 msg = "No selection has been made. Options: %s" % " ".join(self.types.registry.keys())
291 raise FieldValidationError(self._field, self._config, msg)
292 return self.apply_with(self._selection, *args, **kwargs)
294 def apply_with(self, selection, *args, **kwargs):
295 """Call named target(s) with the corresponding config as a keyword
296 arg.
298 Parameters
299 ----------
300 selection : `str` or `~collections.abc.Iterable` [ `str` ]
301 Name or names of targets, depending on whether ``multi=True``.
302 *args, **kwargs
303 Additional arguments will be passed on to the configurable
304 target(s).
306 Returns
307 -------
308 result
309 If this is a single-selection field, the return value from calling
310 the target. If this is a multi-selection field, a list thereof.
312 Notes
313 -----
314 This method ignores the current selection in the ``name`` or ``names``
315 attribute, which is usually not what you want. This method is most
316 useful in ``on_none`` callbacks provided at field construction, which
317 allow a context-dependent default to be used when no selection is
318 configured.
319 """
320 if self._field.multi:
321 retvals = []
322 for c in selection:
323 retvals.append(self.types.registry[c](*args, config=self[c], **kwargs))
324 return retvals
325 else:
326 return self.types.registry[selection](*args, config=self[selection], **kwargs)
328 def __setattr__(self, attr, value):
329 if attr == "registry":
330 object.__setattr__(self, attr, value)
331 else:
332 ConfigInstanceDict.__setattr__(self, attr, value)
335class RegistryField(ConfigChoiceField):
336 """A configuration field whose options are defined in a `Registry`.
338 Parameters
339 ----------
340 doc : `str`
341 A description of the field.
342 registry : `Registry`
343 The registry that contains this field.
344 default : `str`, optional
345 The default target key.
346 optional : `bool`, optional
347 When `False`, `lsst.pex.config.Config.validate` fails if the field's
348 value is `None`.
349 multi : `bool`, optional
350 If `True`, the field allows multiple selections. The default is
351 `False`.
352 on_none: `Callable`, optional
353 A callable that should be invoked when ``apply`` is called but the
354 selected name or names is `None`. Will be passed the field attribute
355 proxy (`RegistryInstanceDict`) and then all positional and keyword
356 arguments passed to ``apply``.
358 See also
359 --------
360 ChoiceField
361 ConfigChoiceField
362 ConfigDictField
363 ConfigField
364 ConfigurableField
365 DictField
366 Field
367 ListField
368 RangeField
369 """
371 instanceDictClass = RegistryInstanceDict
372 """Class used to hold configurable instances in the field.
373 """
375 def __init__(self, doc, registry, default=None, optional=False, multi=False, on_none=None):
376 types = RegistryAdaptor(registry)
377 self.registry = registry
378 self._on_none = on_none
379 ConfigChoiceField.__init__(self, doc, types, default, optional, multi)
381 def __deepcopy__(self, memo):
382 """Customize deep-copying, want a reference to the original registry.
384 WARNING: this must be overridden by subclasses if they change the
385 constructor signature!
386 """
387 other = type(self)(
388 doc=self.doc,
389 registry=self.registry,
390 default=copy.deepcopy(self.default),
391 optional=self.optional,
392 multi=self.multi,
393 on_none=self._on_none,
394 )
395 other.source = self.source
396 return other
399def makeRegistry(doc, configBaseType=Config):
400 """Create a `Registry`.
402 Parameters
403 ----------
404 doc : `str`
405 Docstring for the created `Registry` (this is set as the ``__doc__``
406 attribute of the `Registry` instance.
407 configBaseType : `lsst.pex.config.Config`-type
408 Base type of config classes in the `Registry`
409 (`lsst.pex.config.Registry.configBaseType`).
411 Returns
412 -------
413 registry : `Registry`
414 Registry with ``__doc__`` and `~Registry.configBaseType` attributes
415 set.
416 """
417 cls = type("Registry", (Registry,), {"__doc__": doc})
418 return cls(configBaseType=configBaseType)
421def registerConfigurable(name, registry, ConfigClass=None):
422 """A decorator that adds a class as a configurable in a `Registry`
423 instance.
425 Parameters
426 ----------
427 name : `str`
428 Name of the target (the decorated class) in the ``registry``.
429 registry : `Registry`
430 The `Registry` instance that the decorated class is added to.
431 ConfigClass : `lsst.pex.config.Config`-type, optional
432 Config class associated with the configurable. If `None`, the class's
433 ``ConfigClass`` attribute is used instead.
435 See also
436 --------
437 registerConfig
439 Notes
440 -----
441 Internally, this decorator runs `Registry.register`.
442 """
444 def decorate(cls):
445 registry.register(name, target=cls, ConfigClass=ConfigClass)
446 return cls
448 return decorate
451def registerConfig(name, registry, target):
452 """Decorator that adds a class as a ``ConfigClass`` in a `Registry` and
453 associates it with the given configurable.
455 Parameters
456 ----------
457 name : `str`
458 Name of the ``target`` in the ``registry``.
459 registry : `Registry`
460 The registry containing the ``target``.
461 target : obj
462 A configurable type, such as a subclass of `lsst.pipe.base.Task`.
464 See also
465 --------
466 registerConfigurable
468 Notes
469 -----
470 Internally, this decorator runs `Registry.register`.
471 """
473 def decorate(cls):
474 registry.register(name, target=target, ConfigClass=cls)
475 return cls
477 return decorate