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