diff options
Diffstat (limited to 'bottle.py')
-rw-r--r-- | bottle.py | 208 |
1 files changed, 105 insertions, 103 deletions
@@ -9,14 +9,14 @@ Python Standard Library. Homepage and documentation: http://bottlepy.org/ -Copyright (c) 2011, Marcel Hellkamp. +Copyright (c) 2012, Marcel Hellkamp. License: MIT (see LICENSE for details) """ from __future__ import with_statement __author__ = 'Marcel Hellkamp' -__version__ = '0.11.dev' +__version__ = '0.12-dev' __license__ = 'MIT' # The gevent server adapter needs to patch some modules before they are imported @@ -122,10 +122,11 @@ if py31: class NCTextIOWrapper(TextIOWrapper): def close(self): pass # Keep wrapped buffer open. -# The truth-value of cgi.FieldStorage is misleading. +# File uploads (which are implemented as empty FiledStorage instances...) +# have a negative truth value. That makes no sense, here is a fix. class FieldStorage(cgi.FieldStorage): - def __nonzero__(self): - return bool(self.list or self.file) + def __nonzero__(self): return bool(self.list or self.file) + if py3k: __bool__ = __nonzero__ # A bug in functools causes it to break if the wrapper is an instance method def update_wrapper(wrapper, wrapped, *a, **ka): @@ -211,34 +212,6 @@ class BottleException(Exception): pass -#TODO: This should subclass BaseRequest -class HTTPResponse(BottleException): - """ Used to break execution and immediately finish the response """ - def __init__(self, output='', status=200, header=None): - super(BottleException, self).__init__("HTTP Response %d" % status) - self.status = int(status) - self.output = output - self.headers = HeaderDict(header) if header else None - - def apply(self, response): - if self.headers: - for key, value in self.headers.allitems(): - response.headers[key] = value - response.status = self.status - - -class HTTPError(HTTPResponse): - """ Used to generate an error page """ - 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 __repr__(self): - return tonat(template(ERROR_PAGE_TEMPLATE, e=self)) - - @@ -434,8 +407,7 @@ class Router(object): allowed = [verb for verb in targets if verb != 'ANY'] if 'GET' in allowed and 'HEAD' not in allowed: allowed.append('HEAD') - raise HTTPError(405, "Method not allowed.", - header=[('Allow',",".join(allowed))]) + raise HTTPError(405, "Method not allowed.", Allow=",".join(allowed)) class Route(object): @@ -546,10 +518,10 @@ class Bottle(object): #: If true, most exceptions are caught and returned as :exc:`HTTPError` self.catchall = catchall - #: A :cls:`ResourceManager` for application files + #: A :class:`ResourceManager` for application files self.resources = ResourceManager() - #: A :cls:`ConfigDict` for app specific configuration. + #: A :class:`ConfigDict` for app specific configuration. self.config = ConfigDict() self.config.autojson = autojson @@ -776,11 +748,15 @@ class Bottle(object): return self._handle(path) return self._handle({'PATH_INFO': path, 'REQUEST_METHOD': method.upper()}) + def default_error_handler(self, res): + return tob(template(ERROR_PAGE_TEMPLATE, e=res)) + def _handle(self, environ): try: environ['bottle.app'] = self route, args = self.router.match(environ) - environ['route.handle'] = environ['bottle.route'] = route + environ['route.handle'] = route + environ['bottle.route'] = route environ['route.url_args'] = args return route.call(**args) except HTTPResponse: @@ -805,7 +781,8 @@ class Bottle(object): # Empty output is done here if not out: - response['Content-Length'] = 0 + if 'Content-Length' not in response: + response['Content-Length'] = 0 return [] # Join lists of byte or unicode strings. Mixed lists are NOT supported if isinstance(out, (tuple, list))\ @@ -816,19 +793,18 @@ class Bottle(object): out = out.encode(response.charset) # Byte Strings are just returned if isinstance(out, bytes): - response['Content-Length'] = len(out) + if 'Content-Length' not in response: + response['Content-Length'] = len(out) return [out] # HTTPError or HTTPException (recursive, because they may wrap anything) # TODO: Handle these explicitly in handle() or make them iterable. if isinstance(out, HTTPError): out.apply(response) - out = self.error_handler.get(out.status, repr)(out) - if isinstance(out, HTTPResponse): - depr('Error handlers must not return :exc:`HTTPResponse`.') #0.9 + out = self.error_handler.get(out.status_code, self.default_error_handler)(out) return self._cast(out) if isinstance(out, HTTPResponse): out.apply(response) - return self._cast(out.output) + return self._cast(out.body) # File-like objects. if hasattr(out, 'read'): @@ -881,12 +857,10 @@ class Bottle(object): out = self._cast(self._handle(environ)) # rfc2616 section 4.3 if response._status_code in (100, 101, 204, 304)\ - or request.method == 'HEAD': + or environ['REQUEST_METHOD'] == 'HEAD': if hasattr(out, 'close'): out.close() out = [] - if isinstance(response._status_line, unicode): - response._status_line = str(response._status_line) - start_response(response._status_line, list(response.iter_headers())) + start_response(response._status_line, response.headerlist) return out except (KeyboardInterrupt, SystemExit, MemoryError): raise @@ -1319,8 +1293,6 @@ class BaseResponse(object): 'Content-Md5', 'Last-Modified'))} def __init__(self, body='', status=None, **headers): - self._status_line = None - self._status_code = None self._cookies = None self._headers = {'Content-Type': [self.default_content_type]} self.body = body @@ -1363,7 +1335,7 @@ class BaseResponse(object): raise ValueError('String status line without a reason phrase.') if not 100 <= code <= 999: raise ValueError('Status code out of range.') self._status_code = code - self._status_line = status or ('%d Unknown' % code) + self._status_line = str(status or ('%d Unknown' % code)) def _get_status(self): return self._status_line @@ -1380,7 +1352,7 @@ class BaseResponse(object): def headers(self): ''' An instance of :class:`HeaderDict`, a case-insensitive dict-like view on the response headers. ''' - self.__dict__['headers'] = hdict = HeaderDict() + hdict = HeaderDict() hdict.dict = self._headers return hdict @@ -1394,13 +1366,10 @@ class BaseResponse(object): header with that name, return a default value. ''' return self._headers.get(_hkey(name), [default])[-1] - def set_header(self, name, value, append=False): + def set_header(self, name, value): ''' Create a new response header, replacing any previously defined headers with the same name. ''' - if append: - self.add_header(name, value) - else: - self._headers[_hkey(name)] = [str(value)] + self._headers[_hkey(name)] = [str(value)] def add_header(self, name, value): ''' Add an additional response header, not removing duplicates. ''' @@ -1409,16 +1378,7 @@ class BaseResponse(object): def iter_headers(self): ''' Yield (header, value) tuples, skipping headers that are not allowed with the current response status code. ''' - headers = self._headers.items() - bad_headers = self.bad_headers.get(self._status_code) - if bad_headers: - headers = [h for h in headers if h[0] not in bad_headers] - for name, values in headers: - for value in values: - yield name, value - if self._cookies: - for c in self._cookies.values(): - yield 'Set-Cookie', c.OutputString() + return self.headerlist def wsgiheader(self): depr('The wsgiheader method is deprecated. See headerlist.') #0.10 @@ -1427,7 +1387,16 @@ class BaseResponse(object): @property def headerlist(self): ''' WSGI conform list of (header, value) tuples. ''' - return list(self.iter_headers()) + out = [] + headers = self._headers.items() + if self._status_code in self.bad_headers: + bad_headers = self.bad_headers[self._status_code] + headers = [h for h in headers if h[0] not in bad_headers] + out += [(name, val) for name, vals in headers for val in vals] + if self._cookies: + for c in self._cookies.values(): + out.append(('Set-Cookie', c.OutputString())) + return out content_type = HeaderProperty('Content-Type') content_length = HeaderProperty('Content-Length', reader=int) @@ -1600,6 +1569,34 @@ class LocalResponse(BaseResponse): Response = BaseResponse Request = BaseRequest +class HTTPResponse(Response, BottleException): + def __init__(self, body='', status=None, header=None, **headers): + if header or 'output' in headers: + depr('Call signature changed (for the better)') + if header: headers.update(header) + if 'output' in headers: body = headers.pop('output') + super(HTTPResponse, self).__init__(body, status, **headers) + + def apply(self, response): + response._status_code = self._status_code + response._status_line = self._status_line + response._headers = self._headers + response._cookies = self._cookies + response.body = self.body + + def _output(self, value=None): + depr('Use HTTPResponse.body instead of HTTPResponse.output') + if value is None: return self.body + self.body = value + + output = property(_output, _output, doc='Alias for .body') + +class HTTPError(HTTPResponse): + default_status = 500 + def __init__(self, status=None, body=None, exception=None, traceback=None, header=None, **headers): + self.exception = exception + self.traceback = traceback + super(HTTPError, self).__init__(body, status, header, **headers) @@ -1618,7 +1615,7 @@ class JSONPlugin(object): def __init__(self, json_dumps=json_dumps): self.json_dumps = json_dumps - def apply(self, callback, context): + def apply(self, callback, route): dumps = self.json_dumps if not dumps: return callback def wrapper(*a, **ka): @@ -1668,7 +1665,7 @@ class HooksPlugin(object): if ka.pop('reversed', False): hooks = hooks[::-1] return [hook(*a, **ka) for hook in hooks] - def apply(self, callback, context): + def apply(self, callback, route): if self._empty(): return callback def wrapper(*a, **ka): self.trigger('before_request') @@ -1890,7 +1887,7 @@ class WSGIHeaderDict(DictMixin): Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one that uses non-native strings.) ''' - #: List of keys that do not have a 'HTTP_' prefix. + #: List of keys that do not have a ``HTTP_`` prefix. cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') def __init__(self, environ): @@ -1993,7 +1990,7 @@ class WSGIFileWrapper(object): class ResourceManager(object): ''' This class manages a list of search paths and helps to find and open - aplication-bound resources (files). + application-bound resources (files). :param base: default value for :meth:`add_path` calls. :param opener: callable used to open resources. @@ -2022,7 +2019,6 @@ class ResourceManager(object): Defaults to :attr:`base` which defaults to ``os.getcwd()``. :param index: Position within the list of search paths. Defaults to last index (appends to the list). - :param create: Create non-existent search paths. Off by default. The `base` parameter makes it easy to reference files installed along with a python module or package:: @@ -2098,7 +2094,7 @@ def redirect(url, code=None): if code is None: code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 location = urljoin(request.url, url) - raise HTTPResponse("", status=code, header=dict(Location=location)) + raise HTTPResponse("", status=code, Location=location) def _file_iter_range(fp, offset, bytes, maxread=1024*1024): @@ -2119,8 +2115,8 @@ def static_file(filename, root, mimetype='auto', download=False): """ root = os.path.abspath(root) + os.sep filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) - header = dict() time_format = "%a, %d %b %Y %H:%M:%S GMT" + headers = dict() if not filename.startswith(root): return HTTPError(403, "Access denied.") @@ -2131,36 +2127,35 @@ def static_file(filename, root, mimetype='auto', download=False): if mimetype == 'auto': mimetype, encoding = mimetypes.guess_type(filename) - if mimetype: header['Content-Type'] = mimetype - if encoding: header['Content-Encoding'] = encoding + if mimetype: headers['Content-Type'] = mimetype + if encoding: headers['Content-Encoding'] = encoding elif mimetype: - header['Content-Type'] = mimetype + headers['Content-Type'] = mimetype if download: download = os.path.basename(filename if download == True else download) - header['Content-Disposition'] = 'attachment; filename="%s"' % download + headers['Content-Disposition'] = 'attachment; filename="%s"' % download stats = os.stat(filename) - header['Content-Length'] = clen = stats.st_size + headers['Content-Length'] = clen = stats.st_size lm = time.strftime(time_format, time.gmtime(stats.st_mtime)) - header['Last-Modified'] = lm + headers['Last-Modified'] = lm context = get_context() - if context: ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') if ims: ims = parse_date(ims.split(";")[0].strip()) if ims is not None and ims >= int(stats.st_mtime): - header['Date'] = time.strftime(time_format, time.gmtime()) - return HTTPResponse(status=304, header=header) + headers['Date'] = time.strftime(time_format, time.gmtime()) + return HTTPResponse(status=304, **headers) if context and request.method == 'HEAD': body = '' else: body = open(filename, 'rb') - header["Accept-Ranges"] = "bytes" + headers["Accept-Ranges"] = "bytes" if context: ranges = request.environ.get('HTTP_RANGE') if ranges: @@ -2168,11 +2163,11 @@ def static_file(filename, root, mimetype='auto', download=False): if not ranges: return HTTPError(416, "Requested Range Not Satisfiable") offset, end = ranges[0] - header["Content-Range"] = "bytes %d-%d/%d" % (offset, end-1, clen) - header["Content-Length"] = str(end-offset) + headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end-1, clen) + headers["Content-Length"] = str(end-offset) if body: body = _file_iter_range(body, offset, end-offset) - return HTTPResponse(body, header=header, status=206) - return HTTPResponse(body, header=header) + return HTTPResponse(body, 206, **headers) + return HTTPResponse(body, **headers) @@ -2545,15 +2540,15 @@ class DieselServer(ServerAdapter): class GeventServer(ServerAdapter): """ Untested. Options: - * `monkey` (default: True) fixes the stdlib to use greenthreads. * `fast` (default: False) uses libevent's http server, but has some issues: No streaming, no pipelining, no SSL. """ def run(self, handler): - from gevent import wsgi as wsgi_fast, pywsgi, monkey, local - if self.options.get('monkey', True): - if not threading.local is local.local: monkey.patch_all() - wsgi = wsgi_fast if self.options.get('fast') else pywsgi + from gevent import wsgi, pywsgi, local + if not isinstance(_lctx, local.local): + msg = "Bottle requires gevent.monkey.patch_all() (before import)" + raise RuntimeError(msg) + if not self.options.get('fast'): wsgi = pywsgi log = None if self.quiet else 'default' wsgi.WSGIServer((self.host, self.port), handler, log=log).serve_forever() @@ -2860,11 +2855,19 @@ class BaseTemplate(object): def search(cls, name, lookup=[]): """ Search name in all directories specified in lookup. First without, then with common extensions. Return first hit. """ - if os.path.isfile(name): return name + if not lookup: + depr('The template lookup path list should not be empty.') + lookup = ['.'] + + if os.path.isabs(name) and os.path.isfile(name): + depr('Absolute template path names are deprecated.') + return os.path.abspath(name) + for spath in lookup: - fname = os.path.join(spath, name) - if os.path.isfile(fname): - return fname + spath = os.path.abspath(spath) + os.sep + fname = os.path.abspath(os.path.join(spath, name)) + if not fname.startswith(spath): continue + if os.path.isfile(fname): return fname for ext in cls.extensions: if os.path.isfile('%s.%s' % (fname, ext)): return '%s.%s' % (fname, ext) @@ -3225,11 +3228,10 @@ _HTTP_STATUS_LINES = dict((k, '%d %s'%(k,v)) for (k,v) in HTTP_CODES.items()) ERROR_PAGE_TEMPLATE = """ %%try: %%from %s import DEBUG, HTTP_CODES, request, touni - %%status_name = HTTP_CODES.get(e.status, 'Unknown').title() <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html> <head> - <title>Error {{e.status}}: {{status_name}}</title> + <title>Error: {{e.status}}</title> <style type="text/css"> html {background-color: #eee; font-family: sans;} body {background-color: #fff; border: 1px solid #ddd; @@ -3238,10 +3240,10 @@ ERROR_PAGE_TEMPLATE = """ </style> </head> <body> - <h1>Error {{e.status}}: {{status_name}}</h1> + <h1>Error: {{e.status}}</h1> <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt> caused an error:</p> - <pre>{{e.output}}</pre> + <pre>{{e.body}}</pre> %%if DEBUG and e.exception: <h2>Exception:</h2> <pre>{{repr(e.exception)}}</pre> @@ -3277,7 +3279,7 @@ app.push() #: A virtual package that redirects import statements. #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. -ext = _ImportRedirect(__name__+'.ext', 'bottle_%s').module +ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else __name__+".ext", 'bottle_%s').module if __name__ == '__main__': opt, args, parser = _cmd_options, _cmd_args, _cmd_parser |