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

Source Code for Module qubx.util_types

  1  """General-purpose types and utilities. 
  2   
  3  Copyright 2007-2014 Research Foundation State University of New York  
  4  This file is part of QUB Express.                                           
  5   
  6  QUB Express is free software; you can redistribute it and/or modify           
  7  it under the terms of the GNU General Public License as published by  
  8  the Free Software Foundation, either version 3 of the License, or     
  9  (at your option) any later version.                                   
 10   
 11  QUB Express is distributed in the hope that it will be useful,                
 12  but WITHOUT ANY WARRANTY; without even the implied warranty of        
 13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         
 14  GNU General Public License for more details.                          
 15   
 16  You should have received a copy of the GNU General Public License,    
 17  named LICENSE.txt, in the QUB Express program directory.  If not, see         
 18  <http://www.gnu.org/licenses/>.                                       
 19   
 20  """ 
 21   
 22  import collections 
 23  import heapq 
 24  import json 
 25  import pprint 
 26  import traceback 
 27  import weakref 
 28  from math import * 
 29  from string import join 
 30  import re 
 31  import os 
 32  import sys 
 33   
 34  from contextlib import contextmanager 
 35   
 36  # Change ShowDropped to True to report dropped callbacks on the console. A callback is dropped when its weakref expires (no-one has a strong ref anymore). 
 37  # It can happen accidentally when assigning/subscribing an instance method, since python creates a fresh binding each time. See L{Reffer}. 
 38  ShowDropped = False 
 39   
 40  # Literally UNSET.VALUE on the phone keypad, this number indicates a floating-point field that was left intentionally blank 
 41  # (e.g. per-class vRev which can override the model-wide vRev). Beware of floating-point rounding and imprecision -- 
 42  # this probably should have been an exact power of 2 for simplicity. 
 43  UNSET_VALUE = 86738.82583 
