Package qubx :: Module accept
[hide private]
[frames] | no frames]

Source Code for Module qubx.accept

  1  """Functions for parsing strings into various data types, ranging from 
  2  string (a no-op) to a system of differential equations. 
  3   
  4  An "accept" function takes a string, and returns a value of the desired type. 
  5  If the conversion is impossible, it raises a descriptive exception. 
  6   
  7  Accept functions are used to validate input in various components in qubx.GTK and qubx.tables. 
  8   
  9   
 10  Copyright 2007-2014 Research Foundation State University of New York  
 11  This file is part of QUB Express.                                           
 12   
 13  QUB Express is free software; you can redistribute it and/or modify           
 14  it under the terms of the GNU General Public License as published by  
 15  the Free Software Foundation, either version 3 of the License, or     
 16  (at your option) any later version.                                   
 17   
 18  QUB Express is distributed in the hope that it will be useful,                
 19  but WITHOUT ANY WARRANTY; without even the implied warranty of        
 20  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         
 21  GNU General Public License for more details.                          
 22   
 23  You should have received a copy of the GNU General Public License,    
 24  named LICENSE.txt, in the QUB Express program directory.  If not, see         
 25  <http://www.gnu.org/licenses/>.                                       
 26   
 27  """ 
 28   
 29  import re 
 30  import qubx.pyenv 
 31  import __main__ 
 32  from qubx.util_types import * 
 33  from itertools import izip 
 34  from math import * 
 35   
