Adding utils
[gonvert] / src / util / misc.py
1 #!/usr/bin/env python
2
3 from __future__ import with_statement
4
5 import sys
6 import re
7 import cPickle
8
9 import functools
10 import contextlib
11 import inspect
12
13 import optparse
14 import traceback
15 import warnings
16 import string
17
18
19 _indentationLevel = [0]
20
21
22 def log_call(logger):
23
24         def log_call_decorator(func):
25
26                 @functools.wraps(func)
27                 def wrapper(*args, **kwds):
28                         logger.debug("%s> %s" % (" " * _indentationLevel[0], func.__name__, ))
29                         _indentationLevel[0] += 1
30                         try:
31                                 return func(*args, **kwds)
32                         finally:
33                                 _indentationLevel[0] -= 1
34                                 logger.debug("%s< %s" % (" " * _indentationLevel[0], func.__name__, ))
35
36                 return wrapper
37
38         return log_call_decorator
39
40
41 def log_exception(logger):
42
43         def log_exception_decorator(func):
44
45                 @functools.wraps(func)
46                 def wrapper(*args, **kwds):
47                         try:
48                                 return func(*args, **kwds)
49                         except Exception:
50                                 logger.exception(func.__name__)
51                                 raise
52
53                 return wrapper
54
55         return log_exception_decorator
56
57
58 def printfmt(template):
59         """
60         This hides having to create the Template object and call substitute/safe_substitute on it. For example:
61
62         >>> num = 10
63         >>> word = "spam"
64         >>> printfmt("I would like to order $num units of $word, please") #doctest: +SKIP
65         I would like to order 10 units of spam, please
66         """
67         frame = inspect.stack()[-1][0]
68         try:
69                 print string.Template(template).safe_substitute(frame.f_locals)
70         finally:
71                 del frame
72
73
74 def is_special(name):
75         return name.startswith("__") and name.endswith("__")
76
77
78 def is_private(name):
79         return name.startswith("_") and not is_special(name)
80
81
82 def privatize(clsName, attributeName):
83         """
84         At runtime, make an attributeName private
85
86         Example:
87         >>> class Test(object):
88         ...     pass
89         ...
90         >>> try:
91         ...     dir(Test).index("_Test__me")
92         ...     print dir(Test)
93         ... except:
94         ...     print "Not Found"
95         Not Found
96         >>> setattr(Test, privatize(Test.__name__, "me"), "Hello World")
97         >>> try:
98         ...     dir(Test).index("_Test__me")
99         ...     print "Found"
100         ... except:
101         ...     print dir(Test)
102         0
103         Found
104         >>> print getattr(Test, obfuscate(Test.__name__, "__me"))
105         Hello World
106         >>>
107         >>> is_private(privatize(Test.__name__, "me"))
108         True
109         >>> is_special(privatize(Test.__name__, "me"))
110         False
111         """
112         return "".join(["_", clsName, "__", attributeName])
113
114
115 def obfuscate(clsName, attributeName):
116         """
117         At runtime, turn a private name into the obfuscated form
118
119         Example:
120         >>> class Test(object):
121         ...     __me = "Hello World"
122         ...
123         >>> try:
124         ...     dir(Test).index("_Test__me")
125         ...     print "Found"
126         ... except:
127         ...     print dir(Test)
128         0
129         Found
130         >>> print getattr(Test, obfuscate(Test.__name__, "__me"))
131         Hello World
132         >>> is_private(obfuscate(Test.__name__, "__me"))
133         True
134         >>> is_special(obfuscate(Test.__name__, "__me"))
135         False
136         """
137         return "".join(["_", clsName, attributeName])
138
139
140 class PAOptionParser(optparse.OptionParser, object):
141         """
142         >>> if __name__ == '__main__':
143         ...     #parser = PAOptionParser("My usage str")
144         ...     parser = PAOptionParser()
145         ...     parser.add_posarg("Foo", help="Foo usage")
146         ...     parser.add_posarg("Bar", dest="bar_dest")
147         ...     parser.add_posarg("Language", dest='tr_type', type="choice", choices=("Python", "Other"))
148         ...     parser.add_option('--stocksym', dest='symbol')
149         ...     values, args = parser.parse_args()
150         ...     print values, args
151         ...
152
153         python mycp.py  -h
154         python mycp.py
155         python mycp.py  foo
156         python mycp.py  foo bar
157
158         python mycp.py foo bar lava
159         Usage: pa.py <Foo> <Bar> <Language> [options]
160
161         Positional Arguments:
162         Foo: Foo usage
163         Bar:
164         Language:
165
166         pa.py: error: option --Language: invalid choice: 'lava' (choose from 'Python', 'Other'
167         """
168
169         def __init__(self, *args, **kw):
170                 self.posargs = []
171                 super(PAOptionParser, self).__init__(*args, **kw)
172
173         def add_posarg(self, *args, **kw):
174                 pa_help = kw.get("help", "")
175                 kw["help"] = optparse.SUPPRESS_HELP
176                 o = self.add_option("--%s" % args[0], *args[1:], **kw)
177                 self.posargs.append((args[0], pa_help))
178
179         def get_usage(self, *args, **kwargs):
180                 params = (' '.join(["<%s>" % arg[0] for arg in self.posargs]), '\n '.join(["%s: %s" % (arg) for arg in self.posargs]))
181                 self.usage = "%%prog %s [options]\n\nPositional Arguments:\n %s" % params
182                 return super(PAOptionParser, self).get_usage(*args, **kwargs)
183
184         def parse_args(self, *args, **kwargs):
185                 args = sys.argv[1:]
186                 args0 = []
187                 for p, v in zip(self.posargs, args):
188                         args0.append("--%s" % p[0])
189                         args0.append(v)
190                 args = args0 + args
191                 options, args = super(PAOptionParser, self).parse_args(args, **kwargs)
192                 if len(args) < len(self.posargs):
193                         msg = 'Missing value(s) for "%s"\n' % ", ".join([arg[0] for arg in self.posargs][len(args):])
194                         self.error(msg)
195                 return options, args
196
197
198 def explicitly(name, stackadd=0):
199         """
200         This is an alias for adding to '__all__'.  Less error-prone than using
201         __all__ itself, since setting __all__ directly is prone to stomping on
202         things implicitly exported via L{alias}.
203
204         @note Taken from PyExport (which could turn out pretty cool):
205         @li @a http://codebrowse.launchpad.net/~glyph/
206         @li @a http://glyf.livejournal.com/74356.html
207         """
208         packageVars = sys._getframe(1+stackadd).f_locals
209         globalAll = packageVars.setdefault('__all__', [])
210         globalAll.append(name)
211
212
213 def public(thunk):
214         """
215         This is a decorator, for convenience.  Rather than typing the name of your
216         function twice, you can decorate a function with this.
217
218         To be real, @public would need to work on methods as well, which gets into
219         supporting types...
220
221         @note Taken from PyExport (which could turn out pretty cool):
222         @li @a http://codebrowse.launchpad.net/~glyph/
223         @li @a http://glyf.livejournal.com/74356.html
224         """
225         explicitly(thunk.__name__, 1)
226         return thunk
227
228
229 def _append_docstring(obj, message):
230         if obj.__doc__ is None:
231                 obj.__doc__ = message
232         else:
233                 obj.__doc__ += message
234
235
236 def validate_decorator(decorator):
237
238         def simple(x):
239                 return x
240
241         f = simple
242         f.__name__ = "name"
243         f.__doc__ = "doc"
244         f.__dict__["member"] = True
245
246         g = decorator(f)
247
248         if f.__name__ != g.__name__:
249                 print f.__name__, "!=", g.__name__
250
251         if g.__doc__ is None:
252                 print decorator.__name__, "has no doc string"
253         elif not g.__doc__.startswith(f.__doc__):
254                 print g.__doc__, "didn't start with", f.__doc__
255
256         if not ("member" in g.__dict__ and g.__dict__["member"]):
257                 print "'member' not in ", g.__dict__
258
259
260 def deprecated_api(func):
261         """
262         This is a decorator which can be used to mark functions
263         as deprecated. It will result in a warning being emitted
264         when the function is used.
265
266         >>> validate_decorator(deprecated_api)
267         """
268
269         @functools.wraps(func)
270         def newFunc(*args, **kwargs):
271                 warnings.warn("Call to deprecated function %s." % func.__name__, category=DeprecationWarning)
272                 return func(*args, **kwargs)
273
274         _append_docstring(newFunc, "\n@deprecated")
275         return newFunc
276
277
278 def unstable_api(func):
279         """
280         This is a decorator which can be used to mark functions
281         as deprecated. It will result in a warning being emitted
282         when the function is used.
283
284         >>> validate_decorator(unstable_api)
285         """
286
287         @functools.wraps(func)
288         def newFunc(*args, **kwargs):
289                 warnings.warn("Call to unstable API function %s." % func.__name__, category=FutureWarning)
290                 return func(*args, **kwargs)
291         _append_docstring(newFunc, "\n@unstable")
292         return newFunc
293
294
295 def enabled(func):
296         """
297         This decorator doesn't add any behavior
298
299         >>> validate_decorator(enabled)
300         """
301         return func
302
303
304 def disabled(func):
305         """
306         This decorator disables the provided function, and does nothing
307
308         >>> validate_decorator(disabled)
309         """
310
311         @functools.wraps(func)
312         def emptyFunc(*args, **kargs):
313                 pass
314         _append_docstring(emptyFunc, "\n@note Temporarily Disabled")
315         return emptyFunc
316
317
318 def metadata(document=True, **kwds):
319         """
320         >>> validate_decorator(metadata(author="Ed"))
321         """
322
323         def decorate(func):
324                 for k, v in kwds.iteritems():
325                         setattr(func, k, v)
326                         if document:
327                                 _append_docstring(func, "\n@"+k+" "+v)
328                 return func
329         return decorate
330
331
332 def prop(func):
333         """Function decorator for defining property attributes
334
335         The decorated function is expected to return a dictionary
336         containing one or more of the following pairs:
337                 fget - function for getting attribute value
338                 fset - function for setting attribute value
339                 fdel - function for deleting attribute
340         This can be conveniently constructed by the locals() builtin
341         function; see:
342         http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183
343         @author http://kbyanc.blogspot.com/2007/06/python-property-attribute-tricks.html
344
345         Example:
346         >>> #Due to transformation from function to property, does not need to be validated
347         >>> #validate_decorator(prop)
348         >>> class MyExampleClass(object):
349         ...     @prop
350         ...     def foo():
351         ...             "The foo property attribute's doc-string"
352         ...             def fget(self):
353         ...                     print "GET"
354         ...                     return self._foo
355         ...             def fset(self, value):
356         ...                     print "SET"
357         ...                     self._foo = value
358         ...             return locals()
359         ...
360         >>> me = MyExampleClass()
361         >>> me.foo = 10
362         SET
363         >>> print me.foo
364         GET
365         10
366         """
367         return property(doc=func.__doc__, **func())
368
369
370 def print_handler(e):
371         """
372         @see ExpHandler
373         """
374         print "%s: %s" % (type(e).__name__, e)
375
376
377 def print_ignore(e):
378         """
379         @see ExpHandler
380         """
381         print 'Ignoring %s exception: %s' % (type(e).__name__, e)
382
383
384 def print_traceback(e):
385         """
386         @see ExpHandler
387         """
388         #print sys.exc_info()
389         traceback.print_exc(file=sys.stdout)
390
391
392 def ExpHandler(handler = print_handler, *exceptions):
393         """
394         An exception handling idiom using decorators
395         Examples
396         Specify exceptions in order, first one is handled first
397         last one last.
398
399         >>> validate_decorator(ExpHandler())
400         >>> @ExpHandler(print_ignore, ZeroDivisionError)
401         ... @ExpHandler(None, AttributeError, ValueError)
402         ... def f1():
403         ...     1/0
404         >>> @ExpHandler(print_traceback, ZeroDivisionError)
405         ... def f2():
406         ...     1/0
407         >>> @ExpHandler()
408         ... def f3(*pargs):
409         ...     l = pargs
410         ...     return l[10]
411         >>> @ExpHandler(print_traceback, ZeroDivisionError)
412         ... def f4():
413         ...     return 1
414         >>>
415         >>>
416         >>> f1()
417         Ignoring ZeroDivisionError exception: integer division or modulo by zero
418         >>> f2() # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
419         Traceback (most recent call last):
420         ...
421         ZeroDivisionError: integer division or modulo by zero
422         >>> f3()
423         IndexError: tuple index out of range
424         >>> f4()
425         1
426         """
427
428         def wrapper(f):
429                 localExceptions = exceptions
430                 if not localExceptions:
431                         localExceptions = [Exception]
432                 t = [(ex, handler) for ex in localExceptions]
433                 t.reverse()
434
435                 def newfunc(t, *args, **kwargs):
436                         ex, handler = t[0]
437                         try:
438                                 if len(t) == 1:
439                                         return f(*args, **kwargs)
440                                 else:
441                                         #Recurse for embedded try/excepts
442                                         dec_func = functools.partial(newfunc, t[1:])
443                                         dec_func = functools.update_wrapper(dec_func, f)
444                                         return dec_func(*args, **kwargs)
445                         except ex, e:
446                                 return handler(e)
447
448                 dec_func = functools.partial(newfunc, t)
449                 dec_func = functools.update_wrapper(dec_func, f)
450                 return dec_func
451         return wrapper
452
453
454 def into_debugger(func):
455         """
456         >>> validate_decorator(into_debugger)
457         """
458
459         @functools.wraps(func)
460         def newFunc(*args, **kwargs):
461                 try:
462                         return func(*args, **kwargs)
463                 except:
464                         import pdb
465                         pdb.post_mortem()
466
467         return newFunc
468
469
470 class bindclass(object):
471         """
472         >>> validate_decorator(bindclass)
473         >>> class Foo(BoundObject):
474         ...      @bindclass
475         ...      def foo(this_class, self):
476         ...              return this_class, self
477         ...
478         >>> class Bar(Foo):
479         ...      @bindclass
480         ...      def bar(this_class, self):
481         ...              return this_class, self
482         ...
483         >>> f = Foo()
484         >>> b = Bar()
485         >>>
486         >>> f.foo() # doctest: +ELLIPSIS
487         (<class '...Foo'>, <...Foo object at ...>)
488         >>> b.foo() # doctest: +ELLIPSIS
489         (<class '...Foo'>, <...Bar object at ...>)
490         >>> b.bar() # doctest: +ELLIPSIS
491         (<class '...Bar'>, <...Bar object at ...>)
492         """
493
494         def __init__(self, f):
495                 self.f = f
496                 self.__name__ = f.__name__
497                 self.__doc__ = f.__doc__
498                 self.__dict__.update(f.__dict__)
499                 self.m = None
500
501         def bind(self, cls, attr):
502
503                 def bound_m(*args, **kwargs):
504                         return self.f(cls, *args, **kwargs)
505                 bound_m.__name__ = attr
506                 self.m = bound_m
507
508         def __get__(self, obj, objtype=None):
509                 return self.m.__get__(obj, objtype)
510
511
512 class ClassBindingSupport(type):
513         "@see bindclass"
514
515         def __init__(mcs, name, bases, attrs):
516                 type.__init__(mcs, name, bases, attrs)
517                 for attr, val in attrs.iteritems():
518                         if isinstance(val, bindclass):
519                                 val.bind(mcs, attr)
520
521
522 class BoundObject(object):
523         "@see bindclass"
524         __metaclass__ = ClassBindingSupport
525
526
527 def bindfunction(f):
528         """
529         >>> validate_decorator(bindfunction)
530         >>> @bindfunction
531         ... def factorial(thisfunction, n):
532         ...      # Within this function the name 'thisfunction' refers to the factorial
533         ...      # function(with only one argument), even after 'factorial' is bound
534         ...      # to another object
535         ...      if n > 0:
536         ...              return n * thisfunction(n - 1)
537         ...      else:
538         ...              return 1
539         ...
540         >>> factorial(3)
541         6
542         """
543
544         @functools.wraps(f)
545         def bound_f(*args, **kwargs):
546                 return f(bound_f, *args, **kwargs)
547         return bound_f
548
549
550 class Memoize(object):
551         """
552         Memoize(fn) - an instance which acts like fn but memoizes its arguments
553         Will only work on functions with non-mutable arguments
554         @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52201
555
556         >>> validate_decorator(Memoize)
557         """
558
559         def __init__(self, fn):
560                 self.fn = fn
561                 self.__name__ = fn.__name__
562                 self.__doc__ = fn.__doc__
563                 self.__dict__.update(fn.__dict__)
564                 self.memo = {}
565
566         def __call__(self, *args):
567                 if args not in self.memo:
568                         self.memo[args] = self.fn(*args)
569                 return self.memo[args]
570
571
572 class MemoizeMutable(object):
573         """Memoize(fn) - an instance which acts like fn but memoizes its arguments
574         Will work on functions with mutable arguments(slower than Memoize)
575         @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52201
576
577         >>> validate_decorator(MemoizeMutable)
578         """
579
580         def __init__(self, fn):
581                 self.fn = fn
582                 self.__name__ = fn.__name__
583                 self.__doc__ = fn.__doc__
584                 self.__dict__.update(fn.__dict__)
585                 self.memo = {}
586
587         def __call__(self, *args, **kw):
588                 text = cPickle.dumps((args, kw))
589                 if text not in self.memo:
590                         self.memo[text] = self.fn(*args, **kw)
591                 return self.memo[text]
592
593
594 callTraceIndentationLevel = 0
595
596
597 def call_trace(f):
598         """
599         Synchronization decorator.
600
601         >>> validate_decorator(call_trace)
602         >>> @call_trace
603         ... def a(a, b, c):
604         ...     pass
605         >>> a(1, 2, c=3)
606         Entering a((1, 2), {'c': 3})
607         Exiting a((1, 2), {'c': 3})
608         """
609
610         @functools.wraps(f)
611         def verboseTrace(*args, **kw):
612                 global callTraceIndentationLevel
613
614                 print "%sEntering %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw)
615                 callTraceIndentationLevel += 1
616                 try:
617                         result = f(*args, **kw)
618                 except:
619                         callTraceIndentationLevel -= 1
620                         print "%sException %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw)
621                         raise
622                 callTraceIndentationLevel -= 1
623                 print "%sExiting %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw)
624                 return result
625
626         @functools.wraps(f)
627         def smallTrace(*args, **kw):
628                 global callTraceIndentationLevel
629
630                 print "%sEntering %s" % ("\t"*callTraceIndentationLevel, f.__name__)
631                 callTraceIndentationLevel += 1
632                 try:
633                         result = f(*args, **kw)
634                 except:
635                         callTraceIndentationLevel -= 1
636                         print "%sException %s" % ("\t"*callTraceIndentationLevel, f.__name__)
637                         raise
638                 callTraceIndentationLevel -= 1
639                 print "%sExiting %s" % ("\t"*callTraceIndentationLevel, f.__name__)
640                 return result
641
642         #return smallTrace
643         return verboseTrace
644
645
646 @contextlib.contextmanager
647 def lexical_scope(*args):
648         """
649         @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/520586
650         Example:
651         >>> b = 0
652         >>> with lexical_scope(1) as (a):
653         ...     print a
654         ...
655         1
656         >>> with lexical_scope(1,2,3) as (a,b,c):
657         ...     print a,b,c
658         ...
659         1 2 3
660         >>> with lexical_scope():
661         ...     d = 10
662         ...     def foo():
663         ...             pass
664         ...
665         >>> print b
666         2
667         """
668
669         frame = inspect.currentframe().f_back.f_back
670         saved = frame.f_locals.keys()
671         try:
672                 if not args:
673                         yield
674                 elif len(args) == 1:
675                         yield args[0]
676                 else:
677                         yield args
678         finally:
679                 f_locals = frame.f_locals
680                 for key in (x for x in f_locals.keys() if x not in saved):
681                         del f_locals[key]
682                 del frame
683
684
685 def normalize_number(prettynumber):
686         """
687         function to take a phone number and strip out all non-numeric
688         characters
689
690         >>> normalize_number("+012-(345)-678-90")
691         '+01234567890'
692         >>> normalize_number("1-(345)-678-9000")
693         '+13456789000'
694         >>> normalize_number("+1-(345)-678-9000")
695         '+13456789000'
696         """
697         uglynumber = re.sub('[^0-9+]', '', prettynumber)
698         if uglynumber.startswith("+"):
699                 pass
700         elif uglynumber.startswith("1") and len(uglynumber) == 11:
701                 uglynumber = "+"+uglynumber
702         elif len(uglynumber) == 10:
703                 uglynumber = "+1"+uglynumber
704         else:
705                 pass
706
707         #validateRe = re.compile("^\+?[0-9]{10,}$")
708         #assert validateRe.match(uglynumber) is not None
709
710         return uglynumber
711
712
713 _VALIDATE_RE = re.compile("^\+?[0-9]{10,}$")
714
715
716 def is_valid_number(number):
717         """
718         @returns If This number be called ( syntax validation only )
719         """
720         return _VALIDATE_RE.match(number) is not None
721
722
723 def parse_version(versionText):
724         """
725         >>> parse_version("0.5.2")
726         [0, 5, 2]
727         """
728         return [
729                 int(number)
730                 for number in versionText.split(".")
731         ]
732
733
734 def compare_versions(leftParsedVersion, rightParsedVersion):
735         """
736         >>> compare_versions([0, 1, 2], [0, 1, 2])
737         0
738         >>> compare_versions([0, 1, 2], [0, 1, 3])
739         -1
740         >>> compare_versions([0, 1, 2], [0, 2, 2])
741         -1
742         >>> compare_versions([0, 1, 2], [1, 1, 2])
743         -1
744         >>> compare_versions([0, 1, 3], [0, 1, 2])
745         1
746         >>> compare_versions([0, 2, 2], [0, 1, 2])
747         1
748         >>> compare_versions([1, 1, 2], [0, 1, 2])
749         1
750         """
751         for left, right in zip(leftParsedVersion, rightParsedVersion):
752                 if left < right:
753                         return -1
754                 elif right < left:
755                         return 1
756         else:
757                 return 0