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