# # # add_dir "templates" # # add_file "templates/about.html" # content [6b1c55564ae50e2de59d5eda418bfad86f0b596d] # # add_file "templates/base.html" # content [941ad9f0b2cc72c729f78185d140cce2f5088e29] # # add_file "templates/index.html" # content [e0ae8e2a7cc89a8ff8d012742104ded01f6cf968] # # add_file "viewmtn.py" # content [b33f71afb2e598fadb81114ed5a569af368d390b] # # add_file "web.py" # content [4c396c690078ecd159e98a016dd3c9de67d63f42] # # set "viewmtn.py" # attr "mtn:execute" # value "true" # # set "web.py" # attr "mtn:execute" # value "true" # ============================================================ --- templates/about.html 6b1c55564ae50e2de59d5eda418bfad86f0b596d +++ templates/about.html 6b1c55564ae50e2de59d5eda418bfad86f0b596d @@ -0,0 +1,67 @@ +#extends base + +#def body + +

Authors and Contributors

+ + + +

Licensing

+ +

+Copyright (C) 2005 Grahame Bowland +

+ +

+This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +

+ +

+This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +

+ +

+You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +

+ +

Dependencies

+ +

+ViewMTN is written in Python and +runs under mod_python. +

+ +

+Code highlighting via +GNU Enscript. +

+ +

+Graphing via GraphViz. +

+ +

+Graph colour generation algorithm from monotone-viz with modifications from Matt Johnston. +

+ +

+AJAX funtionality uses the MochiKit Javascript library. +

+ + +#end def ============================================================ --- templates/base.html 941ad9f0b2cc72c729f78185d140cce2f5088e29 +++ templates/base.html 941ad9f0b2cc72c729f78185d140cce2f5088e29 @@ -0,0 +1,35 @@ + + + +ViewMTN: $(page_title) + + + +#block extraheaders +#end block + + + + + + +#block body +#end block + + + + + + + ============================================================ --- templates/index.html e0ae8e2a7cc89a8ff8d012742104ded01f6cf968 +++ templates/index.html e0ae8e2a7cc89a8ff8d012742104ded01f6cf968 @@ -0,0 +1,14 @@ +#extends base + +#def body +

+Welcome to this ViewMTN installation. +The list below shows all branches served within this Monotone +database. +

+ +

+Select one of the branches and you will be shown a list of recent changes that have occurred within it. +If you are looking for a particular revision (for example, a release) the list of tags might be useful. +

