LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
LSST Data Management Base Package
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 sys
34import weakref
35from typing import Any, ForwardRef, Generic, Iterator, Mapping, Type, TypeVar, Union, 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
53if int(sys.version_info.minor) < 9:
54 _bases = (collections.abc.MutableMapping, Generic[KeyTypeVar, ItemTypeVar])
55else:
56 _bases = (collections.abc.MutableMapping[KeyTypeVar, ItemTypeVar],)
57
58
59class Dict(*_bases):
60 """An internal mapping container.
61
62 This class emulates a `dict`, but adds validation and provenance.
63 """
64
65 def __init__(self, config, field, value, at, label, setHistory=True):
66 self._field_field = field
67 self._config__config_ = weakref.ref(config)
68 self._dict_dict = {}
69 self._history_history = self._config_config._history.setdefault(self._field_field.name, [])
70 self.__doc____doc__ = field.doc
71 if value is not None:
72 try:
73 for k in value:
74 # do not set history per-item
75 self.__setitem____setitem__(k, value[k], at=at, label=label, setHistory=False)
76 except TypeError:
77 msg = "Value %s is of incorrect type %s. Mapping type expected." % (value, _typeStr(value))
78 raise FieldValidationError(self._field_field, self._config_config, msg)
79 if setHistory:
80 self._history_history.append((dict(self._dict_dict), at, label))
81
82 @property
83 def _config(self) -> Config:
84 # Config Fields should never outlive their config class instance
85 # assert that as such here
86 value = self._config__config_()
87 assert value is not None
88 return value
89
90 history = property(lambda x: x._history)
91 """History (read-only).
92 """
93
94 def __getitem__(self, k: KeyTypeVar) -> ItemTypeVar:
95 return self._dict_dict[k]
96
97 def __len__(self) -> int:
98 return len(self._dict_dict)
99
100 def __iter__(self) -> Iterator[KeyTypeVar]:
101 return iter(self._dict_dict)
102
103 def __contains__(self, k: Any) -> bool:
104 return k in self._dict_dict
105
107 self, k: KeyTypeVar, x: ItemTypeVar, at: Any = None, label: str = "setitem", setHistory: bool = True
108 ) -> None:
109 if self._config_config._frozen:
110 msg = "Cannot modify a frozen Config. Attempting to set item at key %r to value %s" % (k, x)
111 raise FieldValidationError(self._field_field, self._config_config, msg)
112
113 # validate keytype
114 k = _autocast(k, self._field_field.keytype)
115 if type(k) != self._field_field.keytype:
116 msg = "Key %r is of type %s, expected type %s" % (k, _typeStr(k), _typeStr(self._field_field.keytype))
117 raise FieldValidationError(self._field_field, self._config_config, msg)
118
119 # validate itemtype
120 x = _autocast(x, self._field_field.itemtype)
121 if self._field_field.itemtype is None:
122 if type(x) not in self._field_field.supportedTypes and x is not None:
123 msg = "Value %s at key %r is of invalid type %s" % (x, k, _typeStr(x))
124 raise FieldValidationError(self._field_field, self._config_config, msg)
125 else:
126 if type(x) != self._field_field.itemtype and x is not None:
127 msg = "Value %s at key %r is of incorrect type %s. Expected type %s" % (
128 x,
129 k,
130 _typeStr(x),
131 _typeStr(self._field_field.itemtype),
132 )
133 raise FieldValidationError(self._field_field, self._config_config, msg)
134
135 # validate item using itemcheck
136 if self._field_field.itemCheck is not None and not self._field_field.itemCheck(x):
137 msg = "Item at key %r is not a valid value: %s" % (k, x)
138 raise FieldValidationError(self._field_field, self._config_config, msg)
139
140 if at is None:
141 at = getCallStack()
142
143 self._dict_dict[k] = x
144 if setHistory:
145 self._history_history.append((dict(self._dict_dict), at, label))
146
148 self, k: KeyTypeVar, at: Any = None, label: str = "delitem", setHistory: bool = True
149 ) -> None:
150 if self._config_config._frozen:
151 raise FieldValidationError(self._field_field, self._config_config, "Cannot modify a frozen Config")
152
153 del self._dict_dict[k]
154 if setHistory:
155 if at is None:
156 at = getCallStack()
157 self._history_history.append((dict(self._dict_dict), at, label))
158
159 def __repr__(self):
160 return repr(self._dict_dict)
161
162 def __str__(self):
163 return str(self._dict_dict)
164
165 def __setattr__(self, attr, value, at=None, label="assignment"):
166 if hasattr(getattr(self.__class__, attr, None), "__set__"):
167 # This allows properties to work.
168 object.__setattr__(self, attr, value)
169 elif attr in self.__dict__ or attr in ["_field", "_config_", "_history", "_dict", "__doc__"]:
170 # This allows specific private attributes to work.
171 object.__setattr__(self, attr, value)
172 else:
173 # We throw everything else.
174 msg = "%s has no attribute %s" % (_typeStr(self._field_field), attr)
175 raise FieldValidationError(self._field_field, self._config_config, msg)
176
177 def __reduce__(self):
179 f"Proxy container for config field {self._field.name} cannot "
180 "be pickled; it should be converted to a built-in container before "
181 "being assigned to other objects or variables."
182 )
183
184
185class DictField(Field[Dict[KeyTypeVar, ItemTypeVar]], Generic[KeyTypeVar, ItemTypeVar]):
186 """A configuration field (`~lsst.pex.config.Field` subclass) that maps keys
187 and values.
188
189 The types of both items and keys are restricted to these builtin types:
190 `int`, `float`, `complex`, `bool`, and `str`). All keys share the same type
191 and all values share the same type. Keys can have a different type from
192 values.
193
194 Parameters
195 ----------
196 doc : `str`
197 A documentation string that describes the configuration field.
198 keytype : {`int`, `float`, `complex`, `bool`, `str`}, optional
199 The type of the mapping keys. All keys must have this type. Optional
200 if keytype and itemtype are supplied as typing arguments to the class.
201 itemtype : {`int`, `float`, `complex`, `bool`, `str`}, optional
202 Type of the mapping values. Optional if keytype and itemtype are
203 supplied as typing arguments to the class.
204 default : `dict`, optional
205 The default mapping.
206 optional : `bool`, optional
207 If `True`, the field doesn't need to have a set value.
208 dictCheck : callable
209 A function that validates the dictionary as a whole.
210 itemCheck : callable
211 A function that validates individual mapping values.
212 deprecated : None or `str`, optional
213 A description of why this Field is deprecated, including removal date.
214 If not None, the string is appended to the docstring for this Field.
215
216 See also
217 --------
218 ChoiceField
219 ConfigChoiceField
220 ConfigDictField
221 ConfigField
222 ConfigurableField
223 Field
224 ListField
225 RangeField
226 RegistryField
227
228 Examples
229 --------
230 This field maps has `str` keys and `int` values:
231
232 >>> from lsst.pex.config import Config, DictField
233 >>> class MyConfig(Config):
234 ... field = DictField(
235 ... doc="Example string-to-int mapping field.",
236 ... keytype=str, itemtype=int,
237 ... default={})
238 ...
239 >>> config = MyConfig()
240 >>> config.field['myKey'] = 42
241 >>> print(config.field)
242 {'myKey': 42}
243 """
244
245 DictClass: Type[Dict] = Dict
246
247 @staticmethod
248 def _parseTypingArgs(
249 params: Union[tuple[type, ...], tuple[str, ...]], kwds: Mapping[str, Any]
250 ) -> Mapping[str, Any]:
251 if len(params) != 2:
252 raise ValueError("Only tuples of types that are length 2 are supported")
253 resultParams = []
254 for typ in params:
255 if isinstance(typ, str):
256 _typ = ForwardRef(typ)
257 # type ignore below because typeshed seems to be wrong. It
258 # indicates there are only 2 args, as it was in python 3.8, but
259 # 3.9+ takes 3 args. Attempt in old style and new style to
260 # work with both.
261 try:
262 result = _typ._evaluate(globals(), locals(), set()) # type: ignore
263 except TypeError:
264 # python 3.8 path
265 result = _typ._evaluate(globals(), locals())
266 if result is None:
267 raise ValueError("Could not deduce type from input")
268 typ = cast(type, result)
269 resultParams.append(typ)
270 keyType, itemType = resultParams
271 results = dict(kwds)
272 if (supplied := kwds.get("keytype")) and supplied != keyType:
273 raise ValueError("Conflicting definition for keytype")
274 else:
275 results["keytype"] = keyType
276 if (supplied := kwds.get("itemtype")) and supplied != itemType:
277 raise ValueError("Conflicting definition for itemtype")
278 else:
279 results["itemtype"] = itemType
280 return results
281
283 self,
284 doc,
285 keytype=None,
286 itemtype=None,
287 default=None,
288 optional=False,
289 dictCheck=None,
290 itemCheck=None,
291 deprecated=None,
292 ):
293 source = getStackFrame()
294 self._setup_setup(
295 doc=doc,
296 dtype=Dict,
297 default=default,
298 check=None,
299 optional=optional,
300 source=source,
301 deprecated=deprecated,
302 )
303 if keytype is None:
304 raise ValueError(
305 "keytype must either be supplied as an argument or as a type argument to the class"
306 )
307 if keytype not in self.supportedTypessupportedTypes:
308 raise ValueError("'keytype' %s is not a supported type" % _typeStr(keytype))
309 elif itemtype is not None and itemtype not in self.supportedTypessupportedTypes:
310 raise ValueError("'itemtype' %s is not a supported type" % _typeStr(itemtype))
311 if dictCheck is not None and not hasattr(dictCheck, "__call__"):
312 raise ValueError("'dictCheck' must be callable")
313 if itemCheck is not None and not hasattr(itemCheck, "__call__"):
314 raise ValueError("'itemCheck' must be callable")
315
316 self.keytypekeytype = keytype
317 self.itemtypeitemtype = itemtype
318 self.dictCheckdictCheck = dictCheck
319 self.itemCheckitemCheck = itemCheck
320
321 def validate(self, instance):
322 """Validate the field's value (for internal use only).
323
324 Parameters
325 ----------
326 instance : `lsst.pex.config.Config`
327 The configuration that contains this field.
328
329 Returns
330 -------
331 isValid : `bool`
332 `True` is returned if the field passes validation criteria (see
333 *Notes*). Otherwise `False`.
334
335 Notes
336 -----
337 This method validates values according to the following criteria:
338
339 - A non-optional field is not `None`.
340 - If a value is not `None`, is must pass the `ConfigField.dictCheck`
341 user callback functon.
342
343 Individual item checks by the `ConfigField.itemCheck` user callback
344 function are done immediately when the value is set on a key. Those
345 checks are not repeated by this method.
346 """
347 Field.validate(self, instance)
348 value = self.__get____get____get____get__(instance)
349 if value is not None and self.dictCheckdictCheck is not None and not self.dictCheckdictCheck(value):
350 msg = "%s is not a valid value" % str(value)
351 raise FieldValidationError(self, instance, msg)
352
354 self,
355 instance: Config,
356 value: Union[Mapping[KeyTypeVar, ItemTypeVar], None],
357 at: Any = None,
358 label: str = "assignment",
359 ) -> None:
360 if instance._frozen:
361 msg = "Cannot modify a frozen Config. Attempting to set field to value %s" % value
362 raise FieldValidationError(self, instance, msg)
363
364 if at is None:
365 at = getCallStack()
366 if value is not None:
367 value = self.DictClass(instance, self, value, at=at, label=label)
368 else:
369 history = instance._history.setdefault(self.name, [])
370 history.append((value, at, label))
371
372 instance._storage[self.name] = value
373
374 def toDict(self, instance):
375 """Convert this field's key-value pairs into a regular `dict`.
376
377 Parameters
378 ----------
379 instance : `lsst.pex.config.Config`
380 The configuration that contains this field.
381
382 Returns
383 -------
384 result : `dict` or `None`
385 If this field has a value of `None`, then this method returns
386 `None`. Otherwise, this method returns the field's value as a
387 regular Python `dict`.
388 """
389 value = self.__get____get____get____get__(instance)
390 return dict(value) if value is not None else None
391
392 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
393 """Compare two fields for equality.
394
395 Used by `lsst.pex.ConfigDictField.compare`.
396
397 Parameters
398 ----------
399 instance1 : `lsst.pex.config.Config`
400 Left-hand side config instance to compare.
401 instance2 : `lsst.pex.config.Config`
402 Right-hand side config instance to compare.
403 shortcut : `bool`
404 If `True`, this function returns as soon as an inequality if found.
405 rtol : `float`
406 Relative tolerance for floating point comparisons.
407 atol : `float`
408 Absolute tolerance for floating point comparisons.
409 output : callable
410 A callable that takes a string, used (possibly repeatedly) to
411 report inequalities.
412
413 Returns
414 -------
415 isEqual : bool
416 `True` if the fields are equal, `False` otherwise.
417
418 Notes
419 -----
420 Floating point comparisons are performed by `numpy.allclose`.
421 """
422 d1 = getattr(instance1, self.name)
423 d2 = getattr(instance2, self.name)
424 name = getComparisonName(
425 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
426 )
427 if not compareScalars("isnone for %s" % name, d1 is None, d2 is None, output=output):
428 return False
429 if d1 is None and d2 is None:
430 return True
431 if not compareScalars("keys for %s" % name, set(d1.keys()), set(d2.keys()), output=output):
432 return False
433 equal = True
434 for k, v1 in d1.items():
435 v2 = d2[k]
436 result = compareScalars(
437 "%s[%r]" % (name, k), v1, v2, dtype=self.itemtypeitemtype, rtol=rtol, atol=atol, output=output
438 )
439 if not result and shortcut:
440 return False
441 equal = equal and result
442 return equal
table::Key< int > type
Definition: Detector.cc:163
table::Key< int > a
"Field[FieldTypeVar]" __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition: config.py:713
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:722
FieldTypeVar __get__(self, "Config" instance, Any owner=None, Any at=None, str label="default")
Definition: config.py:719
def _setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition: config.py:494
def __init__(self, doc, keytype=None, itemtype=None, default=None, optional=False, dictCheck=None, itemCheck=None, deprecated=None)
Definition: dictField.py:292
def validate(self, instance)
Definition: dictField.py:321
None __set__(self, Config instance, Union[Mapping[KeyTypeVar, ItemTypeVar], None] value, Any at=None, str label="assignment")
Definition: dictField.py:359
bool __contains__(self, Any k)
Definition: dictField.py:103
None __setitem__(self, KeyTypeVar k, ItemTypeVar x, Any at=None, str label="setitem", bool setHistory=True)
Definition: dictField.py:108
ItemTypeVar __getitem__(self, KeyTypeVar k)
Definition: dictField.py:94
def __setattr__(self, attr, value, at=None, label="assignment")
Definition: dictField.py:165
None __delitem__(self, KeyTypeVar k, Any at=None, str label="delitem", bool setHistory=True)
Definition: dictField.py:149
def __init__(self, config, field, value, at, label, setHistory=True)
Definition: dictField.py:65
Iterator[KeyTypeVar] __iter__(self)
Definition: dictField.py:100
daf::base::PropertySet * set
Definition: fits.cc:912
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
def getCallStack(skip=0)
Definition: callStack.py:174
def getStackFrame(relative=0)
Definition: callStack.py:58
def compareScalars(name, v1, v2, output, rtol=1e-8, atol=1e-8, dtype=None)
Definition: comparison.py:62
def getComparisonName(name1, name2)
Definition: comparison.py:40