diff options
Diffstat (limited to 'bottle.py')
-rwxr-xr-x | bottle.py | 220 |
1 files changed, 120 insertions, 100 deletions
@@ -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) |