36 -class FieldAcceptException(Exception):
37 - def __init__(self, badStr, msg):
38 self.badStr = badStr 39 self.msg = msg
40 - def __str__(self):
41 return self.msg # + ' (' + self.badStr + ')'
42
43 -def acceptNothing(x):
44 raise FieldAcceptException(x, 'not editable')
45
46 -def acceptEval(typ=float):
47 main_globals = qubx.pyenv.env.globals if qubx.pyenv.env else __main__.__dict__ 48 def accept(x): 49 return typ(eval(x, main_globals))
50 return accept 51
52 -def acceptString(x):
53 return x
54
55 -def acceptStringNonempty(x):
56 if x: 57 return x 58 raise FieldAcceptException(x, "can't be empty")
59
60 -def acceptInt(x):
61 try: 62 return int(x) 63 except: 64 raise FieldAcceptException(x, "must be an integer")
65
66 -def acceptFloat(x):
67 try: 68 return float(x) 69 except: 70 raise FieldAcceptException(x, "must be a number")
71
72 -def acceptFloatNonzero(x):
73 try: 74 v = float(x) 75 except: 76 raise FieldAcceptException(x, "must be a number other than 0") 77 if v == 0.0: 78 raise FieldAcceptException(x, "can't be 0") 79 return v
80
81 -def acceptXorUnset(accept):
82 """Interprets '' as UNSET_VALUE, otherwise uses provided accept function.""" 83 def acc(x): 84 if x == '': 85 return UNSET_VALUE 86 else: 87 return accept(x)
88 return acc 89
90 -def acceptFloatOrUnset(x):
91 """Like acceptFloat, but acceptFloatOrUnset('') returns qubx.util_types.UNSET_VALUE.""" 92 if x == '': 93 return UNSET_VALUE 94 return acceptFloat(x)
95
96 -def formatFloatOrUnset(fmt):
97 """Returns f(val): if val != qubx.util_types.UNSET_VALUE: return fmt(val); else: return ''""" 98 fmt_func = acceptFormat(fmt) 99 def format(x): 100 if abs(x - UNSET_VALUE) < 1e-6: 101 return '' 102 return fmt_func(x)
103 return format 104
105 -def acceptBool(x):
106 """Returns False if x is in ['', 'False', 'false']; True otherwise.""" 107 try: 108 if x.lower() == 'false': 109 return False 110 except: 111 pass 112 return bool(x)
113
114 -def acceptCmp(convert, compare, errmsg):
115 """Returns an accept function which enforces `compare(convert(s)) == True. 116 117 @param errmsg: string for complaining 118 """ 119 def accept(s): 120 try: 121 x = convert(s) 122 if not compare(x): 123 raise FieldAcceptException(s, errmsg) 124 return x 125 except ValueError: 126 raise FieldAcceptException(s, errmsg)
127 return accept 128 129 errmsg_cmp = lambda cmpname: lambda typename, bound: "must be %s %s %s" % (typename, cmpname, bound) 130 name_int = "an integer" 131 name_float = "a number" 132 errmsg_gt = errmsg_cmp("greater than") 133 errmsg_ge = errmsg_cmp("greater than or equal to") 134 errmsg_lt = errmsg_cmp("less than") 135 errmsg_le = errmsg_cmp("less than or equal to") 136
137 -def acceptIntGreaterThan(lb):
138 return acceptCmp(int, lambda x:x>lb, errmsg_gt(name_int, lb))
139 -def acceptIntGreaterThanOrEqualTo(lb):
140 return acceptCmp(int, lambda x:x>=lb, errmsg_ge(name_int, lb))
141 -def acceptIntLessThan(ub):
142 return acceptCmp(int, lambda x:x<ub, errmsg_lt(name_int, ub))
143 -def acceptIntLessThanOrEqualTo(ub):
144 return acceptCmp(int, lambda x:x<=ub, errmsg_le(name_int, ub))
145 -def acceptIntBetween(lb, ub):
146 return acceptCmp(int, lambda x:lb<=x<=ub, "must be %s between %s and %s" % (name_int, lb, ub))
147 -def acceptFloatGreaterThan(lb):
148 return acceptCmp(float, lambda x:x>lb, errmsg_gt(name_float, lb))
149 -def acceptFloatGreaterThanOrEqualTo(lb):
150 return acceptCmp(float, lambda x:x>=lb, errmsg_ge(name_float, lb))
151 -def acceptFloatLessThan(ub):
152 return acceptCmp(float, lambda x:x<ub, errmsg_lt(name_float, ub))
153 -def acceptFloatLessThanOrEqualTo(ub):
154 return acceptCmp(float, lambda x:x<=ub, errmsg_le(name_float, ub))
155 -def acceptFloatBetween(lb, ub):
156 return acceptCmp(float, lambda x:lb<=x<=ub, "must be %s between %s and %s" % (name_float, lb, ub))
157 -def acceptFloatAbsBetween(lb, ub):
158 return acceptCmp(float, lambda x:(lb<=x<=ub) or ((-ub)<=x<=(-lb)), "must be %s between +/- %s and %s" % (name_float, lb, ub))
159 160 161 # eats all the spaces commas and dashes, so don't accept on those
162 -def acceptAcceptList(s, accept, dashrange, description):
163 "e.g. '1, 2, 3-5 7' -> [1, 2, 3, 4, 5, 7]" 164 165 if s == '': 166 return [] 167 168 # replace \d+ ?- ?\d+ with \d+-\d+ 169 s = re.sub(r'([^ -]+) ?- ?([^ -]+)', r'\1-\2', s) 170 # split along [, ]+ 171 tokens = re.split(r'[ ,\t]+', s) 172 # foreach token: if \d+ append int; elif \d+-\d+ append ints 173 result = [] 174 for tok in tokens: 175 match = re.search(r'([^ -]+)-([^ -]+)', tok) 176 try: 177 if match: 178 a, b = accept(match.group(1)), accept(match.group(2)) 179 result[len(result):] = dashrange(a, b) 180 elif tok: 181 result.append( accept(tok) ) 182 except FieldAcceptException: 183 raise FieldAcceptException(s, 'must be a list of %s, separated by "," and "-"' % description) 184 return result
185
186 -def acceptList(acceptor, dashrange, description):
187 return lambda x: acceptAcceptList(x, acceptor, dashrange, description)
188
189 -def acceptIntList(acceptor=acceptInt, description='integers'):
190 def dashrange(i, j): 191 if i <= j: 192 return xrange(i, j+1) 193 else: 194 return xrange(i, j-1, -1)
195 return acceptList(acceptor, dashrange, description) 196
197 -def acceptFloatList(acceptor=acceptFloat, description='numbers'):
198 def dashrange(i, j): 199 raise FieldAcceptException('%f-%f', 'How shall I enumerate a continuous range?')
200 return acceptList(acceptor, dashrange, description) 201
202 -def acceptDimList(acceptList, dim, msg):
203 def checkDim(lst): 204 if dim != len(lst): 205 raise FieldAcceptException(s, msg) 206 return lst
207 return lambda x: checkDim( acceptList(x) ) 208
209 -def formatList(fmt=str):
210 f = acceptFormat(fmt) 211 def fmtList(lst): 212 try: 213 return ', '.join(f(x) for x in lst) 214 except TypeError: 215 return f(lst)
216 return fmtList 217
218 -def proper_minus(s):
219 u = s if (s.__class__ == unicode) else s.decode('utf-8') 220 return u.replace(u'-', u'\u2212')
221
222 -def MissingName(msg):
223 """Returns the symbol named in a NameError's message.""" 224 m = re.search("'([^']+)'", msg) 225 if not m: 226 raise NameError, msg 227 return m.group(1)
228
229 -class FuncAcceptor(object):
230 """ 231 FuncAcceptor instance is callable as fa(x, custom1, custom2, ...). 232 233 @ivar f: the expression as lambda locals..., x, custom1, custom2, ...: 234 @ivar args: a list of argument names, starting with "x" 235 @ivar expr: the function string as it was entered 236 @ivar local_args: names of defined local vars 237 @ivar local_vals: corresponding values 238 """
239 - def __init__(self, f, args, expr, local_args=[], local_vals=[], recipe="", static=None, typ=None):
240 self.f = f 241 self.args = args 242 self.expr = expr 243 self.local_args = local_args 244 self.local_vals = local_vals 245 self.recipe = recipe 246 self.static = static 247 self.typ = typ 248 self.__global_names = None # for pickle
249 - def __call__(self, *args, **kw):
250 #print self.local_args, self.args 251 #print self.local_vals, args 252 #print kw 253 return self.f(*list(self.local_vals+list(args)), **kw)
254 - def __str__(self):
255 return self.expr
256 - def __repr__(self):
257 return '%s(%s)' % (self.recipe, repr(self.expr))
258 # pickle protocol: must customize because it can't pickle functions, especially closures with arbitrary global refs
259 - def __getnewargs__(self):
260 if self.static is None: 261 raise TypeError("Can't pickle qubx.accept.FuncAcceptor without explicit static args") 262 #for x in (None, self.args, self.expr, self.local_args, self.local_vals, self.recipe, self.static, self.typ): 263 # print x 264 return (None, self.args, self.expr, self.local_args, self.local_vals, self.recipe, self.static, self.typ)
265 - def __getstate__(self):
266 if self.static is None: 267 raise TypeError("Can't pickle qubx.accept.FuncAcceptor without explicit static args") 268 mydict = self.__dict__.copy() 269 del mydict['f'] # can't pickle function, have to re-create including global state 270 globs = {} # find any relevant globals 271 main_globals = qubx.pyenv.env.globals if qubx.pyenv.env else __main__.__dict__ 272 if self.__global_names is None: # time-consuming name lookup only the first time 273 self.__global_names = [] 274 # all possible local vars as dict 275 locs = dict( (nm, val) for nm, val in izip(self.local_args, self.local_vals) ) 276 for nm in self.args: 277 locs[nm] = 1.0 278 unwanted_names = ['__builtins__'] # auto-added to globs by eval 279 valid = False 280 while not valid: 281 try: 282 try: 283 func = eval(self.expr, globs, locs) 284 except (ValueError, OverflowError, ZeroDivisionError): 285 pass 286 valid = True 287 except NameError as msg: 288 nm = MissingName(str(msg)) 289 globs[nm] = main_globals[nm] 290 if 'numpy.ufunc' in str(globs[nm].__class__): 291 unwanted_names.append(nm) 292 else: 293 self.__global_names.append(nm) 294 for nm in unwanted_names: 295 if nm in globs: 296 del globs[nm] 297 else: 298 for nm in self.__global_names: 299 globs[nm] = main_globals[nm] 300 return mydict, globs
301 - def __setstate__(self, state):
302 self.__dict__, globals = state 303 # copy globals into main namespace 304 main_globals = qubx.pyenv.env.globals if qubx.pyenv.env else __main__.__dict__ 305 for k,v in globals: 306 main_globals[k] = v 307 locals = dict( (nm, val) for nm, val in izip(self.local_args, self.local_vals) ) 308 try: 309 self.f = acceptF(static=self.static, custom=True, typ=self.typ, locals=locals)(self.expr) 310 except: 311 self.f = acceptF()('0')
312
313 -def acceptF(static=["x"], avail=[], custom=False, typ=float, default="1.0", locals=None):
314 """Returns an accept function which converts a Python expression into a L{FuncAcceptor} 315 316 @param static: names arguments which are always present 317 @param avail: names which are added to args if they are in the expression 318 @param custom: whether to add unrecognized names to args, or re-raise the NameError 319 @param typ: desired result type 320 @param default: expression to use in place of '' 321 @param locals: optional dictionary of stuff avail in expr 322 """ 323 def accept(eqn): 324 main_globals = qubx.pyenv.env.globals if qubx.pyenv.env else __main__.__dict__ 325 ty = any_type if (typ is None) else typ 326 recipe = 'acceptF(static=%s, avail=%s, custom=%s, typ=%s, default=%s, locals=%s)' % (static, avail, custom, ty.__name__, default, locals) 327 local_args = [] 328 local_vals = [] 329 args = static[:] 330 valid = False 331 func = lambda : 0.0 332 while not valid: 333 try: 334 try: 335 func = eval( "lambda " + join(local_args+args, ',') + ': ' + (eqn or default), main_globals) 336 # fval = ty( func( *list(local_vals + [1.0]*(len(args))) ) ) 337 func_args = list(local_vals + [1.0]*(len(args))) 338 func_out = func(*func_args) 339 fval = ty(func_out) 340 except (ValueError, OverflowError, ZeroDivisionError): 341 pass 342 valid = 1 343 except NameError as msg: 344 missing = MissingName(str(msg)) 345 if locals and (missing in locals): 346 local_args.append(missing) 347 local_vals.append(locals[missing]) 348 elif custom or (missing in avail): 349 args.append(missing) 350 else: 351 raise 352 #print args 353 return FuncAcceptor(f=func, args=args, expr=eqn, local_args=local_args, local_vals=local_vals, recipe=recipe, static=static, typ=typ)
354 return accept 355
356 -def any_type(x):
357 return x
358
359 -def Union(ls):
360 """Given an iterable of subiterables, returns a list of unique elements in the subiterables.""" 361 have = {} 362 out = [] 363 for l in ls: 364 for x in l: 365 if (not (x in have)): 366 have[x] = 1 367 out.append(x) 368 return out
369
370 -class ODEAcceptor(object):
371 """ 372 @ivar dy: dy(x, custom1, custom2, ..., yname1, yname2, ...) -> [partial of yname wrt x] 373 @ivar args: list of names of args and fit parameters: [x, custom1, custom2, ...] 374 @ivar ynames: list of names of differentiated variables 375 """
376 - def __init__(self, dy, args, ynames, expr):
377 self.f = None 378 self.dy = dy 379 self.args = args 380 self.ynames = ynames 381 self.expr = expr
382 - def __str__(self):
383 return self.expr
384
385 -def acceptODEs(static=["x"], avail=[], custom=False, locals=None):
386 """Returns an accept function which converts a string of ODEs into a L{ODEAcceptor}. 387 388 @param static: names arguments which are always present 389 @param avail: names which are added to args if they are in the expression 390 @param custom: whether to add unrecognized names to args, or re-raise the NameError 391 @param locals: optional dictionary of stuff avail in expr 392 393 The expression can have one or more ODEs separated by ';'. Each ODE has the form 394 395 >>> "foo' = do_something(foo, bar)" 396 397 For example, a sinusoidal: 398 399 >>> "a' = b; b' = -a" 400 """ 401 402 def accept(expr): 403 nospace = re.sub(r"[ \t]+", "", expr) 404 eqns = [x for x in re.split(r";", nospace) if x] 405 lrs = [re.split(r"'=", eqn) for eqn in eqns] 406 for i,x in enumerate(lrs): 407 if len(x) != 2: 408 raise Exception("Malformed differential equation: %s" % eqns[i]) 409 if len(dict(lrs)) < len(lrs): 410 raise Exception("Duplicate variable name.") 411 vars = [l for l,r in lrs] 412 if any((v in static) or (v in avail) for v in vars): 413 raise Exception("Differential variables can't have the same name as data series.") 414 # static_vars = static + vars 415 # ok now we acceptF(static_vars, avail, custom)(each r) 416 # to get each F.args. We combine them into a new static_custom_vars, new acceptF 417 # and re-accept to get derivs[] with the same signature 418 accept1 = acceptF(static+vars, avail, custom, locals=locals) 419 static_vars = static+vars 420 customs = [x for x in Union([accept1(r).args for l,r in lrs]) if not (x in static_vars)] 421 #print static+customs+vars 422 accept2 = acceptF(static+customs+vars, [], False, locals=locals) 423 derivs = [accept2(r) for l,r in lrs] 424 def deriv(*args, **kw): 425 return [d(*args, **kw) for d in derivs]
426 return ODEAcceptor(dy=deriv, args=static+customs, ynames=vars, expr=expr) 427 # dy(static, ...customs..., ...y...) -> dydx[] 428 return accept 429 430
431 -def acceptODEorF(static=["x"], avail=[], custom=False, locals=None):
432 """Returns an accept function which acts like acceptODEs if ' is present, otherwise like acceptF.""" 433 aO = acceptODEs(static, avail, custom, locals=locals) 434 aF = acceptF(static, avail, custom, locals=locals) 435 def accept(x): 436 if re.search("'", x): 437 return aO(x) 438 else: 439 return aF(x)
440 return accept 441 442 443 # for uniform handling of "format" arguments to qub stuff:
444 -def acceptFormat(format):
445 """Returns a function which converts something to a string. If C{format} is callable, 446 it's returned back unchanged. Otherwise we assume it's a format string e.g. "%.8g", 447 and the returned function is lambda val: format % val.""" 448 if hasattr(format, '__call__'): 449 return format 450 else: 451 return lambda v: format % v
452