LSST Applications g00d0e8bbd7+8c5ae1fdc5,g013ef56533+603670b062,g083dd6704c+2e189452a7,g199a45376c+0ba108daf9,g1c5cce2383+bc9f6103a4,g1fd858c14a+cd69ed4fc1,g210f2d0738+c4742f2e9e,g262e1987ae+612fa42d85,g29ae962dfc+83d129e820,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+5eaa884f2a,g47891489e3+e32160a944,g53246c7159+8c5ae1fdc5,g5b326b94bb+dcc56af22d,g64539dfbff+c4742f2e9e,g67b6fd64d1+e32160a944,g74acd417e5+c122e1277d,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g88cb488625+47d24e4084,g89139ef638+e32160a944,g8d7436a09f+d14b4ff40a,g8ea07a8fe4+b212507b11,g90f42f885a+e1755607f3,g97be763408+34be90ab8c,g98df359435+ec1fa61bf1,ga2180abaac+8c5ae1fdc5,ga9e74d7ce9+43ac651df0,gbf99507273+8c5ae1fdc5,gc2a301910b+c4742f2e9e,gca7fc764a6+e32160a944,gd7ef33dd92+e32160a944,gdab6d2f7ff+c122e1277d,gdb1e2cdc75+1b18322db8,ge410e46f29+e32160a944,ge41e95a9f2+c4742f2e9e,geaed405ab2+0d91c11c6d,w.2025.44
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__ = ("ConfigurableActionStruct", "ConfigurableActionStructField")
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 if at is not None:
169 self.history.append(("Struct initialized", at, label))
170
171 if value is not None:
172 for k, v in value.items():
173 setattr(self, k, v)
174
175 def _copy(self, config: Config) -> ConfigurableActionStruct:
176 result = ConfigurableActionStruct(config, self._field, self._attrs, at=None, label="copy")
177 result.history.extend(self.history)
178 return result
179
180 @property
181 def _config(self) -> Config:
182 # Config Fields should never outlive their config class instance
183 # assert that as such here
184 value = self._config_()
185 assert value is not None
186 return value
187
188 @property
189 def history(self) -> list[tuple]:
190 return self._history
191
192 @property
193 def fieldNames(self) -> Iterable[str]:
194 return self._attrs.keys()
195
197 self,
198 attr: str,
199 value: ActionTypeVar | type[ActionTypeVar],
200 at=None,
201 label="setattr",
202 setHistory=False,
203 ) -> None:
204 if hasattr(self._config, "_frozen") and self._config._frozen:
205 msg = f"Cannot modify a frozen Config. Attempting to set item {attr} to value {value}"
206 raise FieldValidationError(self._field, self._config, msg)
207
208 # verify that someone has not passed a string with a space or leading
209 # number or something through the dict assignment update interface
210 if not attr.isidentifier():
211 raise ValueError("Names used in ConfigurableStructs must be valid as python variable names")
212
213 if attr not in (self.__dict__.keys() | type(self).__dict__.keys()):
214 base_name = _joinNamePath(self._config._name, self._field.name)
215 name = _joinNamePath(base_name, attr)
216 if at is None:
217 at = getCallStack()
218 if isinstance(value, ConfigurableAction):
219 valueInst = type(value)(__name=name, __at=at, __label=label, **value._storage)
220 else:
221 valueInst = value(__name=name, __at=at, __label=label)
222 self._attrs[attr] = valueInst
223 else:
224 super().__setattr__(attr, value)
225
226 def __getattr__(self, attr) -> Any:
227 if attr in object.__getattribute__(self, "_attrs"):
228 result = self._attrs[attr]
229 result.identity = attr
230 return result
231 else:
232 super().__getattribute__(attr)
233
234 def __delattr__(self, name):
235 if name in self._attrs:
236 del self._attrs[name]
237 else:
238 super().__delattr__(name)
239
240 def __iter__(self) -> Iterator[ActionTypeVar]:
241 for name in self.fieldNames:
242 yield getattr(self, name)
243
244 def items(self) -> Iterable[tuple[str, ActionTypeVar]]:
245 for name in self.fieldNames:
246 yield name, getattr(self, name)
247
248 def __bool__(self) -> bool:
249 return bool(self._attrs)
250
251
252T = TypeVar("T", bound="ConfigurableActionStructField")
253
254
256 """`ConfigurableActionStructField` is a `~lsst.pex.config.Field` subclass
257 that allows a `ConfigurableAction` to be organized in a
258 `~lsst.pex.config.Config` class in a manner similar to how a
259 `~lsst.pipe.base.Struct` works.
260
261 This class uses a `ConfigurableActionStruct` as an intermediary object to
262 organize the `ConfigurableAction`. See its documentation for further
263 information.
264
265 Parameters
266 ----------
267 doc : `str`
268 Documentation string.
269 default : `~collections.abc.Mapping` [ `str`, `ConfigurableAction` ] \
270 or `None`, optional
271 Default value.
272 optional : `bool`, optional
273 If `True`, the field doesn't need to have a set value.
274 deprecated : `bool` or `None`, optional
275 A description of why this Field is deprecated, including removal date.
276 If not `None`, the string is appended to the docstring for this Field.
277 """
278
279 # specify StructClass to make this more generic for potential future
280 # inheritance
281 StructClass = ConfigurableActionStruct
282
283 # Explicitly annotate these on the class, they are present in the base
284 # class through injection, so type systems have trouble seeing them.
285 name: str
286 default: Mapping[str, ConfigurableAction] | None
287
289 self,
290 doc: str,
291 default: Mapping[str, ConfigurableAction] | None = None,
292 optional: bool = False,
293 deprecated=None,
294 ):
295 source = getStackFrame()
296 self._setup(
297 doc=doc,
298 dtype=self.__class__,
299 default=default,
300 check=None,
301 optional=optional,
302 source=source,
303 deprecated=deprecated,
304 )
305
306 def __class_getitem__(cls, params):
307 return GenericAlias(cls, params)
308
310 self,
311 instance: Config,
312 value: (
313 None
314 | Mapping[str, ConfigurableAction]
315 | SimpleNamespace
316 | ConfigurableActionStruct
317 | ConfigurableActionStructField
318 | type[ConfigurableActionStructField]
319 ),
320 at: Iterable[StackFrame] = None,
321 label: str = "assigment",
322 ):
323 if instance._frozen:
324 msg = f"Cannot modify a frozen Config. Attempting to set field to value {value}"
325 raise FieldValidationError(self, instance, msg)
326
327 if at is None:
328 at = getCallStack()
329
330 if value is None or (self.default is not None and self.default == value):
331 value = self.StructClass(instance, self, value, at=at, label=label)
332 else:
333 # An actual value is being assigned check for what it is
334 if isinstance(value, self.StructClass):
335 # If this is a ConfigurableActionStruct, we need to make our
336 # own copy that references this current field
337 value = self.StructClass(instance, self, value._attrs, at=at, label=label)
338 elif isinstance(value, SimpleNamespace):
339 # If this is a a python analogous container, we need to make
340 # a ConfigurableActionStruct initialized with this data
341 value = self.StructClass(instance, self, vars(value), at=at, label=label)
342
343 elif type(value) is ConfigurableActionStructField:
344 raise ValueError(
345 "ConfigurableActionStructFields can only be used in a class body declaration"
346 f"Use a {self.StructClass}, SimpleNamespace or Struct"
347 )
348 else:
349 raise ValueError(f"Unrecognized value {value}, cannot be assigned to this field")
350
351 history = instance._history.setdefault(self.name, [])
352 history.append((value, at, label))
353
354 if not isinstance(value, ConfigurableActionStruct):
356 self, instance, "Can only assign things that are subclasses of Configurable Action"
357 )
358 instance._storage[self.name] = value
359
360 @overload
362 self, instance: None, owner: Any = None, at: Any = None, label: str = "default"
363 ) -> ConfigurableActionStruct[ActionTypeVar]: ...
364
365 @overload
367 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default"
368 ) -> ConfigurableActionStruct[ActionTypeVar]: ...
369
370 def __get__(self, instance, owner=None, at=None, label="default"):
371 if instance is None or not isinstance(instance, Config):
372 return self
373 else:
374 field: ConfigurableActionStruct | None = instance._storage[self.name]
375 return field
376
377 def rename(self, instance: Config):
378 actionStruct: ConfigurableActionStruct = self.__get__(instance)
379 if actionStruct is not None:
380 for k, v in actionStruct.items():
381 base_name = _joinNamePath(instance._name, self.name)
382 fullname = _joinNamePath(base_name, k)
383 v._rename(fullname)
384
385 def validate(self, instance: Config):
386 value = self.__get__(instance)
387 if value is not None:
388 for item in value:
389 item.validate()
390
391 def toDict(self, instance):
392 actionStruct = self.__get__(instance)
393 if actionStruct is None:
394 return None
395
396 dict_ = {k: v.toDict() for k, v in actionStruct.items()}
397
398 return dict_
399
400 def _copy_storage(self, old: Config, new: Config) -> ConfigurableActionStruct:
401 struct: ConfigurableActionStruct | None = old._storage.get(self.name)
402 if struct is not None:
403 return struct._copy(new)
404 else:
405 return None
406
407 def save(self, outfile, instance):
408 actionStruct = self.__get__(instance)
409 fullname = _joinNamePath(instance._name, self.name)
410
411 # Ensure that a struct is always empty before assigning to it.
412 outfile.write(f"{fullname}=None\n")
413
414 if actionStruct is None:
415 return
416
417 for _, v in sorted(actionStruct.items()):
418 outfile.write(f"{v._name}={_typeStr(v)}()\n")
419 v._save(outfile)
420
421 def freeze(self, instance):
422 actionStruct = self.__get__(instance)
423 if actionStruct is not None:
424 for v in actionStruct:
425 v.freeze()
426
427 def _collectImports(self, instance, imports):
428 # docstring inherited from Field
429 actionStruct = self.__get__(instance)
430 for v in actionStruct:
431 v._collectImports()
432 imports |= v._imports
433
434 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
435 """Compare two fields for equality.
436
437 Parameters
438 ----------
439 instance1 : `lsst.pex.config.Config`
440 Left-hand side config instance to compare.
441 instance2 : `lsst.pex.config.Config`
442 Right-hand side config instance to compare.
443 shortcut : `bool`
444 If `True`, this function returns as soon as an inequality if found.
445 rtol : `float`
446 Relative tolerance for floating point comparisons.
447 atol : `float`
448 Absolute tolerance for floating point comparisons.
449 output : callable
450 A callable that takes a string, used (possibly repeatedly) to
451 report inequalities.
452
453 Returns
454 -------
455 isEqual : bool
456 `True` if the fields are equal, `False` otherwise.
457
458 Notes
459 -----
460 Floating point comparisons are performed by `numpy.allclose`.
461 """
462 d1: ConfigurableActionStruct = getattr(instance1, self.name)
463 d2: ConfigurableActionStruct = getattr(instance2, self.name)
464 name = getComparisonName(
465 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
466 )
467 if not compareScalars(f"{name} (fields)", set(d1.fieldNames), set(d2.fieldNames), output=output):
468 return False
469 equal = True
470 for k, v1 in d1.items():
471 v2 = getattr(d2, k)
472 result = compareConfigs(
473 f"{name}.{k}", v1, v2, shortcut=shortcut, rtol=rtol, atol=atol, output=output
474 )
475 if not result and shortcut:
476 return False
477 equal = equal and result
478 return equal
Field[FieldTypeVar] __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition config.py:714
_setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition config.py:486
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)
compareConfigs(name, c1, c2, shortcut=True, rtol=1e-8, atol=1e-8, output=None)
getComparisonName(name1, name2)
Definition comparison.py:40
compareScalars(name, v1, v2, output, rtol=1e-8, atol=1e-8, dtype=None)
Definition comparison.py:62
_joinNamePath(prefix=None, name=None, index=None)
Definition config.py:107