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