LSST Applications 27.0.0,g0265f82a02+469cd937ee,g02d81e74bb+21ad69e7e1,g1470d8bcf6+cbe83ee85a,g2079a07aa2+e67c6346a6,g212a7c68fe+04a9158687,g2305ad1205+94392ce272,g295015adf3+81dd352a9d,g2bbee38e9b+469cd937ee,g337abbeb29+469cd937ee,g3939d97d7f+72a9f7b576,g487adcacf7+71499e7cba,g50ff169b8f+5929b3527e,g52b1c1532d+a6fc98d2e7,g591dd9f2cf+df404f777f,g5a732f18d5+be83d3ecdb,g64a986408d+21ad69e7e1,g858d7b2824+21ad69e7e1,g8a8a8dda67+a6fc98d2e7,g99cad8db69+f62e5b0af5,g9ddcbc5298+d4bad12328,ga1e77700b3+9c366c4306,ga8c6da7877+71e4819109,gb0e22166c9+25ba2f69a1,gb6a65358fc+469cd937ee,gbb8dafda3b+69d3c0e320,gc07e1c2157+a98bf949bb,gc120e1dc64+615ec43309,gc28159a63d+469cd937ee,gcf0d15dbbd+72a9f7b576,gdaeeff99f8+a38ce5ea23,ge6526c86ff+3a7c1ac5f1,ge79ae78c31+469cd937ee,gee10cc3b42+a6fc98d2e7,gf1cff7945b+21ad69e7e1,gfbcc870c63+9a11dc8c8f
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 @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 = "Value {} is of incorrect type {}. Expected {}".format(
393 value,
394 _typeStr(value),
395 _typeStr(oldValue.ConfigClass),
396 )
397 raise FieldValidationError(self, instance, msg)
398
399 def rename(self, instance):
400 fullname = _joinNamePath(instance._name, self.namenamename)
401 value = self.__getOrMake(instance)
402 value._rename(fullname)
403
404 def _collectImports(self, instance, imports):
405 value = self.__get____get____get____get____get____get__(instance)
406 target = value.target
407 imports.add(target.__module__)
408 value.value._collectImports()
409 imports |= value.value._imports
410
411 def save(self, outfile, instance):
412 fullname = _joinNamePath(instance._name, self.namenamename)
413 value = self.__getOrMake(instance)
414 target = value.target
415
416 if target != self.targettarget:
417 # not targeting the field-default target.
418 # save target information
419 ConfigClass = value.ConfigClass
420 outfile.write(
421 "{}.retarget(target={}, ConfigClass={})\n\n".format(
422 fullname, _typeStr(target), _typeStr(ConfigClass)
423 )
424 )
425 # save field values
426 value._save(outfile)
427
428 def freeze(self, instance):
429 value = self.__getOrMake(instance)
430 value.freeze()
431
432 def toDict(self, instance):
433 value = self.__get____get____get____get____get____get__(instance)
434 return value.toDict()
435
436 def validate(self, instance):
437 value = self.__get____get____get____get____get____get__(instance)
438 value.validate()
439
440 if self.check is not None and not self.check(value):
441 msg = "%s is not a valid value" % str(value)
442 raise FieldValidationError(self, instance, msg)
443
444 def __deepcopy__(self, memo):
445 """Customize deep-copying, because we always want a reference to the
446 original typemap.
447
448 WARNING: this must be overridden by subclasses if they change the
449 constructor signature!
450 """
451 return type(self)(
452 doc=self.doc,
453 target=self.targettarget,
454 ConfigClass=self.ConfigClassConfigClass,
455 default=copy.deepcopy(self.default),
456 )
457
458 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
459 """Compare two fields for equality.
460
461 Used by `lsst.pex.ConfigDictField.compare`.
462
463 Parameters
464 ----------
465 instance1 : `lsst.pex.config.Config`
466 Left-hand side config instance to compare.
467 instance2 : `lsst.pex.config.Config`
468 Right-hand side config instance to compare.
469 shortcut : `bool`
470 If `True`, this function returns as soon as an inequality if found.
471 rtol : `float`
472 Relative tolerance for floating point comparisons.
473 atol : `float`
474 Absolute tolerance for floating point comparisons.
475 output : callable
476 A callable that takes a string, used (possibly repeatedly) to
477 report inequalities. For example: `print`.
478
479 Returns
480 -------
481 isEqual : bool
482 `True` if the fields are equal, `False` otherwise.
483
484 Notes
485 -----
486 Floating point comparisons are performed by `numpy.allclose`.
487 """
488 c1 = getattr(instance1, self.namenamename)._value
489 c2 = getattr(instance2, self.namenamename)._value
490 name = getComparisonName(
491 _joinNamePath(instance1._name, self.namenamename), _joinNamePath(instance2._name, self.namenamename)
492 )
493 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:726
FieldTypeVar __get__(self, Config instance, Any owner=None, Any at=None, str label="default")
Definition config.py:724
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")