+#end def ============================================================ --- viewmtn.py b33f71afb2e598fadb81114ed5a569af368d390b +++ viewmtn.py b33f71afb2e598fadb81114ed5a569af368d390b @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +import web +import config + +import urlparse + +# /about.psp -> /about + +# /branch.psp -> /branch/{branch}/ +# /fileinbranch.psp -> /branch/{branch}/file/path (redir) +# /headofbranch.psp -> /branch/{branch}/head +# /tarofbranch.psp -> /branch/{branch}/tar + +# /revision.psp -> /revision/{id} +# /diff.psp -> /revision/{id}/diff/{id}[/{fname}] +# /file.psp -> /revision/{id}/file/{path} +# /manifest.psp -> /revision/{id}/browse/{subdir} +# /getfile.py -> /revision/{id}/file/{path}&download #??? +# /getdiff.py -> /revision/{id}/diff/{id}[/{fname}]&download #??? +# /gettar.py -> /revision/{id}/tar + +# /error.psp -> /error (perhaps not needed) +# /help.psp -> /help +# /index.psp -> / +# /tags.psp -> /tags + +# /getjson.py -> /json[...] (private) + +class Renderer: + def __init__(self): + # any templates that can be inherited from, should be added to the list here + templates = [ ('base.html', 'base'), ] + for template, mod_name in templates: + web.render(template, None, True, mod_name) + self.terms = { + 'dynamic_uri_path' : config.dynamic_uri_path, + 'dynamic_join' : lambda path: urlparse.urljoin(config.dynamic_uri_path, path), + 'static_uri_path' : config.static_uri_path, + 'static_join' : lambda path: urlparse.urljoin(config.static_uri_path, path), + } + def render(self, template, **kwargs): + terms = self.terms.copy() + terms.update(kwargs) + web.render(template, terms) + +renderer = Renderer() + +class Index: + def GET(self): + renderer.render('index.html', page_title="Branches") + +class About: + def GET(self): + renderer.render('about.html', page_title="About") + page_title = "About" + +id_re = r'[A-Za-z0-9]{40}' +branch_re = r'' +urls = ( + '/', 'Index', + '/about', 'About' +) + +if __name__ == '__main__': + web.internalerror = web.debugerror + web.run(urls, web.reloader) ============================================================ --- web.py 4c396c690078ecd159e98a016dd3c9de67d63f42 +++ web.py 4c396c690078ecd159e98a016dd3c9de67d63f42 @@ -0,0 +1,1270 @@ +#!/usr/bin/env python +"""web.py: makes web apps (http://webpy.org)""" +__version__ = "0.13" +__license__ = "Affero General Public License, Version 1" +__author__ = "Aaron Swartz " + +from __future__ import generators + +# long term todo: +# - new form system +# - new templating system +# - unit tests? + +# todo: +# - add db select, delete function +# - fix that silly nonce stuff in web.update +# - provide an option to use .write() +# - add ip:port support +# - allow people to do $self.id from inside a reparam +# - add sqlite support +# - make storage a subclass of dictionary +# - 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 +import cgi, re, urllib, urlparse, Cookie, pprint +from threading import currentThread +try: import datetime +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): + s = StringIO() + traceback.print_exc(limit, s) + return s.getvalue() + traceback.format_exc = format_exc + +## general utils + +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 "WrongDirection", "Needs to be r or l." + return text + +def rstrips(a, b): return _strips('r', a, b) +def lstrips(a, b): return _strips('l', a, b) +def strips(a, b): return rstrips(lstrips(a,b),b) + +def autoassign(): + locals = sys._getframe(1).f_locals + self = locals['self'] + for (k, v) in locals.iteritems(): + if k == 'self': continue + setattr(self, k, v) + +class Storage: + def __init__(self, initial=None): + if initial: + for k in initial.keys(): setattr(self, k, initial[k]) + + def __getattr__(self, k): + if hasattr(self.__dict__, k) or ( + k.startswith('__') and k.endswith('__')): # special keyword + return getattr(self.__dict__, k) + raise AttributeError, repr(k) + + def __repr__(self): return '' + +storage = Storage + +def storify(f, *requireds, **defaults): + stor = Storage() + + for k in requireds + tuple(f.keys()): + v = f[k] + if isinstance(k, list): v = v[-1] + if hasattr(v, 'value'): v = v.value + setattr(stor, k, v) + + for (k,v) in defaults.iteritems(): + result = v + if hasattr(stor, k): result = stor[k] + if v == () and not isinstance(result, tuple): result = (result,) + setattr(stor, k, result) + + return stor + +class memoize: + def __init__(self, func): self.func = func; self.cache = {} + def __call__(self, *a, **k): + key = (a, tuple(k.items())) + if key not in self.cache: self.cache[key] = self.func(*a, **k) + return self.cache[key] + +re_compile = memoize(re.compile) #@@ threadsafe? + +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""" + r = re_compile(pat) + proxy = _re_subm_proxy() + r.sub(proxy.__call__, string) + return r.sub(repl, string), proxy.match + +def group(seq, size): + """Breaks 'seq' into a generator of lists with length 'size'.""" + if not hasattr(seq, 'next'): seq = iter(seq) + while True: yield [seq.next() for i in xrange(size)] + +class iterbetter: + 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 KeyError, "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 KeyError, repr(i) + +def dictfind(d, elt): + for (k,v) in d.iteritems(): + if elt is v: return k + +def dictincr(d, e): + d.setdefault(e, 0) + d[e] += 1 + return d[e] + +def dictadd(a, b): + result = {} + result.update(a) + result.update(b) + return result + +sumdicts = dictadd # deprecated + +def listget(l, n, v=None): + if len(l)-1 < n: return v + return l[n] + +def upvars(n=2): + return dictadd( + sys._getframe(n).f_globals, + sys._getframe(n).f_locals) + +class capturestdout: + def __init__(self, func): self.func = func + def __call__(self, *args, **kw): + from cStringIO import StringIO + # Not threadsafe! + out = StringIO() + oldstdout = sys.stdout + sys.stdout = out + try: self.func(*args, **kw) + finally: sys.stdout = oldstdout + return out.getvalue() + +class profile: + def __init__(self, func): self.func = func + def __call__(self, *args, **kw): + import hotshot, hotshot.stats, tempfile, time + 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 + +def tryall(context): + context = context.copy() # vars() would update + results = {} + for (k, v) in context.iteritems(): + if not hasattr(v, '__call__'): continue + print k+':', + try: + r = v() + dictincr(results, r) + print r + except: + print 'ERROR' + dictincr(results, 'ERROR') + print ' '+'\n '.join(traceback.format_exc().split('\n')) + + print '-'*40 + print 'results:' + for (k, v) in results.iteritems(): + print ' '*2, str(k)+':', v + +class threadeddict: + def __init__(self, d): self.__dict__['_threadeddict__d'] = d + def __getattr__(self, a): return getattr(self.__d[currentThread()], a) + def __getitem__(self, i): return self.__d[currentThread()][i] + def __setattr__(self, a, v): return setattr(self.__d[currentThread()], a, v) + def __setitem__(self, i, v): self.__d[currentThread()][i] = v + def __hash__(self): return hash(self.__d[currentThread()]) + +## url utils + +def base(base=''): #when would you use a default base? + url = context.path.lstrip('/') + for i in xrange(url.count('/')): base += '../' + if not base: base = './' + return base + +## formatting + +try: + from markdown import markdown # http://webpy.org/markdown.py +except ImportError: pass + +r_url = re_compile('(?', text) + text = markdown(text) + return text + +## db api + +class UnknownParamstyle(Exception): pass +r_dbvar = re_compile(r'\B\$(\w+)') +def reparam(q): + p = ctx.db_module.paramstyle + if p == "pyformat": + return r_dbvar.sub(r'%(\1)s', q) + elif p == "named": + return r_dbvar.sub(r':\1', q) + raise UnknownParamstyle, p + +def aparam(): + p = ctx.db_module.paramstyle + if p == 'qmark': + return '?' + elif p == 'numeric': + return ':1' + elif p == 'format': + return '%s' + elif p == 'pyformat': + return '%s' + raise UnknownParamstyle, p + +class UnknownDB(Exception): pass +def connect(dbn, **kw): + if dbn == "postgres": + try: import psycopg2 as db + except ImportError: + try: import psycopg as db + except ImportError: import pgdb as db + kw['password'] = kw['pw'] + del kw['pw'] + kw['database'] = kw['db'] + del kw['db'] + elif dbn == "mysql": + import MySQLdb as db + kw['passwd'] = kw['pw'] + del kw['pw'] + db.paramstyle = 'pyformat' # it's both, like psycopg + 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, **kw) + ctx.db = globals()['db'].connection() + else: + ctx.db = db.connect(**kw) + ctx.dbq_count = 0 + if globals().get('db_printing'): + def db_execute(cur, q, d=None): + ctx.dbq_count += 1 + try: outq = q % d + except: outq = q + print>>debug, str(ctx.dbq_count)+':', outq + a = time.time() + out = cur.execute(q, d) + b = time.time() + print>>debug, '(%s)' % round(b-a, 2) + return out + ctx.db_execute = db_execute + else: + ctx.db_execute = lambda cur, q, d=None: cur.execute(q, 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(): + ctx.db.commit() + ctx.db_transaction = False + +def rollback(): + ctx.db.rollback() + ctx.db_transaction = False + +def query(q, v=None): + d = ctx.db.cursor() + if v is None: v = upvars() + + ctx.db_execute(d, reparam(q), v) + if d.description: + names = [x[0] for x in d.description] + def iterwrapper(): + x = d.fetchone() + while x: + yield Storage(dict(zip(names, x))) + x = d.fetchone() + out = iterbetter(iterwrapper()) + out.__len__ = lambda: d.rowcount + else: + out = None + out = d.rowcount + + if not ctx.db_transaction: ctx.db.commit() + return out + +def select(tablename, what='*', where=None, vars=None, **wheres): + if vars is None: vars = upvars() + + q = "SELECT "+what+" FROM "+tablename+" WHERE " + if where: q += where + for (k, v) in wheres.iteritems(): + where += k + '=' + nonce(v, vars) + return query(q, vvars) + +def insert(tablename, seqname=None, **values): + d = ctx.db.cursor() + + if values: + ctx.db_execute(d, "INSERT INTO %s (%s) VALUES (%s)" % ( + tablename, + ", ".join(values.keys()), + ', '.join([aparam() for x in values]) #@@ use nonce + ), values.values()) + else: + ctx.db_execute(d, "INSERT INTO %s DEFAULT VALUES" % tablename) + + if ctx.db_name == "postgres" and seqname != False: + if seqname is None: seqname = tablename + "_id_seq" + ctx.db_execute(d, "SELECT currval('%s')" % seqname) + out = d.fetchone()[0] + elif ctx.db_name == "mysql": + ctx.db_execute(d, "SELECT last_insert_id()") + out = d.fetchone()[0] + elif ctx.db_name == "sqlite": + # not really the same... + ctx.db_execute(d, "SELECT last_insert_rowid()") + out = d.fetchone()[0] + else: + out = None + + if not ctx.db_transaction: ctx.db.commit() + return out + +def update(tablename, where, pvars=(), **values): + pvars = list(pvars) + if isinstance(where, int): + pvars.append(where) + where = "id = "+aparam() + else: + where = where #@@ need to figure out positional params + + d = ctx.db.cursor() + ctx.db_execute(d, "UPDATE %s SET %s WHERE %s" % ( + tablename, + ', '.join([k+'='+aparam() for k in values.keys()]), + where), + values.values()+pvars) + + if not ctx.db_transaction: ctx.db.commit() + return d.rowcount + +## request handlers + +def handle(mapping, fvars=None): + 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, context.path) + if result: # it's a match + if fn.split(' ', 1)[0] == "redirect": + url = fn.split(' ', 1)[1] + if context.method == "GET": + x = context.environ.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 = context.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=''): + def internal(self, arg): + func = prefix+arg + if hasattr(self, func): return getattr(self, func)() + else: return notfound() + return internal + +## http defaults + +def redirect(url, status='301 Moved Permanently'): + newloc = urlparse.urljoin(context.home + context.path, url) + context.status = status + header('Content-Type', 'text/html') + header('Location', newloc) + # seems to add a three-second delay for some reason: + # output('moved permanently') + +def found(url): return redirect(url, '302 Found') +def seeother(url): return redirect(url, '303 See Other') +def tempredirect(url): return redirect(url, '307 Temporary Redirect') + +def badrequest(): + context.status = '400 Bad Request' + header('Content-Type', 'text/html') + return output('bad request') + +def notfound(): + context.status = '404 Not Found' + header('Content-Type', 'text/html') + return output('not found') + +def nomethod(cls): + context.status = '405 Method Not Allowed' + header('Content-Type', 'text/html') + header("Allow", ', '.join([x for x in ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'] if hasattr(cls, x)])) + return output('method not allowed') + +def gone(): + context.status = '410 Gone' + header('Content-Type', 'text/html') + return output("gone") + +def expires(delta): + try: datetime + except NameError: raise Exception, "this function requires at least python2.3" + if isinstance(delta, (int, long)): + delta = datetime.timedelta(seconds=delta) + o = datetime.datetime.utcnow() + delta + header('Expires', o.strftime("%a, %d %b %Y %T GMT")) + +def lastmodified(d): + header('Last-Modified', d.strftime("%a, %d %b %Y %T GMT")) + +# adapted from Django +# 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 + + + + + + $exception_type at $context.path + + + + + +
+

