Coverage for python/lsst/pipe/tasks/configurableActions/_configurableActionStructField.py: 23%
167 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-05 12:15 +0000
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-05 12:15 +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 types import SimpleNamespace
26from typing import Iterable, Mapping, Optional, TypeVar, Union, Type, Tuple, List, Any, Dict
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
33from . import ConfigurableAction
35import weakref
38class ConfigurableActionStructUpdater:
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)
58 def __get__(self, instance, objtype=None) -> None:
59 # This descriptor does not support fetching any value
60 return None
63class ConfigurableActionStructRemover:
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.
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)
85 def __get__(self, instance, objtype=None) -> None:
86 # This descriptor does not support fetching any value
87 return None
90class ConfigurableActionStruct:
91 """A ConfigurableActionStruct is the storage backend class that supports
92 the ConfigurableActionStructField. This class should not be created
93 directly.
95 This class allows managing a collection of `ConfigurableActions` with a
96 struct like interface, that is to say in an attribute like notation.
98 Attributes can be dynamically added or removed as such:
100 ConfigurableActionStructInstance.variable1 = a_configurable_action
101 del ConfigurableActionStructInstance.variable1
103 Each action is then available to be individually configured as a normal
104 `lsst.pex.config.Config` object.
106 ConfigurableActionStruct supports two special convenance attributes.
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.
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]
127 # create descriptors to handle special update and remove behavior
128 update = ConfigurableActionStructUpdater()
129 remove = ConfigurableActionStructRemover()
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', [])
138 self.history.append(("Struct initialized", at, label))
140 if value is not None:
141 for k, v in value.items():
142 setattr(self, k, v)
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_()
151 @property
152 def history(self) -> List[tuple]:
153 return self._history
155 @property
156 def fieldNames(self) -> Iterable[str]:
157 return self._attrs.keys()
159 def __setattr__(self, attr: str, value: Union[ConfigurableAction, Type[ConfigurableAction]],
160 at=None, label='setattr', setHistory=False) -> None:
162 if hasattr(self._config, '_frozen') and self._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, msg)
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")
172 if attr not in (self.__dict__.keys() | type(self).__dict__.keys()):
173 base_name = _joinNamePath(self._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)
185 def __getattr__(self, attr):
186 if attr in object.__getattribute__(self, '_attrs'):
187 return self._attrs[attr]
188 else:
189 super().__getattribute__(attr)
191 def __delattr__(self, name):
192 if name in self._attrs:
193 del self._attrs[name]
194 else:
195 super().__delattr__(name)
197 def __iter__(self) -> Iterable[ConfigurableAction]:
198 return iter(self._attrs.values())
200 def items(self) -> Iterable[Tuple[str, ConfigurableAction]]:
201 return iter(self._attrs.items())
204T = TypeVar("T", bound="ConfigurableActionStructField")
207class ConfigurableActionStructField(Field):
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.
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
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]]
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)
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)
244 if at is None:
245 at = getCallStack()
247 if value is None or (self.default is not None and self.default == value):
248 value = self.StructClass(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.StructClass):
252 # If this is a ConfigurableActionStruct, we need to make our own
253 # copy that references this current field
254 value = self.StructClass(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.StructClass(instance, self, vars(value), at=at, label=label)
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")
266 history = instance._history.setdefault(self.name, [])
267 history.append((value, at, label))
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
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
283 def rename(self, instance: Config):
284 actionStruct: ConfigurableActionStruct = self.__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)
291 def validate(self, instance):
292 value = self.__get__(instance)
293 if value is not None:
294 for item in value:
295 item.validate()
297 def toDict(self, instance):
298 actionStruct = self.__get__(instance)
299 if actionStruct is None:
300 return None
302 dict_ = {k: v.toDict() for k, v in actionStruct.items()}
304 return dict_
306 def save(self, outfile, instance):
307 actionStruct = self.__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
313 for v in actionStruct:
314 outfile.write(u"{}={}()\n".format(v._name, _typeStr(v)))
315 v._save(outfile)
317 def freeze(self, instance):
318 actionStruct = self.__get__(instance)
319 if actionStruct is not None:
320 for v in actionStruct:
321 v.freeze()
323 def _collectImports(self, instance, imports):
324 # docstring inherited from Field
325 actionStruct = self.__get__(instance)
326 for v in actionStruct:
327 v._collectImports()
328 imports |= v._imports
330 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
331 """Compare two fields for equality.
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.
349 Returns
350 -------
351 isEqual : bool
352 `True` if the fields are equal, `False` otherwise.
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