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