summaryrefslogtreecommitdiff
path: root/packages/python/python-webpy
diff options
context:
space:
mode:
authorMichael Lauer <mickey@vanille-media.de>2006-07-08 11:02:01 +0000
committerOpenEmbedded Project <openembedded-devel@lists.openembedded.org>2006-07-08 11:02:01 +0000
commitf4351b755f744f65d3ff6702db7fd1376df76fc6 (patch)
tree13153e5d96c4b853e7ac5fb63dd4fb88680ca478 /packages/python/python-webpy
parent696e31b3e3602edb6bd3fbcd331e7248a235adbd (diff)
add python-webpy, a lightweight framework for web applications
Diffstat (limited to 'packages/python/python-webpy')
-rw-r--r--packages/python/python-webpy/.mtn2git_empty0
-rw-r--r--packages/python/python-webpy/web.py2349
2 files changed, 2349 insertions, 0 deletions
diff --git a/packages/python/python-webpy/.mtn2git_empty b/packages/python/python-webpy/.mtn2git_empty
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/packages/python/python-webpy/.mtn2git_empty
diff --git a/packages/python/python-webpy/web.py b/packages/python/python-webpy/web.py
new file mode 100644
index 0000000000..2761fa30a0
--- /dev/null
+++ b/packages/python/python-webpy/web.py
@@ -0,0 +1,2349 @@
+#!/usr/bin/env python
+"""web.py: makes web apps (http://webpy.org)"""
+__version__ = "0.1381"
+__revision__ = "$Rev: 72 $"
+__license__ = "public domain"
+__author__ = "Aaron Swartz <me@aaronsw.com>"
+__contributors__ = "see http://webpy.org/changes"
+
+from __future__ import generators
+
+# long term todo:
+# - new form system
+# - new templating system
+# - unit tests?
+
+# todo:
+# - get rid of upvars
+# - break up into separate files
+# - provide an option to use .write()
+# - allow people to do $self.id from inside a reparam
+# - add sqlite support
+# - convert datetimes, floats in WebSafe
+# - locks around memoize
+# - fix memoize to use cacheify style techniques
+# - merge curval query with the insert
+# - figure out how to handle squid, etc. for web.ctx.ip
+
+import os, os.path, sys, time, types, traceback, threading
+import cgi, re, urllib, urlparse, Cookie, pprint
+from threading import currentThread
+from tokenize import tokenprog
+iters = (list, tuple)
+if hasattr(__builtins__, 'set') or (
+ hasattr(__builtins__, 'has_key') and __builtins__.has_key('set')):
+ iters += (set,)
+try:
+ from sets import Set
+ iters += (Set,)
+except ImportError:
+ pass
+try:
+ import datetime, itertools
+except ImportError:
+ pass
+try:
+ from Cheetah.Compiler import Compiler
+ from Cheetah.Filters import Filter
+ _hasTemplating = True
+except ImportError:
+ _hasTemplating = False
+
+try:
+ from DBUtils.PooledDB import PooledDB
+ _hasPooling = True
+except ImportError:
+ _hasPooling = False
+
+# hack for compatibility with Python 2.3:
+if not hasattr(traceback, 'format_exc'):
+ from cStringIO import StringIO
+ def format_exc(limit=None):
+ strbuf = StringIO()
+ traceback.print_exc(limit, strbuf)
+ return strbuf.getvalue()
+ traceback.format_exc = format_exc
+
+## General Utilities
+
+def _strips(direction, text, remove):
+ if direction == 'l':
+ if text.startswith(remove):
+ return text[len(remove):]
+ elif direction == 'r':
+ if text.endswith(remove):
+ return text[:-len(remove)]
+ else:
+ raise ValueError, "Direction needs to be r or l."
+ return text
+
+def rstrips(text, remove):
+ """removes the string `remove` from the right of `text`"""
+ return _strips('r', text, remove)
+
+def lstrips(text, remove):
+ """removes the string `remove` from the left of `text`"""
+ return _strips('l', text, remove)
+
+def strips(text, remove):
+ """removes the string `remove` from the both sides of `text`"""
+ return rstrips(lstrips(text, remove), remove)
+
+def autoassign(self, locals):
+ """
+ Automatically assigns local variables to `self`.
+ Generally used in `__init__` methods, as in:
+
+ def __init__(self, foo, bar, baz=1): autoassign(self, locals())
+ """
+ #locals = sys._getframe(1).f_locals
+ #self = locals['self']
+ for (key, value) in locals.iteritems():
+ if key == 'self':
+ continue
+ setattr(self, key, value)
+
+class Storage(dict):
+ """
+ A Storage object is like a dictionary except `obj.foo` can be used
+ instead of `obj['foo']`. Create one by doing `storage({'a':1})`.
+ """
+ def __getattr__(self, key):
+ if self.has_key(key):
+ return self[key]
+ raise AttributeError, repr(key)
+ def __setattr__(self, key, value):
+ self[key] = value
+ def __repr__(self):
+ return '<Storage ' + dict.__repr__(self) + '>'
+
+storage = Storage
+
+def storify(mapping, *requireds, **defaults):
+ """
+ Creates a `storage` object from dictionary `mapping`, raising `KeyError` if
+ d doesn't have all of the keys in `requireds` and using the default
+ values for keys found in `defaults`.
+
+ For example, `storify({'a':1, 'c':3}, b=2, c=0)` will return the equivalent of
+ `storage({'a':1, 'b':2, 'c':3})`.
+
+ If a `storify` value is a list (e.g. multiple values in a form submission),
+ `storify` returns the last element of the list, unless the key appears in
+ `defaults` as a list. Thus:
+
+ >>> storify({'a':[1, 2]}).a
+ 2
+ >>> storify({'a':[1, 2]}, a=[]).a
+ [1, 2]
+ >>> storify({'a':1}, a=[]).a
+ [1]
+ >>> storify({}, a=[]).a
+ []
+
+ Similarly, if the value has a `value` attribute, `storify will return _its_
+ value, unless the key appears in `defaults` as a dictionary.
+
+ >>> storify({'a':storage(value=1)}).a
+ 1
+ >>> storify({'a':storage(value=1)}, a={}).a
+ <Storage {'value': 1}>
+ >>> storify({}, a={}).a
+ {}
+
+ """
+ def getvalue(x):
+ if hasattr(x, 'value'):
+ return x.value
+ else:
+ return x
+
+ stor = Storage()
+ for key in requireds + tuple(mapping.keys()):
+ value = mapping[key]
+ if isinstance(value, list):
+ if isinstance(defaults.get(key), list):
+ value = [getvalue(x) for x in value]
+ else:
+ value = value[-1]
+ if not isinstance(defaults.get(key), dict):
+ value = getvalue(value)
+ if isinstance(defaults.get(key), list) and not isinstance(value, list):
+ value = [value]
+ setattr(stor, key, value)
+
+ for (key, value) in defaults.iteritems():
+ result = value
+ if hasattr(stor, key):
+ result = stor[key]
+ if value == () and not isinstance(result, tuple):
+ result = (result,)
+ setattr(stor, key, result)
+
+ return stor
+
+class Memoize:
+ """
+ 'Memoizes' a function, caching its return values for each input.
+ """
+ def __init__(self, func):
+ self.func = func
+ self.cache = {}
+ def __call__(self, *args, **keywords):
+ key = (args, tuple(keywords.items()))
+ if key not in self.cache:
+ self.cache[key] = self.func(*args, **keywords)
+ return self.cache[key]
+memoize = Memoize
+
+re_compile = memoize(re.compile) #@@ threadsafe?
+re_compile.__doc__ = """
+A memoized version of re.compile.
+"""
+
+class _re_subm_proxy:
+ def __init__(self):
+ self.match = None
+ def __call__(self, match):
+ self.match = match
+ return ''
+
+def re_subm(pat, repl, string):
+ """Like re.sub, but returns the replacement _and_ the match object."""
+ compiled_pat = re_compile(pat)
+ proxy = _re_subm_proxy()
+ compiled_pat.sub(proxy.__call__, string)
+ return compiled_pat.sub(repl, string), proxy.match
+
+def group(seq, size):
+ """
+ Returns an iterator over a series of lists of length size from iterable.
+
+ For example, `list(group([1,2,3,4], 2))` returns `[[1,2],[3,4]]`.
+ """
+ if not hasattr(seq, 'next'):
+ seq = iter(seq)
+ while True:
+ yield [seq.next() for i in xrange(size)]
+
+class IterBetter:
+ """
+ Returns an object that can be used as an iterator
+ but can also be used via __getitem__ (although it
+ cannot go backwards -- that is, you cannot request
+ `iterbetter[0]` after requesting `iterbetter[1]`).
+ """
+ def __init__(self, iterator):
+ self.i, self.c = iterator, 0
+ def __iter__(self):
+ while 1:
+ yield self.i.next()
+ self.c += 1
+ def __getitem__(self, i):
+ #todo: slices
+ if i > self.c:
+ raise IndexError, "already passed "+str(i)
+ try:
+ while i < self.c:
+ self.i.next()
+ self.c += 1
+ # now self.c == i
+ self.c += 1
+ return self.i.next()
+ except StopIteration:
+ raise IndexError, str(i)
+iterbetter = IterBetter
+
+def dictreverse(mapping):
+ """Takes a dictionary like `{1:2, 3:4}` and returns `{2:1, 4:3}`."""
+ return dict([(value, key) for (key, value) in mapping.iteritems()])
+
+def dictfind(dictionary, element):
+ """
+ Returns a key whose value in `dictionary` is `element`
+ or, if none exists, None.
+ """
+ for (key, value) in dictionary.iteritems():
+ if element is value:
+ return key
+
+def dictfindall(dictionary, element):
+ """
+ Returns the keys whose values in `dictionary` are `element`
+ or, if none exists, [].
+ """
+ res = []
+ for (key, value) in dictionary.iteritems():
+ if element is value:
+ res.append(key)
+ return res
+
+def dictincr(dictionary, element):
+ """
+ Increments `element` in `dictionary`,
+ setting it to one if it doesn't exist.
+ """
+ dictionary.setdefault(element, 0)
+ dictionary[element] += 1
+ return dictionary[element]
+
+def dictadd(dict_a, dict_b):
+ """
+ Returns a dictionary consisting of the keys in `a` and `b`.
+ If they share a key, the value from b is used.
+ """
+ result = {}
+ result.update(dict_a)
+ result.update(dict_b)
+ return result
+
+sumdicts = dictadd # deprecated
+
+def listget(lst, ind, default=None):
+ """Returns `lst[ind]` if it exists, `default` otherwise."""
+ if len(lst)-1 < ind:
+ return default
+ return lst[ind]
+
+def intget(integer, default=None):
+ """Returns `integer` as an int or `default` if it can't."""
+ try:
+ return int(integer)
+ except (TypeError, ValueError):
+ return default
+
+def datestr(then, now=None):
+ """Converts a (UTC) datetime object to a nice string representation."""
+ def agohence(n, what, divisor=None):
+ if divisor: n = n // divisor
+
+ out = str(abs(n)) + ' ' + what # '2 day'
+ if abs(n) != 1: out += 's' # '2 days'
+ out += ' ' # '2 days '
+ if n < 0:
+ out += 'from now'
+ else:
+ out += 'ago'
+ return out # '2 days ago'
+
+ oneday = 24 * 60 * 60
+
+ if not now: now = datetime.datetime.utcnow()
+ delta = now - then
+ deltaseconds = int(delta.days * oneday + delta.seconds + delta.microseconds * 1e-06)
+ deltadays = abs(deltaseconds) // oneday
+ if deltaseconds < 0: deltadays *= -1 # fix for oddity of floor
+
+ if deltadays:
+ if abs(deltadays) < 4:
+ return agohence(deltadays, 'day')
+
+ out = then.strftime('%B %e') # e.g. 'June 13'
+ if then.year != now.year or deltadays < 0:
+ out += ', %s' % then.year
+ return out
+
+ if int(deltaseconds):
+ if abs(deltaseconds) > (60 * 60):
+ return agohence(deltaseconds, 'hour', 60 * 60)
+ elif abs(deltaseconds) > 60:
+ return agohence(deltaseconds, 'minute', 60)
+ else:
+ return agohence(deltaseconds, 'second')
+
+ deltamicroseconds = delta.microseconds
+ if delta.days: deltamicroseconds = int(delta.microseconds - 1e6) # datetime oddity
+ if abs(deltamicroseconds) > 1000:
+ return agohence(deltamicroseconds, 'millisecond', 1000)
+
+ return agohence(deltamicroseconds, 'microsecond')
+
+def upvars(level=2):
+ """Guido van Rossum doesn't want you to use this function."""
+ return dictadd(
+ sys._getframe(level).f_globals,
+ sys._getframe(level).f_locals)
+
+class CaptureStdout:
+ """
+ Captures everything func prints to stdout and returns it instead.
+
+ **WARNING:** Not threadsafe!
+ """
+ def __init__(self, func):
+ self.func = func
+ def __call__(self, *args, **keywords):
+ from cStringIO import StringIO
+ # Not threadsafe!
+ out = StringIO()
+ oldstdout = sys.stdout
+ sys.stdout = out
+ try:
+ self.func(*args, **keywords)
+ finally:
+ sys.stdout = oldstdout
+ return out.getvalue()
+capturestdout = CaptureStdout
+
+class Profile:
+ """
+ Profiles `func` and returns a tuple containing its output
+ and a string with human-readable profiling information.
+ """
+ def __init__(self, func):
+ self.func = func
+ def __call__(self, *args): ##, **kw): kw unused
+ import hotshot, hotshot.stats, tempfile ##, time already imported
+ temp = tempfile.NamedTemporaryFile()
+ prof = hotshot.Profile(temp.name)
+
+ stime = time.time()
+ result = prof.runcall(self.func, *args)
+ stime = time.time() - stime
+
+ prof.close()
+ stats = hotshot.stats.load(temp.name)
+ stats.strip_dirs()
+ stats.sort_stats('time', 'calls')
+ x = '\n\ntook '+ str(stime) + ' seconds\n'
+ x += capturestdout(stats.print_stats)(40)
+ x += capturestdout(stats.print_callers)()
+ return result, x
+profile = Profile
+
+def tryall(context, prefix=None):
+ """
+ Tries a series of functions and prints their results.
+ `context` is a dictionary mapping names to values;
+ the value will only be tried if it's callable.
+
+ For example, you might have a file `test/stuff.py`
+ with a series of functions testing various things in it.
+ At the bottom, have a line:
+
+ if __name__ == "__main__": tryall(globals())
+
+ Then you can run `python test/stuff.py` and get the results of
+ all the tests.
+ """
+ context = context.copy() # vars() would update
+ results = {}
+ for (key, value) in context.iteritems():
+ if not hasattr(value, '__call__'):
+ continue
+ if prefix and not key.startswith(prefix):
+ continue
+ print key + ':',
+ try:
+ r = value()
+ dictincr(results, r)
+ print r
+ except:
+ print 'ERROR'
+ dictincr(results, 'ERROR')
+ print ' ' + '\n '.join(traceback.format_exc().split('\n'))
+
+ print '-'*40
+ print 'results:'
+ for (key, value) in results.iteritems():
+ print ' '*2, str(key)+':', value
+
+class ThreadedDict:
+ """
+ Takes a dictionary that maps threads to objects.
+ When a thread tries to get or set an attribute or item
+ of the threadeddict, it passes it on to the object
+ for that thread in dictionary.
+ """
+ def __init__(self, dictionary):
+ self.__dict__['_ThreadedDict__d'] = dictionary
+ def __getattr__(self, attr):
+ return getattr(self.__d[currentThread()], attr)
+ def __getitem__(self, item):
+ return self.__d[currentThread()][item]
+ def __setattr__(self, attr, value):
+ if attr == '__doc__':
+ self.__dict__[attr] = value
+ else:
+ return setattr(self.__d[currentThread()], attr, value)
+ def __setitem__(self, item, value):
+ self.__d[currentThread()][item] = value
+ def __hash__(self):
+ return hash(self.__d[currentThread()])
+threadeddict = ThreadedDict
+
+## IP Utilities
+
+def validipaddr(address):
+ """returns True if `address` is a valid IPv4 address"""
+ try:
+ octets = address.split('.')
+ assert len(octets) == 4
+ for x in octets:
+ assert 0 <= int(x) <= 255
+ except (AssertionError, ValueError):
+ return False
+ return True
+
+def validipport(port):
+ """returns True if `port` is a valid IPv4 port"""
+ try:
+ assert 0 <= int(port) <= 65535
+ except (AssertionError, ValueError):
+ return False
+ return True
+
+def validip(ip, defaultaddr="0.0.0.0", defaultport=8080):
+ """returns `(ip_address, port)` from string `ip_addr_port`"""
+ addr = defaultaddr
+ port = defaultport
+
+ ip = ip.split(":", 1)
+ if len(ip) == 1:
+ if not ip[0]:
+ pass
+ elif validipaddr(ip[0]):
+ addr = ip[0]
+ elif validipport(ip[0]):
+ port = int(ip[0])
+ else:
+ raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
+ elif len(ip) == 2:
+ addr, port = ip
+ if not validipaddr(addr) and validipport(port):
+ raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
+ port = int(port)
+ else:
+ raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
+ return (addr, port)
+
+def validaddr(string_):
+ """returns either (ip_address, port) or "/path/to/socket" from string_"""
+ if '/' in string_:
+ return string_
+ else:
+ return validip(string_)
+
+## URL Utilities
+
+def prefixurl(base=''):
+ """
+ Sorry, this function is really difficult to explain.
+ Maybe some other time.
+ """
+ url = ctx.path.lstrip('/')
+ for i in xrange(url.count('/')):
+ base += '../'
+ if not base:
+ base = './'
+ return base
+
+def urlquote(x): return urllib.quote(websafe(x).encode('utf-8'))
+
+## Formatting
+
+try:
+ from markdown import markdown # http://webpy.org/markdown.py
+except ImportError:
+ pass
+
+r_url = re_compile('(?<!\()(http://(\S+))')
+def safemarkdown(text):
+ """
+ Converts text to HTML following the rules of Markdown, but blocking any
+ outside HTML input, so that only the things supported by Markdown
+ can be used. Also converts raw URLs to links.
+
+ (requires [markdown.py](http://webpy.org/markdown.py))
+ """
+ if text:
+ text = text.replace('<', '&lt;')
+ # TODO: automatically get page title?
+ text = r_url.sub(r'<\1>', text)
+ text = markdown(text)
+ return text
+
+## Databases
+
+class _ItplError(ValueError):
+ """String Interpolation Error
+ from <http://lfw.org/python/Itpl.py>
+ (cf. below for license)
+ """
+ def __init__(self, text, pos):
+ ValueError.__init__(self)
+ self.text = text
+ self.pos = pos
+ def __str__(self):
+ return "unfinished expression in %s at char %d" % (
+ repr(self.text), self.pos)
+
+def _interpolate(format):
+ """
+ Takes a format string and returns a list of 2-tuples of the form
+ (boolean, string) where boolean says whether string should be evaled
+ or not.
+
+ from <http://lfw.org/python/Itpl.py> (public domain, Ka-Ping Yee)
+ """
+ def matchorfail(text, pos):
+ match = tokenprog.match(text, pos)
+ if match is None:
+ raise _ItplError(text, pos)
+ return match, match.end()
+
+ namechars = "abcdefghijklmnopqrstuvwxyz" \
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
+ chunks = []
+ pos = 0
+
+ while 1:
+ dollar = format.find("$", pos)
+ if dollar < 0:
+ break
+ nextchar = format[dollar + 1]
+
+ if nextchar == "{":
+ chunks.append((0, format[pos:dollar]))
+ pos, level = dollar + 2, 1
+ while level:
+ match, pos = matchorfail(format, pos)
+ tstart, tend = match.regs[3]
+ token = format[tstart:tend]
+ if token == "{":
+ level = level + 1
+ elif token == "}":
+ level = level - 1
+ chunks.append((1, format[dollar + 2:pos - 1]))
+
+ elif nextchar in namechars:
+ chunks.append((0, format[pos:dollar]))
+ match, pos = matchorfail(format, dollar + 1)
+ while pos < len(format):
+ if format[pos] == "." and \
+ pos + 1 < len(format) and format[pos + 1] in namechars:
+ match, pos = matchorfail(format, pos + 1)
+ elif format[pos] in "([":
+ pos, level = pos + 1, 1
+ while level:
+ match, pos = matchorfail(format, pos)
+ tstart, tend = match.regs[3]
+ token = format[tstart:tend]
+ if token[0] in "([":
+ level = level + 1
+ elif token[0] in ")]":
+ level = level - 1
+ else:
+ break
+ chunks.append((1, format[dollar + 1:pos]))
+
+ else:
+ chunks.append((0, format[pos:dollar + 1]))
+ pos = dollar + 1 + (nextchar == "$")
+
+ if pos < len(format):
+ chunks.append((0, format[pos:]))
+ return chunks
+
+def sqlors(left, lst):
+ """
+ `left is a SQL clause like `tablename.arg = `
+ and `lst` is a list of values. Returns a reparam-style
+ pair featuring the SQL that ORs together the clause
+ for each item in the lst.
+
+ For example:
+
+ web.sqlors('foo =', [1,2,3])
+
+ would result in:
+
+ foo = 1 OR foo = 2 OR foo = 3
+ """
+ if isinstance(lst, iters):
+ lst = list(lst)
+ ln = len(lst)
+ if ln == 0:
+ return ("2+2=5", [])
+ if ln == 1:
+ lst = lst[0]
+
+ if isinstance(lst, iters):
+ return '(' + left + \
+ (' OR ' + left).join([aparam() for param in lst]) + ")", lst
+ else:
+ return left + aparam(), [lst]
+
+class UnknownParamstyle(Exception):
+ """raised for unsupported db paramstyles
+
+ Currently supported: qmark,numeric, format, pyformat
+ """
+ pass
+
+def aparam():
+ """Use in a SQL string to make a spot for a db value."""
+ style = ctx.db_module.paramstyle
+ if style == 'qmark':
+ return '?'
+ elif style == 'numeric':
+ return ':1'
+ elif style in ['format', 'pyformat']:
+ return '%s'
+ raise UnknownParamstyle, style
+
+def reparam(string_, dictionary):
+ """
+ Takes a string and a dictionary and interpolates the string
+ using values from the dictionary. Returns a 2-tuple containing
+ the a string with `aparam()`s in it and a list of the matching values.
+
+ You can pass this sort of thing as a clause in any db function.
+ Otherwise, you can pass a dictionary to the keyword argument `vars`
+ and the function will call reparam for you.
+ """
+ vals = []
+ result = []
+ for live, chunk in _interpolate(string_):
+ if live:
+ result.append(aparam())
+ vals.append(eval(chunk, dictionary))
+ else: result.append(chunk)
+ return ''.join(result), vals
+
+class UnknownDB(Exception):
+ """raised for unsupported dbms"""
+ pass
+def connect(dbn, **keywords):
+ """
+ Connects to the specified database.
+ db currently must be "postgres" or "mysql".
+ If DBUtils is installed, connection pooling will be used.
+ """
+ if dbn == "postgres":
+ try:
+ import psycopg2 as db
+ except ImportError:
+ try:
+ import psycopg as db
+ except ImportError:
+ import pgdb as db
+ keywords['password'] = keywords['pw']
+ del keywords['pw']
+ keywords['database'] = keywords['db']
+ del keywords['db']
+ elif dbn == "mysql":
+ import MySQLdb as db
+ keywords['passwd'] = keywords['pw']
+ del keywords['pw']
+ db.paramstyle = 'pyformat' # it's both, like psycopg
+ elif dbn == "sqlite":
+ try: ## try first sqlite3 version
+ from pysqlite2 import dbapi2 as db
+ db.paramstyle = 'qmark'
+ except ImportError: ## else try sqlite2
+ import sqlite as db
+ keywords['database'] = keywords['db']
+ del keywords['db']
+ else:
+ raise UnknownDB, dbn
+ ctx.db_name = dbn
+ ctx.db_module = db
+ ctx.db_transaction = False
+ if _hasPooling:
+ if 'db' not in globals():
+ globals()['db'] = PooledDB(dbapi=db, **keywords)
+ ctx.db = globals()['db'].connection()
+ else:
+ ctx.db = db.connect(**keywords)
+ ctx.dbq_count = 0
+ if globals().get('db_printing'):
+ def db_execute(cur, sql_query, d=None):
+ """executes an sql query"""
+
+ def sqlquote(obj):
+ """converts `obj` to its proper SQL version"""
+
+ # because `1 == True and hash(1) == hash(True)`
+ # we have to do this the hard way...
+
+ if obj is None:
+ return 'NULL'
+ elif obj is True:
+ return "'t'"
+ elif obj is False:
+ return "'f'"
+ elif isinstance(obj, datetime.datetime):
+ return repr(obj.isoformat())
+ else:
+ return repr(obj)
+
+ ctx.dbq_count += 1
+ try:
+ outq = sql_query % tuple(map(sqlquote, d))
+ except TypeError:
+ outq = sql_query
+ print >> debug, str(ctx.dbq_count)+':', outq
+ a = time.time()
+ out = cur.execute(sql_query, d)
+ b = time.time()
+ print >> debug, '(%s)' % round(b - a, 2)
+ return out
+ ctx.db_execute = db_execute
+ else:
+ ctx.db_execute = lambda cur, sql_query, d=None: \
+ cur.execute(sql_query, d)
+ return ctx.db
+
+def transact():
+ """Start a transaction."""
+ # commit everything up to now, so we don't rollback it later
+ ctx.db.commit()
+ ctx.db_transaction = True
+
+def commit():
+ """Commits a transaction."""
+ ctx.db.commit()
+ ctx.db_transaction = False
+
+def rollback():
+ """Rolls back a transaction."""
+ ctx.db.rollback()
+ ctx.db_transaction = False
+
+def query(sql_query, vars=None, processed=False):
+ """
+ Execute SQL query `sql_query` using dictionary `vars` to interpolate it.
+ If `processed=True`, `vars` is a `reparam`-style list to use
+ instead of interpolating.
+ """
+ if vars is None:
+ vars = {}
+ db_cursor = ctx.db.cursor()
+
+ if not processed:
+ sql_query, vars = reparam(sql_query, vars)
+ ctx.db_execute(db_cursor, sql_query, vars)
+ if db_cursor.description:
+ names = [x[0] for x in db_cursor.description]
+ def iterwrapper():
+ row = db_cursor.fetchone()
+ while row:
+ yield Storage(dict(zip(names, row)))
+ row = db_cursor.fetchone()
+ out = iterbetter(iterwrapper())
+ out.__len__ = lambda: int(db_cursor.rowcount)
+ out.list = lambda: [Storage(dict(zip(names, x))) \
+ for x in db_cursor.fetchall()]
+ else:
+ out = db_cursor.rowcount
+
+ if not ctx.db_transaction:
+ ctx.db.commit()
+ return out
+
+def sqllist(lst):
+ """
+ If a list, converts it to a comma-separated string.
+ Otherwise, returns the string.
+ """
+ if isinstance(lst, str):
+ return lst
+ else: return ', '.join(lst)
+
+def sqlwhere(dictionary):
+ """
+ Converts a `dictionary` to an SQL WHERE clause in
+ `reparam` format. Thus,
+
+ {'cust_id': 2, 'order_id':3}
+
+ would result in the equivalent of:
+
+ 'cust_id = 2 AND order_id = 3'
+
+ but properly quoted.
+ """
+
+ return ' AND '.join([
+ '%s = %s' % (k, aparam()) for k in dictionary.keys()
+ ]), dictionary.values()
+
+def select(tables, vars=None, what='*', where=None, order=None, group=None,
+ limit=None, offset=None):
+ """
+ Selects `what` from `tables` with clauses `where`, `order`,
+ `group`, `limit`, and `offset. Uses vars to interpolate.
+ Otherwise, each clause can take a reparam-style list.
+ """
+ if vars is None:
+ vars = {}
+ values = []
+ qout = ""
+
+ for (sql, val) in (
+ ('SELECT', what),
+ ('FROM', sqllist(tables)),
+ ('WHERE', where),
+ ('GROUP BY', group),
+ ('ORDER BY', order),
+ ('LIMIT', limit),
+ ('OFFSET', offset)):
+ if isinstance(val, (int, long)):
+ if sql == 'WHERE':
+ nquery, nvalue = 'id = '+aparam(), [val]
+ else:
+ nquery, nvalue = str(val), ()
+ elif isinstance(val, (list, tuple)) and len(val) == 2:
+ nquery, nvalue = val
+ elif val:
+ nquery, nvalue = reparam(val, vars)
+ else:
+ continue
+ qout += " " + sql + " " + nquery
+ values.extend(nvalue)
+ return query(qout, values, processed=True)
+
+def insert(tablename, seqname=None, **values):
+ """
+ Inserts `values` into `tablename`. Returns current sequence ID.
+ Set `seqname` to the ID if it's not the default, or to `False`
+ if there isn't one.
+ """
+ db_cursor = ctx.db.cursor()
+
+ if values:
+ sql_query, v = "INSERT INTO %s (%s) VALUES (%s)" % (
+ tablename,
+ ", ".join(values.keys()),
+ ', '.join([aparam() for x in values])
+ ), values.values()
+ else:
+ sql_query, v = "INSERT INTO %s DEFAULT VALUES" % tablename, None
+
+ if seqname is False:
+ pass
+ elif ctx.db_name == "postgres":
+ if seqname is None:
+ seqname = tablename + "_id_seq"
+ sql_query += "; SELECT currval('%s')" % seqname
+ elif ctx.db_name == "mysql":
+ ctx.db_execute(db_cursor, sql_query, v)
+ sql_query = "SELECT last_insert_id()"
+ v = ()
+ elif ctx.db_name == "sqlite":
+ ctx.db_execute(db_cursor, sql_query, v)
+ # not really the same...
+ sql_query = "SELECT last_insert_rowid()"
+ v = ()
+
+ ctx.db_execute(db_cursor, sql_query, v)
+ try:
+ out = db_cursor.fetchone()[0]
+ except Exception:
+ out = None
+
+ if not ctx.db_transaction:
+ ctx.db.commit()
+
+ return out
+
+def update(tables, where, vars=None, **values):
+ """
+ Update `tables` with clause `where` (interpolated using `vars`)
+ and setting `values`.
+ """
+ if vars is None:
+ vars = {}
+ if isinstance(where, (int, long)):
+ vars = [where]
+ where = "id = " + aparam()
+ elif isinstance(where, (list, tuple)) and len(where) == 2:
+ where, vars = where
+ else:
+ where, vars = reparam(where, vars)
+
+ db_cursor = ctx.db.cursor()
+ ctx.db_execute(db_cursor, "UPDATE %s SET %s WHERE %s" % (
+ sqllist(tables),
+ ', '.join([k + '=' + aparam() for k in values.keys()]),
+ where),
+ values.values() + vars)
+
+ if not ctx.db_transaction:
+ ctx.db.commit()
+ return db_cursor.rowcount
+
+def delete(table, where, using=None, vars=None):
+ """
+ Deletes from `table` with clauses `where` and `using`.
+ """
+ if vars is None:
+ vars = {}
+ db_cursor = ctx.db.cursor()
+
+ if isinstance(where, (int, long)):
+ vars = [where]
+ where = "id = " + aparam()
+ elif isinstance(where, (list, tuple)) and len(where) == 2:
+ where, vars = where
+ else:
+ where, vars = reparam(where, vars)
+ q = 'DELETE FROM %s WHERE %s' % (table, where)
+ if using:
+ q += ' USING ' + sqllist(using)
+ ctx.db_execute(db_cursor, q, vars)
+
+ if not ctx.db_transaction:
+ ctx.db.commit()
+ return db_cursor.rowcount
+
+## Request Handlers
+
+def handle(mapping, fvars=None):
+ """
+ Call the appropriate function based on the url to function mapping in `mapping`.
+ If no module for the function is specified, look up the function in `fvars`. If
+ `fvars` is empty, using the caller's context.
+
+ `mapping` should be a tuple of paired regular expressions with function name
+ substitutions. `handle` will import modules as necessary.
+ """
+ for url, ofno in group(mapping, 2):
+ if isinstance(ofno, tuple):
+ ofn, fna = ofno[0], list(ofno[1:])
+ else:
+ ofn, fna = ofno, []
+ fn, result = re_subm('^' + url + '$', ofn, ctx.path)
+ if result: # it's a match
+ if fn.split(' ', 1)[0] == "redirect":
+ url = fn.split(' ', 1)[1]
+ if ctx.method == "GET":
+ x = ctx.env.get('QUERY_STRING', '')
+ if x:
+ url += '?' + x
+ return redirect(url)
+ elif '.' in fn:
+ x = fn.split('.')
+ mod, cls = '.'.join(x[:-1]), x[-1]
+ mod = __import__(mod, globals(), locals(), [""])
+ cls = getattr(mod, cls)
+ else:
+ cls = fn
+ mod = fvars or upvars()
+ if isinstance(mod, types.ModuleType):
+ mod = vars(mod)
+ try:
+ cls = mod[cls]
+ except KeyError:
+ return notfound()
+
+ meth = ctx.method
+ if meth == "HEAD":
+ if not hasattr(cls, meth):
+ meth = "GET"
+ if not hasattr(cls, meth):
+ return nomethod(cls)
+ tocall = getattr(cls(), meth)
+ args = list(result.groups())
+ for d in re.findall(r'\\(\d+)', ofn):
+ args.pop(int(d) - 1)
+ return tocall(*([urllib.unquote(x) for x in args] + fna))
+
+ return notfound()
+
+def autodelegate(prefix=''):
+ """
+ Returns a method that takes one argument and calls the method named prefix+arg,
+ calling `notfound()` if there isn't one. Example:
+
+ urls = ('/prefs/(.*)', 'prefs')
+
+ class prefs:
+ GET = autodelegate('GET_')
+ def GET_password(self): pass
+ def GET_privacy(self): pass
+
+ `GET_password` would get called for `/prefs/password` while `GET_privacy` for
+ `GET_privacy` gets called for `/prefs/privacy`.
+
+ If a user visits `/prefs/password/change` then `GET_password(self, '/change')`
+ is called.
+ """
+ def internal(self, arg):
+ if '/' in arg:
+ first, rest = arg.split('/', 1)
+ func = prefix + first
+ args = ['/' + rest]
+ else:
+ func = prefix + arg
+ args = []
+
+ if hasattr(self, func):
+ try:
+ return getattr(self, func)(*args)
+ except TypeError:
+ return notfound()
+ else:
+ return notfound()
+ return internal
+
+def background(func):
+ """A function decorator to run a long-running function as a background thread."""
+ def internal(*a, **kw):
+ data() # cache it
+ ctx = _context[currentThread()]
+ _context[currentThread()] = storage(ctx.copy())
+
+ def newfunc():
+ _context[currentThread()] = ctx
+ func(*a, **kw)
+
+ t = threading.Thread(target=newfunc)
+ background.threaddb[id(t)] = t
+ t.start()
+ ctx.headers = []
+ return seeother(changequery(_t=id(t)))
+ return internal
+background.threaddb = {}
+
+def backgrounder(func):
+ def internal(*a, **kw):
+ i = input(_method='get')
+ if '_t' in i:
+ try:
+ t = background.threaddb[int(i._t)]
+ except KeyError:
+ return notfound()
+ _context[currentThread()] = _context[t]
+ return
+ else:
+ return func(*a, **kw)
+ return internal
+
+## HTTP Functions
+
+def httpdate(date_obj):
+ """Formats a datetime object for use in HTTP headers."""
+ return date_obj.strftime("%a, %d %b %Y %H:%M:%S GMT")
+
+def parsehttpdate(string_):
+ """Parses an HTTP date into a datetime object."""
+ try:
+ t = time.strptime(string_, "%a, %d %b %Y %H:%M:%S %Z")
+ except ValueError:
+ return None
+ return datetime.datetime(*t[:6])
+
+def expires(delta):
+ """
+ Outputs an `Expires` header for `delta` from now.
+ `delta` is a `timedelta` object or a number of seconds.
+ """
+ try:
+ datetime
+ except NameError:
+ raise Exception, "requires Python 2.3 or later"
+ if isinstance(delta, (int, long)):
+ delta = datetime.timedelta(seconds=delta)
+ date_obj = datetime.datetime.utcnow() + delta
+ header('Expires', httpdate(date_obj))
+
+def lastmodified(date_obj):
+ """Outputs a `Last-Modified` header for `datetime`."""
+ header('Last-Modified', httpdate(date_obj))
+
+def modified(date=None, etag=None):
+ n = ctx.env.get('HTTP_IF_NONE_MATCH')
+ m = parsehttpdate(ctx.env.get('HTTP_IF_MODIFIED_SINCE', '').split(';')[0])
+ validate = False
+ if etag:
+ raise NotImplementedError, "no etag support yet"
+ # should really be a warning
+ if date and m:
+ # we subtract a second because
+ # HTTP dates don't have sub-second precision
+ if date-datetime.timedelta(seconds=1) <= m:
+ validate = True
+
+ if validate: ctx.status = '304 Not Modified'
+ return not validate
+
+"""
+By default, these all return simple error messages that send very short messages
+(like "bad request") to the user. They can and should be overridden
+to return nicer ones.
+"""
+def redirect(url, status='301 Moved Permanently'):
+ """
+ Returns a `status` redirect to the new URL.
+ `url` is joined with the base URL so that things like
+ `redirect("about") will work properly.
+ """
+ newloc = urlparse.urljoin(ctx.home + ctx.path, url)
+ ctx.status = status
+ ctx.output = ''
+ header('Content-Type', 'text/html')
+ header('Location', newloc)
+ # seems to add a three-second delay for some reason:
+ # output('<a href="'+ newloc + '">moved permanently</a>')
+
+def found(url):
+ """A `302 Found` redirect."""
+ return redirect(url, '302 Found')
+
+def seeother(url):
+ """A `303 See Other` redirect."""
+ return redirect(url, '303 See Other')
+
+def tempredirect(url):
+ """A `307 Temporary Redirect` redirect."""
+ return redirect(url, '307 Temporary Redirect')
+
+def badrequest():
+ """Return a `400 Bad Request` error."""
+ ctx.status = '400 Bad Request'
+ header('Content-Type', 'text/html')
+ return output('bad request')
+
+def notfound():
+ """Returns a `404 Not Found` error."""
+ ctx.status = '404 Not Found'
+ header('Content-Type', 'text/html')
+ return output('not found')
+
+def nomethod(cls):
+ """Returns a `405 Method Not Allowed` error for `cls`."""
+ ctx.status = '405 Method Not Allowed'
+ header('Content-Type', 'text/html')
+ header('Allow', \
+ ', '.join([method for method in \
+ ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'] \
+ if hasattr(cls, method)]))
+
+ # commented out for the same reason redirect is
+ # return output('method not allowed')
+
+def gone():
+ """Returns a `410 Gone` error."""
+ ctx.status = '410 Gone'
+ header('Content-Type', 'text/html')
+ return output("gone")
+
+def internalerror():
+ """Returns a `500 Internal Server` error."""
+ ctx.status = "500 Internal Server Error"
+ ctx.headers = [('Content-Type', 'text/html')]
+ ctx.output = "internal server error"
+
+
+# adapted from Django <djangoproject.com>
+# Copyright (c) 2005, the Lawrence Journal-World
+# Used under the modified BSD license:
+# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
+
+DJANGO_500_PAGE = """#import inspect
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta name="robots" content="NONE,NOARCHIVE" />
+ <title>$exception_type at $ctx.path</title>
+ <style type="text/css">
+ html * { padding:0; margin:0; }
+ body * { padding:10px 20px; }
+ body * * { padding:0; }
+ body { font:small sans-serif; }
+ body>div { border-bottom:1px solid #ddd; }
+ h1 { font-weight:normal; }
+ h2 { margin-bottom:.8em; }
+ h2 span { font-size:80%; color:#666; font-weight:normal; }
+ h3 { margin:1em 0 .5em 0; }
+ h4 { margin:0 0 .5em 0; font-weight: normal; }
+ table {
+ border:1px solid #ccc; border-collapse: collapse; background:white; }
+ tbody td, tbody th { vertical-align:top; padding:2px 3px; }
+ thead th {
+ padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
+ font-weight:normal; font-size:11px; border:1px solid #ddd; }
+ tbody th { text-align:right; color:#666; padding-right:.5em; }
+ table.vars { margin:5px 0 2px 40px; }
+ table.vars td, table.req td { font-family:monospace; }
+ table td.code { width:100%;}
+ table td.code div { overflow:hidden; }
+ table.source th { color:#666; }
+ table.source td {
+ font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
+ ul.traceback { list-style-type:none; }
+ ul.traceback li.frame { margin-bottom:1em; }
+ div.context { margin: 10px 0; }
+ div.context ol {
+ padding-left:30px; margin:0 10px; list-style-position: inside; }
+ div.context ol li {
+ font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
+ div.context ol.context-line li { color:black; background-color:#ccc; }
+ div.context ol.context-line li span { float: right; }
+ div.commands { margin-left: 40px; }
+ div.commands a { color:black; text-decoration:none; }
+ #summary { background: #ffc; }
+ #summary h2 { font-weight: normal; color: #666; }
+ #explanation { background:#eee; }
+ #template, #template-not-exist { background:#f6f6f6; }
+ #template-not-exist ul { margin: 0 0 0 20px; }
+ #traceback { background:#eee; }
+ #requestinfo { background:#f6f6f6; padding-left:120px; }
+ #summary table { border:none; background:transparent; }
+ #requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
+ #requestinfo h3 { margin-bottom:-1em; }
+ .error { background: #ffc; }
+ .specific { color:#cc3300; font-weight:bold; }
+ </style>
+ <script type="text/javascript">
+ //<!--
+ function getElementsByClassName(oElm, strTagName, strClassName){
+ // Written by Jonathan Snook, http://www.snook.ca/jon;
+ // Add-ons by Robert Nyman, http://www.robertnyman.com
+ var arrElements = (strTagName == "*" && document.all)? document.all :
+ oElm.getElementsByTagName(strTagName);
+ var arrReturnElements = new Array();
+ strClassName = strClassName.replace(/\-/g, "\\-");
+ var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$)");
+ var oElement;
+ for(var i=0; i<arrElements.length; i++){
+ oElement = arrElements[i];
+ if(oRegExp.test(oElement.className)){
+ arrReturnElements.push(oElement);
+ }
+ }
+ return (arrReturnElements)
+ }
+ function hideAll(elems) {
+ for (var e = 0; e < elems.length; e++) {
+ elems[e].style.display = 'none';
+ }
+ }
+ window.onload = function() {
+ hideAll(getElementsByClassName(document, 'table', 'vars'));
+ hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
+ hideAll(getElementsByClassName(document, 'ol', 'post-context'));
+ }
+ function toggle() {
+ for (var i = 0; i < arguments.length; i++) {
+ var e = document.getElementById(arguments[i]);
+ if (e) {
+ e.style.display = e.style.display == 'none' ? 'block' : 'none';
+ }
+ }
+ return false;
+ }
+ function varToggle(link, id) {
+ toggle('v' + id);
+ var s = link.getElementsByTagName('span')[0];
+ var uarr = String.fromCharCode(0x25b6);
+ var darr = String.fromCharCode(0x25bc);
+ s.innerHTML = s.innerHTML == uarr ? darr : uarr;
+ return false;
+ }
+ //-->
+ </script>
+</head>
+<body>
+
+<div id="summary">
+ <h1>$exception_type at $ctx.path</h1>
+ <h2>$exception_value</h2>
+ <table><tr>
+ <th>Python</th>
+ <td>$lastframe.filename in $lastframe.function, line $lastframe.lineno</td>
+ </tr><tr>
+ <th>Web</th>
+ <td>$ctx.method $ctx.home$ctx.path</td>
+ </tr></table>
+</div>
+<div id="traceback">
+ <h2>Traceback <span>(innermost first)</span></h2>
+ <ul class="traceback">
+ #for frame in $frames
+ <li class="frame">
+ <code>$frame.filename</code> in <code>$frame.function</code>
+
+ #if $frame.context_line
+ <div class="context" id="c$frame.id">
+ #if $frame.pre_context
+ <ol start="$frame.pre_context_lineno" class="pre-context" id="pre$frame.id">#for line in $frame.pre_context#<li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>#end for#</ol>
+ #end if
+ <ol start="$frame.lineno" class="context-line"><li onclick="toggle('pre$frame.id', 'post$frame.id')">$frame.context_line <span>...</span></li></ol>
+ #if $frame.post_context
+ <ol start='$(frame.lineno+1)' class="post-context" id="post$frame.id">#for line in $frame.post_context#<li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>#end for#</ol>
+ #end if
+ </div>
+ #end if
+
+ #if $frame.vars
+ <div class="commands">
+ <a href='#' onclick="return varToggle(this, '$frame.id')"><span>&#x25b6;</span> Local vars</a>## $inspect.formatargvalues(*inspect.getargvalues(frame['tb'].tb_frame))
+ </div>
+ <table class="vars" id="v$frame.id">
+ <thead>
+ <tr>
+ <th>Variable</th>
+ <th>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ #set frameitems = $frame.vars
+ #silent frameitems.sort(lambda x,y: cmp(x[0], y[0]))
+ #for (key, val) in frameitems
+ <tr>
+ <td>$key</td>
+ <td class="code"><div>$prettify(val)</div></td>
+ </tr>
+ #end for
+ </tbody>
+ </table>
+ #end if
+ </li>
+ #end for
+ </ul>
+</div>
+
+<div id="requestinfo">
+ #if $context_.output or $context_.headers
+ <h2>Response so far</h2>
+ <h3>HEADERS</h3>
+ #if $ctx.headers
+ <p class="req"><code>
+ #for (k, v) in $context_.headers
+ $k: $v<br />
+ #end for
+
+ </code></p>
+ #else
+ <p>No headers.</p>
+ #end if
+ <h3>BODY</h3>
+ <p class="req" style="padding-bottom: 2em"><code>
+ $context_.output
+ </code></p>
+ #end if
+
+ <h2>Request information</h2>
+
+ <h3>INPUT</h3>
+ #if $input_
+ <table class="req">
+ <thead>
+ <tr>
+ <th>Variable</th>
+ <th>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ #set myitems = $input_.items()
+ #silent myitems.sort(lambda x,y: cmp(x[0], y[0]))
+ #for (key, val) in myitems
+ <tr>
+ <td>$key</td>
+ <td class="code"><div>$val</div></td>
+ </tr>
+ #end for
+ </tbody>
+ </table>
+ #else
+ <p>No input data.</p>
+ #end if
+
+ <h3 id="cookie-info">COOKIES</h3>
+ #if $cookies_
+ <table class="req">
+ <thead>
+ <tr>
+ <th>Variable</th>
+ <th>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ #for (key, val) in $cookies_.items()
+ <tr>
+ <td>$key</td>
+ <td class="code"><div>$val</div></td>
+ </tr>
+ #end for
+ </tbody>
+ </table>
+ #else
+ <p>No cookie data</p>
+ #end if
+
+ <h3 id="meta-info">META</h3>
+ <table class="req">
+ <thead>
+ <tr>
+ <th>Variable</th>
+ <th>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ #set myitems = $context_.items()
+ #silent myitems.sort(lambda x,y: cmp(x[0], y[0]))
+ #for (key, val) in $myitems
+ #if not $key.startswith('_') and $key not in ['env', 'output', 'headers', 'environ', 'status', 'db_execute']
+ <tr>
+ <td>$key</td>
+ <td class="code"><div>$prettify($val)</div></td>
+ </tr>
+ #end if
+ #end for
+ </tbody>
+ </table>
+
+ <h3 id="meta-info">ENVIRONMENT</h3>
+ <table class="req">
+ <thead>
+ <tr>
+ <th>Variable</th>
+ <th>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ #set myitems = $context_.env.items()
+ #silent myitems.sort(lambda x,y: cmp(x[0], y[0]))
+ #for (key, val) in $myitems
+ <tr>
+ <td>$key</td>
+ <td class="code"><div>$prettify($val)</div></td>
+ </tr>
+ #end for
+ </tbody>
+ </table>
+
+</div>
+
+<div id="explanation">
+ <p>
+ You're seeing this error because you have <code>web.internalerror</code>
+ set to <code>web.debugerror</code>. Change that if you want a different one.
+ </p>
+</div>
+
+</body>
+</html>"""
+
+def djangoerror():
+ def _get_lines_from_file(filename, lineno, context_lines):
+ """
+ Returns context_lines before and after lineno from file.
+ Returns (pre_context_lineno, pre_context, context_line, post_context).
+ """
+ try:
+ source = open(filename).readlines()
+ lower_bound = max(0, lineno - context_lines)
+ upper_bound = lineno + context_lines
+
+ pre_context = \
+ [line.strip('\n') for line in source[lower_bound:lineno]]
+ context_line = source[lineno].strip('\n')
+ post_context = \
+ [line.strip('\n') for line in source[lineno + 1:upper_bound]]
+
+ return lower_bound, pre_context, context_line, post_context
+ except (OSError, IOError):
+ return None, [], None, []
+
+ exception_type, exception_value, tback = sys.exc_info()
+ frames = []
+ while tback is not None:
+ filename = tback.tb_frame.f_code.co_filename
+ function = tback.tb_frame.f_code.co_name
+ lineno = tback.tb_lineno - 1
+ pre_context_lineno, pre_context, context_line, post_context = \
+ _get_lines_from_file(filename, lineno, 7)
+ frames.append({
+ 'tback': tback,
+ 'filename': filename,
+ 'function': function,
+ 'lineno': lineno,
+ 'vars': tback.tb_frame.f_locals.items(),
+ 'id': id(tback),
+ 'pre_context': pre_context,
+ 'context_line': context_line,
+ 'post_context': post_context,
+ 'pre_context_lineno': pre_context_lineno,
+ })
+ tback = tback.tb_next
+ lastframe = frames[-1]
+ frames.reverse()
+ urljoin = urlparse.urljoin
+ input_ = input()
+ cookies_ = cookies()
+ context_ = ctx
+ def prettify(x):
+ try:
+ out = pprint.pformat(x)
+ except Exception, e:
+ out = '[could not display: <' + e.__class__.__name__ + \
+ ': '+str(e)+'>]'
+ return out
+ return render(DJANGO_500_PAGE, asTemplate=True, isString=True)
+
+def debugerror():
+ """
+ A replacement for `internalerror` that presents a nice page with lots
+ of debug information for the programmer.
+
+ (Based on the beautiful 500 page from [Django](http://djangoproject.com/),
+ designed by [Wilson Miner](http://wilsonminer.com/).)
+
+ Requires [Cheetah](http://cheetahtemplate.org/).
+ """
+ # need to do django first, so it can get the old stuff
+ if _hasTemplating:
+ out = str(djangoerror())
+ else:
+ # Cheetah isn't installed
+ out = """<p>You've set web.py to use the fancier debugerror error
+messages, but these messages require you install the Cheetah template
+system. For more information, see
+<a href="http://webpy.org/">the web.py website</a>.</p>
+
+<p>In the meantime, here's a plain old error message:</p>
+
+<pre>%s</pre>
+
+<p>(If it says something about 'Compiler', then it's probably
+because you're trying to use templates and you haven't
+installed Cheetah. See above.)</p>
+""" % htmlquote(traceback.format_exc())
+ ctx.status = "500 Internal Server Error"
+ ctx.headers = [('Content-Type', 'text/html')]
+ ctx.output = out
+
+
+## Rendering
+
+r_include = re_compile(r'(?!\\)#include \"(.*?)\"($|#)', re.M)
+def __compiletemplate(template, base=None, isString=False):
+ if isString:
+ text = template
+ else:
+ text = open('templates/'+template).read()
+ # implement #include at compile-time
+ def do_include(match):
+ text = open('templates/'+match.groups()[0]).read()
+ return text
+ while r_include.findall(text):
+ text = r_include.sub(do_include, text)
+
+ execspace = _compiletemplate.bases.copy()
+ tmpl_compiler = Compiler(source=text, mainClassName='GenTemplate')
+ tmpl_compiler.addImportedVarNames(execspace.keys())
+ exec str(tmpl_compiler) in execspace
+ if base:
+ _compiletemplate.bases[base] = execspace['GenTemplate']
+
+ return execspace['GenTemplate']
+
+_compiletemplate = memoize(__compiletemplate)
+_compiletemplate.bases = {}
+
+def htmlquote(text):
+ """Encodes `text` for raw use in HTML."""
+ text = text.replace("&", "&amp;") # Must be done first!
+ text = text.replace("<", "&lt;")
+ text = text.replace(">", "&gt;")
+ text = text.replace("'", "&#39;")
+ text = text.replace('"', "&quot;")
+ return text
+
+def websafe(val):
+ """
+ Converts `val` so that it's safe for use in HTML.
+
+ HTML metacharacters are encoded,
+ None becomes the empty string, and
+ unicode is converted to UTF-8.
+ """
+ if val is None: return ''
+ if not isinstance(val, unicode): val = str(val)
+ return htmlquote(val)
+
+if _hasTemplating:
+ class WebSafe(Filter):
+ def filter(self, val, **keywords):
+ return websafe(val)
+
+def render(template, terms=None, asTemplate=False, base=None,
+ isString=False):
+ """
+ Renders a template, caching where it can.
+
+ `template` is the name of a file containing the a template in
+ the `templates/` folder, unless `isString`, in which case it's the
+ template itself.
+
+ `terms` is a dictionary used to fill the template. If it's None, then
+ the caller's local variables are used instead, plus context, if it's not
+ already set, is set to `context`.
+
+ If asTemplate is False, it `output`s the template directly. Otherwise,
+ it returns the template object.
+
+ If the template is a potential base template (that is, something other templates)
+ can extend, then base should be a string with the name of the template. The
+ template will be cached and made available for future calls to `render`.
+
+ Requires [Cheetah](http://cheetahtemplate.org/).
+ """
+ # terms=['var1', 'var2'] means grab those variables
+ if isinstance(terms, list):
+ new = {}
+ old = upvars()
+ for k in terms:
+ new[k] = old[k]
+ terms = new
+ # default: grab all locals
+ elif terms is None:
+ terms = {'context': context, 'ctx':ctx}
+ terms.update(sys._getframe(1).f_locals)
+ # terms=d means use d as the searchList
+ if not isinstance(terms, tuple):
+ terms = (terms,)
+
+ if not isString and template.endswith('.html'):
+ header('Content-Type','text/html; charset=utf-8', unique=True)
+
+ compiled_tmpl = _compiletemplate(template, base=base, isString=isString)
+ compiled_tmpl = compiled_tmpl(searchList=terms, filter=WebSafe)
+ if asTemplate:
+ return compiled_tmpl
+ else:
+ return output(str(compiled_tmpl))
+
+## Input Forms
+
+def input(*requireds, **defaults):
+ """
+ Returns a `storage` object with the GET and POST arguments.
+ See `storify` for how `requireds` and `defaults` work.
+ """
+ from cStringIO import StringIO
+ def dictify(fs): return dict([(k, fs[k]) for k in fs.keys()])
+
+ _method = defaults.pop('_method', 'both')
+
+ e = ctx.env.copy()
+ out = {}
+ if _method.lower() in ['both', 'post']:
+ a = {}
+ if e['REQUEST_METHOD'] == 'POST':
+ a = cgi.FieldStorage(fp = StringIO(data()), environ=e,
+ keep_blank_values=1)
+ a = dictify(a)
+ out = dictadd(out, a)
+
+ if _method.lower() in ['both', 'get']:
+ e['REQUEST_METHOD'] = 'GET'
+ a = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1))
+ out = dictadd(out, a)
+
+ try:
+ return storify(out, *requireds, **defaults)
+ except KeyError:
+ badrequest()
+ raise StopIteration
+
+def data():
+ """Returns the data sent with the request."""
+ if 'data' not in ctx:
+ cl = intget(ctx.env.get('CONTENT_LENGTH'), 0)
+ ctx.data = ctx.env['wsgi.input'].read(cl)
+ return ctx.data
+
+def changequery(**kw):
+ """
+ Imagine you're at `/foo?a=1&b=2`. Then `changequery(a=3)` will return
+ `/foo?a=3&b=2` -- the same URL but with the arguments you requested
+ changed.
+ """
+ query = input(_method='get')
+ for k, v in kw.iteritems():
+ if v is None:
+ query.pop(k, None)
+ else:
+ query[k] = v
+ out = ctx.path
+ if query:
+ out += '?' + urllib.urlencode(query)
+ return out
+
+## Cookies
+
+def setcookie(name, value, expires="", domain=None):
+ """Sets a cookie."""
+ if expires < 0:
+ expires = -1000000000
+ kargs = {'expires': expires, 'path':'/'}
+ if domain:
+ kargs['domain'] = domain
+ # @@ should we limit cookies to a different path?
+ cookie = Cookie.SimpleCookie()
+ cookie[name] = value
+ for key, val in kargs.iteritems():
+ cookie[name][key] = val
+ header('Set-Cookie', cookie.items()[0][1].OutputString())
+
+def cookies(*requireds, **defaults):
+ """
+ Returns a `storage` object with all the cookies in it.
+ See `storify` for how `requireds` and `defaults` work.
+ """
+ cookie = Cookie.SimpleCookie()
+ cookie.load(ctx.env.get('HTTP_COOKIE', ''))
+ try:
+ return storify(cookie, *requireds, **defaults)
+ except KeyError:
+ badrequest()
+ raise StopIteration
+
+## WSGI Sugar
+
+def header(hdr, value, unique=False):
+ """
+ Adds the header `hdr: value` with the response.
+
+ If `unique` is True and a header with that name already exists,
+ it doesn't add a new one. If `unique` is None and a header with
+ that name already exists, it replaces it with this one.
+ """
+ if unique is True:
+ for h, v in ctx.headers:
+ if h == hdr: return
+ elif unique is None:
+ ctx.headers = [h for h in ctx.headers if h[0] != hdr]
+
+ ctx.headers.append((hdr, value))
+
+def output(string_):
+ """Appends `string_` to the response."""
+ if isinstance(string_, unicode): string_ = string_.encode('utf8')
+ if ctx.get('flush'):
+ ctx._write(string_)
+ else:
+ ctx.output += str(string_)
+
+def flush():
+ ctx.flush = True
+ return flush
+
+def write(cgi_response):
+ """
+ Converts a standard CGI-style string response into `header` and
+ `output` calls.
+ """
+ cgi_response = str(cgi_response)
+ cgi_response.replace('\r\n', '\n')
+ head, body = cgi_response.split('\n\n', 1)
+ lines = head.split('\n')
+
+ for line in lines:
+ if line.isspace():
+ continue
+ hdr, value = line.split(":", 1)
+ value = value.strip()
+ if hdr.lower() == "status":
+ ctx.status = value
+ else:
+ header(hdr, value)
+
+ output(body)
+
+def webpyfunc(inp, fvars=None, autoreload=False):
+ """If `inp` is a url mapping, returns a function that calls handle."""
+ if not fvars:
+ fvars = upvars()
+ if not hasattr(inp, '__call__'):
+ if autoreload:
+ # black magic to make autoreload work:
+ mod = \
+ __import__(
+ fvars['__file__'].split(os.path.sep).pop().split('.')[0])
+ #@@probably should replace this with some inspect magic
+ name = dictfind(fvars, inp)
+ func = lambda: handle(getattr(mod, name), mod)
+ else:
+ func = lambda: handle(inp, fvars)
+ else:
+ func = inp
+ return func
+
+def wsgifunc(func, *middleware):
+ """Returns a WSGI-compatible function from a webpy-function."""
+ middleware = list(middleware)
+ if reloader in middleware:
+ relr = reloader(None)
+ relrcheck = relr.check
+ middleware.remove(reloader)
+ else:
+ relr = None
+ relrcheck = lambda: None
+
+ def wsgifunc(env, start_resp):
+ _load(env)
+ relrcheck()
+ try:
+ result = func()
+ except StopIteration:
+ result = None
+
+ is_generator = result and hasattr(result, 'next')
+ if is_generator:
+ # wsgi requires the headers first
+ # so we need to do an iteration
+ # and save the result for later
+ try:
+ firstchunk = result.next()
+ except StopIteration:
+ firstchunk = ''
+
+ status, headers, output = ctx.status, ctx.headers, ctx.output
+ ctx._write = start_resp(status, headers)
+
+ # and now, the fun:
+
+ def cleanup():
+ # we insert this little generator
+ # at the end of our itertools.chain
+ # so that it unloads the request
+ # when everything else is done
+
+ yield '' # force it to be a generator
+ _unload()
+
+ # result is the output of calling the webpy function
+ # it could be a generator...
+
+ if is_generator:
+ if firstchunk is flush:
+ # oh, it's just our special flush mode
+ # ctx._write is set up, so just continue execution
+ try:
+ result.next()
+ except StopIteration:
+ pass
+
+ _unload()
+ return []
+ else:
+ return itertools.chain([firstchunk], result, cleanup())
+
+ # ... but it's usually just None
+ #
+ # output is the stuff in ctx.output
+ # it's usually a string...
+ if isinstance(output, str): #@@ other stringlikes?
+ _unload()
+ return [output]
+ # it could be a generator...
+ elif hasattr(output, 'next'):
+ return itertools.chain(output, cleanup())
+ else:
+ _unload()
+ raise Exception, "Invalid web.ctx.output"
+
+ for mw_func in middleware:
+ wsgifunc = mw_func(wsgifunc)
+
+ if relr:
+ relr.func = wsgifunc
+ return wsgifunc
+ return wsgifunc
+
+def run(inp, *middleware):
+ """
+ Starts handling requests. If called in a CGI or FastCGI context, it will follow
+ that protocol. If called from the command line, it will start an HTTP
+ server on the port named in the first command line argument, or, if there
+ is no argument, on port 8080.
+
+ `input` is a callable, then it's called with no arguments.
+ Otherwise, it's a `mapping` object to be passed to `handle(...)`.
+
+ **Caveat:** So that `reloader` will work correctly, input has to be a variable,
+ it can't be a tuple passed in directly.
+
+ `middleware` is a list of WSGI middleware which is applied to the resulting WSGI
+ function.
+ """
+ autoreload = reloader in middleware
+ fvars = upvars()
+ return runwsgi(wsgifunc(webpyfunc(inp, fvars, autoreload), *middleware))
+
+def runwsgi(func):
+ """
+ Runs a WSGI-compatible function using FCGI, SCGI, or a simple web server,
+ as appropriate.
+ """
+ #@@ improve detection
+ if os.environ.has_key('SERVER_SOFTWARE'): # cgi
+ os.environ['FCGI_FORCE_CGI'] = 'Y'
+
+ if (os.environ.has_key('PHP_FCGI_CHILDREN') #lighttpd fastcgi
+ or os.environ.has_key('SERVER_SOFTWARE')
+ or 'fcgi' in sys.argv or 'fastcgi' in sys.argv):
+ return runfcgi(func)
+
+ if 'scgi' in sys.argv:
+ return runscgi(func)
+
+ # command line:
+ return runsimple(func, validip(listget(sys.argv, 1, '')))
+
+def runsimple(func, server_address=("0.0.0.0", 8080)):
+ """
+ Runs a simple HTTP server hosting WSGI app `func`. The directory `static/`
+ is hosted statically.
+
+ Based on [WsgiServer][ws] from [Colin Stewart][cs].
+
+ [ws]: http://www.owlfish.com/software/wsgiutils/documentation/wsgi-server-api.html
+ [cs]: http://www.owlfish.com/
+ """
+ # Copyright (c) 2004 Colin Stewart (http://www.owlfish.com/)
+ # Modified somewhat for simplicity
+ # Used under the modified BSD license:
+ # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
+
+ import SimpleHTTPServer, SocketServer, BaseHTTPServer, urlparse
+ import socket, errno
+ import traceback
+
+ class WSGIHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
+ def run_wsgi_app(self):
+ protocol, host, path, parameters, query, fragment = \
+ urlparse.urlparse('http://dummyhost%s' % self.path)
+ # we only use path, query
+ env = {'wsgi.version': (1, 0)
+ ,'wsgi.url_scheme': 'http'
+ ,'wsgi.input': self.rfile
+ ,'wsgi.errors': sys.stderr
+ ,'wsgi.multithread': 1
+ ,'wsgi.multiprocess': 0
+ ,'wsgi.run_once': 0
+ ,'REQUEST_METHOD': self.command
+ ,'REQUEST_URI': self.path
+ ,'PATH_INFO': path
+ ,'QUERY_STRING': query
+ ,'CONTENT_TYPE': self.headers.get('Content-Type', '')
+ ,'CONTENT_LENGTH': self.headers.get('Content-Length', '')
+ ,'REMOTE_ADDR': self.client_address[0]
+ ,'SERVER_NAME': self.server.server_address[0]
+ ,'SERVER_PORT': str(self.server.server_address[1])
+ ,'SERVER_PROTOCOL': self.request_version
+ }
+
+ for http_header, http_value in self.headers.items():
+ env ['HTTP_%s' % http_header.replace('-', '_').upper()] = \
+ http_value
+
+ # Setup the state
+ self.wsgi_sent_headers = 0
+ self.wsgi_headers = []
+
+ try:
+ # We have there environment, now invoke the application
+ result = self.server.app(env, self.wsgi_start_response)
+ try:
+ try:
+ for data in result:
+ if data:
+ self.wsgi_write_data(data)
+ finally:
+ if hasattr(result, 'close'):
+ result.close()
+ except socket.error, socket_err:
+ # Catch common network errors and suppress them
+ if (socket_err.args[0] in \
+ (errno.ECONNABORTED, errno.EPIPE)):
+ return
+ except socket.timeout, socket_timeout:
+ return
+ except:
+ print >> debug, traceback.format_exc(),
+ internalerror()
+ if not self.wsgi_sent_headers:
+ self.wsgi_start_response(ctx.status, ctx.headers)
+ self.wsgi_write_data(ctx.output)
+
+ if (not self.wsgi_sent_headers):
+ # We must write out something!
+ self.wsgi_write_data(" ")
+ return
+
+ do_POST = run_wsgi_app
+ do_PUT = run_wsgi_app
+ do_DELETE = run_wsgi_app
+
+ def do_GET(self):
+ if self.path.startswith('/static/'):
+ SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
+ else:
+ self.run_wsgi_app()
+
+ def wsgi_start_response(self, response_status, response_headers,
+ exc_info=None):
+ if (self.wsgi_sent_headers):
+ raise Exception \
+ ("Headers already sent and start_response called again!")
+ # Should really take a copy to avoid changes in the application....
+ self.wsgi_headers = (response_status, response_headers)
+ return self.wsgi_write_data
+
+ def wsgi_write_data(self, data):
+ if (not self.wsgi_sent_headers):
+ status, headers = self.wsgi_headers
+ # Need to send header prior to data
+ status_code = status [:status.find(' ')]
+ status_msg = status [status.find(' ') + 1:]
+ self.send_response(int(status_code), status_msg)
+ for header, value in headers:
+ self.send_header(header, value)
+ self.end_headers()
+ self.wsgi_sent_headers = 1
+ # Send the data
+ self.wfile.write(data)
+
+ class WSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
+ def __init__(self, func, server_address):
+ BaseHTTPServer.HTTPServer.__init__(self,
+ server_address,
+ WSGIHandler)
+ self.app = func
+ self.serverShuttingDown = 0
+
+ print "Launching server: http://%s:%d/" % server_address
+ WSGIServer(func, server_address).serve_forever()
+
+def makeserver(wsgi_server):
+ """Updates a flup-style WSGIServer with web.py-style error support."""
+ class MyServer(wsgi_server):
+ def error(self, req):
+ w = req.stdout.write
+ internalerror()
+ w('Status: ' + ctx.status + '\r\n')
+ for (h, v) in ctx.headers:
+ w(h + ': ' + v + '\r\n')
+ w('\r\n' + ctx.output)
+
+ return MyServer
+
+def runfcgi(func):
+ """Runs a WSGI-function with a FastCGI server."""
+ from flup.server.fcgi import WSGIServer
+ if len(sys.argv) > 2: # progname, scgi
+ args = sys.argv[:]
+ if 'fastcgi' in args: args.remove('fastcgi')
+ elif 'fcgi' in args: args.remove('fcgi')
+ hostport = validaddr(args[1])
+ elif len(sys.argv) > 1:
+ hostport = ('localhost', 8000)
+ else:
+ hostport = None
+ return makeserver(WSGIServer)(func, multiplexed=True, bindAddress=hostport).run()
+
+def runscgi(func):
+ """Runs a WSGI-function with an SCGI server."""
+ from flup.server.scgi import WSGIServer
+ my_server = makeserver(WSGIServer)
+ if len(sys.argv) > 2: # progname, scgi
+ args = sys.argv[:]
+ args.remove('scgi')
+ hostport = validaddr(args[1])
+ else:
+ hostport = ('localhost', 4000)
+ return my_server(func, bindAddress=hostport).run()
+
+## Debugging
+
+def debug(*args):
+ """
+ Prints a prettyprinted version of `args` to stderr.
+ """
+ try:
+ out = ctx.environ['wsgi.errors']
+ except:
+ out = sys.stderr
+ for arg in args:
+ print >> out, pprint.pformat(arg)
+ return ''
+
+def debugwrite(x):
+ """writes debug data to error stream"""
+ try:
+ out = ctx.environ['wsgi.errors']
+ except:
+ out = sys.stderr
+ out.write(x)
+debug.write = debugwrite
+
+class Reloader:
+ """
+ Before every request, checks to see if any loaded modules have changed on
+ disk and, if so, reloads them.
+ """
+ def __init__(self, func):
+ self.func = func
+ self.mtimes = {}
+ global _compiletemplate
+ b = _compiletemplate.bases
+ _compiletemplate = globals()['__compiletemplate']
+ _compiletemplate.bases = b
+
+ def check(self):
+ for mod in sys.modules.values():
+ try:
+ mtime = os.stat(mod.__file__).st_mtime
+ except (AttributeError, OSError, IOError):
+ continue
+ if mod.__file__.endswith('.pyc') and \
+ os.path.exists(mod.__file__[:-1]):
+ mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime)
+ if mod not in self.mtimes:
+ self.mtimes[mod] = mtime
+ elif self.mtimes[mod] < mtime:
+ try:
+ reload(mod)
+ except ImportError:
+ pass
+ return True
+
+ def __call__(self, e, o):
+ self.check()
+ return self.func(e, o)
+reloader = Reloader
+
+def profiler(app):
+ """Outputs basic profiling information at the bottom of each response."""
+ def profile_internal(e, o):
+ out, result = profile(app)(e, o)
+ return out + ['<pre>' + result + '</pre>'] #@@encode
+ return profile_internal
+
+## Context
+
+class _outputter:
+ """Wraps `sys.stdout` so that print statements go into the response."""
+ def write(self, string_):
+ if hasattr(ctx, 'output'):
+ return output(string_)
+ else:
+ _oldstdout.write(string_)
+ def flush(self):
+ return _oldstdout.flush()
+ def close(self):
+ return _oldstdout.close()
+
+_context = {currentThread():Storage()}
+ctx = context = threadeddict(_context)
+
+ctx.__doc__ = """
+A `storage` object containing various information about the request:
+
+`environ` (aka `env`)
+ : A dictionary containing the standard WSGI environment variables.
+
+`host`
+ : The domain (`Host` header) requested by the user.
+
+`home`
+ : The base path for the application.
+
+`ip`
+ : The IP address of the requester.
+
+`method`
+ : The HTTP method used.
+
+`path`
+ : The path request.
+
+`fullpath`
+ : The full path requested, including query arguments.
+
+### Response Data
+
+`status` (default: "200 OK")
+ : The status code to be used in the response.
+
+`headers`
+ : A list of 2-tuples to be used in the response.
+
+`output`
+ : A string to be used as the response.
+"""
+
+if not '_oldstdout' in globals():
+ _oldstdout = sys.stdout
+ sys.stdout = _outputter()
+
+loadhooks = {}
+
+def load():
+ """
+ Loads a new context for the thread.
+
+ You can ask for a function to be run at loadtime by
+ adding it to the dictionary `loadhooks`.
+ """
+ _context[currentThread()] = Storage()
+ ctx.status = '200 OK'
+ ctx.headers = []
+ if 'db_parameters' in globals():
+ connect(**db_parameters)
+
+ for x in loadhooks.values(): x()
+
+def _load(env):
+ load()
+ ctx.output = ''
+ ctx.environ = ctx.env = env
+ ctx.host = env.get('HTTP_HOST')
+ ctx.home = 'http://' + env.get('HTTP_HOST', '[unknown]') + \
+ os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', ''))
+ ctx.ip = env.get('REMOTE_ADDR')
+ ctx.method = env.get('REQUEST_METHOD')
+ ctx.path = env.get('PATH_INFO')
+ # http://trac.lighttpd.net/trac/ticket/406 requires:
+ if env.get('SERVER_SOFTWARE', '').startswith('lighttpd/'):
+ ctx.path = lstrips(env.get('REQUEST_URI').split('?')[0],
+ os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', '')))
+
+ ctx.fullpath = ctx.path
+ if env.get('QUERY_STRING'):
+ ctx.fullpath += '?' + env.get('QUERY_STRING', '')
+
+unloadhooks = {}
+
+def unload():
+ """
+ Unloads the context for the thread.
+
+ You can ask for a function to be run at loadtime by
+ adding it ot the dictionary `unloadhooks`.
+ """
+ for x in unloadhooks.values(): x()
+ # ensures db cursors and such are GCed promptly
+ del _context[currentThread()]
+
+def _unload():
+ unload()
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
+
+ urls = ('/web.py', 'source')
+ class source:
+ def GET(self):
+ header('Content-Type', 'text/python')
+ print open(sys.argv[0]).read()
+ run(urls)