From d740aa68a4d205076b2d132067ac2c31ceb3cd2e Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Wed, 16 Jan 2013 01:16:25 +0100 Subject: [PATCH 01/16] Added a simpleerror function. --- wrw/proto.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/wrw/proto.py b/wrw/proto.py index 8474b3a..4cf1951 100644 --- a/wrw/proto.py +++ b/wrw/proto.py @@ -85,6 +85,22 @@ def htmlq(html): ret += c return ret +def simpleerror(env, startreq, code, title, msg): + buf = """ + + + +%s + + +

%s

+

%s

+ + +""" % (title, title, htmlq(msg)) + startreq("%i %s" % (code, title), [("Content-Type", "text/html"), ("Content-Length", str(len(buf)))]) + return [buf] + def urlq(url): ret = "" for c in url: -- 2.11.0 From 7450e2fcc553f66844b34d5c062a965b8fca28c5 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Wed, 16 Jan 2013 01:16:34 +0100 Subject: [PATCH 02/16] Handle missing-Host-header condition. --- wrw/dispatch.py | 8 +++++++- wrw/util.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/wrw/dispatch.py b/wrw/dispatch.py index 15ea99e..fa1f669 100644 --- a/wrw/dispatch.py +++ b/wrw/dispatch.py @@ -1,5 +1,5 @@ import sys, traceback -import env +import env, req, proto __all__ = ["restart"] @@ -73,3 +73,9 @@ def handle(req, startreq, handler): return resp finally: req.cleanup() + +def handleenv(env, startreq, handler): + if not "HTTP_HOST" in env: + return proto.simpleerror(env, startreq, 400, "Bad Request", "Request must include Host header.") + r = req.origrequest(env) + return handle(r, startreq, handler) diff --git a/wrw/util.py b/wrw/util.py index 5ee002b..6368b18 100644 --- a/wrw/util.py +++ b/wrw/util.py @@ -3,7 +3,7 @@ import req, dispatch, session, form def wsgiwrap(callable): def wrapper(env, startreq): - return dispatch.handle(req.origrequest(env), startreq, callable) + return dispatch.handleenv(env, startreq, callable) return wrapper def formparams(callable): -- 2.11.0 From 77dd732a3b3b604a6029e748ffd4fb0b9760642d Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Sat, 2 Feb 2013 06:25:37 +0100 Subject: [PATCH 03/16] Moved the iterproxy to an optionally usable preiter in wrw.util. --- wrw/dispatch.py | 28 +--------------------------- wrw/util.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/wrw/dispatch.py b/wrw/dispatch.py index fa1f669..4d22b72 100644 --- a/wrw/dispatch.py +++ b/wrw/dispatch.py @@ -16,32 +16,6 @@ def mangle(result): return result return [str(result)] -class iterproxy(object): - # Makes sure iter(real).next() is called immediately, in order to - # let generator code run. - def __init__(self, real): - self.bk = real - self.bki = iter(real) - self._next = [None] - self.next() - - def __iter__(self): - return self - - def next(self): - if self._next is None: - raise StopIteration() - ret = self._next[0] - try: - self._next[:] = [self.bki.next()] - except StopIteration: - self._next = None - return ret - - def close(self): - if hasattr(self.bk, "close"): - self.bk.close() - def defaulterror(req, excinfo): import resp traceback.print_exception(*excinfo) @@ -60,7 +34,7 @@ def handle(req, startreq, handler): resp = [""] while True: try: - resp = iterproxy(handler(req)) + resp = handler(req) break except restart, i: handler = i.handle diff --git a/wrw/util.py b/wrw/util.py index 6368b18..55b4ee2 100644 --- a/wrw/util.py +++ b/wrw/util.py @@ -34,6 +34,37 @@ def persession(data = None): return wrapper return dec +class preiter(object): + __slots__ = ["bk", "bki", "_next"] + end = object() + def __init__(self, real): + self.bk = real + self.bki = iter(real) + self._next = None + self.next() + + def __iter__(self): + return self + + def next(self): + if self._next is self.end: + raise StopIteration() + ret = self._next + try: + self._next = next(self.bki) + except StopIteration: + self._next = self.end + return ret + + def close(self): + if hasattr(self.bk, "close"): + self.bk.close() + +def pregen(callable): + def wrapper(*args, **kwargs): + return preiter(callable(*args, **kwargs)) + return wrapper + class sessiondata(object): @classmethod def get(cls, req, create = True): -- 2.11.0 From 5a071401aade4fa5c1d3ef852a5861cb94475e73 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Sat, 6 Apr 2013 18:06:41 +0200 Subject: [PATCH 04/16] Error out more usefully from formparams when required parameters are missing. --- wrw/util.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wrw/util.py b/wrw/util.py index 55b4ee2..64b820f 100644 --- a/wrw/util.py +++ b/wrw/util.py @@ -1,5 +1,5 @@ import inspect -import req, dispatch, session, form +import req, dispatch, session, form, resp def wsgiwrap(callable): def wrapper(env, startreq): @@ -16,6 +16,9 @@ def formparams(callable): for arg in list(args): if arg not in spec.args: del args[arg] + for i in xrange(len(spec.args) - len(spec.defaults)): + if spec.args[i] not in args: + raise resp.httperror(400, "Missing parameter", ("The query parameter `", resp.h.code(spec.args[i]), "' is required but not supplied.")) return callable(**args) return wrapper -- 2.11.0 From b239aa235034b2d44c9f8948503cc2c59f747ed1 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Sun, 7 Apr 2013 19:03:24 +0200 Subject: [PATCH 05/16] Added more convenient ways to use non-XHTML sp output. --- wrw/sp/util.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/wrw/sp/util.py b/wrw/sp/util.py index 50fbd90..3ea7a8a 100644 --- a/wrw/sp/util.py +++ b/wrw/sp/util.py @@ -1,3 +1,5 @@ +import StringIO +from wrw import dispatch import cons def findnsnames(el): @@ -144,6 +146,12 @@ class formatter(object): def fragment(cls, out, el, *args, **kw): cls(out=out, root=el, *args, **kw).node(el) + @classmethod + def format(cls, el, *args, **kw): + buf = StringIO.StringIO() + cls.output(buf, el, *args, **kw) + return buf.getvalue() + def update(self, **ch): ret = type(self).__new__(type(self)) ret.__dict__.update(self.__dict__) @@ -211,3 +219,20 @@ class indenter(formatter): def start(self): super(indenter, self).start() self.write('\n') + +class response(dispatch.restart): + charset = "utf-8" + doctype = None + formatter = indenter + + def __init__(self, root): + super(response, self).__init__() + self.root = root + + @property + def ctype(self): + raise Exception("a subclass of wrw.sp.util.response must override ctype") + + def handle(self, req): + req.ohead["Content-Type"] = self.ctype + return [self.formatter.format(self.root, doctype=self.doctype, charset=self.charset)] -- 2.11.0 From 525d7938bc668f6079f0fd00a0baad75b1aebe24 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Tue, 23 Apr 2013 05:52:54 +0200 Subject: [PATCH 06/16] Added a mod_python-style (but better ^^) function multiplexer. --- wrw/__init__.py | 2 +- wrw/util.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/wrw/__init__.py b/wrw/__init__.py index 8fcb72e..95ff4cd 100644 --- a/wrw/__init__.py +++ b/wrw/__init__.py @@ -1,7 +1,7 @@ __all__ = ["wsgiwrap", "restart", "cookie", "formdata"] import proto -from util import wsgiwrap, formparams, persession, sessiondata, autodirty, manudirty, specdirty +from util import wsgiwrap, formparams, funplex, persession, sessiondata, autodirty, manudirty, specdirty from dispatch import restart import cookie from form import formdata diff --git a/wrw/util.py b/wrw/util.py index 64b820f..cfc2c8c 100644 --- a/wrw/util.py +++ b/wrw/util.py @@ -22,6 +22,27 @@ def formparams(callable): return callable(**args) return wrapper +def funplex(*funs, **nfuns): + dir = {} + dir.update(((fun.__name__, fun) for fun in funs)) + dir.update(nfuns) + def handler(req): + if req.pathinfo == "": + raise resp.redirect(req.uriname + "/") + if req.pathinfo[:1] != "/": + raise resp.notfound() + p = req.pathinfo[1:] + if p == "": + p = "__index__" + bi = 1 + else: + p = p.partition("/")[0] + bi = len(p) + 1 + if p in dir: + return dir[p](req.shift(bi)) + raise resp.notfound() + return handler + def persession(data = None): def dec(callable): def wrapper(req): -- 2.11.0 From 1864be32db482df017bc7dd57bbd2ccadeec4429 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Wed, 29 May 2013 02:47:22 +0200 Subject: [PATCH 07/16] Added a simple cache-helper function. --- wrw/resp.py | 6 ++++++ wrw/util.py | 11 +++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/wrw/resp.py b/wrw/resp.py index 24896ba..159126a 100644 --- a/wrw/resp.py +++ b/wrw/resp.py @@ -71,3 +71,9 @@ class redirect(dispatch.restart): req.status(self.status, "Redirect") req.ohead["Location"] = proto.appendurl(proto.requrl(req), self.url) return [] + +class unmodified(dispatch.restart): + def handle(self, req): + req.status(304, "Not Modified") + req.ohead["Content-Length"] = "0" + return [] diff --git a/wrw/util.py b/wrw/util.py index cfc2c8c..abf865e 100644 --- a/wrw/util.py +++ b/wrw/util.py @@ -1,5 +1,5 @@ -import inspect -import req, dispatch, session, form, resp +import inspect, math +import req, dispatch, session, form, resp, proto def wsgiwrap(callable): def wrapper(env, startreq): @@ -233,3 +233,10 @@ class specdirty(sessiondata): ss[i] = specslot.unbound else: ss[i] = val + +def datecheck(req, mtime): + if "If-Modified-Since" in req.ihead: + rtime = proto.phttpdate(req.ihead["If-Modified-Since"]) + if rtime >= math.floor(mtime): + raise resp.unmodified() + req.ohead["Last-Modified"] = proto.httpdate(mtime) -- 2.11.0 From 0f18b7748229b3b899cbc9b7b320d10cd5a668ed Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Tue, 4 Jun 2013 15:18:52 +0200 Subject: [PATCH 08/16] Save references to wrapped functions. --- wrw/dispatch.py | 1 + wrw/util.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/wrw/dispatch.py b/wrw/dispatch.py index 4d22b72..1dedbe6 100644 --- a/wrw/dispatch.py +++ b/wrw/dispatch.py @@ -24,6 +24,7 @@ def defaulterror(req, excinfo): def wraphandler(handler, excinfo): def wrapped(req): return handler(req, excinfo) + wrapped.__wrapped__ = handler return wrapped errorhandler = env.var(defaulterror) diff --git a/wrw/util.py b/wrw/util.py index abf865e..e601be3 100644 --- a/wrw/util.py +++ b/wrw/util.py @@ -4,6 +4,7 @@ import req, dispatch, session, form, resp, proto def wsgiwrap(callable): def wrapper(env, startreq): return dispatch.handleenv(env, startreq, callable) + wrapper.__wrapped__ = callable return wrapper def formparams(callable): @@ -20,6 +21,7 @@ def formparams(callable): if spec.args[i] not in args: raise resp.httperror(400, "Missing parameter", ("The query parameter `", resp.h.code(spec.args[i]), "' is required but not supplied.")) return callable(**args) + wrapper.__wrapped__ = callable return wrapper def funplex(*funs, **nfuns): @@ -55,6 +57,7 @@ def persession(data = None): sess[data] = data() sess[callable] = callable(data) return sess[callable].handle(req) + wrapper.__wrapped__ = callable return wrapper return dec @@ -87,6 +90,7 @@ class preiter(object): def pregen(callable): def wrapper(*args, **kwargs): return preiter(callable(*args, **kwargs)) + wrapper.__wrapped__ = callable return wrapper class sessiondata(object): -- 2.11.0 From 24e514f00716cf4dd5fae2930d5f32e59bde2422 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Tue, 4 Jun 2013 15:21:56 +0200 Subject: [PATCH 09/16] Try to unwrap functions passed to funplex. --- wrw/util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/wrw/util.py b/wrw/util.py index e601be3..4306e9b 100644 --- a/wrw/util.py +++ b/wrw/util.py @@ -25,8 +25,12 @@ def formparams(callable): return wrapper def funplex(*funs, **nfuns): + def unwrap(fun): + while hasattr(fun, "__wrapped__"): + fun = fun.__wrapped__ + return fun dir = {} - dir.update(((fun.__name__, fun) for fun in funs)) + dir.update(((unwrap(fun).__name__, fun) for fun in funs)) dir.update(nfuns) def handler(req): if req.pathinfo == "": -- 2.11.0 From d22f3483df089fc239b4182807c12aaab5ab1c7d Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Tue, 4 Jun 2013 15:30:59 +0200 Subject: [PATCH 10/16] Set __wrapped__ in xhtmlresp as well. --- wrw/sp/xhtml.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wrw/sp/xhtml.py b/wrw/sp/xhtml.py index 3c6c0f6..447c1b1 100644 --- a/wrw/sp/xhtml.py +++ b/wrw/sp/xhtml.py @@ -58,4 +58,5 @@ def forreq(req, tree): def xhtmlresp(callable): def wrapper(req): return forreq(req, callable(req)) + wrapper.__wrapped__ = callable return wrapper -- 2.11.0 From 612eb9f52dd4bc45b16bdcfcecbedec567df3155 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Tue, 4 Jun 2013 16:11:37 +0200 Subject: [PATCH 11/16] Replaced funplex with a more flexible class implementation. --- wrw/util.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/wrw/util.py b/wrw/util.py index 4306e9b..759c295 100644 --- a/wrw/util.py +++ b/wrw/util.py @@ -24,15 +24,19 @@ def formparams(callable): wrapper.__wrapped__ = callable return wrapper -def funplex(*funs, **nfuns): +class funplex(object): + def __init__(self, *funs, **nfuns): + self.dir = {} + self.dir.update(((self.unwrap(fun).__name__, fun) for fun in funs)) + self.dir.update(nfuns) + + @staticmethod def unwrap(fun): while hasattr(fun, "__wrapped__"): fun = fun.__wrapped__ return fun - dir = {} - dir.update(((unwrap(fun).__name__, fun) for fun in funs)) - dir.update(nfuns) - def handler(req): + + def __call__(self, req): if req.pathinfo == "": raise resp.redirect(req.uriname + "/") if req.pathinfo[:1] != "/": @@ -44,10 +48,19 @@ def funplex(*funs, **nfuns): else: p = p.partition("/")[0] bi = len(p) + 1 - if p in dir: - return dir[p](req.shift(bi)) + if p in self.dir: + return self.dir[p](req.shift(bi)) raise resp.notfound() - return handler + + def add(self, fun): + self.dir[self.unwrap(fun).__name__] = fun + return fun + + def name(self, name): + def dec(fun): + self.dir[name] = fun + return fun + return dec def persession(data = None): def dec(callable): -- 2.11.0 From 98cc090c6a4c3d87e5ee411c056e60f17da31e76 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Wed, 5 Jun 2013 04:42:07 +0200 Subject: [PATCH 12/16] Fixed message bug. --- wrw/resp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrw/resp.py b/wrw/resp.py index 159126a..e901988 100644 --- a/wrw/resp.py +++ b/wrw/resp.py @@ -42,7 +42,7 @@ class message(dispatch.restart): self.detail = detail def handle(self, req): - return skelfor(req).error(req, self.message, *self.detail) + return skelfor(req).message(req, self.message, *self.detail) class httperror(usererror): def __init__(self, status, message = None, detail = None): -- 2.11.0 From a7b35f84508eb5ab8a890065275e5a0f3bc5bef5 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Wed, 5 Jun 2013 12:58:02 +0200 Subject: [PATCH 13/16] Set Content-Length in a couple of places. --- wrw/resp.py | 1 + wrw/sp/xhtml.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/wrw/resp.py b/wrw/resp.py index 24896ba..a27a143 100644 --- a/wrw/resp.py +++ b/wrw/resp.py @@ -70,4 +70,5 @@ class redirect(dispatch.restart): def handle(self, req): req.status(self.status, "Redirect") req.ohead["Location"] = proto.appendurl(proto.requrl(req), self.url) + req.ohead["Content-Length"] = 0 return [] diff --git a/wrw/sp/xhtml.py b/wrw/sp/xhtml.py index 3c6c0f6..d19fc99 100644 --- a/wrw/sp/xhtml.py +++ b/wrw/sp/xhtml.py @@ -53,7 +53,9 @@ def forreq(req, tree): req.ohead["Content-Type"] = "text/html; charset=utf-8" buf = StringIO.StringIO() htmlindenter.output(buf, tree, doctype=(doctype, dtd), charset="utf-8") - return [buf.getvalue()] + ret = buf.getvalue() + req.ohead["Content-Length"] = len(ret) + return [ret] def xhtmlresp(callable): def wrapper(req): -- 2.11.0 From 22dd92278609d35740c8f4b545b994bd19b19533 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Wed, 26 Jun 2013 05:38:40 +0200 Subject: [PATCH 14/16] Fixed formparams bug when defaults are missing. --- wrw/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrw/util.py b/wrw/util.py index 759c295..aea3037 100644 --- a/wrw/util.py +++ b/wrw/util.py @@ -17,7 +17,7 @@ def formparams(callable): for arg in list(args): if arg not in spec.args: del args[arg] - for i in xrange(len(spec.args) - len(spec.defaults)): + for i in xrange(len(spec.args) - (len(spec.defaults) if spec.defaults else 0)): if spec.args[i] not in args: raise resp.httperror(400, "Missing parameter", ("The query parameter `", resp.h.code(spec.args[i]), "' is required but not supplied.")) return callable(**args) -- 2.11.0 From f639ac812fc618d4b45d419dc0a415a2e53ddf6e Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Thu, 27 Jun 2013 07:48:34 +0200 Subject: [PATCH 15/16] Cache the target argspec in formparams. --- wrw/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrw/util.py b/wrw/util.py index aea3037..4fad6e9 100644 --- a/wrw/util.py +++ b/wrw/util.py @@ -8,9 +8,9 @@ def wsgiwrap(callable): return wrapper def formparams(callable): + spec = inspect.getargspec(callable) def wrapper(req): data = form.formdata(req) - spec = inspect.getargspec(callable) args = dict(data.items()) args["req"] = req if not spec.keywords: -- 2.11.0 From 3985f4bbbfd3740d955ec766fb5fb912efcb0d59 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Thu, 29 Aug 2013 10:53:21 +0200 Subject: [PATCH 16/16] Quote some more URL characters --- wrw/proto.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wrw/proto.py b/wrw/proto.py index 4cf1951..8c8dcee 100644 --- a/wrw/proto.py +++ b/wrw/proto.py @@ -103,8 +103,9 @@ def simpleerror(env, startreq, code, title, msg): def urlq(url): ret = "" + invalid = "&=#?/\"'" for c in url: - if c == "&" or c == "=" or c == "#" or c == "?" or c == "/" or (ord(c) <= 32): + if c in invalid or (ord(c) <= 32): ret += "%%%02X" % ord(c) else: ret += c -- 2.11.0