LSST Applications g00d0e8bbd7+8c5ae1fdc5,g013ef56533+603670b062,g083dd6704c+2e189452a7,g199a45376c+0ba108daf9,g1c5cce2383+bc9f6103a4,g1fd858c14a+cd69ed4fc1,g210f2d0738+c4742f2e9e,g262e1987ae+612fa42d85,g29ae962dfc+83d129e820,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+5eaa884f2a,g47891489e3+e32160a944,g53246c7159+8c5ae1fdc5,g5b326b94bb+dcc56af22d,g64539dfbff+c4742f2e9e,g67b6fd64d1+e32160a944,g74acd417e5+c122e1277d,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g88cb488625+47d24e4084,g89139ef638+e32160a944,g8d7436a09f+d14b4ff40a,g8ea07a8fe4+b212507b11,g90f42f885a+e1755607f3,g97be763408+34be90ab8c,g98df359435+ec1fa61bf1,ga2180abaac+8c5ae1fdc5,ga9e74d7ce9+43ac651df0,gbf99507273+8c5ae1fdc5,gc2a301910b+c4742f2e9e,gca7fc764a6+e32160a944,gd7ef33dd92+e32160a944,gdab6d2f7ff+c122e1277d,gdb1e2cdc75+1b18322db8,ge410e46f29+e32160a944,ge41e95a9f2+c4742f2e9e,geaed405ab2+0d91c11c6d,w.2025.44
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__ = ("ConfigurableField", "ConfigurableInstance")
31
32import copy
33import weakref
34from collections.abc import Mapping
35from typing import Any, Generic, overload
36
37from .callStack import StackFrame, 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`
61 Stack frame for history recording. Will be calculated if `None`.
62 label : `str`
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: list[StackFrame] | None, label: str, setHistory: bool = True):
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._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__", field.doc)
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 def _copy(self, parent: Config) -> ConfigurableInstance:
111 result = object.__new__(ConfigurableInstance)
112 object.__setattr__(result, "_config_", weakref.ref(parent))
113 object.__setattr__(result, "_field", self._field)
114 object.__setattr__(result, "__doc__", self.__doc__)
115 object.__setattr__(result, "_target", self._target)
116 object.__setattr__(result, "_ConfigClass", self._ConfigClass)
117 object.__setattr__(result, "_value", self._value.copy())
118 return result
119
120 @property
121 def _config(self) -> Config:
122 # Config Fields should never outlive their config class instance
123 # assert that as such here
124 assert self._config_() is not None
125 return self._config_()
126
127 target = property(lambda x: x._target)
128 """The targeted configurable (read-only).
129 """
130
131 ConfigClass = property(lambda x: x._ConfigClass)
132 """The configuration class (read-only)
133 """
134
135 value = property(lambda x: x._value)
136 """The `ConfigClass` instance (`lsst.pex.config.Config`-type,
137 read-only).
138 """
139
140 def apply(self, *args, **kw):
141 """Call the configurable.
142
143 Parameters
144 ----------
145 *args : `~typing.Any`
146 Arguments to use when calling the configurable.
147 **kw : `~typing.Any`
148 Keyword parameters to use when calling.
149
150 Notes
151 -----
152 In addition to the user-provided positional and keyword arguments,
153 the configurable is also provided a keyword argument ``config`` with
154 the value of `ConfigurableInstance.value`.
155 """
156 return self.target(*args, config=self.value, **kw)
157
158 def retarget(self, target, ConfigClass=None, at=None, label="retarget"):
159 """Target a new configurable and ConfigClass.
160
161 Parameters
162 ----------
163 target : `type`
164 Item to retarget.
165 ConfigClass : `type` or `None`, optional
166 New config class to use.
167 at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`,\
168 optional
169 Stack for history recording.
170 label : `str`, optional
171 Label for history recording.
172 """
173 if self._config._frozen:
174 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
175
176 try:
177 ConfigClass = self._field.validateTarget(target, ConfigClass)
178 except BaseException as e:
179 raise FieldValidationError(self._field, self._config, e.message) from e
180
181 if at is None:
182 at = getCallStack()
183 object.__setattr__(self, "_target", target)
184 if ConfigClass != self.ConfigClass:
185 object.__setattr__(self, "_ConfigClass", ConfigClass)
186 self.__initValue(at, label)
187
188 history = self._config._history.setdefault(self._field.name, [])
189 msg = f"retarget(target={_typeStr(target)}, ConfigClass={_typeStr(ConfigClass)})"
190 history.append((msg, at, label))
191
192 def __getattr__(self, name):
193 return getattr(self._value, name)
194
195 def __setattr__(self, name, value, at=None, label="assignment"):
196 """Pretend to be an instance of ConfigClass.
197
198 Attributes defined by ConfigurableInstance will shadow those defined
199 in ConfigClass
200 """
201 if self._config._frozen:
202 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
203
204 if name in self.__dict__:
205 # attribute exists in the ConfigurableInstance wrapper
206 object.__setattr__(self, name, value)
207 else:
208 if at is None:
209 at = getCallStack()
210 self._value.__setattr__(name, value, at=at, label=label)
211
212 def __delattr__(self, name, at=None, label="delete"):
213 """
214 Pretend to be an isntance of ConfigClass.
215 Attributes defiend by ConfigurableInstance will shadow those defined
216 in ConfigClass.
217 """
218 if self._config._frozen:
219 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
220
221 try:
222 # attribute exists in the ConfigurableInstance wrapper
223 object.__delattr__(self, name)
224 except AttributeError:
225 if at is None:
226 at = getCallStack()
227 self._value.__delattr__(name, at=at, label=label)
228
229 def __reduce__(self):
231 f"Proxy object for config field {self._field.name} cannot "
232 "be pickled; it should be converted to a normal `Config` instance "
233 "via the `value` property before being assigned to other objects "
234 "or variables."
235 )
236
237
238class ConfigurableField(Field[ConfigurableInstance[FieldTypeVar]]):
239 """A configuration field (`~lsst.pex.config.Field` subclass) that can be
240 can be retargeted towards a different configurable (often a
241 `lsst.pipe.base.Task` subclass).
242
243 The ``ConfigurableField`` is often used to configure subtasks, which are
244 tasks (`~lsst.pipe.base.Task`) called by a parent task.
245
246 Parameters
247 ----------
248 doc : `str`
249 A description of the configuration field.
250 target : configurable class
251 The configurable target. Configurables have a ``ConfigClass``
252 attribute. Within the task framework, configurables are
253 `lsst.pipe.base.Task` subclasses).
254 ConfigClass : `lsst.pex.config.Config`-type, optional
255 The subclass of `lsst.pex.config.Config` expected as the configuration
256 class of the ``target``. If ``ConfigClass`` is unset then
257 ``target.ConfigClass`` is used.
258 default : ``ConfigClass``-type, optional
259 The default configuration class. Normally this parameter is not set,
260 and defaults to ``ConfigClass`` (or ``target.ConfigClass``).
261 check : callable, optional
262 Callable that takes the field's value (the ``target``) as its only
263 positional argument, and returns `True` if the ``target`` is valid (and
264 `False` otherwise).
265 deprecated : None or `str`, optional
266 A description of why this Field is deprecated, including removal date.
267 If not None, the string is appended to the docstring for this Field.
268
269 See Also
270 --------
271 ChoiceField
272 ConfigChoiceField
273 ConfigDictField
274 ConfigField
275 DictField
276 Field
277 ListField
278 RangeField
279 RegistryField
280
281 Notes
282 -----
283 You can use the `ConfigurableInstance.apply` method to construct a
284 fully-configured configurable.
285 """
286
287 def validateTarget(self, target, ConfigClass):
288 """Validate the target and configuration class.
289
290 Parameters
291 ----------
292 target : configurable class
293 The configurable being verified.
294 ConfigClass : `lsst.pex.config.Config`-type or `None`
295 The configuration class associated with the ``target``. This can
296 be `None` if ``target`` has a ``ConfigClass`` attribute.
297
298 Raises
299 ------
300 AttributeError
301 Raised if ``ConfigClass`` is `None` and ``target`` does not have a
302 ``ConfigClass`` attribute.
303 TypeError
304 Raised if ``ConfigClass`` is not a `~lsst.pex.config.Config`
305 subclass.
306 ValueError
307 Raised if:
308
309 - ``target`` is not callable (callables have a ``__call__``
310 method).
311 - ``target`` is not startically defined (does not have
312 ``__module__`` or ``__name__`` attributes).
313 """
314 if ConfigClass is None:
315 try:
316 ConfigClass = target.ConfigClass
317 except Exception as e:
318 raise AttributeError("'target' must define attribute 'ConfigClass'") from e
319 if not issubclass(ConfigClass, Config):
320 raise TypeError(
321 f"'ConfigClass' is of incorrect type {_typeStr(ConfigClass)}. "
322 "'ConfigClass' must be a subclass of Config"
323 )
324 if not callable(target):
325 raise ValueError("'target' must be callable")
326 if not hasattr(target, "__module__") or not hasattr(target, "__name__"):
327 raise ValueError(
328 "'target' must be statically defined (must have '__module__' and '__name__' attributes)"
329 )
330 return ConfigClass
331
332 def __init__(self, doc, target, ConfigClass=None, default=None, check=None, deprecated=None):
333 ConfigClass = self.validateTarget(target, ConfigClass)
334
335 if default is None:
336 default = ConfigClass
337 if default != ConfigClass and type(default) is not ConfigClass:
338 raise TypeError(
339 f"'default' is of incorrect type {_typeStr(default)}. Expected {_typeStr(ConfigClass)}"
340 )
341
342 source = getStackFrame()
343 self._setup(
344 doc=doc,
345 dtype=ConfigurableInstance,
346 default=default,
347 check=check,
348 optional=False,
349 source=source,
350 deprecated=deprecated,
351 )
352 self.target = target
353 self.ConfigClass = ConfigClass
354
355 @staticmethod
357 params: tuple[type, ...] | tuple[str, ...], kwds: Mapping[str, Any]
358 ) -> Mapping[str, Any]:
359 return kwds
360
361 def __getOrMake(self, instance, at=None, label="default"):
362 value = instance._storage.get(self.name, None)
363 if value is None:
364 if at is None:
365 at = getCallStack(1)
366 value = ConfigurableInstance(instance, self, at=at, label=label)
367 instance._storage[self.name] = value
368 return value
369
370 @overload
372 self, instance: None, owner: Any = None, at: Any = None, label: str = "default"
373 ) -> ConfigurableField: ...
374
375 @overload
377 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default"
378 ) -> ConfigurableInstance[FieldTypeVar]: ...
379
380 def __get__(self, instance, owner=None, at=None, label="default"):
381 if instance is None or not isinstance(instance, Config):
382 return self
383 else:
384 return self.__getOrMake(instance, at=at, label=label)
385
386 def __set__(self, instance, value, at=None, label="assignment"):
387 if instance._frozen:
388 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
389 if at is None:
390 at = getCallStack()
391 oldValue = self.__getOrMake(instance, at=at)
392
393 if isinstance(value, ConfigurableInstance):
394 oldValue.retarget(value.target, value.ConfigClass, at, label)
395 oldValue.update(__at=at, __label=label, **value._storage)
396 elif type(value) is oldValue._ConfigClass:
397 oldValue.update(__at=at, __label=label, **value._storage)
398 elif value == oldValue.ConfigClass:
399 value = oldValue.ConfigClass()
400 oldValue.update(__at=at, __label=label, **value._storage)
401 else:
402 msg = (
403 f"Value {value} is of incorrect type {_typeStr(value)}. "
404 f"Expected {_typeStr(oldValue.ConfigClass)}"
405 )
406 raise FieldValidationError(self, instance, msg)
407
408 def rename(self, instance):
409 fullname = _joinNamePath(instance._name, self.name)
410 value = self.__getOrMake(instance)
411 value._rename(fullname)
412
413 def _collectImports(self, instance, imports):
414 value = self.__get__(instance)
415 target = value.target
416 imports.add(target.__module__)
417 value.value._collectImports()
418 imports |= value.value._imports
419
420 def save(self, outfile, instance):
421 fullname = _joinNamePath(instance._name, self.name)
422 value = self.__getOrMake(instance)
423 target = value.target
424
425 if target != self.target:
426 # not targeting the field-default target.
427 # save target information
428 ConfigClass = value.ConfigClass
429 outfile.write(
430 f"{fullname}.retarget(target={_typeStr(target)}, ConfigClass={_typeStr(ConfigClass)})\n\n"
431 )
432 # save field values
433 value._save(outfile)
434
435 def freeze(self, instance):
436 value = self.__getOrMake(instance)
437 value.freeze()
438
439 def toDict(self, instance):
440 value = self.__get__(instance)
441 return value.toDict()
442
443 def _copy_storage(self, old: Config, new: Config) -> ConfigurableInstance | None:
444 instance: ConfigurableInstance | None = old._storage.get(self.name)
445 if instance is not None:
446 return instance._copy(new)
447 else:
448 return None
449
450 def validate(self, instance):
451 value = self.__get__(instance)
452 value.validate()
453
454 if self.check is not None and not self.check(value):
455 msg = f"{value} is not a valid value"
456 raise FieldValidationError(self, instance, msg)
457
458 def __deepcopy__(self, memo):
459 """Customize deep-copying, because we always want a reference to the
460 original typemap.
461
462 WARNING: this must be overridden by subclasses if they change the
463 constructor signature!
464 """
465 return type(self)(
466 doc=self.doc,
467 target=self.target,
468 ConfigClass=self.ConfigClass,
469 default=copy.deepcopy(self.default),
470 )
471
472 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
473 """Compare two fields for equality.
474
475 Used by `lsst.pex.ConfigDictField.compare`.
476
477 Parameters
478 ----------
479 instance1 : `lsst.pex.config.Config`
480 Left-hand side config instance to compare.
481 instance2 : `lsst.pex.config.Config`
482 Right-hand side config instance to compare.
483 shortcut : `bool`
484 If `True`, this function returns as soon as an inequality if found.
485 rtol : `float`
486 Relative tolerance for floating point comparisons.
487 atol : `float`
488 Absolute tolerance for floating point comparisons.
489 output : callable
490 A callable that takes a string, used (possibly repeatedly) to
491 report inequalities. For example: `print`.
492
493 Returns
494 -------
495 isEqual : bool
496 `True` if the fields are equal, `False` otherwise.
497
498 Notes
499 -----
500 Floating point comparisons are performed by `numpy.allclose`.
501 """
502 c1 = getattr(instance1, self.name)._value
503 c2 = getattr(instance2, self.name)._value
504 name = getComparisonName(
505 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
506 )
507 return compareConfigs(name, c1, c2, shortcut=shortcut, rtol=rtol, atol=atol, output=output)
Field[FieldTypeVar] __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition config.py:714
_setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition config.py:486
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")
__init__(self, doc, target, ConfigClass=None, default=None, check=None, deprecated=None)
ConfigurableInstance|None _copy_storage(self, Config old, Config new)
__initValue(self, list[StackFrame]|None at, str label, bool setHistory=True)
ConfigurableInstance _copy(self, Config parent)
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")
compareConfigs(name, c1, c2, shortcut=True, rtol=1e-8, atol=1e-8, output=None)
getComparisonName(name1, name2)
Definition comparison.py:40
_joinNamePath(prefix=None, name=None, index=None)
Definition config.py:107