LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
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. " f"Attempting to set field to value {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 @overload
361 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default"
362 ) -> ConfigurableActionStruct[ActionTypeVar]: ...
363
364 def __get__(self, instance, owner=None, at=None, label="default"):
365 if instance is None or not isinstance(instance, Config):
366 return self
367 else:
368 field: ConfigurableActionStruct | None = instance._storage[self.namenamenamename]
369 return field
370
371 def rename(self, instance: Config):
372 actionStruct: ConfigurableActionStruct = self.__get____get____get____get____get____get__(instance)
373 if actionStruct is not None:
374 for k, v in actionStruct.items():
375 base_name = _joinNamePath(instance._name, self.namenamenamename)
376 fullname = _joinNamePath(base_name, k)
377 v._rename(fullname)
378
379 def validate(self, instance: Config):
380 value = self.__get____get____get____get____get____get__(instance)
381 if value is not None:
382 for item in value:
383 item.validate()
384
385 def toDict(self, instance):
386 actionStruct = self.__get____get____get____get____get____get__(instance)
387 if actionStruct is None:
388 return None
389
390 dict_ = {k: v.toDict() for k, v in actionStruct.items()}
391
392 return dict_
393
394 def save(self, outfile, instance):
395 actionStruct = self.__get____get____get____get____get____get__(instance)
396 fullname = _joinNamePath(instance._name, self.namenamenamename)
397
398 # Ensure that a struct is always empty before assigning to it.
399 outfile.write(f"{fullname}=None\n")
400
401 if actionStruct is None:
402 return
403
404 for _, v in sorted(actionStruct.items()):
405 outfile.write(f"{v._name}={_typeStr(v)}()\n")
406 v._save(outfile)
407
408 def freeze(self, instance):
409 actionStruct = self.__get____get____get____get____get____get__(instance)
410 if actionStruct is not None:
411 for v in actionStruct:
412 v.freeze()
413
414 def _collectImports(self, instance, imports):
415 # docstring inherited from Field
416 actionStruct = self.__get____get____get____get____get____get__(instance)
417 for v in actionStruct:
418 v._collectImports()
419 imports |= v._imports
420
421 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
422 """Compare two fields for equality.
423
424 Parameters
425 ----------
426 instance1 : `lsst.pex.config.Config`
427 Left-hand side config instance to compare.
428 instance2 : `lsst.pex.config.Config`
429 Right-hand side config instance to compare.
430 shortcut : `bool`
431 If `True`, this function returns as soon as an inequality if found.
432 rtol : `float`
433 Relative tolerance for floating point comparisons.
434 atol : `float`
435 Absolute tolerance for floating point comparisons.
436 output : callable
437 A callable that takes a string, used (possibly repeatedly) to
438 report inequalities.
439
440 Returns
441 -------
442 isEqual : bool
443 `True` if the fields are equal, `False` otherwise.
444
445 Notes
446 -----
447 Floating point comparisons are performed by `numpy.allclose`.
448 """
449 d1: ConfigurableActionStruct = getattr(instance1, self.namenamenamename)
450 d2: ConfigurableActionStruct = getattr(instance2, self.namenamenamename)
451 name = getComparisonName(
452 _joinNamePath(instance1._name, self.namenamenamename), _joinNamePath(instance2._name, self.namenamenamename)
453 )
454 if not compareScalars(f"keys for {name}", set(d1.fieldNames), set(d2.fieldNames), output=output):
455 return False
456 equal = True
457 for k, v1 in d1.items():
458 v2 = getattr(d2, k)
459 result = compareConfigs(
460 f"{name}.{k}", v1, v2, shortcut=shortcut, rtol=rtol, atol=atol, output=output
461 )
462 if not result and shortcut:
463 return False
464 equal = equal and result
465 return equal
std::vector< SchemaItem< Flag > > * items
__get__(self, instance, owner=None, at=None, label="default")
Definition config.py:707
FieldTypeVar __get__(self, Config instance, Any owner=None, Any at=None, str label="default")
Definition config.py:705
Field[FieldTypeVar] __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition config.py:700
_setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition config.py:480
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)