# HG changeset patch # User Steve Losh # Date 1256167934 14400 # Node ID 26150fdd04b848c68633be6c574d5903098c2004 # Parent 82f04bfc46b87cebf3644fd9b577c7234c98b8ed# Parent 5c274209a7ecfd83648d48a9109784a7b027d4d6 Merge the webui into the mainline. diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/.gitignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/.gitignore Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,2 @@ +*.pyc +.DS_Store \ No newline at end of file diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/ChangeLog.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/ChangeLog.txt Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,293 @@ +# web.py changelog + +## 2009-06-04 0.32 + +* optional from_address to web.emailerrors +* upgrade wsgiserver to CherryPy/3.1.2 +* support for extensions in Jinja2 templates (tx Zhang Huangbin) +* support web.datestr for datetime.date objects also +* support for lists in db queries +* new: uniq and iterview +* fix: set debug=False when application is run with mod_wsgi (tx Patrick Swieskowski) [Bug#370904](https://bugs.launchpad.net/webpy/+bug/370904) +* fix: make web.commify work with decimals [Bug#317204](https://bugs.launchpad.net/webpy/+bug/317204) +* fix: unicode issues with sqlite database [Bug#373219](https://bugs.launchpad.net/webpy/+bug/373219) +* fix: urlquote url when the server is lighttpd [Bug#339858](https://bugs.launchpad.net/webpy/+bug/339858) +* fix: issue with using date.format in templates +* fix: use TOP instead of LIMIT in mssql database [Bug#324049](https://bugs.launchpad.net/webpy/+bug/324049) +* fix: make sessions work well with expirations +* fix: accept both list and tuple as arg values in form.Dropdown [Bug#314970](https://bugs.launchpad.net/webpy/+bug/314970) +* fix: match parenthesis when parsing `for` statement in templates +* fix: fix python 2.3 compatibility +* fix: ignore dot folders when compiling templates (tx Stuart Langridge) +* fix: don't consume KeyboardInterrupt and SystemExit errors +* fix: make application work well with iterators + +## 2008-12-10: 0.31 + +* new: browser module +* new: test utilities +* new: ShelfStore +* fix: web.cookies error when default is None +* fix: paramstyle for OracleDB (tx kromakey) +* fix: performance issue in SQLQuery.join +* fix: use wsgi.url_scheme to find ctx.protocol + +## 2008-12-06: 0.3 + +* new: replace print with return (backward-incompatible) +* new: application framework (backward-incompatible) +* new: modular database system (backward-incompatible) +* new: templetor reimplementation +* new: better unicode support +* new: debug mode (web.config.debug) +* new: better db pooling +* new: sessions +* new: support for GAE +* new: etag support +* new: web.openid module +* new: web.nthstr +* fix: various form.py fixes +* fix: python 2.6 compatibility +* fix: file uploads are not loaded into memory +* fix: SQLLiteral issue (Bug#180027) +* change: web.background is moved to experimental (backward-incompatible) +* improved API doc generation (tx Colin Rothwell) + +## 2008-01-19: 0.23 + +* fix: for web.background gotcha ([133079](http://bugs.launchpad.net/webpy/+bug/133079)) +* fix: for postgres unicode bug ([177265](http://bugs.launchpad.net/webpy/+bug/177265)) +* fix: web.profile behavior in python 2.5 ([133080](http://bugs.launchpad.net/webpy/+bug/133080)) +* fix: only uppercase HTTP methods are allowed. ([176415](http://bugs.launchpad.net/webpy/+bug/176415)) +* fix: transaction error in with statement ([125118](http://bugs.launchpad.net/webpy/+bug/125118)) +* fix: fix in web.reparam ([162085](http://bugs.launchpad.net/webpy/+bug/162085)) +* fix: various unicode issues ([137042](http://bugs.launchpad.net/webpy/+bug/137042), [180510](http://bugs.launchpad.net/webpy/+bug/180510), [180549](http://bugs.launchpad.net/webpy/+bug/180549), [180653](http://bugs.launchpad.net/webpy/+bug/180653)) +* new: support for https +* new: support for secure cookies +* new: sendmail +* new: htmlunquote + +## 2007-08-23: 0.22 + +* compatibility with new DBUtils API ([122112](https://bugs.launchpad.net/webpy/+bug/122112)) +* fix reloading ([118683](https://bugs.launchpad.net/webpy/+bug/118683)) +* fix compatibility between `changequery` and `redirect` ([118234](https://bugs.launchpad.net/webpy/+bug/118234)) +* fix relative URI in `web.redirect` ([118236](https://bugs.launchpad.net/webpy/+bug/118236)) +* fix `ctx._write` support in built-in HTTP server ([121908](https://bugs.launchpad.net/webpy/+bug/121908)) +* fix `numify` strips things after '.'s ([118644](https://bugs.launchpad.net/webpy/+bug/118644)) +* fix various unicode isssues ([114703](https://bugs.launchpad.net/webpy/+bug/114703), [120644](https://bugs.launchpad.net/webpy/+bug/120644), [124280](https://bugs.launchpad.net/webpy/+bug/124280)) + +## 2007-05-28: 0.21 + +* security fix: prevent bad characters in headers +* support for cheetah template reloading +* support for form validation +* new `form.File` +* new `web.url` +* fix rendering issues with hidden and button inputs +* fix 2.3 incompatability with `numify` +* fix multiple headers with same name +* fix web.redirect issues when homepath is not / +* new CherryPy wsgi server +* new nested transactions +* new sqlliteral + +## 2006-05-09: 0.138 + +* New function: `intget` +* New function: `datestr` +* New function: `validaddr` +* New function: `sqlwhere` +* New function: `background`, `backgrounder` +* New function: `changequery` +* New function: `flush` +* New function: `load`, `unload` +* New variable: `loadhooks`, `unloadhooks` +* Better docs; generating [docs](documentation) from web.py now +* global variable `REAL_SCRIPT_NAME` can now be used to work around lighttpd madness +* fastcgi/scgi servers now can listen on sockets +* `output` now encodes Unicode +* `input` now takes optional `_method` argument +* Potentially-incompatible change: `input` now returns `badrequest` automatically when `requireds` aren't found +* `storify` now takes lists and dictionaries as requests (see docs) +* `redirect` now blanks any existing output +* Quote SQL better when `db_printing` is on +* Fix delay in `nomethod` +* Fix `urlquote` to encode better. +* Fix 2.3 incompatibility with `iters` (tx ??) +* Fix duplicate headers +* Improve `storify` docs +* Fix `IterBetter` to raise IndexError, not KeyError + +## 2006-03-27: 0.137 + +* Add function `dictfindall` (tx Steve Huffman) +* Add support to `autodelegate` for arguments +* Add functions `httpdate` and `parsehttpdate` +* Add function `modified` +* Add support for FastCGI server mode +* Clarify `dictadd` documentation (tx Steve Huffman) +* Changed license to public domain +* Clean up to use `ctx` and `env` instead of `context` and `environ` +* Improved support for PUT, DELETE, etc. (tx list) +* Fix `ctx.fullpath` (tx Jesir Vargas) +* Fix sqlite support (tx Dubhead) +* Fix documentation bug in `lstrips` (tx Gregory Petrosyan) +* Fix support for IPs and ports (1/2 tx Jesir Vargas) +* Fix `ctx.fullpath` (tx Jesir Vargas) +* Fix sqlite support (tx Dubhead) +* Fix documentation bug in `lstrips` (tx Gregory Petrosyan) +* Fix `iters` bug with sets +* Fix some breakage introduced by Vargas's patch +* Fix `sqlors` bug +* Fix various small style things (tx Jesir Vargas) +* Fix bug with `input` ignoring GET input + +## 2006-02-22: 0.136 (svn) + +* Major code cleanup (tx to Jesir Vargas for the patch). +* 2006-02-15: 0.135 +* Really fix that mysql regression (tx Sean Leach). +* 2006-02-15: 0.134 +* The `StopIteration` exception is now caught. This can be used by functions that do things like check to see if a user is logged in. If the user isn't, they can output a message with a login box and raise StopIteration, preventing the caller from executing. +* Fix some documentation bugs. +* Fix mysql regression (tx mrstone). + +## 2006-02-12: 0.133 + +* Docstrings! (tx numerous, esp. Jonathan Mark (for the patch) and Guido van Rossum (for the prod)) +* Add `set` to web.iters. +* Make the `len` returned by `query` an int (tx ??). +* Backwards-incompatible change: `base` now called `prefixurl`. +* Backwards-incompatible change: `autoassign` now takes `self` and `locals()` as arguments. + +## 2006-02-07: 0.132 + +* New variable `iters` is now a listing of possible list-like types (currently list, tuple, and, if it exists, Set). +* New function `dictreverse` turns `{1:2}` into `{2:1}`. +* `Storage` now a dictionary subclass. +* `tryall` now takes an optional prefix of functions to run. +* `sqlors` has various improvements. +* Fix a bunch of DB API bugs. +* Fix bug with `storify` when it received multiple inputs (tx Ben Woosley). +* Fix bug with returning a generator (tx Zbynek Winkler). +* Fix bug where len returned a long on query results (tx F.S). + + +## 2006-01-31: 0.131 (not officially released) + +* New function `_interpolate` used internally for interpolating strings. +* Redone database API. `select`, `insert`, `update`, and `delete` all made consistent. Database queries can now do more complicated expressions like `$foo.bar` and `${a+b}`. You now have to explicitly pass the dictionary to look up variables in. Pass `vars=locals()` to get the old functionality of looking up variables . +* New functions `sqllist` and `sqlors` generate certain kinds of SQL. + +## 2006-01-30: 0.13 + +* New functions `found`, `seeother`, and `tempredirect` now let you do other kinds of redirects. `redirect` now also takes an optional status parameter. (tx many) +* New functions `expires` and `lastmodified` make it easy to send those headers. +* New function `gone` returns a 410 Gone (tx David Terrell). +* New function `urlquote` applies url encoding to a string. +* New function `iterbetter` wraps an iterator and allows you to do __getitem__s on it. +* Have `query` return an `iterbetter` instead of an iterator. +* Have `debugerror` show tracebacks with the innermost frame first. +* Add `__hash__` function to `threadeddict` (and thus, `ctx`). +* Add `context.host` value for the requested host name. +* Add option `db_printing` that prints database queries and the time they take. +* Add support for database pooling (tx Steve Huffman). +* Add support for passing values to functions called by `handle`. If you do `('foo', 'value')` it will add `'value'` as an argument when it calls `foo`. +* Add support for scgi (tx David Terrell for the patch). +* Add support for web.py functions that are iterators (tx Brendan O'Connor for the patch). +* Use new database cursors on each call instead of reusing one. +* `setcookie` now takes an optional `domain` argument. +* Fix bug in autoassign. +* Fix bug where `debugerror` would break on objects it couldn't display. +* Fix bug where you couldn't do `#include`s inline. +* Fix bug with `reloader` and database calls. +* Fix bug with `reloader` and base templates. +* Fix bug with CGI mode on certain operating systems. +* Fix bug where `debug` would crash if called outside a request. +* Fix bug with `context.ip` giving weird values with proxies. + +## 2006-01-29: 0.129 + +* Add Python 2.2 support. + +## 2006-01-28: 0.128 + +* Fix typo in `web.profile`. + +## 2006-01-28: 0.127 + +* Fix bug in error message if invalid dbn is sent (tx Panos Laganakos). + +## 2006-01-27: 0.126 + +* Fix typos in Content-Type headers (tx Beat Bolli for the prod). + +## 2006-01-22: 0.125 + +* Support Cheetah 2.0. + +## 2006-01-22: 0.124 + +* Fix spacing bug (tx Tommi Raivio for the prod). + +## 2006-01-16: 0.123 + +* Fix bug with CGI usage (tx Eddie Sowden for the prod). + +## 2006-01-14: 0.122 + +* Allow DELETEs from `web.query` (tx Joost Molenaar for the prod). + +## 2006-01-08: 0.121 + +* Allow import of submodules like `pkg.mod.cn` (tx Sridhar Ratna). +* Fix a bug in `update` (tx Sergey Khenkin). + +## 2006-01-05: 0.12 + +* Backwards-incompatible change: `db_parameters` is now a dictionary. +* Backwards-incompatible change: `sumdicts` is now `dictadd`. +* Add support for PyGreSQL, MySQL (tx Hallgrimur H. Gunnarsson). +* Use HTML for non-Cheetah error message. +* New function `htmlquote()`. +* New function `tryall()`. +* `ctx.output` can now be set to a generator. (tx Brendan O'Connor) + +## 2006-01-04: 0.117 + +* Add support for psycopg 1.x. (tx Gregory Price) + +## 2006-01-04: 0.116 + +* Add support for Python 2.3. (tx Evan Jones) + +## 2006-01-04: 0.115 + +* Fix some bugs where database queries weren't reparameterized. Oops! +* Fix a bug where `run()` wasn't getting the right functions. +* Remove a debug statement accidentally left in. +* Allow `storify` to be used on dictionaries. (tx Joseph Trent) + +## 2006-01-04: 0.114 + +* Make `reloader` work on Windows. (tx manatlan) +* Fix some small typos that affected colorization. (tx Gregory Price) + +## 2006-01-03: 0.113 + +* Reorganize `run()` internals so mod_python can be used. (tx Nicholas Matsakis) + +## 2006-01-03: 0.112 + +* Make `reloader` work when `code.py` is called with a full path. (tx David Terrell) + +## 2006-01-03: 0.111 + +* Fixed bug in `strips()`. (tx Michael Josephson) + +## 2006-01-03: 0.11 + +* First public version. + + diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/LICENSE.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/LICENSE.txt Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,4 @@ +web.py is in the public domain; it can be used for whatever purpose with absolutely no restrictions. + +CherryPy WSGI server that is included in the web.py as web.wsgiserver is licensed under CherryPy License. See web/wsgiserver/LICENSE.txt for more details. + diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/experimental/background.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/experimental/background.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,43 @@ +"""Helpers functions to run log-running tasks.""" +from web import utils +from web import webapi as web + +def background(func): + """A function decorator to run a long-running function as a background thread.""" + def internal(*a, **kw): + web.data() # cache it + + tmpctx = web._context[threading.currentThread()] + web._context[threading.currentThread()] = utils.storage(web.ctx.copy()) + + def newfunc(): + web._context[threading.currentThread()] = tmpctx + func(*a, **kw) + myctx = web._context[threading.currentThread()] + for k in myctx.keys(): + if k not in ['status', 'headers', 'output']: + try: del myctx[k] + except KeyError: pass + + t = threading.Thread(target=newfunc) + background.threaddb[id(t)] = t + t.start() + web.ctx.headers = [] + return seeother(changequery(_t=id(t))) + return internal +background.threaddb = {} + +def backgrounder(func): + def internal(*a, **kw): + i = web.input(_method='get') + if '_t' in i: + try: + t = background.threaddb[int(i._t)] + except KeyError: + return web.notfound() + web._context[threading.currentThread()] = web._context[t] + return + else: + return func(*a, **kw) + return internal + diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/experimental/migration.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/experimental/migration.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,99 @@ +"""Migration script to run web.py 0.23 programs using 0.3. + +Import this module at the beginning of your program. +""" +import web +import sys + +def setup_database(): + if web.config.get('db_parameters'): + db = web.database(**web.config.db_parameters) + web.insert = db.insert + web.select = db.select + web.update = db.update + web.delete = db.delete + web.query = db.query + + def transact(): + t = db.transaction() + web.ctx.setdefault('transaction_stack', []).append(t) + + def rollback(): + stack = web.ctx.get('transaction_stack') + t = stack and stack.pop() + t and t.rollback() + + def commit(): + stack = web.ctx.get('transaction_stack') + t = stack and stack.pop() + t and t.commit() + + web.transact = transact + web.rollback = rollback + web.commit = commit + +web.loadhooks = web.webapi.loadhooks = {} +web._loadhooks = web.webapi._loadhooks = {} +web.unloadhooks = web.webapi.unloadhooks = {} + +def load(): + setup_database() + +web.load = load + +def run(urls, fvars, *middleware): + setup_database() + + def stdout_processor(handler): + handler() + return web.ctx.get('output', '') + + def hook_processor(handler): + for h in web.loadhooks.values() + web._loadhooks.values(): h() + output = handler() + for h in web.unloadhooks.values(): h() + return output + + app = web.application(urls, fvars) + app.add_processor(stdout_processor) + app.add_processor(hook_processor) + app.run(*middleware) + +class _outputter: + """Wraps `sys.stdout` so that print statements go into the response.""" + def __init__(self, file): self.file = file + def write(self, string_): + if hasattr(web.ctx, 'output'): + return output(string_) + else: + self.file.write(string_) + def __getattr__(self, attr): return getattr(self.file, attr) + def __getitem__(self, item): return self.file[item] + +def output(string_): + """Appends `string_` to the response.""" + string_ = web.utf8(string_) + if web.ctx.get('flush'): + web.ctx._write(string_) + else: + web.ctx.output += str(string_) + +def _capturedstdout(): + sysstd = sys.stdout + while hasattr(sysstd, 'file'): + if isinstance(sys.stdout, _outputter): return True + sysstd = sysstd.file + if isinstance(sys.stdout, _outputter): return True + return False + +if not _capturedstdout(): + sys.stdout = _outputter(sys.stdout) + +web.run = run + +class Stowage(web.storage): + def __str__(self): + return self._str + +web.template.Stowage = web.template.stowage = Stowage + diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/experimental/pwt.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/experimental/pwt.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,96 @@ +import web +import simplejson, sudo +urls = ( + '/sudo', 'sudoku', + '/length', 'length', +) + + +class pwt(object): + _inFunc = False + updated = {} + page = """ + + + + +
%s
+ + +""" + + def GET(self): + web.header('Content-Type', 'text/html') + print self.page % self.form() + + def POST(self): + i = web.input() + if '_' in i: del i['_'] + #for k, v in i.iteritems(): setattr(self, k, v) + + self._inFunc = True + self.work(**i) + self._inFunc = False + + web.header('Content-Type', 'text/javascript') + print 'receive('+simplejson.dumps(self.updated)+');' + + def __setattr__(self, k, v): + if self._inFunc and k != '_inFunc': + self.updated[k] = v + object.__setattr__(self, k, v) + +class sudoku(pwt): + def form(self): + import sudo + out = '' + n = 0 + for i in range(9): + for j in range(9): + out += '' % (sudo.squares[n]) + n += 1 + out += '
' + + return out + + def work(self, **kw): + values = dict((s, sudo.digits) for s in sudo.squares) + for k, v in kw.iteritems(): + if v: + sudo.assign(values, k, v) + + for k, v in values.iteritems(): + if len(v) == 1: + setattr(self, k, v) + + return values + +class length(pwt): + def form(self): + return '

 

' + + def work(self): + self.output = ('a' * web.intget(self.n, 0) or ' ') + +if __name__ == "__main__": + web.run(urls, globals(), web.reloader) \ No newline at end of file diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/experimental/untwisted.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/experimental/untwisted.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,134 @@ +import random + +from twisted.internet import reactor, defer +from twisted.web import http + +import simplejson + +import web + +class Request(http.Request): + def process(self): + self.content.seek(0, 0) + env = { + 'REMOTE_ADDR': self.client.host, + 'REQUEST_METHOD': self.method, + 'PATH_INFO': self.path, + 'CONTENT_LENGTH': web.intget(self.getHeader('content-length'), 0), + 'wsgi.input': self.content + } + if '?' in self.uri: + env['QUERY_STRING'] = self.uri.split('?', 1)[1] + + for k, v in self.received_headers.iteritems(): + env['HTTP_' + k.upper()] = v + + if self.path.startswith('/static/'): + f = web.lstrips(self.path, '/static/') + assert '/' not in f + #@@@ big security hole + self.write(file('static/' + f).read()) + return self.finish() + + web.webapi._load(env) + web.ctx.trequest = self + result = self.actualfunc() + self.setResponseCode(int(web.ctx.status.split()[0])) + for (h, v) in web.ctx.headers: + self.setHeader(h, v) + self.write(web.ctx.output) + if not web.ctx.get('persist'): + self.finish() + +class Server(http.HTTPFactory): + def __init__(self, func): + self.func = func + + def buildProtocol(self, addr): + """Generate a channel attached to this site. + """ + channel = http.HTTPFactory.buildProtocol(self, addr) + class MyRequest(Request): + actualfunc = staticmethod(self.func) + channel.requestFactory = MyRequest + channel.site = self + return channel + +def runtwisted(func): + reactor.listenTCP(8086, Server(func)) + reactor.run() + +def newrun(inp, fvars): + print "Running on http://0.0.0.0:8086/" + runtwisted(web.webpyfunc(inp, fvars, False)) + +def iframe(url): + return """ + + """ % url #("http://%s.ajaxpush.lh.theinfo.org:8086%s" % (random.random(), url)) + +class Feed: + def __init__(self): + self.sessions = [] + + def subscribe(self): + request = web.ctx.trequest + self.sessions.append(request) + request.connectionLost = lambda reason: self.sessions.remove(request) + web.ctx.persist = True + + def publish(self, text): + for x in self.sessions: + x.write(text) + +class JSFeed(Feed): + def __init__(self, callback="callback"): + Feed.__init__(self) + self.callback = callback + + def publish(self, obj): + web.debug("publishing") + Feed.publish(self, + '' % (self.callback, simplejson.dumps(obj) + + " " * 2048)) + +if __name__ == "__main__": + mfeed = JSFeed() + + urls = ( + '/', 'view', + '/js', 'js', + '/send', 'send' + ) + + class view: + def GET(self): + print """ + + +

