LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
LSST Data Management Base Package
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 typing import Any, Generic, Mapping, Union, overload
35
36from .callStack import getCallStack, getStackFrame
37from .comparison import compareConfigs, getComparisonName
38from .config import (
39 Config,
40 Field,
41 FieldTypeVar,
42 FieldValidationError,
43 UnexpectedProxyUsageError,
44 _joinNamePath,
45 _typeStr,
46)
47
48
49class ConfigurableInstance(Generic[FieldTypeVar]):
50 """A retargetable configuration in a `ConfigurableField` that proxies
52
53 Notes
54 -----
55 ``ConfigurableInstance`` implements ``__getattr__`` and ``__setattr__``
56 methods that forward to the `~lsst.pex.config.Config` it holds.
57 ``ConfigurableInstance`` adds a `retarget` method.
58
59 The actual `~lsst.pex.config.Config` instance is accessed using the
60 ``value`` property (e.g. to get its documentation). The associated
61 configurable object (usually a `~lsst.pipe.base.Task`) is accessed
62 using the ``target`` property.
63 """
64
65 def __initValue(self, at, label):
66 """Construct value of field.
67
68 Notes
69 -----
70 If field.default is an instance of `lsst.pex.config.ConfigClass`,
71 custom construct ``_value`` with the correct values from default.
72 Otherwise, call ``ConfigClass`` constructor
73 """
74 name = _joinNamePath(self._config_config._name, self._field.name)
75 if type(self._field.default) == self.ConfigClassConfigClass:
76 storage = self._field.default._storage
77 else:
78 storage = {}
79 value = self._ConfigClass(__name=name, __at=at, __label=label, **storage)
80 object.__setattr__(self, "_value", value)
81
82 def __init__(self, config, field, at=None, label="default"):
83 object.__setattr__(self, "_config_", weakref.ref(config))
84 object.__setattr__(self, "_field", field)
85 object.__setattr__(self, "__doc__", config)
86 object.__setattr__(self, "_target", field.target)
87 object.__setattr__(self, "_ConfigClass", field.ConfigClass)
88 object.__setattr__(self, "_value", None)
89
90 if at is None:
91 at = getCallStack()
92 at += [self._field.source]
93 self.__initValue__initValue(at, label)
94
95 history = config._history.setdefault(field.name, [])
96 history.append(("Targeted and initialized from defaults", at, label))
97
98 @property
99 def _config(self) -> Config:
100 # Config Fields should never outlive their config class instance
101 # assert that as such here
102 assert self._config_() is not None
103 return self._config_()
104
105 target = property(lambda x: x._target)
106 """The targeted configurable (read-only).
107 """
108
109 ConfigClass = property(lambda x: x._ConfigClass)
110 """The configuration class (read-only)
111 """
112
113 value = property(lambda x: x._value)
114 """The `ConfigClass` instance (`lsst.pex.config.ConfigClass`-type,
115 read-only).
116 """
117
118 def apply(self, *args, **kw):
119 """Call the configurable.
120
121 Notes
122 -----
123 In addition to the user-provided positional and keyword arguments,
124 the configurable is also provided a keyword argument ``config`` with
125 the value of `ConfigurableInstance.value`.
126 """
127 return self.targettarget(*args, config=self.valuevalue, **kw)
128
129 def retarget(self, target, ConfigClass=None, at=None, label="retarget"):
130 """Target a new configurable and ConfigClass"""
131 if self._config_config._frozen:
132 raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
133
134 try:
135 ConfigClass = self._field.validateTarget(target, ConfigClass)
136 except BaseException as e:
137 raise FieldValidationError(self._field, self._config_config, e.message)
138
139 if at is None:
140 at = getCallStack()
141 object.__setattr__(self, "_target", target)
142 if ConfigClass != self.ConfigClassConfigClass:
143 object.__setattr__(self, "_ConfigClass", ConfigClass)
144 self.__initValue__initValue(at, label)
145
146 history = self._config_config._history.setdefault(self._field.name, [])
147 msg = "retarget(target=%s, ConfigClass=%s)" % (_typeStr(target), _typeStr(ConfigClass))
148 history.append((msg, at, label))
149
150 def __getattr__(self, name):
151 return getattr(self._value, name)
152
153 def __setattr__(self, name, value, at=None, label="assignment"):
154 """Pretend to be an instance of ConfigClass.
155
156 Attributes defined by ConfigurableInstance will shadow those defined
157 in ConfigClass
158 """
159 if self._config_config._frozen:
160 raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
161
162 if name in self.__dict__:
163 # attribute exists in the ConfigurableInstance wrapper
164 object.__setattr__(self, name, value)
165 else:
166 if at is None:
167 at = getCallStack()
168 self._value.__setattr__(name, value, at=at, label=label)
169
170 def __delattr__(self, name, at=None, label="delete"):
171 """
172 Pretend to be an isntance of ConfigClass.
173 Attributes defiend by ConfigurableInstance will shadow those defined
174 in ConfigClass
175 """
176 if self._config_config._frozen:
177 raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
178
179 try:
180 # attribute exists in the ConfigurableInstance wrapper
181 object.__delattr__(self, name)
182 except AttributeError:
183 if at is None:
184 at = getCallStack()
185 self._value.__delattr__(name, at=at, label=label)
186
187 def __reduce__(self):
189 f"Proxy object for config field {self._field.name} cannot "
190 "be pickled; it should be converted to a normal `Config` instance "
191 f"via the `value` property before being assigned to other objects "
192 "or variables."
193 )
194
195
196class ConfigurableField(Field[ConfigurableInstance[FieldTypeVar]]):
197 """A configuration field (`~lsst.pex.config.Field` subclass) that can be
198 can be retargeted towards a different configurable (often a
199 `lsst.pipe.base.Task` subclass).
200
201 The ``ConfigurableField`` is often used to configure subtasks, which are
202 tasks (`~lsst.pipe.base.Task`) called by a parent task.
203
204 Parameters
205 ----------
206 doc : `str`
207 A description of the configuration field.
208 target : configurable class
209 The configurable target. Configurables have a ``ConfigClass``
210 attribute. Within the task framework, configurables are
211 `lsst.pipe.base.Task` subclasses)
212 ConfigClass : `lsst.pex.config.Config`-type, optional
213 The subclass of `lsst.pex.config.Config` expected as the configuration
214 class of the ``target``. If ``ConfigClass`` is unset then
215 ``target.ConfigClass`` is used.
216 default : ``ConfigClass``-type, optional
217 The default configuration class. Normally this parameter is not set,
218 and defaults to ``ConfigClass`` (or ``target.ConfigClass``).
219 check : callable, optional
220 Callable that takes the field's value (the ``target``) as its only
221 positional argument, and returns `True` if the ``target`` is valid (and
222 `False` otherwise).
223 deprecated : None or `str`, optional
224 A description of why this Field is deprecated, including removal date.
225 If not None, the string is appended to the docstring for this Field.
226
227 See also
228 --------
229 ChoiceField
230 ConfigChoiceField
231 ConfigDictField
232 ConfigField
233 DictField
234 Field
235 ListField
236 RangeField
237 RegistryField
238
239 Notes
240 -----
241 You can use the `ConfigurableInstance.apply` method to construct a
242 fully-configured configurable.
243 """
244
245 def validateTarget(self, target, ConfigClass):
246 """Validate the target and configuration class.
247
248 Parameters
249 ----------
250 target
251 The configurable being verified.
252 ConfigClass : `lsst.pex.config.Config`-type or `None`
253 The configuration class associated with the ``target``. This can
254 be `None` if ``target`` has a ``ConfigClass`` attribute.
255
256 Raises
257 ------
258 AttributeError
259 Raised if ``ConfigClass`` is `None` and ``target`` does not have a
260 ``ConfigClass`` attribute.
261 TypeError
262 Raised if ``ConfigClass`` is not a `~lsst.pex.config.Config`
263 subclass.
264 ValueError
265 Raised if:
266
267 - ``target`` is not callable (callables have a ``__call__``
268 method).
269 - ``target`` is not startically defined (does not have
270 ``__module__`` or ``__name__`` attributes).
271 """
272 if ConfigClass is None:
273 try:
274 ConfigClass = target.ConfigClass
275 except Exception:
276 raise AttributeError("'target' must define attribute 'ConfigClass'")
277 if not issubclass(ConfigClass, Config):
278 raise TypeError(
279 "'ConfigClass' is of incorrect type %s."
280 "'ConfigClass' must be a subclass of Config" % _typeStr(ConfigClass)
281 )
282 if not hasattr(target, "__call__"):
283 raise ValueError("'target' must be callable")
284 if not hasattr(target, "__module__") or not hasattr(target, "__name__"):
285 raise ValueError(
286 "'target' must be statically defined (must have '__module__' and '__name__' attributes)"
287 )
288 return ConfigClass
289
290 def __init__(self, doc, target, ConfigClass=None, default=None, check=None, deprecated=None):
291 ConfigClass = self.validateTargetvalidateTarget(target, ConfigClass)
292
293 if default is None:
294 default = ConfigClass
295 if default != ConfigClass and type(default) != ConfigClass:
296 raise TypeError(
297 "'default' is of incorrect type %s. Expected %s" % (_typeStr(default), _typeStr(ConfigClass))
298 )
299
300 source = getStackFrame()
301 self._setup_setup(
302 doc=doc,
303 dtype=ConfigurableInstance,
304 default=default,
305 check=check,
306 optional=False,
307 source=source,
308 deprecated=deprecated,
309 )
310 self.targettargettarget = target
311 self.ConfigClassConfigClassConfigClass = ConfigClass
312
313 @staticmethod
314 def _parseTypingArgs(
315 params: Union[tuple[type, ...], tuple[str, ...]], kwds: Mapping[str, Any]
316 ) -> Mapping[str, Any]:
317 return kwds
318
319 def __getOrMake(self, instance, at=None, label="default"):
320 value = instance._storage.get(self.name, None)
321 if value is None:
322 if at is None:
323 at = getCallStack(1)
324 value = ConfigurableInstance(instance, self, at=at, label=label)
325 instance._storage[self.name] = value
326 return value
327
328 @overload
330 self, instance: None, owner: Any = None, at: Any = None, label: str = "default"
331 ) -> "ConfigurableField":
332 ...
333
334 @overload
336 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default"
337 ) -> ConfigurableInstance[FieldTypeVar]:
338 ...
339
340 def __get__(self, instance, owner=None, at=None, label="default"):
341 if instance is None or not isinstance(instance, Config):
342 return self
343 else:
344 return self.__getOrMake__getOrMake(instance, at=at, label=label)
345
346 def __set__(self, instance, value, at=None, label="assignment"):
347 if instance._frozen:
348 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
349 if at is None:
350 at = getCallStack()
351 oldValue = self.__getOrMake__getOrMake(instance, at=at)
352
353 if isinstance(value, ConfigurableInstance):
354 oldValue.retarget(value.target, value.ConfigClass, at, label)
355 oldValue.update(__at=at, __label=label, **value._storage)
356 elif type(value) == oldValue._ConfigClass:
357 oldValue.update(__at=at, __label=label, **value._storage)
358 elif value == oldValue.ConfigClass:
359 value = oldValue.ConfigClass()
360 oldValue.update(__at=at, __label=label, **value._storage)
361 else:
362 msg = "Value %s is of incorrect type %s. Expected %s" % (
363 value,
364 _typeStr(value),
365 _typeStr(oldValue.ConfigClass),
366 )
367 raise FieldValidationError(self, instance, msg)
368
369 def rename(self, instance):
370 fullname = _joinNamePath(instance._name, self.name)
371 value = self.__getOrMake__getOrMake(instance)
372 value._rename(fullname)
373
374 def _collectImports(self, instance, imports):
375 value = self.__get____get____get____get____get____get____get__(instance)
376 target = value.target
377 imports.add(target.__module__)
378 value.value._collectImports()
379 imports |= value.value._imports
380
381 def save(self, outfile, instance):
382 fullname = _joinNamePath(instance._name, self.name)
383 value = self.__getOrMake__getOrMake(instance)
384 target = value.target
385
386 if target != self.targettargettarget:
387 # not targeting the field-default target.
388 # save target information
389 ConfigClass = value.ConfigClass
390 outfile.write(
391 "{}.retarget(target={}, ConfigClass={})\n\n".format(
392 fullname, _typeStr(target), _typeStr(ConfigClass)
393 )
394 )
395 # save field values
396 value._save(outfile)
397
398 def freeze(self, instance):
399 value = self.__getOrMake__getOrMake(instance)
400 value.freeze()
401
402 def toDict(self, instance):
403 value = self.__get____get____get____get____get____get____get__(instance)
404 return value.toDict()
405
406 def validate(self, instance):
407 value = self.__get____get____get____get____get____get____get__(instance)
408 value.validate()
409
410 if self.checkcheck is not None and not self.checkcheck(value):
411 msg = "%s is not a valid value" % str(value)
412 raise FieldValidationError(self, instance, msg)
413
414 def __deepcopy__(self, memo):
415 """Customize deep-copying, because we always want a reference to the
416 original typemap.
417
418 WARNING: this must be overridden by subclasses if they change the
419 constructor signature!
420 """
421 return type(self)(
422 doc=self.docdoc,
423 target=self.targettargettarget,
424 ConfigClass=self.ConfigClassConfigClassConfigClass,
425 default=copy.deepcopy(self.defaultdefault),
426 )
427
428 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
429 """Compare two fields for equality.
430
431 Used by `lsst.pex.ConfigDictField.compare`.
432
433 Parameters
434 ----------
435 instance1 : `lsst.pex.config.Config`
436 Left-hand side config instance to compare.
437 instance2 : `lsst.pex.config.Config`
438 Right-hand side config instance to compare.
439 shortcut : `bool`
440 If `True`, this function returns as soon as an inequality if found.
441 rtol : `float`
442 Relative tolerance for floating point comparisons.
443 atol : `float`
444 Absolute tolerance for floating point comparisons.
445 output : callable
446 A callable that takes a string, used (possibly repeatedly) to
447 report inequalities. For example: `print`.
448
449 Returns
450 -------
451 isEqual : bool
452 `True` if the fields are equal, `False` otherwise.
453
454 Notes
455 -----
456 Floating point comparisons are performed by `numpy.allclose`.
457 """
458 c1 = getattr(instance1, self.name)._value
459 c2 = getattr(instance2, self.name)._value
460 name = getComparisonName(
461 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
462 )
463 return compareConfigs(name, c1, c2, shortcut=shortcut, rtol=rtol, atol=atol, output=output)
table::Key< int > type
Definition: Detector.cc:163
"Field[FieldTypeVar]" __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition: config.py:713
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:722
FieldTypeVar __get__(self, "Config" instance, Any owner=None, Any at=None, str label="default")
Definition: config.py:719
def _setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition: config.py:494
def __init__(self, doc, target, ConfigClass=None, default=None, check=None, deprecated=None)
ConfigurableInstance[FieldTypeVar] __get__(self, Config instance, Any owner=None, Any at=None, str label="default")
def __set__(self, instance, value, at=None, label="assignment")
def __getOrMake(self, instance, at=None, label="default")
"ConfigurableField" __get__(self, None instance, Any owner=None, Any at=None, str label="default")
def __get__(self, instance, owner=None, at=None, label="default")
def retarget(self, target, ConfigClass=None, at=None, label="retarget")
def __setattr__(self, name, value, at=None, label="assignment")
def __delattr__(self, name, at=None, label="delete")
def __init__(self, config, field, at=None, label="default")
def getCallStack(skip=0)
Definition: callStack.py:174
def getStackFrame(relative=0)
Definition: callStack.py:58
def compareConfigs(name, c1, c2, shortcut=True, rtol=1e-8, atol=1e-8, output=None)
Definition: comparison.py:111
def getComparisonName(name1, name2)
Definition: comparison.py:40
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174