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
dictField.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# (http://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 software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
27
28from __future__ import annotations
29
30__all__ = ["DictField"]
31
32import collections.abc
33import weakref
34from collections.abc import Iterator, Mapping
35from typing import Any, ForwardRef, Generic, TypeVar, cast
36
37from .callStack import getCallStack, getStackFrame
38from .comparison import compareScalars, getComparisonName
39from .config import (
40 Config,
41 Field,
42 FieldValidationError,
43 UnexpectedProxyUsageError,
44 _autocast,
45 _joinNamePath,
46 _typeStr,
47)
48
49KeyTypeVar = TypeVar("KeyTypeVar")
50ItemTypeVar = TypeVar("ItemTypeVar")
51
52
53class Dict(collections.abc.MutableMapping[KeyTypeVar, ItemTypeVar]):
54 """An internal mapping container.
55
56 This class emulates a `dict`, but adds validation and provenance.
57
58 Parameters
59 ----------
60 config : `~lsst.pex.config.Config`
61 Config to proxy.
62 field : `~lsst.pex.config.DictField`
63 Field to use.
64 value : `~typing.Any`
65 Value to store.
66 at : `list` of `~lsst.pex.config.callStack.StackFrame`
67 Stack frame for history recording. Will be calculated if `None`.
68 label : `str`, optional
69 Label to use for history recording.
70 setHistory : `bool`, optional
71 Whether to append to the history record.
72 """
73
74 def __init__(self, config, field, value, at, label, setHistory=True):
75 self._field = field
76 self._config_ = weakref.ref(config)
77 self._dict = {}
78 self._history = self._config_config._history.setdefault(self._field.name, [])
79 self.__doc__ = field.doc
80 if value is not None:
81 try:
82 for k in value:
83 # do not set history per-item
84 self.__setitem__(k, value[k], at=at, label=label, setHistory=False)
85 except TypeError:
86 msg = f"Value {value} is of incorrect type {_typeStr(value)}. Mapping type expected."
88 if setHistory:
89 self._history.append((dict(self._dict), at, label))
90
91 @property
92 def _config(self) -> Config:
93 # Config Fields should never outlive their config class instance
94 # assert that as such here
95 value = self._config_()
96 assert value is not None
97 return value
98
99 history = property(lambda x: x._history)
100 """History (read-only).
101 """
102
103 def __getitem__(self, k: KeyTypeVar) -> ItemTypeVar:
104 return self._dict[k]
105
106 def __len__(self) -> int:
107 return len(self._dict)
108
109 def __iter__(self) -> Iterator[KeyTypeVar]:
110 return iter(self._dict)
111
112 def __contains__(self, k: Any) -> bool:
113 return k in self._dict
114
116 self, k: KeyTypeVar, x: ItemTypeVar, at: Any = None, label: str = "setitem", setHistory: bool = True
117 ) -> None:
118 if self._config_config._frozen:
119 msg = f"Cannot modify a frozen Config. Attempting to set item at key {k!r} to value {x}"
120 raise FieldValidationError(self._field, self._config_config, msg)
121
122 # validate keytype
123 k = _autocast(k, self._field.keytype)
124 if type(k) is not self._field.keytype:
125 msg = f"Key {k!r} is of type {_typeStr(k)}, expected type {_typeStr(self._field.keytype)}"
126 raise FieldValidationError(self._field, self._config_config, msg)
127
128 # validate itemtype
129 x = _autocast(x, self._field.itemtype)
130 if self._field.itemtype is None:
131 if type(x) not in self._field.supportedTypes and x is not None:
132 msg = f"Value {x} at key {k!r} is of invalid type {_typeStr(x)}"
133 raise FieldValidationError(self._field, self._config_config, msg)
134 else:
135 if type(x) is not self._field.itemtype and x is not None:
136 msg = (
137 f"Value {x} at key {k!r} is of incorrect type {_typeStr(x)}. "
138 f"Expected type {_typeStr(self._field.itemtype)}"
139 )
140 raise FieldValidationError(self._field, self._config_config, msg)
141
142 # validate item using itemcheck
143 if self._field.itemCheck is not None and not self._field.itemCheck(x):
144 msg = f"Item at key {k!r} is not a valid value: {x}"
145 raise FieldValidationError(self._field, self._config_config, msg)
146
147 if at is None:
148 at = getCallStack()
149
150 self._dict[k] = x
151 if setHistory:
152 self._history.append((dict(self._dict), at, label))
153
155 self, k: KeyTypeVar, at: Any = None, label: str = "delitem", setHistory: bool = True
156 ) -> None:
157 if self._config_config._frozen:
158 raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
159
160 del self._dict[k]
161 if setHistory:
162 if at is None:
163 at = getCallStack()
164 self._history.append((dict(self._dict), at, label))
165
166 def __repr__(self):
167 return repr(self._dict)
168
169 def __str__(self):
170 return str(self._dict)
171
172 def __setattr__(self, attr, value, at=None, label="assignment"):
173 if hasattr(getattr(self.__class__, attr, None), "__set__"):
174 # This allows properties to work.
175 object.__setattr__(self, attr, value)
176 elif attr in self.__dict__ or attr in ["_field", "_config_", "_history", "_dict", "__doc__"]:
177 # This allows specific private attributes to work.
178 object.__setattr__(self, attr, value)
179 else:
180 # We throw everything else.
181 msg = f"{_typeStr(self._field)} has no attribute {attr}"
182 raise FieldValidationError(self._field, self._config_config, msg)
183
184 def __reduce__(self):
186 f"Proxy container for config field {self._field.name} cannot "
187 "be pickled; it should be converted to a built-in container before "
188 "being assigned to other objects or variables."
189 )
190
191
192class DictField(Field[Dict[KeyTypeVar, ItemTypeVar]], Generic[KeyTypeVar, ItemTypeVar]):
193 """A configuration field (`~lsst.pex.config.Field` subclass) that maps keys
194 and values.
195
196 The types of both items and keys are restricted to these builtin types:
197 `int`, `float`, `complex`, `bool`, and `str`). All keys share the same type
198 and all values share the same type. Keys can have a different type from
199 values.
200
201 Parameters
202 ----------
203 doc : `str`
204 A documentation string that describes the configuration field.
205 keytype : {`int`, `float`, `complex`, `bool`, `str`}, optional
206 The type of the mapping keys. All keys must have this type. Optional
207 if keytype and itemtype are supplied as typing arguments to the class.
208 itemtype : {`int`, `float`, `complex`, `bool`, `str`}, optional
209 Type of the mapping values. Optional if keytype and itemtype are
210 supplied as typing arguments to the class.
211 default : `dict`, optional
212 The default mapping.
213 optional : `bool`, optional
214 If `True`, the field doesn't need to have a set value.
215 dictCheck : callable
216 A function that validates the dictionary as a whole.
217 itemCheck : callable
218 A function that validates individual mapping values.
219 deprecated : None or `str`, optional
220 A description of why this Field is deprecated, including removal date.
221 If not None, the string is appended to the docstring for this Field.
222
223 See Also
224 --------
225 ChoiceField
226 ConfigChoiceField
227 ConfigDictField
228 ConfigField
229 ConfigurableField
230 Field
231 ListField
232 RangeField
233 RegistryField
234
235 Examples
236 --------
237 This field maps has `str` keys and `int` values:
238
239 >>> from lsst.pex.config import Config, DictField
240 >>> class MyConfig(Config):
241 ... field = DictField(
242 ... doc="Example string-to-int mapping field.",
243 ... keytype=str, itemtype=int,
244 ... default={})
245 ...
246 >>> config = MyConfig()
247 >>> config.field['myKey'] = 42
248 >>> print(config.field)
249 {'myKey': 42}
250 """
251
252 DictClass: type[Dict] = Dict
253
254 @staticmethod
256 params: tuple[type, ...] | tuple[str, ...], kwds: Mapping[str, Any]
257 ) -> Mapping[str, Any]:
258 if len(params) != 2:
259 raise ValueError("Only tuples of types that are length 2 are supported")
260 resultParams = []
261 for typ in params:
262 if isinstance(typ, str):
263 _typ = ForwardRef(typ)
264 # type ignore below because typeshed seems to be wrong. It
265 # indicates there are only 2 args, as it was in python 3.8, but
266 # 3.9+ takes 3 args.
267 result = _typ._evaluate(globals(), locals(), recursive_guard=set()) # type: ignore
268 if result is None:
269 raise ValueError("Could not deduce type from input")
270 typ = cast(type, result)
271 resultParams.append(typ)
272 keyType, itemType = resultParams
273 results = dict(kwds)
274 if (supplied := kwds.get("keytype")) and supplied != keyType:
275 raise ValueError("Conflicting definition for keytype")
276 else:
277 results["keytype"] = keyType
278 if (supplied := kwds.get("itemtype")) and supplied != itemType:
279 raise ValueError("Conflicting definition for itemtype")
280 else:
281 results["itemtype"] = itemType
282 return results
283
285 self,
286 doc,
287 keytype=None,
288 itemtype=None,
289 default=None,
290 optional=False,
291 dictCheck=None,
292 itemCheck=None,
293 deprecated=None,
294 ):
295 source = getStackFrame()
296 self._setup(
297 doc=doc,
298 dtype=Dict,
299 default=default,
300 check=None,
301 optional=optional,
302 source=source,
303 deprecated=deprecated,
304 )
305 if keytype is None:
306 raise ValueError(
307 "keytype must either be supplied as an argument or as a type argument to the class"
308 )
309 if keytype not in self.supportedTypes:
310 raise ValueError(f"'keytype' {_typeStr(keytype)} is not a supported type")
311 elif itemtype is not None and itemtype not in self.supportedTypes:
312 raise ValueError(f"'itemtype' {_typeStr(itemtype)} is not a supported type")
313 if dictCheck is not None and not hasattr(dictCheck, "__call__"):
314 raise ValueError("'dictCheck' must be callable")
315 if itemCheck is not None and not hasattr(itemCheck, "__call__"):
316 raise ValueError("'itemCheck' must be callable")
317
318 self.keytype = keytype
319 self.itemtype = itemtype
320 self.dictCheck = dictCheck
321 self.itemCheck = itemCheck
322
323 def validate(self, instance):
324 """Validate the field's value (for internal use only).
325
326 Parameters
327 ----------
328 instance : `lsst.pex.config.Config`
329 The configuration that contains this field.
330
331 Returns
332 -------
333 isValid : `bool`
334 `True` is returned if the field passes validation criteria (see
335 *Notes*). Otherwise `False`.
336
337 Notes
338 -----
339 This method validates values according to the following criteria:
340
341 - A non-optional field is not `None`.
342 - If a value is not `None`, is must pass the `ConfigField.dictCheck`
343 user callback functon.
344
345 Individual item checks by the `ConfigField.itemCheck` user callback
346 function are done immediately when the value is set on a key. Those
347 checks are not repeated by this method.
348 """
349 Field.validate(self, instance)
350 value = self.__get____get____get__(instance)
351 if value is not None and self.dictCheck is not None and not self.dictCheck(value):
352 msg = f"{value} is not a valid value"
353 raise FieldValidationError(self, instance, msg)
354
356 self,
357 instance: Config,
358 value: Mapping[KeyTypeVar, ItemTypeVar] | None,
359 at: Any = None,
360 label: str = "assignment",
361 ) -> None:
362 if instance._frozen:
363 msg = f"Cannot modify a frozen Config. Attempting to set field to value {value}"
364 raise FieldValidationError(self, instance, msg)
365
366 if at is None:
367 at = getCallStack()
368 if value is not None:
369 value = self.DictClass(instance, self, value, at=at, label=label)
370 else:
371 history = instance._history.setdefault(self.namenamename, [])
372 history.append((value, at, label))
373
374 instance._storage[self.namenamename] = value
375
376 def toDict(self, instance):
377 """Convert this field's key-value pairs into a regular `dict`.
378
379 Parameters
380 ----------
381 instance : `lsst.pex.config.Config`
382 The configuration that contains this field.
383
384 Returns
385 -------
386 result : `dict` or `None`
387 If this field has a value of `None`, then this method returns
388 `None`. Otherwise, this method returns the field's value as a
389 regular Python `dict`.
390 """
391 value = self.__get____get____get__(instance)
392 return dict(value) if value is not None else None
393
394 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
395 """Compare two fields for equality.
396
397 Used by `lsst.pex.ConfigDictField.compare`.
398
399 Parameters
400 ----------
401 instance1 : `lsst.pex.config.Config`
402 Left-hand side config instance to compare.
403 instance2 : `lsst.pex.config.Config`
404 Right-hand side config instance to compare.
405 shortcut : `bool`
406 If `True`, this function returns as soon as an inequality if found.
407 rtol : `float`
408 Relative tolerance for floating point comparisons.
409 atol : `float`
410 Absolute tolerance for floating point comparisons.
411 output : callable
412 A callable that takes a string, used (possibly repeatedly) to
413 report inequalities.
414
415 Returns
416 -------
417 isEqual : bool
418 `True` if the fields are equal, `False` otherwise.
419
420 Notes
421 -----
422 Floating point comparisons are performed by `numpy.allclose`.
423 """
424 d1 = getattr(instance1, self.namenamename)
425 d2 = getattr(instance2, self.namenamename)
426 name = getComparisonName(
427 _joinNamePath(instance1._name, self.namenamename), _joinNamePath(instance2._name, self.namenamename)
428 )
429 if not compareScalars(f"isnone for {name}", d1 is None, d2 is None, output=output):
430 return False
431 if d1 is None and d2 is None:
432 return True
433 if not compareScalars(f"keys for {name}", set(d1.keys()), set(d2.keys()), output=output):
434 return False
435 equal = True
436 for k, v1 in d1.items():
437 v2 = d2[k]
438 result = compareScalars(
439 f"{name}[{k!r}]", v1, v2, dtype=self.itemtype, rtol=rtol, atol=atol, output=output
440 )
441 if not result and shortcut:
442 return False
443 equal = equal and result
444 return equal
__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
__init__(self, doc, keytype=None, itemtype=None, default=None, optional=False, dictCheck=None, itemCheck=None, deprecated=None)
Definition dictField.py:294
Mapping[str, Any] _parseTypingArgs(tuple[type,...]|tuple[str,...] params, Mapping[str, Any] kwds)
Definition dictField.py:257
None __set__(self, Config instance, Mapping[KeyTypeVar, ItemTypeVar]|None value, Any at=None, str label="assignment")
Definition dictField.py:361
_compare(self, instance1, instance2, shortcut, rtol, atol, output)
Definition dictField.py:394
bool __contains__(self, Any k)
Definition dictField.py:112
None __setitem__(self, KeyTypeVar k, ItemTypeVar x, Any at=None, str label="setitem", bool setHistory=True)
Definition dictField.py:117
ItemTypeVar __getitem__(self, KeyTypeVar k)
Definition dictField.py:103
None __delitem__(self, KeyTypeVar k, Any at=None, str label="delitem", bool setHistory=True)
Definition dictField.py:156
Iterator[KeyTypeVar] __iter__(self)
Definition dictField.py:109
__init__(self, config, field, value, at, label, setHistory=True)
Definition dictField.py:74
__setattr__(self, attr, value, at=None, label="assignment")
Definition dictField.py:172