28from __future__
import annotations
30__all__ = (
"ConfigurableInstance",
"ConfigurableField")
34from typing
import Any, Generic, Mapping, Union, overload
36from .callStack
import getCallStack, getStackFrame
37from .comparison
import compareConfigs, getComparisonName
43 UnexpectedProxyUsageError,
50 """A retargetable configuration in a `ConfigurableField` that proxies
55 ``ConfigurableInstance`` implements ``__getattr__`` and ``__setattr__``
57 ``ConfigurableInstance`` adds a `retarget` method.
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.
65 def __initValue(self, at, label):
66 """Construct value of field.
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
74 name = _joinNamePath(self._config_config._name, self._field.name)
76 storage = self._field.default._storage
79 value = self._ConfigClass(__name=name, __at=at, __label=label, **storage)
80 object.__setattr__(self,
"_value", value)
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)
92 at += [self._field.source]
95 history = config._history.setdefault(field.name, [])
96 history.append((
"Targeted and initialized from defaults", at, label))
99 def _config(self) -> Config:
102 assert self._config_()
is not None
103 return self._config_()
105 target = property(
lambda x: x._target)
106 """The targeted configurable (read-only).
109 ConfigClass = property(lambda x: x._ConfigClass)
110 """The configuration class (read-only)
113 value = property(lambda x: x._value)
114 """The `ConfigClass` instance (`lsst.pex.config.ConfigClass`-type,
119 """Call the configurable.
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`.
127 return self.
targettarget(*args, config=self.
valuevalue, **kw)
129 def retarget(self, target, ConfigClass=None, at=None, label="retarget"):
130 """Target a new configurable and ConfigClass"""
131 if self.
_config_config._frozen:
135 ConfigClass = self._field.validateTarget(target, ConfigClass)
136 except BaseException
as e:
141 object.__setattr__(self,
"_target", target)
143 object.__setattr__(self,
"_ConfigClass", ConfigClass)
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))
151 return getattr(self._value, name)
154 """Pretend to be an instance of ConfigClass.
156 Attributes defined by ConfigurableInstance will shadow those defined
159 if self.
_config_config._frozen:
162 if name
in self.__dict__:
164 object.__setattr__(self, name, value)
168 self._value.
__setattr__(name, value, at=at, label=label)
172 Pretend to be an isntance of ConfigClass.
173 Attributes defiend by ConfigurableInstance will shadow those defined
176 if self.
_config_config._frozen:
181 object.__delattr__(self, name)
182 except AttributeError:
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 "
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).
201 The ``ConfigurableField`` is often used to configure subtasks, which are
202 tasks (`~lsst.pipe.base.Task`) called by a parent task.
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)
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
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.
241 You can use the `ConfigurableInstance.apply` method to construct a
242 fully-configured configurable.
246 """Validate the target and configuration class.
251 The configurable being verified.
253 The configuration
class associated with the ``target``. This can
254 be `
None`
if ``target`` has a ``ConfigClass`` attribute.
259 Raised
if ``ConfigClass``
is `
None`
and ``target`` does
not have a
260 ``ConfigClass`` attribute.
267 - ``target``
is not callable (callables have a ``__call__``
269 - ``target``
is not startically defined (does
not have
270 ``__module__``
or ``__name__`` attributes).
272 if ConfigClass
is None:
274 ConfigClass = target.ConfigClass
276 raise AttributeError(
"'target' must define attribute 'ConfigClass'")
277 if not issubclass(ConfigClass, Config):
279 "'ConfigClass' is of incorrect type %s."
280 "'ConfigClass' must be a subclass of Config" % _typeStr(ConfigClass)
282 if not hasattr(target,
"__call__"):
283 raise ValueError(
"'target' must be callable")
284 if not hasattr(target,
"__module__")
or not hasattr(target,
"__name__"):
286 "'target' must be statically defined (must have '__module__' and '__name__' attributes)"
290 def __init__(self, doc, target, ConfigClass=None, default=None, check=None, deprecated=None):
291 ConfigClass = self.
validateTargetvalidateTarget(target, ConfigClass)
294 default = ConfigClass
295 if default != ConfigClass
and type(default) != ConfigClass:
297 "'default' is of incorrect type %s. Expected %s" % (_typeStr(default), _typeStr(ConfigClass))
303 dtype=ConfigurableInstance,
308 deprecated=deprecated,
314 def _parseTypingArgs(
315 params: Union[tuple[type, ...], tuple[str, ...]], kwds: Mapping[str, Any]
316 ) -> Mapping[str, Any]:
319 def __getOrMake(self, instance, at=None, label="default"):
320 value = instance._storage.get(self.name,
None)
325 instance._storage[self.name] = value
330 self, instance: None, owner: Any =
None, at: Any =
None, label: str =
"default"
331 ) ->
"ConfigurableField":
336 self, instance: Config, owner: Any =
None, at: Any =
None, label: str =
"default"
337 ) -> ConfigurableInstance[FieldTypeVar]:
340 def __get__(self, instance, owner=None, at=None, label="default"):
341 if instance
is None or not isinstance(instance, Config):
344 return self.
__getOrMake__getOrMake(instance, at=at, label=label)
346 def __set__(self, instance, value, at=None, label="assignment"):
351 oldValue = self.
__getOrMake__getOrMake(instance, at=at)
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)
362 msg =
"Value %s is of incorrect type %s. Expected %s" % (
365 _typeStr(oldValue.ConfigClass),
370 fullname = _joinNamePath(instance._name, self.name)
372 value._rename(fullname)
374 def _collectImports(self, instance, imports):
376 target = value.target
377 imports.add(target.__module__)
378 value.value._collectImports()
379 imports |= value.value._imports
381 def save(self, outfile, instance):
382 fullname = _joinNamePath(instance._name, self.name)
384 target = value.target
389 ConfigClass = value.ConfigClass
391 "{}.retarget(target={}, ConfigClass={})\n\n".
format(
392 fullname, _typeStr(target), _typeStr(ConfigClass)
404 return value.toDict()
410 if self.
checkcheck
is not None and not self.
checkcheck(value):
411 msg =
"%s is not a valid value" %
str(value)
415 """Customize deep-copying, because we always want a reference to the
418 WARNING: this must be overridden by subclasses if they change the
419 constructor signature!
425 default=copy.deepcopy(self.
defaultdefault),
428 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
429 """Compare two fields for equality.
431 Used by `lsst.pex.ConfigDictField.compare`.
436 Left-hand side config instance to compare.
438 Right-hand side config instance to compare.
440 If `True`, this function returns
as soon
as an inequality
if found.
442 Relative tolerance
for floating point comparisons.
444 Absolute tolerance
for floating point comparisons.
446 A callable that takes a string, used (possibly repeatedly) to
447 report inequalities. For example: `
print`.
452 `
True`
if the fields are equal, `
False` otherwise.
456 Floating point comparisons are performed by `numpy.allclose`.
458 c1 = getattr(instance1, self.name)._value
459 c2 = getattr(instance2, self.name)._value
461 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
463 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")
def __get__(self, instance, owner=None, at=None, label="default")
FieldTypeVar __get__(self, "Config" instance, Any owner=None, Any at=None, str label="default")
def _setup(self, doc, dtype, default, check, optional, source, deprecated)
def freeze(self, instance)
def save(self, outfile, instance)
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 validateTarget(self, target, ConfigClass)
def rename(self, instance)
def __set__(self, instance, value, at=None, label="assignment")
def __getOrMake(self, instance, at=None, label="default")
def toDict(self, instance)
def validate(self, instance)
"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 __deepcopy__(self, memo)
def __getattr__(self, name)
def apply(self, *args, **kw)
def __initValue(self, at, label)
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 getStackFrame(relative=0)
def compareConfigs(name, c1, c2, shortcut=True, rtol=1e-8, atol=1e-8, output=None)
def getComparisonName(name1, name2)
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)