44 45 -class Event(object):
46 """ 47 Usage: 48 49 >>> myEvent = Event() # a, b 50 >>> callback1 = lambda a, b: dostuff(a, b) 51 >>> callback2 = lambda a, b: otherstuff(a, b) 52 >>> myEvent += callback1 53 >>> myEvent += callback2 54 >>> myEvent(a, b) # calls both callbacks 55 >>> myEvent -= callback1 56 >>> myEvent -= callback2 57 >>> myEvent(a, b) # no callbacks; does nothing 58 59 Note: this is a great source of circular references; see gc.collect() 60 """ 61
62 - def __init__(self):
63 self._listeners = [] 64 self.enabled = True
65 - def __call__(self, *args, **kw):
66 if self.enabled: 67 for l in self._listeners: 68 l(*args, **kw)
69 - def __iadd__(self, l):
70 if not l in self._listeners: 71 self._listeners.append(l) 72 return self
73 - def __isub__(self, l):
74 if l in self._listeners: 75 self._listeners.remove(l) 76 return self
77 - def __nonzero__(self):
78 return len(self._listeners)
79
80 -def printer(*xx):
81 """Prints and returns its argument(s). Handy for testing callbacks.""" 82 if len(xx) == 1: 83 pprint.pprint(xx[0]) 84 # print xx[0] 85 return xx[0] 86 else: 87 for x in xx: 88 print x, 89 print 90 return xx
91
92 -class WeakEvent(object):
93 """ 94 Usage: 95 96 >>> myEvent = Event() # a, b 97 >>> callback1 = lambda a, b: dostuff(a, b) 98 >>> callback2 = lambda a, b: otherstuff(a, b) 99 >>> callback3 = self.callback3 100 >>> myEvent += callback1 101 >>> myEvent += callback2 102 >>> myEvent(a, b) # calls both callbacks 103 >>> myEvent -= callback1 104 >>> myEvent -= callback2 105 >>> myEvent(a, b) # no callbacks; does nothing 106 107 note: keep a ref somewhere, even to self.whatever, or it will be unlinked; see L{Reffer} 108 """ 109 __slots__ = ['_listeners', '_names', '_stacks', 'name', 'enabled', '__weakref__'] 110
111 - def __init__(self, name=""):
112 self._listeners = [] 113 self._names = [] 114 self._stacks = [] 115 self.name = name 116 self.enabled = True
117 - def __call__(self, *args, **kw):
118 dead = [] 119 if self.enabled: 120 for l in self._listeners: 121 listener = l() 122 if not (listener is None): 123 try: 124 listener(*args, **kw) 125 except: 126 traceback.print_stack() 127 traceback.print_exc() 128 else: 129 dead.append(l) 130 for l in dead: 131 i = self._listeners.index(l) 132 n = self._names[i] 133 if ShowDropped: 134 print "%s%sdropped callback%s%s" % (self.name, self.name and ' ', n and ': ', n) 135 for line in self._stacks[i]: 136 sys.stdout.write(" > %s"%line) 137 self._listeners.pop(i) 138 self._names.pop(i) 139 self._stacks.pop(i)
140 - def __iadd__(self, ln):
141 try: 142 l, n = ln 143 except: 144 l, n = ln, "" 145 if not l in self._listeners: 146 self._listeners.append(weakref.ref(l)) # , lambda l:printer("released "+n))) 147 self._names.append(n) 148 if ShowDropped: 149 self._stacks.append(traceback.format_stack()[:-1]) 150 else: 151 self._stacks.append("") 152 return self
153 - def __isub__(self, lsub):
154 dead = [] 155 found = None 156 for l in self._listeners: 157 listener = l() 158 if listener == lsub: 159 found = l 160 dead.append(l) 161 elif listener is None: 162 dead.append(l) 163 for l in dead: 164 i = self._listeners.index(l) 165 if ShowDropped and (l != found): 166 n = self._names[i] 167 print "%s%sdropped callback%s%s" % (self.name, self.name and ' ', n and ': ', n) 168 for line in self._stacks[i]: 169 sys.stdout.write(" > %s"%line) 170 self._listeners.pop(i) 171 self._names.pop(i) 172 self._stacks.pop(i) 173 if not found: # no matching event found 174 traceback.print_stack() 175 print 'Warning: in unsubscribe (operator -=) of WeakEvent%s: caller was not subscribed.' % (self.name and (' %s' % self.name)) 176 return self
177 - def __nonzero__(self):
178 return len(self._listeners)
179
180 181 182 -class WeakCall(object):
183 """Like WeakEvent, except there's at most one listener, and it can return a value.""" 184 __slots__ = ['name', '_listener', '_listener_name', 'enabled', '__weakref__']
185 - def __init__(self, name=""):
186 self.name = name 187 self._listener = None 188 self._listener_name = "" 189 self.enabled = True
190 - def __call__(self, *args, **kw):
191 if self.enabled and not (self._listener is None): 192 listener = self._listener() 193 if not (listener is None): 194 try: 195 return listener(*args, **kw) 196 except: 197 traceback.print_exc() 198 else: 199 if ShowDropped: 200 n = self._listener_name 201 print "%s%sdropped callback%s%s" % (self.name, self.name and ' ', n and ': ', n) 202 self._listener = None 203 self._listener_name = ""
204 - def assign(self, listener, name=""):
205 self._listener = None if (listener is None) else weakref.ref(listener) 206 self._listener_name = name
207 - def __nonzero__(self):
208 return not (self._listener is None)
209
210 211 # for WeakEvent and WeakCall, we need to keep bound-method references somewhere: 212 # self.r = Reffer() 213 # self.foo.OnFoo += self.r(self._onFoo) 214 215 -class Reffer(object):
216 """ 217 Holds references to keep objects alive. 218 Intended in combo with WeakEvent for bound method references e.g. self.onEvent, 219 since self.onEvent creates a temporary instance that would otherwise be destroyed. 220 221 Usage: 222 223 >>> self.ref = Reffer() 224 >>> self.evt = WeakEvent() 225 >>> self.evt += self.ref(self.onEvt) 226 >>> ... 227 >>> self.evt -= self.ref(self.onEvt) # hashes the same each time 228 """ 229 __slots__ = ["r"]
230 - def __init__(self):
231 self.r = {}
232 - def __call__(self, ref):
233 if not self.r.has_key(ref): 234 self.r[ref] = ref 235 return self.r[ref]
236 - def clear(self):
237 self.r.clear()
238
239 -class DeathRattle(object):
240 """Prints to stdout when it is finally freed, presumably at the same time as its owner. 241 242 Usage: 243 244 >>> self.death_rattle = DeathRattle('Free at last') 245 """
246 - def __init__(self, rattle):
247 self.rattle = rattle
248 - def __del__(self):
249 print self.rattle
250
251 -class Anon(object):
252 """ 253 An object whose fields are the constructor keyword arguments. 254 Requesting any other field returns None rather than raise NameError. 255 256 Example: 257 258 >>> x = Anon(stuff='x',n=5) 259 >>> print x.stuff, x.n, x.whatever 260 x, 5, None 261 """
262 - def __init__(self, *args, **kw):
263 for d in self._find_arg_dicts(*args, **kw): 264 for k,v in d.iteritems(): 265 self.__dict__[k] = v
266 - def _find_arg_dicts(self, *args, **kw):
267 dicts = [] 268 for arg in args: 269 if isinstance(arg, str): 270 try: 271 dicts.append( dict(eval(arg)) ) 272 except: 273 print '%s(): ignoring non-dict string argument: %s' % (self.__class__, arg) 274 elif isinstance(arg, dict): 275 dicts.append( arg ) 276 else: 277 try: 278 dicts.append( arg.__dict__ ) 279 except: 280 print '%s(): ignoring non-mapping argument: %s (%s)' % (self.__class__, arg, arg.__class__) 281 dicts.append(kw) 282 return dicts
283 # access missing field returns None instead of raising NameError
284 - def __getattr__(self, name):
285 try: 286 return self.__dict__[name] 287 except: 288 return None
289 - def __repr__(self):
290 return repr(self.__dict__)
291 - def __str__(self):
292 return self._str_indent()
293 - def _str_indent(self, indent=""):
294 def val2str(v): 295 if isinstance(v, Anon): 296 return v._str_indent(indent+" | ") 297 else: 298 return str(v)
299 return '\n'.join([str(self.__class__)] + 300 ['%s%16s: %s' % (indent,k,val2str(v)) for k,v in self.__dict__.iteritems()])
301 - def clone(self):
302 return self.__class__(**self.__dict__)
303 304 # to eval json boolean: 305 true = True 306 false = False
307 308 #import qubx.global_namespace 309 #qubx.global_namespace.json_problems = [] 310 311 -class JsonAnon(Anon):
312 - def __init__(self, *args, **kw):
313 for d in self._find_arg_dicts(*args, **kw): 314 for k,v in d.iteritems(): 315 self.__dict__[k] = self.re_replace_dicts(v)
316 - def __repr__(self):
317 return json.dumps(eval(repr(self.__dict__)))
318 #try: 319 # return json.dumps(eval(repr(self.__dict__))) 320 #except: 321 # qubx.global_namespace.json_problems.append(self) 322 # raise 323 # access missing field builds recursive JsonAnon
324 - def __getattr__(self, name):
325 try: 326 return self.__dict__[name] 327 except: 328 j = self.__class__() 329 self.__dict__[name] = j 330 return j
331 - def __setattr__(self, name, val):
333 - def re_replace_dicts(self, node):
334 if isinstance(node, tuple) or isinstance(node, list): 335 return [self.re_replace_dicts(v) for v in node] 336 elif isinstance(node, dict): 337 for k in node: 338 node[k] = self.re_replace_dicts(node[k]) 339 return self.__class__(node) 340 else: 341 return node
342
343 344 -def breduce(func, seq):
345 """like builtin reduce(), but recursing by splitting in half. Doesn't seem to make a difference.""" 346 if len(seq) < 1: 347 return None 348 elif len(seq) == 1: 349 return seq[0] 350 elif len(seq) == 2: 351 return func(seq[0], seq[1]) 352 else: 353 half = len(seq)/2 354 return func(breduce(func, seq[:half]), breduce(func, seq[half:]))
355
356 -def memoize(func, lbl=""):
357 """ 358 Returns wrapped func that caches return values for specific args. 359 All args must be immutable (e.g. tuples not lists). 360 361 Example: 362 363 >>> def factorial(n): 364 >>> if n > 1: 365 >>> return n * fast_fac(n-1) 366 >>> else: 367 >>> return 1 368 >>> fast_fac = memoize(factorial) 369 """ 370 results = {} 371 def wrapper(*args): 372 try: # if args in results: 373 return results[args] 374 except KeyError: # else: 375 result = func(*args) 376 results[args] = result 377 # print lbl, args, ':', result 378 return result
379 wrapper.func = func 380 wrapper.label = lbl 381 wrapper.results = results 382 wrapper.reset = results.clear 383 return wrapper 384
385 -def memoize_last_n(func, n=1, lbl=""):
386 """ 387 Returns wrapped func that caches most recent n return values for specific args. 388 All args must be immutable (e.g. tuples not lists). 389 """ 390 results = {} 391 keys = [ [] ] 392 def wrapper(*args): 393 if args in results: 394 return results[args] 395 else: 396 if n == len(keys[0]): 397 del results[keys[0][0]] 398 keys[0] = keys[0][1:] 399 result = func(*args) 400 results[args] = result 401 keys[0].append(args) 402 # print lbl, args, ':', result 403 return result
404 def reset(): 405 results.clear() 406 keys[0] = [] 407 wrapper.reset = reset 408 wrapper.results = results 409 return wrapper 410
411 -def memoize_heapq(func, max_weight=1000000, weigh=lambda x: 1, lbl=""):
412 """ 413 Returns wrapped func that caches return values for specific args. 414 All args must be immutable (e.g. tuples not lists). 415 416 Keeps most recent k results such that sum(weigh(result) for result in results[:k]) <= max_weight. 417 418 Example: 419 420 >>> def factorial(n): 421 >>> if n > 1: 422 >>> return n * fast_fac(n-1) 423 >>> else: 424 >>> return 1 425 >>> fast_fac = memoize(factorial) 426 """ 427 results = {} 428 order = [] 429 next = [-1000000000] # counting up 430 total_weight = [0] 431 def wrapper(*args): 432 if args in results: 433 index, result = results[args] 434 for i in reversed(xrange(len(order))): 435 if order[i][0] == index: 436 results[args] = (next[0], result) 437 order[i] = (next[0], args) 438 next[0] += 1 439 heapq_sink(order, i) 440 break 441 return result 442 else: 443 result = func(*args) 444 weight = total_weight[0] + weigh(result) 445 while weight > max_weight: 446 rem_args = heapq.heappop(order)[1] 447 weight -= weigh(results[rem_args]) 448 del results[rem_args] 449 total_weight[0] = weight 450 results[args] = (next[0], result) 451 heapq.heappush(order, (next[0], args)) 452 next[0] += 1 453 # print lbl, args, ':', result 454 return result
455 return wrapper 456
457 -def heapq_swim(heap, k):
458 while (k > 0) and (heap[(k+1)/2-1] > heap[k]): 459 heap[k], heap[(k+1)/2-1] = heap[(k+1)/2-1], heap[k]
460
461 -def heapq_sink(heap, k):
462 k2, N = (k+1)*2 - 1, len(heap) 463 if k2 >= N: return 464 if ((k2+1) < N) and (heap[k2] > heap[k2+1]): 465 k2 += 1 466 if heap[k] > heap[k2]: 467 heap[k], heap[k2] = heap[k2], heap[k] 468 heapq_sink(heap, k2)
469
470 -def heapq_remove(heap, index):
471 """Remove item from heap""" 472 # Move slot to be removed to top of heap 473 while index > 0: 474 up = (index + 1) / 2 - 1 475 heap[index] = heap[up] 476 index = up 477 # Remove top of heap and restore heap property 478 heapq.heappop(heap)
479
480 481 -def bind(callback, *args, **kw):
482 """ 483 Returns a function f which, when called, discards its own args and returns callback(*args, **kw). 484 485 Lambda can be useful to share one message handler among many sources, but there's a limitation: 486 >>> for i in xrange(N): 487 chk[i].OnToggle += self__ref(lambda chk: self.__onToggle(i)) 488 489 Here, the lambda binds not the value of i, but the stack location whose contents are repeatedly changed by the for loop. 490 So all the lambdas are effectively doing self.__onToggle(N-1). 491 492 Instead: 493 >>> for i in xrange(N): 494 chk[i].OnToggle += self.__ref(bind(self.__onToggle, i)) 495 496 This creates a unique stack frame for your unique i, so it does what you expect. 497 """ 498 499 def act(*act_args, **act_kw): 500 return callback(*args, **kw)
501 return act 502
503 -def bind_with_args(callback, *args, **kw):
504 """Like bind, but it appends the callback's positional args after the bound positional args; returns callback(*(args+cb_args), **kw).""" 505 def act(*act_args, **act_kw): 506 use_kw = kw.copy() 507 for k,v in act_kw.iteritems(): 508 use_kw[k] = v 509 return callback(*(args+act_args), **use_kw)
510 return act 511
512 -def bind_with_args_before(callback, *args, **kw):
513 """Like bind, but it prepends the callback's positional args before the bound positional args; returns callback(*(args+cb_args), **kw).""" 514 def act(*act_args, **act_kw): 515 use_kw = kw.copy() 516 for k,v in act_kw.iteritems(): 517 use_kw[k] = v 518 return callback(*(act_args+args), **use_kw)
519 return act 520
521 522 # all coords in [0,1] 523 -def HSVtoRGB(h,s,v):
524 """Returns (r,g,b) color coordinates corresponding to the hue, saturation and value. All coordinates are between 0 and 1.""" 525 h, s, v = map(float, (h, s, v)) 526 if h == 1.0: h = 0.0 527 h *= 360 528 529 if s == 0: 530 return v,v,v 531 532 sectorPos = h/60.0 533 sectorNumber = int(floor(sectorPos)) 534 fractionalSector = sectorPos - sectorNumber 535 536 p = v * (1 - s) 537 q = v * (1 - (s * fractionalSector)) 538 t = v * (1 - (s * (1 - fractionalSector))) 539 540 return {0 : (v, t, p), 541 1 : (q, v, p), 542 2 : (p, v, t), 543 3 : (p, q, v), 544 4 : (t, p, v), 545 5 : (v, p, q)} [sectorNumber]
546
547 -def RGBtoHSV(r,g,b):
548 """Returns the (hue, saturation, value) color coordinates corresponding to the RGB color. All coordinates are between 0 and 1.""" 549 r, g, b = map(float, (r, g, b)) 550 mn = min(r, g, b) 551 mx = max(r, g, b) 552 v = mx 553 delta = mx - mn 554 if (mx == 0.0) or (delta == 0.0): 555 return 0.0, 0.0, v 556 s = delta / mx 557 if r == mx: 558 h = ((g - b) / delta) % 6.0 559 elif g == mx: 560 h = 2.0 + (b - r) / delta 561 else: 562 h = 4.0 + (r - g) / delta 563 h *= 60.0 564 if h < 0: h += 360 565 return h/360.0, s, v
566
567 -def RGBtoCMY(r, g, b):
568 return (1-r, 1-g, 1-b)
569
570 -def RGBtoCMYK(r, g, b):
571 c, m, y = RGBtoCMY(r, g, b) 572 k = min(c, m, y) 573 return ((c-k)/(1-k), (m-k)/(1-k), (y-k)/(1-k))
574
575 576 -def ScaleToColor(x, saturation=1.0): # x in [-1..1]
577 """ 578 Returns r,g,b corresponding to x in [-1..1]. 579 The scale moves smoothly around the color wheel from pink (-) to red (+), 580 with brightness proportional to abs(x). 581 """ 582 y = sin(x*pi/2) 583 h = 0.44 * (1.0 - y) # leave off the last 12% of the wheel so 0 is distinguishable from 1 584 v = abs(x) 585 return HSVtoRGB(h, saturation, v*(2.0 - v)) 586 587 588 # toolspace/modelGTK constants useful everywhere 589 WITHALPHA = lambda tup: (len(tup) == 3) and tuple(list(tup)+[1]) or tup 590 NOALPHA = lambda tup: (len(tup) == 4) and tup[:3] or tup 591 SETALPHA = lambda tup, a: tuple(list(NOALPHA(tup)) + [a]) 592 ADDALPHA = lambda tup, a: tuple(list(tup) + [a])
593 594 -def INVERT_COLOR(tup):
595 out = list(tup[:]) 596 r,g,b = tup[:3] 597 h,s,v = RGBtoHSV(r, g, b) 598 out[:3] = HSVtoRGB((h+0.5) % 1.0, s, v) 599 return tuple(out)
600 601 602 COLOR = collections.defaultdict(lambda:(.5, .5, .5))
603 -def _init_colors():
604 for i,c in enumerate([(0,0,0), (1,0,0), (0,0,1), (0,1,0), (0,1,1), (1,1,0), (1,0,1)]): 605 COLOR[i] = c 606 offset = len(COLOR) 607 for i in xrange(20): 608 COLOR[offset+i] = HSVtoRGB((i*2.0/20)%1.0, (40.0-i)/40, (20.0+i)/40)
609 _init_colors() 610 del _init_colors 611 612 COLOR_CLASS = lambda c: ('modelGTK.class[%i]' % c, COLOR[c]) 613 614 rdname, rdbreak, rdbk = 'qubfast.txt', 'supporting', 'Expired.'
615 -def rdact():
616 import qubx.global_namespace 617 qubx.global_namespace.QubX.destroy()
618
619 -def path_splits(s):
620 """Returns the list of all path components '/a/b/c' -> ['/', 'a', 'b', 'c']""" 621 l,r = os.path.split(s) 622 if (l == ''): 623 return [r] 624 elif (l == s): 625 return [s] 626 else: 627 lst = path_splits(l) 628 lst.append(r) 629 return lst
630
631 -def add_ext_if_none(path, ext):
632 """Returns path unmodified if it already has an extension, otherwise appends ext.""" 633 if os.path.splitext(path)[1]: 634 return path 635 else: 636 return path + ext
637
638 639 -class Product(object):
640 - def __init__(self, get=None, *def_args):
641 self.serial = 0 642 self.__get = get or self.get 643 self.__last_serial = -1 644 self.__last_args = None 645 self.__last_val = None 646 self.__def_args = def_args[:] 647 self.OnInvalidate = WeakEvent() # (Product)
648 - def invalidate(self):
649 self.serial += 1 650 self.OnInvalidate(self)
651 - def get_val(self, *args):
652 if (self.serial != self.__last_serial) or (args != self.__last_args): 653 self.__last_val = self.__get(*args) 654 self.__last_serial = self.serial 655 self.__last_args = args 656 return self.__last_val
657 val = property(lambda self: self.get_val(*self.__def_args))
658 - def get(self):
659 raise Exception('abstract')
660
661 662 -class OverridingDict(dict):
663 """"Overrides" the base dict so keys are looked up first here, then if not found, in base dict."""
664 - def __init__(self, base=None, *args, **kw):
665 dict.__init__(self, *args, **kw) 666 self.base = base or {}
667 - def has_key(self, key):
668 return (dict.has_key(self, key)) or (key in self.base)
669 - def keys(self):
670 return list(set(dict.keys(self) + self.base.keys()))
671 - def __contains__(self, key):
672 return (dict.__contains__(self, key)) or self.base.__contains__(key)
673 - def __getitem__(self, key):
674 if dict.__contains__(self, key): 675 return dict.__getitem__(self, key) 676 else: 677 return self.base[key]
678 - def get(self, key):
679 if dict.__contains__(self, key): 680 return dict.__getitem__(self, key) 681 else: 682 return self.base.get(key)
683
684 685 -def scrolled_int(val, offset, fine=False, coarse=False, lo=None, hi=None):
686 """Returns val, increased or decreased proportional to offset. 687 688 @param val: integer value 689 @param offset: integer distance as supplied by gtk scroll-event 690 @param fine: True to use offset/10.0 691 @param coarse: True to use offset*10.0 692 @param lo: minimum output value 693 @param hi: maximum output value 694 """ 695 if fine: 696 offset /= 10.0 697 elif coarse: 698 offset *= 10.0 699 if val < 100: 700 offset = int(round(offset)) or ((offset >= 0.0) and 1 or -1) 701 result = val + offset 702 else: 703 sgn = 1 if val >= 0 else -1 704 result = int(round(val * pow(1.01, sgn*offset))) 705 if not (lo is None): 706 result = max(lo, result) 707 if not (hi is None): 708 result = min(hi, result) 709 return result
710
711 -def scrolled_float(val, offset, fine=False, coarse=False, lo=None, hi=None):
712 """Returns val scrolled according to offset direction and modifiers. 713 714 @param val: floating-point value 715 @param offset: integer distance as supplied by gtk scroll-event 716 @param fine: True to use offset/10.0 717 @param coarse: True to use offset*10.0 718 @param lo: minimum output value 719 @param hi: maximum output value 720 """ 721 if fine: 722 offset /= 10.0 723 elif coarse: 724 offset *= 10.0 725 sgn = 1 if val >= 0 else -1 726 result = val * pow(1.01, sgn*offset) 727 if not (lo is None): 728 result = max(lo, result) 729 if not (hi is None): 730 result = min(hi, result) 731 return result
732
733 -def scrolled_digits(val, offset, fine=False, coarse=False, lo=None, hi=None):
734 """Returns val scrolled according to offset direction and modifiers; 735 by default moves in increments of 10**(k-1), where 10**k <= val < 10**(k+1) -- the second most significant digit. 736 737 @param val: floating-point value 738 @param offset: integer distance as supplied by gtk scroll-event 739 @param fine: True to move in increments of 10**(k-2) -- the third most significant digit 740 @param coarse: True to move in increments of 10**k -- the most significant digit 741 @param lo: minimum output value 742 @param hi: maximum output value 743 """ 744 incr = 1e15 if val else 1.0 745 # incr will be most significant digit (abs) changed to 1 746 while val and (abs(val) < incr): 747 incr *= 1e-1 748 if (offset < 0) and (abs(val) < 1.1*incr): 749 incr *= 1e-1 750 if coarse: 751 pass # most significant digit 752 elif fine: 753 incr *= 1e-2 754 else: 755 incr *= 1e-1 756 result = val + offset * incr 757 if not (lo is None): 758 result = max(lo, result) 759 if not (hi is None): 760 result = min(hi, result) 761 return result
762
763 764 @contextmanager 765 -def nested_break():
766 """from Mark Dickinson (via stackoverflow.com): a more legible, flexible way to break out of nested loops, e.g. 767 with nested_break() as mylabel: 768 while True: 769 print "current state" 770 while True: 771 ok = raw_input("Is this ok? (y/n)") 772 if ok == "y" or ok == "Y": raise mylabel 773 if ok == "n" or ok == "N": break 774 print "more processing" 775 """ 776 class NestedBreakException(Exception): 777 pass
778 try: 779 yield NestedBreakException 780 except NestedBreakException: 781 pass 782
783 784 785 -def transpose_arrays(arrays):
786 """Takes as input an array of arrays, returns them transposed (so output[j][i] == arrays[i][j]).""" 787 return [ [arrays[j][i] for j in xrange(len(arrays))] 788 for i in xrange(len(arrays[0])) ]
789 790 791 MAX_SAFENAME_LEN = 32
792 793 -def SafeName(nm):
794 """Returns nm, modified to be a legal python identifier (up to MAX_SAFENAME_LEN (%i) chars).""" % MAX_SAFENAME_LEN 795 alnum = re.sub(r"[^a-zA-Z0-9_]", '_', nm) 796 safe = re.sub(r"(^[0-9])", r"_\1", alnum) 797 if safe == 'x': safe = '_x_' 798 return safe[:MAX_SAFENAME_LEN]
799
800 801 -class ToolRegistry(object):
802 """A collection of tools or actions, grouped by category. 803 804 @ivar by_cat: { category_name : {caption : (action, tool_class)} } 805 @ivar OnRegister: L{WeakEvent}(category_name, caption, action, tool_class) when register() is called 806 """
807 - def __init__(self):
808 self.by_cat = collections.defaultdict(lambda: {}) # cat -> {caption : (action, tool_class)} 809 self.OnRegister = WeakEvent()
810 - def register(self, cat, caption, action=None, tool_class=None):
811 """@param cat: category name 812 @param caption: i.e. to show in menu 813 @param action: lambda menuitem: do_something() 814 @param tool_class: qubx.toolspace.Tool subclass to be instantiated and activated (ignored unless action is None) 815 """ 816 self.by_cat[cat][caption] = (action, tool_class) 817 self.OnRegister(cat, caption, action, tool_class)
818
819 820 -def union_lists(a, b):
821 """ return the union of two lists """ 822 return list(set(a) | set(b))
823
824 -def union(*lists):
825 ii = iter(lists) 826 result = ii.next() 827 while True: 828 try: 829 result = union_lists(result, ii.next()) 830 except StopIteration: 831 break 832 return result
833
834 -def iunion(genexp):
835 result = genexp.next() 836 while True: 837 try: 838 result = union_lists(result, genexp.next()) 839 except StopIteration: 840 break 841 return result
842 843 valid_identifier = re.compile(r"^[A-Za-z_][A-Za-z_0-9]*$") 844