Today's News

+ +
+ +

Contribute

+
+ + +
+ + """ + + class js: + def GET(self): + mfeed.subscribe() + + class send: + def POST(self): + mfeed.publish('

%s

' % web.input().text + (" " * 2048)) + web.seeother('/') + + newrun(urls, globals()) \ No newline at end of file diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/setup.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +# ... + +from distutils.core import setup + +setup(name='web.py', + version='0.32', + description='web.py: makes web apps', + author='Aaron Swartz', + author_email='me@aaronsw.com', + maintainer='Anand Chitipothu', + maintainer_email='anandology@gmail.com', + url=' http://webpy.org/', + packages=['web', 'web.wsgiserver', 'web.contrib'], + long_description="Think about the ideal way to write a web app. Write the code to make it happen.", + license="Public domain", + platforms=["any"], + ) diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/test/README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/test/README Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,27 @@ +# web.py unit tests + +## Setup + +All databases expect a database with name `webpy` with username `scott` and password `tiger`. + +## Running all tests + +To run all tests: + + $ python test/alltests.py + +## Running individual tests + +To run all tests in a file: + + $ python test/db.py + +To run all tests in a class: + + $ python test/db.py SqliteTest + +To run a single test: + + $ python test/db.py SqliteTest.testUnicode + + diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/test/__init__.py diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/test/alltests.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/test/alltests.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,8 @@ +import webtest + +def suite(): + modules = ["doctests", "db", "application", "session"] + return webtest.suite(modules) + +if __name__ == "__main__": + webtest.main() diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/test/application.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/test/application.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,297 @@ +import webtest +import time + +import web +import urllib + +data = """ +import web + +urls = ("/", "%(classname)s") +app = web.application(urls, globals(), autoreload=True) + +class %(classname)s: + def GET(self): + return "%(output)s" + +""" + +urls = ( + "/iter", "do_iter", +) +app = web.application(urls, globals()) + +class do_iter: + def GET(self): + yield 'hello, ' + yield web.input(name='world').name + + POST = GET + +def write(filename, data): + f = open(filename, 'w') + f.write(data) + f.close() + +class ApplicationTest(webtest.TestCase): + def test_reloader(self): + write('foo.py', data % dict(classname='a', output='a')) + import foo + app = foo.app + + self.assertEquals(app.request('/').data, 'a') + + # test class change + time.sleep(1) + write('foo.py', data % dict(classname='a', output='b')) + self.assertEquals(app.request('/').data, 'b') + + # test urls change + time.sleep(1) + write('foo.py', data % dict(classname='c', output='c')) + self.assertEquals(app.request('/').data, 'c') + + def testUppercaseMethods(self): + urls = ("/", "hello") + app = web.application(urls, locals()) + class hello: + def GET(self): return "hello" + def internal(self): return "secret" + + response = app.request('/', method='internal') + self.assertEquals(response.status, '405 Method Not Allowed') + + def testRedirect(self): + urls = ( + "/a", "redirect /hello/", + "/b/(.*)", r"redirect /hello/\1", + "/hello/(.*)", "hello" + ) + app = web.application(urls, locals()) + class hello: + def GET(self, name): + name = name or 'world' + return "hello " + name + + response = app.request('/a') + self.assertEquals(response.status, '301 Moved Permanently') + self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/hello/') + + response = app.request('/a?x=2') + self.assertEquals(response.status, '301 Moved Permanently') + self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/hello/?x=2') + + response = app.request('/b/foo?x=2') + self.assertEquals(response.status, '301 Moved Permanently') + self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/hello/foo?x=2') + + def test_subdirs(self): + urls = ( + "/(.*)", "blog" + ) + class blog: + def GET(self, path): + return "blog " + path + app_blog = web.application(urls, locals()) + + urls = ( + "/blog", app_blog, + "/(.*)", "index" + ) + class index: + def GET(self, path): + return "hello " + path + app = web.application(urls, locals()) + + self.assertEquals(app.request('/blog/foo').data, 'blog foo') + self.assertEquals(app.request('/foo').data, 'hello foo') + + def processor(handler): + return web.ctx.path + ":" + handler() + app.add_processor(processor) + self.assertEquals(app.request('/blog/foo').data, '/blog/foo:blog foo') + + def test_subdomains(self): + def create_app(name): + urls = ("/", "index") + class index: + def GET(self): + return name + return web.application(urls, locals()) + + urls = ( + "a.example.com", create_app('a'), + "b.example.com", create_app('b'), + ".*.example.com", create_app('*') + ) + app = web.subdomain_application(urls, locals()) + + def test(host, expected_result): + result = app.request('/', host=host) + self.assertEquals(result.data, expected_result) + + test('a.example.com', 'a') + test('b.example.com', 'b') + test('c.example.com', '*') + test('d.example.com', '*') + + def test_redirect(self): + urls = ( + "/(.*)", "blog" + ) + class blog: + def GET(self, path): + if path == 'foo': + raise web.seeother('/login', absolute=True) + else: + raise web.seeother('/bar') + app_blog = web.application(urls, locals()) + + urls = ( + "/blog", app_blog, + "/(.*)", "index" + ) + class index: + def GET(self, path): + return "hello " + path + app = web.application(urls, locals()) + + response = app.request('/blog/foo') + self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/login') + + response = app.request('/blog/foo', env={'SCRIPT_NAME': '/x'}) + self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/x/login') + + response = app.request('/blog/foo2') + self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/blog/bar') + + response = app.request('/blog/foo2', env={'SCRIPT_NAME': '/x'}) + self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/x/blog/bar') + + def test_processors(self): + urls = ( + "/(.*)", "blog" + ) + class blog: + def GET(self, path): + return 'blog ' + path + + state = web.storage(x=0, y=0) + def f(): + state.x += 1 + + app_blog = web.application(urls, locals()) + app_blog.add_processor(web.loadhook(f)) + + urls = ( + "/blog", app_blog, + "/(.*)", "index" + ) + class index: + def GET(self, path): + return "hello " + path + app = web.application(urls, locals()) + def g(): + state.y += 1 + app.add_processor(web.loadhook(g)) + + app.request('/blog/foo') + assert state.x == 1 and state.y == 1, repr(state) + app.request('/foo') + assert state.x == 1 and state.y == 2, repr(state) + + def testUnicodeInput(self): + urls = ( + "(/.*)", "foo" + ) + class foo: + def GET(self, path): + i = web.input(name='') + return repr(i.name) + + def POST(self, path): + if path == '/multipart': + i = web.input(file={}) + return i.file.value + else: + i = web.input() + return repr(dict(i)) + + app = web.application(urls, locals()) + + def f(name): + path = '/?' + urllib.urlencode({"name": name.encode('utf-8')}) + self.assertEquals(app.request(path).data, repr(name)) + + f(u'\u1234') + f(u'foo') + + response = app.request('/', method='POST', data=dict(name='foo')) + self.assertEquals(response.data, "{'name': u'foo'}") + + data = '--boundary\r\nContent-Disposition: form-data; name="x"\r\nfoo\r\n--boundary\r\nContent-Disposition: form-data; name="file"; filename="a.txt"\r\nContent-Type: text/plain\r\n\r\na\r\n--boundary--\r\n' + headers = {'Content-Type': 'multipart/form-data; boundary=boundary'} + response = app.request('/multipart', method="POST", data=data, headers=headers) + self.assertEquals(response.data, 'a') + + def testCustomNotFound(self): + urls_a = ("/", "a") + urls_b = ("/", "b") + + app_a = web.application(urls_a, locals()) + app_b = web.application(urls_b, locals()) + + app_a.notfound = lambda: web.HTTPError("404 Not Found", {}, "not found 1") + + urls = ( + "/a", app_a, + "/b", app_b + ) + app = web.application(urls, locals()) + + def assert_notfound(path, message): + response = app.request(path) + self.assertEquals(response.status.split()[0], "404") + self.assertEquals(response.data, message) + + assert_notfound("/a/foo", "not found 1") + assert_notfound("/b/foo", "not found") + + app.notfound = lambda: web.HTTPError("404 Not Found", {}, "not found 2") + assert_notfound("/a/foo", "not found 1") + assert_notfound("/b/foo", "not found 2") + + def testIter(self): + self.assertEquals(app.request('/iter').data, 'hello, world') + self.assertEquals(app.request('/iter?name=web').data, 'hello, web') + + self.assertEquals(app.request('/iter', method='POST').data, 'hello, world') + self.assertEquals(app.request('/iter', method='POST', data='name=web').data, 'hello, web') + + def testUnload(self): + x = web.storage(a=0) + + urls = ( + "/foo", "foo", + "/bar", "bar" + ) + class foo: + def GET(self): + return "foo" + class bar: + def GET(self): + raise web.notfound() + + app = web.application(urls, locals()) + def unload(): + x.a += 1 + app.add_processor(web.unloadhook(unload)) + + app.request('/foo') + self.assertEquals(x.a, 1) + + app.request('/bar') + self.assertEquals(x.a, 2) + +if __name__ == '__main__': + webtest.main() diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/test/browser.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/test/browser.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,64 @@ +import webtest +import web + +urls = ( + "/", "index", + "/hello/(.*)", "hello", + "/cookie", "cookie", + "/setcookie", "setcookie", + "/redirect", "redirect", +) +app = web.application(urls, globals()) + +class index: + def GET(self): + return "welcome" + +class hello: + def GET(self, name): + name = name or 'world' + return "hello, " + name + '!' + +class cookie: + def GET(self): + return ",".join(sorted(web.cookies().keys())) + +class setcookie: + def GET(self): + i = web.input() + for k, v in i.items(): + web.setcookie(k, v) + return "done" + +class redirect: + def GET(self): + i = web.input(url='/') + raise web.seeother(i.url) + +class BrowserTest(webtest.TestCase): + def testCookies(self): + b = app.browser() + b.open('http://0.0.0.0/setcookie?x=1&y=2') + b.open('http://0.0.0.0/cookie') + self.assertEquals(b.data, 'x,y') + + def testNotfound(self): + b = app.browser() + b.open('http://0.0.0.0/notfound') + self.assertEquals(b.status, 404) + + def testRedirect(self): + b = app.browser() + + b.open('http://0.0.0.0:8080/redirect') + self.assertEquals(b.url, 'http://0.0.0.0:8080/') + b.open('http://0.0.0.0:8080/redirect?url=/hello/foo') + self.assertEquals(b.url, 'http://0.0.0.0:8080/hello/foo') + + b.open('https://0.0.0.0:8080/redirect') + self.assertEquals(b.url, 'https://0.0.0.0:8080/') + b.open('https://0.0.0.0:8080/redirect?url=/hello/foo') + self.assertEquals(b.url, 'https://0.0.0.0:8080/hello/foo') + +if __name__ == "__main__": + webtest.main() diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/test/db.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/test/db.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,162 @@ +"""DB test""" +import webtest +import web + +class DBTest(webtest.TestCase): + dbname = 'postgres' + driver = None + + def setUp(self): + self.db = webtest.setup_database(self.dbname, driver=self.driver) + self.db.query("CREATE TABLE person (name text, email text, active boolean)") + + def tearDown(self): + # there might be some error with the current connection, delete from a new connection + self.db = webtest.setup_database(self.dbname, driver=self.driver) + self.db.query('DROP TABLE person') + + def _testable(self): + try: + webtest.setup_database(self.dbname, driver=self.driver) + return True + except ImportError, e: + print >> web.debug, str(e), "(ignoring %s)" % self.__class__.__name__ + return False + + def testUnicode(self): + # Bug#177265: unicode queries throw errors + self.db.select('person', where='name=$name', vars={'name': u'\xf4'}) + + def assertRows(self, n): + result = self.db.select('person') + self.assertEquals(len(list(result)), n) + + def testCommit(self): + t = self.db.transaction() + self.db.insert('person', False, name='user1') + t.commit() + + t = self.db.transaction() + self.db.insert('person', False, name='user2') + self.db.insert('person', False, name='user3') + t.commit() + + self.assertRows(3) + + def testRollback(self): + t = self.db.transaction() + self.db.insert('person', False, name='user1') + self.db.insert('person', False, name='user2') + self.db.insert('person', False, name='user3') + t.rollback() + self.assertRows(0) + + def testWrongQuery(self): + # It should be possible to run a correct query after getting an error from a wrong query. + try: + self.db.select('notthere') + except: + pass + self.db.select('person') + + def testNestedTransactions(self): + t1 = self.db.transaction() + self.db.insert('person', False, name='user1') + self.assertRows(1) + + t2 = self.db.transaction() + self.db.insert('person', False, name='user2') + self.assertRows(2) + t2.rollback() + self.assertRows(1) + t3 = self.db.transaction() + self.db.insert('person', False, name='user3') + self.assertRows(2) + t3.commit() + t1.commit() + self.assertRows(2) + + def testPooling(self): + # can't test pooling if DBUtils is not installed + try: + import DBUtils + except ImportError: + return + db = webtest.setup_database(self.dbname, pooling=True) + self.assertEquals(db.ctx.db.__class__.__module__, 'DBUtils.PooledDB') + db.select('person', limit=1) + + def test_multiple_insert(self): + db = webtest.setup_database(self.dbname) + db.multiple_insert('person', [dict(name='a'), dict(name='b')], seqname=False) + + assert db.select("person", where="name='a'") + assert db.select("person", where="name='b'") + + def test_result_is_unicode(self): + db = webtest.setup_database(self.dbname) + self.db.insert('person', False, name='user') + name = db.select('person')[0].name + self.assertEquals(type(name), unicode) + + def testBoolean(self): + def t(active): + name ='name-%s' % active + self.db.insert('person', False, name=name, active=active) + a = self.db.select('person', where='name=$name', vars=locals())[0].active + self.assertEquals(a, active) + t(False) + t(True) + +class PostgresTest(DBTest): + dbname = "postgres" + driver = "psycopg2" + +class PostgresTest_psycopg(PostgresTest): + driver = "psycopg" + +class PostgresTest_pgdb(PostgresTest): + driver = "pgdb" + +class SqliteTest(DBTest): + dbname = "sqlite" + driver = "sqlite3" + + def testNestedTransactions(self): + #nested transactions does not work with sqlite + pass + +class SqliteTest_pysqlite2(SqliteTest): + driver = "pysqlite2.dbapi2" + +class MySQLTest(DBTest): + dbname = "mysql" + + def setUp(self): + self.db = webtest.setup_database(self.dbname) + # In mysql, transactions are supported only with INNODB engine. + self.db.query("CREATE TABLE person (name text, email text) ENGINE=INNODB") + + def testBoolean(self): + # boolean datatype is not suppoted in MySQL (at least until v5.0) + pass + +del DBTest + +def is_test(cls): + import inspect + return inspect.isclass(cls) and webtest.TestCase in inspect.getmro(cls) + +# ignore db tests when the required db adapter is not found. +for t in globals().values(): + if is_test(t) and not t('_testable')._testable(): + del globals()[t.__name__] +del t + +try: + import DBUtils +except ImportError, e: + print >> web.debug, str(e) + "(ignoring testPooling)" + +if __name__ == '__main__': + webtest.main() diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/test/doctests.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/test/doctests.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,20 @@ +"""Run all doctests in web.py. +""" +import webtest + +def suite(): + modules = [ + "web.application", + "web.db", + "web.http", + "web.net", + "web.session", + "web.template", + "web.utils", +# "web.webapi", +# "web.wsgi", + ] + return webtest.doctest_suite(modules) + +if __name__ == "__main__": + webtest.main() diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/test/session.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/test/session.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,74 @@ +import webtest +import web +import tempfile + +class SessionTest(webtest.TestCase): + def setUp(self): + app = web.auto_application() + session = self.make_session(app) + class count(app.page): + def GET(self): + session.count += 1 + return str(session.count) + + class reset(app.page): + def GET(self): + session.kill() + return "" + + self.app = app + self.session = session + + def make_session(self, app): + dir = tempfile.mkdtemp() + store = web.session.DiskStore(tempfile.mkdtemp()) + return web.session.Session(app, store, {'count': 0}) + + def testSession(self): + b = self.app.browser() + self.assertEquals(b.open('/count').read(), '1') + self.assertEquals(b.open('/count').read(), '2') + self.assertEquals(b.open('/count').read(), '3') + b.open('/reset') + self.assertEquals(b.open('/count').read(), '1') + + def testParallelSessions(self): + b1 = self.app.browser() + b2 = self.app.browser() + + b1.open('/count') + + for i in range(1, 10): + self.assertEquals(b1.open('/count').read(), str(i+1)) + self.assertEquals(b2.open('/count').read(), str(i)) + + def testBadSessionId(self): + b = self.app.browser() + self.assertEquals(b.open('/count').read(), '1') + self.assertEquals(b.open('/count').read(), '2') + + cookie = b.cookiejar._cookies['0.0.0.0']['/']['webpy_session_id'] + cookie.value = '/etc/password' + self.assertEquals(b.open('/count').read(), '1') + +class DBSessionTest(SessionTest): + """Session test with db store.""" + def make_session(self, app): + db = webtest.setup_database("postgres") + #db.printing = True + db.query("" + + "CREATE TABLE session (" + + " session_id char(128) unique not null," + + " atime timestamp default (current_timestamp at time zone 'utc')," + + " data text)" + ) + store = web.session.DBStore(db, 'session') + return web.session.Session(app, store, {'count': 0}) + + def tearDown(self): + # there might be some error with the current connection, delete from a new connection + self.db = webtest.setup_database("postgres") + self.db.query('DROP TABLE session') + +if __name__ == "__main__": + webtest.main() diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/test/webtest.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/test/webtest.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,20 @@ +"""webtest: test utilities. +""" +import sys, os + +# adding current directory to path to make sure local modules can be imported +sys.path.insert(0, '.') + +from web.test import * + +def setup_database(dbname, driver=None, pooling=False): + if dbname == 'sqlite': + db = web.database(dbn=dbname, db='webpy.db', pooling=pooling, driver=driver) + elif dbname == 'postgres': + user = os.getenv('USER') + db = web.database(dbn=dbname, db='webpy', user=user, pw='', pooling=pooling, driver=driver) + else: + db = web.database(dbn=dbname, db='webpy', user='scott', pw='tiger', pooling=pooling, driver=driver) + + db.printing = '-v' in sys.argv + return db diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/tools/_makedoc.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/tools/_makedoc.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,81 @@ +import os +import web + +class Parser: + def __init__(self): + self.mode = 'normal' + self.text = '' + + def go(self, pyfile): + for line in file(pyfile): + if self.mode == 'in def': + self.text += ' ' + line.strip() + if line.strip().endswith(':'): + if self.definition(self.text): + self.text = '' + self.mode = 'in func' + else: + self.text = '' + self.mode = 'normal' + + elif self.mode == 'in func': + if '"""' in line: + self.text += line.strip().strip('"') + self.mode = 'in doc' + if line.count('"""') == 2: + self.mode = 'normal' + self.docstring(self.text) + self.text = '' + else: + self.mode = 'normal' + + elif self.mode == 'in doc': + self.text += ' ' + line + if '"""' in line: + self.mode = 'normal' + self.docstring(self.text.strip().strip('"')) + self.text = '' + + elif line.startswith('## '): + self.header(line.strip().strip('#')) + + elif line.startswith('def ') or line.startswith('class '): + self.text += line.strip().strip(':') + if line.strip().endswith(':'): + if self.definition(self.text): + self.text = '' + self.mode = 'in func' + else: + self.text = '' + self.mode = 'normal' + else: + self.mode = 'in def' + + def clean(self, text): + text = text.strip() + text = text.replace('*', r'\*') + return text + + def definition(self, text): + text = web.lstrips(text, 'def ') + if text.startswith('_') or text.startswith('class _'): + return False + print '`'+text.strip()+'`' + return True + + def docstring(self, text): + print ' :', text.strip() + print + + def header(self, text): + print '##', text.strip() + print + +for pyfile in os.listdir('trunk/web'): + if pyfile[-2:] == 'py': + print + print '## ' + pyfile + print + Parser().go('trunk/web/' + pyfile) +print '`ctx`\n :', +print '\n'.join(' '+x for x in web.ctx.__doc__.strip().split('\n')) diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/tools/makedoc.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/tools/makedoc.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,170 @@ +""" +Outputs web.py docs as html +version 2.0: documents all code, and indents nicely. +By Colin Rothwell (TheBoff) +""" +import sys +import inspect +import markdown +sys.path.insert(0, '..') + +modules = [ + 'web.application', + 'web.contrib.template', + 'web.db', + 'web.debugerror', + 'web.form', + 'web.http', + 'web.httpserver', + 'web.net', + 'web.session', + 'web.template', + 'web.utils', + 'web.webapi', + 'web.webopenid', + 'web.wsgi' +] + +item_start = '' +item_end = '' + +indent_amount = 30 + +doc_these = ( #These are the types of object that should be docced + 'module', + 'classobj', + 'instancemethod', + 'function', + 'type', + 'property', +) + +not_these_names = ( #Any particular object names that shouldn't be doced + 'fget', + 'fset', + 'fdel', + 'storage', #These stop the lower case versions getting docced + 'memoize', + 'iterbetter', + 'capturesstdout', + 'profile', + 'threadeddict', + 'd', #Don't know what this is, but only only conclude it shouldn't be doc'd +) + +css = ''' + +''' + + +indent_start = '
' +indent_end = '
' + +header = ''' + +''' + +def type_string(ob): + return str(type(ob)).split("'")[1] + +def ts_css(text): + """applies nice css to the type string""" + return '%s' % text + +def arg_string(func): + """Returns a nice argstring for a function or method""" + return inspect.formatargspec(*inspect.getargspec(func)) + +def recurse_over(ob, name, indent_level=0): + ts = type_string(ob) + if not ts in doc_these: return #stos what shouldn't be docced getting docced + if indent_level > 0 and ts == 'module': return #Stops it getting into the stdlib + if name in not_these_names: return #Stops things we don't want getting docced + + indent = indent_level * indent_amount #Indents nicely + ds_indent = indent + (indent_amount / 2) + if indent_level > 0: print indent_start % indent + + argstr = '' + if ts.endswith(('function', 'method')): + argstr = arg_string(ob) + elif ts == 'classobj' or ts == 'type': + if ts == 'classobj': ts = 'class' + if hasattr(ob, '__init__'): + if type_string(ob.__init__) == 'instancemethod': + argstr = arg_string(ob.__init__) + else: + argstr = '(self)' + if ts == 'instancemethod': ts = 'method' #looks much nicer + + ds = inspect.getdoc(ob) + if ds is None: ds = '' + ds = markdown.Markdown(ds) + + mlink = '' % name if ts == 'module' else '' + mend = '' if ts == 'module' else '' + + print ''.join(('

', ts_css(ts), item_start % ts, ' ', mlink, name, argstr, + mend, item_end, '
')) + print ''.join((indent_start % ds_indent, ds, indent_end, '

')) + #Although ''.join looks wierd, it's alot faster is string addition + members = '' + + if hasattr(ob, '__all__'): members = ob.__all__ + else: members = [item for item in dir(ob) if not item.startswith('_')] + + if not 'im_class' in members: + for name in members: + recurse_over(getattr(ob, name), name, indent_level + 1) + if indent_level > 0: print indent_end + +def main(): + print '
' #Stops markdown vandalising my html. + print css + print header + print '' + for name in modules: + mod = __import__(name, {}, {}, 'x') + recurse_over(mod, name) + print '
' + +if __name__ == '__main__': + main() \ No newline at end of file diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/tools/markdown.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/tools/markdown.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,677 @@ +#!/usr/bin/python +import re, md5, sys, string + +"""markdown.py: A Markdown-styled-text to HTML converter in Python. + +Usage: + ./markdown.py textfile.markdown + +Calling: + import markdown + somehtml = markdown.markdown(sometext) + +For other versions of markdown, see: + http://www.freewisdom.org/projects/python-markdown/ + http://en.wikipedia.org/wiki/Markdown +""" + +__version__ = '1.0.1-2' # port of 1.0.1 +__license__ = "GNU GPL 2" +__author__ = [ + 'John Gruber ', + 'Tollef Fog Heen ', + 'Aaron Swartz ' +] + +def htmlquote(text): + """Encodes `text` for raw use in HTML.""" + text = text.replace("&", "&") # Must be done first! + text = text.replace("<", "<") + text = text.replace(">", ">") + text = text.replace("'", "'") + text = text.replace('"', """) + return text + +def semirandom(seed): + x = 0 + for c in md5.new(seed).digest(): x += ord(c) + return x / (255*16.) + +class _Markdown: + emptyelt = " />" + tabwidth = 4 + + escapechars = '\\`*_{}[]()>#+-.!' + escapetable = {} + for char in escapechars: + escapetable[char] = md5.new(char).hexdigest() + + r_multiline = re.compile("\n{2,}") + r_stripspace = re.compile(r"^[ \t]+$", re.MULTILINE) + def parse(self, text): + self.urls = {} + self.titles = {} + self.html_blocks = {} + self.list_level = 0 + + text = text.replace("\r\n", "\n") + text = text.replace("\r", "\n") + text += "\n\n" + text = self._Detab(text) + text = self.r_stripspace.sub("", text) + text = self._HashHTMLBlocks(text) + text = self._StripLinkDefinitions(text) + text = self._RunBlockGamut(text) + text = self._UnescapeSpecialChars(text) + return text + + r_StripLinkDefinitions = re.compile(r""" + ^[ ]{0,%d}\[(.+)\]: # id = $1 + [ \t]*\n?[ \t]* + ? # url = $2 + [ \t]*\n?[ \t]* + (?: + (?<=\s) # lookbehind for whitespace + [\"\(] # " is backlashed so it colorizes our code right + (.+?) # title = $3 + [\"\)] + [ \t]* + )? # title is optional + (?:\n+|\Z) + """ % (tabwidth-1), re.MULTILINE|re.VERBOSE) + def _StripLinkDefinitions(self, text): + def replacefunc(matchobj): + (t1, t2, t3) = matchobj.groups() + #@@ case sensitivity? + self.urls[t1.lower()] = self._EncodeAmpsAndAngles(t2) + if t3 is not None: + self.titles[t1.lower()] = t3.replace('"', '"') + return "" + + text = self.r_StripLinkDefinitions.sub(replacefunc, text) + return text + + blocktagsb = r"p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|math" + blocktagsa = blocktagsb + "|ins|del" + + r_HashHTMLBlocks1 = re.compile(r""" + ( # save in $1 + ^ # start of line (with /m) + <(%s) # start tag = $2 + \b # word break + (.*\n)*? # any number of lines, minimally matching + # the matching end tag + [ \t]* # trailing spaces/tabs + (?=\n+|$) # followed by a newline or end of document + ) + """ % blocktagsa, re.MULTILINE | re.VERBOSE) + + r_HashHTMLBlocks2 = re.compile(r""" + ( # save in $1 + ^ # start of line (with /m) + <(%s) # start tag = $2 + \b # word break + (.*\n)*? # any number of lines, minimally matching + .* # the matching end tag + [ \t]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + ) + """ % blocktagsb, re.MULTILINE | re.VERBOSE) + + r_HashHR = re.compile(r""" + (?: + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in $1 + [ ]{0,%d} + <(hr) # start tag = $2 + \b # word break + ([^<>])*? # + /?> # the matching end tag + [ \t]* + (?=\n{2,}|\Z)# followed by a blank line or end of document + ) + """ % (tabwidth-1), re.VERBOSE) + r_HashComment = re.compile(r""" + (?: + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in $1 + [ ]{0,%d} + (?: + + ) + [ \t]* + (?=\n{2,}|\Z)# followed by a blank line or end of document + ) + """ % (tabwidth-1), re.VERBOSE) + + def _HashHTMLBlocks(self, text): + def handler(m): + key = md5.new(m.group(1)).hexdigest() + self.html_blocks[key] = m.group(1) + return "\n\n%s\n\n" % key + + text = self.r_HashHTMLBlocks1.sub(handler, text) + text = self.r_HashHTMLBlocks2.sub(handler, text) + oldtext = text + text = self.r_HashHR.sub(handler, text) + text = self.r_HashComment.sub(handler, text) + return text + + #@@@ wrong! + r_hr1 = re.compile(r'^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$', re.M) + r_hr2 = re.compile(r'^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$', re.M) + r_hr3 = re.compile(r'^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$', re.M) + + def _RunBlockGamut(self, text): + text = self._DoHeaders(text) + for x in [self.r_hr1, self.r_hr2, self.r_hr3]: + text = x.sub("\ns. + text = self._HashHTMLBlocks(text) + text = self._FormParagraphs(text) + return text + + r_NewLine = re.compile(" {2,}\n") + def _RunSpanGamut(self, text): + text = self._DoCodeSpans(text) + text = self._EscapeSpecialChars(text) + text = self._DoImages(text) + text = self._DoAnchors(text) + text = self._DoAutoLinks(text) + text = self._EncodeAmpsAndAngles(text) + text = self._DoItalicsAndBold(text) + text = self.r_NewLine.sub(" ? # href = $3 + [ \t]* + ( # $4 + ([\'\"]) # quote char = $5 + (.*?) # Title = $6 + \5 # matching quote + )? # title is optional + \) + ) + """, re.S|re.VERBOSE) + def _DoAnchors(self, text): + # We here don't do the same as the perl version, as python's regex + # engine gives us no way to match brackets. + + def handler1(m): + whole_match = m.group(1) + link_text = m.group(2) + link_id = m.group(3).lower() + if not link_id: link_id = link_text.lower() + title = self.titles.get(link_id, None) + + + if self.urls.has_key(link_id): + url = self.urls[link_id] + url = url.replace("*", self.escapetable["*"]) + url = url.replace("_", self.escapetable["_"]) + res = '? # src url = $3 + [ \t]* + ( # $4 + ([\'\"]) # quote char = $5 + (.*?) # title = $6 + \5 # matching quote + [ \t]* + )? # title is optional + \) + ) + """, re.VERBOSE|re.S) + + def _DoImages(self, text): + def handler1(m): + whole_match = m.group(1) + alt_text = m.group(2) + link_id = m.group(3).lower() + + if not link_id: + link_id = alt_text.lower() + + alt_text = alt_text.replace('"', """) + if self.urls.has_key(link_id): + url = self.urls[link_id] + url = url.replace("*", self.escapetable["*"]) + url = url.replace("_", self.escapetable["_"]) + res = '''%s= len(textl): continue + count = textl[i].strip().count(c) + if count > 0 and count == len(textl[i].strip()) and textl[i+1].strip() == '' and textl[i-1].strip() != '': + textl = textl[:i] + textl[i+1:] + textl[i-1] = ''+self._RunSpanGamut(textl[i-1])+'' + textl = textl[:i] + textl[i+1:] + text = '\n'.join(textl) + return text + + def handler(m): + level = len(m.group(1)) + header = self._RunSpanGamut(m.group(2)) + return "%s\n\n" % (level, header, level) + + text = findheader(text, '=', '1') + text = findheader(text, '-', '2') + text = self.r_DoHeaders.sub(handler, text) + return text + + rt_l = r""" + ( + ( + [ ]{0,%d} + ([*+-]|\d+[.]) + [ \t]+ + ) + (?:.+?) + ( + \Z + | + \n{2,} + (?=\S) + (?![ \t]* ([*+-]|\d+[.])[ \t]+) + ) + ) + """ % (tabwidth - 1) + r_DoLists = re.compile('^'+rt_l, re.M | re.VERBOSE | re.S) + r_DoListsTop = re.compile( + r'(?:\A\n?|(?<=\n\n))'+rt_l, re.M | re.VERBOSE | re.S) + + def _DoLists(self, text): + def handler(m): + list_type = "ol" + if m.group(3) in [ "*", "-", "+" ]: + list_type = "ul" + listn = m.group(1) + listn = self.r_multiline.sub("\n\n\n", listn) + res = self._ProcessListItems(listn) + res = "<%s>\n%s\n" % (list_type, res, list_type) + return res + + if self.list_level: + text = self.r_DoLists.sub(handler, text) + else: + text = self.r_DoListsTop.sub(handler, text) + return text + + r_multiend = re.compile(r"\n{2,}\Z") + r_ProcessListItems = re.compile(r""" + (\n)? # leading line = $1 + (^[ \t]*) # leading whitespace = $2 + ([*+-]|\d+[.]) [ \t]+ # list marker = $3 + ((?:.+?) # list item text = $4 + (\n{1,2})) + (?= \n* (\Z | \2 ([*+-]|\d+[.]) [ \t]+)) + """, re.VERBOSE | re.M | re.S) + + def _ProcessListItems(self, text): + self.list_level += 1 + text = self.r_multiend.sub("\n", text) + + def handler(m): + item = m.group(4) + leading_line = m.group(1) + leading_space = m.group(2) + + if leading_line or self.r_multiline.search(item): + item = self._RunBlockGamut(self._Outdent(item)) + else: + item = self._DoLists(self._Outdent(item)) + if item[-1] == "\n": item = item[:-1] # chomp + item = self._RunSpanGamut(item) + return "
  • %s
  • \n" % item + + text = self.r_ProcessListItems.sub(handler, text) + self.list_level -= 1 + return text + + r_DoCodeBlocks = re.compile(r""" + (?:\n\n|\A) + ( # $1 = the code block + (?: + (?:[ ]{%d} | \t) # Lines must start with a tab or equiv + .*\n+ + )+ + ) + ((?=^[ ]{0,%d}\S)|\Z) # Lookahead for non-space/end of doc + """ % (tabwidth, tabwidth), re.M | re.VERBOSE) + def _DoCodeBlocks(self, text): + def handler(m): + codeblock = m.group(1) + codeblock = self._EncodeCode(self._Outdent(codeblock)) + codeblock = self._Detab(codeblock) + codeblock = codeblock.lstrip("\n") + codeblock = codeblock.rstrip() + res = "\n\n
    %s\n
    \n\n" % codeblock + return res + + text = self.r_DoCodeBlocks.sub(handler, text) + return text + r_DoCodeSpans = re.compile(r""" + (`+) # $1 = Opening run of ` + (.+?) # $2 = The code block + (?%s" % c + + text = self.r_DoCodeSpans.sub(handler, text) + return text + + def _EncodeCode(self, text): + text = text.replace("&","&") + text = text.replace("<","<") + text = text.replace(">",">") + for c in "*_{}[]\\": + text = text.replace(c, self.escapetable[c]) + return text + + + r_DoBold = re.compile(r"(\*\*|__) (?=\S) (.+?[*_]*) (?<=\S) \1", re.VERBOSE | re.S) + r_DoItalics = re.compile(r"(\*|_) (?=\S) (.+?) (?<=\S) \1", re.VERBOSE | re.S) + def _DoItalicsAndBold(self, text): + text = self.r_DoBold.sub(r"\2", text) + text = self.r_DoItalics.sub(r"\2", text) + return text + + r_start = re.compile(r"^", re.M) + r_DoBlockQuotes1 = re.compile(r"^[ \t]*>[ \t]?", re.M) + r_DoBlockQuotes2 = re.compile(r"^[ \t]+$", re.M) + r_DoBlockQuotes3 = re.compile(r""" + ( # Wrap whole match in $1 + ( + ^[ \t]*>[ \t]? # '>' at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + )""", re.M | re.VERBOSE) + r_protectpre = re.compile(r'(\s*
    .+?
    )', re.S) + r_propre = re.compile(r'^ ', re.M) + + def _DoBlockQuotes(self, text): + def prehandler(m): + return self.r_propre.sub('', m.group(1)) + + def handler(m): + bq = m.group(1) + bq = self.r_DoBlockQuotes1.sub("", bq) + bq = self.r_DoBlockQuotes2.sub("", bq) + bq = self._RunBlockGamut(bq) + bq = self.r_start.sub(" ", bq) + bq = self.r_protectpre.sub(prehandler, bq) + return "
    \n%s\n
    \n\n" % bq + + text = self.r_DoBlockQuotes3.sub(handler, text) + return text + + r_tabbed = re.compile(r"^([ \t]*)") + def _FormParagraphs(self, text): + text = text.strip("\n") + grafs = self.r_multiline.split(text) + + for g in xrange(len(grafs)): + t = grafs[g].strip() #@@? + if not self.html_blocks.has_key(t): + t = self._RunSpanGamut(t) + t = self.r_tabbed.sub(r"

    ", t) + t += "

    " + grafs[g] = t + + for g in xrange(len(grafs)): + t = grafs[g].strip() + if self.html_blocks.has_key(t): + grafs[g] = self.html_blocks[t] + + return "\n\n".join(grafs) + + r_EncodeAmps = re.compile(r"&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)") + r_EncodeAngles = re.compile(r"<(?![a-z/?\$!])") + def _EncodeAmpsAndAngles(self, text): + text = self.r_EncodeAmps.sub("&", text) + text = self.r_EncodeAngles.sub("<", text) + return text + + def _EncodeBackslashEscapes(self, text): + for char in self.escapechars: + text = text.replace("\\" + char, self.escapetable[char]) + return text + + r_link = re.compile(r"<((https?|ftp):[^\'\">\s]+)>", re.I) + r_email = re.compile(r""" + < + (?:mailto:)? + ( + [-.\w]+ + \@ + [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ + ) + >""", re.VERBOSE|re.I) + def _DoAutoLinks(self, text): + text = self.r_link.sub(r'
    \1', text) + + def handler(m): + l = m.group(1) + return self._EncodeEmailAddress(self._UnescapeSpecialChars(l)) + + text = self.r_email.sub(handler, text) + return text + + r_EncodeEmailAddress = re.compile(r">.+?:") + def _EncodeEmailAddress(self, text): + encode = [ + lambda x: "&#%s;" % ord(x), + lambda x: "&#x%X;" % ord(x), + lambda x: x + ] + + text = "mailto:" + text + addr = "" + for c in text: + if c == ':': addr += c; continue + + r = semirandom(addr) + if r < 0.45: + addr += encode[1](c) + elif r > 0.9 and c != '@': + addr += encode[2](c) + else: + addr += encode[0](c) + + text = '%s' % (addr, addr) + text = self.r_EncodeEmailAddress.sub('>', text) + return text + + def _UnescapeSpecialChars(self, text): + for key in self.escapetable.keys(): + text = text.replace(self.escapetable[key], key) + return text + + tokenize_depth = 6 + tokenize_nested_tags = '|'.join([r'(?:<[a-z/!$](?:[^<>]'] * tokenize_depth) + (')*>)' * tokenize_depth) + r_TokenizeHTML = re.compile( + r"""(?: ) | # comment + (?: <\? .*? \?> ) | # processing instruction + %s # nested tags + """ % tokenize_nested_tags, re.I|re.VERBOSE) + def _TokenizeHTML(self, text): + pos = 0 + tokens = [] + matchobj = self.r_TokenizeHTML.search(text, pos) + while matchobj: + whole_tag = matchobj.string[matchobj.start():matchobj.end()] + sec_start = matchobj.end() + tag_start = sec_start - len(whole_tag) + if pos < tag_start: + tokens.append(["text", matchobj.string[pos:tag_start]]) + + tokens.append(["tag", whole_tag]) + pos = sec_start + matchobj = self.r_TokenizeHTML.search(text, pos) + + if pos < len(text): + tokens.append(["text", text[pos:]]) + return tokens + + r_Outdent = re.compile(r"""^(\t|[ ]{1,%d})""" % tabwidth, re.M) + def _Outdent(self, text): + text = self.r_Outdent.sub("", text) + return text + + def _Detab(self, text): return text.expandtabs(self.tabwidth) + +def Markdown(*args, **kw): return _Markdown().parse(*args, **kw) +markdown = Markdown + +if __name__ == '__main__': + if len(sys.argv) > 1: + print Markdown(open(sys.argv[1]).read()) + else: + print Markdown(sys.stdin.read()) diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/web/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/web/__init__.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,34 @@ +#!/usr/bin/env python +"""web.py: makes web apps (http://webpy.org)""" + +from __future__ import generators + +__version__ = "0.32" +__author__ = [ + "Aaron Swartz ", + "Anand Chitipothu " +] +__license__ = "public domain" +__contributors__ = "see http://webpy.org/changes" + +import utils, db, net, wsgi, http, webapi, httpserver, debugerror +import template, form + +import session + +from utils import * +from db import * +from net import * +from wsgi import * +from http import * +from webapi import * +from httpserver import * +from debugerror import * +from application import * +from browser import * +import test +try: + import webopenid as openid +except ImportError: + pass # requires openid module + diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/web/application.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/web/application.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,667 @@ +#!/usr/bin/python +""" +Web application +(from web.py) +""" +import webapi as web +import webapi, wsgi, utils +import debugerror +from utils import lstrips, safeunicode +import sys + +import urllib +import traceback +import itertools +import os +import re +import types +from exceptions import SystemExit + +try: + import wsgiref.handlers +except ImportError: + pass # don't break people with old Pythons + +__all__ = [ + "application", "auto_application", + "subdir_application", "subdomain_application", + "loadhook", "unloadhook", + "autodelegate" +] + +class application: + """ + Application to delegate requests based on path. + + >>> urls = ("/hello", "hello") + >>> app = application(urls, globals()) + >>> class hello: + ... def GET(self): return "hello" + >>> + >>> app.request("/hello").data + 'hello' + """ + def __init__(self, mapping=(), fvars={}, autoreload=None): + if autoreload is None: + autoreload = web.config.get('debug', False) + self.mapping = mapping + self.fvars = fvars + self.processors = [] + + self.add_processor(loadhook(self._load)) + self.add_processor(unloadhook(self._unload)) + + if autoreload: + def main_module_name(): + mod = sys.modules['__main__'] + file = getattr(mod, '__file__', None) # make sure this works even from python interpreter + return file and os.path.splitext(os.path.basename(file))[0] + + def modname(fvars): + """find name of the module name from fvars.""" + file, name = fvars.get('__file__'), fvars.get('__name__') + if file is None or name is None: + return None + + if name == '__main__': + # Since the __main__ module can't be reloaded, the module has + # to be imported using its file name. + name = main_module_name() + return name + + mapping_name = utils.dictfind(fvars, mapping) + module_name = modname(fvars) + + def reload_mapping(): + """loadhook to reload mapping and fvars.""" + mod = __import__(module_name) + mapping = getattr(mod, mapping_name, None) + if mapping: + self.fvars = mod.__dict__ + self.mapping = mapping + + self.add_processor(loadhook(Reloader())) + if mapping_name and module_name: + self.add_processor(loadhook(reload_mapping)) + + # load __main__ module usings its filename, so that it can be reloaded. + if main_module_name() and '__main__' in sys.argv: + try: + __import__(main_module_name()) + except ImportError: + pass + + def _load(self): + web.ctx.app_stack.append(self) + + def _unload(self): + web.ctx.app_stack = web.ctx.app_stack[:-1] + + if web.ctx.app_stack: + # this is a sub-application, revert ctx to earlier state. + oldctx = web.ctx.get('_oldctx') + if oldctx: + web.ctx.home = oldctx.home + web.ctx.homepath = oldctx.homepath + web.ctx.path = oldctx.path + web.ctx.fullpath = oldctx.fullpath + + def _cleanup(self): + #@@@ + # Since the CherryPy Webserver uses thread pool, the thread-local state is never cleared. + # This interferes with the other requests. + # clearing the thread-local storage to avoid that. + # see utils.ThreadedDict for details + import threading + t = threading.currentThread() + if hasattr(t, '_d'): + del t._d + + def add_mapping(self, pattern, classname): + self.mapping += (pattern, classname) + + def add_processor(self, processor): + """ + Adds a processor to the application. + + >>> urls = ("/(.*)", "echo") + >>> app = application(urls, globals()) + >>> class echo: + ... def GET(self, name): return name + ... + >>> + >>> def hello(handler): return "hello, " + handler() + ... + >>> app.add_processor(hello) + >>> app.request("/web.py").data + 'hello, web.py' + """ + self.processors.append(processor) + + def request(self, localpart='/', method='GET', data=None, + host="0.0.0.0:8080", headers=None, https=False, **kw): + """Makes request to this application for the specified path and method. + Response will be a storage object with data, status and headers. + + >>> urls = ("/hello", "hello") + >>> app = application(urls, globals()) + >>> class hello: + ... def GET(self): + ... web.header('Content-Type', 'text/plain') + ... return "hello" + ... + >>> response = app.request("/hello") + >>> response.data + 'hello' + >>> response.status + '200 OK' + >>> response.headers['Content-Type'] + 'text/plain' + + To use https, use https=True. + + >>> urls = ("/redirect", "redirect") + >>> app = application(urls, globals()) + >>> class redirect: + ... def GET(self): raise web.seeother("/foo") + ... + >>> response = app.request("/redirect") + >>> response.headers['Location'] + 'http://0.0.0.0:8080/foo' + >>> response = app.request("/redirect", https=True) + >>> response.headers['Location'] + 'https://0.0.0.0:8080/foo' + + The headers argument specifies HTTP headers as a mapping object + such as a dict. + + >>> urls = ('/ua', 'uaprinter') + >>> class uaprinter: + ... def GET(self): + ... return 'your user-agent is ' + web.ctx.env['HTTP_USER_AGENT'] + ... + >>> app = application(urls, globals()) + >>> app.request('/ua', headers = { + ... 'User-Agent': 'a small jumping bean/1.0 (compatible)' + ... }).data + 'your user-agent is a small jumping bean/1.0 (compatible)' + + """ + path, maybe_query = urllib.splitquery(localpart) + query = maybe_query or "" + + if 'env' in kw: + env = kw['env'] + else: + env = {} + env = dict(env, HTTP_HOST=host, REQUEST_METHOD=method, PATH_INFO=path, QUERY_STRING=query, HTTPS=str(https)) + headers = headers or {} + + for k, v in headers.items(): + env['HTTP_' + k.upper().replace('-', '_')] = v + + if 'HTTP_CONTENT_LENGTH' in env: + env['CONTENT_LENGTH'] = env.pop('HTTP_CONTENT_LENGTH') + + if 'HTTP_CONTENT_TYPE' in env: + env['CONTENT_TYPE'] = env.pop('HTTP_CONTENT_TYPE') + + if method in ["POST", "PUT"]: + data = data or '' + import StringIO + if isinstance(data, dict): + q = urllib.urlencode(data) + else: + q = data + env['wsgi.input'] = StringIO.StringIO(q) + if not env.get('CONTENT_TYPE', '').lower().startswith('multipart/') and 'CONTENT_LENGTH' not in env: + env['CONTENT_LENGTH'] = len(q) + response = web.storage() + def start_response(status, headers): + response.status = status + response.headers = dict(headers) + response.header_items = headers + response.data = "".join(self.wsgifunc()(env, start_response)) + return response + + def browser(self): + import browser + return browser.AppBrowser(self) + + def handle(self): + fn, args = self._match(self.mapping, web.ctx.path) + return self._delegate(fn, self.fvars, args) + + def handle_with_processors(self): + def process(processors): + try: + if processors: + p, processors = processors[0], processors[1:] + return p(lambda: process(processors)) + else: + return self.handle() + except web.HTTPError: + raise + except (KeyboardInterrupt, SystemExit): + raise + except: + print >> web.debug, traceback.format_exc() + raise self.internalerror() + + # processors must be applied in the resvere order. (??) + return process(self.processors) + + def wsgifunc(self, *middleware): + """Returns a WSGI-compatible function for this application.""" + def peep(iterator): + """Peeps into an iterator by doing an iteration + and returns an equivalent iterator. + """ + # wsgi requires the headers first + # so we need to do an iteration + # and save the result for later + try: + firstchunk = iterator.next() + except StopIteration: + firstchunk = '' + + return itertools.chain([firstchunk], iterator) + + def is_generator(x): return x and hasattr(x, 'next') + + def wsgi(env, start_resp): + self.load(env) + try: + # allow uppercase methods only + if web.ctx.method.upper() != web.ctx.method: + raise web.nomethod() + + result = self.handle_with_processors() + if is_generator(result): + result = peep(result) + else: + result = [result] + except web.HTTPError, e: + result = [e.data] + + result = web.utf8(iter(result)) + + status, headers = web.ctx.status, web.ctx.headers + start_resp(status, headers) + + def cleanup(): + self._cleanup() + yield '' # force this function to be a generator + + return itertools.chain(result, cleanup()) + + for m in middleware: + wsgi = m(wsgi) + + return wsgi + + def run(self, *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. + + `middleware` is a list of WSGI middleware which is applied to the resulting WSGI + function. + """ + return wsgi.runwsgi(self.wsgifunc(*middleware)) + + def cgirun(self, *middleware): + """ + Return a CGI handler. This is mostly useful with Google App Engine. + There you can just do: + + main = app.cgirun() + """ + wsgiapp = self.wsgifunc(*middleware) + + try: + from google.appengine.ext.webapp.util import run_wsgi_app + return run_wsgi_app(wsgiapp) + except ImportError: + # we're not running from within Google App Engine + return wsgiref.handlers.CGIHandler().run(wsgiapp) + + def load(self, env): + """Initializes ctx using env.""" + ctx = web.ctx + ctx.clear() + ctx.status = '200 OK' + ctx.headers = [] + ctx.output = '' + ctx.environ = ctx.env = env + ctx.host = env.get('HTTP_HOST') + + if env.get('wsgi.url_scheme') in ['http', 'https']: + ctx.protocol = env['wsgi.url_scheme'] + elif env.get('HTTPS', '').lower() in ['on', 'true', '1']: + ctx.protocol = 'https' + else: + ctx.protocol = 'http' + ctx.homedomain = ctx.protocol + '://' + env.get('HTTP_HOST', '[unknown]') + ctx.homepath = os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', '')) + ctx.home = ctx.homedomain + ctx.homepath + #@@ home is changed when the request is handled to a sub-application. + #@@ but the real home is required for doing absolute redirects. + ctx.realhome = ctx.home + 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], ctx.homepath) + # Apache and CherryPy webservers unquote the url but lighttpd doesn't. + # unquote explicitly for lighttpd to make ctx.path uniform across all servers. + ctx.path = urllib.unquote(ctx.path) + + if env.get('QUERY_STRING'): + ctx.query = '?' + env.get('QUERY_STRING', '') + else: + ctx.query = '' + + ctx.fullpath = ctx.path + ctx.query + + for k, v in ctx.iteritems(): + if isinstance(v, str): + ctx[k] = safeunicode(v) + + # status must always be str + ctx.status = '200 OK' + + ctx.app_stack = [] + + def _delegate(self, f, fvars, args=[]): + def handle_class(cls): + meth = web.ctx.method + if meth == 'HEAD' and not hasattr(cls, meth): + meth = 'GET' + if not hasattr(cls, meth): + raise web.nomethod(cls) + tocall = getattr(cls(), meth) + return tocall(*args) + + def is_class(o): return isinstance(o, (types.ClassType, type)) + + if f is None: + raise web.notfound() + elif isinstance(f, application): + return f.handle_with_processors() + elif is_class(f): + return handle_class(f) + elif isinstance(f, basestring): + if f.startswith('redirect '): + url = f.split(' ', 1)[1] + if web.ctx.method == "GET": + x = web.ctx.env.get('QUERY_STRING', '') + if x: + url += '?' + x + raise web.redirect(url) + elif '.' in f: + x = f.split('.') + mod, cls = '.'.join(x[:-1]), x[-1] + mod = __import__(mod, globals(), locals(), [""]) + cls = getattr(mod, cls) + else: + cls = fvars[f] + return handle_class(cls) + elif hasattr(f, '__call__'): + return f() + else: + return web.notfound() + + def _match(self, mapping, value): + for pat, what in utils.group(mapping, 2): + if isinstance(what, application): + if value.startswith(pat): + f = lambda: self._delegate_sub_application(pat, what) + return f, None + else: + continue + elif isinstance(what, basestring): + what, result = utils.re_subm('^' + pat + '$', what, value) + else: + result = utils.re_compile('^' + pat + '$').match(value) + + if result: # it's a match + return what, [x for x in result.groups()] + return None, None + + def _delegate_sub_application(self, dir, app): + """Deletes request to sub application `app` rooted at the directory `dir`. + The home, homepath, path and fullpath values in web.ctx are updated to mimic request + to the subapp and are restored after it is handled. + + @@Any issues with when used with yield? + """ + web.ctx._oldctx = web.storage(web.ctx) + web.ctx.home += dir + web.ctx.homepath += dir + web.ctx.path = web.ctx.path[len(dir):] + web.ctx.fullpath = web.ctx.fullpath[len(dir):] + return app.handle_with_processors() + + def get_parent_app(self): + if self in web.ctx.app_stack: + index = web.ctx.app_stack.index(self) + if index > 0: + return web.ctx.app_stack[index-1] + + def notfound(self): + """Returns HTTPError with '404 not found' message""" + parent = self.get_parent_app() + if parent: + return parent.notfound() + else: + return web._NotFound() + + def internalerror(self): + """Returns HTTPError with '500 internal error' message""" + parent = self.get_parent_app() + if parent: + return parent.internalerror() + elif web.config.get('debug'): + import debugerror + return debugerror.debugerror() + else: + return web._InternalError() + +class auto_application(application): + """Application similar to `application` but urls are constructed + automatiacally using metaclass. + + >>> app = auto_application() + >>> class hello(app.page): + ... def GET(self): return "hello, world" + ... + >>> class foo(app.page): + ... path = '/foo/.*' + ... def GET(self): return "foo" + >>> app.request("/hello").data + 'hello, world' + >>> app.request('/foo/bar').data + 'foo' + """ + def __init__(self): + application.__init__(self) + + class metapage(type): + def __init__(klass, name, bases, attrs): + type.__init__(klass, name, bases, attrs) + path = attrs.get('path', '/' + name) + + # path can be specified as None to ignore that class + # typically required to create a abstract base class. + if path is not None: + self.add_mapping(path, klass) + + class page: + path = None + __metaclass__ = metapage + + self.page = page + +# The application class already has the required functionality of subdir_application +subdir_application = application + +class subdomain_application(application): + """ + Application to delegate requests based on the host. + + >>> urls = ("/hello", "hello") + >>> app = application(urls, globals()) + >>> class hello: + ... def GET(self): return "hello" + >>> + >>> mapping = (r"hello\.example\.com", app) + >>> app2 = subdomain_application(mapping) + >>> app2.request("/hello", host="hello.example.com").data + 'hello' + >>> response = app2.request("/hello", host="something.example.com") + >>> response.status + '404 Not Found' + >>> response.data + 'not found' + """ + def handle(self): + host = web.ctx.host.split(':')[0] #strip port + fn, args = self._match(self.mapping, host) + return self._delegate(fn, self.fvars, args) + + def _match(self, mapping, value): + for pat, what in utils.group(mapping, 2): + if isinstance(what, basestring): + what, result = utils.re_subm('^' + pat + '$', what, value) + else: + result = utils.re_compile('^' + pat + '$').match(value) + + if result: # it's a match + return what, [x for x in result.groups()] + return None, None + +def loadhook(h): + """ + Converts a load hook into an application processor. + + >>> app = auto_application() + >>> def f(): "something done before handling request" + ... + >>> app.add_processor(loadhook(f)) + """ + def processor(handler): + h() + return handler() + + return processor + +def unloadhook(h): + """ + Converts an unload hook into an application processor. + + >>> app = auto_application() + >>> def f(): "something done after handling request" + ... + >>> app.add_processor(unloadhook(f)) + """ + def processor(handler): + try: + result = handler() + is_generator = result and hasattr(result, 'next') + except: + # run the hook even when handler raises some exception + h() + raise + + if is_generator: + return wrap(result) + else: + h() + return result + + def wrap(result): + def next(): + try: + return result.next() + except: + # call the hook at the and of iterator + h() + raise + + result = iter(result) + while True: + yield next() + + return processor + +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 + +class Reloader: + """Checks to see if any loaded modules have changed on disk and, + if so, reloads them. + """ + def __init__(self): + self.mtimes = {} + + def __call__(self): + for mod in sys.modules.values(): + self.check(mod) + + def check(self, mod): + try: + mtime = os.stat(mod.__file__).st_mtime + except (AttributeError, OSError, IOError): + return + 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) + self.mtimes[mod] = mtime + except ImportError: + pass + +if __name__ == "__main__": + import doctest + doctest.testmod() diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/web/browser.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/web/browser.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,236 @@ +"""Browser to test web applications. +(from web.py) +""" +from utils import re_compile +from net import htmlunquote + +import httplib, urllib, urllib2 +import copy +from StringIO import StringIO + +DEBUG = False + +__all__ = [ + "BrowserError", + "Browser", "AppBrowser", + "AppHandler" +] + +class BrowserError(Exception): + pass + +class Browser: + def __init__(self): + import cookielib + self.cookiejar = cookielib.CookieJar() + self._cookie_processor = urllib2.HTTPCookieProcessor(self.cookiejar) + self.form = None + + self.url = "http://0.0.0.0:8080/" + self.path = "/" + + self.status = None + self.data = None + self._response = None + self._forms = None + + def reset(self): + """Clears all cookies and history.""" + self.cookiejar.clear() + + def build_opener(self): + """Builds the opener using urllib2.build_opener. + Subclasses can override this function to prodive custom openers. + """ + return urllib2.build_opener() + + def do_request(self, req): + if DEBUG: + print 'requesting', req.get_method(), req.get_full_url() + opener = self.build_opener() + opener.add_handler(self._cookie_processor) + try: + self._response = opener.open(req) + except urllib2.HTTPError, e: + self._response = e + + self.url = self._response.geturl() + self.path = urllib2.Request(self.url).get_selector() + self.data = self._response.read() + self.status = self._response.code + self._forms = None + self.form = None + return self.get_response() + + def open(self, url, data=None, headers={}): + """Opens the specified url.""" + url = urllib.basejoin(self.url, url) + req = urllib2.Request(url, data, headers) + return self.do_request(req) + + def show(self): + """Opens the current page in real web browser.""" + f = open('page.html', 'w') + f.write(self.data) + f.close() + + import webbrowser, os + url = 'file://' + os.path.abspath('page.html') + webbrowser.open(url) + + def get_response(self): + """Returns a copy of the current response.""" + return urllib.addinfourl(StringIO(self.data), self._response.info(), self._response.geturl()) + + def get_soup(self): + """Returns beautiful soup of the current document.""" + import BeautifulSoup + return BeautifulSoup.BeautifulSoup(self.data) + + def get_text(self, e=None): + """Returns content of e or the current document as plain text.""" + e = e or self.get_soup() + return ''.join([htmlunquote(c) for c in e.recursiveChildGenerator() if isinstance(c, unicode)]) + + def _get_links(self): + soup = self.get_soup() + return [a for a in soup.findAll(name='a')] + + def get_links(self, text=None, text_regex=None, url=None, url_regex=None, predicate=None): + """Returns all links in the document.""" + return self._filter_links(self._get_links(), + text=text, text_regex=text_regex, url=url, url_regex=url_regex, predicate=predicate) + + def follow_link(self, link=None, text=None, text_regex=None, url=None, url_regex=None, predicate=None): + if link is None: + links = self._filter_links(self.get_links(), + text=text, text_regex=text_regex, url=url, url_regex=url_regex, predicate=predicate) + link = links and links[0] + + if link: + return self.open(link['href']) + else: + raise BrowserError("No link found") + + def find_link(self, text=None, text_regex=None, url=None, url_regex=None, predicate=None): + links = self._filter_links(self.get_links(), + text=text, text_regex=text_regex, url=url, url_regex=url_regex, predicate=predicate) + return links and links[0] or None + + def _filter_links(self, links, + text=None, text_regex=None, + url=None, url_regex=None, + predicate=None): + predicates = [] + if text is not None: + predicates.append(lambda link: link.string == text) + if text_regex is not None: + predicates.append(lambda link: re_compile(text_regex).search(link.string or '')) + if url is not None: + predicates.append(lambda link: link.get('href') == url) + if url_regex is not None: + predicates.append(lambda link: re_compile(url_regex).search(link.get('href', ''))) + if predicate: + predicate.append(predicate) + + def f(link): + for p in predicates: + if not p(link): + return False + return True + + return [link for link in links if f(link)] + + def get_forms(self): + """Returns all forms in the current document. + The returned form objects implement the ClientForm.HTMLForm interface. + """ + if self._forms is None: + import ClientForm + self._forms = ClientForm.ParseResponse(self.get_response(), backwards_compat=False) + return self._forms + + def select_form(self, name=None, predicate=None, index=0): + """Selects the specified form.""" + forms = self.get_forms() + + if name is not None: + forms = [f for f in forms if f.name == name] + if predicate: + forms = [f for f in forms if predicate(f)] + + if forms: + self.form = forms[index] + return self.form + else: + raise BrowserError("No form selected.") + + def submit(self, **kw): + """submits the currently selected form.""" + if self.form is None: + raise BrowserError("No form selected.") + req = self.form.click(**kw) + return self.do_request(req) + + def __getitem__(self, key): + return self.form[key] + + def __setitem__(self, key, value): + self.form[key] = value + +class AppBrowser(Browser): + """Browser interface to test web.py apps. + + b = AppBrowser(app) + b.open('/') + b.follow_link(text='Login') + + b.select_form(name='login') + b['username'] = 'joe' + b['password'] = 'secret' + b.submit() + + assert b.path == '/' + assert 'Welcome joe' in b.get_text() + """ + def __init__(self, app): + Browser.__init__(self) + self.app = app + + def build_opener(self): + return urllib2.build_opener(AppHandler(self.app)) + +class AppHandler(urllib2.HTTPHandler): + """urllib2 handler to handle requests using web.py application.""" + handler_order = 100 + + def __init__(self, app): + self.app = app + + def http_open(self, req): + result = self.app.request( + localpart=req.get_selector(), + method=req.get_method(), + host=req.get_host(), + data=req.get_data(), + headers=dict(req.header_items()), + https=req.get_type() == "https" + ) + return self._make_response(result, req.get_full_url()) + + def https_open(self, req): + return self.http_open(req) + + try: + https_request = urllib2.HTTPHandler.do_request_ + except AttributeError: + # for python 2.3 + pass + + def _make_response(self, result, url): + data = "\r\n".join(["%s: %s" % (k, v) for k, v in result.header_items]) + headers = httplib.HTTPMessage(StringIO(data)) + response = urllib.addinfourl(StringIO(result.data), headers, url) + code, msg = result.status.split(None, 1) + response.code, response.msg = int(code), msg + return response diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/web/contrib/__init__.py diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/web/contrib/template.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/web/contrib/template.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,131 @@ +""" +Interface to various templating engines. +""" +import os.path + +__all__ = [ + "render_cheetah", "render_genshi", "render_mako", + "cache", +] + +class render_cheetah: + """Rendering interface to Cheetah Templates. + + Example: + + render = render_cheetah('templates') + render.hello(name="cheetah") + """ + def __init__(self, path): + # give error if Chetah is not installed + from Cheetah.Template import Template + self.path = path + + def __getattr__(self, name): + from Cheetah.Template import Template + path = os.path.join(self.path, name + ".html") + + def template(**kw): + t = Template(file=path, searchList=[kw]) + return t.respond() + + return template + +class render_genshi: + """Rendering interface genshi templates. + Example: + + for xml/html templates. + + render = render_genshi(['templates/']) + render.hello(name='genshi') + + For text templates: + + render = render_genshi(['templates/'], type='text') + render.hello(name='genshi') + """ + + def __init__(self, *a, **kwargs): + from genshi.template import TemplateLoader + + self._type = kwargs.pop('type', None) + self._loader = TemplateLoader(*a, **kwargs) + + def __getattr__(self, name): + # Assuming all templates are html + path = name + ".html" + + if self._type == "text": + from genshi.template import TextTemplate + cls = TextTemplate + type = "text" + else: + cls = None + type = None + + t = self._loader.load(path, cls=cls) + def template(**kw): + stream = t.generate(**kw) + if type: + return stream.render(type) + else: + return stream.render() + return template + +class render_jinja: + """Rendering interface to Jinja2 Templates + + Example: + + render= render_jinja('templates') + render.hello(name='jinja2') + """ + def __init__(self, *a, **kwargs): + extensions = kwargs.pop('extensions', []) + globals = kwargs.pop('globals', {}) + + from jinja2 import Environment,FileSystemLoader + self._lookup = Environment(loader=FileSystemLoader(*a, **kwargs), extensions=extensions) + self._lookup.globals.update(globals) + + def __getattr__(self, name): + # Assuming all templates end with .html + path = name + '.html' + t = self._lookup.get_template(path) + return t.render + +class render_mako: + """Rendering interface to Mako Templates. + + Example: + + render = render_mako(directories=['templates']) + render.hello(name="mako") + """ + def __init__(self, *a, **kwargs): + from mako.lookup import TemplateLookup + self._lookup = TemplateLookup(*a, **kwargs) + + def __getattr__(self, name): + # Assuming all templates are html + path = name + ".html" + t = self._lookup.get_template(path) + return t.render + +class cache: + """Cache for any rendering interface. + + Example: + + render = cache(render_cheetah("templates/")) + render.hello(name='cache') + """ + def __init__(self, render): + self._render = render + self._cache = {} + + def __getattr__(self, name): + if name not in self._cache: + self._cache[name] = getattr(self._render, name) + return self._cache[name] diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/web/db.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/web/db.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,1137 @@ +""" +Database API +(part of web.py) +""" + +__all__ = [ + "UnknownParamstyle", "UnknownDB", "TransactionError", + "sqllist", "sqlors", "reparam", "sqlquote", + "SQLQuery", "SQLParam", "sqlparam", + "SQLLiteral", "sqlliteral", + "database", 'DB', +] + +import time +try: + import datetime +except ImportError: + datetime = None + +from utils import threadeddict, storage, iters, iterbetter + +try: + # db module can work independent of web.py + from webapi import debug, config +except: + import sys + debug = sys.stderr + config = storage() + +class UnknownDB(Exception): + """raised for unsupported dbms""" + pass + +class _ItplError(ValueError): + def __init__(self, text, pos): + ValueError.__init__(self) + self.text = text + self.pos = pos + def __str__(self): + return "unfinished expression in %s at char %d" % ( + repr(self.text), self.pos) + +class TransactionError(Exception): pass + +class UnknownParamstyle(Exception): + """ + raised for unsupported db paramstyles + + (currently supported: qmark, numeric, format, pyformat) + """ + pass + +class SQLParam: + """ + Parameter in SQLQuery. + + >>> q = SQLQuery(["SELECT * FROM test WHERE name=", SQLParam("joe")]) + >>> q + + >>> q.query() + 'SELECT * FROM test WHERE name=%s' + >>> q.values() + ['joe'] + """ + def __init__(self, value): + self.value = value + + def get_marker(self, paramstyle='pyformat'): + if paramstyle == 'qmark': + return '?' + elif paramstyle == 'numeric': + return ':1' + elif paramstyle is None or paramstyle in ['format', 'pyformat']: + return '%s' + raise UnknownParamstyle, paramstyle + + def sqlquery(self): + return SQLQuery([self]) + + def __add__(self, other): + return self.sqlquery() + other + + def __radd__(self, other): + return other + self.sqlquery() + + def __str__(self): + return str(self.value) + + def __repr__(self): + return '' % repr(self.value) + +sqlparam = SQLParam + +class SQLQuery: + """ + You can pass this sort of thing as a clause in any db function. + Otherwise, you can pass a dictionary to the keyword argument `vars` + and the function will call reparam for you. + + Internally, consists of `items`, which is a list of strings and + SQLParams, which get concatenated to produce the actual query. + """ + # tested in sqlquote's docstring + def __init__(self, items=[]): + """Creates a new SQLQuery. + + >>> SQLQuery("x") + + >>> q = SQLQuery(['SELECT * FROM ', 'test', ' WHERE x=', SQLParam(1)]) + >>> q + + >>> q.query(), q.values() + ('SELECT * FROM test WHERE x=%s', [1]) + >>> SQLQuery(SQLParam(1)) + + """ + if isinstance(items, list): + self.items = items + elif isinstance(items, SQLParam): + self.items = [items] + elif isinstance(items, SQLQuery): + self.items = list(items.items) + else: + self.items = [str(items)] + + # Take care of SQLLiterals + for i, item in enumerate(self.items): + if isinstance(item, SQLParam) and isinstance(item.value, SQLLiteral): + self.items[i] = item.value.v + + def __add__(self, other): + if isinstance(other, basestring): + items = [other] + elif isinstance(other, SQLQuery): + items = other.items + else: + return NotImplemented + return SQLQuery(self.items + items) + + def __radd__(self, other): + if isinstance(other, basestring): + items = [other] + else: + return NotImplemented + + return SQLQuery(items + self.items) + + def __iadd__(self, other): + if isinstance(other, basestring): + items = [other] + elif isinstance(other, SQLQuery): + items = other.items + else: + return NotImplemented + self.items.extend(items) + return self + + def __len__(self): + return len(self.query()) + + def query(self, paramstyle=None): + """ + Returns the query part of the sql query. + >>> q = SQLQuery(["SELECT * FROM test WHERE name=", SQLParam('joe')]) + >>> q.query() + 'SELECT * FROM test WHERE name=%s' + >>> q.query(paramstyle='qmark') + 'SELECT * FROM test WHERE name=?' + """ + s = '' + for x in self.items: + if isinstance(x, SQLParam): + x = x.get_marker(paramstyle) + s += x + return s + + def values(self): + """ + Returns the values of the parameters used in the sql query. + >>> q = SQLQuery(["SELECT * FROM test WHERE name=", SQLParam('joe')]) + >>> q.values() + ['joe'] + """ + return [i.value for i in self.items if isinstance(i, SQLParam)] + + def join(items, sep=' '): + """ + Joins multiple queries. + + >>> SQLQuery.join(['a', 'b'], ', ') + + """ + if len(items) == 0: + return SQLQuery("") + + q = SQLQuery(items[0]) + for item in items[1:]: + q += sep + q += item + return q + + join = staticmethod(join) + + def __str__(self): + try: + return self.query() % tuple([sqlify(x) for x in self.values()]) + except (ValueError, TypeError): + return self.query() + + def __repr__(self): + return '' % repr(str(self)) + +class SQLLiteral: + """ + Protects a string from `sqlquote`. + + >>> sqlquote('NOW()') + + >>> sqlquote(SQLLiteral('NOW()')) + + """ + def __init__(self, v): + self.v = v + + def __repr__(self): + return self.v + +sqlliteral = SQLLiteral + +def _sqllist(values): + """ + >>> _sqllist([1, 2, 3]) + + """ + items = [] + items.append('(') + for i, v in enumerate(values): + if i != 0: + items.append(', ') + items.append(sqlparam(v)) + items.append(')') + return SQLQuery(items) + +def reparam(string_, dictionary): + """ + Takes a string and a dictionary and interpolates the string + using values from the dictionary. Returns an `SQLQuery` for the result. + + >>> reparam("s = $s", dict(s=True)) + + >>> reparam("s IN $s", dict(s=[1, 2])) + + """ + dictionary = dictionary.copy() # eval mucks with it + vals = [] + result = [] + for live, chunk in _interpolate(string_): + if live: + v = eval(chunk, dictionary) + result.append(sqlquote(v)) + else: + result.append(chunk) + return SQLQuery.join(result, '') + +def sqlify(obj): + """ + converts `obj` to its proper SQL version + + >>> sqlify(None) + 'NULL' + >>> sqlify(True) + "'t'" + >>> sqlify(3) + '3' + """ + # because `1 == True and hash(1) == hash(True)` + # we have to do this the hard way... + + if obj is None: + return 'NULL' + elif obj is True: + return "'t'" + elif obj is False: + return "'f'" + elif datetime and isinstance(obj, datetime.datetime): + return repr(obj.isoformat()) + else: + return repr(obj) + +def sqllist(lst): + """ + Converts the arguments for use in something like a WHERE clause. + + >>> sqllist(['a', 'b']) + 'a, b' + >>> sqllist('a') + 'a' + >>> sqllist(u'abc') + u'abc' + """ + if isinstance(lst, basestring): + return lst + else: + return ', '.join(lst) + +def sqlors(left, lst): + """ + `left is a SQL clause like `tablename.arg = ` + and `lst` is a list of values. Returns a reparam-style + pair featuring the SQL that ORs together the clause + for each item in the lst. + + >>> sqlors('foo = ', []) + + >>> sqlors('foo = ', [1]) + + >>> sqlors('foo = ', 1) + + >>> sqlors('foo = ', [1,2,3]) + + """ + if isinstance(lst, iters): + lst = list(lst) + ln = len(lst) + if ln == 0: + return SQLQuery("1=2") + if ln == 1: + lst = lst[0] + + if isinstance(lst, iters): + return SQLQuery(['('] + + sum([[left, sqlparam(x), ' OR '] for x in lst], []) + + ['1=2)'] + ) + else: + return left + sqlparam(lst) + +def sqlwhere(dictionary, grouping=' AND '): + """ + Converts a `dictionary` to an SQL WHERE clause `SQLQuery`. + + >>> sqlwhere({'cust_id': 2, 'order_id':3}) + + >>> sqlwhere({'cust_id': 2, 'order_id':3}, grouping=', ') + + >>> sqlwhere({'a': 'a', 'b': 'b'}).query() + 'a = %s AND b = %s' + """ + return SQLQuery.join([k + ' = ' + sqlparam(v) for k, v in dictionary.items()], grouping) + +def sqlquote(a): + """ + Ensures `a` is quoted properly for use in a SQL query. + + >>> 'WHERE x = ' + sqlquote(True) + ' AND y = ' + sqlquote(3) + + >>> 'WHERE x = ' + sqlquote(True) + ' AND y IN ' + sqlquote([2, 3]) + + """ + if isinstance(a, list): + return _sqllist(a) + else: + return sqlparam(a).sqlquery() + +class Transaction: + """Database transaction.""" + def __init__(self, ctx): + self.ctx = ctx + self.transaction_count = transaction_count = len(ctx.transactions) + + class transaction_engine: + """Transaction Engine used in top level transactions.""" + def do_transact(self): + ctx.commit(unload=False) + + def do_commit(self): + ctx.commit() + + def do_rollback(self): + ctx.rollback() + + class subtransaction_engine: + """Transaction Engine used in sub transactions.""" + def query(self, q): + db_cursor = ctx.db.cursor() + ctx.db_execute(db_cursor, SQLQuery(q % transaction_count)) + + def do_transact(self): + self.query('SAVEPOINT webpy_sp_%s') + + def do_commit(self): + self.query('RELEASE SAVEPOINT webpy_sp_%s') + + def do_rollback(self): + self.query('ROLLBACK TO SAVEPOINT webpy_sp_%s') + + class dummy_engine: + """Transaction Engine used instead of subtransaction_engine + when sub transactions are not supported.""" + do_transact = do_commit = do_rollback = lambda self: None + + if self.transaction_count: + # nested transactions are not supported in some databases + if self.ctx.get('ignore_nested_transactions'): + self.engine = dummy_engine() + else: + self.engine = subtransaction_engine() + else: + self.engine = transaction_engine() + + self.engine.do_transact() + self.ctx.transactions.append(self) + + def __enter__(self): + return self + + def __exit__(self, exctype, excvalue, traceback): + if exctype is not None: + self.rollback() + else: + self.commit() + + def commit(self): + if len(self.ctx.transactions) > self.transaction_count: + self.engine.do_commit() + self.ctx.transactions = self.ctx.transactions[:self.transaction_count] + + def rollback(self): + if len(self.ctx.transactions) > self.transaction_count: + self.engine.do_rollback() + self.ctx.transactions = self.ctx.transactions[:self.transaction_count] + +class DB: + """Database""" + def __init__(self, db_module, keywords): + """Creates a database. + """ + # some DB implementaions take optional paramater `driver` to use a specific driver modue + # but it should not be passed to connect + keywords.pop('driver', None) + + self.db_module = db_module + self.keywords = keywords + + + self._ctx = threadeddict() + # flag to enable/disable printing queries + self.printing = config.get('debug', False) + self.supports_multiple_insert = False + + try: + import DBUtils + # enable pooling if DBUtils module is available. + self.has_pooling = True + except ImportError: + self.has_pooling = False + + # Pooling can be disabled by passing pooling=False in the keywords. + self.has_pooling = self.keywords.pop('pooling', True) and self.has_pooling + + def _getctx(self): + if not self._ctx.get('db'): + self._load_context(self._ctx) + return self._ctx + ctx = property(_getctx) + + def _load_context(self, ctx): + ctx.dbq_count = 0 + ctx.transactions = [] # stack of transactions + + if self.has_pooling: + ctx.db = self._connect_with_pooling(self.keywords) + else: + ctx.db = self._connect(self.keywords) + ctx.db_execute = self._db_execute + + if not hasattr(ctx.db, 'commit'): + ctx.db.commit = lambda: None + + if not hasattr(ctx.db, 'rollback'): + ctx.db.rollback = lambda: None + + def commit(unload=True): + # do db commit and release the connection if pooling is enabled. + ctx.db.commit() + if unload and self.has_pooling: + self._unload_context(self._ctx) + + def rollback(): + # do db rollback and release the connection if pooling is enabled. + ctx.db.rollback() + if self.has_pooling: + self._unload_context(self._ctx) + + ctx.commit = commit + ctx.rollback = rollback + + def _unload_context(self, ctx): + del ctx.db + + def _connect(self, keywords): + return self.db_module.connect(**keywords) + + def _connect_with_pooling(self, keywords): + def get_pooled_db(): + from DBUtils import PooledDB + + # In DBUtils 0.9.3, `dbapi` argument is renamed as `creator` + # see Bug#122112 + + if PooledDB.__version__.split('.') < '0.9.3'.split('.'): + return PooledDB.PooledDB(dbapi=self.db_module, **keywords) + else: + return PooledDB.PooledDB(creator=self.db_module, **keywords) + + if getattr(self, '_pooleddb', None) is None: + self._pooleddb = get_pooled_db() + + return self._pooleddb.connection() + + def _db_cursor(self): + return self.ctx.db.cursor() + + def _param_marker(self): + """Returns parameter marker based on paramstyle attribute if this database.""" + style = getattr(self, 'paramstyle', 'pyformat') + + if style == 'qmark': + return '?' + elif style == 'numeric': + return ':1' + elif style in ['format', 'pyformat']: + return '%s' + raise UnknownParamstyle, style + + def _db_execute(self, cur, sql_query): + """executes an sql query""" + self.ctx.dbq_count += 1 + + try: + a = time.time() + paramstyle = getattr(self, 'paramstyle', 'pyformat') + out = cur.execute(sql_query.query(paramstyle), sql_query.values()) + b = time.time() + except: + if self.printing: + print >> debug, 'ERR:', str(sql_query) + if self.ctx.transactions: + self.ctx.transactions[-1].rollback() + else: + self.ctx.rollback() + raise + + if self.printing: + print >> debug, '%s (%s): %s' % (round(b-a, 2), self.ctx.dbq_count, str(sql_query)) + return out + + def _where(self, where, vars): + if isinstance(where, (int, long)): + where = "id = " + sqlparam(where) + #@@@ for backward-compatibility + elif isinstance(where, (list, tuple)) and len(where) == 2: + where = SQLQuery(where[0], where[1]) + elif isinstance(where, SQLQuery): + pass + else: + where = reparam(where, vars) + return where + + def query(self, sql_query, vars=None, processed=False, _test=False): + """ + Execute SQL query `sql_query` using dictionary `vars` to interpolate it. + If `processed=True`, `vars` is a `reparam`-style list to use + instead of interpolating. + + >>> db = DB(None, {}) + >>> db.query("SELECT * FROM foo", _test=True) + + >>> db.query("SELECT * FROM foo WHERE x = $x", vars=dict(x='f'), _test=True) + + >>> db.query("SELECT * FROM foo WHERE x = " + sqlquote('f'), _test=True) + + """ + if vars is None: vars = {} + + if not processed and not isinstance(sql_query, SQLQuery): + sql_query = reparam(sql_query, vars) + + if _test: return sql_query + + db_cursor = self._db_cursor() + self._db_execute(db_cursor, sql_query) + + if db_cursor.description: + names = [x[0] for x in db_cursor.description] + def iterwrapper(): + row = db_cursor.fetchone() + while row: + yield storage(dict(zip(names, row))) + row = db_cursor.fetchone() + out = iterbetter(iterwrapper()) + out.__len__ = lambda: int(db_cursor.rowcount) + out.list = lambda: [storage(dict(zip(names, x))) \ + for x in db_cursor.fetchall()] + else: + out = db_cursor.rowcount + + if not self.ctx.transactions: + self.ctx.commit() + return out + + def select(self, tables, vars=None, what='*', where=None, order=None, group=None, + limit=None, offset=None, _test=False): + """ + Selects `what` from `tables` with clauses `where`, `order`, + `group`, `limit`, and `offset`. Uses vars to interpolate. + Otherwise, each clause can be a SQLQuery. + + >>> db = DB(None, {}) + >>> db.select('foo', _test=True) + + >>> db.select(['foo', 'bar'], where="foo.bar_id = bar.id", limit=5, _test=True) + + """ + if vars is None: vars = {} + sql_clauses = self.sql_clauses(what, tables, where, group, order, limit, offset) + clauses = [self.gen_clause(sql, val, vars) for sql, val in sql_clauses if val is not None] + qout = SQLQuery.join(clauses) + if _test: return qout + return self.query(qout, processed=True) + + def where(self, table, what='*', order=None, group=None, limit=None, + offset=None, _test=False, **kwargs): + """ + Selects from `table` where keys are equal to values in `kwargs`. + + >>> db = DB(None, {}) + >>> db.where('foo', bar_id=3, _test=True) + + >>> db.where('foo', source=2, crust='dewey', _test=True) + + """ + where = [] + for k, v in kwargs.iteritems(): + where.append(k + ' = ' + sqlquote(v)) + return self.select(table, what=what, order=order, + group=group, limit=limit, offset=offset, _test=_test, + where=SQLQuery.join(where, ' AND ')) + + def sql_clauses(self, what, tables, where, group, order, limit, offset): + return ( + ('SELECT', what), + ('FROM', sqllist(tables)), + ('WHERE', where), + ('GROUP BY', group), + ('ORDER BY', order), + ('LIMIT', limit), + ('OFFSET', offset)) + + def gen_clause(self, sql, val, vars): + if isinstance(val, (int, long)): + if sql == 'WHERE': + nout = 'id = ' + sqlquote(val) + else: + nout = SQLQuery(val) + #@@@ + elif isinstance(val, (list, tuple)) and len(val) == 2: + nout = SQLQuery(val[0], val[1]) # backwards-compatibility + elif isinstance(val, SQLQuery): + nout = val + else: + nout = reparam(val, vars) + + def xjoin(a, b): + if a and b: return a + ' ' + b + else: return a or b + + return xjoin(sql, nout) + + def insert(self, tablename, seqname=None, _test=False, **values): + """ + Inserts `values` into `tablename`. Returns current sequence ID. + Set `seqname` to the ID if it's not the default, or to `False` + if there isn't one. + + >>> db = DB(None, {}) + >>> q = db.insert('foo', name='bob', age=2, created=SQLLiteral('NOW()'), _test=True) + >>> q + + >>> q.query() + 'INSERT INTO foo (age, name, created) VALUES (%s, %s, NOW())' + >>> q.values() + [2, 'bob'] + """ + def q(x): return "(" + x + ")" + + if values: + _keys = SQLQuery.join(values.keys(), ', ') + _values = SQLQuery.join([sqlparam(v) for v in values.values()], ', ') + sql_query = "INSERT INTO %s " % tablename + q(_keys) + ' VALUES ' + q(_values) + else: + sql_query = SQLQuery("INSERT INTO %s DEFAULT VALUES" % tablename) + + if _test: return sql_query + + db_cursor = self._db_cursor() + if seqname is not False: + sql_query = self._process_insert_query(sql_query, tablename, seqname) + + if isinstance(sql_query, tuple): + # for some databases, a separate query has to be made to find + # the id of the inserted row. + q1, q2 = sql_query + self._db_execute(db_cursor, q1) + self._db_execute(db_cursor, q2) + else: + self._db_execute(db_cursor, sql_query) + + try: + out = db_cursor.fetchone()[0] + except Exception: + out = None + + if not self.ctx.transactions: + self.ctx.commit() + return out + + def multiple_insert(self, tablename, values, seqname=None, _test=False): + """ + Inserts multiple rows into `tablename`. The `values` must be a list of dictioanries, + one for each row to be inserted, each with the same set of keys. + Returns the list of ids of the inserted rows. + Set `seqname` to the ID if it's not the default, or to `False` + if there isn't one. + + >>> db = DB(None, {}) + >>> db.supports_multiple_insert = True + >>> values = [{"name": "foo", "email": "foo@example.com"}, {"name": "bar", "email": "bar@example.com"}] + >>> db.multiple_insert('person', values=values, _test=True) + + """ + if not values: + return [] + + if not self.supports_multiple_insert: + out = [self.insert(tablename, seqname=seqname, _test=_test, **v) for v in values] + if seqname is False: + return None + else: + return out + + keys = values[0].keys() + #@@ make sure all keys are valid + + # make sure all rows have same keys. + for v in values: + if v.keys() != keys: + raise ValueError, 'Bad data' + + sql_query = SQLQuery('INSERT INTO %s (%s) VALUES ' % (tablename, ', '.join(keys))) + + data = [] + for row in values: + d = SQLQuery.join([SQLParam(row[k]) for k in keys], ', ') + data.append('(' + d + ')') + sql_query += SQLQuery.join(data, ', ') + + if _test: return sql_query + + db_cursor = self._db_cursor() + if seqname is not False: + sql_query = self._process_insert_query(sql_query, tablename, seqname) + + if isinstance(sql_query, tuple): + # for some databases, a separate query has to be made to find + # the id of the inserted row. + q1, q2 = sql_query + self._db_execute(db_cursor, q1) + self._db_execute(db_cursor, q2) + else: + self._db_execute(db_cursor, sql_query) + + try: + out = db_cursor.fetchone()[0] + out = range(out-len(values)+1, out+1) + except Exception: + out = None + + if not self.ctx.transactions: + self.ctx.commit() + return out + + + def update(self, tables, where, vars=None, _test=False, **values): + """ + Update `tables` with clause `where` (interpolated using `vars`) + and setting `values`. + + >>> db = DB(None, {}) + >>> name = 'Joseph' + >>> q = db.update('foo', where='name = $name', name='bob', age=2, + ... created=SQLLiteral('NOW()'), vars=locals(), _test=True) + >>> q + + >>> q.query() + 'UPDATE foo SET age = %s, name = %s, created = NOW() WHERE name = %s' + >>> q.values() + [2, 'bob', 'Joseph'] + """ + if vars is None: vars = {} + where = self._where(where, vars) + + query = ( + "UPDATE " + sqllist(tables) + + " SET " + sqlwhere(values, ', ') + + " WHERE " + where) + + if _test: return query + + db_cursor = self._db_cursor() + self._db_execute(db_cursor, query) + if not self.ctx.transactions: + self.ctx.commit() + return db_cursor.rowcount + + def delete(self, table, where, using=None, vars=None, _test=False): + """ + Deletes from `table` with clauses `where` and `using`. + + >>> db = DB(None, {}) + >>> name = 'Joe' + >>> db.delete('foo', where='name = $name', vars=locals(), _test=True) + + """ + if vars is None: vars = {} + where = self._where(where, vars) + + q = 'DELETE FROM ' + table + if where: q += ' WHERE ' + where + if using: q += ' USING ' + sqllist(using) + + if _test: return q + + db_cursor = self._db_cursor() + self._db_execute(db_cursor, q) + if not self.ctx.transactions: + self.ctx.commit() + return db_cursor.rowcount + + def _process_insert_query(self, query, tablename, seqname): + return query + + def transaction(self): + """Start a transaction.""" + return Transaction(self.ctx) + +class PostgresDB(DB): + """Postgres driver.""" + def __init__(self, **keywords): + if 'pw' in keywords: + keywords['password'] = keywords['pw'] + del keywords['pw'] + + db_module = import_driver(["psycopg2", "psycopg", "pgdb"], preferred=keywords.pop('driver', None)) + if db_module.__name__ == "psycopg2": + import psycopg2.extensions + psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) + + keywords['database'] = keywords.pop('db') + self.dbname = "postgres" + self.paramstyle = db_module.paramstyle + DB.__init__(self, db_module, keywords) + self.supports_multiple_insert = True + + def _process_insert_query(self, query, tablename, seqname): + if seqname is None: + seqname = tablename + "_id_seq" + return query + "; SELECT currval('%s')" % seqname + + def _connect(self, keywords): + conn = DB._connect(self, keywords) + conn.set_client_encoding('UTF8') + return conn + + def _connect_with_pooling(self, keywords): + conn = DB._connect_with_pooling(self, keywords) + conn._con._con.set_client_encoding('UTF8') + return conn + +class MySQLDB(DB): + def __init__(self, **keywords): + import MySQLdb as db + if 'pw' in keywords: + keywords['passwd'] = keywords['pw'] + del keywords['pw'] + + if 'charset' not in keywords: + keywords['charset'] = 'utf8' + elif keywords['charset'] is None: + del keywords['charset'] + + self.paramstyle = db.paramstyle = 'pyformat' # it's both, like psycopg + self.dbname = "mysql" + DB.__init__(self, db, keywords) + self.supports_multiple_insert = True + + def _process_insert_query(self, query, tablename, seqname): + return query, SQLQuery('SELECT last_insert_id();') + +def import_driver(drivers, preferred=None): + """Import the first available driver or preferred driver. + """ + if preferred: + drivers = [preferred] + + for d in drivers: + try: + return __import__(d, None, None, ['x']) + except ImportError: + pass + raise ImportError("Unable to import " + " or ".join(drivers)) + +class SqliteDB(DB): + def __init__(self, **keywords): + db = import_driver(["sqlite3", "pysqlite2.dbapi2", "sqlite"], preferred=keywords.pop('driver', None)) + + if db.__name__ in ["sqlite3", "pysqlite2.dbapi2"]: + db.paramstyle = 'qmark' + + self.paramstyle = db.paramstyle + keywords['database'] = keywords.pop('db') + self.dbname = "sqlite" + DB.__init__(self, db, keywords) + + def _process_insert_query(self, query, tablename, seqname): + return query, SQLQuery('SELECT last_insert_rowid();') + + def query(self, *a, **kw): + out = DB.query(self, *a, **kw) + if isinstance(out, iterbetter): + # rowcount is not provided by sqlite + del out.__len__ + return out + +class FirebirdDB(DB): + """Firebird Database. + """ + def __init__(self, **keywords): + try: + import kinterbasdb as db + except Exception: + db = None + pass + if 'pw' in keywords: + keywords['passwd'] = keywords['pw'] + del keywords['pw'] + keywords['database'] = keywords['db'] + del keywords['db'] + DB.__init__(self, db, keywords) + + def delete(self, table, where=None, using=None, vars=None, _test=False): + # firebird doesn't support using clause + using=None + return DB.delete(self, table, where, using, vars, _test) + + def sql_clauses(self, what, tables, where, group, order, limit, offset): + return ( + ('SELECT', ''), + ('FIRST', limit), + ('SKIP', offset), + ('', what), + ('FROM', sqllist(tables)), + ('WHERE', where), + ('GROUP BY', group), + ('ORDER BY', order) + ) + +class MSSQLDB(DB): + def __init__(self, **keywords): + import pymssql as db + if 'pw' in keywords: + keywords['password'] = keywords.pop('pw') + keywords['database'] = keywords.pop('db') + self.dbname = "mssql" + DB.__init__(self, db, keywords) + + def sql_clauses(self, what, tables, where, group, order, limit, offset): + return ( + ('SELECT', what), + ('TOP', limit), + ('FROM', sqllist(tables)), + ('WHERE', where), + ('GROUP BY', group), + ('ORDER BY', order), + ('OFFSET', offset)) + + def _test(self): + """Test LIMIT. + + Fake presence of pymssql module for running tests. + >>> import sys + >>> sys.modules['pymssql'] = sys.modules['sys'] + + MSSQL has TOP clause instead of LIMIT clause. + >>> db = MSSQLDB(db='test', user='joe', pw='secret') + >>> db.select('foo', limit=4, _test=True) + + """ + pass + +class OracleDB(DB): + def __init__(self, **keywords): + import cx_Oracle as db + if 'pw' in keywords: + keywords['password'] = keywords.pop('pw') + + #@@ TODO: use db.makedsn if host, port is specified + keywords['dsn'] = keywords.pop('db') + self.dbname = 'oracle' + db.paramstyle = 'numeric' + self.paramstyle = db.paramstyle + + # oracle doesn't support pooling + keywords.pop('pooling', None) + DB.__init__(self, db, keywords) + + def _process_insert_query(self, query, tablename, seqname): + if seqname is None: + # It is not possible to get seq name from table name in Oracle + return query + else: + return query + "; SELECT %s.currval FROM dual" % seqname + +_databases = {} +def database(dburl=None, **params): + """Creates appropriate database using params. + + Pooling will be enabled if DBUtils module is available. + Pooling can be disabled by passing pooling=False in params. + """ + dbn = params.pop('dbn') + if dbn in _databases: + return _databases[dbn](**params) + else: + raise UnknownDB, dbn + +def register_database(name, clazz): + """ + Register a database. + + >>> class LegacyDB(DB): + ... def __init__(self, **params): + ... pass + ... + >>> register_database('legacy', LegacyDB) + >>> db = database(dbn='legacy', db='test', user='joe', passwd='secret') + """ + _databases[name] = clazz + +register_database('mysql', MySQLDB) +register_database('postgres', PostgresDB) +register_database('sqlite', SqliteDB) +register_database('firebird', FirebirdDB) +register_database('mssql', MSSQLDB) +register_database('oracle', OracleDB) + +def _interpolate(format): + """ + Takes a format string and returns a list of 2-tuples of the form + (boolean, string) where boolean says whether string should be evaled + or not. + + from (public domain, Ka-Ping Yee) + """ + from tokenize import tokenprog + + def matchorfail(text, pos): + match = tokenprog.match(text, pos) + if match is None: + raise _ItplError(text, pos) + return match, match.end() + + namechars = "abcdefghijklmnopqrstuvwxyz" \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; + chunks = [] + pos = 0 + + while 1: + dollar = format.find("$", pos) + if dollar < 0: + break + nextchar = format[dollar + 1] + + if nextchar == "{": + chunks.append((0, format[pos:dollar])) + pos, level = dollar + 2, 1 + while level: + match, pos = matchorfail(format, pos) + tstart, tend = match.regs[3] + token = format[tstart:tend] + if token == "{": + level = level + 1 + elif token == "}": + level = level - 1 + chunks.append((1, format[dollar + 2:pos - 1])) + + elif nextchar in namechars: + chunks.append((0, format[pos:dollar])) + match, pos = matchorfail(format, dollar + 1) + while pos < len(format): + if format[pos] == "." and \ + pos + 1 < len(format) and format[pos + 1] in namechars: + match, pos = matchorfail(format, pos + 1) + elif format[pos] in "([": + pos, level = pos + 1, 1 + while level: + match, pos = matchorfail(format, pos) + tstart, tend = match.regs[3] + token = format[tstart:tend] + if token[0] in "([": + level = level + 1 + elif token[0] in ")]": + level = level - 1 + else: + break + chunks.append((1, format[dollar + 1:pos])) + else: + chunks.append((0, format[pos:dollar + 1])) + pos = dollar + 1 + (nextchar == "$") + + if pos < len(format): + chunks.append((0, format[pos:])) + return chunks + +if __name__ == "__main__": + import doctest + doctest.testmod() diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/web/debugerror.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/web/debugerror.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,356 @@ +""" +pretty debug errors +(part of web.py) + +portions 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 +""" + +__all__ = ["debugerror", "djangoerror", "emailerrors"] + +import sys, urlparse, pprint, traceback +from net import websafe +from template import Template +from utils import sendmail +import webapi as web + +import os, os.path +whereami = os.path.join(os.getcwd(), __file__) +whereami = os.path.sep.join(whereami.split(os.path.sep)[:-1]) +djangoerror_t = """\ +$def with (exception_type, exception_value, frames) + + + + + + $exception_type at $ctx.path + + + + + +$def dicttable (d, kls='req', id=None): + $ items = d and d.items() or [] + $items.sort() + $:dicttable_items(items, kls, id) + +$def dicttable_items(items, kls='req', id=None): + $if items: + + + $for k, v in items: + + +
    VariableValue
    $k
    $prettify(v)
    + $else: +

    No data.

    + +
    +

    $exception_type at $ctx.path

    +

    $exception_value

    + + + + + + +
    Python$frames[0].filename in $frames[0].function, line $frames[0].lineno
    Web$ctx.method $ctx.home$ctx.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. +
      +
      1. $frame.context_line ...
      + $if frame.post_context: +
        + $for line in frame.post_context: +
      1. $line
      2. +
      +
      + + $if frame.vars: +
      + Local vars + $# $inspect.formatargvalues(*inspect.getargvalues(frame['tb'].tb_frame)) +
      + $:dicttable(frame.vars, kls='vars', id=('v' + str(frame.id))) +
    • +
    +
    + +
    +$if ctx.output or ctx.headers: +

    Response so far

    +

    HEADERS

    + $:dicttable_items(ctx.headers) + +

    BODY

    +

    + $ctx.output +

    + +

    Request information

    + +

    INPUT

    +$:dicttable(web.input()) + + +$:dicttable(web.cookies()) + +

    META

    +$ newctx = [(k, v) for (k, v) in ctx.iteritems() if not k.startswith('_') and not isinstance(v, dict)] +$:dicttable(dict(newctx)) + +

    ENVIRONMENT

    +$:dicttable(ctx.env) +
    + +
    +

    + You're seeing this error because you have web.config.debug + set to True. Set that to False if you don't to see this. +

    +
    + + + +""" + +djangoerror_r = None + +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 + + global djangoerror_r + if djangoerror_r is None: + djangoerror_r = Template(djangoerror_t, filename=__file__, filter=websafe) + + t = djangoerror_r + globals = {'ctx': web.ctx, 'web':web, 'dict':dict, 'str':str, 'prettify': prettify} + t.t.func_globals.update(globals) + 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/).) + """ + return web._InternalError(djangoerror()) + +def emailerrors(to_address, olderror, from_address=None): + """ + Wraps the old `internalerror` handler (pass as `olderror`) to + additionally email all errors to `to_address`, to aid in + debugging production websites. + + Emails contain a normal text traceback as well as an + attachment containing the nice `debugerror` page. + """ + from_address = from_address or to_address + + def emailerrors_internal(): + error = olderror() + tb = sys.exc_info() + error_name = tb[0] + error_value = tb[1] + tb_txt = ''.join(traceback.format_exception(*tb)) + path = web.ctx.path + request = web.ctx.method + ' ' + web.ctx.home + web.ctx.fullpath + text = ("""\ +------here---- +Content-Type: text/plain +Content-Disposition: inline + +%(request)s + +%(tb_txt)s + +------here---- +Content-Type: text/html; name="bug.html" +Content-Disposition: attachment; filename="bug.html" + +""" % locals()) + str(djangoerror()) + sendmail( + "your buggy site <%s>" % from_address, + "the bugfixer <%s>" % to_address, + "bug: %(error_name)s: %(error_value)s (%(path)s)" % locals(), + text, + headers={'Content-Type': 'multipart/mixed; boundary="----here----"'}) + return error + + return emailerrors_internal + +if __name__ == "__main__": + urls = ( + '/', 'index' + ) + from application import application + app = application(urls, globals()) + app.internalerror = debugerror + + class index: + def GET(self): + thisdoesnotexist + + app.run() diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/web/form.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/web/form.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,264 @@ +""" +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: + r""" + HTML form. + + >>> f = Form(Textbox("x")) + >>> f.render() + '\n \n
    ' + """ + def __init__(self, *inputs, **kw): + self.inputs = inputs + self.valid = True + self.note = None + self.validators = kw.pop('validators', []) + + 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 += '\n' + for i in self.inputs: + out += ' ' % (i.id, net.websafe(i.description)) + out += "\n" + out += "
    "+i.pre+i.render()+i.post+"
    " + return out + + def render_css(self): + out = [] + out.append(self.rendernote(self.note)) + for i in self.inputs: + out.append('' % (i.id, net.websafe(i.description))) + out.append(i.pre) + out.append(i.render()) + out.append(i.post) + out.append('\n') + return ''.join(out) + + def rendernote(self, note): + if note: return '%s' % net.websafe(note) + else: return "" + + def validates(self, source=None, _validate=True, **kw): + source = source or kw or web.input() + out = True + for i in self.inputs: + v = attrget(source, i.name) + if _validate: + out = i.validate(v) and out + else: + i.value = v + if _validate: + out = out and self._validate(source) + self.valid = out + return out + + def _validate(self, value): + self.value = value + for v in self.validators: + if not v.valid(value): + self.note = v.msg + return False + return True + + def fill(self, source=None, **kw): + return self.validates(source, _validate=False, **kw) + + def __getitem__(self, i): + for x in self.inputs: + if x.name == i: return x + raise KeyError, i + + def __getattr__(self, name): + # don't interfere with deepcopy + inputs = self.__dict__.get('inputs') or [] + for x in inputs: + if x.name == name: return x + raise AttributeError, name + + def get(self, i, default=None): + try: + return self[i] + except KeyError: + return default + + def _get_d(self): #@@ should really be form.attr, no? + return utils.storage([(i.name, i.value) for i in self.inputs]) + d = property(_get_d) + +class Input(object): + def __init__(self, name, *validators, **attrs): + self.description = attrs.pop('description', name) + self.value = attrs.pop('value', None) + self.pre = attrs.pop('pre', "") + self.post = attrs.pop('post', "") + self.id = attrs.setdefault('id', name) + if 'class_' in attrs: + attrs['class'] = attrs['class_'] + del attrs['class_'] + self.name, self.validators, self.attrs, self.note = name, validators, attrs, None + + def validate(self, value): + self.value = value + for v in self.validators: + if not v.valid(value): + self.note = v.msg + return False + return True + + def render(self): raise NotImplementedError + + def rendernote(self, note): + if note: return '%s' % net.websafe(note) + else: return "" + + def addatts(self): + str = "" + for (n, v) in self.attrs.items(): + str += ' %s="%s"' % (n, net.websafe(v)) + return str + +#@@ quoting + +class Textbox(Input): + def render(self, shownote=True): + x = '>> urlencode({'text':'foo bar'}) + 'text=foo+bar' + """ + query = dict([(k, utils.utf8(v)) for k, v in query.items()]) + return urllib.urlencode(query) + +def changequery(query=None, **kw): + """ + Imagine you're at `/foo?a=1&b=2`. Then `changequery(a=3)` will return + `/foo?a=3&b=2` -- the same URL but with the arguments you requested + changed. + """ + if query is None: + query = web.input(_method='get') + for k, v in kw.iteritems(): + if v is None: + query.pop(k, None) + else: + query[k] = v + out = web.ctx.path + if query: + out += '?' + urlencode(query) + return out + +def url(path=None, **kw): + """ + Makes url by concatinating web.ctx.homepath and path and the + query string created using the arguments. + """ + if path is None: + path = web.ctx.path + if path.startswith("/"): + out = web.ctx.homepath + path + else: + out = path + + if kw: + out += '?' + urlencode(kw) + + return out + +def profiler(app): + """Outputs basic profiling information at the bottom of each response.""" + from utils import profile + def profile_internal(e, o): + out, result = profile(app)(e, o) + return list(out) + ['
    ' + net.websafe(result) + '
    '] + return profile_internal + +if __name__ == "__main__": + import doctest + doctest.testmod() diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/web/httpserver.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/web/httpserver.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,225 @@ +__all__ = ["runsimple"] + +import sys, os +import webapi as web +import net +import utils + +def runbasic(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() + +def runsimple(func, server_address=("0.0.0.0", 8080)): + """ + Runs [CherryPy][cp] WSGI server hosting WSGI app `func`. + The directory `static/` is hosted statically. + + [cp]: http://www.cherrypy.org + """ + from wsgiserver import CherryPyWSGIServer + from SimpleHTTPServer import SimpleHTTPRequestHandler + from BaseHTTPServer import BaseHTTPRequestHandler + + class StaticApp(SimpleHTTPRequestHandler): + """WSGI application for serving static files.""" + def __init__(self, environ, start_response): + self.headers = [] + self.environ = environ + self.start_response = start_response + + def send_response(self, status, msg=""): + self.status = str(status) + " " + msg + + def send_header(self, name, value): + self.headers.append((name, value)) + + def end_headers(self): + pass + + def log_message(*a): pass + + def __iter__(self): + environ = self.environ + + self.path = environ.get('PATH_INFO', '') + self.client_address = environ.get('REMOTE_ADDR','-'), \ + environ.get('REMOTE_PORT','-') + self.command = environ.get('REQUEST_METHOD', '-') + + from cStringIO import StringIO + self.wfile = StringIO() # for capturing error + + f = self.send_head() + self.start_response(self.status, self.headers) + + if f: + block_size = 16 * 1024 + while True: + buf = f.read(block_size) + if not buf: + break + yield buf + f.close() + else: + value = self.wfile.getvalue() + yield value + + class WSGIWrapper(BaseHTTPRequestHandler): + """WSGI wrapper for logging the status and serving static files.""" + def __init__(self, app): + self.app = app + self.format = '%s - - [%s] "%s %s %s" - %s' + + def __call__(self, environ, start_response): + def xstart_response(status, response_headers, *args): + write = start_response(status, response_headers, *args) + self.log(status, environ) + return write + + path = environ.get('PATH_INFO', '') + if path.startswith('/static/'): + return StaticApp(environ, xstart_response) + else: + return self.app(environ, xstart_response) + + def log(self, status, environ): + outfile = environ.get('wsgi.errors', web.debug) + req = environ.get('PATH_INFO', '_') + protocol = environ.get('ACTUAL_SERVER_PROTOCOL', '-') + method = environ.get('REQUEST_METHOD', '-') + host = "%s:%s" % (environ.get('REMOTE_ADDR','-'), + environ.get('REMOTE_PORT','-')) + + #@@ It is really bad to extend from + #@@ BaseHTTPRequestHandler just for this method + time = self.log_date_time_string() + + msg = self.format % (host, time, protocol, method, req, status) + print >> outfile, utils.safestr(msg) + + func = WSGIWrapper(func) + server = CherryPyWSGIServer(server_address, func, server_name="localhost") + + print "http://%s:%d/" % server_address + try: + server.start() + except KeyboardInterrupt: + server.stop() diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/web/net.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/web/net.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,190 @@ +""" +Network Utilities +(from web.py) +""" + +__all__ = [ + "validipaddr", "validipport", "validip", "validaddr", + "urlquote", + "httpdate", "parsehttpdate", + "htmlquote", "htmlunquote", "websafe", +] + +import urllib, time +try: import datetime +except ImportError: pass + +def validipaddr(address): + """ + Returns True if `address` is a valid IPv4 address. + + >>> validipaddr('192.168.1.1') + True + >>> validipaddr('192.168.1.800') + False + >>> validipaddr('192.168.1') + False + """ + try: + octets = address.split('.') + if len(octets) != 4: + return False + for x in octets: + if not (0 <= int(x) <= 255): + return False + except ValueError: + return False + return True + +def validipport(port): + """ + Returns True if `port` is a valid IPv4 port. + + >>> validipport('9000') + True + >>> validipport('foo') + False + >>> validipport('1000000') + False + """ + try: + if not (0 <= int(port) <= 65535): + return False + except 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 htmlunquote(text): + """ + Decodes `text` that's HTML quoted. + + >>> htmlunquote('<'&">') + '<\\'&">' + """ + text = text.replace(""", '"') + text = text.replace("'", "'") + text = text.replace(">", ">") + text = text.replace("<", "<") + text = text.replace("&", "&") # Must be done last! + return text + +def websafe(val): + """ + Converts `val` so that it's safe for use in UTF-8 HTML. + + >>> websafe("<'&\\">") + '<'&">' + >>> websafe(None) + '' + >>> websafe(u'\u203d') + '\\xe2\\x80\\xbd' + """ + if val is None: + return '' + if isinstance(val, unicode): + val = val.encode('utf-8') + val = str(val) + return htmlquote(val) + +if __name__ == "__main__": + import doctest + doctest.testmod() diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/web/session.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/web/session.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,319 @@ +""" +Session Management +(from web.py) +""" + +import os, time, datetime, random, base64 +try: + import cPickle as pickle +except ImportError: + import pickle +try: + import hashlib + sha1 = hashlib.sha1 +except ImportError: + import sha + sha1 = sha.new + +import utils +import webapi as web + +__all__ = [ + 'Session', 'SessionExpired', + 'Store', 'DiskStore', 'DBStore', +] + +web.config.session_parameters = utils.storage({ + 'cookie_name': 'webpy_session_id', + 'cookie_domain': None, + 'timeout': 86400, #24 * 60 * 60, # 24 hours in seconds + 'ignore_expiry': True, + 'ignore_change_ip': True, + 'secret_key': 'fLjUfxqXtfNoIldA0A0J', + 'expired_message': 'Session expired', +}) + +class SessionExpired(web.HTTPError): + def __init__(self, message): + web.HTTPError.__init__(self, '200 OK', {}, data=message) + +class Session(utils.ThreadedDict): + """Session management for web.py + """ + + def __init__(self, app, store, initializer=None): + self.__dict__['store'] = store + self.__dict__['_initializer'] = initializer + self.__dict__['_last_cleanup_time'] = 0 + self.__dict__['_config'] = utils.storage(web.config.session_parameters) + + if app: + app.add_processor(self._processor) + + def _processor(self, handler): + """Application processor to setup session for every request""" + self._cleanup() + self._load() + + try: + return handler() + finally: + self._save() + + def _load(self): + """Load the session from the store, by the id from cookie""" + cookie_name = self._config.cookie_name + cookie_domain = self._config.cookie_domain + self.session_id = web.cookies().get(cookie_name) + + # protection against session_id tampering + if self.session_id and not self._valid_session_id(self.session_id): + self.session_id = None + + self._check_expiry() + if self.session_id: + d = self.store[self.session_id] + self.update(d) + self._validate_ip() + + if not self.session_id: + self.session_id = self._generate_session_id() + + if self._initializer: + if isinstance(self._initializer, dict): + self.update(self._initializer) + elif hasattr(self._initializer, '__call__'): + self._initializer() + + self.ip = web.ctx.ip + + def _check_expiry(self): + # check for expiry + if self.session_id and self.session_id not in self.store: + if self._config.ignore_expiry: + self.session_id = None + else: + return self.expired() + + def _validate_ip(self): + # check for change of IP + if self.session_id and self.get('ip', None) != web.ctx.ip: + if not self._config.ignore_change_ip: + return self.expired() + + def _save(self): + cookie_name = self._config.cookie_name + cookie_domain = self._config.cookie_domain + if not self.get('_killed'): + web.setcookie(cookie_name, self.session_id, domain=cookie_domain) + self.store[self.session_id] = dict(self) + else: + web.setcookie(cookie_name, self.session_id, expires=-1, domain=cookie_domain) + + def _generate_session_id(self): + """Generate a random id for session""" + + while True: + rand = os.urandom(16) + now = time.time() + secret_key = self._config.secret_key + session_id = sha1("%s%s%s%s" %(rand, now, utils.safestr(web.ctx.ip), secret_key)) + session_id = session_id.hexdigest() + if session_id not in self.store: + break + return session_id + + def _valid_session_id(self, session_id): + rx = utils.re_compile('^[0-9a-fA-F]+$') + return rx.match(session_id) + + def _cleanup(self): + """Cleanup the stored sessions""" + current_time = time.time() + timeout = self._config.timeout + if current_time - self._last_cleanup_time > timeout: + self.store.cleanup(timeout) + self.__dict__['_last_cleanup_time'] = current_time + + def expired(self): + """Called when an expired session is atime""" + self._killed = True + self._save() + raise SessionExpired(self._config.expired_message) + + def kill(self): + """Kill the session, make it no longer available""" + del self.store[self.session_id] + self._killed = True + +class Store: + """Base class for session stores""" + + def __contains__(self, key): + raise NotImplementedError + + def __getitem__(self, key): + raise NotImplementedError + + def __setitem__(self, key, value): + raise NotImplementedError + + def cleanup(self, timeout): + """removes all the expired sessions""" + raise NotImplementedError + + def encode(self, session_dict): + """encodes session dict as a string""" + pickled = pickle.dumps(session_dict) + return base64.encodestring(pickled) + + def decode(self, session_data): + """decodes the data to get back the session dict """ + pickled = base64.decodestring(session_data) + return pickle.loads(pickled) + +class DiskStore(Store): + """ + Store for saving a session on disk. + + >>> import tempfile + >>> root = tempfile.mkdtemp() + >>> s = DiskStore(root) + >>> s['a'] = 'foo' + >>> s['a'] + 'foo' + >>> time.sleep(0.01) + >>> s.cleanup(0.01) + >>> s['a'] + Traceback (most recent call last): + ... + KeyError: 'a' + """ + def __init__(self, root): + # if the storage root doesn't exists, create it. + if not os.path.exists(root): + os.mkdir(root) + self.root = root + + def _get_path(self, key): + if os.path.sep in key: + raise ValueError, "Bad key: %s" % repr(key) + return os.path.join(self.root, key) + + def __contains__(self, key): + path = self._get_path(key) + return os.path.exists(path) + + def __getitem__(self, key): + path = self._get_path(key) + if os.path.exists(path): + pickled = open(path).read() + return self.decode(pickled) + else: + raise KeyError, key + + def __setitem__(self, key, value): + path = self._get_path(key) + pickled = self.encode(value) + try: + f = open(path, 'w') + try: + f.write(pickled) + finally: + f.close() + except IOError: + pass + + def __delitem__(self, key): + path = self._get_path(key) + if os.path.exists(path): + os.remove(path) + + def cleanup(self, timeout): + now = time.time() + for f in os.listdir(self.root): + path = self._get_path(f) + atime = os.stat(path).st_atime + if now - atime > timeout : + os.remove(path) + +class DBStore(Store): + """Store for saving a session in database + Needs a table with the following columns: + + session_id CHAR(128) UNIQUE NOT NULL, + atime DATETIME NOT NULL default current_timestamp, + data TEXT + """ + def __init__(self, db, table_name): + self.db = db + self.table = table_name + + def __contains__(self, key): + data = self.db.select(self.table, where="session_id=$key", vars=locals()) + return bool(list(data)) + + def __getitem__(self, key): + now = datetime.datetime.now() + try: + s = self.db.select(self.table, where="session_id=$key", vars=locals())[0] + self.db.update(self.table, where="session_id=$key", atime=now, vars=locals()) + except IndexError: + raise KeyError + else: + return self.decode(s.data) + + def __setitem__(self, key, value): + pickled = self.encode(value) + now = datetime.datetime.now() + if key in self: + self.db.update(self.table, where="session_id=$key", data=pickled, vars=locals()) + else: + self.db.insert(self.table, False, session_id=key, data=pickled ) + + def __delitem__(self, key): + self.db.delete(self.table, where="session_id=$key", vars=locals()) + + def cleanup(self, timeout): + timeout = datetime.timedelta(timeout/(24.0*60*60)) #timedelta takes numdays as arg + last_allowed_time = datetime.datetime.now() - timeout + self.db.delete(self.table, where="$last_allowed_time > atime", vars=locals()) + +class ShelfStore: + """Store for saving session using `shelve` module. + + import shelve + store = ShelfStore(shelve.open('session.shelf')) + + XXX: is shelve thread-safe? + """ + def __init__(self, shelf): + self.shelf = shelf + + def __contains__(self, key): + return key in self.shelf + + def __getitem__(self, key): + atime, v = self.shelf[key] + self[key] = v # update atime + return v + + def __setitem__(self, key, value): + self.shelf[key] = time.time(), value + + def __delitem__(self, key): + try: + del self.shelf[key] + except KeyError: + pass + + def cleanup(self, timeout): + now = time.time() + for k in self.shelf.keys(): + atime, v = self.shelf[k] + if now - atime > timeout : + del self[k] + +if __name__ == '__main__' : + import doctest + doctest.testmod() diff -r 82f04bfc46b8 -r 26150fdd04b8 bundled/webpy/web/template.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bundled/webpy/web/template.py Wed Oct 21 19:32:14 2009 -0400 @@ -0,0 +1,1412 @@ +""" +simple, elegant templating +(part of web.py) + +Template design: + +Template string is split into tokens and the tokens are combined into nodes. +Parse tree is a nodelist. TextNode and ExpressionNode are simple nodes and +for-loop, if-loop etc are block nodes, which contain multiple child nodes. + +Each node can emit some python string. python string emitted by the +root node is validated for safeeval and executed using python in the given environment. + +Enough care is taken to make sure the generated code and the template has line to line match, +so that the error messages can point to exact line number in template. (It doesn't work in some cases still.) + +Grammar: + + template -> defwith sections + defwith -> '$def with (' arguments ')' | '' + sections -> section* + section -> block | assignment | line + + assignment -> '$ ' + line -> (text|expr)* + text -> + expr -> '$' pyexpr | '$(' pyexpr ')' | '${' pyexpr '}' + pyexpr -> + +""" + +__all__ = [ + "Template", + "Render", "render", "frender", + "ParseError", "SecurityError", + "test" +] + +import tokenize +import os +import glob +import re + +from utils import storage, safeunicode, safestr, re_compile +from webapi import config +from net import websafe + +def splitline(text): + r""" + Splits the given text at newline. + + >>> splitline('foo\nbar') + ('foo\n', 'bar') + >>> splitline('foo') + ('foo', '') + >>> splitline('') + ('', '') + """ + index = text.find('\n') + 1 + if index: + return text[:index], text[index:] + else: + return text, '' + +class Parser: + """Parser Base. + """ + def __init__(self, text, name="