Coverage for python/lsst/pipe/tasks/configurableActions/_configurableActionStructField.py : 21%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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
34class ConfigurableActionStructUpdater:
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)
54 def __get__(self, instance, objtype=None) -> None:
55 # This descriptor does not support fetching any value
56 return None
59class ConfigurableActionStructRemover:
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.
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)
81 def __get__(self, instance, objtype=None) -> None:
82 # This descriptor does not support fetching any value
83 return None
86class ConfigurableActionStruct:
87 """A ConfigurableActionStruct is the storage backend class that supports
88 the ConfigurableActionStructField. This class should not be created
89 directly.
91 This class allows managing a collection of `ConfigurableActions` with a
92 struct like interface, that is to say in an attribute like notation.
94 Attributes can be dynamically added or removed as such:
96 ConfigurableActionStructInstance.variable1 = a_configurable_action
97 del ConfigurableActionStructInstance.variable1
99 Each action is then available to be individually configured as a normal
100 `lsst.pex.config.Config` object.
102 ConfigurableActionStruct supports two special convenance attributes.
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.
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]
123 # create descriptors to handle special update and remove behavior
124 update = ConfigurableActionStructUpdater()
125 remove = ConfigurableActionStructRemover()
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', [])
134 self.history.append(("Struct initialized", at, label))
136 if value is not None:
137 for k, v in value.items():
138 setattr(self, k, v)
140 @property
141 def history(self) -> List[tuple]:
142 return self._history
144 @property
145 def fieldNames(self) -> Iterable[str]:
146 return self._attrs.keys()
148 def __setattr__(self, attr: str, value: Union[ConfigurableAction, Type[ConfigurableAction]],
149 at=None, label='setattr', setHistory=False) -> None:
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)
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)
168 def __getattr__(self, attr):
169 if attr in object.__getattribute__(self, '_attrs'):
170 return self._attrs[attr]
171 else:
172 super().__getattribute__(attr)
174 def __delattr__(self, name):
175 if name in self._attrs:
176 del self._attrs[name]
177 else:
178 super().__delattr__(name)
180 def __iter__(self) -> Iterable[ConfigurableAction]:
181 return iter(self._attrs.values())
183 def items(self) -> Iterable[Tuple[str, ConfigurableAction]]:
184 return iter(self._attrs.items())
187T = TypeVar("T", bound="ConfigurableActionStructField")
190class ConfigurableActionStructField(Field):
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.
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
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]]
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)
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)
224 if at is None:
225 at = getCallStack()
227 if value is None or value == self.default:
228 value = self.StructClass(instance, self, value, at=at, label=label)
229 else:
230 history = instance._history.setdefault(self.name, [])
231 history.append((value, at, label))
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
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
247 def rename(self, instance: Config):
248 actionStruct: ConfigurableActionStruct = self.__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)
254 def validate(self, instance):
255 value = self.__get__(instance)
256 if value is not None:
257 for item in value:
258 item.validate()
260 def toDict(self, instance):
261 actionStruct = self.__get__(instance)
262 if actionStruct is None:
263 return None
265 dict_ = {k: v.toDict() for k, v in actionStruct.items()}
267 return dict_
269 def save(self, outfile, instance):
270 actionStruct = self.__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
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)
281 def freeze(self, instance):
282 actionStruct = self.__get__(instance)
283 if actionStruct is not None:
284 for v in actionStruct:
285 v.freeze()
287 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
288 """Compare two fields for equality.
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.
306 Returns
307 -------
308 isEqual : bool
309 `True` if the fields are equal, `False` otherwise.
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