# Copyright (C) 2002, 2003 by Intevation GmbH # Authors: # Thomas Arendsen Hein # # This program is free software under the GPL (>=v2) # Read the file COPYING coming with the software for details. """ Scientific Parameter """ __version__ = "$Revision: 1.51 $" # $Source: /greaterrepository/sciparam/SciParam/parameter.py,v $ # $Id: parameter.py,v 1.51 2003/09/29 16:39:19 thomas Exp $ import types from range import Range from distribution import Distribution class Nothing: """Dummy class for default parameters.""" pass class SciParam: """Base class for scientific parameters.""" _attr = ['name', 'description', 'unit', 'default', 'value', 'comment', 'required', 'notunknown', 'disabled', 'hook_isvalid', 'hook_isusual', 'hook_updated'] unknown = ['unknown', 'Unknown', '', 'None', None] def __init__(self, *args, **kwargs): """Create a SciParam instance. Accept arguments listed in self._attr with default value 'None'. Non-keyword arguments are not allowed for derived classes. """ if len(args) > len(SciParam._attr): raise TypeError, ( "%s.%s() takes at most %d non-keyword arguments (%d given)" % (self.__class__, __name__, len(self._attr)+1, len(args)+1)) # merge non-keyword args into kwargs for key, value in zip(SciParam._attr, args): if kwargs.has_key(key): raise TypeError, ( "%s.%s() got multiple values for '%s'" % (self.__class__, __name__, key)) else: kwargs[key] = value # set attributes from kwargs for key in self._attr: self.__setattr__(key, kwargs.get(key)) if kwargs.has_key(key): del kwargs[key] if kwargs: raise TypeError, ( "%s.%s() got an unexpected keyword argument '%s'" % (self.__class__, __name__, kwargs.keys()[0])) if not self.name: raise ValueError, "%s got no name." % (self.__class__,) def isvalid(self, value=Nothing, errors=[]): """are values valid (or unknown if allowed) for this parameter?""" if value is Nothing: value = self.value unknown = self.is1unknown(value) ok = not (self.notunknown and unknown) if ok: ok = unknown or self.is1valid(value, errors) else: errors.append("%s must be given." % (self.name,)) for hook in self.hook_isvalid: ok = ok and hook(self, value, errors) return ok def is1valid(self, value, errors): """is exactly one value valid for this parameter?""" return 1 def isusual(self, value=Nothing, errors=[]): """are values usual (or unknown if allowed) for this parameter?""" if value is Nothing: value = self.value unknown = self.is1unknown(value) ok = not (self.notunknown and unknown) if ok: ok = unknown or self.is1usual(value, errors) else: errors.append("%s must be given." % (self.name,)) for hook in self.hook_isusual: ok = ok and hook(self, value, errors) return ok def is1usual(self, value, errors): """is exactly one value usual for this parameter?""" return self.is1valid(value, errors) def isunknown(self, value=Nothing): """is value unknown?""" if value is Nothing: value = self.value return self.is1unknown(value) def is1unknown(self, value): """is exactly one value unknown?""" return value in self.unknown def range(self): """Return a string telling what kind of value is expected.""" return "" def convert(self, value): """Convert string or number to internal representation. internal representation is: value or None return internal representation or raise ValueError """ if self.is1unknown(value): return None else: return value def string(self, value): """convert internal representation to string""" if self.is1unknown(value): return self.unknown[0] else: return str(value) def normalize(self, value, default=None): """Convert string to normalized string. return either a string that is valid for convert() or default """ try: if self.is1unknown(value): return default else: return self.string(self.convert(value)) except ValueError, why: return default def __str__(self): return self.string(self.value) def __setattr__(self, name, value): if name == 'value': self.__dict__[name] = self.convert(value) elif name == 'default': self.__dict__[name] = self.normalize(value) elif name in ['name', 'description', 'unit', 'comment'] \ and value == None: self.__dict__[name] = '' elif name in ['required', 'notunknown', 'disabled'] and value == None: self.__dict__[name] = 0 elif name in ['hook_isvalid', 'hook_isusual', 'hook_updated']: if value: if not hasattr(self, name): self.__dict__[name] = [] self.__dict__[name].append(value) else: self.__dict__[name] = [] elif name in self._attr: self.__dict__[name] = value else: raise AttributeError, "%s has no attribute '%s'" % \ (self.__class__, name) class FloatParam(SciParam): """Float values with warning and error range.""" _attr = SciParam._attr + ['wrange', 'erange'] def is1valid(self, value, errors): """is exactly one value valid for this parameter?""" return self.is1inrange(value, errors) def is1usual(self, value, errors): """is exactly one value usual for this parameter?""" return self.is1inrange(value, errors, usual=1) def is1inrange(self, value, errors, usual=0): """is exactly one value (can be string) in range? usual=0: check for valid range (default) usual=1: check for usual range """ if type(value) == types.StringType: value = self.convert(value) if usual: inrange = value in self.wrange if not inrange: errors.append("%s is outside usual range." % (self.name,)) else: inrange = value in self.erange if not inrange: errors.append("%s is outside valid range." % (self.name,)) return inrange def range(self): """return a string telling what kind of value is expected""" any = Range() if self.wrange == any: if self.erange == any: return "Decimal" else: return "Decimal: valid is %s" % self.erange else: if self.erange == any: return "Decimal: usual is %s" % self.wrange else: return "Decimal: usual is %s valid is %s" % \ (self.wrange, self.erange) def convert(self, value): """Convert string or number to internal representation internal representation is: float or None return internal representation or raise ValueError """ if self.is1unknown(value): return None else: return float(value) def __setattr__(self, name, value): if name in ['wrange', 'erange']: if isinstance(value, Range): self.__dict__[name] = value else: self.__dict__[name] = Range(value) else: SciParam.__setattr__(self, name, value) class DistParam(FloatParam): """Float values with warning/error range and optional distribution.""" def is1inrange(self, value, errors, usual=0): """is exactly one value (can be string) in range? usual=0: check for valid range (default) usual=1: check for usual range """ if type(value) in [types.StringType, types.FloatType, types.IntType]: value = self.convert(value) elif not isinstance(value, Distribution): raise ValueError, "is1inrange() doesn't accept this value" errors.append("%s:" % self.name) if usual: inrange = value.isusual(errors=errors) else: inrange = value.isvalid(errors=errors) if inrange: try: for ci_name, ci_value in value.confidence_interval(): ci_par = FloatParam(ci_name, wrange=self.wrange, erange=self.erange) inrange = ci_par.is1inrange(ci_value, errors, usual=usual) if not inrange: break except OverflowError, why: errors.append(str(why)) inrange = 0 if inrange: errors.pop() return inrange def is1unknown(self, value): """is exactly one value unknown?""" if isinstance(value, Distribution): return value.descriptives[0] in self.unknown else: return FloatParam.is1unknown(self, value) def convert(self, value): """Convert string or number to internal representation. internal representation is: Distribution or None return internal representation or raise ValueError """ if isinstance(value, Distribution): return Distribution(value) try: return Distribution(FloatParam.convert(self, value)) except ValueError, why: pass if type(value) != types.StringType: raise ValueError, \ "%s doesn't accept this value: %r" % (self.__class__, value) parts = value.split('/', 1) if len(parts) == 2: parts, dtype = parts else: if hasattr(self, 'value'): dtype = self.value.type else: dtype = None parts = value parts = parts.split(';') descriptives = [FloatParam.convert(self, val) for val in parts] return Distribution(tuple(descriptives), dtype) def string(self, value): """convert internal representation to string""" if isinstance(value, Distribution): if value.type == Distribution.none: return FloatParam.string(self, value.descriptives[0]) else: result = [FloatParam.string(self, val) for val in value.descriptives] if self.is1unknown(value): return self.unknown[0] else: return "%s/%s" % (';'.join(result), value.type) else: return FloatParam.string(self, value) class IntParam(FloatParam): """Long integer values with warning and error range.""" def range(self): """return a string telling what kind of value is expected""" return FloatParam.range(self).replace('Decimal', 'Integer') def convert(self, value): """Convert string or number to internal representation. internal representation is: long int or None return internal representation or raise ValueError """ if self.is1unknown(value): return None else: # first make a float to catch numbers like 1E+02 for 100 return int(float(value)) class StringParam(SciParam): """String values with maximum length.""" _attr = SciParam._attr + ['maxlength'] def is1valid(self, value, errors): """is exactly one value valid for this parameter?""" ok = not (self.maxlength and len(value) > self.maxlength) if not ok: errors.append("%s is too long." % (self.name,)) errors.append("Maximum length is %d characters." % self.maxlength) return ok def range(self): """return a string telling what kind of value is expected""" if self.maxlength: return "String: at most %d characters" % self.maxlength else: return "String" def convert(self, value): """Convert string or number to internal representation. internal representation is: string or None return internal representation or raise ValueError """ if self.is1unknown(value): return None else: return str(value) def __setattr__(self, name, value): if name == 'maxlength': if value == None: self.__dict__[name] = 0 elif value < 0: raise ValueError, "maxlength is negative" else: self.__dict__[name] = value else: SciParam.__setattr__(self, name, value) class ChoiceParam(SciParam): """Values from a list of choices. long=1: state that this will be a long list of choices """ _attr = ['choices', 'long'] + SciParam._attr unknown_yes_no = [(SciParam.unknown[0], None), ('Yes', 1), ('No', 0)] yes_no = unknown_yes_no[1:] def is1valid(self, value, errors): """is exactly one value valid for this parameter?""" try: self.convert(value) return 1 except ValueError, why: errors.append("%s has no valid choice." % (self.name,)) return 0 def convert(self, value): """Convert string or number to internal representation. internal representation is: (choice string, choice value) return internal representation or raise ValueError """ if self.choicedict.has_key(value): return (value, self.choicedict[value]) elif self.valuedict.has_key(value): return (self.valuedict[value], value) else: raise ValueError, \ "%s instance with name '%s' has no choice '%s'" % ( self.__class__, self.name, value) def string(self, value): """convert internal representation to string""" return SciParam.string(self, value[0]) def __str__(self): return self.string((self.currentchoice, self.value)) def __setattr__(self, name, value): if name == 'value': if value is None: value = self.choices[0] self.__dict__['currentchoice'], \ self.__dict__[name] = self.convert(value) elif name == 'choices': if value == None: value = self.unknown_yes_no self.__dict__[name] = [] self.__dict__['choicedict'] = {} self.__dict__['valuedict'] = {} for choice in value: if self.is1unknown(choice): choice = self.unknown_yes_no[0] elif type(choice) == types.TupleType: choice = str(choice[0]), choice[1] else: choice = str(choice), choice self.__dict__['choices'].append(choice[0]) self.__dict__['choicedict'][choice[0]] = choice[1] self.__dict__['valuedict'][choice[1]] = choice[0] elif name == 'long' and value == None: self.__dict__[name] = 0 else: SciParam.__setattr__(self, name, value) if __name__ == "__main__": import os.path, sys print "Use test_%s to test this module." % os.path.basename(sys.argv[0])