The branch, master has been updated
via 0f52e799322b101c324d906df6e0670e0141322a (commit)
from 481c657e340a912f0aaa5d3251a809d50f897594 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit 0f52e799322b101c324d906df6e0670e0141322a
Author: Aaron Swartz <m...@aaronsw.com>
Date: Thu Aug 13 15:56:51 2009 -0400
update web.py
-----------------------------------------------------------------------
Summary of changes:
vendor/webpy/setup.py | 4 +-
vendor/webpy/test.py | 58 ----
vendor/webpy/test/application.py | 162 +++++++++++-
vendor/webpy/test/db.py | 74 +++++-
vendor/webpy/test/doctests.py | 5 +-
vendor/webpy/test/session.py | 67 +----
vendor/webpy/test/webtest.py | 130 +--------
vendor/webpy/web/__init__.py | 52 +---
vendor/webpy/web/application.py | 164 +++++++----
vendor/webpy/web/browser.py | 14 +-
vendor/webpy/web/cheetah.py | 98 -------
vendor/webpy/web/contrib/template.py | 14 +-
vendor/webpy/web/db.py | 184 ++++++++-----
vendor/webpy/web/debugerror.py | 13 +-
vendor/webpy/web/form.py | 21 ++-
vendor/webpy/web/http.py | 109 +-------
vendor/webpy/web/httpserver.py | 3 +-
vendor/webpy/web/net.py | 39 ++-
vendor/webpy/web/session.py | 58 +++-
vendor/webpy/web/template.py | 235 +++++++++++++---
vendor/webpy/web/utils.py | 459 ++++++++++++++++++++-----------
vendor/webpy/web/webapi.py | 131 ++++++---
vendor/webpy/web/wsgi.py | 14 +-
vendor/webpy/web/wsgiserver/__init__.py | 124 ++++++---
24 files changed, 1309 insertions(+), 923 deletions(-)
delete mode 100644 vendor/webpy/test.py
delete mode 100644 vendor/webpy/web/cheetah.py
diff --git a/vendor/webpy/setup.py b/vendor/webpy/setup.py
index 761c632..db6f945 100644
--- a/vendor/webpy/setup.py
+++ b/vendor/webpy/setup.py
@@ -5,10 +5,12 @@
from distutils.core import setup
setup(name='web.py',
- version='0.3',
+ version='0.32',
description='web.py: makes web apps',
author='Aaron Swartz',
author_email='m...@aaronsw.com',
+ maintainer='Anand Chitipothu',
+ maintainer_email='anand...@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.",
diff --git a/vendor/webpy/test.py b/vendor/webpy/test.py
deleted file mode 100644
index 2036de0..0000000
--- a/vendor/webpy/test.py
+++ /dev/null
@@ -1,58 +0,0 @@
-"""
-web.py test suite.
-"""
-
-import sys
-import web
-import unittest
-
-def commit(value):
- web.transact()
- web.insert("test", seqname=False, value=value)
- web.commit()
-
-def rollback(value):
- web.transact()
- web.insert("test", seqname=False, value=value)
- web.rollback()
-
-def getrow(value):
- d = web.select('test', where='value=$value', vars=locals())
- a = (d and d[0].value) or None
- return a
-
-def assert_presence(*values):
- for v in values:
- assert getrow(v) is not None
-
-def assert_absence(*values):
- for v in values:
- assert getrow(v) is None
-
-class PostgresTest(unittest.TestCase):
- parameters = dict(dbn="postgres", db="webtest", user="postgres", pw="")
-
- def setUp(self):
- web.config.db_parameters = self.parameters
- #web.config.db_printing = True
- web.load()
- web.delete("test", where="1=1")
-
- def tearDown(self):
- sys.stdout.flush()
- sys.stderr.flush()
-
- def testCommit(self):
- commit(1)
- assert_presence(1)
-
- def testRollback(self):
- rollback(1)
- assert_absence(1)
-
-class MySQLTest(PostgresTest):
- parameters = dict(dbn="mysql", db="webtest", user="webpy", pw="")
-
-if __name__ == "__main__":
- unittest.main()
-
diff --git a/vendor/webpy/test/application.py b/vendor/webpy/test/application.py
index c6ccb4a..9129afd 100644
--- a/vendor/webpy/test/application.py
+++ b/vendor/webpy/test/application.py
@@ -2,6 +2,7 @@ import webtest
import time
import web
+import urllib
data = """
import web
@@ -15,6 +16,18 @@ class %(classname)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)
@@ -97,6 +110,30 @@ class ApplicationTest(webtest.TestCase):
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 = (
@@ -131,7 +168,130 @@ class ApplicationTest(webtest.TestCase):
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 --git a/vendor/webpy/test/db.py b/vendor/webpy/test/db.py
index a988aff..39a45f9 100644
--- a/vendor/webpy/test/db.py
+++ b/vendor/webpy/test/db.py
@@ -4,18 +4,27 @@ import web
class DBTest(webtest.TestCase):
dbname = 'postgres'
+ driver = None
def setUp(self):
- self.db = webtest.setup_database(self.dbname)
- self.db.query("CREATE TABLE person (name text, email text)")
+ 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)
+ 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"""
+ # Bug#177265: unicode queries throw errors
self.db.select('person', where='name=$name', vars={'name': u'\xf4'})
def assertRows(self, n):
@@ -68,6 +77,11 @@ class DBTest(webtest.TestCase):
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)
@@ -79,13 +93,42 @@ class DBTest(webtest.TestCase):
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"
@@ -94,5 +137,26 @@ class MySQLTest(DBTest):
# 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 --git a/vendor/webpy/test/doctests.py b/vendor/webpy/test/doctests.py
index 6cdb970..d795131 100644
--- a/vendor/webpy/test/doctests.py
+++ b/vendor/webpy/test/doctests.py
@@ -8,12 +8,11 @@ def suite():
"web.db",
"web.http",
"web.net",
- "web.request",
"web.session",
"web.template",
"web.utils",
- "web.webapi",
- "web.wsgi",
+# "web.webapi",
+# "web.wsgi",
]
return webtest.doctest_suite(modules)
diff --git a/vendor/webpy/test/session.py b/vendor/webpy/test/session.py
index ca88f5d..e0493fc 100644
--- a/vendor/webpy/test/session.py
+++ b/vendor/webpy/test/session.py
@@ -2,42 +2,6 @@ import webtest
import web
import tempfile
-class Browser:
- """Browser simulation.
- Stores cookies across requests.
- """
- def __init__(self, app):
- self.app = app
- self.cookies = {}
- self.response = None
-
- def open(self, path):
- headers = {}
- if self.cookies:
- headers['cookie'] = self.cookie_header()
- self.response = self.app.request(path, headers=headers)
- if 'Set-Cookie' in self.response.headers:
- self.read_cookie(self.response.headers['Set-Cookie'])
- return self.response.data
-
- def cookie_header(self):
- return "; ".join(["%s=%s" % (k, v) for k, v in self.cookies.items()])
-
- def read_cookie(self, header):
- tokens = header.split('; ')
- d = {}
- name, value = tokens[0].split("=")
- for t in tokens[1:]:
- k, v = t.split("=")
- d[k.lower()] = v
-
- #@@ fix this
- if 'expires' in d:
- d.pop('name', None)
- return
-
- self.cookies[name] = value
-
class SessionTest(webtest.TestCase):
def setUp(self):
app = web.auto_application()
@@ -61,30 +25,31 @@ class SessionTest(webtest.TestCase):
return web.session.Session(app, store, {'count': 0})
def testSession(self):
- b = Browser(self.app)
- self.assertEquals(b.open('/count'), '1')
- self.assertEquals(b.open('/count'), '2')
- self.assertEquals(b.open('/count'), '3')
+ 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'), '1')
+ self.assertEquals(b.open('/count').read(), '1')
def testParallelSessions(self):
- b1 = Browser(self.app)
- b2 = Browser(self.app)
+ b1 = self.app.browser()
+ b2 = self.app.browser()
b1.open('/count')
for i in range(1, 10):
- self.assertEquals(b1.open('/count'), str(i+1))
- self.assertEquals(b2.open('/count'), str(i))
+ self.assertEquals(b1.open('/count').read(), str(i+1))
+ self.assertEquals(b2.open('/count').read(), str(i))
def testBadSessionId(self):
- b = Browser(self.app)
- self.assertEquals(b.open('/count'), '1')
- self.assertEquals(b.open('/count'), '2')
-
- b.cookies['webpy_session_id'] = '/etc/password'
- self.assertEquals(b.open('/count'), '1')
+ 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."""
diff --git a/vendor/webpy/test/webtest.py b/vendor/webpy/test/webtest.py
index 74de9d4..1cbc6ca 100644
--- a/vendor/webpy/test/webtest.py
+++ b/vendor/webpy/test/webtest.py
@@ -1,130 +1,20 @@
"""webtest: test utilities.
"""
-import unittest
import sys, os
-# adding current directory to path to make sure local copy of web module is used.
+# adding current directory to path to make sure local modules can be imported
sys.path.insert(0, '.')
-import web
-
-
-class TestCase(unittest.TestCase):
- def setUpAll(self):
- pass
-
- def tearDownAll(self):
- pass
-
- def shortDescription(self):
- """overridden to not return docstrings"""
- return None
-
-class TestSuite(unittest.TestSuite):
- """A TestSuite with once per TestCase setUpAll() and tearDownAll().
- Adopted from test/testlib/testing.py file in SQLAlchemy test suite.
- """
-
- def __init__(self, tests=()):
- if len(tests) >0 and isinstance(tests[0], TestCase):
- self._initTest = tests[0]
- else:
- self._initTest = None
- unittest.TestSuite.__init__(self, tests)
-
- def do_run(self, result):
- # nice job unittest ! you switched __call__ and run() between py2.3
- # and 2.4 thereby making straight subclassing impossible !
- for test in self._tests:
- if result.shouldStop:
- break
- test(result)
- return result
-
- def run(self, result):
- return self(result)
-
- def __call__(self, result):
- try:
- if self._initTest is not None:
- self._initTest.setUpAll()
- except:
- # skip tests if global setup fails
- ex = self.__exc_info()
- for test in self._tests:
- result.addError(test, ex)
- return False
- try:
- return self.do_run(result)
- finally:
- try:
- if self._initTest is not None:
- self._initTest.tearDownAll()
- except:
- result.addError(self._initTest, self.__exc_info())
- pass
-
- def __exc_info(self):
- """Return a version of sys.exc_info() with the traceback frame
- minimised; usually the top level of the traceback frame is not
- needed.
- ripped off out of unittest module since its double __
- """
- exctype, excvalue, tb = sys.exc_info()
- if sys.platform[:4] == 'java': ## tracebacks look different in Jython
- return (exctype, excvalue, tb)
- return (exctype, excvalue, tb)
-
-# monkeypatch
-unittest.TestLoader.suiteClass = TestSuite
-
-def runTests(suite):
- runner = unittest.TextTestRunner()
- return runner.run(suite)
-
-def main(suite=None):
- if not suite:
- main_module = __import__('__main__')
- # allow command line switches
- args = [a for a in sys.argv[1:] if not a.startswith('-')]
- suite = module_suite(main_module, args or None)
-
- result = runTests(suite)
- sys.exit(not result.wasSuccessful())
-
-def load_modules(names):
- return [__import__(name, None, None, "x") for name in names]
-
-def module_suite(module, classnames=None):
- """Makes a suite from a module."""
- if hasattr(module, 'suite'):
- return module.suite()
- elif classnames:
- return unittest.TestLoader().loadTestsFromNames(classnames, module)
- else:
- return unittest.TestLoader().loadTestsFromModule(module)
-
-def doctest_suite(module_names):
- """Makes a test suite from doctests."""
- import doctest
- suite = unittest.TestSuite()
- for mod in load_modules(module_names):
- suite.addTest(doctest.DocTestSuite(mod))
- return suite
+from web.test import *
-def suite(module_names):
- """Creates a suite from multiple modules."""
- suite = unittest.TestSuite()
- for mod in load_modules(module_names):
- suite.addTest(module_suite(mod))
- return suite
-
-def setup_database(dbname, pooling=False):
+def setup_database(dbname, driver=None, pooling=False):
if dbname == 'sqlite':
- db = web.database(dbn=dbname, db='webpy.db', pooling=pooling)
+ 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)
-
- if '-v' in sys.argv:
- db.printing = True
+ db = web.database(dbn=dbname, db='webpy', user='scott', pw='tiger', pooling=pooling, driver=driver)
+
+ db.printing = '-v' in sys.argv
return db
diff --git a/vendor/webpy/web/__init__.py b/vendor/webpy/web/__init__.py
index 266a701..d85d30e 100644
--- a/vendor/webpy/web/__init__.py
+++ b/vendor/webpy/web/__init__.py
@@ -1,16 +1,16 @@
#!/usr/bin/env python
+"""web.py: makes web apps (http://webpy.org)"""
+
from __future__ import generators
-"""web.py: makes web apps (http://webpy.org)"""
-__version__ = "0.3"
-__revision__ = "$Rev$"
-__author__ = "Aaron Swartz <m...@aaronsw.com>"
+__version__ = "0.32"
+__author__ = [
+ "Aaron Swartz <m...@aaronsw.com>",
+ "Anand Chitipothu <anand...@gmail.com>"
+]
__license__ = "public domain"
__contributors__ = "see http://webpy.org/changes"
-# todo:
-# - some sort of accounts system
-
import utils, db, net, wsgi, http, webapi, httpserver, debugerror
import template, form
@@ -25,44 +25,10 @@ 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
-try:
- import cheetah
- from cheetah import *
-except ImportError:
- pass
-
-def main():
- import doctest
-
- doctest.testmod(utils)
- doctest.testmod(db)
- doctest.testmod(net)
- doctest.testmod(wsgi)
- doctest.testmod(http)
- doctest.testmod(webapi)
-
- try:
- doctest.testmod(cheetah)
- except NameError:
- pass
-
- template.test()
-
- import sys
- urls = ('/web.py', 'source')
- class source:
- def GET(self):
- header('Content-Type', 'text/python')
- return open(sys.argv[0]).read()
-
- if listget(sys.argv, 1) != 'test':
- app = application(urls, locals())
- app.run()
-
-if __name__ == "__main__": main()
-
diff --git a/vendor/webpy/web/application.py b/vendor/webpy/web/application.py
index adfcd12..f65ea09 100755
--- a/vendor/webpy/web/application.py
+++ b/vendor/webpy/web/application.py
@@ -15,6 +15,7 @@ import itertools
import os
import re
import types
+from exceptions import SystemExit
try:
import wsgiref.handlers
@@ -46,7 +47,10 @@ class application:
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__']
@@ -86,7 +90,33 @@ class application:
__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)
@@ -101,6 +131,7 @@ class application:
...
>>>
>>> def hello(handler): return "hello, " + handler()
+ ...
>>> app.add_processor(hello)
>>> app.request("/web.py").data
'hello, web.py'
@@ -175,7 +206,8 @@ class application:
if 'HTTP_CONTENT_TYPE' in env:
env['CONTENT_TYPE'] = env.pop('HTTP_CONTENT_TYPE')
- if data:
+ if method in ["POST", "PUT"]:
+ data = data or ''
import StringIO
if isinstance(data, dict):
q = urllib.urlencode(data)
@@ -189,7 +221,7 @@ class application:
response.status = status
response.headers = dict(headers)
response.header_items = headers
- response.data = "".join(self.wsgifunc(cleanup_threadlocal=False)(env, start_response))
+ response.data = "".join(self.wsgifunc()(env, start_response))
return response
def browser(self):
@@ -202,25 +234,24 @@ class application:
def handle_with_processors(self):
def process(processors):
- web.ctx.app_stack.append(self)
- if processors:
- p, processors = processors[0], processors[1:]
- return p(lambda: process(processors))
- else:
- return self.handle()
-
- try:
- # processors must be applied in the resvere order. (??)
- return process(self.processors)
- except web.HTTPError:
- raise
- except:
- print >> web.debug, traceback.format_exc()
- raise self.internalerror()
- finally:
- web.ctx.app_stack = web.ctx.app_stack[:-1]
+ 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, **kw):
+ def wsgifunc(self, *middleware):
"""Returns a WSGI-compatible function for this application."""
def peep(iterator):
"""Peeps into an iterator by doing an iteration
@@ -233,42 +264,36 @@ class application:
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 = [e.data]
- if is_generator(result):
- result = peep(result)
- else:
- result = [utils.utf8(result)]
+ result = web.utf8(iter(result))
status, headers = web.ctx.status, web.ctx.headers
start_resp(status, headers)
-
- #@@@
- # 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 kw.get('cleanup_threadlocal', True) and hasattr(t, '_d'):
- del t._d
-
- return result
+
+ def cleanup():
+ self._cleanup()
+ yield '' # force this function to be a generator
+
+ return itertools.chain(result, cleanup())
for m in middleware:
wsgi = m(wsgi)
@@ -315,7 +340,7 @@ class application:
if env.get('wsgi.url_scheme') in ['http', 'https']:
ctx.protocol = env['wsgi.url_scheme']
- elif env.get('HTTPS', '').lower() in ['on', 'true']:
+ elif env.get('HTTPS', '').lower() in ['on', 'true', '1']:
ctx.protocol = 'https'
else:
ctx.protocol = 'http'
@@ -331,6 +356,9 @@ class application:
# 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', '')
@@ -401,7 +429,7 @@ class application:
result = utils.re_compile('^' + pat + '$').match(value)
if result: # it's a match
- return what, [x and urllib.unquote(x) for x in result.groups()]
+ return what, [x for x in result.groups()]
return None, None
def _delegate_sub_application(self, dir, app):
@@ -411,18 +439,12 @@ class application:
@@Any issues with when used with yield?
"""
- try:
- 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()
- finally:
- web.ctx.home = oldctx.home
- web.ctx.homepath = oldctx.homepath
- web.ctx.path = oldctx.path
- web.ctx.fullpath = oldctx.fullpath
+ 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:
@@ -496,7 +518,7 @@ class subdomain_application(application):
>>> class hello:
... def GET(self): return "hello"
>>>
- >>> mapping = ("hello.example.com", app)
+ >>> mapping = (r"hello\.example\.com", app)
>>> app2 = subdomain_application(mapping)
>>> app2.request("/hello", host="hello.example.com").data
'hello'
@@ -519,7 +541,7 @@ class subdomain_application(application):
result = utils.re_compile('^' + pat + '$').match(value)
if result: # it's a match
- return what, [x and urllib.unquote(x) for x in result.groups()]
+ return what, [x for x in result.groups()]
return None, None
def loadhook(h):
@@ -548,9 +570,31 @@ def unloadhook(h):
"""
def processor(handler):
try:
- return handler()
- finally:
+ 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
@@ -617,7 +661,7 @@ class Reloader:
self.mtimes[mod] = mtime
except ImportError:
pass
-
+
if __name__ == "__main__":
import doctest
doctest.testmod()
diff --git a/vendor/webpy/web/browser.py b/vendor/webpy/web/browser.py
index 36da3df..66d859e 100644
--- a/vendor/webpy/web/browser.py
+++ b/vendor/webpy/web/browser.py
@@ -1,11 +1,10 @@
"""Browser to test web applications.
(from web.py)
"""
-from utils import re_compile, utf8
+from utils import re_compile
from net import htmlunquote
import httplib, urllib, urllib2
-import cookielib
import copy
from StringIO import StringIO
@@ -22,6 +21,7 @@ class BrowserError(Exception):
class Browser:
def __init__(self):
+ import cookielib
self.cookiejar = cookielib.CookieJar()
self._cookie_processor = urllib2.HTTPCookieProcessor(self.cookiejar)
self.form = None
@@ -65,7 +65,7 @@ class Browser:
def open(self, url, data=None, headers={}):
"""Opens the specified url."""
url = urllib.basejoin(self.url, url)
- req = urllib2.Request(utf8(url), data, headers)
+ req = urllib2.Request(url, data, headers)
return self.do_request(req)
def show(self):
@@ -220,8 +220,12 @@ class AppHandler(urllib2.HTTPHandler):
def https_open(self, req):
return self.http_open(req)
-
- https_request = urllib2.HTTPHandler.do_request_
+
+ 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])
diff --git a/vendor/webpy/web/cheetah.py b/vendor/webpy/web/cheetah.py
deleted file mode 100644
index db9fbf3..0000000
--- a/vendor/webpy/web/cheetah.py
+++ /dev/null
@@ -1,98 +0,0 @@
-"""
-Cheetah API
-(from web.py)
-"""
-
-__all__ = ["render"]
-
-import re, urlparse, pprint, traceback, sys
-from Cheetah.Compiler import Compiler
-from Cheetah.Filters import Filter
-from utils import re_compile, memoize, dictadd
-from net import htmlquote, websafe
-from webapi import ctx, header, output, input, cookies, loadhooks
-
-def upvars(level=2):
- """Guido van Rossum sez: don't use this function."""
- return dictadd(
- sys._getframe(level).f_globals,
- sys._getframe(level).f_locals)
-
-r_include = re_compile(r'(?!\\)#include \"(.*?)\"($|#)', re.M)
-def __compiletemplate(template, base=None, isString=False):
- if isString:
- text = template
- else:
- text = open('templates/'+template).read()
- # implement #include at compile-time
- def do_include(match):
- text = open('templates/'+match.groups()[0]).read()
- return text
- while r_include.findall(text):
- text = r_include.sub(do_include, text)
-
- execspace = _compiletemplate.bases.copy()
- tmpl_compiler = Compiler(source=text, mainClassName='GenTemplate')
- tmpl_compiler.addImportedVarNames(execspace.keys())
- exec str(tmpl_compiler) in execspace
- if base:
- _compiletemplate.bases[base] = execspace['GenTemplate']
-
- return execspace['GenTemplate']
-
-_compiletemplate = memoize(__compiletemplate)
-_compiletemplate.bases = {}
-
-def render(template, terms=None, asTemplate=False, base=None,
- isString=False):
- """
- Renders a template, caching where it can.
-
- `template` is the name of a file containing the a template in
- the `templates/` folder, unless `isString`, in which case it's the
- template itself.
-
- `terms` is a dictionary used to fill the template. If it's None, then
- the caller's local variables are used instead, plus context, if it's not
- already set, is set to `context`.
-
- If asTemplate is False, it `output`s the template directly. Otherwise,
- it returns the template object.
-
- If the template is a potential base template (that is, something other templates)
- can extend, then base should be a string with the name of the template. The
- template will be cached and made available for future calls to `render`.
-
- Requires [Cheetah](http://cheetahtemplate.org/).
- """
- # terms=['var1', 'var2'] means grab those variables
- if isinstance(terms, list):
- new = {}
- old = upvars()
- for k in terms:
- new[k] = old[k]
- terms = new
- # default: grab all locals
- elif terms is None:
- terms = {'context': ctx, 'ctx':ctx}
- terms.update(sys._getframe(1).f_locals)
- # terms=d means use d as the searchList
- if not isinstance(terms, tuple):
- terms = (terms,)
-
- if 'headers' in ctx and not isString and template.endswith('.html'):
- header('Content-Type','text/html; charset=utf-8', unique=True)
-
- if loadhooks.has_key('reloader'):
- compiled_tmpl = __compiletemplate(template, base=base, isString=isString)
- else:
- compiled_tmpl = _compiletemplate(template, base=base, isString=isString)
- compiled_tmpl = compiled_tmpl(searchList=terms, filter=WebSafe)
- if asTemplate:
- return compiled_tmpl
- else:
- return output(str(compiled_tmpl))
-
-class WebSafe(Filter):
- def filter(self, val, **keywords):
- return websafe(val)
diff --git a/vendor/webpy/web/contrib/template.py b/vendor/webpy/web/contrib/template.py
index 59d6135..7495d39 100644
--- a/vendor/webpy/web/contrib/template.py
+++ b/vendor/webpy/web/contrib/template.py
@@ -12,6 +12,7 @@ class render_cheetah:
"""Rendering interface to Cheetah Templates.
Example:
+
render = render_cheetah('templates')
render.hello(name="cheetah")
"""
@@ -32,13 +33,15 @@ class render_cheetah:
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')
"""
@@ -74,12 +77,17 @@ 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))
+ self._lookup = Environment(loader=FileSystemLoader(*a, **kwargs), extensions=extensions)
+ self._lookup.globals.update(globals)
def __getattr__(self, name):
# Assuming all templates end with .html
@@ -91,6 +99,7 @@ class render_mako:
"""Rendering interface to Mako Templates.
Example:
+
render = render_mako(directories=['templates'])
render.hello(name="mako")
"""
@@ -108,6 +117,7 @@ class cache:
"""Cache for any rendering interface.
Example:
+
render = cache(render_cheetah("templates/"))
render.hello(name='cache')
"""
diff --git a/vendor/webpy/web/db.py b/vendor/webpy/web/db.py
index 74d9da3..31940d8 100644
--- a/vendor/webpy/web/db.py
+++ b/vendor/webpy/web/db.py
@@ -21,10 +21,11 @@ from utils import threadeddict, storage, iters, iterbetter
try:
# db module can work independent of web.py
- from webapi import debug
+ from webapi import debug, config
except:
import sys
debug = sys.stderr
+ config = storage()
class UnknownDB(Exception):
"""raised for unsupported dbms"""
@@ -50,7 +51,8 @@ class UnknownParamstyle(Exception):
pass
class SQLParam:
- """Parameter in SQLQuery.
+ """
+ Parameter in SQLQuery.
>>> q = SQLQuery(["SELECT * FROM test WHERE name=", SQLParam("joe")])
>>> q
@@ -143,6 +145,16 @@ class SQLQuery:
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())
@@ -180,11 +192,11 @@ class SQLQuery:
"""
if len(items) == 0:
return SQLQuery("")
-
+
q = SQLQuery(items[0])
- for i in items[1:]:
- q = q + sep + i
-
+ for item in items[1:]:
+ q += sep
+ q += item
return q
join = staticmethod(join)
@@ -215,6 +227,20 @@ class SQLLiteral:
sqlliteral = SQLLiteral
+def _sqllist(values):
+ """
+ >>> _sqllist([1, 2, 3])
+ <sql: '(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
@@ -222,6 +248,8 @@ def reparam(string_, dictionary):
>>> reparam("s = $s", dict(s=True))
<sql: "s = 't'">
+ >>> reparam("s IN $s", dict(s=[1, 2]))
+ <sql: 's IN (1, 2)'>
"""
dictionary = dictionary.copy() # eval mucks with it
vals = []
@@ -229,7 +257,7 @@ def reparam(string_, dictionary):
for live, chunk in _interpolate(string_):
if live:
v = eval(chunk, dictionary)
- result.append(sqlparam(v))
+ result.append(sqlquote(v))
else:
result.append(chunk)
return SQLQuery.join(result, '')
@@ -283,13 +311,13 @@ def sqlors(left, lst):
for each item in the lst.
>>> sqlors('foo = ', [])
- <sql: '2+2=5'>
+ <sql: '1=2'>
>>> sqlors('foo = ', [1])
<sql: 'foo = 1'>
>>> sqlors('foo = ', 1)
<sql: 'foo = 1'>
>>> sqlors('foo = ', [1,2,3])
- <sql: '(foo = 1 OR foo = 2 OR foo = 3)'>
+ <sql: '(foo = 1 OR foo = 2 OR foo = 3 OR 1=2)'>
"""
if isinstance(lst, iters):
lst = list(lst)
@@ -326,8 +354,13 @@ def sqlquote(a):
>>> 'WHERE x = ' + sqlquote(True) + ' AND y = ' + sqlquote(3)
<sql: "WHERE x = 't' AND y = 3">
+ >>> 'WHERE x = ' + sqlquote(True) + ' AND y IN ' + sqlquote([2, 3])
+ <sql: "WHERE x = 't' AND y IN (2, 3)">
"""
- return sqlparam(a).sqlquery()
+ if isinstance(a, list):
+ return _sqllist(a)
+ else:
+ return sqlparam(a).sqlquery()
class Transaction:
"""Database transaction."""
@@ -402,12 +435,17 @@ class DB:
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 = False
+ self.printing = config.get('debug', False)
self.supports_multiple_insert = False
try:
@@ -418,7 +456,7 @@ class DB:
self.has_pooling = False
# Pooling can be disabled by passing pooling=False in the keywords.
- self.has_pooling = self.has_pooling and self.keywords.pop('pooling', True)
+ self.has_pooling = self.keywords.pop('pooling', True) and self.has_pooling
def _getctx(self):
if not self._ctx.get('db'):
@@ -495,17 +533,6 @@ class DB:
return '%s'
raise UnknownParamstyle, style
- def _py2sql(self, val):
- """
- Transforms a Python value into a value to pass to cursor.execute.
-
- This exists specifically for a workaround in SqliteDB.
-
- """
- if isinstance(val, unicode):
- val = val.encode('UTF-8')
- return val
-
def _db_execute(self, cur, sql_query):
"""executes an sql query"""
self.ctx.dbq_count += 1
@@ -513,9 +540,7 @@ class DB:
try:
a = time.time()
paramstyle = getattr(self, 'paramstyle', 'pyformat')
- out = cur.execute(sql_query.query(paramstyle),
- [self._py2sql(x)
- for x in sql_query.values()])
+ out = cur.execute(sql_query.query(paramstyle), sql_query.values())
b = time.time()
except:
if self.printing:
@@ -836,26 +861,17 @@ class PostgresDB(DB):
keywords['password'] = keywords['pw']
del keywords['pw']
- db_module = self.get_db_module()
+ 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
- self.supports_multiple_insert = True
-
DB.__init__(self, db_module, keywords)
+ self.supports_multiple_insert = True
- def get_db_module(self):
- try:
- import psycopg2 as db
- import psycopg2.extensions
- psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
- except ImportError:
- try:
- import psycopg as db
- except ImportError:
- import pgdb as db
- return db
-
def _process_insert_query(self, query, tablename, seqname):
if seqname is None:
seqname = tablename + "_id_seq"
@@ -877,25 +893,40 @@ class MySQLDB(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"
- self.supports_multiple_insert = True
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):
- try:
- import sqlite3 as db
+ db = import_driver(["sqlite3", "pysqlite2.dbapi2", "sqlite"], preferred=keywords.pop('driver', None))
+
+ if db.__name__ in ["sqlite3", "pysqlite2.dbapi2"]:
db.paramstyle = 'qmark'
- except ImportError:
- try:
- from pysqlite2 import dbapi2 as db
- db.paramstyle = 'qmark'
- except ImportError:
- import sqlite as db
+
self.paramstyle = db.paramstyle
keywords['database'] = keywords.pop('db')
self.dbname = "sqlite"
@@ -911,29 +942,6 @@ class SqliteDB(DB):
del out.__len__
return out
- # as with PostgresDB, the database is assumed to be in UTF-8.
- # This doesn't mean we turn byte-strings coming out of it into
- # Unicode objects, but we avoid trying to put Unicode objects into
- # it.
- encoding = 'UTF-8'
-
- def _py2sql(self, val):
- r"""
- Work around a couple of problems in SQLite that maybe pysqlite
- should take care of: give it True and False and it thinks
- they're column names; give it Unicode and it tries to insert
- it in, possibly, ASCII.
-
- >>> meth = SqliteDB(db='nonexistent')._py2sql
- >>> [meth(x) for x in [True, False, 1, 2, 'foo', u'souffl\xe9']]
- [1, 0, 1, 2, 'foo', 'souffl\xc3\xa9']
-
- """
- if val is True: return 1
- elif val is False: return 0
- elif isinstance(val, unicode): return val.encode(self.encoding)
- else: return val
-
class FirebirdDB(DB):
"""Firebird Database.
"""
@@ -971,11 +979,35 @@ class MSSQLDB(DB):
def __init__(self, **keywords):
import pymssql as db
if 'pw' in keywords:
- keywords['password'] = keywords.pop('kw')
+ 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)
+ <sql: 'SELECT * TOP 4 FROM foo'>
+ """
+ pass
+
class OracleDB(DB):
def __init__(self, **keywords):
import cx_Oracle as db
@@ -986,6 +1018,7 @@ class OracleDB(DB):
keywords['dsn'] = keywords.pop('db')
self.dbname = 'oracle'
db.paramstyle = 'numeric'
+ self.paramstyle = db.paramstyle
# oracle doesn't support pooling
keywords.pop('pooling', None)
@@ -1012,7 +1045,8 @@ def database(dburl=None, **params):
raise UnknownDB, dbn
def register_database(name, clazz):
- """Register a database.
+ """
+ Register a database.
>>> class LegacyDB(DB):
... def __init__(self, **params):
diff --git a/vendor/webpy/web/debugerror.py b/vendor/webpy/web/debugerror.py
index 5ff62ff..831be4a 100644
--- a/vendor/webpy/web/debugerror.py
+++ b/vendor/webpy/web/debugerror.py
@@ -298,15 +298,17 @@ def debugerror():
"""
return web._InternalError(djangoerror())
-def emailerrors(email_address, olderror):
+def emailerrors(to_address, olderror, from_address=None):
"""
Wraps the old `internalerror` handler (pass as `olderror`) to
- additionally email all errors to `email_address`, to aid in
+ 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()
@@ -314,8 +316,7 @@ def emailerrors(email_address, olderror):
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
- eaddr = email_address
+ request = web.ctx.method + ' ' + web.ctx.home + web.ctx.fullpath
text = ("""\
------here----
Content-Type: text/plain
@@ -331,8 +332,8 @@ Content-Disposition: attachment; filename="bug.html"
""" % locals()) + str(djangoerror())
sendmail(
- "your buggy site <%s>" % eaddr,
- "the bugfixer <%s>" % eaddr,
+ "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----"'})
diff --git a/vendor/webpy/web/form.py b/vendor/webpy/web/form.py
index fdd7152..f3dceee 100644
--- a/vendor/webpy/web/form.py
+++ b/vendor/webpy/web/form.py
@@ -13,7 +13,8 @@ def attrget(obj, attr, value=None):
return value
class Form:
- r"""HTML form.
+ r"""
+ HTML form.
>>> f = Form(Textbox("x"))
>>> f.render()
@@ -39,7 +40,18 @@ class Form:
out += "<td>"+i.pre+i.render()+i.post+"</td></tr>\n"
out += "</table>"
return out
-
+
+ def render_css(self):
+ out = []
+ out.append(self.rendernote(self.note))
+ for i in self.inputs:
+ out.append('<label for="%s">%s</label>' % (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 '<strong class="wrong">%s</strong>' % net.websafe(note)
else: return ""
@@ -162,7 +174,7 @@ class Dropdown(Input):
def render(self):
x = '<select name="%s"%s>\n' % (net.websafe(self.name), self.addatts())
for arg in self.args:
- if type(arg) == tuple:
+ if isinstance(arg, (tuple, list)):
value, desc= arg
else:
value, desc = arg, arg
@@ -218,6 +230,7 @@ class Hidden(Input):
def render(self):
x = '<input type="hidden" name="%s"' % net.websafe(self.name)
if self.value: x += ' value="%s"' % net.websafe(self.value)
+ x += self.addatts()
x += ' />'
return x
@@ -248,4 +261,4 @@ class regexp(Validator):
if __name__ == "__main__":
import doctest
- doctest.testmod()
\ No newline at end of file
+ doctest.testmod()
diff --git a/vendor/webpy/web/http.py b/vendor/webpy/web/http.py
index 5e99e39..5a32436 100644
--- a/vendor/webpy/web/http.py
+++ b/vendor/webpy/web/http.py
@@ -8,8 +8,7 @@ __all__ = [
"prefixurl", "modified",
"write",
"changequery", "url",
- "background", "backgrounder",
- "Reloader", "reloader", "profiler",
+ "profiler",
]
import sys, os, threading, urllib, urlparse
@@ -61,7 +60,13 @@ def modified(date=None, etag=None):
`True` and sets the response status to `304 Not Modified`. It also
sets `Last-Modified and `ETag` output headers.
"""
- n = set(x.strip('" ') for x in web.ctx.env.get('HTTP_IF_NONE_MATCH', '').split(','))
+ try:
+ from __builtin__ import set
+ except ImportError:
+ # for python 2.3
+ from sets import Set as set
+
+ n = set([x.strip('" ') for x in web.ctx.env.get('HTTP_IF_NONE_MATCH', '').split(',')])
m = net.parsehttpdate(web.ctx.env.get('HTTP_IF_MODIFIED_SINCE', '').split(';')[0])
validate = False
if etag:
@@ -74,8 +79,8 @@ def modified(date=None, etag=None):
validate = True
if validate: web.ctx.status = '304 Not Modified'
- lastmodified(date)
- web.header('ETag', '"' + etag + '"')
+ if date: lastmodified(date)
+ if etag: web.header('ETag', '"' + etag + '"')
return not validate
def write(cgi_response):
@@ -145,104 +150,12 @@ def url(path=None, **kw):
return out
-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
-
-class Reloader:
- """
- Before every request, checks to see if any loaded modules have changed on
- disk and, if so, reloads them.
- """
- def __init__(self, func):
- self.func = func
- self.mtimes = {}
- # cheetah:
- # b = _compiletemplate.bases
- # _compiletemplate = globals()['__compiletemplate']
- # _compiletemplate.bases = b
-
- #web.loadhooks['reloader'] = self.check
- # todo:
- # - replace relrcheck with a loadhook
- #if reloader in middleware:
- # relr = reloader(None)
- # relrcheck = relr.check
- # middleware.remove(reloader)
- #else:
- # relr = None
- # relrcheck = lambda: None
- # if relr:
- # relr.func = wsgifunc
- # return wsgifunc
- #
-
- def check(self):
- for mod in sys.modules.values():
- try:
- mtime = os.stat(mod.__file__).st_mtime
- except (AttributeError, OSError, IOError):
- continue
- if mod.__file__.endswith('.pyc') and \
- os.path.exists(mod.__file__[:-1]):
- mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime)
- if mod not in self.mtimes:
- self.mtimes[mod] = mtime
- elif self.mtimes[mod] < mtime:
- try:
- reload(mod)
- self.mtimes[mod] = mtime
- except ImportError:
- pass
- return True
-
- def __call__(self, e, o):
- self.check()
- return self.func(e, o)
-
-reloader = Reloader
-
def profiler(app):
"""Outputs basic profiling information at the bottom of each response."""
from utils import profile
def profile_internal(e, o):
out, result = profile(app)(e, o)
- return out + ['<pre>' + net.websafe(result) + '</pre>']
+ return list(out) + ['<pre>' + net.websafe(result) + '</pre>']
return profile_internal
if __name__ == "__main__":
diff --git a/vendor/webpy/web/httpserver.py b/vendor/webpy/web/httpserver.py
index 99ea4e2..317601e 100644
--- a/vendor/webpy/web/httpserver.py
+++ b/vendor/webpy/web/httpserver.py
@@ -3,6 +3,7 @@ __all__ = ["runsimple"]
import sys, os
import webapi as web
import net
+import utils
def runbasic(func, server_address=("0.0.0.0", 8080)):
"""
@@ -212,7 +213,7 @@ def runsimple(func, server_address=("0.0.0.0", 8080)):
time = self.log_date_time_string()
msg = self.format % (host, time, protocol, method, req, status)
- print >> outfile, msg.encode('utf-8')
+ print >> outfile, utils.safestr(msg)
func = WSGIWrapper(func)
server = CherryPyWSGIServer(server_address, func, server_name="localhost")
diff --git a/vendor/webpy/web/net.py b/vendor/webpy/web/net.py
index 1e6bb8a..6c3ee85 100644
--- a/vendor/webpy/web/net.py
+++ b/vendor/webpy/web/net.py
@@ -15,26 +15,47 @@ try: import datetime
except ImportError: pass
def validipaddr(address):
- """returns True if `address` is a valid IPv4 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('.')
- assert len(octets) == 4
+ if len(octets) != 4:
+ return False
for x in octets:
- assert 0 <= int(x) <= 255
- except (AssertionError, ValueError):
+ 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"""
+ """
+ Returns True if `port` is a valid IPv4 port.
+
+ >>> validipport('9000')
+ True
+ >>> validipport('foo')
+ False
+ >>> validipport('1000000')
+ False
+ """
try:
- assert 0 <= int(port) <= 65535
- except (AssertionError, ValueError):
+ 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`"""
+ """Returns `(ip_address, port)` from string `ip_addr_port`"""
addr = defaultaddr
port = defaultport
@@ -59,7 +80,7 @@ def validip(ip, defaultaddr="0.0.0.0", defaultport=8080):
def validaddr(string_):
"""
- returns either (ip_address, port) or "/path/to/socket" from string_
+ Returns either (ip_address, port) or "/path/to/socket" from string_
>>> validaddr('/path/to/socket')
'/path/to/socket'
diff --git a/vendor/webpy/web/session.py b/vendor/webpy/web/session.py
index fc4b482..a260318 100644
--- a/vendor/webpy/web/session.py
+++ b/vendor/webpy/web/session.py
@@ -137,31 +137,30 @@ class Session(utils.ThreadedDict):
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"""
- try:
- del self.store[self.session_id]
- except KeyError:
- pass
+ del self.store[self.session_id]
self._killed = True
class Store:
"""Base class for session stores"""
def __contains__(self, key):
- raise NotImplemented
+ raise NotImplementedError
def __getitem__(self, key):
- raise NotImplemented
+ raise NotImplementedError
def __setitem__(self, key, value):
- raise NotImplemented
+ raise NotImplementedError
def cleanup(self, timeout):
"""removes all the expired sessions"""
- raise NotImplemented
+ raise NotImplementedError
def encode(self, session_dict):
"""encodes session dict as a string"""
@@ -171,13 +170,11 @@ class Store:
def decode(self, session_data):
"""decodes the data to get back the session dict """
pickled = base64.decodestring(session_data)
- try:
- return pickle.loads(pickled)
- except:
- return {}
+ return pickle.loads(pickled)
class DiskStore(Store):
- """Store for saving a session on disk
+ """
+ Store for saving a session on disk.
>>> import tempfile
>>> root = tempfile.mkdtemp()
@@ -282,6 +279,41 @@ class DBStore(Store):
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 --git a/vendor/webpy/web/template.py b/vendor/webpy/web/template.py
index b11dfb3..e9dbdb1 100644
--- a/vendor/webpy/web/template.py
+++ b/vendor/webpy/web/template.py
@@ -36,16 +36,18 @@ __all__ = [
"test"
]
-import tokenize, compiler
+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.
+ r"""
+ Splits the given text at newline.
>>> splitline('foo\nbar')
('foo\n', 'bar')
@@ -418,7 +420,7 @@ class Parser:
r"""
>>> read_block_section = Parser('').read_block_section
>>> read_block_section('for i in range(10): hello $i\nfoo')
- (<block: 'for i in range(10):', [<line: [t' hello ', $i, t'\n']>]>, 'foo')
+ (<block: 'for i in range(10):', [<line: [t'hello ', $i, t'\n']>]>, 'foo')
>>> read_block_section('for i in range(10):\n hello $i\n foo', begin_indent=' ')
(<block: 'for i in range(10):', [<line: [t'hello ', $i, t'\n']>]>, ' foo')
>>> read_block_section('for i in range(10):\n hello $i\nfoo')
@@ -430,7 +432,7 @@ class Parser:
# if there is some thing left in the line
if line.strip():
- block = line
+ block = line.lstrip()
else:
def find_indent(text):
rx = re_compile(' +')
@@ -475,6 +477,12 @@ class PythonTokenizer:
t = self.next()
if t.value == delim:
break
+ elif t.value == '(':
+ self.consume_till(')')
+ elif t.value == '[':
+ self.consume_till(']')
+ elif t.value == '{':
+ self.consume_till('}')
# if end of line is found, it is an exception.
# Since there is no easy way to report the line number,
@@ -662,14 +670,17 @@ TEMPLATE_BUILTIN_NAMES = [
"set", "slice", "tuple", "xrange",
"abs", "all", "any", "callable", "chr", "cmp", "divmod", "filter", "hex",
"id", "isinstance", "iter", "len", "max", "min", "oct", "ord", "pow", "range",
- "True", "False"
+ "True", "False",
+ "None",
+ "__import__", # some c-libraries like datetime requires __import__ to present in the namespace
]
import __builtin__
TEMPLATE_BUILTINS = dict([(name, getattr(__builtin__, name)) for name in TEMPLATE_BUILTIN_NAMES if name in __builtin__.__dict__])
class ForLoop:
- """Wrapper for expression in for stament to support loop.xxx helpers.
+ """
+ Wrapper for expression in for stament to support loop.xxx helpers.
>>> loop = ForLoop()
>>> for x in loop.setup(['a', 'b', 'c']):
@@ -746,16 +757,18 @@ class ForLoopContext:
class BaseTemplate:
def __init__(self, code, filename, filter, globals, builtins):
- self.code = code
self.filename = filename
self.filter = filter
self._globals = globals
self._builtins = builtins
- self.t = self._compile()
+ if code:
+ self.t = self._compile(code)
+ else:
+ self.t = lambda: ''
- def _compile(self):
+ def _compile(self, code):
env = self.make_env(self._globals or {}, self._builtins)
- exec(self.code, env)
+ exec(code, env)
return env['__template__']
def __call__(self, *a, **kw):
@@ -801,22 +814,18 @@ class BaseTemplate:
class Template(BaseTemplate):
CONTENT_TYPES = {
'.html' : 'text/html; charset=utf-8',
+ '.xhtml' : 'application/xhtml+xml; charset=utf-8',
'.txt' : 'text/plain',
}
FILTERS = {
'.html': websafe,
+ '.xhtml': websafe,
'.xml': websafe
}
globals = {}
def __init__(self, text, filename='<template>', filter=None, globals=None, builtins=None):
- text = text.replace('\r\n', '\n').replace('\r', '\n').expandtabs()
- if not text.endswith('\n'):
- text += '\n'
-
- # support fort \$ for backward-compatibility
- text = text.replace(r'\$', '$$')
-
+ text = Template.normalize_text(text)
code = self.compile_template(text, filename)
_, ext = os.path.splitext(filename)
@@ -830,10 +839,26 @@ class Template(BaseTemplate):
BaseTemplate.__init__(self, code=code, filename=filename, filter=filter, globals=globals, builtins=builtins)
+ def normalize_text(text):
+ """Normalizes template text by correcting \r\n, tabs and BOM chars."""
+ text = text.replace('\r\n', '\n').replace('\r', '\n').expandtabs()
+ if not text.endswith('\n'):
+ text += '\n'
+
+ # ignore BOM chars at the begining of template
+ BOM = '\xef\xbb\xbf'
+ if isinstance(text, str) and text.startswith(BOM):
+ text = text[len(BOM):]
+
+ # support fort \$ for backward-compatibility
+ text = text.replace(r'\$', '$$')
+ return text
+ normalize_text = staticmethod(normalize_text)
+
def __call__(self, *a, **kw):
import webapi as web
if 'headers' in web.ctx and self.content_type:
- web.header('Content-Type', self.content_type)
+ web.header('Content-Type', self.content_type, unique=True)
return BaseTemplate.__call__(self, *a, **kw)
@@ -870,10 +895,22 @@ class Template(BaseTemplate):
raise
# make sure code is safe
+ import compiler
ast = compiler.parse(code)
SafeVisitor().walk(ast, filename)
return compiled_code
+
+class CompiledTemplate(Template):
+ def __init__(self, f, filename):
+ Template.__init__(self, '', filename)
+ self.t = f
+
+ def compile_template(self, *a):
+ return None
+
+ def _compile(self, *a):
+ return None
class Render:
"""The most preferred way of using templates.
@@ -889,28 +926,41 @@ class Render:
def __init__(self, loc='templates', cache=None, base=None, **keywords):
self._loc = loc
self._keywords = keywords
+
+ if cache is None:
+ cache = not config.get('debug', False)
- if not cache or config.get('debug', False):
- self._cache = None
- else:
+ if cache:
self._cache = {}
+ else:
+ self._cache = None
if base and not hasattr(base, '__call__'):
# make base a function, so that it can be passed to sub-renders
self._base = lambda page: self._template(base)(page)
else:
self._base = base
-
- def _load_template(self, name):
+
+ def _lookup(self, name):
path = os.path.join(self._loc, name)
if os.path.isdir(path):
- return Render(path, cache=self._cache is not None, base=self._base, **self._keywords)
+ return 'dir', path
else:
path = self._findfile(path)
if path:
- return Template(open(path).read(), filename=path, **self._keywords)
+ return 'file', path
else:
- raise AttributeError, "No template named " + name
+ return 'none', None
+
+ def _load_template(self, name):
+ kind, path = self._lookup(name)
+
+ if kind == 'dir':
+ return Render(path, cache=self._cache is not None, base=self._base, **self._keywords)
+ elif kind == 'file':
+ return Template(open(path).read(), filename=path, **self._keywords)
+ else:
+ raise AttributeError, "No template named " + name
def _findfile(self, path_prefix):
p = [f for f in glob.glob(path_prefix + '.*') if not f.endswith('~')] # skip backup files
@@ -932,14 +982,93 @@ class Render:
return template
else:
return self._template(name)
+
+class GAE_Render(Render):
+ # Render gets over-written. make a copy here.
+ super = Render
+ def __init__(self, loc, *a, **kw):
+ GAE_Render.super.__init__(self, loc, *a, **kw)
-render = Render
+ import types
+ if isinstance(loc, types.ModuleType):
+ self.mod = loc
+ else:
+ name = loc.rstrip('/').replace('/', '.')
+ self.mod = __import__(name, None, None, ['x'])
+
+ self.mod.__dict__.update(kw.get('builtins', TEMPLATE_BUILTINS))
+ self.mod.__dict__.update(Template.globals)
+ self.mod.__dict__.update(kw.get('globals', {}))
+
+ def _load_template(self, name):
+ t = getattr(self.mod, name)
+ import types
+ if isinstance(t, types.ModuleType):
+ return GAE_Render(t, cache=self._cache is not None, base=self._base, **self._keywords)
+ else:
+ return t
+render = Render
+# setup render for Google App Engine.
+try:
+ from google import appengine
+ render = Render = GAE_Render
+except ImportError:
+ pass
+
def frender(path, **keywords):
"""Creates a template from the given file path.
"""
return Template(open(path).read(), filename=path, **keywords)
+
+def compile_templates(root):
+ """Compiles templates to python code."""
+ re_start = re_compile('^', re.M)
+
+ for dirpath, dirnames, filenames in os.walk(root):
+ filenames = [f for f in filenames if not f.startswith('.') and not f.endswith('~') and not f.startswith('__init__.py')]
+
+ for d in dirnames[:]:
+ if d.startswith('.'):
+ dirnames.remove(d) # don't visit this dir
+
+ out = open(os.path.join(dirpath, '__init__.py'), 'w')
+ out.write('from web.template import CompiledTemplate, ForLoop\n\n')
+ if dirnames:
+ out.write("import " + ", ".join(dirnames))
+ for f in filenames:
+ path = os.path.join(dirpath, f)
+
+ if '.' in f:
+ name, _ = f.split('.', 1)
+ else:
+ name = f
+
+ text = open(path).read()
+ text = Template.normalize_text(text)
+ code = Template.generate_code(text, path)
+ code = re_start.sub(' ', code)
+
+ _gen = '' + \
+ '\ndef %s():' + \
+ '\n loop = ForLoop()' + \
+ '\n _dummy = CompiledTemplate(lambda: None, "dummy")' + \
+ '\n join_ = _dummy._join' + \
+ '\n escape_ = _dummy._escape' + \
+ '\n' + \
+ '\n%s' + \
+ '\n return __template__'
+
+ gen_code = _gen % (name, code)
+ out.write(gen_code)
+ out.write('\n\n')
+ out.write('%s = CompiledTemplate(%s(), %s)\n\n' % (name, name, repr(path)))
+
+ # create template to make sure it compiles
+ t = Template(open(path).read(), path)
+ out.close()
+
class ParseError(Exception):
pass
@@ -1120,25 +1249,30 @@ def test():
Test if, for and while.
>>> t('$if 1: 1')()
- u' 1\n'
+ u'1\n'
>>> t('$if 1:\n 1')()
u'1\n'
>>> t('$if 1:\n 1\\')()
u'1'
>>> t('$if 0: 0\n$elif 1: 1')()
- u' 1\n'
+ u'1\n'
>>> t('$if 0: 0\n$elif None: 0\n$else: 1')()
- u' 1\n'
+ u'1\n'
>>> t('$if 0 < 1 and 1 < 2: 1')()
- u' 1\n'
+ u'1\n'
>>> t('$for x in [1, 2, 3]: $x')()
- u' 1\n 2\n 3\n'
+ u'1\n2\n3\n'
>>> t('$def with (d)\n$for k, v in d.iteritems(): $k')({1: 1})
- u' 1\n'
+ u'1\n'
>>> t('$for x in [1, 2, 3]:\n\t$x')()
u' 1\n 2\n 3\n'
>>> t('$def with (a)\n$while a and a.pop():1')([1, 2, 3])
u'1\n1\n1\n'
+
+ The space after : must be ignored.
+
+ >>> t('$if True: foo')()
+ u'foo\n'
Test loop.xxx.
@@ -1156,9 +1290,9 @@ def test():
>>> t('$ a = {1: 1}\n$a.keys()[0]')()
u'1\n'
>>> t('$ a = []\n$if not a: 1')()
- u' 1\n'
+ u'1\n'
>>> t('$ a = {}\n$if not a: 1')()
- u' 1\n'
+ u'1\n'
>>> t('$ a = -1\n$a')()
u'-1\n'
>>> t('$ a = "1"\n$a')()
@@ -1234,6 +1368,7 @@ def test():
NameError: global name 'min' is not defined
Test vars.
+
>>> x = t('$var x: 1')()
>>> x.x
u'1'
@@ -1243,9 +1378,35 @@ def test():
>>> x = t('$var x: \n foo\n bar')()
>>> x.x
u'foo\nbar\n'
+
+ Test BOM chars.
+
+ >>> t('\xef\xbb\xbf$def with(x)\n$x')('foo')
+ u'foo\n'
+
+ Test for with weird cases.
+
+ >>> t('$for i in range(10)[1:5]:\n $i')()
+ u'1\n2\n3\n4\n'
+ >>> t("$for k, v in {'a': 1, 'b': 2}.items():\n $k $v")()
+ u'a 1\nb 2\n'
+ >>> t("$for k, v in ({'a': 1, 'b': 2}.items():\n $k $v")()
+ Traceback (most recent call last):
+ ...
+ SyntaxError: invalid syntax
+
+ Test datetime.
+
+ >>> import datetime
+ >>> t("$def with (date)\n$date.strftime('%m %Y')")(datetime.datetime(2009, 1, 1))
+ u'01 2009\n'
"""
pass
if __name__ == "__main__":
- import doctest
- doctest.testmod()
+ import sys
+ if '--compile' in sys.argv:
+ compile_templates(sys.argv[2])
+ else:
+ import doctest
+ doctest.testmod()
diff --git a/vendor/webpy/web/utils.py b/vendor/webpy/web/utils.py
index 3dc53a8..d047fb3 100755
--- a/vendor/webpy/web/utils.py
+++ b/vendor/webpy/web/utils.py
@@ -5,14 +5,14 @@ General Utilities
"""
__all__ = [
- "Storage", "storage", "storify",
- "iters",
- "rstrips", "lstrips", "strips",
+ "Storage", "storage", "storify",
+ "iters",
+ "rstrips", "lstrips", "strips",
"safeunicode", "safestr", "utf8",
"TimeoutError", "timelimit",
"Memoize", "memoize",
"re_compile", "re_subm",
- "group",
+ "group", "uniq", "iterview",
"IterBetter", "iterbetter",
"dictreverse", "dictfind", "dictfindall", "dictincr", "dictadd",
"listget", "intget", "datestr",
@@ -27,15 +27,25 @@ __all__ = [
"sendmail"
]
-import re, sys, time, threading, subprocess
+import re, sys, time, threading, itertools
+
+try:
+ import subprocess
+except ImportError:
+ subprocess = None
+
try: import datetime
except ImportError: pass
+try: set
+except NameError:
+ from sets import Set as set
+
class Storage(dict):
"""
A Storage object is like a dictionary except `obj.foo` can be used
in addition to `obj['foo']`.
-
+
>>> o = storage(a=1)
>>> o.a
1
@@ -49,24 +59,24 @@ class Storage(dict):
Traceback (most recent call last):
...
AttributeError: 'a'
-
+
"""
- def __getattr__(self, key):
+ def __getattr__(self, key):
try:
return self[key]
except KeyError, k:
raise AttributeError, k
-
- def __setattr__(self, key, value):
+
+ def __setattr__(self, key, value):
self[key] = value
-
+
def __delattr__(self, key):
try:
del self[key]
except KeyError, k:
raise AttributeError, k
-
- def __repr__(self):
+
+ def __repr__(self):
return '<Storage ' + dict.__repr__(self) + '>'
storage = Storage
@@ -74,16 +84,16 @@ storage = Storage
def storify(mapping, *requireds, **defaults):
"""
Creates a `storage` object from dictionary `mapping`, raising `KeyError` if
- d doesn't have all of the keys in `requireds` and using the default
+ d doesn't have all of the keys in `requireds` and using the default
values for keys found in `defaults`.
For example, `storify({'a':1, 'c':3}, b=2, c=0)` will return the equivalent of
`storage({'a':1, 'b':2, 'c':3})`.
-
- If a `storify` value is a list (e.g. multiple values in a form submission),
- `storify` returns the last element of the list, unless the key appears in
+
+ If a `storify` value is a list (e.g. multiple values in a form submission),
+ `storify` returns the last element of the list, unless the key appears in
`defaults` as a list. Thus:
-
+
>>> storify({'a':[1, 2]}).a
2
>>> storify({'a':[1, 2]}, a=[]).a
@@ -92,33 +102,37 @@ def storify(mapping, *requireds, **defaults):
[1]
>>> storify({}, a=[]).a
[]
-
+
Similarly, if the value has a `value` attribute, `storify will return _its_
value, unless the key appears in `defaults` as a dictionary.
-
+
>>> storify({'a':storage(value=1)}).a
1
>>> storify({'a':storage(value=1)}, a={}).a
<Storage {'value': 1}>
>>> storify({}, a={}).a
{}
-
+
Optionally, keyword parameter `_unicode` can be passed to convert all values to unicode.
-
+
>>> storify({'x': 'a'}, _unicode=True)
<Storage {'x': u'a'}>
+ >>> storify({'x': storage(value='a')}, x={}, _unicode=True)
+ <Storage {'x': <Storage {'value': 'a'}>}>
+ >>> storify({'x': storage(value='a')}, _unicode=True)
+ <Storage {'x': u'a'}>
"""
_unicode = defaults.pop('_unicode', False)
def unicodify(s):
if _unicode and isinstance(s, str): return safeunicode(s)
else: return s
-
+
def getvalue(x):
if hasattr(x, 'value'):
return unicodify(x.value)
else:
return unicodify(x)
-
+
stor = Storage()
for key in requireds + tuple(mapping.keys()):
value = mapping[key]
@@ -135,12 +149,12 @@ def storify(mapping, *requireds, **defaults):
for (key, value) in defaults.iteritems():
result = value
- if hasattr(stor, key):
+ if hasattr(stor, key):
result = stor[key]
- if value == () and not isinstance(result, tuple):
+ if value == () and not isinstance(result, tuple):
result = (result,)
setattr(stor, key, result)
-
+
return stor
iters = [list, tuple]
@@ -153,9 +167,9 @@ if sys.version_info < (2,6): # sets module deprecated in 2.6
try:
from sets import Set
iters.append(Set)
- except ImportError:
+ except ImportError:
pass
-
+
class _hack(tuple): pass
iters = _hack(iters)
iters.__doc__ = """
@@ -164,13 +178,13 @@ of lists, tuples, sets, and Sets are available in this version of Python.
"""
def _strips(direction, text, remove):
- if direction == 'l':
- if text.startswith(remove):
+ if direction == 'l':
+ if text.startswith(remove):
return text[len(remove):]
elif direction == 'r':
- if text.endswith(remove):
+ if text.endswith(remove):
return text[:-len(remove)]
- else:
+ else:
raise ValueError, "Direction needs to be r or l."
return text
@@ -180,32 +194,34 @@ def rstrips(text, remove):
>>> rstrips("foobar", "bar")
'foo'
-
+
"""
return _strips('r', text, remove)
def lstrips(text, remove):
"""
removes the string `remove` from the left of `text`
-
+
>>> lstrips("foobar", "foo")
'bar'
-
+
"""
return _strips('l', text, remove)
def strips(text, remove):
- """removes the string `remove` from the both sides of `text`
+ """
+ removes the string `remove` from the both sides of `text`
>>> strips("foobarfoo", "foo")
'bar'
-
+
"""
return rstrips(lstrips(text, remove), remove)
def safeunicode(obj, encoding='utf-8'):
- r"""Converts any given object to unicode string.
-
+ r"""
+ Converts any given object to unicode string.
+
>>> safeunicode('hello')
u'hello'
>>> safeunicode(2)
@@ -222,10 +238,11 @@ def safeunicode(obj, encoding='utf-8'):
return unicode(obj)
else:
return str(obj).decode(encoding)
-
+
def safestr(obj, encoding='utf-8'):
- r"""Converts any given object to utf-8 encoded string.
-
+ r"""
+ Converts any given object to utf-8 encoded string.
+
>>> safestr('hello')
'hello'
>>> safestr(u'\u1234')
@@ -237,23 +254,25 @@ def safestr(obj, encoding='utf-8'):
return obj.encode('utf-8')
elif isinstance(obj, str):
return obj
+ elif hasattr(obj, 'next') and hasattr(obj, '__iter__'): # iterator
+ return itertools.imap(safestr, obj)
else:
return str(obj)
# for backward-compatibility
utf8 = safestr
-
+
class TimeoutError(Exception): pass
def timelimit(timeout):
"""
A decorator to limit a function to `timeout` seconds, raising `TimeoutError`
if it takes longer.
-
+
>>> import time
>>> def meaningoflife():
... time.sleep(.2)
... return 42
- >>>
+ >>>
>>> timelimit(.1)(meaningoflife)()
Traceback (most recent call last):
...
@@ -261,7 +280,7 @@ def timelimit(timeout):
>>> timelimit(1)(meaningoflife)()
42
- _Caveat:_ The function isn't stopped after `timeout` seconds but continues
+ _Caveat:_ The function isn't stopped after `timeout` seconds but continues
executing in a separate thread. (There seems to be no way to kill a thread.)
inspired by <http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/473878>
@@ -296,32 +315,80 @@ def timelimit(timeout):
class Memoize:
"""
'Memoizes' a function, caching its return values for each input.
-
+ If `expires` is specified, values are recalculated after `expires` seconds.
+ If `background` is specified, values are recalculated in a separate thread.
+
+ >>> calls = 0
+ >>> def howmanytimeshaveibeencalled():
+ ... global calls
+ ... calls += 1
+ ... return calls
+ >>> fastcalls = memoize(howmanytimeshaveibeencalled)
+ >>> howmanytimeshaveibeencalled()
+ 1
+ >>> howmanytimeshaveibeencalled()
+ 2
+ >>> fastcalls()
+ 3
+ >>> fastcalls()
+ 3
>>> import time
- >>> def meaningoflife():
- ... time.sleep(.2)
- ... return 42
- >>> fastlife = memoize(meaningoflife)
- >>> meaningoflife()
- 42
- >>> timelimit(.1)(meaningoflife)()
- Traceback (most recent call last):
- ...
- TimeoutError: took too long
- >>> fastlife()
- 42
- >>> timelimit(.1)(fastlife)()
- 42
-
+ >>> fastcalls = memoize(howmanytimeshaveibeencalled, .1, background=False)
+ >>> fastcalls()
+ 4
+ >>> fastcalls()
+ 4
+ >>> time.sleep(.2)
+ >>> fastcalls()
+ 5
+ >>> def slowfunc():
+ ... time.sleep(.1)
+ ... return howmanytimeshaveibeencalled()
+ >>> fastcalls = memoize(slowfunc, .2, background=True)
+ >>> fastcalls()
+ 6
+ >>> timelimit(.05)(fastcalls)()
+ 6
+ >>> time.sleep(.2)
+ >>> timelimit(.05)(fastcalls)()
+ 6
+ >>> timelimit(.05)(fastcalls)()
+ 6
+ >>> time.sleep(.2)
+ >>> timelimit(.05)(fastcalls)()
+ 7
+ >>> fastcalls = memoize(slowfunc, None, background=True)
+ >>> threading.Thread(target=fastcalls).start()
+ >>> time.sleep(.01)
+ >>> fastcalls()
+ 9
"""
- def __init__(self, func):
+ def __init__(self, func, expires=None, background=True):
self.func = func
self.cache = {}
+ self.expires = expires
+ self.background = background
+ self.running = {}
+
def __call__(self, *args, **keywords):
key = (args, tuple(keywords.items()))
- if key not in self.cache:
- self.cache[key] = self.func(*args, **keywords)
- return self.cache[key]
+ if not self.running.get(key):
+ self.running[key] = threading.Lock()
+ def update(block=False):
+ if self.running[key].acquire(block):
+ try:
+ self.cache[key] = (self.func(*args, **keywords), time.time())
+ finally:
+ self.running[key].release()
+
+ if key not in self.cache:
+ update(block=True)
+ elif self.expires and (time.time() - self.cache[key][1]) > self.expires:
+ if self.background:
+ threading.Thread(target=update).start()
+ else:
+ update()
+ return self.cache[key][0]
memoize = Memoize
@@ -331,16 +398,16 @@ A memoized version of re.compile.
"""
class _re_subm_proxy:
- def __init__(self):
+ def __init__(self):
self.match = None
- def __call__(self, match):
+ def __call__(self, match):
self.match = match
return ''
def re_subm(pat, repl, string):
"""
Like re.sub, but returns the replacement _and_ the match object.
-
+
>>> t, m = re_subm('g(oo+)fball', r'f\\1lish', 'goooooofball')
>>> t
'foooooolish'
@@ -352,25 +419,88 @@ def re_subm(pat, repl, string):
compiled_pat.sub(proxy.__call__, string)
return compiled_pat.sub(repl, string), proxy.match
-def group(seq, size):
+def group(seq, size):
"""
Returns an iterator over a series of lists of length size from iterable.
>>> list(group([1,2,3,4], 2))
[[1, 2], [3, 4]]
"""
- if not hasattr(seq, 'next'):
+ if not hasattr(seq, 'next'):
seq = iter(seq)
- while True:
+ while True:
yield [seq.next() for i in xrange(size)]
+def uniq(seq):
+ """
+ Removes duplicate elements from a list.
+
+ >>> uniq([1,2,3,1,4,5,6])
+ [1, 2, 3, 4, 5, 6]
+ """
+ seen = set()
+ result = []
+ for item in seq:
+ if item in seen: continue
+ seen.add(item)
+ result.append(item)
+ return result
+
+def iterview(x):
+ """
+ Takes an iterable `x` and returns an iterator over it
+ which prints its progress to stderr as it iterates through.
+ """
+ WIDTH = 70
+
+ def plainformat(n, lenx):
+ return '%5.1f%% (%*d/%d)' % ((float(n)/lenx)*100, len(str(lenx)), n, lenx)
+
+ def bars(size, n, lenx):
+ val = int((float(n)*size)/lenx + 0.5)
+ if size - val:
+ spacing = ">" + (" "*(size-val))[1:]
+ else:
+ spacing = ""
+ return "[%s%s]" % ("="*val, spacing)
+
+ def eta(elapsed, n, lenx):
+ if n == 0:
+ return '--:--:--'
+ if n == lenx:
+ secs = int(elapsed)
+ else:
+ secs = int((elapsed/n) * (lenx-n))
+ mins, secs = divmod(secs, 60)
+ hrs, mins = divmod(mins, 60)
+
+ return '%02d:%02d:%02d' % (hrs, mins, secs)
+
+ def format(starttime, n, lenx):
+ out = plainformat(n, lenx) + ' '
+ if n == lenx:
+ end = ' '
+ else:
+ end = ' ETA '
+ end += eta(time.time() - starttime, n, lenx)
+ out += bars(WIDTH - len(out) - len(end), n, lenx)
+ out += end
+ return out
+
+ starttime = time.time()
+ lenx = len(x)
+ for n, y in enumerate(x):
+ sys.stderr.write('\r' + format(starttime, n, lenx))
+ yield y
+ sys.stderr.write('\r' + format(starttime, n+1, lenx) + '\n')
+
class IterBetter:
"""
- Returns an object that can be used as an iterator
- but can also be used via __getitem__ (although it
- cannot go backwards -- that is, you cannot request
+ Returns an object that can be used as an iterator
+ but can also be used via __getitem__ (although it
+ cannot go backwards -- that is, you cannot request
`iterbetter[0]` after requesting `iterbetter[1]`).
-
+
>>> import itertools
>>> c = iterbetter(itertools.count())
>>> c[1]
@@ -382,29 +512,31 @@ class IterBetter:
...
IndexError: already passed 3
"""
- def __init__(self, iterator):
+ def __init__(self, iterator):
self.i, self.c = iterator, 0
- def __iter__(self):
- while 1:
+ def __iter__(self):
+ while 1:
yield self.i.next()
self.c += 1
def __getitem__(self, i):
#todo: slices
- if i < self.c:
+ if i < self.c:
raise IndexError, "already passed "+str(i)
try:
- while i > self.c:
+ while i > self.c:
self.i.next()
self.c += 1
# now self.c == i
self.c += 1
return self.i.next()
- except StopIteration:
+ except StopIteration:
raise IndexError, str(i)
iterbetter = IterBetter
def dictreverse(mapping):
"""
+ Returns a new dictionary with keys and values swapped.
+
>>> dictreverse({1: 2, 3: 4})
{2: 1, 4: 3}
"""
@@ -412,23 +544,23 @@ def dictreverse(mapping):
def dictfind(dictionary, element):
"""
- Returns a key whose value in `dictionary` is `element`
+ Returns a key whose value in `dictionary` is `element`
or, if none exists, None.
-
+
>>> d = {1:2, 3:4}
>>> dictfind(d, 4)
3
>>> dictfind(d, 5)
"""
for (key, value) in dictionary.iteritems():
- if element is value:
+ if element is value:
return key
def dictfindall(dictionary, element):
"""
Returns the keys whose values in `dictionary` are `element`
or, if none exists, [].
-
+
>>> d = {1:4, 3:4}
>>> dictfindall(d, 4)
[1, 3]
@@ -443,9 +575,9 @@ def dictfindall(dictionary, element):
def dictincr(dictionary, element):
"""
- Increments `element` in `dictionary`,
+ Increments `element` in `dictionary`,
setting it to one if it doesn't exist.
-
+
>>> d = {1:2, 3:4}
>>> dictincr(d, 1)
3
@@ -464,7 +596,7 @@ def dictadd(*dicts):
"""
Returns a dictionary consisting of the keys in the argument dictionaries.
If they share a key, the value from the last argument is used.
-
+
>>> dictadd({1: 0, 2: 0}, {2: 1, 3: 1})
{1: 0, 2: 1, 3: 1}
"""
@@ -476,21 +608,21 @@ def dictadd(*dicts):
def listget(lst, ind, default=None):
"""
Returns `lst[ind]` if it exists, `default` otherwise.
-
+
>>> listget(['a'], 0)
'a'
>>> listget(['a'], 1)
>>> listget(['a'], 1, 'b')
'b'
"""
- if len(lst)-1 < ind:
+ if len(lst)-1 < ind:
return default
return lst[ind]
def intget(integer, default=None):
"""
Returns `integer` as an int or `default` if it can't.
-
+
>>> intget('3')
3
>>> intget('3a')
@@ -505,7 +637,7 @@ def intget(integer, default=None):
def datestr(then, now=None):
"""
Converts a (UTC) datetime object to a nice string representation.
-
+
>>> from datetime import datetime, timedelta
>>> d = datetime(1970, 5, 1)
>>> datestr(d, now=d)
@@ -528,6 +660,8 @@ def datestr(then, now=None):
'January 1, 1969'
>>> datestr(datetime(1970, 6, 1), now=d)
'June 1, 1970'
+ >>> datestr(None)
+ ''
"""
def agohence(n, what, divisor=None):
if divisor: n = n // divisor
@@ -541,9 +675,9 @@ def datestr(then, now=None):
out += 'ago'
return out # '2 days ago'
- if not then: return ""
oneday = 24 * 60 * 60
+ if not then: return ""
if not now: now = datetime.datetime.utcnow()
if type(now).__name__ == "DateTime":
now = datetime.datetime.fromtimestamp(now)
@@ -551,6 +685,7 @@ def datestr(then, now=None):
then = datetime.datetime.fromtimestamp(then)
elif type(then).__name__ == "date":
then = datetime.datetime(then.year, then.month, then.day)
+
delta = now - then
deltaseconds = int(delta.days * oneday + delta.seconds + delta.microseconds * 1e-06)
deltadays = abs(deltaseconds) // oneday
@@ -583,12 +718,12 @@ def datestr(then, now=None):
def numify(string):
"""
Removes all non-digit characters from `string`.
-
+
>>> numify('800-555-1212')
'8005551212'
>>> numify('800.555.1212')
'8005551212'
-
+
"""
return ''.join([c for c in str(string) if c.isdigit()])
@@ -596,10 +731,10 @@ def denumify(string, pattern):
"""
Formats `string` according to `pattern`, where the letter X gets replaced
by characters from `string`.
-
+
>>> denumify("8005551212", "(XXX) XXX-XXXX")
'(800) 555-1212'
-
+
"""
out = []
for c in pattern:
@@ -613,7 +748,7 @@ def denumify(string, pattern):
def commify(n):
"""
Add commas to an integer `n`.
-
+
>>> commify(1)
'1'
>>> commify(123)
@@ -632,7 +767,7 @@ def commify(n):
'1,234.50'
>>> commify(None)
>>>
-
+
"""
if n is None: return None
n = str(n)
@@ -640,9 +775,9 @@ def commify(n):
dollars, cents = n.split('.')
else:
dollars, cents = n, None
-
+
r = []
- for i, c in enumerate(reversed(str(dollars))):
+ for i, c in enumerate(str(dollars)[::-1]):
if i and (not (i % 3)):
r.insert(0, ',')
r.insert(0, c)
@@ -650,6 +785,7 @@ def commify(n):
if cents:
out += '.' + cents
return out
+
def dateify(datestring):
"""
Formats a numified `datestring` properly.
@@ -674,14 +810,15 @@ def nthstr(n):
['111th', '112th', '113th', '114th', '115th']
"""
-
+
assert n >= 0
if n % 100 in [11, 12, 13]: return '%sth' % n
return {1: '%sst', 2: '%snd', 3: '%srd'}.get(n % 10, '%sth') % n
def cond(predicate, consequence, alternative=None):
- """Function replacement for if-else to use in expressions.
-
+ """
+ Function replacement for if-else to use in expressions.
+
>>> x = 2
>>> cond(x % 2 == 0, "even", "odd")
'even'
@@ -696,15 +833,15 @@ def cond(predicate, consequence, alternative=None):
class CaptureStdout:
"""
Captures everything `func` prints to stdout and returns it instead.
-
+
>>> def idiot():
... print "foo"
>>> capturestdout(idiot)()
'foo\\n'
-
+
**WARNING:** Not threadsafe!
"""
- def __init__(self, func):
+ def __init__(self, func):
self.func = func
def __call__(self, *args, **keywords):
from cStringIO import StringIO
@@ -712,9 +849,9 @@ class CaptureStdout:
out = StringIO()
oldstdout = sys.stdout
sys.stdout = out
- try:
+ try:
self.func(*args, **keywords)
- finally:
+ finally:
sys.stdout = oldstdout
return out.getvalue()
@@ -724,14 +861,14 @@ class Profile:
"""
Profiles `func` and returns a tuple containing its output
and a string with human-readable profiling information.
-
+
>>> import time
>>> out, inf = profile(time.sleep)(.001)
>>> out
>>> inf[:10].strip()
'took 0.0'
"""
- def __init__(self, func):
+ def __init__(self, func):
self.func = func
def __call__(self, *args): ##, **kw): kw unused
import hotshot, hotshot.stats, tempfile ##, time already imported
@@ -743,15 +880,17 @@ class Profile:
stime = time.time() - stime
prof.close()
- def print_stats():
- stats = hotshot.stats.load(temp.name)
- stats.strip_dirs()
- stats.sort_stats('time', 'calls')
- stats.print_stats(40)
- stats.print_callers()
+ import cStringIO
+ out = cStringIO.StringIO()
+ stats = hotshot.stats.load(temp.name)
+ stats.stream = out
+ stats.strip_dirs()
+ stats.sort_stats('time', 'calls')
+ stats.print_stats(40)
+ stats.print_callers()
x = '\n\ntook '+ str(stime) + ' seconds\n'
- x += capturestdout(print_stats)()
+ x += out.getvalue()
return result, x
@@ -770,31 +909,31 @@ if not hasattr(traceback, 'format_exc'):
def tryall(context, prefix=None):
"""
- Tries a series of functions and prints their results.
- `context` is a dictionary mapping names to values;
+ Tries a series of functions and prints their results.
+ `context` is a dictionary mapping names to values;
the value will only be tried if it's callable.
-
+
>>> tryall(dict(j=lambda: True))
j: True
----------------------------------------
results:
True: 1
- For example, you might have a file `test/stuff.py`
- with a series of functions testing various things in it.
+ For example, you might have a file `test/stuff.py`
+ with a series of functions testing various things in it.
At the bottom, have a line:
if __name__ == "__main__": tryall(globals())
- Then you can run `python test/stuff.py` and get the results of
+ Then you can run `python test/stuff.py` and get the results of
all the tests.
"""
context = context.copy() # vars() would update
results = {}
for (key, value) in context.iteritems():
- if not hasattr(value, '__call__'):
+ if not hasattr(value, '__call__'):
continue
- if prefix and not key.startswith(prefix):
+ if prefix and not key.startswith(prefix):
continue
print key + ':',
try:
@@ -805,21 +944,23 @@ def tryall(context, prefix=None):
print 'ERROR'
dictincr(results, 'ERROR')
print ' ' + '\n '.join(traceback.format_exc().split('\n'))
-
+
print '-'*40
print 'results:'
for (key, value) in results.iteritems():
print ' '*2, str(key)+':', value
-
+
class ThreadedDict:
- """Thread local storage.
-
+ """
+ Thread local storage.
+
>>> d = ThreadedDict()
>>> d.x = 1
>>> d.x
1
>>> import threading
>>> def f(): d.x = 2
+ ...
>>> t = threading.Thread(target=f)
>>> t.start()
>>> t.join()
@@ -835,7 +976,7 @@ class ThreadedDict:
def __delattr__(self, key):
return delattr(self._getd(), key)
- def __hash__(self):
+ def __hash__(self):
return id(self)
def _getd(self):
@@ -855,25 +996,25 @@ threadeddict = ThreadedDict
def autoassign(self, locals):
"""
Automatically assigns local variables to `self`.
-
+
>>> self = storage()
>>> autoassign(self, dict(a=1, b=2))
>>> self
<Storage {'a': 1, 'b': 2}>
-
+
Generally used in `__init__` methods, as in:
def __init__(self, foo, bar, baz=1): autoassign(self, locals())
"""
for (key, value) in locals.iteritems():
- if key == 'self':
+ if key == 'self':
continue
setattr(self, key, value)
def to36(q):
"""
Converts an integer to base 36 (a useful scheme for human-sayable IDs).
-
+
>>> to36(35)
'z'
>>> to36(119292)
@@ -884,9 +1025,9 @@ def to36(q):
'0'
>>> to36(-393)
Traceback (most recent call last):
- ...
+ ...
ValueError: must supply a positive integer
-
+
"""
if q < 0: raise ValueError, "must supply a positive integer"
letters = "0123456789abcdefghijklmnopqrstuvwxyz"
@@ -917,12 +1058,12 @@ def safemarkdown(text):
def sendmail(from_address, to_address, subject, message, headers=None, **kw):
"""
Sends the email message `message` with mail and envelope headers
- for from `from_address_` to `to_address` with `subject`.
- Additional email headers can be specified with the dictionary
+ for from `from_address_` to `to_address` with `subject`.
+ Additional email headers can be specified with the dictionary
`headers.
If `web.config.smtp_server` is set, it will send the message
- to that SMTP server. Otherwise it will look for
+ to that SMTP server. Otherwise it will look for
`/usr/sbin/sendmail`, the typical location for the sendmail-style
binary. To use sendmail from a different path, set `web.config.sendmail_path`.
"""
@@ -930,12 +1071,12 @@ def sendmail(from_address, to_address, subject, message, headers=None, **kw):
import webapi
except ImportError:
webapi = Storage(config=Storage())
-
+
if headers is None: headers = {}
-
+
cc = kw.get('cc', [])
bcc = kw.get('bcc', [])
-
+
def listify(x):
if not isinstance(x, list):
return [safestr(x)]
@@ -949,7 +1090,7 @@ def sendmail(from_address, to_address, subject, message, headers=None, **kw):
bcc = listify(bcc)
recipients = to_address + cc + bcc
-
+
headers = dictadd({
'MIME-Version': '1.0',
'Content-Type': 'text/plain; charset=UTF-8',
@@ -961,7 +1102,7 @@ def sendmail(from_address, to_address, subject, message, headers=None, **kw):
if cc:
headers['Cc'] = ", ".join(cc)
-
+
import email.Utils
from_address = email.Utils.parseaddr(from_address)[1]
recipients = [email.Utils.parseaddr(r)[1] for r in recipients]
@@ -971,7 +1112,7 @@ def sendmail(from_address, to_address, subject, message, headers=None, **kw):
if webapi.config.get('smtp_server'):
server = webapi.config.get('smtp_server')
port = webapi.config.get('smtp_port', 0)
- username = webapi.config.get('smtp_username')
+ username = webapi.config.get('smtp_username')
password = webapi.config.get('smtp_password')
debug_level = webapi.config.get('smtp_debuglevel', None)
starttls = webapi.config.get('smtp_starttls', False)
@@ -994,15 +1135,25 @@ def sendmail(from_address, to_address, subject, message, headers=None, **kw):
smtpserver.quit()
else:
sendmail = webapi.config.get('sendmail_path', '/usr/sbin/sendmail')
-
+
assert not from_address.startswith('-'), 'security'
for r in recipients:
assert not r.startswith('-'), 'security'
-
- p = subprocess.Popen(['/usr/sbin/sendmail', '-f', from_address] + recipients, stdin=subprocess.PIPE)
- p.stdin.write(message)
- p.stdin.close()
- p.wait()
+
+ cmd = [sendmail, '-f', from_address] + recipients
+
+ if subprocess:
+ p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
+ p.stdin.write(message)
+ p.stdin.close()
+ p.wait()
+ else:
+ import os
+ i, o = os.popen2(cmd)
+ i.write(message)
+ i.close()
+ o.close()
+ del i, o
if __name__ == "__main__":
import doctest
diff --git a/vendor/webpy/web/webapi.py b/vendor/webpy/web/webapi.py
index 62f4824..06a4672 100644
--- a/vendor/webpy/web/webapi.py
+++ b/vendor/webpy/web/webapi.py
@@ -10,11 +10,22 @@ __all__ = [
"setcookie", "cookies",
"ctx",
"HTTPError",
- "BadRequest", "NotFound", "Gone", "InternalError",
- "badrequest", "notfound", "gone", "internalerror",
- "Redirect", "Found", "SeeOther", "TempRedirect",
- "redirect", "found", "seeother", "tempredirect",
- "NoMethod", "nomethod",
+
+ # 200, 201, 202
+ "OK", "Created", "Accepted",
+ "ok", "created", "accepted",
+
+ # 301, 302, 303, 304, 407
+ "Redirect", "Found", "SeeOther", "NotModified", "TempRedirect",
+ "redirect", "found", "seeother", "notmodified", "tempredirect",
+
+ # 400, 401, 403, 404, 405, 406, 409, 410, 412
+ "BadRequest", "Unauthorized", "Forbidden", "NoMethod", "NotFound", "NotAcceptable", "Conflict", "Gone", "PreconditionFailed",
+ "badrequest", "unauthorized", "forbidden", "nomethod", "notfound", "notacceptable", "conflict", "gone", "preconditionfailed",
+
+ # 500
+ "InternalError",
+ "internalerror",
]
import sys, cgi, Cookie, pprint, urlparse, urllib
@@ -29,52 +40,31 @@ A configuration object for various aspects of web.py.
"""
class HTTPError(Exception):
- def __init__(self, status, headers, data=""):
+ def __init__(self, status, headers={}, data=""):
ctx.status = status
for k, v in headers.items():
header(k, v)
self.data = data
Exception.__init__(self, status)
+
+def _status_code(status, data=None, classname=None, docstring=None):
+ if data is None:
+ data = status.split(" ", 1)[1]
+ classname = status.split(" ", 1)[1].replace(' ', '') # 304 Not Modified -> NotModified
+ docstring = docstring or '`%s` status' % status
-class BadRequest(HTTPError):
- """`400 Bad Request` error."""
- message = "bad request"
- def __init__(self):
- status = "400 Bad Request"
- headers = {'Content-Type': 'text/html'}
- HTTPError.__init__(self, status, headers, self.message)
-
-badrequest = BadRequest
-
-class _NotFound(HTTPError):
- """`404 Not Found` error."""
- message = "not found"
- def __init__(self, message=None):
- status = '404 Not Found'
- headers = {'Content-Type': 'text/html'}
- HTTPError.__init__(self, status, headers, message or self.message)
-
-def NotFound(message=None):
- """Returns HTTPError with '404 Not Found' error from the active application.
- """
- if message:
- return _NotFound(message)
- elif ctx.get('app_stack'):
- return ctx.app_stack[-1].notfound()
- else:
- return _NotFound()
-
-notfound = NotFound
-
-class Gone(HTTPError):
- """`410 Gone` error."""
- message = "gone"
- def __init__(self):
- status = '410 Gone'
- headers = {'Content-Type': 'text/html'}
- HTTPError.__init__(self, status, headers, self.message)
+ def __init__(self, data=data, headers={}):
+ HTTPError.__init__(self, status, headers, data)
+
+ # trick to create class dynamically with dynamic docstring.
+ return type(classname, (HTTPError, object), {
+ '__doc__': docstring,
+ '__init__': __init__
+ })
-gone = Gone
+ok = OK = _status_code("200 OK", data="")
+created = Created = _status_code("201 Created")
+accepted = Accepted = _status_code("202 Accepted")
class Redirect(HTTPError):
"""A `301 Moved Permanently` redirect."""
@@ -115,6 +105,13 @@ class SeeOther(Redirect):
seeother = SeeOther
+class NotModified(HTTPError):
+ """A `304 Not Modified` status."""
+ def __init__(self):
+ HTTPError.__init__(self, "304 Not Modified")
+
+notmodified = NotModified
+
class TempRedirect(Redirect):
"""A `307 Temporary Redirect` redirect."""
def __init__(self, url, absolute=False):
@@ -122,6 +119,42 @@ class TempRedirect(Redirect):
tempredirect = TempRedirect
+class BadRequest(HTTPError):
+ """`400 Bad Request` error."""
+ message = "bad request"
+ def __init__(self):
+ status = "400 Bad Request"
+ headers = {'Content-Type': 'text/html'}
+ HTTPError.__init__(self, status, headers, self.message)
+
+badrequest = BadRequest
+
+class _NotFound(HTTPError):
+ """`404 Not Found` error."""
+ message = "not found"
+ def __init__(self, message=None):
+ status = '404 Not Found'
+ headers = {'Content-Type': 'text/html'}
+ HTTPError.__init__(self, status, headers, message or self.message)
+
+def NotFound(message=None):
+ """Returns HTTPError with '404 Not Found' error from the active application.
+ """
+ if message:
+ return _NotFound(message)
+ elif ctx.get('app_stack'):
+ return ctx.app_stack[-1].notfound()
+ else:
+ return _NotFound()
+
+notfound = NotFound
+
+unauthorized = Unauthorized = _status_code("401 Unauthorized")
+forbidden = Forbidden = _status_code("403 Forbidden")
+notacceptable = NotAcceptable = _status_code("406 Not Acceptable")
+conflict = Conflict = _status_code("409 Conflict")
+preconditionfailed = PreconditionFailed = _status_code("412 Precondition Failed")
+
class NoMethod(HTTPError):
"""A `405 Method Not Allowed` error."""
def __init__(self, cls=None):
@@ -139,6 +172,16 @@ class NoMethod(HTTPError):
nomethod = NoMethod
+class Gone(HTTPError):
+ """`410 Gone` error."""
+ message = "gone"
+ def __init__(self):
+ status = '410 Gone'
+ headers = {'Content-Type': 'text/html'}
+ HTTPError.__init__(self, status, headers, self.message)
+
+gone = Gone
+
class _InternalError(HTTPError):
"""500 Internal Server Error`."""
message = "internal server error"
diff --git a/vendor/webpy/web/wsgi.py b/vendor/webpy/web/wsgi.py
index 8616ba7..f3f0488 100644
--- a/vendor/webpy/web/wsgi.py
+++ b/vendor/webpy/web/wsgi.py
@@ -51,6 +51,16 @@ def runwsgi(func):
else:
return runscgi(func)
- # When running the builtin-server, enable debug mode if not already set.
- web.config.setdefault('debug', True)
return httpserver.runsimple(func, validip(listget(sys.argv, 1, '')))
+
+def _is_dev_mode():
+ # quick hack to check if the program is running in dev mode.
+ if os.environ.has_key('SERVER_SOFTWARE') \
+ or os.environ.has_key('PHP_FCGI_CHILDREN') \
+ or 'fcgi' in sys.argv or 'fastcgi' in sys.argv \
+ or 'mod_wsgi' in sys.argv:
+ return False
+ return True
+
+# When running the builtin-server, enable debug mode if not already set.
+web.config.setdefault('debug', _is_dev_mode())
diff --git a/vendor/webpy/web/wsgiserver/__init__.py b/vendor/webpy/web/wsgiserver/__init__.py
index d8ab40c..c380e18 100644
--- a/vendor/webpy/web/wsgiserver/__init__.py
+++ b/vendor/webpy/web/wsgiserver/__init__.py
@@ -88,6 +88,9 @@ try:
import cStringIO as StringIO
except ImportError:
import StringIO
+
+_fileobject_uses_str_type = isinstance(socket._fileobject(None)._rbuf, basestring)
+
import sys
import threading
import time
@@ -332,7 +335,12 @@ class HTTPRequest(object):
environ = self.environ
- method, path, req_protocol = request_line.strip().split(" ", 2)
+ try:
+ method, path, req_protocol = request_line.strip().split(" ", 2)
+ except ValueError:
+ self.simple_response(400, "Malformed Request-Line")
+ return
+
environ["REQUEST_METHOD"] = method
# path may be an abs_path (including "http://host.domain.tld");
@@ -402,13 +410,6 @@ class HTTPRequest(object):
self.simple_response("413 Request Entity Too Large")
return
- # Set AUTH_TYPE, REMOTE_USER
- creds = environ.get("HTTP_AUTHORIZATION", "").split(" ", 1)
- environ["AUTH_TYPE"] = creds[0]
- if creds[0].lower() == 'basic':
- user, pw = base64.decodestring(creds[1]).split(":", 1)
- environ["REMOTE_USER"] = user
-
# Persistent connection support
if self.response_protocol == "HTTP/1.1":
# Both server and client are HTTP/1.1
@@ -588,7 +589,12 @@ class HTTPRequest(object):
buf.append("\r\n")
if msg:
buf.append(msg)
- self.wfile.sendall("".join(buf))
+
+ try:
+ self.wfile.sendall("".join(buf))
+ except socket.error, x:
+ if x.args[0] not in socket_errors_to_ignore:
+ raise
def start_response(self, status, headers, exc_info = None):
"""WSGI callable to begin the HTTP response."""
@@ -646,7 +652,8 @@ class HTTPRequest(object):
if status < 200 or status in (204, 205, 304):
pass
else:
- if self.response_protocol == 'HTTP/1.1':
+ if (self.response_protocol == 'HTTP/1.1'
+ and self.environ["REQUEST_METHOD"] != 'HEAD'):
# Use the chunked transfer-coding
self.chunked_write = True
self.outheaders.append(("Transfer-Encoding", "chunked"))
@@ -710,7 +717,8 @@ class FatalSSLAlert(Exception):
"""Exception raised when the SSL implementation signals a fatal alert."""
pass
-if sys.version_info[:2] >= (2, 6):
+
+if not _fileobject_uses_str_type:
class CP_fileobject(socket._fileobject):
"""Faux file object attached to a socket object."""
@@ -738,7 +746,8 @@ if sys.version_info[:2] >= (2, 6):
try:
return self._sock.recv(size)
except socket.error, e:
- if e.args[0] not in socket_errors_nonblocking:
+ if (e.args[0] not in socket_errors_nonblocking
+ and e.args[0] not in socket_error_eintr):
raise
def read(self, size=-1):
@@ -838,8 +847,8 @@ if sys.version_info[:2] >= (2, 6):
nl = data.find('\n')
if nl >= 0:
nl += 1
- buf.write(buffer(data, 0, nl))
- self._rbuf.write(buffer(data, nl))
+ buf.write(data[:nl])
+ self._rbuf.write(data[nl:])
del data
break
buf.write(data)
@@ -865,9 +874,9 @@ if sys.version_info[:2] >= (2, 6):
if nl >= 0:
nl += 1
# save the excess data to _rbuf
- self._rbuf.write(buffer(data, nl))
+ self._rbuf.write(data[nl:])
if buf_len:
- buf.write(buffer(data, 0, nl))
+ buf.write(data[:nl])
break
else:
# Shortcut. Avoid data copy through buf when returning
@@ -879,8 +888,8 @@ if sys.version_info[:2] >= (2, 6):
# returning exactly all of our first recv().
return data
if n >= left:
- buf.write(buffer(data, 0, left))
- self._rbuf.write(buffer(data, left))
+ buf.write(data[:left])
+ self._rbuf.write(data[left:])
break
buf.write(data)
buf_len += n
@@ -915,7 +924,8 @@ else:
try:
return self._sock.recv(size)
except socket.error, e:
- if e.args[0] not in socket_errors_nonblocking:
+ if (e.args[0] not in socket_errors_nonblocking
+ and e.args[0] not in socket_error_eintr):
raise
def read(self, size=-1):
@@ -958,7 +968,7 @@ else:
buffers[-1] = data[:left]
break
buf_len += n
- return "".join(buffers)
+ return "".join(buffers)
def readline(self, size=-1):
data = self._rbuf
@@ -1029,6 +1039,7 @@ else:
break
buf_len += n
return "".join(buffers)
+
class SSL_fileobject(CP_fileobject):
"""SSL file object attached to a socket object."""
@@ -1180,28 +1191,39 @@ class HTTPConnection(object):
# Close the connection.
return
except NoSSLError:
- # Unwrap our wfile
- req.wfile = CP_fileobject(self.socket, "wb", -1)
if req and not req.sent_headers:
+ # Unwrap our wfile
+ req.wfile = CP_fileobject(self.socket._sock, "wb", -1)
req.simple_response("400 Bad Request",
"The client sent a plain HTTP request, but "
"this server only speaks HTTPS on this port.")
+ self.linger = True
except Exception, e:
if req and not req.sent_headers:
req.simple_response("500 Internal Server Error", format_exc())
+ linger = False
+
def close(self):
"""Close the socket underlying this connection."""
self.rfile.close()
- # Python's socket module does NOT call close on the kernel socket
- # when you call socket.close(). We do so manually here because we
- # want this server to send a FIN TCP segment immediately. Note this
- # must be called *before* calling socket.close(), because the latter
- # drops its reference to the kernel socket.
- self.socket._sock.close()
-
- self.socket.close()
+ if not self.linger:
+ # Python's socket module does NOT call close on the kernel socket
+ # when you call socket.close(). We do so manually here because we
+ # want this server to send a FIN TCP segment immediately. Note this
+ # must be called *before* calling socket.close(), because the latter
+ # drops its reference to the kernel socket.
+ self.socket._sock.close()
+ self.socket.close()
+ else:
+ # On the other hand, sometimes we want to hang around for a bit
+ # to make sure the client has a chance to read our entire
+ # response. Skipping the close() calls here delays the FIN
+ # packet until the socket object is garbage-collected later.
+ # Someday, perhaps, we'll do the full lingering_close that
+ # Apache does, but not today.
+ pass
def format_exc(limit=None):
@@ -1379,6 +1401,27 @@ class SSLConnection:
""" % (f, f)
+try:
+ import fcntl
+except ImportError:
+ try:
+ from ctypes import windll, WinError
+ except ImportError:
+ def prevent_socket_inheritance(sock):
+ """Dummy function, since neither fcntl nor ctypes are available."""
+ pass
+ else:
+ def prevent_socket_inheritance(sock):
+ """Mark the given socket fd as non-inheritable (Windows)."""
+ if not windll.kernel32.SetHandleInformation(sock.fileno(), 1, 0):
+ raise WinError()
+else:
+ def prevent_socket_inheritance(sock):
+ """Mark the given socket fd as non-inheritable (POSIX)."""
+ fd = sock.fileno()
+ old_flags = fcntl.fcntl(fd, fcntl.F_GETFD)
+ fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
+
class CherryPyWSGIServer(object):
"""An HTTP server for WSGI.
@@ -1425,7 +1468,7 @@ class CherryPyWSGIServer(object):
protocol = "HTTP/1.1"
_bind_addr = "127.0.0.1"
- version = "CherryPy/3.1.0"
+ version = "CherryPy/3.1.2"
ready = False
_interrupt = None
@@ -1572,6 +1615,7 @@ class CherryPyWSGIServer(object):
def bind(self, family, type, proto=0):
"""Create (or recreate) the actual socket object."""
self.socket = socket.socket(family, type, proto)
+ prevent_socket_inheritance(self.socket)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if self.nodelay:
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
@@ -1585,12 +1629,25 @@ class CherryPyWSGIServer(object):
ctx.use_certificate_file(self.ssl_certificate)
self.socket = SSLConnection(ctx, self.socket)
self.populate_ssl_environ()
+
+ # If listening on the IPV6 any address ('::' = IN6ADDR_ANY),
+ # activate dual-stack. See http://www.cherrypy.org/ticket/871.
+ if (not isinstance(self.bind_addr, basestring)
+ and self.bind_addr[0] == '::' and family == socket.AF_INET6):
+ try:
+ self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
+ except (AttributeError, socket.error):
+ # Apparently, the socket option is not available in
+ # this machine's TCP stack
+ pass
+
self.socket.bind(self.bind_addr)
def tick(self):
"""Accept a new connection and put it on the Queue."""
try:
s, addr = self.socket.accept()
+ prevent_socket_inheritance(s)
if not self.ready:
return
if hasattr(s, 'settimeout'):
@@ -1599,7 +1656,8 @@ class CherryPyWSGIServer(object):
environ = self.environ.copy()
# SERVER_SOFTWARE is common for IIS. It's also helpful for
# us to pass a default value for the "Server" response header.
- environ["SERVER_SOFTWARE"] = "%s WSGI Server" % self.version
+ if environ.get("SERVER_SOFTWARE") is None:
+ environ["SERVER_SOFTWARE"] = "%s WSGI Server" % self.version
# set a non-standard environ entry so the WSGI app can know what
# the *real* server protocol is (and what features to support).
# See http://www.faqs.org/rfcs/rfc2145.html.
@@ -1662,7 +1720,7 @@ class CherryPyWSGIServer(object):
try:
host, port = sock.getsockname()[:2]
except socket.error, x:
- if x.args[1] != "Bad file descriptor":
+ if x.args[0] not in socket_errors_to_ignore:
raise
else:
# Note that we're explicitly NOT using AI_PASSIVE,
hooks/post-receive
--
watchdog