22 from __future__
import print_function
23 from builtins
import str
28 from .config
import Config, Field, FieldValidationError, _typeStr, _joinNamePath
29 from .comparison
import getComparisonName, compareScalars, compareConfigs
30 from .callStack
import getCallStack, getStackFrame
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):
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")
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:
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")
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)
231 at.insert(0, dtype._source)
232 value = self._dict.setdefault(k, dtype(__name=name, __at=at, __label=label))
236 if self._config._frozen:
237 raise FieldValidationError(self.
_field, self.
_config,
"Cannot modify a frozen Config")
240 dtype = self._field.typemap[k]
242 raise FieldValidationError(self.
_field, self.
_config,
"Unknown key %r" % k)
244 if value != dtype
and type(value) != dtype:
245 msg =
"Value %s at key %k is of incorrect type %s. Expected type %s" % \
246 (value, k, _typeStr(value), _typeStr(dtype))
251 name = _joinNamePath(self._config._name, self._field.name, k)
252 oldValue = self._dict.get(k,
None)
255 self.
_dict[k] = value(__name=name, __at=at, __label=label)
257 self.
_dict[k] = dtype(__name=name, __at=at, __label=label, **value._storage)
261 oldValue.update(__at=at, __label=label, **value._storage)
263 def _rename(self, fullname):
264 for k, v
in self._dict.items():
265 v._rename(_joinNamePath(name=fullname, index=k))
268 if hasattr(getattr(self.__class__, attr,
None),
'__set__'):
270 object.__setattr__(self, attr, value)
271 elif attr
in self.__dict__
or attr
in [
"_history",
"_field",
"_config",
"_dict",
272 "_selection",
"__doc__"]:
274 object.__setattr__(self, attr, value)
277 msg =
"%s has no attribute %s" % (_typeStr(self.
_field), attr)
283 ConfigChoiceFields allow the config to choose from a set of possible Config types.
284 The set of allowable types is given by the typemap argument to the constructor
286 The typemap object must implement typemap[name], which must return a Config subclass.
288 While the typemap is shared by all instances of the field, each instance of
289 the field has its own instance of a particular sub-config type
293 class AaaConfig(Config):
294 somefield = Field(int, "...")
295 TYPEMAP = {"A", AaaConfig}
296 class MyConfig(Config):
297 choice = ConfigChoiceField("doc for choice", TYPEMAP)
299 instance = MyConfig()
300 instance.choice['AAA'].somefield = 5
301 instance.choice = "AAA"
303 Alternatively, the last line can be written:
304 instance.choice.name = "AAA"
306 Validation of this field is performed only the "active" selection.
307 If active is None and the field is not optional, validation will fail.
309 ConfigChoiceFields can allow single selections or multiple selections.
310 Single selection fields set selection through property name, and
311 multi-selection fields use the property names.
313 ConfigChoiceFields also allow multiple values of the same type:
314 TYPEMAP["CCC"] = AaaConfig
315 TYPEMAP["BBB"] = AaaConfig
317 When saving a config with a ConfigChoiceField, the entire set is saved, as well as the active selection
319 instanceDictClass = ConfigInstanceDict
321 def __init__(self, doc, typemap, default=None, optional=False, multi=False):
323 self._setup(doc=doc, dtype=self.
instanceDictClass, default=default, check=
None, optional=optional,
328 def _getOrMake(self, instance, label="default"):
329 instanceDict = instance._storage.get(self.name)
330 if instanceDict
is None:
332 instanceDict = self.dtype(instance, self)
333 instanceDict.__doc__ = self.doc
334 instance._storage[self.name] = instanceDict
335 history = instance._history.setdefault(self.name, [])
336 history.append((
"Initialized from defaults", at, label))
341 if instance
is None or not isinstance(instance, Config):
346 def __set__(self, instance, value, at=None, label="assignment"):
348 raise FieldValidationError(self, instance,
"Cannot modify a frozen Config")
353 for k, v
in value.items():
354 instanceDict.__setitem__(k, v, at=at, label=label)
355 instanceDict._setSelection(value._selection, at=at, label=label)
358 instanceDict._setSelection(value, at=at, label=label)
361 instanceDict = self.
__get__(instance)
362 fullname = _joinNamePath(instance._name, self.name)
363 instanceDict._rename(fullname)
366 instanceDict = self.
__get__(instance)
367 if instanceDict.active
is None and not self.optional:
368 msg =
"Required field cannot be None"
369 raise FieldValidationError(self, instance, msg)
370 elif instanceDict.active
is not None:
372 for a
in instanceDict.active:
375 instanceDict.active.validate()
378 instanceDict = self.
__get__(instance)
382 dict_[
"names"] = instanceDict.names
384 dict_[
"name"] = instanceDict.name
387 for k, v
in instanceDict.items():
388 values[k] = v.toDict()
389 dict_[
"values"] = values
394 instanceDict = self.
__get__(instance)
395 for v
in instanceDict.values():
398 def save(self, outfile, instance):
399 instanceDict = self.
__get__(instance)
400 fullname = _joinNamePath(instance._name, self.name)
401 for v
in instanceDict.values():
404 outfile.write(
u"{}.names={!r}\n".
format(fullname, instanceDict.names))
406 outfile.write(
u"{}.name={!r}\n".
format(fullname, instanceDict.name))
409 """Customize deep-copying, because we always want a reference to the original typemap.
411 WARNING: this must be overridden by subclasses if they change the constructor signature!
413 other = type(self)(doc=self.doc, typemap=self.
typemap, default=copy.deepcopy(self.default),
414 optional=self.optional, multi=self.
multi)
415 other.source = self.source
418 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
419 """Helper function for Config.compare; used to compare two fields for equality.
421 Only the selected config(s) are compared, as the parameters of any others do not matter.
423 @param[in] instance1 LHS Config instance to compare.
424 @param[in] instance2 RHS Config instance to compare.
425 @param[in] shortcut If True, return as soon as an inequality is found.
426 @param[in] rtol Relative tolerance for floating point comparisons.
427 @param[in] atol Absolute tolerance for floating point comparisons.
428 @param[in] output If not None, a callable that takes a string, used (possibly repeatedly)
429 to report inequalities.
431 Floating point comparisons are performed by numpy.allclose; refer to that for details.
433 d1 = getattr(instance1, self.name)
434 d2 = getattr(instance2, self.name)
436 _joinNamePath(instance1._name, self.name),
437 _joinNamePath(instance2._name, self.name)
439 if not compareScalars(
"selection for %s" % name, d1._selection, d2._selection, output=output):
441 if d1._selection
is None:
444 nested = [(k, d1[k], d2[k])
for k
in d1._selection]
446 nested = [(d1._selection, d1[d1._selection], d2[d1._selection])]
448 for k, c1, c2
in nested:
449 result =
compareConfigs(
"%s[%r]" % (name, k), c1, c2, shortcut=shortcut,
450 rtol=rtol, atol=atol, output=output)
451 if not result
and shortcut:
453 equal = equal
and result