LSST Applications 27.0.0,g0265f82a02+469cd937ee,g02d81e74bb+21ad69e7e1,g1470d8bcf6+cbe83ee85a,g2079a07aa2+e67c6346a6,g212a7c68fe+04a9158687,g2305ad1205+94392ce272,g295015adf3+81dd352a9d,g2bbee38e9b+469cd937ee,g337abbeb29+469cd937ee,g3939d97d7f+72a9f7b576,g487adcacf7+71499e7cba,g50ff169b8f+5929b3527e,g52b1c1532d+a6fc98d2e7,g591dd9f2cf+df404f777f,g5a732f18d5+be83d3ecdb,g64a986408d+21ad69e7e1,g858d7b2824+21ad69e7e1,g8a8a8dda67+a6fc98d2e7,g99cad8db69+f62e5b0af5,g9ddcbc5298+d4bad12328,ga1e77700b3+9c366c4306,ga8c6da7877+71e4819109,gb0e22166c9+25ba2f69a1,gb6a65358fc+469cd937ee,gbb8dafda3b+69d3c0e320,gc07e1c2157+a98bf949bb,gc120e1dc64+615ec43309,gc28159a63d+469cd937ee,gcf0d15dbbd+72a9f7b576,gdaeeff99f8+a38ce5ea23,ge6526c86ff+3a7c1ac5f1,ge79ae78c31+469cd937ee,gee10cc3b42+a6fc98d2e7,gf1cff7945b+21ad69e7e1,gfbcc870c63+9a11dc8c8f
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 = "Value {} at key {!r} is of incorrect type {}. Expected type {}".format(
137 x,
138 k,
139 _typeStr(x),
140 _typeStr(self._field.itemtype),
141 )
142 raise FieldValidationError(self._field, self._config_config, msg)
143
144 # validate item using itemcheck
145 if self._field.itemCheck is not None and not self._field.itemCheck(x):
146 msg = f"Item at key {k!r} is not a valid value: {x}"
147 raise FieldValidationError(self._field, self._config_config, msg)
148
149 if at is None:
150 at = getCallStack()
151
152 self._dict[k] = x
153 if setHistory:
154 self._history.append((dict(self._dict), at, label))
155
157 self, k: KeyTypeVar, at: Any = None, label: str = "delitem", setHistory: bool = True
158 ) -> None:
159 if self._config_config._frozen:
160 raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
161
162 del self._dict[k]
163 if setHistory:
164 if at is None:
165 at = getCallStack()
166 self._history.append((dict(self._dict), at, label))
167
168 def __repr__(self):
169 return repr(self._dict)
170
171 def __str__(self):
172 return str(self._dict)
173
174 def __setattr__(self, attr, value, at=None, label="assignment"):
175 if hasattr(getattr(self.__class__, attr, None), "__set__"):
176 # This allows properties to work.
177 object.__setattr__(self, attr, value)
178 elif attr in self.__dict__ or attr in ["_field", "_config_", "_history", "_dict", "__doc__"]:
179 # This allows specific private attributes to work.
180 object.__setattr__(self, attr, value)
181 else:
182 # We throw everything else.
183 msg = f"{_typeStr(self._field)} has no attribute {attr}"
184 raise FieldValidationError(self._field, self._config_config, msg)
185
186 def __reduce__(self):
188 f"Proxy container for config field {self._field.name} cannot "
189 "be pickled; it should be converted to a built-in container before "
190 "being assigned to other objects or variables."
191 )
192
193
194class DictField(Field[Dict[KeyTypeVar, ItemTypeVar]], Generic[KeyTypeVar, ItemTypeVar]):
195 """A configuration field (`~lsst.pex.config.Field` subclass) that maps keys
196 and values.
197
198 The types of both items and keys are restricted to these builtin types:
199 `int`, `float`, `complex`, `bool`, and `str`). All keys share the same type
200 and all values share the same type. Keys can have a different type from
201 values.
202
203 Parameters
204 ----------
205 doc : `str`
206 A documentation string that describes the configuration field.
207 keytype : {`int`, `float`, `complex`, `bool`, `str`}, optional
208 The type of the mapping keys. All keys must have this type. Optional
209 if keytype and itemtype are supplied as typing arguments to the class.
210 itemtype : {`int`, `float`, `complex`, `bool`, `str`}, optional
211 Type of the mapping values. Optional if keytype and itemtype are
212 supplied as typing arguments to the class.
213 default : `dict`, optional
214 The default mapping.
215 optional : `bool`, optional
216 If `True`, the field doesn't need to have a set value.
217 dictCheck : callable
218 A function that validates the dictionary as a whole.
219 itemCheck : callable
220 A function that validates individual mapping values.
221 deprecated : None or `str`, optional
222 A description of why this Field is deprecated, including removal date.
223 If not None, the string is appended to the docstring for this Field.
224
225 See Also
226 --------
227 ChoiceField
228 ConfigChoiceField
229 ConfigDictField
230 ConfigField
231 ConfigurableField
232 Field
233 ListField
234 RangeField
235 RegistryField
236
237 Examples
238 --------
239 This field maps has `str` keys and `int` values:
240
241 >>> from lsst.pex.config import Config, DictField
242 >>> class MyConfig(Config):
243 ... field = DictField(
244 ... doc="Example string-to-int mapping field.",
245 ... keytype=str, itemtype=int,
246 ... default={})
247 ...
248 >>> config = MyConfig()
249 >>> config.field['myKey'] = 42
250 >>> print(config.field)
251 {'myKey': 42}
252 """
253
254 DictClass: type[Dict] = Dict
255
256 @staticmethod
258 params: tuple[type, ...] | tuple[str, ...], kwds: Mapping[str, Any]
259 ) -> Mapping[str, Any]:
260 if len(params) != 2:
261 raise ValueError("Only tuples of types that are length 2 are supported")
262 resultParams = []
263 for typ in params:
264 if isinstance(typ, str):
265 _typ = ForwardRef(typ)
266 # type ignore below because typeshed seems to be wrong. It
267 # indicates there are only 2 args, as it was in python 3.8, but
268 # 3.9+ takes 3 args. Attempt in old style and new style to
269 # work with both.
270 try:
271 result = _typ._evaluate(globals(), locals(), set()) # type: ignore
272 except TypeError:
273 # python 3.8 path
274 result = _typ._evaluate(globals(), locals())
275 if result is None:
276 raise ValueError("Could not deduce type from input")
277 typ = cast(type, result)
278 resultParams.append(typ)
279 keyType, itemType = resultParams
280 results = dict(kwds)
281 if (supplied := kwds.get("keytype")) and supplied != keyType:
282 raise ValueError("Conflicting definition for keytype")
283 else:
284 results["keytype"] = keyType
285 if (supplied := kwds.get("itemtype")) and supplied != itemType:
286 raise ValueError("Conflicting definition for itemtype")
287 else:
288 results["itemtype"] = itemType
289 return results
290
292 self,
293 doc,
294 keytype=None,
295 itemtype=None,
296 default=None,
297 optional=False,
298 dictCheck=None,
299 itemCheck=None,
300 deprecated=None,
301 ):
302 source = getStackFrame()
303 self._setup(
304 doc=doc,
305 dtype=Dict,
306 default=default,
307 check=None,
308 optional=optional,
309 source=source,
310 deprecated=deprecated,
311 )
312 if keytype is None:
313 raise ValueError(
314 "keytype must either be supplied as an argument or as a type argument to the class"
315 )
316 if keytype not in self.supportedTypes:
317 raise ValueError("'keytype' %s is not a supported type" % _typeStr(keytype))
318 elif itemtype is not None and itemtype not in self.supportedTypes:
319 raise ValueError("'itemtype' %s is not a supported type" % _typeStr(itemtype))
320 if dictCheck is not None and not hasattr(dictCheck, "__call__"):
321 raise ValueError("'dictCheck' must be callable")
322 if itemCheck is not None and not hasattr(itemCheck, "__call__"):
323 raise ValueError("'itemCheck' must be callable")
324
325 self.keytype = keytype
326 self.itemtype = itemtype
327 self.dictCheck = dictCheck
328 self.itemCheck = itemCheck
329
330 def validate(self, instance):
331 """Validate the field's value (for internal use only).
332
333 Parameters
334 ----------
335 instance : `lsst.pex.config.Config`
336 The configuration that contains this field.
337
338 Returns
339 -------
340 isValid : `bool`
341 `True` is returned if the field passes validation criteria (see
342 *Notes*). Otherwise `False`.
343
344 Notes
345 -----
346 This method validates values according to the following criteria:
347
348 - A non-optional field is not `None`.
349 - If a value is not `None`, is must pass the `ConfigField.dictCheck`
350 user callback functon.
351
352 Individual item checks by the `ConfigField.itemCheck` user callback
353 function are done immediately when the value is set on a key. Those
354 checks are not repeated by this method.
355 """
356 Field.validate(self, instance)
357 value = self.__get____get____get__(instance)
358 if value is not None and self.dictCheck is not None and not self.dictCheck(value):
359 msg = "%s is not a valid value" % str(value)
360 raise FieldValidationError(self, instance, msg)
361
363 self,
364 instance: Config,
365 value: Mapping[KeyTypeVar, ItemTypeVar] | None,
366 at: Any = None,
367 label: str = "assignment",
368 ) -> None:
369 if instance._frozen:
370 msg = "Cannot modify a frozen Config. Attempting to set field to value %s" % value
371 raise FieldValidationError(self, instance, msg)
372
373 if at is None:
374 at = getCallStack()
375 if value is not None:
376 value = self.DictClass(instance, self, value, at=at, label=label)
377 else:
378 history = instance._history.setdefault(self.namenamename, [])
379 history.append((value, at, label))
380
381 instance._storage[self.namenamename] = value
382
383 def toDict(self, instance):
384 """Convert this field's key-value pairs into a regular `dict`.
385
386 Parameters
387 ----------
388 instance : `lsst.pex.config.Config`
389 The configuration that contains this field.
390
391 Returns
392 -------
393 result : `dict` or `None`
394 If this field has a value of `None`, then this method returns
395 `None`. Otherwise, this method returns the field's value as a
396 regular Python `dict`.
397 """
398 value = self.__get____get____get__(instance)
399 return dict(value) if value is not None else None
400
401 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
402 """Compare two fields for equality.
403
404 Used by `lsst.pex.ConfigDictField.compare`.
405
406 Parameters
407 ----------
408 instance1 : `lsst.pex.config.Config`
409 Left-hand side config instance to compare.
410 instance2 : `lsst.pex.config.Config`
411 Right-hand side config instance to compare.
412 shortcut : `bool`
413 If `True`, this function returns as soon as an inequality if found.
414 rtol : `float`
415 Relative tolerance for floating point comparisons.
416 atol : `float`
417 Absolute tolerance for floating point comparisons.
418 output : callable
419 A callable that takes a string, used (possibly repeatedly) to
420 report inequalities.
421
422 Returns
423 -------
424 isEqual : bool
425 `True` if the fields are equal, `False` otherwise.
426
427 Notes
428 -----
429 Floating point comparisons are performed by `numpy.allclose`.
430 """
431 d1 = getattr(instance1, self.namenamename)
432 d2 = getattr(instance2, self.namenamename)
433 name = getComparisonName(
434 _joinNamePath(instance1._name, self.namenamename), _joinNamePath(instance2._name, self.namenamename)
435 )
436 if not compareScalars("isnone for %s" % name, d1 is None, d2 is None, output=output):
437 return False
438 if d1 is None and d2 is None:
439 return True
440 if not compareScalars("keys for %s" % name, set(d1.keys()), set(d2.keys()), output=output):
441 return False
442 equal = True
443 for k, v1 in d1.items():
444 v2 = d2[k]
445 result = compareScalars(
446 f"{name}[{k!r}]", v1, v2, dtype=self.itemtype, rtol=rtol, atol=atol, output=output
447 )
448 if not result and shortcut:
449 return False
450 equal = equal and result
451 return equal
__get__(self, instance, owner=None, at=None, label="default")
Definition config.py:726
FieldTypeVar __get__(self, Config instance, Any owner=None, Any at=None, str label="default")
Definition config.py:724
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
__init__(self, doc, keytype=None, itemtype=None, default=None, optional=False, dictCheck=None, itemCheck=None, deprecated=None)
Definition dictField.py:301
Mapping[str, Any] _parseTypingArgs(tuple[type,...]|tuple[str,...] params, Mapping[str, Any] kwds)
Definition dictField.py:259
None __set__(self, Config instance, Mapping[KeyTypeVar, ItemTypeVar]|None value, Any at=None, str label="assignment")
Definition dictField.py:368
_compare(self, instance1, instance2, shortcut, rtol, atol, output)
Definition dictField.py:401
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:158
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:174
daf::base::PropertySet * set
Definition fits.cc:931