28from __future__
import annotations
30__all__ = (
"ConfigurableInstance",
"ConfigurableField")
35from typing
import Any, Generic, overload
37from .callStack
import getCallStack, getStackFrame
38from .comparison
import compareConfigs, getComparisonName
44 UnexpectedProxyUsageError,
51 """A retargetable configuration in a `ConfigurableField` that proxies
56 ``ConfigurableInstance`` implements ``__getattr__`` and ``__setattr__``
58 ``ConfigurableInstance`` adds a `retarget` method.
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.
66 def __initValue(self, at, label):
67 """Construct value of field.
72 custom construct ``_value``
with the correct values
from default.
73 Otherwise, call ``ConfigClass`` constructor
77 storage = self.
_field.default._storage
80 value = self._ConfigClass(__name=name, __at=at, __label=label, **storage)
81 object.__setattr__(self,
"_value", value)
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)
96 history = config._history.setdefault(field.name, [])
97 history.append((
"Targeted and initialized from defaults", at, label))
103 assert self._config_()
is not None
104 return self._config_()
106 target = property(
lambda x: x._target)
107 """The targeted configurable (read-only).
110 ConfigClass = property(lambda x: x._ConfigClass)
111 """The configuration class (read-only)
114 value = property(lambda x: x._value)
115 """The `ConfigClass` instance (`lsst.pex.config.Config`-type,
120 """Call the configurable.
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`.
130 def retarget(self, target, ConfigClass=None, at=None, label="retarget"):
131 """Target a new configurable and ConfigClass."""
136 ConfigClass = self.
_field.validateTarget(target, ConfigClass)
137 except BaseException
as e:
142 object.__setattr__(self,
"_target", target)
144 object.__setattr__(self,
"_ConfigClass", ConfigClass)
148 msg = f
"retarget(target={_typeStr(target)}, ConfigClass={_typeStr(ConfigClass)})"
149 history.append((msg, at, label))
155 """Pretend to be an instance of ConfigClass.
157 Attributes defined by ConfigurableInstance will shadow those defined
163 if name
in self.__dict__:
165 object.__setattr__(self, name, value)
173 Pretend to be an isntance of ConfigClass.
174 Attributes defiend by ConfigurableInstance will shadow those defined
182 object.__delattr__(self, name)
183 except AttributeError:
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 "
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).
202 The ``ConfigurableField`` is often used to configure subtasks, which are
203 tasks (`~lsst.pipe.base.Task`) called by a parent task.
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)
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
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.
242 You can use the `ConfigurableInstance.apply` method to construct a
243 fully-configured configurable.
247 """Validate the target and configuration class.
252 The configurable being verified.
254 The configuration
class associated with the ``target``. This can
255 be `
None`
if ``target`` has a ``ConfigClass`` attribute.
260 Raised
if ``ConfigClass``
is `
None`
and ``target`` does
not have a
261 ``ConfigClass`` attribute.
268 - ``target``
is not callable (callables have a ``__call__``
270 - ``target``
is not startically defined (does
not have
271 ``__module__``
or ``__name__`` attributes).
273 if ConfigClass
is None:
275 ConfigClass = target.ConfigClass
277 raise AttributeError(
"'target' must define attribute 'ConfigClass'")
278 if not issubclass(ConfigClass, Config):
280 "'ConfigClass' is of incorrect type %s.'ConfigClass' must be a subclass of Config"
281 % _typeStr(ConfigClass)
283 if not hasattr(target,
"__call__"):
284 raise ValueError(
"'target' must be callable")
285 if not hasattr(target,
"__module__")
or not hasattr(target,
"__name__"):
287 "'target' must be statically defined (must have '__module__' and '__name__' attributes)"
291 def __init__(self, doc, target, ConfigClass=None, default=None, check=None, deprecated=None):
295 default = ConfigClass
296 if default != ConfigClass
and type(default) != ConfigClass:
298 f
"'default' is of incorrect type {_typeStr(default)}. Expected {_typeStr(ConfigClass)}"
301 source = getStackFrame()
304 dtype=ConfigurableInstance,
309 deprecated=deprecated,
316 params: tuple[type, ...] | tuple[str, ...], kwds: Mapping[str, Any]
317 ) -> Mapping[str, Any]:
320 def __getOrMake(self, instance, at=None, label="default"):
331 self, instance: None, owner: Any =
None, at: Any =
None, label: str =
"default"
332 ) -> ConfigurableField:
337 self, instance: Config, owner: Any =
None, at: Any =
None, label: str =
"default"
338 ) -> ConfigurableInstance[FieldTypeVar]:
341 def __get__(self, instance, owner=None, at=None, label="default"):
342 if instance
is None or not isinstance(instance, Config):
345 return self.
__getOrMake(instance, at=at, label=label)
347 def __set__(self, instance, value, at=None, label="assignment"):
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)
363 msg =
"Value {} is of incorrect type {}. Expected {}".format(
366 _typeStr(oldValue.ConfigClass),
371 fullname = _joinNamePath(instance._name, self.
namenamename)
373 value._rename(fullname)
377 target = value.target
378 imports.add(target.__module__)
379 value.value._collectImports()
380 imports |= value.value._imports
382 def save(self, outfile, instance):
383 fullname = _joinNamePath(instance._name, self.
namenamename)
385 target = value.target
390 ConfigClass = value.ConfigClass
392 "{}.retarget(target={}, ConfigClass={})\n\n".format(
393 fullname, _typeStr(target), _typeStr(ConfigClass)
405 return value.toDict()
411 if self.
check is not None and not self.
check(value):
412 msg =
"%s is not a valid value" % str(value)
416 """Customize deep-copying, because we always want a reference to the
419 WARNING: this must be overridden by subclasses if they change the
420 constructor signature!
426 default=copy.deepcopy(self.
default),
429 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
430 """Compare two fields for equality.
432 Used by `lsst.pex.ConfigDictField.compare`.
437 Left-hand side config instance to compare.
439 Right-hand side config instance to compare.
441 If `True`, this function returns
as soon
as an inequality
if found.
443 Relative tolerance
for floating point comparisons.
445 Absolute tolerance
for floating point comparisons.
447 A callable that takes a string, used (possibly repeatedly) to
448 report inequalities. For example: `
print`.
453 `
True`
if the fields are equal, `
False` otherwise.
457 Floating point comparisons are performed by `numpy.allclose`.
461 name = getComparisonName(
464 return compareConfigs(name, c1, c2, shortcut=shortcut, rtol=rtol, atol=atol, output=output)
__get__(self, instance, owner=None, at=None, label="default")
FieldTypeVar __get__(self, Config instance, Any owner=None, Any at=None, str label="default")
Field[FieldTypeVar] __get__(self, None instance, Any owner=None, Any at=None, str label="default")
_setup(self, doc, dtype, default, check, optional, source, deprecated)
validateTarget(self, target, ConfigClass)
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)
_collectImports(self, instance, imports)
save(self, outfile, instance)
__get__(self, instance, owner=None, at=None, label="default")
__initValue(self, at, label)
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")
__delattr__(self, name, at=None, label="delete")