Coverage for python/lsst/pex/config/configurableActions/_configurableActionStructField.py: 22%
182 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-04 21:14 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-04 21:14 +0000
1# This file is part of pex_config.
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")
25import weakref
26from collections.abc import Iterable, Iterator, Mapping
27from types import GenericAlias, SimpleNamespace
28from typing import Any, Generic, TypeVar, overload
30from lsst.pex.config.callStack import StackFrame, getCallStack, getStackFrame
31from lsst.pex.config.comparison import compareConfigs, compareScalars, getComparisonName
32from lsst.pex.config.config import Config, Field, FieldValidationError, _joinNamePath, _typeStr
34from . import ActionTypeVar, ConfigurableAction
37class ConfigurableActionStructUpdater:
38 """Abstract the logic of using a dictionary to update a
39 `ConfigurableActionStruct` through attribute assignment.
41 This is useful in the context of setting configuration through pipelines
42 or on the command line.
43 """
45 def __set__(
46 self,
47 instance: ConfigurableActionStruct,
48 value: Mapping[str, ConfigurableAction] | ConfigurableActionStruct,
49 ) -> None:
50 if isinstance(value, Mapping):
51 pass
52 elif isinstance(value, ConfigurableActionStruct):
53 # If the update target is a ConfigurableActionStruct, get the
54 # internal dictionary
55 value = value._attrs
56 else:
57 raise ValueError(
58 "Can only update a ConfigurableActionStruct with an instance of such, or a " "mapping"
59 )
60 for name, action in value.items():
61 setattr(instance, name, action)
63 def __get__(self, instance, objtype=None) -> None:
64 # This descriptor does not support fetching any value
65 return None
68class ConfigurableActionStructRemover:
69 """Abstract the logic of removing an iterable of action names from a
70 `ConfigurableActionStruct` at one time using attribute assignment.
72 This is useful in the context of setting configuration through pipelines
73 or on the command line.
75 Raises
76 ------
77 AttributeError
78 Raised if an attribute specified for removal does not exist in the
79 ConfigurableActionStruct
80 """
82 def __set__(self, instance: ConfigurableActionStruct, value: str | Iterable[str]) -> None:
83 # strings are iterable, but not in the way that is intended. If a
84 # single name is specified, turn it into a tuple before attempting
85 # to remove the attribute
86 if isinstance(value, str):
87 value = (value,)
88 for name in value:
89 delattr(instance, name)
91 def __get__(self, instance, objtype=None) -> None:
92 # This descriptor does not support fetching any value
93 return None
96class ConfigurableActionStruct(Generic[ActionTypeVar]):
97 """A ConfigurableActionStruct is the storage backend class that supports
98 the ConfigurableActionStructField. This class should not be created
99 directly.
101 This class allows managing a collection of `ConfigurableAction` with a
102 struct like interface, that is to say in an attribute like notation.
104 Attributes can be dynamically added or removed as such:
106 .. code-block:: python
108 ConfigurableActionStructInstance.variable1 = a_configurable_action
109 del ConfigurableActionStructInstance.variable1
111 Each action is then available to be individually configured as a normal
112 `lsst.pex.config.Config` object.
114 `ConfigurableActionStruct` supports two special convenience attributes.
116 The first is ``update``. You may assign a dict of `ConfigurableAction` or a
117 `ConfigurableActionStruct` to this attribute which will update the
118 `ConfigurableActionStruct` on which the attribute is invoked such that it
119 will be updated to contain the entries specified by the structure on the
120 right hand side of the equals sign.
122 The second convenience attribute is named ``remove``. You may assign an
123 iterable of strings which correspond to attribute names on the
124 `ConfigurableActionStruct`. All of the corresponding attributes will then
125 be removed. If any attribute does not exist, an `AttributeError` will be
126 raised. Any attributes in the Iterable prior to the name which raises will
127 have been removed from the `ConfigurableActionStruct`
128 """
130 # declare attributes that are set with __setattr__
131 _config_: weakref.ref
132 _attrs: dict[str, ActionTypeVar]
133 _field: ConfigurableActionStructField
134 _history: list[tuple]
136 # create descriptors to handle special update and remove behavior
137 update = ConfigurableActionStructUpdater()
138 remove = ConfigurableActionStructRemover()
140 def __init__(
141 self,
142 config: Config,
143 field: ConfigurableActionStructField,
144 value: Mapping[str, ConfigurableAction],
145 at: Any,
146 label: str,
147 ):
148 object.__setattr__(self, "_config_", weakref.ref(config))
149 object.__setattr__(self, "_attrs", {})
150 object.__setattr__(self, "_field", field)
151 object.__setattr__(self, "_history", [])
153 self.history.append(("Struct initialized", at, label))
155 if value is not None:
156 for k, v in value.items():
157 setattr(self, k, v)
159 @property
160 def _config(self) -> Config:
161 # Config Fields should never outlive their config class instance
162 # assert that as such here
163 value = self._config_()
164 assert value is not None
165 return value
167 @property
168 def history(self) -> list[tuple]:
169 return self._history
171 @property
172 def fieldNames(self) -> Iterable[str]:
173 return self._attrs.keys()
175 def __setattr__(
176 self,
177 attr: str,
178 value: ActionTypeVar | type[ActionTypeVar],
179 at=None,
180 label="setattr",
181 setHistory=False,
182 ) -> None:
183 if hasattr(self._config, "_frozen") and self._config._frozen:
184 msg = "Cannot modify a frozen Config. " f"Attempting to set item {attr} to value {value}"
185 raise FieldValidationError(self._field, self._config, msg)
187 # verify that someone has not passed a string with a space or leading
188 # number or something through the dict assignment update interface
189 if not attr.isidentifier():
190 raise ValueError("Names used in ConfigurableStructs must be valid as python variable names")
192 if attr not in (self.__dict__.keys() | type(self).__dict__.keys()):
193 base_name = _joinNamePath(self._config._name, self._field.name)
194 name = _joinNamePath(base_name, attr)
195 if at is None:
196 at = getCallStack()
197 if isinstance(value, ConfigurableAction):
198 valueInst = type(value)(__name=name, __at=at, __label=label, **value._storage)
199 else:
200 valueInst = value(__name=name, __at=at, __label=label)
201 self._attrs[attr] = valueInst
202 else:
203 super().__setattr__(attr, value)
205 def __getattr__(self, attr) -> Any:
206 if attr in object.__getattribute__(self, "_attrs"):
207 result = self._attrs[attr]
208 result.identity = attr
209 return result
210 else:
211 super().__getattribute__(attr)
213 def __delattr__(self, name):
214 if name in self._attrs:
215 del self._attrs[name]
216 else:
217 super().__delattr__(name)
219 def __iter__(self) -> Iterator[ActionTypeVar]:
220 for name in self.fieldNames:
221 yield getattr(self, name)
223 def items(self) -> Iterable[tuple[str, ActionTypeVar]]:
224 for name in self.fieldNames:
225 yield name, getattr(self, name)
227 def __bool__(self) -> bool:
228 return bool(self._attrs)
231T = TypeVar("T", bound="ConfigurableActionStructField")
234class ConfigurableActionStructField(Field[ActionTypeVar]):
235 r"""`ConfigurableActionStructField` is a `~lsst.pex.config.Field` subclass
236 that allows `ConfigurableAction`\ s to be organized in a
237 `~lsst.pex.config.Config` class in a manner similar to how a
238 `~lsst.pipe.base.Struct` works.
240 This class uses a `ConfigurableActionStruct` as an intermediary object to
241 organize the `ConfigurableAction`. See its documentation for further
242 information.
243 """
245 # specify StructClass to make this more generic for potential future
246 # inheritance
247 StructClass = ConfigurableActionStruct
249 # Explicitly annotate these on the class, they are present in the base
250 # class through injection, so type systems have trouble seeing them.
251 name: str
252 default: Mapping[str, ConfigurableAction] | None
254 def __init__(
255 self,
256 doc: str,
257 default: Mapping[str, ConfigurableAction] | None = None,
258 optional: bool = False,
259 deprecated=None,
260 ):
261 source = getStackFrame()
262 self._setup(
263 doc=doc,
264 dtype=self.__class__,
265 default=default,
266 check=None,
267 optional=optional,
268 source=source,
269 deprecated=deprecated,
270 )
272 def __class_getitem__(cls, params):
273 return GenericAlias(cls, params)
275 def __set__(
276 self,
277 instance: Config,
278 value: (
279 None
280 | Mapping[str, ConfigurableAction]
281 | SimpleNamespace
282 | ConfigurableActionStruct
283 | ConfigurableActionStructField
284 | type[ConfigurableActionStructField]
285 ),
286 at: Iterable[StackFrame] = None,
287 label: str = "assigment",
288 ):
289 if instance._frozen:
290 msg = "Cannot modify a frozen Config. " "Attempting to set field to value %s" % value
291 raise FieldValidationError(self, instance, msg)
293 if at is None:
294 at = getCallStack()
296 if value is None or (self.default is not None and self.default == value):
297 value = self.StructClass(instance, self, value, at=at, label=label)
298 else:
299 # An actual value is being assigned check for what it is
300 if isinstance(value, self.StructClass):
301 # If this is a ConfigurableActionStruct, we need to make our
302 # own copy that references this current field
303 value = self.StructClass(instance, self, value._attrs, at=at, label=label)
304 elif isinstance(value, SimpleNamespace):
305 # If this is a a python analogous container, we need to make
306 # a ConfigurableActionStruct initialized with this data
307 value = self.StructClass(instance, self, vars(value), at=at, label=label)
309 elif type(value) == ConfigurableActionStructField:
310 raise ValueError(
311 "ConfigurableActionStructFields can only be used in a class body declaration"
312 f"Use a {self.StructClass}, SimpleNamespace or Struct"
313 )
314 else:
315 raise ValueError(f"Unrecognized value {value}, cannot be assigned to this field")
317 history = instance._history.setdefault(self.name, [])
318 history.append((value, at, label))
320 if not isinstance(value, ConfigurableActionStruct):
321 raise FieldValidationError(
322 self, instance, "Can only assign things that are subclasses of Configurable Action"
323 )
324 instance._storage[self.name] = value
326 @overload
327 def __get__(
328 self, instance: None, owner: Any = None, at: Any = None, label: str = "default"
329 ) -> ConfigurableActionStruct[ActionTypeVar]:
330 ...
332 @overload
333 def __get__(
334 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default"
335 ) -> ConfigurableActionStruct[ActionTypeVar]:
336 ...
338 def __get__(self, instance, owner=None, at=None, label="default"):
339 if instance is None or not isinstance(instance, Config):
340 return self
341 else:
342 field: ConfigurableActionStruct | None = instance._storage[self.name]
343 return field
345 def rename(self, instance: Config):
346 actionStruct: ConfigurableActionStruct = self.__get__(instance)
347 if actionStruct is not None:
348 for k, v in actionStruct.items():
349 base_name = _joinNamePath(instance._name, self.name)
350 fullname = _joinNamePath(base_name, k)
351 v._rename(fullname)
353 def validate(self, instance: Config):
354 value = self.__get__(instance)
355 if value is not None:
356 for item in value:
357 item.validate()
359 def toDict(self, instance):
360 actionStruct = self.__get__(instance)
361 if actionStruct is None:
362 return None
364 dict_ = {k: v.toDict() for k, v in actionStruct.items()}
366 return dict_
368 def save(self, outfile, instance):
369 actionStruct = self.__get__(instance)
370 fullname = _joinNamePath(instance._name, self.name)
372 # Ensure that a struct is always empty before assigning to it.
373 outfile.write(f"{fullname}=None\n")
375 if actionStruct is None:
376 return
378 for _, v in sorted(actionStruct.items()):
379 outfile.write(f"{v._name}={_typeStr(v)}()\n")
380 v._save(outfile)
382 def freeze(self, instance):
383 actionStruct = self.__get__(instance)
384 if actionStruct is not None:
385 for v in actionStruct:
386 v.freeze()
388 def _collectImports(self, instance, imports):
389 # docstring inherited from Field
390 actionStruct = self.__get__(instance)
391 for v in actionStruct:
392 v._collectImports()
393 imports |= v._imports
395 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
396 """Compare two fields for equality.
398 Parameters
399 ----------
400 instance1 : `lsst.pex.config.Config`
401 Left-hand side config instance to compare.
402 instance2 : `lsst.pex.config.Config`
403 Right-hand side config instance to compare.
404 shortcut : `bool`
405 If `True`, this function returns as soon as an inequality if found.
406 rtol : `float`
407 Relative tolerance for floating point comparisons.
408 atol : `float`
409 Absolute tolerance for floating point comparisons.
410 output : callable
411 A callable that takes a string, used (possibly repeatedly) to
412 report inequalities.
414 Returns
415 -------
416 isEqual : bool
417 `True` if the fields are equal, `False` otherwise.
419 Notes
420 -----
421 Floating point comparisons are performed by `numpy.allclose`.
422 """
423 d1: ConfigurableActionStruct = getattr(instance1, self.name)
424 d2: ConfigurableActionStruct = getattr(instance2, self.name)
425 name = getComparisonName(
426 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
427 )
428 if not compareScalars(f"keys for {name}", set(d1.fieldNames), set(d2.fieldNames), output=output):
429 return False
430 equal = True
431 for k, v1 in d1.items():
432 v2 = getattr(d2, k)
433 result = compareConfigs(
434 f"{name}.{k}", v1, v2, shortcut=shortcut, rtol=rtol, atol=atol, output=output
435 )
436 if not result and shortcut:
437 return False
438 equal = equal and result
439 return equal