lsst.pipe.tasks  21.0.0-107-gd7efe290+5c5fa69342
_configurableActionStructField.py
Go to the documentation of this file.
1 # This file is part of pipe_tasks.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://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 program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 from __future__ import annotations
22 
23 __all__ = ("ConfigurableActionStructField", "ConfigurableActionStruct")
24 
25 from typing import Iterable, Mapping, Optional, TypeVar, Union, Type, Tuple, List, Any, Dict
26 
27 from lsst.pex.config.config import Config, Field, FieldValidationError, _typeStr, _joinNamePath
28 from lsst.pex.config.comparison import compareConfigs, compareScalars, getComparisonName
29 from lsst.pex.config.callStack import StackFrame, getCallStack, getStackFrame
30 
31 from . import ConfigurableAction
32 
33 
35  """This descriptor exists to abstract the logic of using a dictionary to
36  update a ConfigurableActionStruct through attribute assignment. This is
37  useful in the context of setting configuration through pipelines or on
38  the command line.
39  """
40  def __set__(self, instance: ConfigurableActionStruct,
41  value: Union[Mapping[str, ConfigurableAction], ConfigurableActionStruct]) -> None:
42  if isinstance(value, Mapping):
43  pass
44  elif isinstance(value, ConfigurableActionStruct):
45  # If the update target is a ConfigurableActionStruct, get the
46  # internal dictionary
47  value = value._attrs
48  else:
49  raise ValueError("Can only update a ConfigurableActionStruct with an instance of such, or a "
50  "mapping")
51  for name, action in value.items():
52  setattr(instance, name, action)
53 
54  def __get__(self, instance, objtype=None) -> None:
55  # This descriptor does not support fetching any value
56  return None
57 
58 
60  """This descriptor exists to abstract the logic of removing an interable
61  of action names from a ConfigurableActionStruct at one time using
62  attribute assignment. This is useful in the context of setting
63  configuration through pipelines or on the command line.
64 
65  Raises
66  ------
67  AttributeError
68  Raised if an attribute specified for removal does not exist in the
69  ConfigurableActionStruct
70  """
71  def __set__(self, instance: ConfigurableActionStruct,
72  value: Union[str, Iterable[str]]) -> None:
73  # strings are iterable, but not in the way that is intended. If a
74  # single name is specified, turn it into a tuple before attempting
75  # to remove the attribute
76  if isinstance(value, str):
77  value = (value, )
78  for name in value:
79  delattr(instance, name)
80 
81  def __get__(self, instance, objtype=None) -> None:
82  # This descriptor does not support fetching any value
83  return None
84 
85 
87  """A ConfigurableActionStruct is the storage backend class that supports
88  the ConfigurableActionStructField. This class should not be created
89  directly.
90 
91  This class allows managing a collection of `ConfigurableActions` with a
92  struct like interface, that is to say in an attribute like notation.
93 
94  Attributes can be dynamically added or removed as such:
95 
96  ConfigurableActionStructInstance.variable1 = a_configurable_action
97  del ConfigurableActionStructInstance.variable1
98 
99  Each action is then available to be individually configured as a normal
100  `lsst.pex.config.Config` object.
101 
102  ConfigurableActionStruct supports two special convenance attributes.
103 
104  The first is `update`. You may assign a dict of `ConfigurableActions` or
105  a `ConfigurableActionStruct` to this attribute which will update the
106  `ConfigurableActionStruct` on which the attribute is invoked such that it
107  will be updated to contain the entries specified by the structure on the
108  right hand side of the equals sign.
109 
110  The second convenience attribute is named remove. You may assign an
111  iterable of strings which correspond to attribute names on the
112  `ConfigurableActionStruct`. All of the corresponding attributes will then
113  be removed. If any attribute does not exist, an `AttributeError` will be
114  raised. Any attributes in the Iterable prior to the name which raises will
115  have been removed from the `ConfigurableActionStruct`
116  """
117  # declare attributes that are set with __setattr__
118  _config: Config
119  _attrs: Dict[str, ConfigurableAction]
120  _field: ConfigurableActionStructField
121  _history: List[tuple]
122 
123  # create descriptors to handle special update and remove behavior
126 
127  def __init__(self, config: Config, field: ConfigurableActionStructField,
128  value: Mapping[str, ConfigurableAction], at: Any, label: str):
129  object.__setattr__(self, '_config', config)
130  object.__setattr__(self, '_attrs', {})
131  object.__setattr__(self, '_field', field)
132  object.__setattr__(self, '_history', [])
133 
134  self.historyhistory.append(("Struct initialized", at, label))
135 
136  if value is not None:
137  for k, v in value.items():
138  setattr(self, k, v)
139 
140  @property
141  def history(self) -> List[tuple]:
142  return self._history
143 
144  @property
145  def fieldNames(self) -> Iterable[str]:
146  return self._attrs.keys()
147 
148  def __setattr__(self, attr: str, value: Union[ConfigurableAction, Type[ConfigurableAction]],
149  at=None, label='setattr', setHistory=False) -> None:
150 
151  if hasattr(self._config, '_frozen') and self._config._frozen:
152  msg = "Cannot modify a frozen Config. "\
153  f"Attempting to set item {attr} to value {value}"
154  raise FieldValidationError(self._field, self._config, msg)
155 
156  if attr not in (self.__dict__.keys() | type(self).__dict__.keys()):
157  name = _joinNamePath(self._config._name, self._field.name, attr)
158  if at is None:
159  at = getCallStack()
160  if isinstance(value, ConfigurableAction):
161  valueInst = type(value)(__name=name, __at=at, __label=label, **value._storage)
162  else:
163  valueInst = value(__name=name, __at=at, __label=label)
164  self._attrs[attr] = valueInst
165  else:
166  super().__setattr__(attr, value)
167 
168  def __getattr__(self, attr):
169  if attr in object.__getattribute__(self, '_attrs'):
170  return self._attrs[attr]
171  else:
172  super().__getattribute__(attr)
173 
174  def __delattr__(self, name):
175  if name in self._attrs:
176  del self._attrs[name]
177  else:
178  super().__delattr__(name)
179 
180  def __iter__(self) -> Iterable[ConfigurableAction]:
181  return iter(self._attrs.values())
182 
183  def items(self) -> Iterable[Tuple[str, ConfigurableAction]]:
184  return iter(self._attrs.items())
185 
186 
187 T = TypeVar("T", bound="ConfigurableActionStructField")
188 
189 
191  r"""`ConfigurableActionStructField` is a `~lsst.pex.config.Field` subclass
192  that allows `ConfigurableAction`\ s to be organized in a
193  `~lsst.pex.config.Config` class in a manor similar to how a
194  `~lsst.pipe.base.Struct` works.
195 
196  This class implements a `ConfigurableActionStruct` as an intermediary
197  object to organize the `ConfigurableActions`. See it's documentation for
198  futher information.
199  """
200  # specify StructClass to make this more generic for potential future
201  # inheritance
202  StructClass = ConfigurableActionStruct
203 
204  # Explicitly annotate these on the class, they are present in the base
205  # class through injection, so type systems have trouble seeing them.
206  name: str
207  default: Optional[Mapping[str, ConfigurableAction]]
208 
209  def __init__(self, doc: str, default: Optional[Mapping[str, ConfigurableAction]] = None,
210  optional: bool = False,
211  deprecated=None):
212  source = getStackFrame()
213  self._setup(doc=doc, dtype=self.__class__, default=default, check=None,
214  optional=optional, source=source, deprecated=deprecated)
215 
216  def __set__(self, instance: Config,
217  value: Union[None, Mapping[str, ConfigurableAction], ConfigurableActionStruct],
218  at: Iterable[StackFrame] = None, label: str = 'assigment'):
219  if instance._frozen:
220  msg = "Cannot modify a frozen Config. "\
221  "Attempting to set field to value %s" % value
222  raise FieldValidationError(self, instance, msg)
223 
224  if at is None:
225  at = getCallStack()
226 
227  if value is None or value == self.default:
228  value = self.StructClassStructClass(instance, self, value, at=at, label=label)
229  else:
230  history = instance._history.setdefault(self.name, [])
231  history.append((value, at, label))
232 
233  if not isinstance(value, ConfigurableActionStruct):
234  raise FieldValidationError(self, instance,
235  "Can only assign things that are subclasses of Configurable Action")
236  instance._storage[self.name] = value
237 
238  def __get__(self: T, instance: Config, owner: None = None, at: Iterable[StackFrame] = None,
239  label: str = "default"
240  ) -> Union[None, T, ConfigurableActionStruct]:
241  if instance is None or not isinstance(instance, Config):
242  return self
243  else:
244  field: Optional[ConfigurableActionStruct] = instance._storage[self.name]
245  return field
246 
247  def rename(self, instance: Config):
248  actionStruct: ConfigurableActionStruct = self.__get____get____get__(instance)
249  if actionStruct is not None:
250  for k, v in actionStruct.items():
251  fullname = _joinNamePath(instance._name, self.name, k)
252  v._rename(fullname)
253 
254  def validate(self, instance):
255  value = self.__get____get____get__(instance)
256  if value is not None:
257  for item in value:
258  item.validate()
259 
260  def toDict(self, instance):
261  actionStruct = self.__get____get____get__(instance)
262  if actionStruct is None:
263  return None
264 
265  dict_ = {k: v.toDict() for k, v in actionStruct.items()}
266 
267  return dict_
268 
269  def save(self, outfile, instance):
270  actionStruct = self.__get____get____get__(instance)
271  fullname = _joinNamePath(instance._name, self.name)
272  if actionStruct is None:
273  outfile.write(u"{}={!r}\n".format(fullname, actionStruct))
274  return
275 
276  outfile.write(u"{}={!r}\n".format(fullname, {}))
277  for v in actionStruct:
278  outfile.write(u"{}={}()\n".format(v._name, _typeStr(v)))
279  v._save(outfile)
280 
281  def freeze(self, instance):
282  actionStruct = self.__get____get____get__(instance)
283  if actionStruct is not None:
284  for v in actionStruct:
285  v.freeze()
286 
287  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
288  """Compare two fields for equality.
289 
290  Parameters
291  ----------
292  instance1 : `lsst.pex.config.Config`
293  Left-hand side config instance to compare.
294  instance2 : `lsst.pex.config.Config`
295  Right-hand side config instance to compare.
296  shortcut : `bool`
297  If `True`, this function returns as soon as an inequality if found.
298  rtol : `float`
299  Relative tolerance for floating point comparisons.
300  atol : `float`
301  Absolute tolerance for floating point comparisons.
302  output : callable
303  A callable that takes a string, used (possibly repeatedly) to
304  report inequalities.
305 
306  Returns
307  -------
308  isEqual : bool
309  `True` if the fields are equal, `False` otherwise.
310 
311  Notes
312  -----
313  Floating point comparisons are performed by `numpy.allclose`.
314  """
315  d1: ConfigurableActionStruct = getattr(instance1, self.name)
316  d2: ConfigurableActionStruct = getattr(instance2, self.name)
317  name = getComparisonName(
318  _joinNamePath(instance1._name, self.name),
319  _joinNamePath(instance2._name, self.name)
320  )
321  if not compareScalars(f"keys for {name}", set(d1.fieldNames), set(d2.fieldNames), output=output):
322  return False
323  equal = True
324  for k, v1 in d1.items():
325  v2 = getattr(d2, k)
326  result = compareConfigs(f"{name}.{k}", v1, v2, shortcut=shortcut,
327  rtol=rtol, atol=atol, output=output)
328  if not result and shortcut:
329  return False
330  equal = equal and result
331  return equal
def __get__(self, instance, owner=None, at=None, label="default")
Union[None, T, ConfigurableActionStruct] __get__(T self, Config instance, None owner=None, Iterable[StackFrame] at=None, str label="default")
def __init__(self, str doc, Optional[Mapping[str, ConfigurableAction]] default=None, bool optional=False, deprecated=None)
def __set__(self, Config instance, Union[None, Mapping[str, ConfigurableAction], ConfigurableActionStruct] value, Iterable[StackFrame] at=None, str label='assigment')
def __init__(self, Config config, ConfigurableActionStructField field, Mapping[str, ConfigurableAction] value, Any at, str label)
None __setattr__(self, str attr, Union[ConfigurableAction, Type[ConfigurableAction]] value, at=None, label='setattr', setHistory=False)
None __set__(self, ConfigurableActionStruct instance, Union[str, Iterable[str]] value)
None __set__(self, ConfigurableActionStruct instance, Union[Mapping[str, ConfigurableAction], ConfigurableActionStruct] value)