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