Coverage for python/lsst/pex/config/configurableField.py: 29%
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__ = ("ConfigurableInstance", "ConfigurableField")
30import copy
31import weakref
33from .callStack import getCallStack, getStackFrame
34from .comparison import compareConfigs, getComparisonName
35from .config import Config, Field, FieldValidationError, _joinNamePath, _typeStr
38class ConfigurableInstance:
39 """A retargetable configuration in a `ConfigurableField` that proxies
40 a `~lsst.pex.config.Config`.
42 Notes
43 -----
44 ``ConfigurableInstance`` implements ``__getattr__`` and ``__setattr__``
45 methods that forward to the `~lsst.pex.config.Config` it holds.
46 ``ConfigurableInstance`` adds a `retarget` method.
48 The actual `~lsst.pex.config.Config` instance is accessed using the
49 ``value`` property (e.g. to get its documentation). The associated
50 configurable object (usually a `~lsst.pipe.base.Task`) is accessed
51 using the ``target`` property.
52 """
54 def __initValue(self, at, label):
55 """Construct value of field.
57 Notes
58 -----
59 If field.default is an instance of `lsst.pex.config.ConfigClass`,
60 custom construct ``_value`` with the correct values from default.
61 Otherwise, call ``ConfigClass`` constructor
62 """
63 name = _joinNamePath(self._config._name, self._field.name)
64 if type(self._field.default) == self.ConfigClass:
65 storage = self._field.default._storage
66 else:
67 storage = {}
68 value = self._ConfigClass(__name=name, __at=at, __label=label, **storage)
69 object.__setattr__(self, "_value", value)
71 def __init__(self, config, field, at=None, label="default"):
72 object.__setattr__(self, "_config_", weakref.ref(config))
73 object.__setattr__(self, "_field", field)
74 object.__setattr__(self, "__doc__", config)
75 object.__setattr__(self, "_target", field.target)
76 object.__setattr__(self, "_ConfigClass", field.ConfigClass)
77 object.__setattr__(self, "_value", None)
79 if at is None:
80 at = getCallStack()
81 at += [self._field.source]
82 self.__initValue(at, label)
84 history = config._history.setdefault(field.name, [])
85 history.append(("Targeted and initialized from defaults", at, label))
87 @property
88 def _config(self) -> Config:
89 # Config Fields should never outlive their config class instance
90 # assert that as such here
91 assert self._config_() is not None
92 return self._config_()
94 target = property(lambda x: x._target) 94 ↛ exitline 94 didn't run the lambda on line 94
95 """The targeted configurable (read-only).
96 """
98 ConfigClass = property(lambda x: x._ConfigClass) 98 ↛ exitline 98 didn't run the lambda on line 98
99 """The configuration class (read-only)
100 """
102 value = property(lambda x: x._value) 102 ↛ exitline 102 didn't run the lambda on line 102
103 """The `ConfigClass` instance (`lsst.pex.config.ConfigClass`-type,
104 read-only).
105 """
107 def apply(self, *args, **kw):
108 """Call the configurable.
110 Notes
111 -----
112 In addition to the user-provided positional and keyword arguments,
113 the configurable is also provided a keyword argument ``config`` with
114 the value of `ConfigurableInstance.value`.
115 """
116 return self.target(*args, config=self.value, **kw)
118 def retarget(self, target, ConfigClass=None, at=None, label="retarget"):
119 """Target a new configurable and ConfigClass"""
120 if self._config._frozen:
121 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
123 try:
124 ConfigClass = self._field.validateTarget(target, ConfigClass)
125 except BaseException as e:
126 raise FieldValidationError(self._field, self._config, e.message)
128 if at is None:
129 at = getCallStack()
130 object.__setattr__(self, "_target", target)
131 if ConfigClass != self.ConfigClass:
132 object.__setattr__(self, "_ConfigClass", ConfigClass)
133 self.__initValue(at, label)
135 history = self._config._history.setdefault(self._field.name, [])
136 msg = "retarget(target=%s, ConfigClass=%s)" % (_typeStr(target), _typeStr(ConfigClass))
137 history.append((msg, at, label))
139 def __getattr__(self, name):
140 return getattr(self._value, name)
142 def __setattr__(self, name, value, at=None, label="assignment"):
143 """Pretend to be an instance of ConfigClass.
145 Attributes defined by ConfigurableInstance will shadow those defined
146 in ConfigClass
147 """
148 if self._config._frozen:
149 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
151 if name in self.__dict__:
152 # attribute exists in the ConfigurableInstance wrapper
153 object.__setattr__(self, name, value)
154 else:
155 if at is None:
156 at = getCallStack()
157 self._value.__setattr__(name, value, at=at, label=label)
159 def __delattr__(self, name, at=None, label="delete"):
160 """
161 Pretend to be an isntance of ConfigClass.
162 Attributes defiend by ConfigurableInstance will shadow those defined
163 in ConfigClass
164 """
165 if self._config._frozen:
166 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
168 try:
169 # attribute exists in the ConfigurableInstance wrapper
170 object.__delattr__(self, name)
171 except AttributeError:
172 if at is None:
173 at = getCallStack()
174 self._value.__delattr__(name, at=at, label=label)
177class ConfigurableField(Field):
178 """A configuration field (`~lsst.pex.config.Field` subclass) that can be
179 can be retargeted towards a different configurable (often a
180 `lsst.pipe.base.Task` subclass).
182 The ``ConfigurableField`` is often used to configure subtasks, which are
183 tasks (`~lsst.pipe.base.Task`) called by a parent task.
185 Parameters
186 ----------
187 doc : `str`
188 A description of the configuration field.
189 target : configurable class
190 The configurable target. Configurables have a ``ConfigClass``
191 attribute. Within the task framework, configurables are
192 `lsst.pipe.base.Task` subclasses)
193 ConfigClass : `lsst.pex.config.Config`-type, optional
194 The subclass of `lsst.pex.config.Config` expected as the configuration
195 class of the ``target``. If ``ConfigClass`` is unset then
196 ``target.ConfigClass`` is used.
197 default : ``ConfigClass``-type, optional
198 The default configuration class. Normally this parameter is not set,
199 and defaults to ``ConfigClass`` (or ``target.ConfigClass``).
200 check : callable, optional
201 Callable that takes the field's value (the ``target``) as its only
202 positional argument, and returns `True` if the ``target`` is valid (and
203 `False` otherwise).
204 deprecated : None or `str`, optional
205 A description of why this Field is deprecated, including removal date.
206 If not None, the string is appended to the docstring for this Field.
208 See also
209 --------
210 ChoiceField
211 ConfigChoiceField
212 ConfigDictField
213 ConfigField
214 DictField
215 Field
216 ListField
217 RangeField
218 RegistryField
220 Notes
221 -----
222 You can use the `ConfigurableInstance.apply` method to construct a
223 fully-configured configurable.
224 """
226 def validateTarget(self, target, ConfigClass):
227 """Validate the target and configuration class.
229 Parameters
230 ----------
231 target
232 The configurable being verified.
233 ConfigClass : `lsst.pex.config.Config`-type or `None`
234 The configuration class associated with the ``target``. This can
235 be `None` if ``target`` has a ``ConfigClass`` attribute.
237 Raises
238 ------
239 AttributeError
240 Raised if ``ConfigClass`` is `None` and ``target`` does not have a
241 ``ConfigClass`` attribute.
242 TypeError
243 Raised if ``ConfigClass`` is not a `~lsst.pex.config.Config`
244 subclass.
245 ValueError
246 Raised if:
248 - ``target`` is not callable (callables have a ``__call__``
249 method).
250 - ``target`` is not startically defined (does not have
251 ``__module__`` or ``__name__`` attributes).
252 """
253 if ConfigClass is None:
254 try:
255 ConfigClass = target.ConfigClass
256 except Exception:
257 raise AttributeError("'target' must define attribute 'ConfigClass'")
258 if not issubclass(ConfigClass, Config): 258 ↛ 259line 258 didn't jump to line 259, because the condition on line 258 was never true
259 raise TypeError(
260 "'ConfigClass' is of incorrect type %s."
261 "'ConfigClass' must be a subclass of Config" % _typeStr(ConfigClass)
262 )
263 if not hasattr(target, "__call__"): 263 ↛ 264line 263 didn't jump to line 264, because the condition on line 263 was never true
264 raise ValueError("'target' must be callable")
265 if not hasattr(target, "__module__") or not hasattr(target, "__name__"): 265 ↛ 266line 265 didn't jump to line 266, because the condition on line 265 was never true
266 raise ValueError(
267 "'target' must be statically defined (must have '__module__' and '__name__' attributes)"
268 )
269 return ConfigClass
271 def __init__(self, doc, target, ConfigClass=None, default=None, check=None, deprecated=None):
272 ConfigClass = self.validateTarget(target, ConfigClass)
274 if default is None:
275 default = ConfigClass
276 if default != ConfigClass and type(default) != ConfigClass: 276 ↛ 277line 276 didn't jump to line 277, because the condition on line 276 was never true
277 raise TypeError(
278 "'default' is of incorrect type %s. Expected %s" % (_typeStr(default), _typeStr(ConfigClass))
279 )
281 source = getStackFrame()
282 self._setup(
283 doc=doc,
284 dtype=ConfigurableInstance,
285 default=default,
286 check=check,
287 optional=False,
288 source=source,
289 deprecated=deprecated,
290 )
291 self.target = target
292 self.ConfigClass = ConfigClass
294 def __getOrMake(self, instance, at=None, label="default"):
295 value = instance._storage.get(self.name, None)
296 if value is None:
297 if at is None:
298 at = getCallStack(1)
299 value = ConfigurableInstance(instance, self, at=at, label=label)
300 instance._storage[self.name] = value
301 return value
303 def __get__(self, instance, owner=None, at=None, label="default"):
304 if instance is None or not isinstance(instance, Config):
305 return self
306 else:
307 return self.__getOrMake(instance, at=at, label=label)
309 def __set__(self, instance, value, at=None, label="assignment"):
310 if instance._frozen:
311 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
312 if at is None:
313 at = getCallStack()
314 oldValue = self.__getOrMake(instance, at=at)
316 if isinstance(value, ConfigurableInstance):
317 oldValue.retarget(value.target, value.ConfigClass, at, label)
318 oldValue.update(__at=at, __label=label, **value._storage)
319 elif type(value) == oldValue._ConfigClass:
320 oldValue.update(__at=at, __label=label, **value._storage)
321 elif value == oldValue.ConfigClass:
322 value = oldValue.ConfigClass()
323 oldValue.update(__at=at, __label=label, **value._storage)
324 else:
325 msg = "Value %s is of incorrect type %s. Expected %s" % (
326 value,
327 _typeStr(value),
328 _typeStr(oldValue.ConfigClass),
329 )
330 raise FieldValidationError(self, instance, msg)
332 def rename(self, instance):
333 fullname = _joinNamePath(instance._name, self.name)
334 value = self.__getOrMake(instance)
335 value._rename(fullname)
337 def _collectImports(self, instance, imports):
338 value = self.__get__(instance)
339 target = value.target
340 imports.add(target.__module__)
341 value.value._collectImports()
342 imports |= value.value._imports
344 def save(self, outfile, instance):
345 fullname = _joinNamePath(instance._name, self.name)
346 value = self.__getOrMake(instance)
347 target = value.target
349 if target != self.target:
350 # not targeting the field-default target.
351 # save target information
352 ConfigClass = value.ConfigClass
353 outfile.write(
354 "{}.retarget(target={}, ConfigClass={})\n\n".format(
355 fullname, _typeStr(target), _typeStr(ConfigClass)
356 )
357 )
358 # save field values
359 value._save(outfile)
361 def freeze(self, instance):
362 value = self.__getOrMake(instance)
363 value.freeze()
365 def toDict(self, instance):
366 value = self.__get__(instance)
367 return value.toDict()
369 def validate(self, instance):
370 value = self.__get__(instance)
371 value.validate()
373 if self.check is not None and not self.check(value):
374 msg = "%s is not a valid value" % str(value)
375 raise FieldValidationError(self, instance, msg)
377 def __deepcopy__(self, memo):
378 """Customize deep-copying, because we always want a reference to the
379 original typemap.
381 WARNING: this must be overridden by subclasses if they change the
382 constructor signature!
383 """
384 return type(self)(
385 doc=self.doc,
386 target=self.target,
387 ConfigClass=self.ConfigClass,
388 default=copy.deepcopy(self.default),
389 )
391 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
392 """Compare two fields for equality.
394 Used by `lsst.pex.ConfigDictField.compare`.
396 Parameters
397 ----------
398 instance1 : `lsst.pex.config.Config`
399 Left-hand side config instance to compare.
400 instance2 : `lsst.pex.config.Config`
401 Right-hand side config instance to compare.
402 shortcut : `bool`
403 If `True`, this function returns as soon as an inequality if found.
404 rtol : `float`
405 Relative tolerance for floating point comparisons.
406 atol : `float`
407 Absolute tolerance for floating point comparisons.
408 output : callable
409 A callable that takes a string, used (possibly repeatedly) to
410 report inequalities. For example: `print`.
412 Returns
413 -------
414 isEqual : bool
415 `True` if the fields are equal, `False` otherwise.
417 Notes
418 -----
419 Floating point comparisons are performed by `numpy.allclose`.
420 """
421 c1 = getattr(instance1, self.name)._value
422 c2 = getattr(instance2, self.name)._value
423 name = getComparisonName(
424 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
425 )
426 return compareConfigs(name, c1, c2, shortcut=shortcut, rtol=rtol, atol=atol, output=output)