summaryrefslogtreecommitdiff
path: root/bottle.py
diff options
context:
space:
mode:
Diffstat (limited to 'bottle.py')
-rwxr-xr-xbottle.py220
1 files changed, 120 insertions, 100 deletions
diff --git a/bottle.py b/bottle.py
index 1b0fb34..ea698df 100755
--- a/bottle.py
+++ b/bottle.py
@@ -58,35 +58,33 @@ This is an example::
run(host='localhost', port=8080)
"""
+
from __future__ import with_statement
__author__ = 'Marcel Hellkamp'
__version__ = '0.7.0a'
__license__ = 'MIT'
-import types
-import sys
+import base64
import cgi
+import email.utils
+import functools
+import hmac
+import inspect
+import itertools
import mimetypes
import os
-import os.path
-from traceback import format_exc
import re
-import random
+import subprocess
+import sys
+import thread
import threading
import time
-import warnings
-import email.utils
+
from Cookie import SimpleCookie
-import subprocess
-import thread
from tempfile import TemporaryFile
-import hmac
-import base64
+from traceback import format_exc
from urllib import quote as urlquote
from urlparse import urlunsplit, urljoin
-import functools
-import itertools
-import inspect
try:
from collections import MutableMapping as DictMixin
@@ -115,10 +113,12 @@ if sys.version_info >= (3,0,0): # pragma: no cover
# See Request.POST
from io import BytesIO
from io import TextIOWrapper
+ StringType = bytes
def touni(x, enc='utf8'): # Convert anything to unicode (py3)
return str(x, encoding=enc) if isinstance(x, bytes) else str(x)
else:
from StringIO import StringIO as BytesIO
+ from types import StringType
TextIOWrapper = None
def touni(x, enc='utf8'): # Convert anything to unicode (py2)
return x if isinstance(x, unicode) else unicode(str(x), encoding=enc)
@@ -155,17 +155,13 @@ class HTTPResponse(BottleException):
class HTTPError(HTTPResponse):
""" Used to generate an error page """
- def __init__(self, code=500, message='Unknown Error', exception=None, header=None):
- super(HTTPError, self).__init__(message, code, header)
+ def __init__(self, code=500, output='Unknown Error', exception=None, traceback=None, header=None):
+ super(HTTPError, self).__init__(output, code, header)
self.exception = exception
+ self.traceback = traceback
- def __str__(self):
- return ERROR_PAGE_TEMPLATE % {
- 'status' : self.status,
- 'url' : str(request.path),
- 'error_name' : HTTP_CODES.get(self.status, 'Unknown').title(),
- 'error_message' : str(self.output)
- }
+ def __repr__(self):
+ return ''.join(ERROR_PAGE_TEMPLATE.render(e=self))
@@ -347,10 +343,21 @@ class Bottle(object):
self.routes = Router()
self.default_route = None
self.error_handler = {}
- self.jsondump = json_dumps if autojson and json_dumps else False
self.catchall = catchall
self.config = dict()
self.serve = True
+ self.castfilter = []
+ if autojson and json_dumps:
+ self.add_filter(dict, dict2json)
+
+ def add_filter(self, ftype, func):
+ ''' Register a new output filter. Whenever bottle hits a handler output
+ matching `ftype`, `func` is applyed to it. '''
+ if not isinstance(ftype, type):
+ raise TypeError("Expected type object, got %s" % type(ftype))
+ self.castfilter = [(t, f) for (t, f) in self.castfilter if t != ftype]
+ self.castfilter.append((ftype, func))
+ self.castfilter.sort()
def match_url(self, path, method='GET'):
""" Find a callback bound to a path and a specific HTTP method.
@@ -406,10 +413,9 @@ class Bottle(object):
return wrapper
def handle(self, url, method, catchall=True):
- """ Handle a single request. Return handler output, HTTPResponse or
- HTTPError. If catchall is true, all exceptions thrown within a
- handler function are catched and returned as HTTPError(500).
- """
+ """ Execute the handler bound to the specified url and method and return
+ its output. If catchall is true, exceptions are catched and returned as
+ HTTPError(500) objects. """
if not self.serve:
return HTTPError(503, "Server stopped")
@@ -421,18 +427,13 @@ class Bottle(object):
return handler(**args)
except HTTPResponse, e:
return e
- except (KeyboardInterrupt, SystemExit, MemoryError):
- raise
except Exception, e:
- if not self.catchall:
+ if isinstance(e, (KeyboardInterrupt, SystemExit, MemoryError))\
+ or not self.catchall:
raise
- err = "Unhandled Exception: %s\n" % (repr(e))
- if DEBUG:
- err += '\n\nTraceback:\n' + format_exc(10)
- request.environ['wsgi.errors'].write(err)
- return HTTPError(500, err, e)
+ return HTTPError(500, 'Unhandled exception', e, format_exc(10))
- def _cast(self, out):
+ def _cast(self, out, peek=None):
""" Try to convert the parameter into something WSGI compatible and set
correct HTTP headers when possible.
Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like,
@@ -442,52 +443,59 @@ class Bottle(object):
if not out:
response.header['Content-Length'] = 0
return []
- # Join lists of byte or unicode strings (TODO: benchmark this against map)
- if isinstance(out, list) and isinstance(out[0], (types.StringType, unicode)):
+ # Join lists of byte or unicode strings. Mixed lists are NOT supported
+ if isinstance(out, list) and isinstance(out[0], (StringType, unicode)):
out = out[0][0:0].join(out) # b'abc'[0:0] -> b''
- # Convert dictionaries to JSON
- if isinstance(out, dict) and self.jsondump:
- response.content_type = 'application/json'
- out = self.jsondump(out)
# Encode unicode strings
if isinstance(out, unicode):
out = out.encode(response.charset)
- # Byte Strings
- if isinstance(out, types.StringType):
+ # Byte Strings are just returned
+ if isinstance(out, StringType):
response.header['Content-Length'] = str(len(out))
return [out]
-
# HTTPError or HTTPException (recursive, because they may wrap anything)
if isinstance(out, HTTPError):
out.apply(response)
- return self._cast(self.error_handler.get(out.status, str)(out))
+ return self._cast(self.error_handler.get(out.status, repr)(out))
if isinstance(out, HTTPResponse):
out.apply(response)
return self._cast(out.output)
- # Handle Files and other more complex iterables here...
+ # Filtered types (recursive, because they may return anything)
+ for testtype, filterfunc in self.castfilter:
+ if isinstance(out, testtype):
+ return self._cast(filterfunc(out))
+
+ # Cast Files into iterables
if hasattr(out, 'read') and 'wsgi.file_wrapper' in request.environ:
out = request.environ.get('wsgi.file_wrapper',
lambda x, y: iter(lambda: x.read(y), ''))(out, 1024*64)
- else:
- out = iter(out)
- # We peek into iterables to detect their inner type and to support
- # generators as callbacks. They should not try to set any headers after
- # their first yield statement.
+
+ # Handle Iterables. We peek into them to detect their inner type.
try:
- while 1:
+ out = iter(out)
+ first = out.next()
+ while not first:
first = out.next()
- if first: break
except StopIteration:
- response.header['Content-Length'] = 0
- return []
-
- if isinstance(first, types.StringType):
+ return self._cast('')
+ except HTTPResponse, e:
+ first = e
+ except Exception, e:
+ first = HTTPError(500, 'Unhandled exception', e, format_exc(10))
+ if isinstance(e, (KeyboardInterrupt, SystemExit, MemoryError))\
+ or not self.catchall:
+ raise
+ # These are the inner types allowed in iterator or generator objects.
+ if isinstance(first, HTTPResponse):
+ return self._cast(first)
+ if isinstance(first, StringType):
return itertools.chain([first], out)
- elif isinstance(first, unicode):
+ if isinstance(first, unicode):
return itertools.imap(lambda x: x.encode(response.charset),
itertools.chain([first], out))
- raise TypeError('Unsupported response type: %s' % type(first))
+ return self._cast(HTTPError(500, 'Unsupported response type: %s'\
+ % type(first)))
def __call__(self, environ, start_response):
""" The bottle WSGI-interface. """
@@ -498,24 +506,21 @@ class Bottle(object):
out = self._cast(out)
if response.status in (100, 101, 204, 304) or request.method == 'HEAD':
out = [] # rfc2616 section 4.3
- if isinstance(out, list) and len(out) == 1:
- response.header['Content-Length'] = str(len(out[0]))
status = '%d %s' % (response.status, HTTP_CODES[response.status])
start_response(status, response.wsgiheader())
- # TODO: Yield here to catch errors in generator callbacks.
return out
except (KeyboardInterrupt, SystemExit, MemoryError):
raise
except Exception, e:
if not self.catchall:
raise
- err = '<h1>Critial error while processing request: %s</h1>' \
+ err = '<h1>Critical error while processing request: %s</h1>' \
% environ.get('PATH_INFO', '/')
if DEBUG:
err += '<h2>Error:</h2>\n<pre>%s</pre>\n' % repr(e)
err += '<h2>Traceback:</h2>\n<pre>%s</pre>\n' % format_exc(10)
environ['wsgi.errors'].write(err) #TODO: wsgi.error should not get html
- start_response('500 INTERNAL SERVER ERROR', [])
+ start_response('500 INTERNAL SERVER ERROR', [('Content-Type', 'text/html')])
return [tob(err)]
@@ -782,14 +787,6 @@ class Response(threading.local):
# Data Structures
-class BaseController(object):
- _singleton = None
- def __new__(cls, *a, **k):
- if not cls._singleton:
- cls._singleton = object.__new__(cls, *a, **k)
- return cls._singleton
-
-
class MultiDict(DictMixin):
""" A dict that remembers old values for each key """
# collections.MutableMapping would be better for Python >= 2.6
@@ -850,8 +847,11 @@ class AppStack(list):
# Module level functions
-# BC: 0.6.4 and needed for run()
-app = default_app = AppStack([Bottle()])
+# Output filter
+
+def dict2json(d):
+ response.content_type = 'application/json'
+ return json_dumps(d)
def abort(code=500, text='Unknown Error: Appliction stopped.'):
@@ -921,13 +921,20 @@ def url(routename, **kargs):
# Utilities
+def debug(mode=True):
+ """ Change the debug level.
+ There is only one debug level supported at the moment."""
+ global DEBUG
+ DEBUG = bool(mode)
+
+
def url(routename, **kargs):
""" Return a named route filled with arguments """
return app().get_url(routename, **kargs)
def parse_date(ims):
- """ Parses rfc1123, rfc850 and asctime timestamps and returns UTC epoch. """
+ """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """
try:
ts = email.utils.parsedate_tz(ims)
return time.mktime(ts[:8] + (0,)) - (ts[9] or 0) - time.timezone
@@ -936,12 +943,13 @@ def parse_date(ims):
def parse_auth(header):
+ """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None"""
try:
method, data = header.split(None, 1)
if method.lower() == 'basic':
name, pwd = base64.b64decode(data).split(':', 1)
return name, pwd
- except (KeyError, ValueError, TypeError), a:
+ except (KeyError, ValueError, TypeError):
return None
@@ -1012,7 +1020,7 @@ def validate(**vkargs):
abort(403, 'Missing parameter: %s' % key)
try:
kargs[key] = value(kargs[key])
- except ValueError, e:
+ except ValueError:
abort(403, 'Wrong parameter format for: %s' % key)
return func(**kargs)
return wrapper
@@ -1145,13 +1153,15 @@ class AppEngineServer(ServerAdapter):
class TwistedServer(ServerAdapter):
""" Untested. """
def run(self, handler):
- import twisted.web.wsgi
- import twisted.internet
- resource = twisted.web.wsgi.WSGIResource(twisted.internet.reactor,
- twisted.internet.reactor.getThreadPool(), handler)
- site = server.Site(resource)
- twisted.internet.reactor.listenTCP(self.port, self.host)
- twisted.internet.reactor.run()
+ from twisted.web import server, wsgi
+ from twisted.python.threadpool import ThreadPool
+ from twisted.internet import reactor
+ thread_pool = ThreadPool()
+ thread_pool.start()
+ reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop)
+ server.Site(wsgi.WSGIResource(reactor, thread_pool, handler))
+ reactor.listenTCP(self.port, self.host)
+ reactor.run()
class DieselServer(ServerAdapter):
@@ -1592,20 +1602,31 @@ HTTP_CODES = {
""" A dict of known HTTP error and status codes """
-ERROR_PAGE_TEMPLATE = """<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
+
+ERROR_PAGE_TEMPLATE = SimpleTemplate("""
+%import cgi
+%from bottle import DEBUG, HTTP_CODES, request
+%status_name = HTTP_CODES.get(e.status, 'Unknown').title()
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
<head>
- <title>Error %(status)d: %(error_name)s</title>
+ <title>Error {{e.status}}: {{status_name}}</title>
</head>
<body>
- <h1>Error %(status)d: %(error_name)s</h1>
- <p>Sorry, the requested URL <tt>%(url)s</tt> caused an error:</p>
- <pre>
- %(error_message)s
- </pre>
+ <h1>Error {{e.status}}: {{status_name}}</h1>
+ <p>Sorry, the requested URL <tt>{{cgi.escape(request.url)}}</tt> caused an error:</p>
+ <pre>{{cgi.escape(str(e.output))}}</pre>
+ %if DEBUG and e.exception:
+ <h2>Exception:</h2>
+ <pre>{{cgi.escape(repr(e.exception))}}</pre>
+ %end
+ %if DEBUG and e.traceback:
+ <h2>Traceback:</h2>
+ <pre>{{cgi.escape(e.traceback)}}</pre>
+ %end
</body>
</html>
-"""
+""") #TODO: use {{!bla}} instead of cgi.escape as soon as strlunicode is merged
""" The HTML template used for error messages """
TRACEBACK_TEMPLATE = '<h2>Error:</h2>\n<pre>%s</pre>\n' \
@@ -1621,11 +1642,10 @@ response = Response()
of :class:`Response` to generate the WSGI response. """
local = threading.local()
+""" Thread-local namespace. Not used by Bottle, but could get handy """
-#TODO: Global and app local configuration (debug, defaults, ...) is a mess
+# Initialize app stack (create first empty Bottle app)
+# BC: 0.6.4 and needed for run()
+app = default_app = AppStack()
+app.push()
-def debug(mode=True):
- """ Change the debug level.
- There is only one debug level supported at the moment."""
- global DEBUG
- DEBUG = bool(mode)