LSST Applications  21.0.0-147-g0e635eb1+1acddb5be5,22.0.0+052faf71bd,22.0.0+1ea9a8b2b2,22.0.0+6312710a6c,22.0.0+729191ecac,22.0.0+7589c3a021,22.0.0+9f079a9461,22.0.1-1-g7d6de66+b8044ec9de,22.0.1-1-g87000a6+536b1ee016,22.0.1-1-g8e32f31+6312710a6c,22.0.1-10-gd060f87+016f7cdc03,22.0.1-12-g9c3108e+df145f6f68,22.0.1-16-g314fa6d+c825727ab8,22.0.1-19-g93a5c75+d23f2fb6d8,22.0.1-19-gb93eaa13+aab3ef7709,22.0.1-2-g8ef0a89+b8044ec9de,22.0.1-2-g92698f7+9f079a9461,22.0.1-2-ga9b0f51+052faf71bd,22.0.1-2-gac51dbf+052faf71bd,22.0.1-2-gb66926d+6312710a6c,22.0.1-2-gcb770ba+09e3807989,22.0.1-20-g32debb5+b8044ec9de,22.0.1-23-gc2439a9a+fb0756638e,22.0.1-3-g496fd5d+09117f784f,22.0.1-3-g59f966b+1e6ba2c031,22.0.1-3-g849a1b8+f8b568069f,22.0.1-3-gaaec9c0+c5c846a8b1,22.0.1-32-g5ddfab5d3+60ce4897b0,22.0.1-4-g037fbe1+64e601228d,22.0.1-4-g8623105+b8044ec9de,22.0.1-5-g096abc9+d18c45d440,22.0.1-5-g15c806e+57f5c03693,22.0.1-7-gba73697+57f5c03693,master-g6e05de7fdc+c1283a92b8,master-g72cdda8301+729191ecac,w.2021.39
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 
28 __all__ = ('ConfigurableInstance', 'ConfigurableField')
29 
30 import copy
31 
32 from .config import Config, Field, _joinNamePath, _typeStr, FieldValidationError
33 from .comparison import compareConfigs, getComparisonName
34 from .callStack import getCallStack, getStackFrame
35 
36 import weakref
37 
38 
40  """A retargetable configuration in a `ConfigurableField` that proxies
41  a `~lsst.pex.config.Config`.
42 
43  Notes
44  -----
45  ``ConfigurableInstance`` implements ``__getattr__`` and ``__setattr__``
46  methods that forward to the `~lsst.pex.config.Config` it holds.
47  ``ConfigurableInstance`` adds a `retarget` method.
48 
49  The actual `~lsst.pex.config.Config` instance is accessed using the
50  ``value`` property (e.g. to get its documentation). The associated
51  configurable object (usually a `~lsst.pipe.base.Task`) is accessed
52  using the ``target`` property.
53  """
54 
55  def __initValue(self, at, label):
56  """Construct value of field.
57 
58  Notes
59  -----
60  If field.default is an instance of `lsst.pex.config.ConfigClass`,
61  custom construct ``_value`` with the correct values from default.
62  Otherwise, call ``ConfigClass`` constructor
63  """
64  name = _joinNamePath(self._config_config._name, self._field.name)
65  if type(self._field.default) == self.ConfigClassConfigClass:
66  storage = self._field.default._storage
67  else:
68  storage = {}
69  value = self._ConfigClass(__name=name, __at=at, __label=label, **storage)
70  object.__setattr__(self, "_value", value)
71 
72  def __init__(self, config, field, at=None, label="default"):
73  object.__setattr__(self, "_config_", weakref.ref(config))
74  object.__setattr__(self, "_field", field)
75  object.__setattr__(self, "__doc__", config)
76  object.__setattr__(self, "_target", field.target)
77  object.__setattr__(self, "_ConfigClass", field.ConfigClass)
78  object.__setattr__(self, "_value", None)
79 
80  if at is None:
81  at = getCallStack()
82  at += [self._field.source]
83  self.__initValue__initValue(at, label)
84 
85  history = config._history.setdefault(field.name, [])
86  history.append(("Targeted and initialized from defaults", at, label))
87 
88  @property
89  def _config(self) -> Config:
90  # Config Fields should never outlive their config class instance
91  # assert that as such here
92  assert(self._config_() is not None)
93  return self._config_()
94 
95  target = property(lambda x: x._target)
96  """The targeted configurable (read-only).
97  """
98 
99  ConfigClass = property(lambda x: x._ConfigClass)
100  """The configuration class (read-only)
101  """
102 
103  value = property(lambda x: x._value)
104  """The `ConfigClass` instance (`lsst.pex.config.ConfigClass`-type,
105  read-only).
106  """
107 
108  def apply(self, *args, **kw):
109  """Call the configurable.
110 
111  Notes
112  -----
113  In addition to the user-provided positional and keyword arguments,
114  the configurable is also provided a keyword argument ``config`` with
115  the value of `ConfigurableInstance.value`.
116  """
117  return self.targettarget(*args, config=self.valuevalue, **kw)
118 
119  def retarget(self, target, ConfigClass=None, at=None, label="retarget"):
120  """Target a new configurable and ConfigClass
121  """
122  if self._config_config._frozen:
123  raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
124 
125  try:
126  ConfigClass = self._field.validateTarget(target, ConfigClass)
127  except BaseException as e:
128  raise FieldValidationError(self._field, self._config_config, e.message)
129 
130  if at is None:
131  at = getCallStack()
132  object.__setattr__(self, "_target", target)
133  if ConfigClass != self.ConfigClassConfigClass:
134  object.__setattr__(self, "_ConfigClass", ConfigClass)
135  self.__initValue__initValue(at, label)
136 
137  history = self._config_config._history.setdefault(self._field.name, [])
138  msg = "retarget(target=%s, ConfigClass=%s)" % (_typeStr(target), _typeStr(ConfigClass))
139  history.append((msg, at, label))
140 
141  def __getattr__(self, name):
142  return getattr(self._value, name)
143 
144  def __setattr__(self, name, value, at=None, label="assignment"):
145  """Pretend to be an instance of ConfigClass.
146 
147  Attributes defined by ConfigurableInstance will shadow those defined
148  in ConfigClass
149  """
150  if self._config_config._frozen:
151  raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
152 
153  if name in self.__dict__:
154  # attribute exists in the ConfigurableInstance wrapper
155  object.__setattr__(self, name, value)
156  else:
157  if at is None:
158  at = getCallStack()
159  self._value.__setattr__(name, value, at=at, label=label)
160 
161  def __delattr__(self, name, at=None, label="delete"):
162  """
163  Pretend to be an isntance of ConfigClass.
164  Attributes defiend by ConfigurableInstance will shadow those defined
165  in ConfigClass
166  """
167  if self._config_config._frozen:
168  raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
169 
170  try:
171  # attribute exists in the ConfigurableInstance wrapper
172  object.__delattr__(self, name)
173  except AttributeError:
174  if at is None:
175  at = getCallStack()
176  self._value.__delattr__(name, at=at, label=label)
177 
178 
180  """A configuration field (`~lsst.pex.config.Field` subclass) that can be
181  can be retargeted towards a different configurable (often a
182  `lsst.pipe.base.Task` subclass).
183 
184  The ``ConfigurableField`` is often used to configure subtasks, which are
185  tasks (`~lsst.pipe.base.Task`) called by a parent task.
186 
187  Parameters
188  ----------
189  doc : `str`
190  A description of the configuration field.
191  target : configurable class
192  The configurable target. Configurables have a ``ConfigClass``
193  attribute. Within the task framework, configurables are
194  `lsst.pipe.base.Task` subclasses)
195  ConfigClass : `lsst.pex.config.Config`-type, optional
196  The subclass of `lsst.pex.config.Config` expected as the configuration
197  class of the ``target``. If ``ConfigClass`` is unset then
198  ``target.ConfigClass`` is used.
199  default : ``ConfigClass``-type, optional
200  The default configuration class. Normally this parameter is not set,
201  and defaults to ``ConfigClass`` (or ``target.ConfigClass``).
202  check : callable, optional
203  Callable that takes the field's value (the ``target``) as its only
204  positional argument, and returns `True` if the ``target`` is valid (and
205  `False` otherwise).
206  deprecated : None or `str`, optional
207  A description of why this Field is deprecated, including removal date.
208  If not None, the string is appended to the docstring for this Field.
209 
210  See also
211  --------
212  ChoiceField
213  ConfigChoiceField
214  ConfigDictField
215  ConfigField
216  DictField
217  Field
218  ListField
219  RangeField
220  RegistryField
221 
222  Notes
223  -----
224  You can use the `ConfigurableInstance.apply` method to construct a
225  fully-configured configurable.
226  """
227 
228  def validateTarget(self, target, ConfigClass):
229  """Validate the target and configuration class.
230 
231  Parameters
232  ----------
233  target
234  The configurable being verified.
235  ConfigClass : `lsst.pex.config.Config`-type or `None`
236  The configuration class associated with the ``target``. This can
237  be `None` if ``target`` has a ``ConfigClass`` attribute.
238 
239  Raises
240  ------
241  AttributeError
242  Raised if ``ConfigClass`` is `None` and ``target`` does not have a
243  ``ConfigClass`` attribute.
244  TypeError
245  Raised if ``ConfigClass`` is not a `~lsst.pex.config.Config`
246  subclass.
247  ValueError
248  Raised if:
249 
250  - ``target`` is not callable (callables have a ``__call__``
251  method).
252  - ``target`` is not startically defined (does not have
253  ``__module__`` or ``__name__`` attributes).
254  """
255  if ConfigClass is None:
256  try:
257  ConfigClass = target.ConfigClass
258  except Exception:
259  raise AttributeError("'target' must define attribute 'ConfigClass'")
260  if not issubclass(ConfigClass, Config):
261  raise TypeError("'ConfigClass' is of incorrect type %s."
262  "'ConfigClass' must be a subclass of Config" % _typeStr(ConfigClass))
263  if not hasattr(target, '__call__'):
264  raise ValueError("'target' must be callable")
265  if not hasattr(target, '__module__') or not hasattr(target, '__name__'):
266  raise ValueError("'target' must be statically defined"
267  "(must have '__module__' and '__name__' attributes)")
268  return ConfigClass
269 
270  def __init__(self, doc, target, ConfigClass=None, default=None, check=None, deprecated=None):
271  ConfigClass = self.validateTargetvalidateTarget(target, ConfigClass)
272 
273  if default is None:
274  default = ConfigClass
275  if default != ConfigClass and type(default) != ConfigClass:
276  raise TypeError("'default' is of incorrect type %s. Expected %s" %
277  (_typeStr(default), _typeStr(ConfigClass)))
278 
279  source = getStackFrame()
280  self._setup_setup(doc=doc, dtype=ConfigurableInstance, default=default,
281  check=check, optional=False, source=source, deprecated=deprecated)
282  self.targettarget = target
283  self.ConfigClassConfigClass = ConfigClass
284 
285  def __getOrMake(self, instance, at=None, label="default"):
286  value = instance._storage.get(self.name, None)
287  if value is None:
288  if at is None:
289  at = getCallStack(1)
290  value = ConfigurableInstance(instance, self, at=at, label=label)
291  instance._storage[self.name] = value
292  return value
293 
294  def __get__(self, instance, owner=None, at=None, label="default"):
295  if instance is None or not isinstance(instance, Config):
296  return self
297  else:
298  return self.__getOrMake__getOrMake(instance, at=at, label=label)
299 
300  def __set__(self, instance, value, at=None, label="assignment"):
301  if instance._frozen:
302  raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
303  if at is None:
304  at = getCallStack()
305  oldValue = self.__getOrMake__getOrMake(instance, at=at)
306 
307  if isinstance(value, ConfigurableInstance):
308  oldValue.retarget(value.target, value.ConfigClass, at, label)
309  oldValue.update(__at=at, __label=label, **value._storage)
310  elif type(value) == oldValue._ConfigClass:
311  oldValue.update(__at=at, __label=label, **value._storage)
312  elif value == oldValue.ConfigClass:
313  value = oldValue.ConfigClass()
314  oldValue.update(__at=at, __label=label, **value._storage)
315  else:
316  msg = "Value %s is of incorrect type %s. Expected %s" % \
317  (value, _typeStr(value), _typeStr(oldValue.ConfigClass))
318  raise FieldValidationError(self, instance, msg)
319 
320  def rename(self, instance):
321  fullname = _joinNamePath(instance._name, self.name)
322  value = self.__getOrMake__getOrMake(instance)
323  value._rename(fullname)
324 
325  def _collectImports(self, instance, imports):
326  value = self.__get____get____get__(instance)
327  target = value.target
328  imports.add(target.__module__)
329  value.value._collectImports()
330  imports |= value.value._imports
331 
332  def save(self, outfile, instance):
333  fullname = _joinNamePath(instance._name, self.name)
334  value = self.__getOrMake__getOrMake(instance)
335  target = value.target
336 
337  if target != self.targettarget:
338  # not targeting the field-default target.
339  # save target information
340  ConfigClass = value.ConfigClass
341  outfile.write(u"{}.retarget(target={}, ConfigClass={})\n\n".format(fullname,
342  _typeStr(target),
343  _typeStr(ConfigClass)))
344  # save field values
345  value._save(outfile)
346 
347  def freeze(self, instance):
348  value = self.__getOrMake__getOrMake(instance)
349  value.freeze()
350 
351  def toDict(self, instance):
352  value = self.__get____get____get__(instance)
353  return value.toDict()
354 
355  def validate(self, instance):
356  value = self.__get____get____get__(instance)
357  value.validate()
358 
359  if self.checkcheck is not None and not self.checkcheck(value):
360  msg = "%s is not a valid value" % str(value)
361  raise FieldValidationError(self, instance, msg)
362 
363  def __deepcopy__(self, memo):
364  """Customize deep-copying, because we always want a reference to the
365  original typemap.
366 
367  WARNING: this must be overridden by subclasses if they change the
368  constructor signature!
369  """
370  return type(self)(doc=self.docdoc, target=self.targettarget, ConfigClass=self.ConfigClassConfigClass,
371  default=copy.deepcopy(self.defaultdefault))
372 
373  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
374  """Compare two fields for equality.
375 
376  Used by `lsst.pex.ConfigDictField.compare`.
377 
378  Parameters
379  ----------
380  instance1 : `lsst.pex.config.Config`
381  Left-hand side config instance to compare.
382  instance2 : `lsst.pex.config.Config`
383  Right-hand side config instance to compare.
384  shortcut : `bool`
385  If `True`, this function returns as soon as an inequality if found.
386  rtol : `float`
387  Relative tolerance for floating point comparisons.
388  atol : `float`
389  Absolute tolerance for floating point comparisons.
390  output : callable
391  A callable that takes a string, used (possibly repeatedly) to
392  report inequalities. For example: `print`.
393 
394  Returns
395  -------
396  isEqual : bool
397  `True` if the fields are equal, `False` otherwise.
398 
399  Notes
400  -----
401  Floating point comparisons are performed by `numpy.allclose`.
402  """
403  c1 = getattr(instance1, self.name)._value
404  c2 = getattr(instance2, self.name)._value
405  name = getComparisonName(
406  _joinNamePath(instance1._name, self.name),
407  _joinNamePath(instance2._name, self.name)
408  )
409  return compareConfigs(name, c1, c2, shortcut=shortcut, rtol=rtol, atol=atol, output=output)
table::Key< int > type
Definition: Detector.cc:163
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:550
def _setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition: config.py:336
def __init__(self, doc, target, ConfigClass=None, default=None, check=None, deprecated=None)
def __set__(self, instance, value, at=None, label="assignment")
def __getOrMake(self, instance, at=None, 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:175
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