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