22 import traceback, copy, collections
24 from .config
import Config, Field, FieldValidationError, _typeStr, _joinNamePath
25 from .comparison
import getComparisonName, compareScalars, compareConfigs
27 __all__ = [
"ConfigChoiceField"]
31 Custom set class used to track the selection of multi-select
34 This class allows user a multi-select ConfigChoiceField to add/discard
35 items from the set of active configs. Each change to the selection is
36 tracked in the field's history.
38 def __init__(self, dict_, value, at=None, label="assignment", setHistory=True):
40 at = traceback.extract_stack()[:-1]
44 self.
__history = self._config._history.setdefault(self._field.name, [])
48 if v
not in self.
_dict:
50 self._dict.__getitem__(v, at=at)
52 msg =
"Value %s is of incorrect type %s. Sequence type expected"(value,
_typeStr(value))
59 self.__history.append((
"Set selection to %s"%self, at, label))
61 def add(self, value, at= None):
62 if self._config._frozen:
64 "Cannot modify a frozen Config")
67 at = traceback.extract_stack()[:-1]
69 if value
not in self.
_dict:
71 self._dict.__getitem__(value, at=at)
73 self.__history.append((
"added %s to selection"%value, at,
"selection"))
77 if self._config._frozen:
79 "Cannot modify a frozen Config")
81 if value
not in self.
_dict:
85 at = traceback.extract_stack()[:-1]
87 self.__history.append((
"removed %s from selection"%value, at,
"selection"))
88 self._set.discard(value)
98 """A dict of instantiated configs, used to populate a ConfigChoiceField.
100 typemap must support the following:
101 - typemap[name]: return the config class associated with the given name
104 collections.Mapping.__init__(self)
109 self.
_history = config._history.setdefault(field.name, [])
112 types = property(
lambda x: x._field.typemap)
116 def __len__(self):
return len(self._field.typemap)
118 def __iter__(self):
return iter(self._field.typemap)
121 if self._config._frozen:
122 raise FieldValidationError(self.
_field, self.
_config,
"Cannot modify a frozen Config")
125 at = traceback.extract_stack()[:-2]
129 elif self._field.multi:
132 if value
not in self.
_dict:
135 self._history.append((value, at, label))
138 if not self._field.multi:
140 "Single-selection field has no attribute 'names'")
143 if not self._field.multi:
145 "Single-selection field has no attribute 'names'")
148 if not self._field.multi:
150 "Single-selection field has no attribute 'names'")
154 if self._field.multi:
156 "Multi-selection field has no attribute 'name'")
159 if self._field.multi:
161 "Multi-selection field has no attribute 'name'")
164 if self._field.multi:
166 "Multi-selection field has no attribute 'name'")
170 In a multi-selection ConfigInstanceDict, list of names of active items
171 Disabled In a single-selection _Regsitry)
173 names = property(_getNames, _setNames, _delNames)
176 In a single-selection ConfigInstanceDict, name of the active item
177 Disabled In a multi-selection _Regsitry)
179 name = property(_getName, _setName, _delName)
185 if self._field.multi:
191 Readonly shortcut to access the selected item(s)
192 for multi-selection, this is equivalent to: [self[name] for name in self.names]
193 for single-selection, this is equivalent to: self[name]
195 active = property(_getActive)
199 value = self.
_dict[k]
202 dtype = self._field.typemap[k]
205 "Unknown key %r in Registry/ConfigChoiceField" % k)
206 name =
_joinNamePath(self._config._name, self._field.name, k)
208 at = traceback.extract_stack()[:-1] + [dtype._source]
209 value = self._dict.setdefault(k, dtype(__name=name, __at=at, __label=label))
213 if self._config._frozen:
214 raise FieldValidationError(self.
_field, self.
_config,
"Cannot modify a frozen Config")
217 dtype = self._field.typemap[k]
219 raise FieldValidationError(self.
_field, self.
_config,
"Unknown key %r"%k)
221 if value != dtype
and type(value) != dtype:
222 msg =
"Value %s at key %k is of incorrect type %s. Expected type %s"%\
227 at = traceback.extract_stack()[:-1]
228 name =
_joinNamePath(self._config._name, self._field.name, k)
229 oldValue = self._dict.get(k,
None)
232 self.
_dict[k] = value(__name=name, __at=at, __label=label)
234 self.
_dict[k] = dtype(__name=name, __at=at, __label=label, **value._storage)
238 oldValue.update(__at=at, __label=label, **value._storage)
241 for k, v
in self._dict.iteritems():
245 if hasattr(getattr(self.__class__, attr,
None),
'__set__'):
247 object.__setattr__(self, attr, value)
248 elif attr
in self.__dict__
or attr
in [
"_history",
"_field",
"_config",
"_dict",
249 "_selection",
"__doc__"]:
251 object.__setattr__(self, attr, value)
262 ConfigChoiceFields allow the config to choose from a set of possible Config types.
263 The set of allowable types is given by the typemap argument to the constructor
265 The typemap object must implement typemap[name], which must return a Config subclass.
267 While the typemap is shared by all instances of the field, each instance of
268 the field has its own instance of a particular sub-config type
272 class AaaConfig(Config):
273 somefield = Field(int, "...")
274 TYPEMAP = {"A", AaaConfig}
275 class MyConfig(Config):
276 choice = ConfigChoiceField("doc for choice", TYPEMAP)
278 instance = MyConfig()
279 instance.choice['AAA'].somefield = 5
280 instance.choice = "AAA"
282 Alternatively, the last line can be written:
283 instance.choice.name = "AAA"
285 Validation of this field is performed only the "active" selection.
286 If active is None and the field is not optional, validation will fail.
288 ConfigChoiceFields can allow single selections or multiple selections.
289 Single selection fields set selection through property name, and
290 multi-selection fields use the property names.
292 ConfigChoiceFields also allow multiple values of the same type:
293 TYPEMAP["CCC"] = AaaConfig
294 TYPEMAP["BBB"] = AaaConfig
296 When saving a config with a ConfigChoiceField, the entire set is saved, as well as the active selection
298 instanceDictClass = ConfigInstanceDict
299 def __init__(self, doc, typemap, default=None, optional=False, multi=False):
300 source = traceback.extract_stack(limit=2)[0]
301 self._setup( doc=doc, dtype=self.
instanceDictClass, default=default, check=
None, optional=optional, source=source)
306 instanceDict = instance._storage.get(self.name)
307 if instanceDict
is None:
308 at = traceback.extract_stack()[:-2]
309 instanceDict = self.dtype(instance, self)
310 instanceDict.__doc__ = self.doc
311 instance._storage[self.name] = instanceDict
312 history = instance._history.setdefault(self.name, [])
313 history.append((
"Initialized from defaults", at, label))
318 if instance
is None or not isinstance(instance, Config):
323 def __set__(self, instance, value, at=None, label="assignment"):
325 raise FieldValidationError(self, instance,
"Cannot modify a frozen Config")
327 at = traceback.extract_stack()[:-1]
330 for k,v
in value.iteritems():
331 instanceDict.__setitem__(k, v, at=at, label=label)
332 instanceDict._setSelection(value._selection, at=at, label=label)
335 instanceDict._setSelection(value, at=at, label=label)
338 instanceDict = self.
__get__(instance)
340 instanceDict._rename(fullname)
343 instanceDict = self.
__get__(instance)
344 if instanceDict.active
is None and not self.optional:
345 msg =
"Required field cannot be None"
346 raise FieldValidationError(self, instance, msg)
347 elif instanceDict.active
is not None:
349 for a
in instanceDict.active:
352 instanceDict.active.validate()
355 instanceDict = self.
__get__(instance)
359 dict_[
"names"]=instanceDict.names
361 dict_[
"name"] =instanceDict.name
364 for k, v
in instanceDict.iteritems():
366 dict_[
"values"]=values
371 instanceDict = self.
__get__(instance)
372 for v
in instanceDict.itervalues():
375 def save(self, outfile, instance):
376 instanceDict = self.
__get__(instance)
378 for v
in instanceDict.itervalues():
381 print >> outfile,
"%s.names=%r"%(fullname, instanceDict.names)
383 print >> outfile,
"%s.name=%r"%(fullname, instanceDict.name)
386 """Customize deep-copying, because we always want a reference to the original typemap.
388 WARNING: this must be overridden by subclasses if they change the constructor signature!
390 other = type(self)(doc=self.doc, typemap=self.
typemap, default=copy.deepcopy(self.default),
391 optional=self.optional, multi=self.
multi)
392 other.source = self.source
395 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
396 """Helper function for Config.compare; used to compare two fields for equality.
398 Only the selected config(s) are compared, as the parameters of any others do not matter.
400 @param[in] instance1 LHS Config instance to compare.
401 @param[in] instance2 RHS Config instance to compare.
402 @param[in] shortcut If True, return as soon as an inequality is found.
403 @param[in] rtol Relative tolerance for floating point comparisons.
404 @param[in] atol Absolute tolerance for floating point comparisons.
405 @param[in] output If not None, a callable that takes a string, used (possibly repeatedly)
406 to report inequalities.
408 Floating point comparisons are performed by numpy.allclose; refer to that for details.
410 d1 = getattr(instance1, self.name)
411 d2 = getattr(instance2, self.name)
416 if not compareScalars(
"selection for %s" % name, d1._selection, d2._selection, output=output):
418 if d1._selection
is None:
421 nested = [(k, d1[k], d2[k])
for k
in d1._selection]
423 nested = [(d1._selection, d1[d1._selection], d2[d1._selection])]
425 for k, c1, c2
in nested:
426 result =
compareConfigs(
"%s[%r]" % (name, k), c1, c2, shortcut=shortcut,
427 rtol=rtol, atol=atol, output=output)
428 if not result
and shortcut:
430 equal = equal
and result