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