# # # delete "web.py" # # add_dir "web" # # add_file "templates/branchtags.html" # content [451943bb4f5dc413e1b737dfc03944f5664b8f87] # # add_file "web/__init__.py" # content [48d7afda5b9fd9654898380784d3f031a988b85d] # # add_file "web/cheetah.py" # content [06b4764d548093c228c4b27b3c08bc49588fa4a9] # # add_file "web/db.py" # content [6a2db17632e0730936276f1ece7faf1c74ef39dd] # # add_file "web/debugerror.py" # content [dbd19a03c29519c7f45301222c73fc3b36053362] # # add_file "web/form.py" # content [5abbec430ef66a48c76249afe4d41ec5136170a8] # # add_file "web/http.py" # content [5be2ec57345dcad9abafa21199cf9e5c252178a9] # # add_file "web/httpserver.py" # content [fc34d04446669325863e2151dd9d34c037fa4518] # # add_file "web/net.py" # content [82d2ec267f761eb1b45417faf08ffcbf3ddf304d] # # add_file "web/request.py" # content [ef22e4a2d55339f64fda752bd6084bf39576c29d] # # add_file "web/template.py" # content [a0091de1f9fc61a72f60d89da15f301d931ef36c] # # add_file "web/utils.py" # content [2eee2f94d96a42148b310b7eb16eace1409b8718] # # add_file "web/webapi.py" # content [304cb537871312a0612dc0389e5f5aaad7a5563e] # # add_file "web/wsgi.py" # content [d7214b2a4c8693a87db5d17eacd245d441ad7113] # # patch "ChangeLog" # from [87fb5200d9a933a3be459d1a27c458a4b2482693] # to [015d7db885438b0af94e61594203f362e26735f6] # # patch "INSTALL" # from [e6b186c3fbe5e1843af38b941f284add5e3ceaa6] # to [0733df65c4055cb30a766df4fdbc82ea1a1df412] # # patch "README" # from [46ffe1ce5080c9fd0081574ae8a8da30c3df65dc] # to [5fa5a33cef3770ba09a216acc1659490712ba505] # # patch "mtn.py" # from [10bae73eaa4b6b891d434bfcffa8ca66968b204b] # to [0d4cebb009164df2416ea03172a37654f5c95f69] # # patch "release.py" # from [9606208d767e8ed4999ca37fa5e20d13f3fa7be5] # to [456c957746d295e19427471ad9f0e94e1d80195e] # # patch "release.sh" # from [65fe70e241d026a9e09530245362cb09287b6608] # to [1023631dc1b334d6460c18db73b74655744370da] # # patch "templates/branch.html" # from [b69ea4961e6850301e711aea1d7323661e347b4e] # to [b06c53458ef16e6810e5307bae103cf02cc4ccb7] # # patch "templates/branchchangesrss.html" # from [815fa5b5b1d06136d19fbf83b3c9f0c521d7bb05] # to [d8a477c41c496f672863755ad8c79e840715ebcc] # # patch "viewmtn.py" # from [242d0049dd0c9e60441aa30b92c5e908b0d44b9c] # to [a9a27961ccedb3d13dfd697b4c355d3c1ebba32d] # ============================================================ --- templates/branchtags.html 451943bb4f5dc413e1b737dfc03944f5664b8f87 +++ templates/branchtags.html 451943bb4f5dc413e1b737dfc03944f5664b8f87 @@ -0,0 +1,40 @@ +#extends branch + +#def body +
+A tag marks a particular revision that is in some way significant. +A common use of tags is to mark public release of a piece of software. +To view a particular tag, select it from the list below. +
+ ++All tags on this branch are listed below. +
+ +Tag | Signed by | Branches | Age |
---|---|---|---|
+ #filter Filter + $link($tag).html() + #end filter + | ++ $tag.author + | +
+ #filter Filter
+ #for branch in $tag.branches
+ $link($branch).html() + #end for + #end filter + |
+ + $revision_ago($tag.revision) + | +
Python | +$frames[0].filename in $frames[0].function, line $frames[0].lineno | +
---|---|
Web | +$ctx.method $ctx.home$ctx.path | +
$frame.filename
in $frame.function
+ $if frame.context_line:
+
+ $for kv in ctx.headers:
+ $kv[0]: $kv[1]
+ $else:
+ [no headers]
+
+ $ctx.output
+
+ You're seeing this error because you have web.internalerror
+ set to web.debugerror
. Change that if you want a different one.
+
Variable | Value |
---|---|
$kv[0] | $prettify(kv[1]) |
No data.
+""" + +dicttable_r = Template(dicttable_t, filter=websafe) +djangoerror_r = Template(djangoerror_t, filter=websafe) + +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(web.storage({ + 'tback': tback, + 'filename': filename, + 'function': function, + 'lineno': lineno, + 'vars': tback.tb_frame.f_locals, + '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 + frames.reverse() + urljoin = urlparse.urljoin + def prettify(x): + try: + out = pprint.pformat(x) + except Exception, e: + out = '[could not display: <' + e.__class__.__name__ + \ + ': '+str(e)+'>]' + return out + dt = dicttable_r + dt.globals = {'prettify': prettify} + t = djangoerror_r + t.globals = {'ctx': web.ctx, 'web':web, 'dicttable':dt, 'dict':dict, 'str':str} + return t(exception_type, exception_value, frames) + +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/).) + """ + + web.ctx.headers = [('Content-Type', 'text/html')] + web.ctx.output = djangoerror() + +if __name__ == "__main__": + urls = ( + '/', 'index' + ) + + class index: + def GET(self): + thisdoesnotexist + + web.internalerror = web.debugerror + web.run(urls) ============================================================ --- web/form.py 5abbec430ef66a48c76249afe4d41ec5136170a8 +++ web/form.py 5abbec430ef66a48c76249afe4d41ec5136170a8 @@ -0,0 +1,183 @@ +""" +HTML forms +(part of web.py) +""" + +import copy, re +import webapi as web +import utils, net + +def attrget(obj, attr, value=None): + if hasattr(obj, 'has_key') and obj.has_key(attr): return obj[attr] + if hasattr(obj, attr): return getattr(obj, attr) + return value + +class Form: + def __init__(self, *inputs): + self.inputs = inputs + self.valid = True + self.note = None + + def __call__(self, x=None): + o = copy.deepcopy(self) + if x: o.validates(x) + return o + + def render(self): + out = '' + out += self.rendernote(self.note) + out += '' % (i.name, i.description) + out += " | "+i.pre+i.render()+i.post+" | " + out += '%s |
---|
' + net.websafe(result) + ''] + return profile_internal + +if __name__ == "__main__": + import doctest + doctest.testmod() ============================================================ --- web/httpserver.py fc34d04446669325863e2151dd9d34c037fa4518 +++ web/httpserver.py fc34d04446669325863e2151dd9d34c037fa4518 @@ -0,0 +1,125 @@ +__all__ = ["runsimple"] + +import sys +import webapi as web + +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 >> web.debug, traceback.format_exc(), + + 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 "http://%s:%d/" % server_address + WSGIServer(func, server_address).serve_forever() ============================================================ --- web/net.py 82d2ec267f761eb1b45417faf08ffcbf3ddf304d +++ web/net.py 82d2ec267f761eb1b45417faf08ffcbf3ddf304d @@ -0,0 +1,150 @@ +""" +Network Utilities +(from web.py) +""" + +__all__ = [ + "validipaddr", "validipport", "validip", "validaddr", + "urlquote", + "httpdate", "parsehttpdate", + "htmlquote", "websafe", +] + +import urllib, time +try: import datetime +except ImportError: pass + +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_ + + >>> validaddr('/path/to/socket') + '/path/to/socket' + >>> validaddr('8000') + ('0.0.0.0', 8000) + >>> validaddr('127.0.0.1') + ('127.0.0.1', 8080) + >>> validaddr('127.0.0.1:8000') + ('127.0.0.1', 8000) + >>> validaddr('fff') + Traceback (most recent call last): + ... + ValueError: fff is not a valid IP address/port + """ + if '/' in string_: + return string_ + else: + return validip(string_) + +def urlquote(val): + """ + Quotes a string for use in a URL. + + >>> urlquote('://?f=1&j=1') + '%3A//%3Ff%3D1%26j%3D1' + >>> urlquote(None) + '' + >>> urlquote(u'\u203d') + '%E2%80%BD' + """ + if val is None: return '' + if not isinstance(val, unicode): val = str(val) + else: val = val.encode('utf-8') + return urllib.quote(val) + +def httpdate(date_obj): + """ + Formats a datetime object for use in HTTP headers. + + >>> import datetime + >>> httpdate(datetime.datetime(1970, 1, 1, 1, 1, 1)) + 'Thu, 01 Jan 1970 01:01:01 GMT' + """ + return date_obj.strftime("%a, %d %b %Y %H:%M:%S GMT") + +def parsehttpdate(string_): + """ + Parses an HTTP date into a datetime object. + + >>> parsehttpdate('Thu, 01 Jan 1970 01:01:01 GMT') + datetime.datetime(1970, 1, 1, 1, 1, 1) + """ + try: + t = time.strptime(string_, "%a, %d %b %Y %H:%M:%S %Z") + except ValueError: + return None + return datetime.datetime(*t[:6]) + +def htmlquote(text): + """ + Encodes `text` for raw use in HTML. + + >>> htmlquote("<'&\\">") + '<'&">' + """ + text = text.replace("&", "&") # Must be done first! + text = text.replace("<", "<") + text = text.replace(">", ">") + text = text.replace("'", "'") + text = text.replace('"', """) + return text + +def websafe(val): + """ + Converts `val` so that it's safe for use in HTML. + + >>> websafe("<'&\\">") + '<'&">' + >>> websafe(None) + '' + """ + if val is None: return '' + if not isinstance(val, unicode): val = str(val) + return htmlquote(val) + +if __name__ == "__main__": + import doctest + doctest.testmod() ============================================================ --- web/request.py ef22e4a2d55339f64fda752bd6084bf39576c29d +++ web/request.py ef22e4a2d55339f64fda752bd6084bf39576c29d @@ -0,0 +1,147 @@ +""" +Request Delegation +(from web.py) +""" + +__all__ = ["handle", "nomethod", "autodelegate", "webpyfunc", "run"] + +import sys, re, types, os.path, urllib + +import http, wsgi, utils, webapi +import webapi as web + +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 utils.group(mapping, 2): + if isinstance(ofno, tuple): + ofn, fna = ofno[0], list(ofno[1:]) + else: + ofn, fna = ofno, [] + fn, result = utils.re_subm('^' + url + '$', ofn, web.ctx.path) + if result: # it's a match + if fn.split(' ', 1)[0] == "redirect": + url = fn.split(' ', 1)[1] + if web.ctx.method == "GET": + x = web.ctx.env.get('QUERY_STRING', '') + if x: + url += '?' + x + return http.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 + if isinstance(mod, types.ModuleType): + mod = vars(mod) + try: + cls = mod[cls] + except KeyError: + return web.notfound() + + meth = web.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 web.notfound() + +def nomethod(cls): + """Returns a `405 Method Not Allowed` error for `cls`.""" + web.ctx.status = '405 Method Not Allowed' + web.header('Content-Type', 'text/html') + web.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 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 web.notfound() + else: + return web.notfound() + return internal + +def webpyfunc(inp, fvars, autoreload=False): + """If `inp` is a url mapping, returns a function that calls handle.""" + 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 = utils.dictfind(fvars, inp) + func = lambda: handle(getattr(mod, name), mod) + else: + func = lambda: handle(inp, fvars) + else: + func = inp + return func + +def run(inp, fvars, *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 = http.reloader in middleware + return wsgi.runwsgi(webapi.wsgifunc(webpyfunc(inp, fvars, autoreload), *middleware)) ============================================================ --- web/template.py a0091de1f9fc61a72f60d89da15f301d931ef36c +++ web/template.py a0091de1f9fc61a72f60d89da15f301d931ef36c @@ -0,0 +1,838 @@ +""" +simple, elegant templating +(part of web.py) +""" + +import re, glob, os, os.path +from types import FunctionType as function +from utils import storage, group +from net import websafe + +# differences from python: +# - for: has an optional else: that gets called if the loop never runs +# differences to add: +# - you can use the expression inside if, while blocks +# - special for loop attributes, like django? +# - you can check to see if a variable is defined (perhaps w/ get func?) +# all these are probably good ideas for python... + +# todo: +# inline tuple +# relax constraints on spacing +# continue, break, etc. +# tracebacks + +global_globals = {'None':None, 'False':False, 'True': True} +MAX_ITERS = 100000 + +WHAT = 0 +ARGS = 4 +KWARGS = 6 +NAME = 2 +BODY = 4 +CLAUSE = 2 +ELIF = 6 +ELSE = 8 +IN = 6 +NAME = 2 +EXPR = 4 +FILTER = 4 +THING = 2 +ATTR = 4 +ITEM = 4 +NEGATE = 4 +X = 2 +OP = 4 +Y = 6 +LINENO = -1 + +# http://docs.python.org/ref/identifiers.html +r_var = '[a-zA-Z_][a-zA-Z0-9_]*' + +class ParseError(Exception): pass +class Parser: + def __init__(self, text): + self.t = text + self.p = 0 + self._lock = [False] + + def lock(self): + self._lock[-1] = True + + def curline(self): + return self.t[:self.p].count('\n')+1 + + def csome(self): + return repr(self.t[self.p:self.p+5]+'...') + + def Error(self, x, y=None): + if y is None: y = self.csome() + raise ParseError, "expected %s, got %s (line %s)" % (x, y, self.curline()) + + def q(self, f): + def internal(*a, **kw): + checkp = self.p + self._lock.append(False) + try: + q = f(*a, **kw) + except ParseError: + if self._lock[-1]: + raise + self.p = checkp + self._lock.pop() + return False + self._lock.pop() + return q or True + return internal + + def tokr(self, t): + text = self.c(len(t)) + if text != t: + self.Error(repr(t), repr(text)) + return t + + def ltokr(self, *l): + for x in l: + o = self.tokq(x) + if o: return o + self.Error('one of '+repr(l)) + + def rer(self, r): + x = re.match(r, self.t[self.p:]) #@@re_compile + if not x: + self.Error('r'+repr(r)) + return self.tokr(x.group()) + + def endr(self): + if self.p != len(self.t): + self.Error('EOF') + + def c(self, n=1): + out = self.t[self.p:self.p+n] + if out == '' and n != 0: + self.Error('character', 'EOF') + self.p += n + return out + + def lookbehind(self, t): + return self.t[self.p-len(t):self.p] == t + + def __getattr__(self, a): + if a.endswith('q'): + return self.q(getattr(self, a[:-1]+'r')) + raise AttributeError, a + +class TemplateParser(Parser): + def __init__(self, *a, **kw): + Parser.__init__(self, *a, **kw) + self.curws = '' + self.curind = '' + + def o(self, *a): + return a+('lineno', self.curline()) + + def go(self): + # maybe try to do some traceback parsing/hacking + return self.gor() + + def gor(self): + header = self.defwithq() + results = self.lines(start=True) + self.endr() + return header, results + + def ws(self): + n = 0 + while self.tokq(" "): n += 1 + return " " * n + + def defwithr(self): + self.tokr('$def with ') + self.lock() + self.tokr('(') + args = [] + kw = [] + x = self.req(r_var) + while x: + if self.tokq('='): + v = self.exprr() + kw.append((x, v)) + else: + args.append(x) + x = self.tokq(', ') and self.req(r_var) + self.tokr(')\n') + return self.o('defwith', 'null', None, 'args', args, 'kwargs', kw) + + def literalr(self): + o = ( + self.req('"[^"]*"') or #@@ no support for escapes + self.req("'[^']*'") + ) + if o is False: + o = self.req('\-?[0-9]+(\.[0-9]*)?') + if o is not False: + if '.' in o: o = float(o) + else: o = int(o) + + if o is False: self.Error('literal') + return self.o('literal', 'thing', o) + + def listr(self): + self.tokr('[') + self.lock() + x = [] + if not self.tokq(']'): + while True: + t = self.exprr() + x.append(t) + if not self.tokq(', '): break + self.tokr(']') + return self.o('list', 'thing', x) + + def dictr(self): + self.tokr('{') + self.lock() + x = {} + if not self.tokq('}'): + while True: + k = self.exprr() + self.tokr(': ') + v = self.exprr() + x[k] = v + if not self.tokq(', '): break + self.tokr('}') + return self.o('dict', 'thing', x) + + def parenr(self): + self.tokr('(') + self.lock() + o = self.exprr() # todo: allow list + self.tokr(')') + return self.o('paren', 'thing', o) + + def atomr(self): + """returns var, literal, paren, dict, or list""" + o = ( + self.varq() or + self.parenq() or + self.dictq() or + self.listq() or + self.literalq() + ) + if o is False: self.Error('atom') + return o + + def primaryr(self): + """returns getattr, call, or getitem""" + n = self.atomr() + while 1: + if self.tokq('.'): + v = self.req(r_var) + if not v: + self.p -= 1 # get rid of the '.' + break + else: + n = self.o('getattr', 'thing', n, 'attr', v) + elif self.tokq('('): + args = [] + kw = [] + + while 1: + # need to see if we're doing a keyword argument + checkp = self.p + k = self.req(r_var) + if k and self.tokq('='): # yup + v = self.exprr() + kw.append((k, v)) + else: + self.p = checkp + x = self.exprq() + if x: # at least it's something + args.append(x) + else: + break + + if not self.tokq(', '): break + self.tokr(')') + n = self.o('call', 'thing', n, 'args', args, 'kwargs', kw) + elif self.tokq('['): + v = self.exprr() + self.tokr(']') + n = self.o('getitem', 'thing', n, 'item', v) + else: + break + + return n + + def exprr(self): + negate = self.tokq('not ') + x = self.primaryr() + if self.tokq(' '): + operator = self.ltokr('not in', 'in', 'is not', 'is', '==', '!=', '>=', '<=', '<', '>', 'and', 'or', '*', '+', '-', '/', '%') + self.tokr(' ') + y = self.exprr() + x = self.o('test', 'x', x, 'op', operator, 'y', y) + + return self.o('expr', 'thing', x, 'negate', negate) + + def varr(self): + return self.o('var', 'name', self.rer(r_var)) + + def liner(self): + out = [] + o = self.curws + while 1: + c = self.c() + self.lock() + if c == '\n': + self.p -= 1 + break + if c == '$': + if self.lookbehind('\\$'): + o = o[:-1] + c + else: + filter = not bool(self.tokq(':')) + + if self.tokq('{'): + out.append(o) + out.append(self.o('itpl', 'name', self.exprr(), 'filter', filter)) + self.tokr('}') + o = '' + else: + g = self.primaryq() + if g: + out.append(o) + out.append(self.o('itpl', 'name', g, 'filter', filter)) + o = '' + else: + o += c + else: + o += c + self.tokr('\n') + if not self.lookbehind('\\\n'): + o += '\n' + else: + o = o[:-1] + out.append(o) + return self.o('line', 'thing', out) + + def varsetr(self): + self.tokr('$var ') + self.lock() + what = self.rer(r_var) + self.tokr(':') + body = self.lines() + return self.o('varset', 'name', what, 'body', body) + + def ifr(self): + self.tokr("$if ") + self.lock() + expr = self.exprr() + self.tokr(":") + ifc = self.lines() + + elifs = [] + while self.tokq(self.curws + self.curind + '$elif '): + v = self.exprr() + self.tokr(':') + c = self.lines() + elifs.append(self.o('elif', 'clause', v, 'body', c)) + + if self.tokq(self.curws + self.curind + "$else:"): + elsec = self.lines() + else: + elsec = None + + return self.o('if', 'clause', expr, 'then', ifc, 'elif', elifs, 'else', elsec) + + def forr(self): + self.tokr("$for ") + self.lock() + v = self.setabler() + self.tokr(" in ") + g = self.exprr() + self.tokr(":") + l = self.lines() + + if self.tokq(self.curws + self.curind + '$else:'): + elsec = self.lines() + else: + elsec = None + + return self.o('for', 'name', v, 'body', l, 'in', g, 'else', elsec) + + def whiler(self): + self.tokr('$while ') + self.lock() + v = self.exprr() + self.tokr(":") + l = self.lines() + + if self.tokq(self.curws + self.curind + '$else:'): + elsec = self.lines() + else: + elsec = None + + return self.o('while', 'clause', v, 'body', l, 'null', None, 'else', elsec) + + def assignr(self): + self.tokr('$ ') + assign = self.rer(r_var) # NOTE: setable + self.tokr(' = ') + expr = self.exprr() + self.tokr('\n') + + return self.o('assign', 'name', assign, 'expr', expr) + + def commentr(self): + self.tokr('$#') + self.lock() + while self.c() != '\n': pass + return self.o('comment') + + def setabler(self): + out = [self.varr()] #@@ not quite right + while self.tokq(', '): + out.append(self.varr()) + return out + + def lines(self, start=False): + """ + This function gets called from two places: + 1. at the start, where it's matching the document itself + 2. after any command, where it matches one line or an indented block + """ + o = [] + if not start: # try to match just one line + singleline = self.tokq(' ') and self.lineq() + if singleline: + return [singleline] + else: + self.rer(' *') #@@slurp space? + self.tokr('\n') + oldind = self.curind + self.curind += ' ' + while 1: + oldws = self.curws + t = self.tokq(oldws + self.curind) + if not t: break + + self.curws += self.ws() + x = t and ( + self.varsetq() or + self.ifq() or + self.forq() or + self.whileq() or + self.assignq() or + self.commentq() or + self.lineq()) + self.curws = oldws + if not x: + break + elif x[WHAT] == 'comment': + pass + else: + o.append(x) + + if not start: self.curind = oldind + return o + +class Stowage(storage): + def __str__(self): return self.get('_str') + #@@ edits in place + def __add__(self, other): + if isinstance(other, (unicode, str)): + self._str += other + return self + else: + raise TypeError, 'cannot add' + def __radd__(self, other): + if isinstance(other, (unicode, str)): + self._str = other + self._str + return self + else: + raise TypeError, 'cannot add' + +class WTF(AssertionError): pass +class SecurityError(Exception): + """The template seems to be trying to do something naughty.""" + pass + +Required = object() +class Template: + globals = {} + def __init__(self, text, filter=None): + self.filter = filter + # universal newlines: + text = text.replace('\r\n', '\n').replace('\r', '\n') + if not text.endswith('\n'): text += '\n' + header, tree = TemplateParser(text).go() + self.tree = tree + if header: + self.h_defwith(header) + else: + self.args, self.kwargs = (), {} + + def __call__(self, *a, **kw): + d = self.globals.copy() + d.update(self._parseargs(a, kw)) + f = Fill(self.tree, d=d) + if self.filter: f.filter = self.filter + return f.go() + + def _parseargs(self, inargs, inkwargs): + # difference from Python: + # no error on setting a keyword arg twice + d = {} + for arg in self.args: + d[arg] = Required + for kw, val in self.kwargs: + d[kw] = val + + for n, val in enumerate(inargs): + if n < len(self.args): + d[self.args[n]] = val + elif n < len(self.args)+len(self.kwargs): + kw = self.kwargs[n - len(self.args)][0] + d[kw] = val + + for kw, val in inkwargs.iteritems(): + d[kw] = val + + unset = [] + for k, v in d.iteritems(): + if v is Required: + unset.append(k) + if unset: + raise TypeError, 'values for %s are required' % unset + + return d + + def h_defwith(self, header): + assert header[WHAT] == 'defwith' + f = Fill(self.tree, d={}) + + self.args = header[ARGS] + self.kwargs = [] + for var, valexpr in header[KWARGS]: + self.kwargs.append((var, f.h(valexpr))) + +class Handle: + def __init__(self, parsetree, **kw): + self._funccache = {} + self.parsetree = parsetree + for (k, v) in kw.iteritems(): setattr(self, k, v) + + def h(self, item): + return getattr(self, 'h_' + item[WHAT])(item) + +class Fill(Handle): + builtins = global_globals + def filter(self, text): + if text is None: return '' + else: return str(text) + # later: can do stuff like WebSafe + + def h_literal(self, i): + item = i[THING] + if isinstance(item, str) and item[0] in ['"', "'"]: + item = item[1:-1] + elif isinstance(item, (float, int)): + pass + return item + + def h_list(self, i): + x = i[THING] + out = [] + for item in x: + out.append(self.h(item)) + return out + + def h_dict(self, i): + x = i[THING] + out = {} + for k, v in x.iteritems(): + out[self.h(k)] = self.h(v) + return out + + def h_paren(self, i): + item = i[THING] + if isinstance(item, list): + raise NotImplementedError, 'tuples' + return self.h(item) + + def h_getattr(self, i): + thing, attr = i[THING], i[ATTR] + thing = self.h(thing) + if attr.startswith('_') or attr.startswith('func_') or attr.startswith('im_'): + raise SecurityError, 'tried to get ' + attr + try: + if thing in self.builtins: + raise SecurityError, 'tried to getattr on ' + repr(thing) + except TypeError: + pass # raised when testing an unhashable object + try: + return getattr(thing, attr) + except AttributeError: + if isinstance(thing, list) and attr == 'join': + return lambda s: s.join(thing) + else: + raise + + def h_call(self, i): + call = self.h(i[THING]) + args = [self.h(x) for x in i[ARGS]] + kw = dict([(x, self.h(y)) for (x, y) in i[KWARGS]]) + return call(*args, **kw) + + def h_getitem(self, i): + thing, item = i[THING], i[ITEM] + thing = self.h(thing) + item = self.h(item) + return thing[item] + + def h_expr(self, i): + item = self.h(i[THING]) + if i[NEGATE]: + item = not item + return item + + def h_test(self, item): + ox, op, oy = item[X], item[OP], item[Y] + # for short-circuiting to work, we can't eval these here + e = self.h + if op == 'is': + return e(ox) is e(oy) + elif op == 'is not': + return e(ox) is not e(oy) + elif op == 'in': + return e(ox) in e(oy) + elif op == 'not in': + return e(ox) not in e(oy) + elif op == '==': + return e(ox) == e(oy) + elif op == '!=': + return e(ox) != e(oy) + elif op == '>': + return e(ox) > e(oy) + elif op == '<': + return e(ox) < e(oy) + elif op == '<=': + return e(ox) <= e(oy) + elif op == '>=': + return e(ox) >= e(oy) + elif op == 'and': + return e(ox) and e(oy) + elif op == 'or': + return e(ox) or e(oy) + elif op == '+': + return e(ox) + e(oy) + elif op == '-': + return e(ox) - e(oy) + elif op == '*': + return e(ox) * e(oy) + elif op == '/': + return e(ox) / e(oy) + elif op == '%': + return e(ox) % e(oy) + else: + raise WTF, 'op ' + op + + def h_var(self, i): + v = i[NAME] + if v in self.d: + return self.d[v] + elif v in self.builtins: + return self.builtins[v] + elif v == 'self': + return self.output + else: + raise NameError, 'could not find %s (line %s)' % (repr(i[NAME]), i[LINENO]) + + def h_line(self, i): + out = [] + for x in i[THING]: + if isinstance(x, str): + out.append(x) + elif x[WHAT] == 'itpl': + o = self.h(x[NAME]) + if x[FILTER]: + o = self.filter(o) + else: + if isinstance(o, Stowage): + o = o._str + out.append(o) + else: + raise WTF, x + return ''.join(out) + + def h_varset(self, i): + self.output[i[NAME]] = ''.join(self.h_lines(i[BODY])) + return '' + + def h_if(self, i): + expr = self.h(i[CLAUSE]) + if expr: + do = i[BODY] + else: + for e in i[ELIF]: + expr = self.h(e[CLAUSE]) + if expr: + do = e[BODY] + break + else: + do = i[ELSE] + return ''.join(self.h_lines(do)) + + def h_for(self, i): + out = [] + assert i[IN][WHAT] == 'expr' + invar = self.h(i[IN]) + forvar = i[NAME] + if invar: + for nv in invar: + if len(forvar) == 1: + fv = forvar[0] + assert fv[WHAT] == 'var' + self.d[fv[NAME]] = nv # same (lack of) scoping as Python + else: + for x, y in zip(forvar, nv): + assert x[WHAT] == 'var' + self.d[x[NAME]] = y + + out.extend(self.h_lines(i[BODY])) + else: + if i[ELSE]: + out.extend(self.h_lines(i[ELSE])) + return ''.join(out) + + def h_while(self, i): + out = [] + expr = self.h(i[CLAUSE]) + if not expr: + return ''.join(self.h_lines(i[ELSE])) + c = 0 + while expr: + c += 1 + if c >= MAX_ITERS: + raise RuntimeError, 'too many while-loop iterations (line %s)' % i[LINENO] + out.extend(self.h_lines(i[BODY])) + expr = self.h(i[CLAUSE]) + return ''.join(out) + + def h_assign(self, i): + self.d[i[NAME]] = self.h(i[EXPR]) + return '' + + def h_comment(self, i): pass + + def h_lines(self, lines): + if lines is None: return [] + return map(self.h, lines) + + def go(self): + self.output = Stowage() + self.output._str = ''.join(map(self.h, self.parsetree)) + if self.output.keys() == ['_str']: + self.output = self.output['_str'] + return self.output + +class render: + def __init__(self, loc='templates/', cache=True): + self.loc = loc + if cache: + self.cache = {} + else: + self.cache = False + + def _do(self, name, filter=None): + if self.cache is False or name not in self.cache: + p = glob.glob(self.loc + name + '.*') + if not p and os.path.isdir(self.loc + name): + return render(self.loc + name + '/', cache=self.cache) + elif not p: + raise AttributeError, 'no template named ' + name + p = p[0] + c = Template(open(p).read()) + if self.cache is not False: self.cache[name] = (p, c) + + if self.cache is not False: p, c = self.cache[name] + + if p.endswith('.html'): + import webapi as web + if 'headers' in web.ctx: + web.header('Content-Type', 'text/html; charset=utf-8', unique=True) + if not filter: c.filter = websafe + elif p.endswith('.xml'): + if not filter: c.filter = websafe + + return c + + def __getattr__(self, p): + return self._do(p) + +def frender(fn, *a, **kw): + return Template(open(fn).read(), *a, **kw) + +def test(): + import sys + verbose = '-v' in sys.argv + def assertEqual(a, b): + if a == b: + if verbose: + sys.stderr.write('.') + sys.stderr.flush() + else: + assert a == b, "\nexpected: %s\ngot: %s" % (repr(b), repr(a)) + + from utils import storage, group + t = Template + + tests = [ + lambda: t('1')(), '1\n', + lambda: t('$def with ()\n1')(), '1\n', + lambda: t('$def with (a)\n$a')(1), '1\n', + lambda: t('$def with (a=0)\n$a')(1), '1\n', + lambda: t('$def with (a=0)\n$a')(a=1), '1\n', + lambda: t('$if 1: 1')(), '1\n', + lambda: t('$if 1:\n 1')(), '1\n', + lambda: t('$if 0: 0\n$elif 1: 1')(), '1\n', + lambda: t('$if 0: 0\n$elif None: 0\n$else: 1')(), '1\n', + lambda: t('$if (0 < 1) and (1 < 2): 1')(), '1\n', + lambda: t('$for x in [1, 2, 3]: $x')(), '1\n2\n3\n', + lambda: t('$for x in []: 0\n$else: 1')(), '1\n', + lambda: t('$def with (a)\n$while a and a.pop(): 1')([1, 2, 3]), '1\n1\n1\n', + lambda: t('$while 0: 0\n$else: 1')(), '1\n', + lambda: t('$ a = 1\n$a')(), '1\n', + lambda: t('$# 0')(), '', + lambda: t('$def with (d)\n$for k, v in d.iteritems(): $k')({1: 1}), '1\n', + lambda: t('$def with (a)\n$(a)')(1), '1\n', + lambda: t('$def with (a)\n$a')(1), '1\n', + lambda: t('$def with (a)\n$a.b')(storage(b=1)), '1\n', + lambda: t('$def with (a)\n$a[0]')([1]), '1\n', + lambda: t('${0 or 1}')(), '1\n', + lambda: t('$ a = [1]\n$a[0]')(), '1\n', + lambda: t('$ a = {1: 1}\n$a.keys()[0]')(), '1\n', + lambda: t('$ a = []\n$if not a: 1')(), '1\n', + lambda: t('$ a = {}\n$if not a: 1')(), '1\n', + lambda: t('$ a = -1\n$a')(), '-1\n', + lambda: t('$ a = "1"\n$a')(), '1\n', + lambda: t('$if 1 is 1: 1')(), '1\n', + lambda: t('$if not 0: 1')(), '1\n', + lambda: t('$if 1:\n $if 1: 1')(), '1\n', + lambda: t('$ a = 1\n$a')(), '1\n', + lambda: t('$ a = 1.\n$a')(), '1.0\n', + lambda: t('$({1: 1}.keys()[0])')(), '1\n', + ] + + for func, value in group(tests, 2): + assertEqual(func(), value) + + j = Template("$var foo: bar")() + assertEqual(str(j), '') + assertEqual(j.foo, 'bar\n') + if verbose: sys.stderr.write('\n') + + +if __name__ == "__main__": + test() ============================================================ --- web/utils.py 2eee2f94d96a42148b310b7eb16eace1409b8718 +++ web/utils.py 2eee2f94d96a42148b310b7eb16eace1409b8718 @@ -0,0 +1,763 @@ +""" +General Utilities +(part of web.py) +""" + +__all__ = [ + "Storage", "storage", "storify", + "iters", + "rstrips", "lstrips", "strips", + "TimeoutError", "timelimit", + "Memoize", "memoize", + "re_compile", "re_subm", + "group", + "IterBetter", "iterbetter", + "dictreverse", "dictfind", "dictfindall", "dictincr", "dictadd", + "listget", "intget", "datestr", + "numify", "denumify", "dateify", + "CaptureStdout", "capturestdout", "Profile", "profile", + "tryall", + "ThreadedDict", + "autoassign", + "to36", + "safemarkdown" +] + +import re, sys, time, threading +try: import datetime +except ImportError: pass + +class Storage(dict): + """ + A Storage object is like a dictionary except `obj.foo` can be used + in addition to `obj['foo']`. + + >>> o = storage(a=1) + >>> o.a + 1 + >>> o['a'] + 1 + >>> o.a = 2 + >>> o['a'] + 2 + >>> del o.a + >>> o.a + Traceback (most recent call last): + ... + AttributeError: 'a' + + """ + def __getattr__(self, key): + try: + return self[key] + except KeyError, k: + raise AttributeError, k + + def __setattr__(self, key, value): + self[key] = value + + def __delattr__(self, key): + try: + del self[key] + except KeyError, k: + raise AttributeError, k + + def __repr__(self): + return '