LSST Applications g0aad566f14+98bdd3b178,g0dd44d6229+e0c2448475,g2079a07aa2+86d27d4dc4,g2305ad1205+28192cebb5,g2bbee38e9b+047b288a59,g337abbeb29+047b288a59,g33d1c0ed96+047b288a59,g3a166c0a6a+047b288a59,g3d1719c13e+98bdd3b178,g487adcacf7+08349b74a1,g4be5004598+98bdd3b178,g50ff169b8f+96c6868917,g52b1c1532d+585e252eca,g591dd9f2cf+742623b69a,g63cd9335cc+585e252eca,g858d7b2824+98bdd3b178,g88963caddf+0cb8e002cc,g88c60e1ba4+10d1129650,g99cad8db69+26979ce5a5,g9ddcbc5298+9a081db1e4,ga1e77700b3+a912195c07,gae0086650b+585e252eca,gb0e22166c9+60f28cb32d,gb1fda7adca+90d62c6f66,gb3a676b8dc+b4feba26a1,gb4b16eec92+950562e714,gba4ed39666+c2a2e4ac27,gbb8dafda3b+2fe9a6a885,gc120e1dc64+0553034b2f,gc28159a63d+047b288a59,gc3e9b769f7+f8a3f20ab6,gcf0d15dbbd+e0c2448475,gdaeeff99f8+f9a426f77a,gdb0af172c8+f61b8374e1,ge79ae78c31+047b288a59,w.2024.20
LSST Data Management Base Package
Loading...
Searching...
No Matches
_configurableActionStructField.py
Go to the documentation of this file.
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
22
23__all__ = ("ConfigurableActionStructField", "ConfigurableActionStruct")
24
25import weakref
26from collections.abc import Iterable, Iterator, Mapping
27from types import GenericAlias, SimpleNamespace
28from typing import Any, Generic, TypeVar, overload
29
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
33
34from . import ActionTypeVar, ConfigurableAction
35
36
38 """Abstract the logic of using a dictionary to update a
39 `ConfigurableActionStruct` through attribute assignment.
40
41 This is useful in the context of setting configuration through pipelines
42 or on the command line.
43 """
44
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)
62
63 def __get__(self, instance, objtype=None) -> None:
64 # This descriptor does not support fetching any value
65 return None
66
67
69 """Abstract the logic of removing an iterable of action names from a
70 `ConfigurableActionStruct` at one time using attribute assignment.
71
72 This is useful in the context of setting configuration through pipelines
73 or on the command line.
74
75 Raises
76 ------
77 AttributeError
78 Raised if an attribute specified for removal does not exist in the
79 ConfigurableActionStruct
80 """
81
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)
90
91 def __get__(self, instance, objtype=None) -> None:
92 # This descriptor does not support fetching any value
93 return None
94
95
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.
100
101 This class allows managing a collection of `ConfigurableAction` with a
102 struct like interface, that is to say in an attribute like notation.
103
104 Parameters
105 ----------
106 config : `~lsst.pex.config.Config`
107 Config to use.
108 field : `ConfigurableActionStructField`
109 Field to use.
110 value : `~collections.abc.Mapping` [`str`, `ConfigurableAction`]
111 Value to assign.
112 at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`, optional
113 Stack frames to use for history recording.
114 label : `str`, optional
115 Label to use for history recording.
116
117 Notes
118 -----
119 Attributes can be dynamically added or removed as such:
120
121 .. code-block:: python
122
123 ConfigurableActionStructInstance.variable1 = a_configurable_action
124 del ConfigurableActionStructInstance.variable1
125
126 Each action is then available to be individually configured as a normal
127 `lsst.pex.config.Config` object.
128
129 `ConfigurableActionStruct` supports two special convenience attributes.
130
131 The first is ``update``. You may assign a dict of `ConfigurableAction` or a
132 `ConfigurableActionStruct` to this attribute which will update the
133 `ConfigurableActionStruct` on which the attribute is invoked such that it
134 will be updated to contain the entries specified by the structure on the
135 right hand side of the equals sign.
136
137 The second convenience attribute is named ``remove``. You may assign an
138 iterable of strings which correspond to attribute names on the
139 `ConfigurableActionStruct`. All of the corresponding attributes will then
140 be removed. If any attribute does not exist, an `AttributeError` will be
141 raised. Any attributes in the Iterable prior to the name which raises will
142 have been removed from the `ConfigurableActionStruct`
143 """
144
145 # declare attributes that are set with __setattr__
146 _config_: weakref.ref
147 _attrs: dict[str, ActionTypeVar]
148 _field: ConfigurableActionStructField
149 _history: list[tuple]
150
151 # create descriptors to handle special update and remove behavior
154
156 self,
157 config: Config,
158 field: ConfigurableActionStructField,
159 value: Mapping[str, ConfigurableAction],
160 at: Any,
161 label: str,
162 ):
163 object.__setattr__(self, "_config_", weakref.ref(config))
164 object.__setattr__(self, "_attrs", {})
165 object.__setattr__(self, "_field", field)
166 object.__setattr__(self, "_history", [])
167
168 self.history.append(("Struct initialized", at, label))
169
170 if value is not None:
171 for k, v in value.items():
172 setattr(self, k, v)
173
174 @property
175 def _config(self) -> Config:
176 # Config Fields should never outlive their config class instance
177 # assert that as such here
178 value = self._config_()
179 assert value is not None
180 return value
181
182 @property
183 def history(self) -> list[tuple]:
184 return self._history
185
186 @property
187 def fieldNames(self) -> Iterable[str]:
188 return self._attrs_attrs.keys()
189
191 self,
192 attr: str,
193 value: ActionTypeVar | type[ActionTypeVar],
194 at=None,
195 label="setattr",
196 setHistory=False,
197 ) -> None:
198 if hasattr(self._config_config, "_frozen") and self._config_config._frozen:
199 msg = "Cannot modify a frozen Config. " f"Attempting to set item {attr} to value {value}"
201
202 # verify that someone has not passed a string with a space or leading
203 # number or something through the dict assignment update interface
204 if not attr.isidentifier():
205 raise ValueError("Names used in ConfigurableStructs must be valid as python variable names")
206
207 if attr not in (self.__dict__.keys() | type(self).__dict__.keys()):
208 base_name = _joinNamePath(self._config_config._name, self._field_field.name)
209 name = _joinNamePath(base_name, attr)
210 if at is None:
211 at = getCallStack()
212 if isinstance(value, ConfigurableAction):
213 valueInst = type(value)(__name=name, __at=at, __label=label, **value._storage)
214 else:
215 valueInst = value(__name=name, __at=at, __label=label)
216 self._attrs_attrs[attr] = valueInst
217 else:
218 super().__setattr__(attr, value)
219
220 def __getattr__(self, attr) -> Any:
221 if attr in object.__getattribute__(self, "_attrs"):
222 result = self._attrs_attrs[attr]
223 result.identity = attr
224 return result
225 else:
226 super().__getattribute__(attr)
227
228 def __delattr__(self, name):
229 if name in self._attrs_attrs:
230 del self._attrs_attrs[name]
231 else:
232 super().__delattr__(name)
233
234 def __iter__(self) -> Iterator[ActionTypeVar]:
235 for name in self.fieldNames:
236 yield getattr(self, name)
237
238 def items(self) -> Iterable[tuple[str, ActionTypeVar]]:
239 for name in self.fieldNames:
240 yield name, getattr(self, name)
241
242 def __bool__(self) -> bool:
243 return bool(self._attrs_attrs)
244
245
246T = TypeVar("T", bound="ConfigurableActionStructField")
247
248
250 """`ConfigurableActionStructField` is a `~lsst.pex.config.Field` subclass
251 that allows a `ConfigurableAction` to be organized in a
252 `~lsst.pex.config.Config` class in a manner similar to how a
253 `~lsst.pipe.base.Struct` works.
254
255 This class uses a `ConfigurableActionStruct` as an intermediary object to
256 organize the `ConfigurableAction`. See its documentation for further
257 information.
258
259 Parameters
260 ----------
261 doc : `str`
262 Documentation string.
263 default : `~collections.abc.Mapping` [ `str`, `ConfigurableAction` ] \
264 or `None`, optional
265 Default value.
266 optional : `bool`, optional
267 If `True`, the field doesn't need to have a set value.
268 deprecated : `bool` or `None`, optional
269 A description of why this Field is deprecated, including removal date.
270 If not `None`, the string is appended to the docstring for this Field.
271 """
272
273 # specify StructClass to make this more generic for potential future
274 # inheritance
275 StructClass = ConfigurableActionStruct
276
277 # Explicitly annotate these on the class, they are present in the base
278 # class through injection, so type systems have trouble seeing them.
279 name: str
280 default: Mapping[str, ConfigurableAction] | None
281
283 self,
284 doc: str,
285 default: Mapping[str, ConfigurableAction] | None = None,
286 optional: bool = False,
287 deprecated=None,
288 ):
289 source = getStackFrame()
290 self._setup(
291 doc=doc,
292 dtype=self.__class__,
293 default=default,
294 check=None,
295 optional=optional,
296 source=source,
297 deprecated=deprecated,
298 )
299
300 def __class_getitem__(cls, params):
301 return GenericAlias(cls, params)
302
304 self,
305 instance: Config,
306 value: (
307 None
308 | Mapping[str, ConfigurableAction]
309 | SimpleNamespace
310 | ConfigurableActionStruct
311 | ConfigurableActionStructField
312 | type[ConfigurableActionStructField]
313 ),
314 at: Iterable[StackFrame] = None,
315 label: str = "assigment",
316 ):
317 if instance._frozen:
318 msg = "Cannot modify a frozen Config. " "Attempting to set field to value %s" % value
319 raise FieldValidationError(self, instance, msg)
320
321 if at is None:
322 at = getCallStack()
323
324 if value is None or (self.defaultdefaultdefault is not None and self.defaultdefaultdefault == value):
325 value = self.StructClass(instance, self, value, at=at, label=label)
326 else:
327 # An actual value is being assigned check for what it is
328 if isinstance(value, self.StructClass):
329 # If this is a ConfigurableActionStruct, we need to make our
330 # own copy that references this current field
331 value = self.StructClass(instance, self, value._attrs, at=at, label=label)
332 elif isinstance(value, SimpleNamespace):
333 # If this is a a python analogous container, we need to make
334 # a ConfigurableActionStruct initialized with this data
335 value = self.StructClass(instance, self, vars(value), at=at, label=label)
336
337 elif type(value) is ConfigurableActionStructField:
338 raise ValueError(
339 "ConfigurableActionStructFields can only be used in a class body declaration"
340 f"Use a {self.StructClass}, SimpleNamespace or Struct"
341 )
342 else:
343 raise ValueError(f"Unrecognized value {value}, cannot be assigned to this field")
344
345 history = instance._history.setdefault(self.namenamenamename, [])
346 history.append((value, at, label))
347
348 if not isinstance(value, ConfigurableActionStruct):
350 self, instance, "Can only assign things that are subclasses of Configurable Action"
351 )
352 instance._storage[self.namenamenamename] = value
353
354 @overload
356 self, instance: None, owner: Any = None, at: Any = None, label: str = "default"
357 ) -> ConfigurableActionStruct[ActionTypeVar]:
358 ...
359
360 @overload
362 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default"
363 ) -> ConfigurableActionStruct[ActionTypeVar]:
364 ...
365
366 def __get__(self, instance, owner=None, at=None, label="default"):
367 if instance is None or not isinstance(instance, Config):
368 return self
369 else:
370 field: ConfigurableActionStruct | None = instance._storage[self.namenamenamename]
371 return field
372
373 def rename(self, instance: Config):
374 actionStruct: ConfigurableActionStruct = self.__get____get____get____get____get____get__(instance)
375 if actionStruct is not None:
376 for k, v in actionStruct.items():
377 base_name = _joinNamePath(instance._name, self.namenamenamename)
378 fullname = _joinNamePath(base_name, k)
379 v._rename(fullname)
380
381 def validate(self, instance: Config):
382 value = self.__get____get____get____get____get____get__(instance)
383 if value is not None:
384 for item in value:
385 item.validate()
386
387 def toDict(self, instance):
388 actionStruct = self.__get____get____get____get____get____get__(instance)
389 if actionStruct is None:
390 return None
391
392 dict_ = {k: v.toDict() for k, v in actionStruct.items()}
393
394 return dict_
395
396 def save(self, outfile, instance):
397 actionStruct = self.__get____get____get____get____get____get__(instance)
398 fullname = _joinNamePath(instance._name, self.namenamenamename)
399
400 # Ensure that a struct is always empty before assigning to it.
401 outfile.write(f"{fullname}=None\n")
402
403 if actionStruct is None:
404 return
405
406 for _, v in sorted(actionStruct.items()):
407 outfile.write(f"{v._name}={_typeStr(v)}()\n")
408 v._save(outfile)
409
410 def freeze(self, instance):
411 actionStruct = self.__get____get____get____get____get____get__(instance)
412 if actionStruct is not None:
413 for v in actionStruct:
414 v.freeze()
415
416 def _collectImports(self, instance, imports):
417 # docstring inherited from Field
418 actionStruct = self.__get____get____get____get____get____get__(instance)
419 for v in actionStruct:
420 v._collectImports()
421 imports |= v._imports
422
423 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
424 """Compare two fields for equality.
425
426 Parameters
427 ----------
428 instance1 : `lsst.pex.config.Config`
429 Left-hand side config instance to compare.
430 instance2 : `lsst.pex.config.Config`
431 Right-hand side config instance to compare.
432 shortcut : `bool`
433 If `True`, this function returns as soon as an inequality if found.
434 rtol : `float`
435 Relative tolerance for floating point comparisons.
436 atol : `float`
437 Absolute tolerance for floating point comparisons.
438 output : callable
439 A callable that takes a string, used (possibly repeatedly) to
440 report inequalities.
441
442 Returns
443 -------
444 isEqual : bool
445 `True` if the fields are equal, `False` otherwise.
446
447 Notes
448 -----
449 Floating point comparisons are performed by `numpy.allclose`.
450 """
451 d1: ConfigurableActionStruct = getattr(instance1, self.namenamenamename)
452 d2: ConfigurableActionStruct = getattr(instance2, self.namenamenamename)
453 name = getComparisonName(
454 _joinNamePath(instance1._name, self.namenamenamename), _joinNamePath(instance2._name, self.namenamenamename)
455 )
456 if not compareScalars(f"keys for {name}", set(d1.fieldNames), set(d2.fieldNames), output=output):
457 return False
458 equal = True
459 for k, v1 in d1.items():
460 v2 = getattr(d2, k)
461 result = compareConfigs(
462 f"{name}.{k}", v1, v2, shortcut=shortcut, rtol=rtol, atol=atol, output=output
463 )
464 if not result and shortcut:
465 return False
466 equal = equal and result
467 return equal
std::vector< SchemaItem< Flag > > * items
__get__(self, instance, owner=None, at=None, label="default")
Definition config.py:728
FieldTypeVar __get__(self, Config instance, Any owner=None, Any at=None, str label="default")
Definition config.py:725
Field[FieldTypeVar] __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition config.py:719
_setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition config.py:497
ConfigurableActionStruct[ActionTypeVar] __get__(self, Config instance, Any owner=None, Any at=None, str label="default")
ConfigurableActionStruct[ActionTypeVar] __get__(self, None instance, Any owner=None, Any at=None, str label="default")
__set__(self, Config instance,(None|Mapping[str, ConfigurableAction]|SimpleNamespace|ConfigurableActionStruct|ConfigurableActionStructField|type[ConfigurableActionStructField]) value, Iterable[StackFrame] at=None, str label="assigment")
__init__(self, str doc, Mapping[str, ConfigurableAction]|None default=None, bool optional=False, deprecated=None)
None __setattr__(self, str attr, ActionTypeVar|type[ActionTypeVar] value, at=None, label="setattr", setHistory=False)
__init__(self, Config config, ConfigurableActionStructField field, Mapping[str, ConfigurableAction] value, Any at, str label)
None __set__(self, ConfigurableActionStruct instance, Mapping[str, ConfigurableAction]|ConfigurableActionStruct value)
daf::base::PropertySet * set
Definition fits.cc:931