summaryrefslogtreecommitdiff
path: root/bottle.py
diff options
context:
space:
mode:
authorMarcel Hellkamp <marc@gsites.de>2012-10-27 23:04:37 +0200
committerMarcel Hellkamp <marc@gsites.de>2012-10-28 00:11:15 +0200
commit9cd6fe3a27dca7dfe0dff0aed1499b7739f962ba (patch)
tree0dcd62c601f31afa3dcfece78222e1806268f9f6 /bottle.py
parent08117068cc22b540241c2ca330382d77e4910223 (diff)
parent26ac343788ac4dd2bbdcf8d6734411a495571b17 (diff)
downloadbottle-context.tar.gz
Merge branch 'master' into context-mergecontext
Conflicts: bottle.py test/test_environ.py test/test_sendfile.py
Diffstat (limited to 'bottle.py')
-rw-r--r--bottle.py208
1 files changed, 105 insertions, 103 deletions
diff --git a/bottle.py b/bottle.py
index c1a7aef..05e1f88 100644
--- a/bottle.py
+++ b/bottle.py
@@ -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