$exception_type at $context.path

+

$exception_value

+ + + + + + +
Python$lastframe.filename in $lastframe.function, line $lastframe.lineno
Web$context.method $context.home$context.path
+
+
+

Traceback (innermost first)

+
    + #for frame in $frames +
  • + $frame.filename in $frame.function + + #if $frame.context_line +
    + #if $frame.pre_context +
      #for line in $frame.pre_context#
    1. $line
    2. #end for#
    + #end if +
    1. $frame.context_line ...
    + #if $frame.post_context +
      #for line in $frame.post_context#
    1. $line
    2. #end for#
    + #end if +
    + #end if + + #if $frame.vars +
    + Local vars## $inspect.formatargvalues(*inspect.getargvalues(frame['tb'].tb_frame)) +
    + + + + + + + + + #set frameitems = $frame.vars + #silent frameitems.sort(lambda x,y: cmp(x[0], y[0])) + #for (key, val) in frameitems + + + + + #end for + +
    VariableValue
    $key
    $prettify(val)
    + #end if +
  • + #end for +
+
+ +
+ #if $context_.output or $context_.headers +

Response so far

+

HEADERS

+ #if $context.headers +

+ #for (k, v) in $context_.headers + $k: $v
+ #end for + +

+ #else +

No headers.

+ #end if +

