OpenSecurity/install/web.py-0.37/web/utils.py
author om
Mon, 02 Dec 2013 14:02:05 +0100
changeset 3 65432e6c6042
permissions -rwxr-xr-x
initial deployment and project layout commit
     1 #!/usr/bin/env python
     2 """
     3 General Utilities
     4 (part of web.py)
     5 """
     6 
     7 __all__ = [
     8   "Storage", "storage", "storify", 
     9   "Counter", "counter",
    10   "iters", 
    11   "rstrips", "lstrips", "strips", 
    12   "safeunicode", "safestr", "utf8",
    13   "TimeoutError", "timelimit",
    14   "Memoize", "memoize",
    15   "re_compile", "re_subm",
    16   "group", "uniq", "iterview",
    17   "IterBetter", "iterbetter",
    18   "safeiter", "safewrite",
    19   "dictreverse", "dictfind", "dictfindall", "dictincr", "dictadd",
    20   "requeue", "restack",
    21   "listget", "intget", "datestr",
    22   "numify", "denumify", "commify", "dateify",
    23   "nthstr", "cond",
    24   "CaptureStdout", "capturestdout", "Profile", "profile",
    25   "tryall",
    26   "ThreadedDict", "threadeddict",
    27   "autoassign",
    28   "to36",
    29   "safemarkdown",
    30   "sendmail"
    31 ]
    32 
    33 import re, sys, time, threading, itertools, traceback, os
    34 
    35 try:
    36     import subprocess
    37 except ImportError: 
    38     subprocess = None
    39 
    40 try: import datetime
    41 except ImportError: pass
    42 
    43 try: set
    44 except NameError:
    45     from sets import Set as set
    46     
    47 try:
    48     from threading import local as threadlocal
    49 except ImportError:
    50     from python23 import threadlocal
    51 
    52 class Storage(dict):
    53     """
    54     A Storage object is like a dictionary except `obj.foo` can be used
    55     in addition to `obj['foo']`.
    56     
    57         >>> o = storage(a=1)
    58         >>> o.a
    59         1
    60         >>> o['a']
    61         1
    62         >>> o.a = 2
    63         >>> o['a']
    64         2
    65         >>> del o.a
    66         >>> o.a
    67         Traceback (most recent call last):
    68             ...
    69         AttributeError: 'a'
    70     
    71     """
    72     def __getattr__(self, key): 
    73         try:
    74             return self[key]
    75         except KeyError, k:
    76             raise AttributeError, k
    77     
    78     def __setattr__(self, key, value): 
    79         self[key] = value
    80     
    81     def __delattr__(self, key):
    82         try:
    83             del self[key]
    84         except KeyError, k:
    85             raise AttributeError, k
    86     
    87     def __repr__(self):     
    88         return '<Storage ' + dict.__repr__(self) + '>'
    89 
    90 storage = Storage
    91 
    92 def storify(mapping, *requireds, **defaults):
    93     """
    94     Creates a `storage` object from dictionary `mapping`, raising `KeyError` if
    95     d doesn't have all of the keys in `requireds` and using the default 
    96     values for keys found in `defaults`.
    97 
    98     For example, `storify({'a':1, 'c':3}, b=2, c=0)` will return the equivalent of
    99     `storage({'a':1, 'b':2, 'c':3})`.
   100     
   101     If a `storify` value is a list (e.g. multiple values in a form submission), 
   102     `storify` returns the last element of the list, unless the key appears in 
   103     `defaults` as a list. Thus:
   104     
   105         >>> storify({'a':[1, 2]}).a
   106         2
   107         >>> storify({'a':[1, 2]}, a=[]).a
   108         [1, 2]
   109         >>> storify({'a':1}, a=[]).a
   110         [1]
   111         >>> storify({}, a=[]).a
   112         []
   113     
   114     Similarly, if the value has a `value` attribute, `storify will return _its_
   115     value, unless the key appears in `defaults` as a dictionary.
   116     
   117         >>> storify({'a':storage(value=1)}).a
   118         1
   119         >>> storify({'a':storage(value=1)}, a={}).a
   120         <Storage {'value': 1}>
   121         >>> storify({}, a={}).a
   122         {}
   123         
   124     Optionally, keyword parameter `_unicode` can be passed to convert all values to unicode.
   125     
   126         >>> storify({'x': 'a'}, _unicode=True)
   127         <Storage {'x': u'a'}>
   128         >>> storify({'x': storage(value='a')}, x={}, _unicode=True)
   129         <Storage {'x': <Storage {'value': 'a'}>}>
   130         >>> storify({'x': storage(value='a')}, _unicode=True)
   131         <Storage {'x': u'a'}>
   132     """
   133     _unicode = defaults.pop('_unicode', False)
   134 
   135     # if _unicode is callable object, use it convert a string to unicode.
   136     to_unicode = safeunicode
   137     if _unicode is not False and hasattr(_unicode, "__call__"):
   138         to_unicode = _unicode
   139     
   140     def unicodify(s):
   141         if _unicode and isinstance(s, str): return to_unicode(s)
   142         else: return s
   143         
   144     def getvalue(x):
   145         if hasattr(x, 'file') and hasattr(x, 'value'):
   146             return x.value
   147         elif hasattr(x, 'value'):
   148             return unicodify(x.value)
   149         else:
   150             return unicodify(x)
   151     
   152     stor = Storage()
   153     for key in requireds + tuple(mapping.keys()):
   154         value = mapping[key]
   155         if isinstance(value, list):
   156             if isinstance(defaults.get(key), list):
   157                 value = [getvalue(x) for x in value]
   158             else:
   159                 value = value[-1]
   160         if not isinstance(defaults.get(key), dict):
   161             value = getvalue(value)
   162         if isinstance(defaults.get(key), list) and not isinstance(value, list):
   163             value = [value]
   164         setattr(stor, key, value)
   165 
   166     for (key, value) in defaults.iteritems():
   167         result = value
   168         if hasattr(stor, key): 
   169             result = stor[key]
   170         if value == () and not isinstance(result, tuple): 
   171             result = (result,)
   172         setattr(stor, key, result)
   173     
   174     return stor
   175 
   176 class Counter(storage):
   177     """Keeps count of how many times something is added.
   178         
   179         >>> c = counter()
   180         >>> c.add('x')
   181         >>> c.add('x')
   182         >>> c.add('x')
   183         >>> c.add('x')
   184         >>> c.add('x')
   185         >>> c.add('y')
   186         >>> c
   187         <Counter {'y': 1, 'x': 5}>
   188         >>> c.most()
   189         ['x']
   190     """
   191     def add(self, n):
   192         self.setdefault(n, 0)
   193         self[n] += 1
   194     
   195     def most(self):
   196         """Returns the keys with maximum count."""
   197         m = max(self.itervalues())
   198         return [k for k, v in self.iteritems() if v == m]
   199         
   200     def least(self):
   201         """Returns the keys with mininum count."""
   202         m = min(self.itervalues())
   203         return [k for k, v in self.iteritems() if v == m]
   204 
   205     def percent(self, key):
   206        """Returns what percentage a certain key is of all entries.
   207 
   208            >>> c = counter()
   209            >>> c.add('x')
   210            >>> c.add('x')
   211            >>> c.add('x')
   212            >>> c.add('y')
   213            >>> c.percent('x')
   214            0.75
   215            >>> c.percent('y')
   216            0.25
   217        """
   218        return float(self[key])/sum(self.values())
   219              
   220     def sorted_keys(self):
   221         """Returns keys sorted by value.
   222              
   223              >>> c = counter()
   224              >>> c.add('x')
   225              >>> c.add('x')
   226              >>> c.add('y')
   227              >>> c.sorted_keys()
   228              ['x', 'y']
   229         """
   230         return sorted(self.keys(), key=lambda k: self[k], reverse=True)
   231     
   232     def sorted_values(self):
   233         """Returns values sorted by value.
   234             
   235             >>> c = counter()
   236             >>> c.add('x')
   237             >>> c.add('x')
   238             >>> c.add('y')
   239             >>> c.sorted_values()
   240             [2, 1]
   241         """
   242         return [self[k] for k in self.sorted_keys()]
   243     
   244     def sorted_items(self):
   245         """Returns items sorted by value.
   246             
   247             >>> c = counter()
   248             >>> c.add('x')
   249             >>> c.add('x')
   250             >>> c.add('y')
   251             >>> c.sorted_items()
   252             [('x', 2), ('y', 1)]
   253         """
   254         return [(k, self[k]) for k in self.sorted_keys()]
   255     
   256     def __repr__(self):
   257         return '<Counter ' + dict.__repr__(self) + '>'
   258        
   259 counter = Counter
   260 
   261 iters = [list, tuple]
   262 import __builtin__
   263 if hasattr(__builtin__, 'set'):
   264     iters.append(set)
   265 if hasattr(__builtin__, 'frozenset'):
   266     iters.append(set)
   267 if sys.version_info < (2,6): # sets module deprecated in 2.6
   268     try:
   269         from sets import Set
   270         iters.append(Set)
   271     except ImportError: 
   272         pass
   273     
   274 class _hack(tuple): pass
   275 iters = _hack(iters)
   276 iters.__doc__ = """
   277 A list of iterable items (like lists, but not strings). Includes whichever
   278 of lists, tuples, sets, and Sets are available in this version of Python.
   279 """
   280 
   281 def _strips(direction, text, remove):
   282     if isinstance(remove, iters):
   283         for subr in remove:
   284             text = _strips(direction, text, subr)
   285         return text
   286     
   287     if direction == 'l': 
   288         if text.startswith(remove): 
   289             return text[len(remove):]
   290     elif direction == 'r':
   291         if text.endswith(remove):   
   292             return text[:-len(remove)]
   293     else: 
   294         raise ValueError, "Direction needs to be r or l."
   295     return text
   296 
   297 def rstrips(text, remove):
   298     """
   299     removes the string `remove` from the right of `text`
   300 
   301         >>> rstrips("foobar", "bar")
   302         'foo'
   303     
   304     """
   305     return _strips('r', text, remove)
   306 
   307 def lstrips(text, remove):
   308     """
   309     removes the string `remove` from the left of `text`
   310     
   311         >>> lstrips("foobar", "foo")
   312         'bar'
   313         >>> lstrips('http://foo.org/', ['http://', 'https://'])
   314         'foo.org/'
   315         >>> lstrips('FOOBARBAZ', ['FOO', 'BAR'])
   316         'BAZ'
   317         >>> lstrips('FOOBARBAZ', ['BAR', 'FOO'])
   318         'BARBAZ'
   319     
   320     """
   321     return _strips('l', text, remove)
   322 
   323 def strips(text, remove):
   324     """
   325     removes the string `remove` from the both sides of `text`
   326 
   327         >>> strips("foobarfoo", "foo")
   328         'bar'
   329     
   330     """
   331     return rstrips(lstrips(text, remove), remove)
   332 
   333 def safeunicode(obj, encoding='utf-8'):
   334     r"""
   335     Converts any given object to unicode string.
   336     
   337         >>> safeunicode('hello')
   338         u'hello'
   339         >>> safeunicode(2)
   340         u'2'
   341         >>> safeunicode('\xe1\x88\xb4')
   342         u'\u1234'
   343     """
   344     t = type(obj)
   345     if t is unicode:
   346         return obj
   347     elif t is str:
   348         return obj.decode(encoding)
   349     elif t in [int, float, bool]:
   350         return unicode(obj)
   351     elif hasattr(obj, '__unicode__') or isinstance(obj, unicode):
   352         return unicode(obj)
   353     else:
   354         return str(obj).decode(encoding)
   355     
   356 def safestr(obj, encoding='utf-8'):
   357     r"""
   358     Converts any given object to utf-8 encoded string. 
   359     
   360         >>> safestr('hello')
   361         'hello'
   362         >>> safestr(u'\u1234')
   363         '\xe1\x88\xb4'
   364         >>> safestr(2)
   365         '2'
   366     """
   367     if isinstance(obj, unicode):
   368         return obj.encode(encoding)
   369     elif isinstance(obj, str):
   370         return obj
   371     elif hasattr(obj, 'next'): # iterator
   372         return itertools.imap(safestr, obj)
   373     else:
   374         return str(obj)
   375 
   376 # for backward-compatibility
   377 utf8 = safestr
   378     
   379 class TimeoutError(Exception): pass
   380 def timelimit(timeout):
   381     """
   382     A decorator to limit a function to `timeout` seconds, raising `TimeoutError`
   383     if it takes longer.
   384     
   385         >>> import time
   386         >>> def meaningoflife():
   387         ...     time.sleep(.2)
   388         ...     return 42
   389         >>> 
   390         >>> timelimit(.1)(meaningoflife)()
   391         Traceback (most recent call last):
   392             ...
   393         TimeoutError: took too long
   394         >>> timelimit(1)(meaningoflife)()
   395         42
   396 
   397     _Caveat:_ The function isn't stopped after `timeout` seconds but continues 
   398     executing in a separate thread. (There seems to be no way to kill a thread.)
   399 
   400     inspired by <http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/473878>
   401     """
   402     def _1(function):
   403         def _2(*args, **kw):
   404             class Dispatch(threading.Thread):
   405                 def __init__(self):
   406                     threading.Thread.__init__(self)
   407                     self.result = None
   408                     self.error = None
   409 
   410                     self.setDaemon(True)
   411                     self.start()
   412 
   413                 def run(self):
   414                     try:
   415                         self.result = function(*args, **kw)
   416                     except:
   417                         self.error = sys.exc_info()
   418 
   419             c = Dispatch()
   420             c.join(timeout)
   421             if c.isAlive():
   422                 raise TimeoutError, 'took too long'
   423             if c.error:
   424                 raise c.error[0], c.error[1]
   425             return c.result
   426         return _2
   427     return _1
   428 
   429 class Memoize:
   430     """
   431     'Memoizes' a function, caching its return values for each input.
   432     If `expires` is specified, values are recalculated after `expires` seconds.
   433     If `background` is specified, values are recalculated in a separate thread.
   434     
   435         >>> calls = 0
   436         >>> def howmanytimeshaveibeencalled():
   437         ...     global calls
   438         ...     calls += 1
   439         ...     return calls
   440         >>> fastcalls = memoize(howmanytimeshaveibeencalled)
   441         >>> howmanytimeshaveibeencalled()
   442         1
   443         >>> howmanytimeshaveibeencalled()
   444         2
   445         >>> fastcalls()
   446         3
   447         >>> fastcalls()
   448         3
   449         >>> import time
   450         >>> fastcalls = memoize(howmanytimeshaveibeencalled, .1, background=False)
   451         >>> fastcalls()
   452         4
   453         >>> fastcalls()
   454         4
   455         >>> time.sleep(.2)
   456         >>> fastcalls()
   457         5
   458         >>> def slowfunc():
   459         ...     time.sleep(.1)
   460         ...     return howmanytimeshaveibeencalled()
   461         >>> fastcalls = memoize(slowfunc, .2, background=True)
   462         >>> fastcalls()
   463         6
   464         >>> timelimit(.05)(fastcalls)()
   465         6
   466         >>> time.sleep(.2)
   467         >>> timelimit(.05)(fastcalls)()
   468         6
   469         >>> timelimit(.05)(fastcalls)()
   470         6
   471         >>> time.sleep(.2)
   472         >>> timelimit(.05)(fastcalls)()
   473         7
   474         >>> fastcalls = memoize(slowfunc, None, background=True)
   475         >>> threading.Thread(target=fastcalls).start()
   476         >>> time.sleep(.01)
   477         >>> fastcalls()
   478         9
   479     """
   480     def __init__(self, func, expires=None, background=True): 
   481         self.func = func
   482         self.cache = {}
   483         self.expires = expires
   484         self.background = background
   485         self.running = {}
   486     
   487     def __call__(self, *args, **keywords):
   488         key = (args, tuple(keywords.items()))
   489         if not self.running.get(key):
   490             self.running[key] = threading.Lock()
   491         def update(block=False):
   492             if self.running[key].acquire(block):
   493                 try:
   494                     self.cache[key] = (self.func(*args, **keywords), time.time())
   495                 finally:
   496                     self.running[key].release()
   497         
   498         if key not in self.cache: 
   499             update(block=True)
   500         elif self.expires and (time.time() - self.cache[key][1]) > self.expires:
   501             if self.background:
   502                 threading.Thread(target=update).start()
   503             else:
   504                 update()
   505         return self.cache[key][0]
   506 
   507 memoize = Memoize
   508 
   509 re_compile = memoize(re.compile) #@@ threadsafe?
   510 re_compile.__doc__ = """
   511 A memoized version of re.compile.
   512 """
   513 
   514 class _re_subm_proxy:
   515     def __init__(self): 
   516         self.match = None
   517     def __call__(self, match): 
   518         self.match = match
   519         return ''
   520 
   521 def re_subm(pat, repl, string):
   522     """
   523     Like re.sub, but returns the replacement _and_ the match object.
   524     
   525         >>> t, m = re_subm('g(oo+)fball', r'f\\1lish', 'goooooofball')
   526         >>> t
   527         'foooooolish'
   528         >>> m.groups()
   529         ('oooooo',)
   530     """
   531     compiled_pat = re_compile(pat)
   532     proxy = _re_subm_proxy()
   533     compiled_pat.sub(proxy.__call__, string)
   534     return compiled_pat.sub(repl, string), proxy.match
   535 
   536 def group(seq, size): 
   537     """
   538     Returns an iterator over a series of lists of length size from iterable.
   539 
   540         >>> list(group([1,2,3,4], 2))
   541         [[1, 2], [3, 4]]
   542         >>> list(group([1,2,3,4,5], 2))
   543         [[1, 2], [3, 4], [5]]
   544     """
   545     def take(seq, n):
   546         for i in xrange(n):
   547             yield seq.next()
   548 
   549     if not hasattr(seq, 'next'):  
   550         seq = iter(seq)
   551     while True: 
   552         x = list(take(seq, size))
   553         if x:
   554             yield x
   555         else:
   556             break
   557 
   558 def uniq(seq, key=None):
   559     """
   560     Removes duplicate elements from a list while preserving the order of the rest.
   561 
   562         >>> uniq([9,0,2,1,0])
   563         [9, 0, 2, 1]
   564 
   565     The value of the optional `key` parameter should be a function that
   566     takes a single argument and returns a key to test the uniqueness.
   567 
   568         >>> uniq(["Foo", "foo", "bar"], key=lambda s: s.lower())
   569         ['Foo', 'bar']
   570     """
   571     key = key or (lambda x: x)
   572     seen = set()
   573     result = []
   574     for v in seq:
   575         k = key(v)
   576         if k in seen:
   577             continue
   578         seen.add(k)
   579         result.append(v)
   580     return result
   581 
   582 def iterview(x):
   583    """
   584    Takes an iterable `x` and returns an iterator over it
   585    which prints its progress to stderr as it iterates through.
   586    """
   587    WIDTH = 70
   588 
   589    def plainformat(n, lenx):
   590        return '%5.1f%% (%*d/%d)' % ((float(n)/lenx)*100, len(str(lenx)), n, lenx)
   591 
   592    def bars(size, n, lenx):
   593        val = int((float(n)*size)/lenx + 0.5)
   594        if size - val:
   595            spacing = ">" + (" "*(size-val))[1:]
   596        else:
   597            spacing = ""
   598        return "[%s%s]" % ("="*val, spacing)
   599 
   600    def eta(elapsed, n, lenx):
   601        if n == 0:
   602            return '--:--:--'
   603        if n == lenx:
   604            secs = int(elapsed)
   605        else:
   606            secs = int((elapsed/n) * (lenx-n))
   607        mins, secs = divmod(secs, 60)
   608        hrs, mins = divmod(mins, 60)
   609 
   610        return '%02d:%02d:%02d' % (hrs, mins, secs)
   611 
   612    def format(starttime, n, lenx):
   613        out = plainformat(n, lenx) + ' '
   614        if n == lenx:
   615            end = '     '
   616        else:
   617            end = ' ETA '
   618        end += eta(time.time() - starttime, n, lenx)
   619        out += bars(WIDTH - len(out) - len(end), n, lenx)
   620        out += end
   621        return out
   622 
   623    starttime = time.time()
   624    lenx = len(x)
   625    for n, y in enumerate(x):
   626        sys.stderr.write('\r' + format(starttime, n, lenx))
   627        yield y
   628    sys.stderr.write('\r' + format(starttime, n+1, lenx) + '\n')
   629 
   630 class IterBetter:
   631     """
   632     Returns an object that can be used as an iterator 
   633     but can also be used via __getitem__ (although it 
   634     cannot go backwards -- that is, you cannot request 
   635     `iterbetter[0]` after requesting `iterbetter[1]`).
   636     
   637         >>> import itertools
   638         >>> c = iterbetter(itertools.count())
   639         >>> c[1]
   640         1
   641         >>> c[5]
   642         5
   643         >>> c[3]
   644         Traceback (most recent call last):
   645             ...
   646         IndexError: already passed 3
   647 
   648     For boolean test, IterBetter peeps at first value in the itertor without effecting the iteration.
   649 
   650         >>> c = iterbetter(iter(range(5)))
   651         >>> bool(c)
   652         True
   653         >>> list(c)
   654         [0, 1, 2, 3, 4]
   655         >>> c = iterbetter(iter([]))
   656         >>> bool(c)
   657         False
   658         >>> list(c)
   659         []
   660     """
   661     def __init__(self, iterator): 
   662         self.i, self.c = iterator, 0
   663 
   664     def __iter__(self): 
   665         if hasattr(self, "_head"):
   666             yield self._head
   667 
   668         while 1:    
   669             yield self.i.next()
   670             self.c += 1
   671 
   672     def __getitem__(self, i):
   673         #todo: slices
   674         if i < self.c: 
   675             raise IndexError, "already passed "+str(i)
   676         try:
   677             while i > self.c: 
   678                 self.i.next()
   679                 self.c += 1
   680             # now self.c == i
   681             self.c += 1
   682             return self.i.next()
   683         except StopIteration: 
   684             raise IndexError, str(i)
   685             
   686     def __nonzero__(self):
   687         if hasattr(self, "__len__"):
   688             return len(self) != 0
   689         elif hasattr(self, "_head"):
   690             return True
   691         else:
   692             try:
   693                 self._head = self.i.next()
   694             except StopIteration:
   695                 return False
   696             else:
   697                 return True
   698 
   699 iterbetter = IterBetter
   700 
   701 def safeiter(it, cleanup=None, ignore_errors=True):
   702     """Makes an iterator safe by ignoring the exceptions occured during the iteration.
   703     """
   704     def next():
   705         while True:
   706             try:
   707                 return it.next()
   708             except StopIteration:
   709                 raise
   710             except:
   711                 traceback.print_exc()
   712 
   713     it = iter(it)
   714     while True:
   715         yield next()
   716 
   717 def safewrite(filename, content):
   718     """Writes the content to a temp file and then moves the temp file to 
   719     given filename to avoid overwriting the existing file in case of errors.
   720     """
   721     f = file(filename + '.tmp', 'w')
   722     f.write(content)
   723     f.close()
   724     os.rename(f.name, filename)
   725 
   726 def dictreverse(mapping):
   727     """
   728     Returns a new dictionary with keys and values swapped.
   729     
   730         >>> dictreverse({1: 2, 3: 4})
   731         {2: 1, 4: 3}
   732     """
   733     return dict([(value, key) for (key, value) in mapping.iteritems()])
   734 
   735 def dictfind(dictionary, element):
   736     """
   737     Returns a key whose value in `dictionary` is `element` 
   738     or, if none exists, None.
   739     
   740         >>> d = {1:2, 3:4}
   741         >>> dictfind(d, 4)
   742         3
   743         >>> dictfind(d, 5)
   744     """
   745     for (key, value) in dictionary.iteritems():
   746         if element is value: 
   747             return key
   748 
   749 def dictfindall(dictionary, element):
   750     """
   751     Returns the keys whose values in `dictionary` are `element`
   752     or, if none exists, [].
   753     
   754         >>> d = {1:4, 3:4}
   755         >>> dictfindall(d, 4)
   756         [1, 3]
   757         >>> dictfindall(d, 5)
   758         []
   759     """
   760     res = []
   761     for (key, value) in dictionary.iteritems():
   762         if element is value:
   763             res.append(key)
   764     return res
   765 
   766 def dictincr(dictionary, element):
   767     """
   768     Increments `element` in `dictionary`, 
   769     setting it to one if it doesn't exist.
   770     
   771         >>> d = {1:2, 3:4}
   772         >>> dictincr(d, 1)
   773         3
   774         >>> d[1]
   775         3
   776         >>> dictincr(d, 5)
   777         1
   778         >>> d[5]
   779         1
   780     """
   781     dictionary.setdefault(element, 0)
   782     dictionary[element] += 1
   783     return dictionary[element]
   784 
   785 def dictadd(*dicts):
   786     """
   787     Returns a dictionary consisting of the keys in the argument dictionaries.
   788     If they share a key, the value from the last argument is used.
   789     
   790         >>> dictadd({1: 0, 2: 0}, {2: 1, 3: 1})
   791         {1: 0, 2: 1, 3: 1}
   792     """
   793     result = {}
   794     for dct in dicts:
   795         result.update(dct)
   796     return result
   797 
   798 def requeue(queue, index=-1):
   799     """Returns the element at index after moving it to the beginning of the queue.
   800 
   801         >>> x = [1, 2, 3, 4]
   802         >>> requeue(x)
   803         4
   804         >>> x
   805         [4, 1, 2, 3]
   806     """
   807     x = queue.pop(index)
   808     queue.insert(0, x)
   809     return x
   810 
   811 def restack(stack, index=0):
   812     """Returns the element at index after moving it to the top of stack.
   813 
   814            >>> x = [1, 2, 3, 4]
   815            >>> restack(x)
   816            1
   817            >>> x
   818            [2, 3, 4, 1]
   819     """
   820     x = stack.pop(index)
   821     stack.append(x)
   822     return x
   823 
   824 def listget(lst, ind, default=None):
   825     """
   826     Returns `lst[ind]` if it exists, `default` otherwise.
   827     
   828         >>> listget(['a'], 0)
   829         'a'
   830         >>> listget(['a'], 1)
   831         >>> listget(['a'], 1, 'b')
   832         'b'
   833     """
   834     if len(lst)-1 < ind: 
   835         return default
   836     return lst[ind]
   837 
   838 def intget(integer, default=None):
   839     """
   840     Returns `integer` as an int or `default` if it can't.
   841     
   842         >>> intget('3')
   843         3
   844         >>> intget('3a')
   845         >>> intget('3a', 0)
   846         0
   847     """
   848     try:
   849         return int(integer)
   850     except (TypeError, ValueError):
   851         return default
   852 
   853 def datestr(then, now=None):
   854     """
   855     Converts a (UTC) datetime object to a nice string representation.
   856     
   857         >>> from datetime import datetime, timedelta
   858         >>> d = datetime(1970, 5, 1)
   859         >>> datestr(d, now=d)
   860         '0 microseconds ago'
   861         >>> for t, v in {
   862         ...   timedelta(microseconds=1): '1 microsecond ago',
   863         ...   timedelta(microseconds=2): '2 microseconds ago',
   864         ...   -timedelta(microseconds=1): '1 microsecond from now',
   865         ...   -timedelta(microseconds=2): '2 microseconds from now',
   866         ...   timedelta(microseconds=2000): '2 milliseconds ago',
   867         ...   timedelta(seconds=2): '2 seconds ago',
   868         ...   timedelta(seconds=2*60): '2 minutes ago',
   869         ...   timedelta(seconds=2*60*60): '2 hours ago',
   870         ...   timedelta(days=2): '2 days ago',
   871         ... }.iteritems():
   872         ...     assert datestr(d, now=d+t) == v
   873         >>> datestr(datetime(1970, 1, 1), now=d)
   874         'January  1'
   875         >>> datestr(datetime(1969, 1, 1), now=d)
   876         'January  1, 1969'
   877         >>> datestr(datetime(1970, 6, 1), now=d)
   878         'June  1, 1970'
   879         >>> datestr(None)
   880         ''
   881     """
   882     def agohence(n, what, divisor=None):
   883         if divisor: n = n // divisor
   884 
   885         out = str(abs(n)) + ' ' + what       # '2 day'
   886         if abs(n) != 1: out += 's'           # '2 days'
   887         out += ' '                           # '2 days '
   888         if n < 0:
   889             out += 'from now'
   890         else:
   891             out += 'ago'
   892         return out                           # '2 days ago'
   893 
   894     oneday = 24 * 60 * 60
   895 
   896     if not then: return ""
   897     if not now: now = datetime.datetime.utcnow()
   898     if type(now).__name__ == "DateTime":
   899         now = datetime.datetime.fromtimestamp(now)
   900     if type(then).__name__ == "DateTime":
   901         then = datetime.datetime.fromtimestamp(then)
   902     elif type(then).__name__ == "date":
   903         then = datetime.datetime(then.year, then.month, then.day)
   904 
   905     delta = now - then
   906     deltaseconds = int(delta.days * oneday + delta.seconds + delta.microseconds * 1e-06)
   907     deltadays = abs(deltaseconds) // oneday
   908     if deltaseconds < 0: deltadays *= -1 # fix for oddity of floor
   909 
   910     if deltadays:
   911         if abs(deltadays) < 4:
   912             return agohence(deltadays, 'day')
   913 
   914         try:
   915             out = then.strftime('%B %e') # e.g. 'June  3'
   916         except ValueError:
   917             # %e doesn't work on Windows.
   918             out = then.strftime('%B %d') # e.g. 'June 03'
   919 
   920         if then.year != now.year or deltadays < 0:
   921             out += ', %s' % then.year
   922         return out
   923 
   924     if int(deltaseconds):
   925         if abs(deltaseconds) > (60 * 60):
   926             return agohence(deltaseconds, 'hour', 60 * 60)
   927         elif abs(deltaseconds) > 60:
   928             return agohence(deltaseconds, 'minute', 60)
   929         else:
   930             return agohence(deltaseconds, 'second')
   931 
   932     deltamicroseconds = delta.microseconds
   933     if delta.days: deltamicroseconds = int(delta.microseconds - 1e6) # datetime oddity
   934     if abs(deltamicroseconds) > 1000:
   935         return agohence(deltamicroseconds, 'millisecond', 1000)
   936 
   937     return agohence(deltamicroseconds, 'microsecond')
   938 
   939 def numify(string):
   940     """
   941     Removes all non-digit characters from `string`.
   942     
   943         >>> numify('800-555-1212')
   944         '8005551212'
   945         >>> numify('800.555.1212')
   946         '8005551212'
   947     
   948     """
   949     return ''.join([c for c in str(string) if c.isdigit()])
   950 
   951 def denumify(string, pattern):
   952     """
   953     Formats `string` according to `pattern`, where the letter X gets replaced
   954     by characters from `string`.
   955     
   956         >>> denumify("8005551212", "(XXX) XXX-XXXX")
   957         '(800) 555-1212'
   958     
   959     """
   960     out = []
   961     for c in pattern:
   962         if c == "X":
   963             out.append(string[0])
   964             string = string[1:]
   965         else:
   966             out.append(c)
   967     return ''.join(out)
   968 
   969 def commify(n):
   970     """
   971     Add commas to an integer `n`.
   972 
   973         >>> commify(1)
   974         '1'
   975         >>> commify(123)
   976         '123'
   977         >>> commify(1234)
   978         '1,234'
   979         >>> commify(1234567890)
   980         '1,234,567,890'
   981         >>> commify(123.0)
   982         '123.0'
   983         >>> commify(1234.5)
   984         '1,234.5'
   985         >>> commify(1234.56789)
   986         '1,234.56789'
   987         >>> commify('%.2f' % 1234.5)
   988         '1,234.50'
   989         >>> commify(None)
   990         >>>
   991 
   992     """
   993     if n is None: return None
   994     n = str(n)
   995     if '.' in n:
   996         dollars, cents = n.split('.')
   997     else:
   998         dollars, cents = n, None
   999 
  1000     r = []
  1001     for i, c in enumerate(str(dollars)[::-1]):
  1002         if i and (not (i % 3)):
  1003             r.insert(0, ',')
  1004         r.insert(0, c)
  1005     out = ''.join(r)
  1006     if cents:
  1007         out += '.' + cents
  1008     return out
  1009 
  1010 def dateify(datestring):
  1011     """
  1012     Formats a numified `datestring` properly.
  1013     """
  1014     return denumify(datestring, "XXXX-XX-XX XX:XX:XX")
  1015 
  1016 
  1017 def nthstr(n):
  1018     """
  1019     Formats an ordinal.
  1020     Doesn't handle negative numbers.
  1021 
  1022         >>> nthstr(1)
  1023         '1st'
  1024         >>> nthstr(0)
  1025         '0th'
  1026         >>> [nthstr(x) for x in [2, 3, 4, 5, 10, 11, 12, 13, 14, 15]]
  1027         ['2nd', '3rd', '4th', '5th', '10th', '11th', '12th', '13th', '14th', '15th']
  1028         >>> [nthstr(x) for x in [91, 92, 93, 94, 99, 100, 101, 102]]
  1029         ['91st', '92nd', '93rd', '94th', '99th', '100th', '101st', '102nd']
  1030         >>> [nthstr(x) for x in [111, 112, 113, 114, 115]]
  1031         ['111th', '112th', '113th', '114th', '115th']
  1032 
  1033     """
  1034     
  1035     assert n >= 0
  1036     if n % 100 in [11, 12, 13]: return '%sth' % n
  1037     return {1: '%sst', 2: '%snd', 3: '%srd'}.get(n % 10, '%sth') % n
  1038 
  1039 def cond(predicate, consequence, alternative=None):
  1040     """
  1041     Function replacement for if-else to use in expressions.
  1042         
  1043         >>> x = 2
  1044         >>> cond(x % 2 == 0, "even", "odd")
  1045         'even'
  1046         >>> cond(x % 2 == 0, "even", "odd") + '_row'
  1047         'even_row'
  1048     """
  1049     if predicate:
  1050         return consequence
  1051     else:
  1052         return alternative
  1053 
  1054 class CaptureStdout:
  1055     """
  1056     Captures everything `func` prints to stdout and returns it instead.
  1057     
  1058         >>> def idiot():
  1059         ...     print "foo"
  1060         >>> capturestdout(idiot)()
  1061         'foo\\n'
  1062     
  1063     **WARNING:** Not threadsafe!
  1064     """
  1065     def __init__(self, func): 
  1066         self.func = func
  1067     def __call__(self, *args, **keywords):
  1068         from cStringIO import StringIO
  1069         # Not threadsafe!
  1070         out = StringIO()
  1071         oldstdout = sys.stdout
  1072         sys.stdout = out
  1073         try: 
  1074             self.func(*args, **keywords)
  1075         finally: 
  1076             sys.stdout = oldstdout
  1077         return out.getvalue()
  1078 
  1079 capturestdout = CaptureStdout
  1080 
  1081 class Profile:
  1082     """
  1083     Profiles `func` and returns a tuple containing its output
  1084     and a string with human-readable profiling information.
  1085         
  1086         >>> import time
  1087         >>> out, inf = profile(time.sleep)(.001)
  1088         >>> out
  1089         >>> inf[:10].strip()
  1090         'took 0.0'
  1091     """
  1092     def __init__(self, func): 
  1093         self.func = func
  1094     def __call__(self, *args): ##, **kw):   kw unused
  1095         import hotshot, hotshot.stats, os, tempfile ##, time already imported
  1096         f, filename = tempfile.mkstemp()
  1097         os.close(f)
  1098         
  1099         prof = hotshot.Profile(filename)
  1100 
  1101         stime = time.time()
  1102         result = prof.runcall(self.func, *args)
  1103         stime = time.time() - stime
  1104         prof.close()
  1105 
  1106         import cStringIO
  1107         out = cStringIO.StringIO()
  1108         stats = hotshot.stats.load(filename)
  1109         stats.stream = out
  1110         stats.strip_dirs()
  1111         stats.sort_stats('time', 'calls')
  1112         stats.print_stats(40)
  1113         stats.print_callers()
  1114 
  1115         x =  '\n\ntook '+ str(stime) + ' seconds\n'
  1116         x += out.getvalue()
  1117 
  1118         # remove the tempfile
  1119         try:
  1120             os.remove(filename)
  1121         except IOError:
  1122             pass
  1123             
  1124         return result, x
  1125 
  1126 profile = Profile
  1127 
  1128 
  1129 import traceback
  1130 # hack for compatibility with Python 2.3:
  1131 if not hasattr(traceback, 'format_exc'):
  1132     from cStringIO import StringIO
  1133     def format_exc(limit=None):
  1134         strbuf = StringIO()
  1135         traceback.print_exc(limit, strbuf)
  1136         return strbuf.getvalue()
  1137     traceback.format_exc = format_exc
  1138 
  1139 def tryall(context, prefix=None):
  1140     """
  1141     Tries a series of functions and prints their results. 
  1142     `context` is a dictionary mapping names to values; 
  1143     the value will only be tried if it's callable.
  1144     
  1145         >>> tryall(dict(j=lambda: True))
  1146         j: True
  1147         ----------------------------------------
  1148         results:
  1149            True: 1
  1150 
  1151     For example, you might have a file `test/stuff.py` 
  1152     with a series of functions testing various things in it. 
  1153     At the bottom, have a line:
  1154 
  1155         if __name__ == "__main__": tryall(globals())
  1156 
  1157     Then you can run `python test/stuff.py` and get the results of 
  1158     all the tests.
  1159     """
  1160     context = context.copy() # vars() would update
  1161     results = {}
  1162     for (key, value) in context.iteritems():
  1163         if not hasattr(value, '__call__'): 
  1164             continue
  1165         if prefix and not key.startswith(prefix): 
  1166             continue
  1167         print key + ':',
  1168         try:
  1169             r = value()
  1170             dictincr(results, r)
  1171             print r
  1172         except:
  1173             print 'ERROR'
  1174             dictincr(results, 'ERROR')
  1175             print '   ' + '\n   '.join(traceback.format_exc().split('\n'))
  1176         
  1177     print '-'*40
  1178     print 'results:'
  1179     for (key, value) in results.iteritems():
  1180         print ' '*2, str(key)+':', value
  1181         
  1182 class ThreadedDict(threadlocal):
  1183     """
  1184     Thread local storage.
  1185     
  1186         >>> d = ThreadedDict()
  1187         >>> d.x = 1
  1188         >>> d.x
  1189         1
  1190         >>> import threading
  1191         >>> def f(): d.x = 2
  1192         ...
  1193         >>> t = threading.Thread(target=f)
  1194         >>> t.start()
  1195         >>> t.join()
  1196         >>> d.x
  1197         1
  1198     """
  1199     _instances = set()
  1200     
  1201     def __init__(self):
  1202         ThreadedDict._instances.add(self)
  1203         
  1204     def __del__(self):
  1205         ThreadedDict._instances.remove(self)
  1206         
  1207     def __hash__(self):
  1208         return id(self)
  1209     
  1210     def clear_all():
  1211         """Clears all ThreadedDict instances.
  1212         """
  1213         for t in list(ThreadedDict._instances):
  1214             t.clear()
  1215     clear_all = staticmethod(clear_all)
  1216     
  1217     # Define all these methods to more or less fully emulate dict -- attribute access
  1218     # is built into threading.local.
  1219 
  1220     def __getitem__(self, key):
  1221         return self.__dict__[key]
  1222 
  1223     def __setitem__(self, key, value):
  1224         self.__dict__[key] = value
  1225 
  1226     def __delitem__(self, key):
  1227         del self.__dict__[key]
  1228 
  1229     def __contains__(self, key):
  1230         return key in self.__dict__
  1231 
  1232     has_key = __contains__
  1233         
  1234     def clear(self):
  1235         self.__dict__.clear()
  1236 
  1237     def copy(self):
  1238         return self.__dict__.copy()
  1239 
  1240     def get(self, key, default=None):
  1241         return self.__dict__.get(key, default)
  1242 
  1243     def items(self):
  1244         return self.__dict__.items()
  1245 
  1246     def iteritems(self):
  1247         return self.__dict__.iteritems()
  1248 
  1249     def keys(self):
  1250         return self.__dict__.keys()
  1251 
  1252     def iterkeys(self):
  1253         return self.__dict__.iterkeys()
  1254 
  1255     iter = iterkeys
  1256 
  1257     def values(self):
  1258         return self.__dict__.values()
  1259 
  1260     def itervalues(self):
  1261         return self.__dict__.itervalues()
  1262 
  1263     def pop(self, key, *args):
  1264         return self.__dict__.pop(key, *args)
  1265 
  1266     def popitem(self):
  1267         return self.__dict__.popitem()
  1268 
  1269     def setdefault(self, key, default=None):
  1270         return self.__dict__.setdefault(key, default)
  1271 
  1272     def update(self, *args, **kwargs):
  1273         self.__dict__.update(*args, **kwargs)
  1274 
  1275     def __repr__(self):
  1276         return '<ThreadedDict %r>' % self.__dict__
  1277 
  1278     __str__ = __repr__
  1279     
  1280 threadeddict = ThreadedDict
  1281 
  1282 def autoassign(self, locals):
  1283     """
  1284     Automatically assigns local variables to `self`.
  1285     
  1286         >>> self = storage()
  1287         >>> autoassign(self, dict(a=1, b=2))
  1288         >>> self
  1289         <Storage {'a': 1, 'b': 2}>
  1290     
  1291     Generally used in `__init__` methods, as in:
  1292 
  1293         def __init__(self, foo, bar, baz=1): autoassign(self, locals())
  1294     """
  1295     for (key, value) in locals.iteritems():
  1296         if key == 'self': 
  1297             continue
  1298         setattr(self, key, value)
  1299 
  1300 def to36(q):
  1301     """
  1302     Converts an integer to base 36 (a useful scheme for human-sayable IDs).
  1303     
  1304         >>> to36(35)
  1305         'z'
  1306         >>> to36(119292)
  1307         '2k1o'
  1308         >>> int(to36(939387374), 36)
  1309         939387374
  1310         >>> to36(0)
  1311         '0'
  1312         >>> to36(-393)
  1313         Traceback (most recent call last):
  1314             ... 
  1315         ValueError: must supply a positive integer
  1316     
  1317     """
  1318     if q < 0: raise ValueError, "must supply a positive integer"
  1319     letters = "0123456789abcdefghijklmnopqrstuvwxyz"
  1320     converted = []
  1321     while q != 0:
  1322         q, r = divmod(q, 36)
  1323         converted.insert(0, letters[r])
  1324     return "".join(converted) or '0'
  1325 
  1326 
  1327 r_url = re_compile('(?<!\()(http://(\S+))')
  1328 def safemarkdown(text):
  1329     """
  1330     Converts text to HTML following the rules of Markdown, but blocking any
  1331     outside HTML input, so that only the things supported by Markdown
  1332     can be used. Also converts raw URLs to links.
  1333 
  1334     (requires [markdown.py](http://webpy.org/markdown.py))
  1335     """
  1336     from markdown import markdown
  1337     if text:
  1338         text = text.replace('<', '&lt;')
  1339         # TODO: automatically get page title?
  1340         text = r_url.sub(r'<\1>', text)
  1341         text = markdown(text)
  1342         return text
  1343 
  1344 def sendmail(from_address, to_address, subject, message, headers=None, **kw):
  1345     """
  1346     Sends the email message `message` with mail and envelope headers
  1347     for from `from_address_` to `to_address` with `subject`. 
  1348     Additional email headers can be specified with the dictionary 
  1349     `headers.
  1350     
  1351     Optionally cc, bcc and attachments can be specified as keyword arguments.
  1352     Attachments must be an iterable and each attachment can be either a 
  1353     filename or a file object or a dictionary with filename, content and 
  1354     optionally content_type keys.
  1355 
  1356     If `web.config.smtp_server` is set, it will send the message
  1357     to that SMTP server. Otherwise it will look for 
  1358     `/usr/sbin/sendmail`, the typical location for the sendmail-style
  1359     binary. To use sendmail from a different path, set `web.config.sendmail_path`.
  1360     """
  1361     attachments = kw.pop("attachments", [])
  1362     mail = _EmailMessage(from_address, to_address, subject, message, headers, **kw)
  1363 
  1364     for a in attachments:
  1365         if isinstance(a, dict):
  1366             mail.attach(a['filename'], a['content'], a.get('content_type'))
  1367         elif hasattr(a, 'read'): # file
  1368             filename = os.path.basename(getattr(a, "name", ""))
  1369             content_type = getattr(a, 'content_type', None)
  1370             mail.attach(filename, a.read(), content_type)
  1371         elif isinstance(a, basestring):
  1372             f = open(a, 'rb')
  1373             content = f.read()
  1374             f.close()
  1375             filename = os.path.basename(a)
  1376             mail.attach(filename, content, None)
  1377         else:
  1378             raise ValueError, "Invalid attachment: %s" % repr(a)
  1379             
  1380     mail.send()
  1381 
  1382 class _EmailMessage:
  1383     def __init__(self, from_address, to_address, subject, message, headers=None, **kw):
  1384         def listify(x):
  1385             if not isinstance(x, list):
  1386                 return [safestr(x)]
  1387             else:
  1388                 return [safestr(a) for a in x]
  1389     
  1390         subject = safestr(subject)
  1391         message = safestr(message)
  1392 
  1393         from_address = safestr(from_address)
  1394         to_address = listify(to_address)    
  1395         cc = listify(kw.get('cc', []))
  1396         bcc = listify(kw.get('bcc', []))
  1397         recipients = to_address + cc + bcc
  1398 
  1399         import email.Utils
  1400         self.from_address = email.Utils.parseaddr(from_address)[1]
  1401         self.recipients = [email.Utils.parseaddr(r)[1] for r in recipients]        
  1402     
  1403         self.headers = dictadd({
  1404           'From': from_address,
  1405           'To': ", ".join(to_address),
  1406           'Subject': subject
  1407         }, headers or {})
  1408 
  1409         if cc:
  1410             self.headers['Cc'] = ", ".join(cc)
  1411     
  1412         self.message = self.new_message()
  1413         self.message.add_header("Content-Transfer-Encoding", "7bit")
  1414         self.message.add_header("Content-Disposition", "inline")
  1415         self.message.add_header("MIME-Version", "1.0")
  1416         self.message.set_payload(message, 'utf-8')
  1417         self.multipart = False
  1418         
  1419     def new_message(self):
  1420         from email.Message import Message
  1421         return Message()
  1422         
  1423     def attach(self, filename, content, content_type=None):
  1424         if not self.multipart:
  1425             msg = self.new_message()
  1426             msg.add_header("Content-Type", "multipart/mixed")
  1427             msg.attach(self.message)
  1428             self.message = msg
  1429             self.multipart = True
  1430                         
  1431         import mimetypes
  1432         try:
  1433             from email import encoders
  1434         except:
  1435             from email import Encoders as encoders
  1436             
  1437         content_type = content_type or mimetypes.guess_type(filename)[0] or "applcation/octet-stream"
  1438         
  1439         msg = self.new_message()
  1440         msg.set_payload(content)
  1441         msg.add_header('Content-Type', content_type)
  1442         msg.add_header('Content-Disposition', 'attachment', filename=filename)
  1443         
  1444         if not content_type.startswith("text/"):
  1445             encoders.encode_base64(msg)
  1446             
  1447         self.message.attach(msg)
  1448 
  1449     def prepare_message(self):
  1450         for k, v in self.headers.iteritems():
  1451             if k.lower() == "content-type":
  1452                 self.message.set_type(v)
  1453             else:
  1454                 self.message.add_header(k, v)
  1455 
  1456         self.headers = {}
  1457 
  1458     def send(self):
  1459         try:
  1460             import webapi
  1461         except ImportError:
  1462             webapi = Storage(config=Storage())
  1463 
  1464         self.prepare_message()
  1465         message_text = self.message.as_string()
  1466     
  1467         if webapi.config.get('smtp_server'):
  1468             server = webapi.config.get('smtp_server')
  1469             port = webapi.config.get('smtp_port', 0)
  1470             username = webapi.config.get('smtp_username') 
  1471             password = webapi.config.get('smtp_password')
  1472             debug_level = webapi.config.get('smtp_debuglevel', None)
  1473             starttls = webapi.config.get('smtp_starttls', False)
  1474 
  1475             import smtplib
  1476             smtpserver = smtplib.SMTP(server, port)
  1477 
  1478             if debug_level:
  1479                 smtpserver.set_debuglevel(debug_level)
  1480 
  1481             if starttls:
  1482                 smtpserver.ehlo()
  1483                 smtpserver.starttls()
  1484                 smtpserver.ehlo()
  1485 
  1486             if username and password:
  1487                 smtpserver.login(username, password)
  1488 
  1489             smtpserver.sendmail(self.from_address, self.recipients, message_text)
  1490             smtpserver.quit()
  1491         elif webapi.config.get('email_engine') == 'aws':
  1492             import boto.ses
  1493             c = boto.ses.SESConnection(
  1494               aws_access_key_id=webapi.config.get('aws_access_key_id'),
  1495               aws_secret_access_key=web.api.config.get('aws_secret_access_key'))
  1496             c.send_raw_email(self.from_address, message_text, self.from_recipients)
  1497         else:
  1498             sendmail = webapi.config.get('sendmail_path', '/usr/sbin/sendmail')
  1499         
  1500             assert not self.from_address.startswith('-'), 'security'
  1501             for r in self.recipients:
  1502                 assert not r.startswith('-'), 'security'
  1503                 
  1504             cmd = [sendmail, '-f', self.from_address] + self.recipients
  1505 
  1506             if subprocess:
  1507                 p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
  1508                 p.stdin.write(message_text)
  1509                 p.stdin.close()
  1510                 p.wait()
  1511             else:
  1512                 i, o = os.popen2(cmd)
  1513                 i.write(message)
  1514                 i.close()
  1515                 o.close()
  1516                 del i, o
  1517                 
  1518     def __repr__(self):
  1519         return "<EmailMessage>"
  1520     
  1521     def __str__(self):
  1522         return self.message.as_string()
  1523 
  1524 if __name__ == "__main__":
  1525     import doctest
  1526     doctest.testmod()