LSST Applications g180d380827+6621f76652,g2079a07aa2+86d27d4dc4,g2305ad1205+f5a9e323a1,g2bbee38e9b+c6a8a0fb72,g337abbeb29+c6a8a0fb72,g33d1c0ed96+c6a8a0fb72,g3a166c0a6a+c6a8a0fb72,g3ddfee87b4+9a10e1fe7b,g48712c4677+c9a099281a,g487adcacf7+f2e03ea30b,g50ff169b8f+96c6868917,g52b1c1532d+585e252eca,g591dd9f2cf+aead732c78,g64a986408d+eddffb812c,g858d7b2824+eddffb812c,g864b0138d7+aa38e45daa,g974c55ee3d+f37bf00e57,g99cad8db69+119519a52d,g9c22b2923f+e2510deafe,g9ddcbc5298+9a081db1e4,ga1e77700b3+03d07e1c1f,gb0e22166c9+60f28cb32d,gb23b769143+eddffb812c,gba4ed39666+c2a2e4ac27,gbb8dafda3b+27317ec8e9,gbd998247f1+585e252eca,gc120e1dc64+5817c176a8,gc28159a63d+c6a8a0fb72,gc3e9b769f7+6707aea8b4,gcf0d15dbbd+9a10e1fe7b,gdaeeff99f8+f9a426f77a,ge6526c86ff+6a2e01d432,ge79ae78c31+c6a8a0fb72,gee10cc3b42+585e252eca,gff1a9f87cc+eddffb812c,v27.0.0.rc1
LSST Data Management Base Package
Loading...
Searching...
No Matches
configurableField.py
Go to the documentation of this file.
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/>.
27
28from __future__ import annotations
29
30__all__ = ("ConfigurableInstance", "ConfigurableField")
31
32import copy
33import weakref
34from collections.abc import Mapping
35from typing import Any, Generic, overload
36
37from .callStack import getCallStack, getStackFrame
38from .comparison import compareConfigs, getComparisonName
39from .config import (
40 Config,
41 Field,
42 FieldTypeVar,
43 FieldValidationError,
44 UnexpectedProxyUsageError,
45 _joinNamePath,
46 _typeStr,
47)
48
49
50class ConfigurableInstance(Generic[FieldTypeVar]):
51 """A retargetable configuration in a `ConfigurableField` that proxies
52 a `~lsst.pex.config.Config`.
53
54 Parameters
55 ----------
56 config : `~lsst.pex.config.Config`
57 Config to proxy.
58 field : `~lsst.pex.config.ConfigurableField`
59 Field to use.
60 at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`, optional
61 Stack frame for history recording. Will be calculated if `None`.
62 label : `str`, optional
63 Label to use for history recording.
64
65 Notes
66 -----
67 ``ConfigurableInstance`` implements ``__getattr__`` and ``__setattr__``
68 methods that forward to the `~lsst.pex.config.Config` it holds.
69 ``ConfigurableInstance`` adds a `retarget` method.
70
71 The actual `~lsst.pex.config.Config` instance is accessed using the
72 ``value`` property (e.g. to get its documentation). The associated
73 configurable object (usually a `~lsst.pipe.base.Task`) is accessed
74 using the ``target`` property.
75 """
76
77 def __initValue(self, at, label):
78 """Construct value of field.
79
80 Notes
81 -----
82 If field.default is an instance of `lsst.pex.config.Config`,
83 custom construct ``_value`` with the correct values from default.
84 Otherwise, call ``ConfigClass`` constructor
85 """
86 name = _joinNamePath(self._config_config._name, self._field.name)
87 if type(self._field.default) is self.ConfigClass:
88 storage = self._field.default._storage
89 else:
90 storage = {}
91 value = self._ConfigClass(__name=name, __at=at, __label=label, **storage)
92 object.__setattr__(self, "_value", value)
93
94 def __init__(self, config, field, at=None, label="default"):
95 object.__setattr__(self, "_config_", weakref.ref(config))
96 object.__setattr__(self, "_field", field)
97 object.__setattr__(self, "__doc__", config)
98 object.__setattr__(self, "_target", field.target)
99 object.__setattr__(self, "_ConfigClass", field.ConfigClass)
100 object.__setattr__(self, "_value", None)
101
102 if at is None:
103 at = getCallStack()
104 at += [self._field.source]
105 self.__initValue(at, label)
106
107 history = config._history.setdefault(field.name, [])
108 history.append(("Targeted and initialized from defaults", at, label))
109
110 @property
111 def _config(self) -> Config:
112 # Config Fields should never outlive their config class instance
113 # assert that as such here
114 assert self._config_() is not None
115 return self._config_()
116
117 target = property(lambda x: x._target)
118 """The targeted configurable (read-only).
119 """
120
121 ConfigClass = property(lambda x: x._ConfigClass)
122 """The configuration class (read-only)
123 """
124
125 value = property(lambda x: x._value)
126 """The `ConfigClass` instance (`lsst.pex.config.Config`-type,
127 read-only).
128 """
129
130 def apply(self, *args, **kw):
131 """Call the configurable.
132
133 Parameters
134 ----------
135 *args : `~typing.Any`
136 Arguments to use when calling the configurable.
137 **kw : `~typing.Any`
138 Keyword parameters to use when calling.
139
140 Notes
141 -----
142 In addition to the user-provided positional and keyword arguments,
143 the configurable is also provided a keyword argument ``config`` with
144 the value of `ConfigurableInstance.value`.
145 """
146 return self.target(*args, config=self.value, **kw)
147
148 def retarget(self, target, ConfigClass=None, at=None, label="retarget"):
149 """Target a new configurable and ConfigClass.
150
151 Parameters
152 ----------
153 target : `type`
154 Item to retarget.
155 ConfigClass : `type` or `None`, optional
156 New config class to use.
157 at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`,\
158 optional
159 Stack for history recording.
160 label : `str`, optional
161 Label for history recording.
162 """
163 if self._config_config._frozen:
164 raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
165
166 try:
167 ConfigClass = self._field.validateTarget(target, ConfigClass)
168 except BaseException as e:
169 raise FieldValidationError(self._field, self._config_config, e.message)
170
171 if at is None:
172 at = getCallStack()
173 object.__setattr__(self, "_target", target)
174 if ConfigClass != self.ConfigClass:
175 object.__setattr__(self, "_ConfigClass", ConfigClass)
176 self.__initValue(at, label)
177
178 history = self._config_config._history.setdefault(self._field.name, [])
179 msg = f"retarget(target={_typeStr(target)}, ConfigClass={_typeStr(ConfigClass)})"
180 history.append((msg, at, label))
181
182 def __getattr__(self, name):
183 return getattr(self._value, name)
184
185 def __setattr__(self, name, value, at=None, label="assignment"):
186 """Pretend to be an instance of ConfigClass.
187
188 Attributes defined by ConfigurableInstance will shadow those defined
189 in ConfigClass
190 """
191 if self._config_config._frozen:
192 raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
193
194 if name in self.__dict__:
195 # attribute exists in the ConfigurableInstance wrapper
196 object.__setattr__(self, name, value)
197 else:
198 if at is None:
199 at = getCallStack()
200 self._value.__setattr__(name, value, at=at, label=label)
201
202 def __delattr__(self, name, at=None, label="delete"):
203 """
204 Pretend to be an isntance of ConfigClass.
205 Attributes defiend by ConfigurableInstance will shadow those defined
206 in ConfigClass.
207 """
208 if self._config_config._frozen:
209 raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
210
211 try:
212 # attribute exists in the ConfigurableInstance wrapper
213 object.__delattr__(self, name)
214 except AttributeError:
215 if at is None:
216 at = getCallStack()
217 self._value.__delattr__(name, at=at, label=label)
218
219 def __reduce__(self):
221 f"Proxy object for config field {self._field.name} cannot "
222 "be pickled; it should be converted to a normal `Config` instance "
223 "via the `value` property before being assigned to other objects "
224 "or variables."
225 )
226
227
228class ConfigurableField(Field[ConfigurableInstance[FieldTypeVar]]):
229 """A configuration field (`~lsst.pex.config.Field` subclass) that can be
230 can be retargeted towards a different configurable (often a
231 `lsst.pipe.base.Task` subclass).
232
233 The ``ConfigurableField`` is often used to configure subtasks, which are
234 tasks (`~lsst.pipe.base.Task`) called by a parent task.
235
236 Parameters
237 ----------
238 doc : `str`
239 A description of the configuration field.
240 target : configurable class
241 The configurable target. Configurables have a ``ConfigClass``
242 attribute. Within the task framework, configurables are
243 `lsst.pipe.base.Task` subclasses).
244 ConfigClass : `lsst.pex.config.Config`-type, optional
245 The subclass of `lsst.pex.config.Config` expected as the configuration
246 class of the ``target``. If ``ConfigClass`` is unset then
247 ``target.ConfigClass`` is used.
248 default : ``ConfigClass``-type, optional
249 The default configuration class. Normally this parameter is not set,
250 and defaults to ``ConfigClass`` (or ``target.ConfigClass``).
251 check : callable, optional
252 Callable that takes the field's value (the ``target``) as its only
253 positional argument, and returns `True` if the ``target`` is valid (and
254 `False` otherwise).
255 deprecated : None or `str`, optional
256 A description of why this Field is deprecated, including removal date.
257 If not None, the string is appended to the docstring for this Field.
258
259 See Also
260 --------
261 ChoiceField
262 ConfigChoiceField
263 ConfigDictField
264 ConfigField
265 DictField
266 Field
267 ListField
268 RangeField
269 RegistryField
270
271 Notes
272 -----
273 You can use the `ConfigurableInstance.apply` method to construct a
274 fully-configured configurable.
275 """
276
277 def validateTarget(self, target, ConfigClass):
278 """Validate the target and configuration class.
279
280 Parameters
281 ----------
282 target : configurable class
283 The configurable being verified.
284 ConfigClass : `lsst.pex.config.Config`-type or `None`
285 The configuration class associated with the ``target``. This can
286 be `None` if ``target`` has a ``ConfigClass`` attribute.
287
288 Raises
289 ------
290 AttributeError
291 Raised if ``ConfigClass`` is `None` and ``target`` does not have a
292 ``ConfigClass`` attribute.
293 TypeError
294 Raised if ``ConfigClass`` is not a `~lsst.pex.config.Config`
295 subclass.
296 ValueError
297 Raised if:
298
299 - ``target`` is not callable (callables have a ``__call__``
300 method).
301 - ``target`` is not startically defined (does not have
302 ``__module__`` or ``__name__`` attributes).
303 """
304 if ConfigClass is None:
305 try:
306 ConfigClass = target.ConfigClass
307 except Exception:
308 raise AttributeError("'target' must define attribute 'ConfigClass'")
309 if not issubclass(ConfigClass, Config):
310 raise TypeError(
311 "'ConfigClass' is of incorrect type %s.'ConfigClass' must be a subclass of Config"
312 % _typeStr(ConfigClass)
313 )
314 if not hasattr(target, "__call__"):
315 raise ValueError("'target' must be callable")
316 if not hasattr(target, "__module__") or not hasattr(target, "__name__"):
317 raise ValueError(
318 "'target' must be statically defined (must have '__module__' and '__name__' attributes)"
319 )
320 return ConfigClass
321
322 def __init__(self, doc, target, ConfigClass=None, default=None, check=None, deprecated=None):
323 ConfigClass = self.validateTarget(target, ConfigClass)
324
325 if default is None:
326 default = ConfigClass
327 if default != ConfigClass and type(default) is not ConfigClass:
328 raise TypeError(
329 f"'default' is of incorrect type {_typeStr(default)}. Expected {_typeStr(ConfigClass)}"
330 )
331
332 source = getStackFrame()
333 self._setup(
334 doc=doc,
335 dtype=ConfigurableInstance,
336 default=default,
337 check=check,
338 optional=False,
339 source=source,
340 deprecated=deprecated,
341 )
342 self.targettarget = target
343 self.ConfigClassConfigClass = ConfigClass
344
345 @staticmethod
347 params: tuple[type, ...] | tuple[str, ...], kwds: Mapping[str, Any]
348 ) -> Mapping[str, Any]:
349 return kwds
350
351 def __getOrMake(self, instance, at=None, label="default"):
352 value = instance._storage.get(self.namenamename, None)
353 if value is None:
354 if at is None:
355 at = getCallStack(1)
356 value = ConfigurableInstance(instance, self, at=at, label=label)
357 instance._storage[self.namenamename] = value
358 return value
359
360 @overload
362 self, instance: None, owner: Any = None, at: Any = None, label: str = "default"
363 ) -> ConfigurableField:
364 ...
365
366 @overload
368 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default"
369 ) -> ConfigurableInstance[FieldTypeVar]:
370 ...
371
372 def __get__(self, instance, owner=None, at=None, label="default"):
373 if instance is None or not isinstance(instance, Config):
374 return self
375 else:
376 return self.__getOrMake(instance, at=at, label=label)
377
378 def __set__(self, instance, value, at=None, label="assignment"):
379 if instance._frozen:
380 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
381 if at is None:
382 at = getCallStack()
383 oldValue = self.__getOrMake(instance, at=at)
384
385 if isinstance(value, ConfigurableInstance):
386 oldValue.retarget(value.target, value.ConfigClass, at, label)
387 oldValue.update(__at=at, __label=label, **value._storage)
388 elif type(value) is oldValue._ConfigClass:
389 oldValue.update(__at=at, __label=label, **value._storage)
390 elif value == oldValue.ConfigClass:
391 value = oldValue.ConfigClass()
392 oldValue.update(__at=at, __label=label, **value._storage)
393 else:
394 msg = "Value {} is of incorrect type {}. Expected {}".format(
395 value,
396 _typeStr(value),
397 _typeStr(oldValue.ConfigClass),
398 )
399 raise FieldValidationError(self, instance, msg)
400
401 def rename(self, instance):
402 fullname = _joinNamePath(instance._name, self.namenamename)
403 value = self.__getOrMake(instance)
404 value._rename(fullname)
405
406 def _collectImports(self, instance, imports):
407 value = self.__get____get____get____get____get____get__(instance)
408 target = value.target
409 imports.add(target.__module__)
410 value.value._collectImports()
411 imports |= value.value._imports
412
413 def save(self, outfile, instance):
414 fullname = _joinNamePath(instance._name, self.namenamename)
415 value = self.__getOrMake(instance)
416 target = value.target
417
418 if target != self.targettarget:
419 # not targeting the field-default target.
420 # save target information
421 ConfigClass = value.ConfigClass
422 outfile.write(
423 "{}.retarget(target={}, ConfigClass={})\n\n".format(
424 fullname, _typeStr(target), _typeStr(ConfigClass)
425 )
426 )
427 # save field values
428 value._save(outfile)
429
430 def freeze(self, instance):
431 value = self.__getOrMake(instance)
432 value.freeze()
433
434 def toDict(self, instance):
435 value = self.__get____get____get____get____get____get__(instance)
436 return value.toDict()
437
438 def validate(self, instance):
439 value = self.__get____get____get____get____get____get__(instance)
440 value.validate()
441
442 if self.check is not None and not self.check(value):
443 msg = "%s is not a valid value" % str(value)
444 raise FieldValidationError(self, instance, msg)
445
446 def __deepcopy__(self, memo):
447 """Customize deep-copying, because we always want a reference to the
448 original typemap.
449
450 WARNING: this must be overridden by subclasses if they change the
451 constructor signature!
452 """
453 return type(self)(
454 doc=self.doc,
455 target=self.targettarget,
456 ConfigClass=self.ConfigClassConfigClass,
457 default=copy.deepcopy(self.default),
458 )
459
460 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
461 """Compare two fields for equality.
462
463 Used by `lsst.pex.ConfigDictField.compare`.
464
465 Parameters
466 ----------
467 instance1 : `lsst.pex.config.Config`
468 Left-hand side config instance to compare.
469 instance2 : `lsst.pex.config.Config`
470 Right-hand side config instance to compare.
471 shortcut : `bool`
472 If `True`, this function returns as soon as an inequality if found.
473 rtol : `float`
474 Relative tolerance for floating point comparisons.
475 atol : `float`
476 Absolute tolerance for floating point comparisons.
477 output : callable
478 A callable that takes a string, used (possibly repeatedly) to
479 report inequalities. For example: `print`.
480
481 Returns
482 -------
483 isEqual : bool
484 `True` if the fields are equal, `False` otherwise.
485
486 Notes
487 -----
488 Floating point comparisons are performed by `numpy.allclose`.
489 """
490 c1 = getattr(instance1, self.namenamename)._value
491 c2 = getattr(instance2, self.namenamename)._value
492 name = getComparisonName(
493 _joinNamePath(instance1._name, self.namenamename), _joinNamePath(instance2._name, self.namenamename)
494 )
495 return compareConfigs(name, c1, c2, shortcut=shortcut, rtol=rtol, atol=atol, output=output)
__get__(self, instance, owner=None, at=None, label="default")
Definition config.py:728
FieldTypeVar __get__(self, Config instance, Any owner=None, Any at=None, str label="default")
Definition config.py:725
Field[FieldTypeVar] __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition config.py:719
_setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition config.py:497
ConfigurableField __get__(self, None instance, Any owner=None, Any at=None, str label="default")
_compare(self, instance1, instance2, shortcut, rtol, atol, output)
Mapping[str, Any] _parseTypingArgs(tuple[type,...]|tuple[str,...] params, Mapping[str, Any] kwds)
__getOrMake(self, instance, at=None, label="default")
__set__(self, instance, value, at=None, label="assignment")
ConfigurableInstance[FieldTypeVar] __get__(self, Config instance, Any owner=None, Any at=None, str label="default")
__init__(self, doc, target, ConfigClass=None, default=None, check=None, deprecated=None)
__get__(self, instance, owner=None, at=None, label="default")
retarget(self, target, ConfigClass=None, at=None, label="retarget")
__setattr__(self, name, value, at=None, label="assignment")
__init__(self, config, field, at=None, label="default")