LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
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 f"'ConfigClass' is of incorrect type {_typeStr(ConfigClass)}. "
312 "'ConfigClass' must be a subclass of Config"
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 @overload
367 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default"
368 ) -> ConfigurableInstance[FieldTypeVar]: ...
369
370 def __get__(self, instance, owner=None, at=None, label="default"):
371 if instance is None or not isinstance(instance, Config):
372 return self
373 else:
374 return self.__getOrMake(instance, at=at, label=label)
375
376 def __set__(self, instance, value, at=None, label="assignment"):
377 if instance._frozen:
378 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
379 if at is None:
380 at = getCallStack()
381 oldValue = self.__getOrMake(instance, at=at)
382
383 if isinstance(value, ConfigurableInstance):
384 oldValue.retarget(value.target, value.ConfigClass, at, label)
385 oldValue.update(__at=at, __label=label, **value._storage)
386 elif type(value) is oldValue._ConfigClass:
387 oldValue.update(__at=at, __label=label, **value._storage)
388 elif value == oldValue.ConfigClass:
389 value = oldValue.ConfigClass()
390 oldValue.update(__at=at, __label=label, **value._storage)
391 else:
392 msg = (
393 f"Value {value} is of incorrect type {_typeStr(value)}. "
394 f"Expected {_typeStr(oldValue.ConfigClass)}"
395 )
396 raise FieldValidationError(self, instance, msg)
397
398 def rename(self, instance):
399 fullname = _joinNamePath(instance._name, self.namenamename)
400 value = self.__getOrMake(instance)
401 value._rename(fullname)
402
403 def _collectImports(self, instance, imports):
404 value = self.__get____get____get____get____get____get__(instance)
405 target = value.target
406 imports.add(target.__module__)
407 value.value._collectImports()
408 imports |= value.value._imports
409
410 def save(self, outfile, instance):
411 fullname = _joinNamePath(instance._name, self.namenamename)
412 value = self.__getOrMake(instance)
413 target = value.target
414
415 if target != self.targettarget:
416 # not targeting the field-default target.
417 # save target information
418 ConfigClass = value.ConfigClass
419 outfile.write(
420 f"{fullname}.retarget(target={_typeStr(target)}, ConfigClass={_typeStr(ConfigClass)})\n\n"
421 )
422 # save field values
423 value._save(outfile)
424
425 def freeze(self, instance):
426 value = self.__getOrMake(instance)
427 value.freeze()
428
429 def toDict(self, instance):
430 value = self.__get____get____get____get____get____get__(instance)
431 return value.toDict()
432
433 def validate(self, instance):
434 value = self.__get____get____get____get____get____get__(instance)
435 value.validate()
436
437 if self.check is not None and not self.check(value):
438 msg = f"{value} is not a valid value"
439 raise FieldValidationError(self, instance, msg)
440
441 def __deepcopy__(self, memo):
442 """Customize deep-copying, because we always want a reference to the
443 original typemap.
444
445 WARNING: this must be overridden by subclasses if they change the
446 constructor signature!
447 """
448 return type(self)(
449 doc=self.doc,
450 target=self.targettarget,
451 ConfigClass=self.ConfigClassConfigClass,
452 default=copy.deepcopy(self.default),
453 )
454
455 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
456 """Compare two fields for equality.
457
458 Used by `lsst.pex.ConfigDictField.compare`.
459
460 Parameters
461 ----------
462 instance1 : `lsst.pex.config.Config`
463 Left-hand side config instance to compare.
464 instance2 : `lsst.pex.config.Config`
465 Right-hand side config instance to compare.
466 shortcut : `bool`
467 If `True`, this function returns as soon as an inequality if found.
468 rtol : `float`
469 Relative tolerance for floating point comparisons.
470 atol : `float`
471 Absolute tolerance for floating point comparisons.
472 output : callable
473 A callable that takes a string, used (possibly repeatedly) to
474 report inequalities. For example: `print`.
475
476 Returns
477 -------
478 isEqual : bool
479 `True` if the fields are equal, `False` otherwise.
480
481 Notes
482 -----
483 Floating point comparisons are performed by `numpy.allclose`.
484 """
485 c1 = getattr(instance1, self.namenamename)._value
486 c2 = getattr(instance2, self.namenamename)._value
487 name = getComparisonName(
488 _joinNamePath(instance1._name, self.namenamename), _joinNamePath(instance2._name, self.namenamename)
489 )
490 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:707
FieldTypeVar __get__(self, Config instance, Any owner=None, Any at=None, str label="default")
Definition config.py:705
Field[FieldTypeVar] __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition config.py:700
_setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition config.py:480
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")