22 from __future__
import print_function
23 from builtins
import str
29 from .config
import Config, Field, FieldValidationError, _typeStr, _joinNamePath
30 from .comparison
import getComparisonName, compareScalars, compareConfigs
32 __all__ = [
"ConfigChoiceField"]
37 Custom set class used to track the selection of multi-select
40 This class allows user a multi-select ConfigChoiceField to add/discard
41 items from the set of active configs. Each change to the selection is
42 tracked in the field's history.
44 def __init__(self, dict_, value, at=None, label="assignment", setHistory=True):
46 at = traceback.extract_stack()[:-1]
48 self.
_field = self._dict._field
49 self.
_config = self._dict._config
50 self.
__history = self._config._history.setdefault(self._field.name, [])
54 if v
not in self.
_dict:
56 self._dict.__getitem__(v, at=at)
58 msg =
"Value %s is of incorrect type %s. Sequence type expected"(value, _typeStr(value))
60 self.
_set = set(value)
65 self.__history.append((
"Set selection to %s" % self, at, label))
67 def add(self, value, at=None):
68 if self._config._frozen:
70 "Cannot modify a frozen Config")
73 at = traceback.extract_stack()[:-1]
75 if value
not in self.
_dict:
77 self._dict.__getitem__(value, at=at)
79 self.__history.append((
"added %s to selection" % value, at,
"selection"))
83 if self._config._frozen:
85 "Cannot modify a frozen Config")
87 if value
not in self.
_dict:
91 at = traceback.extract_stack()[:-1]
93 self.__history.append((
"removed %s from selection" % value, at,
"selection"))
94 self._set.discard(value)
100 return iter(self.
_set)
103 return value
in self.
_set
106 return repr(list(self.
_set))
109 return str(list(self.
_set))
113 """A dict of instantiated configs, used to populate a ConfigChoiceField.
115 typemap must support the following:
116 - typemap[name]: return the config class associated with the given name
119 collections.Mapping.__init__(self)
124 self.
_history = config._history.setdefault(field.name, [])
127 types = property(
lambda x: x._field.typemap)
130 return k
in self._field.typemap
133 return len(self._field.typemap)
136 return iter(self._field.typemap)
138 def _setSelection(self, value, at=None, label="assignment"):
139 if self._config._frozen:
140 raise FieldValidationError(self.
_field, self.
_config,
"Cannot modify a frozen Config")
143 at = traceback.extract_stack()[:-2]
147 elif self._field.multi:
150 if value
not in self.
_dict:
153 self._history.append((value, at, label))
156 if not self._field.multi:
158 "Single-selection field has no attribute 'names'")
161 def _setNames(self, value):
162 if not self._field.multi:
164 "Single-selection field has no attribute 'names'")
168 if not self._field.multi:
170 "Single-selection field has no attribute 'names'")
174 if self._field.multi:
176 "Multi-selection field has no attribute 'name'")
179 def _setName(self, value):
180 if self._field.multi:
182 "Multi-selection field has no attribute 'name'")
186 if self._field.multi:
188 "Multi-selection field has no attribute 'name'")
192 In a multi-selection ConfigInstanceDict, list of names of active items
193 Disabled In a single-selection _Regsitry)
195 names = property(_getNames, _setNames, _delNames)
198 In a single-selection ConfigInstanceDict, name of the active item
199 Disabled In a multi-selection _Regsitry)
201 name = property(_getName, _setName, _delName)
203 def _getActive(self):
207 if self._field.multi:
213 Readonly shortcut to access the selected item(s)
214 for multi-selection, this is equivalent to: [self[name] for name in self.names]
215 for single-selection, this is equivalent to: self[name]
217 active = property(_getActive)
221 value = self.
_dict[k]
224 dtype = self._field.typemap[k]
227 "Unknown key %r in Registry/ConfigChoiceField" % k)
228 name = _joinNamePath(self._config._name, self._field.name, k)
230 at = traceback.extract_stack()[:-1] + [dtype._source]
231 value = self._dict.setdefault(k, dtype(__name=name, __at=at, __label=label))
235 if self._config._frozen:
236 raise FieldValidationError(self.
_field, self.
_config,
"Cannot modify a frozen Config")
239 dtype = self._field.typemap[k]
241 raise FieldValidationError(self.
_field, self.
_config,
"Unknown key %r" % k)
243 if value != dtype
and type(value) != dtype:
244 msg =
"Value %s at key %k is of incorrect type %s. Expected type %s" % \
245 (value, k, _typeStr(value), _typeStr(dtype))
249 at = traceback.extract_stack()[:-1]
250 name = _joinNamePath(self._config._name, self._field.name, k)
251 oldValue = self._dict.get(k,
None)
254 self.
_dict[k] = value(__name=name, __at=at, __label=label)
256 self.
_dict[k] = dtype(__name=name, __at=at, __label=label, **value._storage)
260 oldValue.update(__at=at, __label=label, **value._storage)
262 def _rename(self, fullname):
263 for k, v
in self._dict.items():
264 v._rename(_joinNamePath(name=fullname, index=k))
267 if hasattr(getattr(self.__class__, attr,
None),
'__set__'):
269 object.__setattr__(self, attr, value)
270 elif attr
in self.__dict__
or attr
in [
"_history",
"_field",
"_config",
"_dict",
271 "_selection",
"__doc__"]:
273 object.__setattr__(self, attr, value)
276 msg =
"%s has no attribute %s" % (_typeStr(self.
_field), attr)
282 ConfigChoiceFields allow the config to choose from a set of possible Config types.
283 The set of allowable types is given by the typemap argument to the constructor
285 The typemap object must implement typemap[name], which must return a Config subclass.
287 While the typemap is shared by all instances of the field, each instance of
288 the field has its own instance of a particular sub-config type
292 class AaaConfig(Config):
293 somefield = Field(int, "...")
294 TYPEMAP = {"A", AaaConfig}
295 class MyConfig(Config):
296 choice = ConfigChoiceField("doc for choice", TYPEMAP)
298 instance = MyConfig()
299 instance.choice['AAA'].somefield = 5
300 instance.choice = "AAA"
302 Alternatively, the last line can be written:
303 instance.choice.name = "AAA"
305 Validation of this field is performed only the "active" selection.
306 If active is None and the field is not optional, validation will fail.
308 ConfigChoiceFields can allow single selections or multiple selections.
309 Single selection fields set selection through property name, and
310 multi-selection fields use the property names.
312 ConfigChoiceFields also allow multiple values of the same type:
313 TYPEMAP["CCC"] = AaaConfig
314 TYPEMAP["BBB"] = AaaConfig
316 When saving a config with a ConfigChoiceField, the entire set is saved, as well as the active selection
318 instanceDictClass = ConfigInstanceDict
320 def __init__(self, doc, typemap, default=None, optional=False, multi=False):
321 source = traceback.extract_stack(limit=2)[0]
322 self._setup(doc=doc, dtype=self.
instanceDictClass, default=default, check=
None, optional=optional,
327 def _getOrMake(self, instance, label="default"):
328 instanceDict = instance._storage.get(self.name)
329 if instanceDict
is None:
330 at = traceback.extract_stack()[:-2]
331 instanceDict = self.dtype(instance, self)
332 instanceDict.__doc__ = self.doc
333 instance._storage[self.name] = instanceDict
334 history = instance._history.setdefault(self.name, [])
335 history.append((
"Initialized from defaults", at, label))
340 if instance
is None or not isinstance(instance, Config):
345 def __set__(self, instance, value, at=None, label="assignment"):
347 raise FieldValidationError(self, instance,
"Cannot modify a frozen Config")
349 at = traceback.extract_stack()[:-1]
352 for k, v
in value.items():
353 instanceDict.__setitem__(k, v, at=at, label=label)
354 instanceDict._setSelection(value._selection, at=at, label=label)
357 instanceDict._setSelection(value, at=at, label=label)
360 instanceDict = self.
__get__(instance)
361 fullname = _joinNamePath(instance._name, self.name)
362 instanceDict._rename(fullname)
365 instanceDict = self.
__get__(instance)
366 if instanceDict.active
is None and not self.optional:
367 msg =
"Required field cannot be None"
368 raise FieldValidationError(self, instance, msg)
369 elif instanceDict.active
is not None:
371 for a
in instanceDict.active:
374 instanceDict.active.validate()
377 instanceDict = self.
__get__(instance)
381 dict_[
"names"] = instanceDict.names
383 dict_[
"name"] = instanceDict.name
386 for k, v
in instanceDict.items():
387 values[k] = v.toDict()
388 dict_[
"values"] = values
393 instanceDict = self.
__get__(instance)
394 for v
in instanceDict.values():
397 def save(self, outfile, instance):
398 instanceDict = self.
__get__(instance)
399 fullname = _joinNamePath(instance._name, self.name)
400 for v
in instanceDict.values():
403 outfile.write(
u"{}.names={!r}\n".
format(fullname, instanceDict.names))
405 outfile.write(
u"{}.name={!r}\n".
format(fullname, instanceDict.name))
408 """Customize deep-copying, because we always want a reference to the original typemap.
410 WARNING: this must be overridden by subclasses if they change the constructor signature!
412 other = type(self)(doc=self.doc, typemap=self.
typemap, default=copy.deepcopy(self.default),
413 optional=self.optional, multi=self.
multi)
414 other.source = self.source
417 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
418 """Helper function for Config.compare; used to compare two fields for equality.
420 Only the selected config(s) are compared, as the parameters of any others do not matter.
422 @param[in] instance1 LHS Config instance to compare.
423 @param[in] instance2 RHS Config instance to compare.
424 @param[in] shortcut If True, return as soon as an inequality is found.
425 @param[in] rtol Relative tolerance for floating point comparisons.
426 @param[in] atol Absolute tolerance for floating point comparisons.
427 @param[in] output If not None, a callable that takes a string, used (possibly repeatedly)
428 to report inequalities.
430 Floating point comparisons are performed by numpy.allclose; refer to that for details.
432 d1 = getattr(instance1, self.name)
433 d2 = getattr(instance2, self.name)
435 _joinNamePath(instance1._name, self.name),
436 _joinNamePath(instance2._name, self.name)
438 if not compareScalars(
"selection for %s" % name, d1._selection, d2._selection, output=output):
440 if d1._selection
is None:
443 nested = [(k, d1[k], d2[k])
for k
in d1._selection]
445 nested = [(d1._selection, d1[d1._selection], d2[d1._selection])]
447 for k, c1, c2
in nested:
448 result =
compareConfigs(
"%s[%r]" % (name, k), c1, c2, shortcut=shortcut,
449 rtol=rtol, atol=atol, output=output)
450 if not result
and shortcut:
452 equal = equal
and result