BODY

+

+ $context_.output +

+ #end if + +

Request information

+ +

INPUT

+ #if $input_ + + + + + + + + + #set myitems = $input_.items() + #silent myitems.sort(lambda x,y: cmp(x[0], y[0])) + #for (key, val) in myitems + + + + + #end for + +
VariableValue
$key
$val
+ #else +

No input data.

+ #end if + + + #if $cookies_ + + + + + + + + + #for (key, val) in $cookies_.items() + + + + + #end for + +
VariableValue
$key
$val
+ #else +

No cookie data

+ #end if + +

META

+ + + + + + + + + #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'] + + + + + #end if + #end for + +
VariableValue
$key
$prettify($val)
+ +

ENVIRONMENT

+ + + + + + + + + #set myitems = $context_.environ.items() + #silent myitems.sort(lambda x,y: cmp(x[0], y[0])) + #for (key, val) in $myitems + + + + + #end for + +
VariableValue
$key
$prettify($val)
+ +
+ +
+

+ You're seeing this error because you have web.internalerror + set to web.debugerror. Change that if you want a different one. +

+
+ + +""" + +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, tb = sys.exc_info() + frames = [] + while tb is not None: + filename = tb.tb_frame.f_code.co_filename + function = tb.tb_frame.f_code.co_name + lineno = tb.tb_lineno - 1 + pre_context_lineno, pre_context, context_line, post_context = _get_lines_from_file(filename, lineno, 7) + frames.append({ + 'tb': tb, + 'filename': filename, + 'function': function, + 'lineno': lineno, + 'vars': tb.tb_frame.f_locals.items(), + 'id': id(tb), + 'pre_context': pre_context, + 'context_line': context_line, + 'post_context': post_context, + 'pre_context_lineno': pre_context_lineno, + }) + tb = tb.tb_next + lastframe = frames[-1] + frames.reverse() + urljoin = urlparse.urljoin + input_ = input() + cookies_ = cookies() + context_ = context + 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 internalerror(): + context.status = "500 Internal Server Error" + context.headers = [('Content-Type', 'text/html')] + context.output = "internal server error" + +def debugerror(): + # need to do django first, so it can get the old stuff + if _hasTemplating: + out = str(djangoerror()) + else: + # Cheetah isn't installed + out = """

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 +the web.py website.

