Coverage for python/lsst/pex/config/configurableField.py : 27%

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