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