+ +

In the meantime, here's a plain old error message:

+ +
%s
+ +

(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.)

+""" % htmlquote(traceback.format_exc()) + context.status = "500 Internal Server Error" + context.headers = [('Content-Type', 'text/html')] + context.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() + c = Compiler(source=text, mainClassName='GenTemplate') + c.addImportedVarNames(execspace.keys()) + exec str(c) in execspace + if base: _compiletemplate.bases[base] = execspace['GenTemplate'] + + return execspace['GenTemplate'] + +_compiletemplate = memoize(__compiletemplate) +_compiletemplate.bases = {} + +def htmlquote(s): + s = s.replace("&", "&") # Must be done first! + s = s.replace("<", "<") + s = s.replace(">", ">") + s = s.replace("'", "'") + s = s.replace('"', """) + return s + +urlquote = urllib.quote + +if _hasTemplating: + class WebSafe(Filter): + def filter(selv, val, **kw): + if val is None: return '' + return htmlquote(str(val)) + +def render(template, terms=None, asTemplate=False, base=None, isString=False): + # 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} + 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') + + t = _compiletemplate(template, base=base, isString=isString) + t = t(searchList=terms, filter=WebSafe) + if asTemplate: return t + else: return output(str(t)) + +## input forms + +def input(*requireds, **defaults): + if not hasattr(context, '_inputfs'): context._inputfs = cgi.FieldStorage(fp = context.environ['wsgi.input'],environ=context.environ, keep_blank_values=1) + return storify(context._inputfs, *requireds, **defaults) + +## cookies + +def setcookie(name, value, expires="", domain=None): + if expires < 0: expires = -1000000000 + kargs = {'expires': expires, 'path':'/'} + if domain: kargs['domain'] = domain + # @@ should we limit cookies to a different path? + c = Cookie.SimpleCookie() + c[name] = value + for key, val in kargs.iteritems(): c[name][key] = val + header('Set-Cookie', c.items()[0][1].OutputString()) + +def cookies(*requireds, **defaults): + c = Cookie.SimpleCookie() + c.load(context.environ.get('HTTP_COOKIE', '')) + return storify(c, *requireds, **defaults) + +## WSGI Sugar + +def header(h, v): context.headers.append((h, v)) +def output(t): context.output += str(t) + +def write(t): + t = str(t) + t.replace('\r\n', '\n') + head, body = t.split('\n\n', 1) + lines = head.split('\n') + + for line in lines: + if line.isspace(): continue + h, v = line.split(":", 1) + v = v.strip() + if h.lower() == "status": context.status = v + else: header(h, v) + + output(body) + +def webpyfunc(inp, fvars=None, autoreload=False): + 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): + middleware = list(middleware) + if reloader in middleware: + relr = reloader(None) + relrcheck = relr.check + middleware.remove(reloader) + else: + relr = None + relrcheck = lambda: None + + def wsgifunc(e, r): + _load(e) + relrcheck() + result = func() + is_generator = result and hasattr(result, 'next') + if is_generator: + # we need to give wsgi back the headers first, + # so we need to do at iteration + try: firstchunk = handler_result.next() + except StopIteration: firstchunk = '' + status, headers, output = ctx.status, ctx.headers, ctx.output + _unload() + r(status, headers) + if is_generator: return itertools.chain([firstchunk], result) + elif isinstance(output, str): return [output] #@@ other stringlikes? + elif hasattr(output, 'next'): return output + else: raise Exception, "Invalid web.context.output" + + for x in middleware: wsgifunc = x(wsgifunc) + + if relr: + relr.func = wsgifunc + return wsgifunc + return wsgifunc + +def run(inp, *middleware): + autoreload = reloader in middleware + fvars = upvars() + return runwsgi(wsgifunc(webpyfunc(inp, fvars, autoreload), *middleware)) + +def runwsgi(func): + #@@ 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')): + import flup.server.fcgi + return runfcgi(func) + + if 'scgi' in sys.argv: + import flup.server.scgi + return runscgi(func) + + # command line: + return runsimple(func, listget(sys.argv, 1, 8080)) + +def runsimple(func, port=8080): + # 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 sys, socket, errno + import traceback + + class WSGIHandler (SimpleHTTPServer.SimpleHTTPRequestHandler): + def runWSGIApp(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 httpHeader, httpValue in self.headers.items(): + env ['HTTP_%s' % httpHeader.replace ('-', '_').upper()] = httpValue + + # Setup the state + self.wsgiSentHeaders = 0 + self.wsgiHeaders = [] + + try: + # We have there environment, now invoke the application + result = self.server.app(env, self.wsgiStartResponse) + try: + try: + for data in result: + if data: self.wsgiWriteData (data) + finally: + if hasattr(result, 'close'): result.close() + except socket.error, socketErr: + # Catch common network errors and suppress them + if (socketErr.args[0] in (errno.ECONNABORTED, errno.EPIPE)): return + except socket.timeout, socketTimeout: return + except: + print >> debug, traceback.format_exc(), + internalerror() + if not self.wsgiSentHeaders: + self.wsgiStartResponse(ctx.status, ctx.headers) + self.wsgiWriteData(ctx.output) + + if (not self.wsgiSentHeaders): + # We must write out something! + self.wsgiWriteData(" ") + return + + do_POST = runWSGIApp + + def do_GET(self): + if self.path.startswith('/static/'): + SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) + else: + self.runWSGIApp() + + def wsgiStartResponse (self, response_status, response_headers, exc_info=None): + if (self.wsgiSentHeaders): + raise Exception ("Headers already sent and start_response called again!") + # Should really take a copy to avoid changes in the application.... + self.wsgiHeaders = (response_status, response_headers) + return self.wsgiWriteData + + def wsgiWriteData (self, data): + if (not self.wsgiSentHeaders): + status, headers = self.wsgiHeaders + # Need to send header prior to data + statusCode = status [:status.find (' ')] + statusMsg = status [status.find (' ') + 1:] + self.send_response (int (statusCode), statusMsg) + for header, value in headers: + self.send_header (header, value) + self.end_headers() + self.wsgiSentHeaders = 1 + # Send the data + self.wfile.write (data) + + class WSGIServer (SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): + def __init__(self, func): + BaseHTTPServer.HTTPServer.__init__(self, ("0.0.0.0", int(port)), WSGIHandler) + self.app = func + self.serverShuttingDown = 0 + + print "Launching server: http://0.0.0.0:"+str(port)+"/" + WSGIServer(func).serve_forever() + + +def makeserver(WSGIServer): + class MyServer(WSGIServer): + def error(self, req): + w = req.stdout.write + internalerror() + w('Status: '+context.status+'\r\n') + for (h, v) in context.headers: + w(h+': '+v+'\r\n') + w('\r\n'+context.output) + + return MyServer + +def runfcgi(func): + from flup.server.fcgi import WSGIServer + return makeserver(WSGIServer)(func, multiplexed=True).run() + +def runscgi(func): + from flup.server.scgi import WSGIServer + MyServer = makeserver(WSGIServer) + if len(sys.argv) > 2: # progname, scgi + args = sys.argv[:] + args.remove('scgi') + hostport = args[1] + hostport = hostport.split(':',1) + if len(hostport) == 2: hostport = (hostport[0], int(hostport[1])) + else: hostport = ('localhost',int(hostport[0])) + else: hostport = ('localhost',4000) + return MyServer(func, bindAddress=hostport).run() + +## debug + +def debug(*args): + try: out = context.environ['wsgi.errors'] + except: out = sys.stderr + for x in args: + print >> out, pprint.pformat(x) + return '' + +def debugwrite(x): + try: out = context.environ['wsgi.errors'] + except: out = sys.stderr + out.write(x) +debug.write = debugwrite + +class reloader: + def __init__(self, func, tocheck=None): + 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) + +def profiler(app): + def profile_internal(e, o): + out, result = profile(app)(e, o) + return out + ['
'+result+'
'] #@@encode + return profile_internal + +## setting up the context + +class _outputter: + def write(self, x): + if hasattr(ctx, 'output'): return output(x) + else: _oldstdout.write(x) + def flush(self): return _oldstdout.flush() + def close(self): return _oldstdout.close() + +_context = {currentThread():Storage()} +ctx = context = threadeddict(_context) + +if not '_oldstdout' in globals(): + _oldstdout = sys.stdout + sys.stdout = _outputter() + +def _load(env): + _context[currentThread()] = Storage() + ctx.environ = ctx.env = env + ctx.host = env.get('HTTP_HOST') + ctx.home = 'http://' + env.get('HTTP_HOST', '[unknown]') + 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], env.get('SCRIPT_NAME')) + + ctx.fullpath = ctx.path + if dict(input()): ctx.fullpath+='?'+urllib.urlencode(dict(input())) + ctx.status = '200 OK' + ctx.headers = [] + ctx.output = '' + if 'db_parameters' in globals(): + connect(**db_parameters) + +def _unload(): + # ensures db cursors and such are GCed promptly + del _context[currentThread()] + +if __name__ == "__main__": + urls = ('/web.py', 'source') + class source: + def GET(self): + header('Content-Type', 'text/python') + print open(sys.argv[0]).read() + run(urls)