LSST Applications 26.0.0,g0265f82a02+6660c170cc,g07994bdeae+30b05a742e,g0a0026dc87+17526d298f,g0a60f58ba1+17526d298f,g0e4bf8285c+96dd2c2ea9,g0ecae5effc+c266a536c8,g1e7d6db67d+6f7cb1f4bb,g26482f50c6+6346c0633c,g2bbee38e9b+6660c170cc,g2cc88a2952+0a4e78cd49,g3273194fdb+f6908454ef,g337abbeb29+6660c170cc,g337c41fc51+9a8f8f0815,g37c6e7c3d5+7bbafe9d37,g44018dc512+6660c170cc,g4a941329ef+4f7594a38e,g4c90b7bd52+5145c320d2,g58be5f913a+bea990ba40,g635b316a6c+8d6b3a3e56,g67924a670a+bfead8c487,g6ae5381d9b+81bc2a20b4,g93c4d6e787+26b17396bd,g98cecbdb62+ed2cb6d659,g98ffbb4407+81bc2a20b4,g9ddcbc5298+7f7571301f,ga1e77700b3+99e9273977,gae46bcf261+6660c170cc,gb2715bf1a1+17526d298f,gc86a011abf+17526d298f,gcf0d15dbbd+96dd2c2ea9,gdaeeff99f8+0d8dbea60f,gdb4ec4c597+6660c170cc,ge23793e450+96dd2c2ea9,gf041782ebf+171108ac67
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
59 def __init__(self, config, field, value, at, label, setHistory=True):
60 self._field = field
61 self._config_ = weakref.ref(config)
62 self._dict = {}
63 self._history = self._config_config._history.setdefault(self._field.name, [])
64 self.__doc__ = field.doc
65 if value is not None:
66 try:
67 for k in value:
68 # do not set history per-item
69 self.__setitem__(k, value[k], at=at, label=label, setHistory=False)
70 except TypeError:
71 msg = f"Value {value} is of incorrect type {_typeStr(value)}. Mapping type expected."
73 if setHistory:
74 self._history.append((dict(self._dict), at, label))
75
76 @property
77 def _config(self) -> Config:
78 # Config Fields should never outlive their config class instance
79 # assert that as such here
80 value = self._config_()
81 assert value is not None
82 return value
83
84 history = property(lambda x: x._history)
85 """History (read-only).
86 """
87
88 def __getitem__(self, k: KeyTypeVar) -> ItemTypeVar:
89 return self._dict[k]
90
91 def __len__(self) -> int:
92 return len(self._dict)
93
94 def __iter__(self) -> Iterator[KeyTypeVar]:
95 return iter(self._dict)
96
97 def __contains__(self, k: Any) -> bool:
98 return k in self._dict
99
101 self, k: KeyTypeVar, x: ItemTypeVar, at: Any = None, label: str = "setitem", setHistory: bool = True
102 ) -> None:
103 if self._config_config._frozen:
104 msg = f"Cannot modify a frozen Config. Attempting to set item at key {k!r} to value {x}"
105 raise FieldValidationError(self._field, self._config_config, msg)
106
107 # validate keytype
108 k = _autocast(k, self._field.keytype)
109 if type(k) != self._field.keytype:
110 msg = f"Key {k!r} is of type {_typeStr(k)}, expected type {_typeStr(self._field.keytype)}"
111 raise FieldValidationError(self._field, self._config_config, msg)
112
113 # validate itemtype
114 x = _autocast(x, self._field.itemtype)
115 if self._field.itemtype is None:
116 if type(x) not in self._field.supportedTypes and x is not None:
117 msg = f"Value {x} at key {k!r} is of invalid type {_typeStr(x)}"
118 raise FieldValidationError(self._field, self._config_config, msg)
119 else:
120 if type(x) != self._field.itemtype and x is not None:
121 msg = "Value {} at key {!r} is of incorrect type {}. Expected type {}".format(
122 x,
123 k,
124 _typeStr(x),
125 _typeStr(self._field.itemtype),
126 )
127 raise FieldValidationError(self._field, self._config_config, msg)
128
129 # validate item using itemcheck
130 if self._field.itemCheck is not None and not self._field.itemCheck(x):
131 msg = f"Item at key {k!r} is not a valid value: {x}"
132 raise FieldValidationError(self._field, self._config_config, msg)
133
134 if at is None:
135 at = getCallStack()
136
137 self._dict[k] = x
138 if setHistory:
139 self._history.append((dict(self._dict), at, label))
140
142 self, k: KeyTypeVar, at: Any = None, label: str = "delitem", setHistory: bool = True
143 ) -> None:
144 if self._config_config._frozen:
145 raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
146
147 del self._dict[k]
148 if setHistory:
149 if at is None:
150 at = getCallStack()
151 self._history.append((dict(self._dict), at, label))
152
153 def __repr__(self):
154 return repr(self._dict)
155
156 def __str__(self):
157 return str(self._dict)
158
159 def __setattr__(self, attr, value, at=None, label="assignment"):
160 if hasattr(getattr(self.__class__, attr, None), "__set__"):
161 # This allows properties to work.
162 object.__setattr__(self, attr, value)
163 elif attr in self.__dict__ or attr in ["_field", "_config_", "_history", "_dict", "__doc__"]:
164 # This allows specific private attributes to work.
165 object.__setattr__(self, attr, value)
166 else:
167 # We throw everything else.
168 msg = f"{_typeStr(self._field)} has no attribute {attr}"
169 raise FieldValidationError(self._field, self._config_config, msg)
170
171 def __reduce__(self):
173 f"Proxy container for config field {self._field.name} cannot "
174 "be pickled; it should be converted to a built-in container before "
175 "being assigned to other objects or variables."
176 )
177
178
179class DictField(Field[Dict[KeyTypeVar, ItemTypeVar]], Generic[KeyTypeVar, ItemTypeVar]):
180 """A configuration field (`~lsst.pex.config.Field` subclass) that maps keys
181 and values.
182
183 The types of both items and keys are restricted to these builtin types:
184 `int`, `float`, `complex`, `bool`, and `str`). All keys share the same type
185 and all values share the same type. Keys can have a different type from
186 values.
187
188 Parameters
189 ----------
190 doc : `str`
191 A documentation string that describes the configuration field.
192 keytype : {`int`, `float`, `complex`, `bool`, `str`}, optional
193 The type of the mapping keys. All keys must have this type. Optional
194 if keytype and itemtype are supplied as typing arguments to the class.
195 itemtype : {`int`, `float`, `complex`, `bool`, `str`}, optional
196 Type of the mapping values. Optional if keytype and itemtype are
197 supplied as typing arguments to the class.
198 default : `dict`, optional
199 The default mapping.
200 optional : `bool`, optional
201 If `True`, the field doesn't need to have a set value.
202 dictCheck : callable
203 A function that validates the dictionary as a whole.
204 itemCheck : callable
205 A function that validates individual mapping values.
206 deprecated : None or `str`, optional
207 A description of why this Field is deprecated, including removal date.
208 If not None, the string is appended to the docstring for this Field.
209
210 See Also
211 --------
212 ChoiceField
213 ConfigChoiceField
214 ConfigDictField
215 ConfigField
216 ConfigurableField
217 Field
218 ListField
219 RangeField
220 RegistryField
221
222 Examples
223 --------
224 This field maps has `str` keys and `int` values:
225
226 >>> from lsst.pex.config import Config, DictField
227 >>> class MyConfig(Config):
228 ... field = DictField(
229 ... doc="Example string-to-int mapping field.",
230 ... keytype=str, itemtype=int,
231 ... default={})
232 ...
233 >>> config = MyConfig()
234 >>> config.field['myKey'] = 42
235 >>> print(config.field)
236 {'myKey': 42}
237 """
238
239 DictClass: type[Dict] = Dict
240
241 @staticmethod
243 params: tuple[type, ...] | tuple[str, ...], kwds: Mapping[str, Any]
244 ) -> Mapping[str, Any]:
245 if len(params) != 2:
246 raise ValueError("Only tuples of types that are length 2 are supported")
247 resultParams = []
248 for typ in params:
249 if isinstance(typ, str):
250 _typ = ForwardRef(typ)
251 # type ignore below because typeshed seems to be wrong. It
252 # indicates there are only 2 args, as it was in python 3.8, but
253 # 3.9+ takes 3 args. Attempt in old style and new style to
254 # work with both.
255 try:
256 result = _typ._evaluate(globals(), locals(), set()) # type: ignore
257 except TypeError:
258 # python 3.8 path
259 result = _typ._evaluate(globals(), locals())
260 if result is None:
261 raise ValueError("Could not deduce type from input")
262 typ = cast(type, result)
263 resultParams.append(typ)
264 keyType, itemType = resultParams
265 results = dict(kwds)
266 if (supplied := kwds.get("keytype")) and supplied != keyType:
267 raise ValueError("Conflicting definition for keytype")
268 else:
269 results["keytype"] = keyType
270 if (supplied := kwds.get("itemtype")) and supplied != itemType:
271 raise ValueError("Conflicting definition for itemtype")
272 else:
273 results["itemtype"] = itemType
274 return results
275
277 self,
278 doc,
279 keytype=None,
280 itemtype=None,
281 default=None,
282 optional=False,
283 dictCheck=None,
284 itemCheck=None,
285 deprecated=None,
286 ):
287 source = getStackFrame()
288 self._setup(
289 doc=doc,
290 dtype=Dict,
291 default=default,
292 check=None,
293 optional=optional,
294 source=source,
295 deprecated=deprecated,
296 )
297 if keytype is None:
298 raise ValueError(
299 "keytype must either be supplied as an argument or as a type argument to the class"
300 )
301 if keytype not in self.supportedTypes:
302 raise ValueError("'keytype' %s is not a supported type" % _typeStr(keytype))
303 elif itemtype is not None and itemtype not in self.supportedTypes:
304 raise ValueError("'itemtype' %s is not a supported type" % _typeStr(itemtype))
305 if dictCheck is not None and not hasattr(dictCheck, "__call__"):
306 raise ValueError("'dictCheck' must be callable")
307 if itemCheck is not None and not hasattr(itemCheck, "__call__"):
308 raise ValueError("'itemCheck' must be callable")
309
310 self.keytype = keytype
311 self.itemtype = itemtype
312 self.dictCheck = dictCheck
313 self.itemCheck = itemCheck
314
315 def validate(self, instance):
316 """Validate the field's value (for internal use only).
317
318 Parameters
319 ----------
320 instance : `lsst.pex.config.Config`
321 The configuration that contains this field.
322
323 Returns
324 -------
325 isValid : `bool`
326 `True` is returned if the field passes validation criteria (see
327 *Notes*). Otherwise `False`.
328
329 Notes
330 -----
331 This method validates values according to the following criteria:
332
333 - A non-optional field is not `None`.
334 - If a value is not `None`, is must pass the `ConfigField.dictCheck`
335 user callback functon.
336
337 Individual item checks by the `ConfigField.itemCheck` user callback
338 function are done immediately when the value is set on a key. Those
339 checks are not repeated by this method.
340 """
341 Field.validate(self, instance)
342 value = self.__get____get____get__(instance)
343 if value is not None and self.dictCheck is not None and not self.dictCheck(value):
344 msg = "%s is not a valid value" % str(value)
345 raise FieldValidationError(self, instance, msg)
346
348 self,
349 instance: Config,
350 value: Mapping[KeyTypeVar, ItemTypeVar] | None,
351 at: Any = None,
352 label: str = "assignment",
353 ) -> None:
354 if instance._frozen:
355 msg = "Cannot modify a frozen Config. Attempting to set field to value %s" % value
356 raise FieldValidationError(self, instance, msg)
357
358 if at is None:
359 at = getCallStack()
360 if value is not None:
361 value = self.DictClass(instance, self, value, at=at, label=label)
362 else:
363 history = instance._history.setdefault(self.namenamename, [])
364 history.append((value, at, label))
365
366 instance._storage[self.namenamename] = value
367
368 def toDict(self, instance):
369 """Convert this field's key-value pairs into a regular `dict`.
370
371 Parameters
372 ----------
373 instance : `lsst.pex.config.Config`
374 The configuration that contains this field.
375
376 Returns
377 -------
378 result : `dict` or `None`
379 If this field has a value of `None`, then this method returns
380 `None`. Otherwise, this method returns the field's value as a
381 regular Python `dict`.
382 """
383 value = self.__get____get____get__(instance)
384 return dict(value) if value is not None else None
385
386 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
387 """Compare two fields for equality.
388
389 Used by `lsst.pex.ConfigDictField.compare`.
390
391 Parameters
392 ----------
393 instance1 : `lsst.pex.config.Config`
394 Left-hand side config instance to compare.
395 instance2 : `lsst.pex.config.Config`
396 Right-hand side config instance to compare.
397 shortcut : `bool`
398 If `True`, this function returns as soon as an inequality if found.
399 rtol : `float`
400 Relative tolerance for floating point comparisons.
401 atol : `float`
402 Absolute tolerance for floating point comparisons.
403 output : callable
404 A callable that takes a string, used (possibly repeatedly) to
405 report inequalities.
406
407 Returns
408 -------
409 isEqual : bool
410 `True` if the fields are equal, `False` otherwise.
411
412 Notes
413 -----
414 Floating point comparisons are performed by `numpy.allclose`.
415 """
416 d1 = getattr(instance1, self.namenamename)
417 d2 = getattr(instance2, self.namenamename)
418 name = getComparisonName(
419 _joinNamePath(instance1._name, self.namenamename), _joinNamePath(instance2._name, self.namenamename)
420 )
421 if not compareScalars("isnone for %s" % name, d1 is None, d2 is None, output=output):
422 return False
423 if d1 is None and d2 is None:
424 return True
425 if not compareScalars("keys for %s" % name, set(d1.keys()), set(d2.keys()), output=output):
426 return False
427 equal = True
428 for k, v1 in d1.items():
429 v2 = d2[k]
430 result = compareScalars(
431 f"{name}[{k!r}]", v1, v2, dtype=self.itemtype, rtol=rtol, atol=atol, output=output
432 )
433 if not result and shortcut:
434 return False
435 equal = equal and result
436 return equal
table::Key< int > type
Definition Detector.cc:163
table::Key< int > a
__get__(self, instance, owner=None, at=None, label="default")
Definition config.py:720
FieldTypeVar __get__(self, Config instance, Any owner=None, Any at=None, str label="default")
Definition config.py:717
Field[FieldTypeVar] __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition config.py:711
_setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition config.py:489
__init__(self, doc, keytype=None, itemtype=None, default=None, optional=False, dictCheck=None, itemCheck=None, deprecated=None)
Definition dictField.py:286
Mapping[str, Any] _parseTypingArgs(tuple[type,...]|tuple[str,...] params, Mapping[str, Any] kwds)
Definition dictField.py:244
None __set__(self, Config instance, Mapping[KeyTypeVar, ItemTypeVar]|None value, Any at=None, str label="assignment")
Definition dictField.py:353
_compare(self, instance1, instance2, shortcut, rtol, atol, output)
Definition dictField.py:386
bool __contains__(self, Any k)
Definition dictField.py:97
None __setitem__(self, KeyTypeVar k, ItemTypeVar x, Any at=None, str label="setitem", bool setHistory=True)
Definition dictField.py:102
ItemTypeVar __getitem__(self, KeyTypeVar k)
Definition dictField.py:88
None __delitem__(self, KeyTypeVar k, Any at=None, str label="delitem", bool setHistory=True)
Definition dictField.py:143
Iterator[KeyTypeVar] __iter__(self)
Definition dictField.py:94
__init__(self, config, field, value, at, label, setHistory=True)
Definition dictField.py:59
__setattr__(self, attr, value, at=None, label="assignment")
Definition dictField.py:159
daf::base::PropertySet * set
Definition fits.cc:927