Coverage for python/lsst/pipe/tasks/configurableActions/_configurableActionStructField.py: 22%
150 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-01 21:12 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-01 21:12 +0000
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
23__all__ = ("ConfigurableActionStructField", "ConfigurableActionStruct")
25from typing import Iterable, Mapping, Optional, TypeVar, Union, Type, Tuple, List, Any, Dict
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
31from . import ConfigurableAction
33import weakref
36class ConfigurableActionStructUpdater:
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)
56 def __get__(self, instance, objtype=None) -> None:
57 # This descriptor does not support fetching any value
58 return None
61class ConfigurableActionStructRemover:
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.
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)
83 def __get__(self, instance, objtype=None) -> None:
84 # This descriptor does not support fetching any value
85 return None
88class ConfigurableActionStruct:
89 """A ConfigurableActionStruct is the storage backend class that supports
90 the ConfigurableActionStructField. This class should not be created
91 directly.
93 This class allows managing a collection of `ConfigurableActions` with a
94 struct like interface, that is to say in an attribute like notation.
96 Attributes can be dynamically added or removed as such:
98 ConfigurableActionStructInstance.variable1 = a_configurable_action
99 del ConfigurableActionStructInstance.variable1
101 Each action is then available to be individually configured as a normal
102 `lsst.pex.config.Config` object.
104 ConfigurableActionStruct supports two special convenance attributes.
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.
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]
125 # create descriptors to handle special update and remove behavior
126 update = ConfigurableActionStructUpdater()
127 remove = ConfigurableActionStructRemover()
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', [])
136 self.history.append(("Struct initialized", at, label))
138 if value is not None:
139 for k, v in value.items():
140 setattr(self, k, v)
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_()
149 @property
150 def history(self) -> List[tuple]:
151 return self._history
153 @property
154 def fieldNames(self) -> Iterable[str]:
155 return self._attrs.keys()
157 def __setattr__(self, attr: str, value: Union[ConfigurableAction, Type[ConfigurableAction]],
158 at=None, label='setattr', setHistory=False) -> None:
160 if hasattr(self._config, '_frozen') and self._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, msg)
165 if attr not in (self.__dict__.keys() | type(self).__dict__.keys()):
166 name = _joinNamePath(self._config._name, self._field.name, attr)
167 if at is None:
168 at = getCallStack()
169 if isinstance(value, ConfigurableAction):
170 valueInst = type(value)(__name=name, __at=at, __label=label, **value._storage)
171 else:
172 valueInst = value(__name=name, __at=at, __label=label)
173 self._attrs[attr] = valueInst
174 else:
175 super().__setattr__(attr, value)
177 def __getattr__(self, attr):
178 if attr in object.__getattribute__(self, '_attrs'):
179 return self._attrs[attr]
180 else:
181 super().__getattribute__(attr)
183 def __delattr__(self, name):
184 if name in self._attrs:
185 del self._attrs[name]
186 else:
187 super().__delattr__(name)
189 def __iter__(self) -> Iterable[ConfigurableAction]:
190 return iter(self._attrs.values())
192 def items(self) -> Iterable[Tuple[str, ConfigurableAction]]:
193 return iter(self._attrs.items())
196T = TypeVar("T", bound="ConfigurableActionStructField")
199class ConfigurableActionStructField(Field):
200 r"""`ConfigurableActionStructField` is a `~lsst.pex.config.Field` subclass
201 that allows `ConfigurableAction`\ s to be organized in a
202 `~lsst.pex.config.Config` class in a manor similar to how a
203 `~lsst.pipe.base.Struct` works.
205 This class implements a `ConfigurableActionStruct` as an intermediary
206 object to organize the `ConfigurableActions`. See it's documentation for
207 futher information.
208 """
209 # specify StructClass to make this more generic for potential future
210 # inheritance
211 StructClass = ConfigurableActionStruct
213 # Explicitly annotate these on the class, they are present in the base
214 # class through injection, so type systems have trouble seeing them.
215 name: str
216 default: Optional[Mapping[str, ConfigurableAction]]
218 def __init__(self, doc: str, default: Optional[Mapping[str, ConfigurableAction]] = None,
219 optional: bool = False,
220 deprecated=None):
221 source = getStackFrame()
222 self._setup(doc=doc, dtype=self.__class__, default=default, check=None,
223 optional=optional, source=source, deprecated=deprecated)
225 def __set__(self, instance: Config,
226 value: Union[None, Mapping[str, ConfigurableAction], ConfigurableActionStruct],
227 at: Iterable[StackFrame] = None, label: str = 'assigment'):
228 if instance._frozen:
229 msg = "Cannot modify a frozen Config. "\
230 "Attempting to set field to value %s" % value
231 raise FieldValidationError(self, instance, msg)
233 if at is None:
234 at = getCallStack()
236 if value is None or value == self.default:
237 value = self.StructClass(instance, self, value, at=at, label=label)
238 else:
239 history = instance._history.setdefault(self.name, [])
240 history.append((value, at, label))
242 if not isinstance(value, ConfigurableActionStruct):
243 raise FieldValidationError(self, instance,
244 "Can only assign things that are subclasses of Configurable Action")
245 instance._storage[self.name] = value
247 def __get__(self: T, instance: Config, owner: None = None, at: Iterable[StackFrame] = None,
248 label: str = "default"
249 ) -> Union[None, T, ConfigurableActionStruct]:
250 if instance is None or not isinstance(instance, Config):
251 return self
252 else:
253 field: Optional[ConfigurableActionStruct] = instance._storage[self.name]
254 return field
256 def rename(self, instance: Config):
257 actionStruct: ConfigurableActionStruct = self.__get__(instance)
258 if actionStruct is not None:
259 for k, v in actionStruct.items():
260 fullname = _joinNamePath(instance._name, self.name, k)
261 v._rename(fullname)
263 def validate(self, instance):
264 value = self.__get__(instance)
265 if value is not None:
266 for item in value:
267 item.validate()
269 def toDict(self, instance):
270 actionStruct = self.__get__(instance)
271 if actionStruct is None:
272 return None
274 dict_ = {k: v.toDict() for k, v in actionStruct.items()}
276 return dict_
278 def save(self, outfile, instance):
279 actionStruct = self.__get__(instance)
280 fullname = _joinNamePath(instance._name, self.name)
281 if actionStruct is None:
282 outfile.write(u"{}={!r}\n".format(fullname, actionStruct))
283 return
285 outfile.write(u"{}={!r}\n".format(fullname, {}))
286 for v in actionStruct:
287 outfile.write(u"{}={}()\n".format(v._name, _typeStr(v)))
288 v._save(outfile)
290 def freeze(self, instance):
291 actionStruct = self.__get__(instance)
292 if actionStruct is not None:
293 for v in actionStruct:
294 v.freeze()
296 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
297 """Compare two fields for equality.
299 Parameters
300 ----------
301 instance1 : `lsst.pex.config.Config`
302 Left-hand side config instance to compare.
303 instance2 : `lsst.pex.config.Config`
304 Right-hand side config instance to compare.
305 shortcut : `bool`
306 If `True`, this function returns as soon as an inequality if found.
307 rtol : `float`
308 Relative tolerance for floating point comparisons.
309 atol : `float`
310 Absolute tolerance for floating point comparisons.
311 output : callable
312 A callable that takes a string, used (possibly repeatedly) to
313 report inequalities.
315 Returns
316 -------
317 isEqual : bool
318 `True` if the fields are equal, `False` otherwise.
320 Notes
321 -----
322 Floating point comparisons are performed by `numpy.allclose`.
323 """
324 d1: ConfigurableActionStruct = getattr(instance1, self.name)
325 d2: ConfigurableActionStruct = getattr(instance2, self.name)
326 name = getComparisonName(
327 _joinNamePath(instance1._name, self.name),
328 _joinNamePath(instance2._name, self.name)
329 )
330 if not compareScalars(f"keys for {name}", set(d1.fieldNames), set(d2.fieldNames), output=output):
331 return False
332 equal = True
333 for k, v1 in d1.items():
334 v2 = getattr(d2, k)
335 result = compareConfigs(f"{name}.{k}", v1, v2, shortcut=shortcut,
336 rtol=rtol, atol=atol, output=output)
337 if not result and shortcut:
338 return False
339 equal = equal and result
340 return equal